diff options
22 files changed, 148 insertions, 124 deletions
diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php index 0290e36c..fd8ed8bb 100644 --- a/app/Auth/RememberMe.php +++ b/app/Auth/RememberMe.php @@ -3,9 +3,9 @@ namespace Kanboard\Auth; use Kanboard\Core\Base; -use Kanboard\Core\Request; +use Kanboard\Core\Http\Request; use Kanboard\Event\AuthEvent; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; /** * RememberMe model @@ -165,8 +165,8 @@ class RememberMe extends Base */ public function create($user_id, $ip, $user_agent) { - $token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken()); - $sequence = Security::generateToken(); + $token = hash('sha256', $user_id.$user_agent.$ip.Token::getToken()); + $sequence = Token::getToken(); $expiration = time() + self::EXPIRATION; $this->cleanup($user_id); @@ -216,7 +216,7 @@ class RememberMe extends Base */ public function update($token) { - $new_sequence = Security::generateToken(); + $new_sequence = Token::getToken(); $this->db ->table(self::TABLE) diff --git a/app/Controller/Base.php b/app/Controller/Base.php index a955b12c..829e0ad2 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -3,9 +3,6 @@ namespace Kanboard\Controller; use Pimple\Container; -use Kanboard\Core\Security; -use Kanboard\Core\Request; -use Kanboard\Core\Response; use Symfony\Component\EventDispatcher\Event; /** @@ -17,22 +14,6 @@ use Symfony\Component\EventDispatcher\Event; abstract class Base extends \Kanboard\Core\Base { /** - * Request instance - * - * @accesss protected - * @var \Kanboard\Core\Request - */ - protected $request; - - /** - * Response instance - * - * @accesss protected - * @var \Kanboard\Core\Response - */ - protected $response; - - /** * Constructor * * @access public @@ -41,11 +22,9 @@ abstract class Base extends \Kanboard\Core\Base public function __construct(Container $container) { $this->container = $container; - $this->request = new Request; - $this->response = new Response; if (DEBUG) { - $this->container['logger']->debug('START_REQUEST='.$_SERVER['REQUEST_URI']); + $this->logger->debug('START_REQUEST='.$_SERVER['REQUEST_URI']); } } @@ -57,14 +36,14 @@ abstract class Base extends \Kanboard\Core\Base public function __destruct() { if (DEBUG) { - foreach ($this->container['db']->getLogMessages() as $message) { - $this->container['logger']->debug($message); + foreach ($this->db->getLogMessages() as $message) { + $this->logger->debug($message); } - $this->container['logger']->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries)); - $this->container['logger']->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT'])); - $this->container['logger']->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage())); - $this->container['logger']->debug('END_REQUEST='.$_SERVER['REQUEST_URI']); + $this->logger->debug('SQL_QUERIES={nb}', array('nb' => $this->container['db']->nbQueries)); + $this->logger->debug('RENDERING={time}', array('time' => microtime(true) - @$_SERVER['REQUEST_TIME_FLOAT'])); + $this->logger->debug('MEMORY='.$this->helper->text->bytes(memory_get_usage())); + $this->logger->debug('END_REQUEST='.$_SERVER['REQUEST_URI']); } } @@ -201,7 +180,7 @@ abstract class Base extends \Kanboard\Core\Base */ protected function checkCSRFParam() { - if (! Security::validateCSRFToken($this->request->getStringParam('csrf_token'))) { + if (! $this->token->validateCSRFToken($this->request->getStringParam('csrf_token'))) { $this->forbidden(); } } diff --git a/app/Core/Base.php b/app/Core/Base.php index d402fb37..11f4e31b 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -12,18 +12,20 @@ use Pimple\Container; * * @property \Kanboard\Core\Helper $helper * @property \Kanboard\Core\Mail\Client $emailClient - * @property \Kanboard\Core\HttpClient $httpClient * @property \Kanboard\Core\Paginator $paginator - * @property \Kanboard\Core\Request $request + * @property \Kanboard\Core\Http\Client $httpClient + * @property \Kanboard\Core\Http\Request $request + * @property \Kanboard\Core\Http\Router $router + * @property \Kanboard\Core\Http\Response $response * @property \Kanboard\Core\Session $session * @property \Kanboard\Core\Template $template * @property \Kanboard\Core\OAuth2 $oauth - * @property \Kanboard\Core\Router $router * @property \Kanboard\Core\Lexer $lexer * @property \Kanboard\Core\ObjectStorage\ObjectStorageInterface $objectStorage * @property \Kanboard\Core\Cache\Cache $memoryCache * @property \Kanboard\Core\Plugin\Hook $hook * @property \Kanboard\Core\Plugin\Loader $pluginLoader + * @property \Kanboard\Core\Security\Token $token * @property \Kanboard\Integration\BitbucketWebhook $bitbucketWebhook * @property \Kanboard\Integration\GithubWebhook $githubWebhook * @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook diff --git a/app/Core/HttpClient.php b/app/Core/Http/Client.php index 7f4ea47a..c6bf36a6 100644 --- a/app/Core/HttpClient.php +++ b/app/Core/Http/Client.php @@ -1,14 +1,16 @@ <?php -namespace Kanboard\Core; +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; /** * HTTP client * - * @package core + * @package http * @author Frederic Guillot */ -class HttpClient extends Base +class Client extends Base { /** * HTTP connection timeout in seconds @@ -99,6 +101,36 @@ class HttpClient extends Base return ''; } + $stream = @fopen(trim($url), 'r', false, stream_context_create($this->getContext($method, $content, $headers))); + $response = ''; + + if (is_resource($stream)) { + $response = stream_get_contents($stream); + } else { + $this->logger->error('HttpClient: request failed'); + } + + if (DEBUG) { + $this->logger->debug('HttpClient: url='.$url); + $this->logger->debug('HttpClient: payload='.$content); + $this->logger->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true)); + $this->logger->debug('HttpClient: response='.$response); + } + + return $response; + } + + /** + * Get stream context + * + * @access private + * @param string $method + * @param string $content + * @param string[] $headers + * @return array + */ + private function getContext($method, $content, array $headers) + { $default_headers = array( 'User-Agent: '.self::HTTP_USER_AGENT, 'Connection: close', @@ -126,22 +158,6 @@ class HttpClient extends Base $context['http']['request_fulluri'] = true; } - $stream = @fopen(trim($url), 'r', false, stream_context_create($context)); - $response = ''; - - if (is_resource($stream)) { - $response = stream_get_contents($stream); - } else { - $this->container['logger']->error('HttpClient: request failed'); - } - - if (DEBUG) { - $this->container['logger']->debug('HttpClient: url='.$url); - $this->container['logger']->debug('HttpClient: payload='.$content); - $this->container['logger']->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true)); - $this->container['logger']->debug('HttpClient: response='.$response); - } - - return $response; + return $context; } } diff --git a/app/Core/Request.php b/app/Core/Http/Request.php index 5eda2d02..9f89a6e2 100644 --- a/app/Core/Request.php +++ b/app/Core/Http/Request.php @@ -1,14 +1,16 @@ <?php -namespace Kanboard\Core; +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; /** * Request class * - * @package core + * @package http * @author Frederic Guillot */ -class Request +class Request extends Base { /** * Get URL string parameter @@ -57,7 +59,8 @@ class Request */ public function getValues() { - if (! empty($_POST) && Security::validateCSRFFormToken($_POST)) { + if (! empty($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) { + unset($_POST['csrf_token']); return $_POST; } diff --git a/app/Core/Response.php b/app/Core/Http/Response.php index 528a6302..a793e58b 100644 --- a/app/Core/Response.php +++ b/app/Core/Http/Response.php @@ -1,14 +1,16 @@ <?php -namespace Kanboard\Core; +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; /** * Response class * - * @package core + * @package http * @author Frederic Guillot */ -class Response +class Response extends Base { /** * Send no cache headers diff --git a/app/Core/Router.php b/app/Core/Http/Router.php index 843f5139..0080b23a 100644 --- a/app/Core/Router.php +++ b/app/Core/Http/Router.php @@ -1,13 +1,14 @@ <?php -namespace Kanboard\Core; +namespace Kanboard\Core\Http; use RuntimeException; +use Kanboard\Core\Base; /** * Router class * - * @package core + * @package http * @author Frederic Guillot */ class Router extends Base diff --git a/app/Core/Security.php b/app/Core/Security/Token.php index 54207ee1..7aca08af 100644 --- a/app/Core/Security.php +++ b/app/Core/Security/Token.php @@ -1,14 +1,16 @@ <?php -namespace Kanboard\Core; +namespace Kanboard\Core\Security; + +use Kanboard\Core\Base; /** - * Security class + * Token Handler * - * @package core + * @package security * @author Frederic Guillot */ -class Security +class Token extends Base { /** * Generate a random token with different methods: openssl or /dev/urandom or fallback to uniqid() @@ -17,7 +19,7 @@ class Security * @access public * @return string Random token */ - public static function generateToken() + public static function getToken() { if (function_exists('openssl_random_pseudo_bytes')) { return bin2hex(\openssl_random_pseudo_bytes(30)); @@ -31,18 +33,16 @@ class Security /** * Generate and store a CSRF token in the current session * - * @static * @access public * @return string Random token */ - public static function getCSRFToken() + public function getCSRFToken() { - $nonce = self::generateToken(); - - if (empty($_SESSION['csrf_tokens'])) { + if (! isset($_SESSION['csrf_tokens'])) { $_SESSION['csrf_tokens'] = array(); } + $nonce = self::getToken(); $_SESSION['csrf_tokens'][$nonce] = true; return $nonce; @@ -51,12 +51,11 @@ class Security /** * Check if the token exists for the current session (a token can be used only one time) * - * @static * @access public * @param string $token CSRF token * @return bool */ - public static function validateCSRFToken($token) + public function validateCSRFToken($token) { if (isset($_SESSION['csrf_tokens'][$token])) { unset($_SESSION['csrf_tokens'][$token]); @@ -65,22 +64,4 @@ class Security return false; } - - /** - * Check if the token used in a form is correct and then remove the value - * - * @static - * @access public - * @param array $values Form values - * @return bool - */ - public static function validateCSRFFormToken(array &$values) - { - if (! empty($values['csrf_token']) && self::validateCSRFToken($values['csrf_token'])) { - unset($values['csrf_token']); - return true; - } - - return false; - } } diff --git a/app/Core/Session.php b/app/Core/Session.php index a93131c7..dd1e760e 100644 --- a/app/Core/Session.php +++ b/app/Core/Session.php @@ -3,6 +3,7 @@ namespace Kanboard\Core; use ArrayAccess; +use Kanboard\Core\Http\Request; /** * Session class diff --git a/app/Helper/Form.php b/app/Helper/Form.php index 5f19f2a8..bfd75ee3 100644 --- a/app/Helper/Form.php +++ b/app/Helper/Form.php @@ -2,7 +2,7 @@ namespace Kanboard\Helper; -use Kanboard\Core\Security; +use Kanboard\Core\Base; /** * Form helpers @@ -10,7 +10,7 @@ use Kanboard\Core\Security; * @package helper * @author Frederic Guillot */ -class Form extends \Kanboard\Core\Base +class Form extends Base { /** * Hidden CSRF token field @@ -20,7 +20,7 @@ class Form extends \Kanboard\Core\Base */ public function csrf() { - return '<input type="hidden" name="csrf_token" value="'.Security::getCSRFToken().'"/>'; + return '<input type="hidden" name="csrf_token" value="'.$this->token->getCSRFToken().'"/>'; } /** diff --git a/app/Helper/Url.php b/app/Helper/Url.php index f120252d..edb26841 100644 --- a/app/Helper/Url.php +++ b/app/Helper/Url.php @@ -2,8 +2,8 @@ namespace Kanboard\Helper; -use Kanboard\Core\Request; -use Kanboard\Core\Security; +use Kanboard\Core\Http\Request; +use Kanboard\Core\Base; /** * Url helpers @@ -11,7 +11,7 @@ use Kanboard\Core\Security; * @package helper * @author Frederic Guillot */ -class Url extends \Kanboard\Core\Base +class Url extends Base { private $base = ''; private $directory = ''; @@ -158,7 +158,7 @@ class Url extends \Kanboard\Core\Base } if ($csrf) { - $qs['csrf_token'] = Security::getCSRFToken(); + $qs['csrf_token'] = $this->token->getCSRFToken(); } if (! empty($qs)) { diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php index 580c1e14..11e32313 100644 --- a/app/Model/Authentication.php +++ b/app/Model/Authentication.php @@ -2,7 +2,7 @@ namespace Kanboard\Model; -use Kanboard\Core\Request; +use Kanboard\Core\Http\Request; use SimpleValidator\Validator; use SimpleValidator\Validators; use Gregwar\Captcha\CaptchaBuilder; diff --git a/app/Model/Config.php b/app/Model/Config.php index cf634f80..3b90f58d 100644 --- a/app/Model/Config.php +++ b/app/Model/Config.php @@ -3,7 +3,7 @@ namespace Kanboard\Model; use Kanboard\Core\Translator; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; use Kanboard\Core\Session; /** @@ -265,7 +265,7 @@ class Config extends Setting */ public function regenerateToken($option) { - $this->save(array($option => Security::generateToken())); + $this->save(array($option => Token::getToken())); } /** diff --git a/app/Model/Project.php b/app/Model/Project.php index b767af26..9e30a9b8 100644 --- a/app/Model/Project.php +++ b/app/Model/Project.php @@ -4,7 +4,7 @@ namespace Kanboard\Model; use SimpleValidator\Validator; use SimpleValidator\Validators; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; /** * Project model @@ -491,7 +491,7 @@ class Project extends Base $this->db ->table(self::TABLE) ->eq('id', $project_id) - ->save(array('is_public' => 1, 'token' => Security::generateToken())); + ->save(array('is_public' => 1, 'token' => Token::getToken())); } /** diff --git a/app/Model/User.php b/app/Model/User.php index 6e7e94e0..dc00c0c5 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -6,7 +6,7 @@ use PicoDb\Database; use SimpleValidator\Validator; use SimpleValidator\Validators; use Kanboard\Core\Session; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; /** * User model @@ -383,7 +383,7 @@ class User extends Base return $this->db ->table(self::TABLE) ->eq('id', $user_id) - ->save(array('token' => Security::generateToken())); + ->save(array('token' => Token::getToken())); } /** diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index a021c1cc..54d58592 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -3,7 +3,7 @@ namespace Schema; use PDO; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; const VERSION = 93; @@ -869,7 +869,7 @@ function version_20(PDO $pdo) function version_19(PDO $pdo) { $pdo->exec("ALTER TABLE config ADD COLUMN api_token VARCHAR(255) DEFAULT ''"); - $pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'"); + $pdo->exec("UPDATE config SET api_token='".Token::getToken()."'"); } function version_18(PDO $pdo) @@ -1091,6 +1091,6 @@ function version_1(PDO $pdo) $pdo->exec(" INSERT INTO config (webhooks_token) - VALUES ('".Security::generateToken()."') + VALUES ('".Token::getToken()."') "); } diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index a3fb6d49..6f7efed0 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -3,7 +3,7 @@ namespace Schema; use PDO; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; const VERSION = 73; @@ -994,6 +994,6 @@ function version_1(PDO $pdo) $pdo->exec(" INSERT INTO config (webhooks_token, api_token) - VALUES ('".Security::generateToken()."', '".Security::generateToken()."') + VALUES ('".Token::getToken()."', '".Token::getToken()."') "); } diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index b9ab86f8..d27f11ec 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -2,7 +2,7 @@ namespace Schema; -use Kanboard\Core\Security; +use Kanboard\Core\Security\Token; use PDO; const VERSION = 88; @@ -799,7 +799,7 @@ function version_20(PDO $pdo) function version_19(PDO $pdo) { $pdo->exec("ALTER TABLE config ADD COLUMN api_token TEXT DEFAULT ''"); - $pdo->exec("UPDATE config SET api_token='".Security::generateToken()."'"); + $pdo->exec("UPDATE config SET api_token='".Token::getToken()."'"); } function version_18(PDO $pdo) @@ -1068,6 +1068,6 @@ function version_1(PDO $pdo) $pdo->exec(" INSERT INTO config (webhooks_token) - VALUES ('".Security::generateToken()."') + VALUES ('".Token::getToken()."') "); } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index c103d639..79bb734f 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -11,6 +11,7 @@ use Kanboard\Core\ObjectStorage\FileStorage; use Kanboard\Core\Paginator; use Kanboard\Core\OAuth2; use Kanboard\Core\Tool; +use Kanboard\Core\Http\Client as HttpClient; use Kanboard\Model\UserNotificationType; use Kanboard\Model\ProjectNotificationType; @@ -81,19 +82,24 @@ class ClassProvider implements ServiceProviderInterface 'Core' => array( 'DateParser', 'Helper', - 'HttpClient', 'Lexer', - 'Request', - 'Router', 'Session', 'Template', ), + 'Core\Http' => array( + 'Request', + 'Response', + 'Router', + ), 'Core\Cache' => array( 'MemoryCache', ), 'Core\Plugin' => array( 'Hook', ), + 'Core\Security' => array( + 'Token', + ), 'Integration' => array( 'BitbucketWebhook', 'GithubWebhook', @@ -113,6 +119,10 @@ class ClassProvider implements ServiceProviderInterface return new OAuth2($c); }); + $container['httpClient'] = function ($c) { + return new HttpClient($c); + }; + $container['htmlConverter'] = function () { return new HtmlConverter(array('strip_tags' => true)); }; diff --git a/app/Subscriber/AuthSubscriber.php b/app/Subscriber/AuthSubscriber.php index 2461b52c..77a39942 100644 --- a/app/Subscriber/AuthSubscriber.php +++ b/app/Subscriber/AuthSubscriber.php @@ -2,7 +2,7 @@ namespace Kanboard\Subscriber; -use Kanboard\Core\Request; +use Kanboard\Core\Http\Request; use Kanboard\Event\AuthEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; diff --git a/tests/units/Core/RouterTest.php b/tests/units/Core/Http/RouterTest.php index 753e1204..c2380247 100644 --- a/tests/units/Core/RouterTest.php +++ b/tests/units/Core/Http/RouterTest.php @@ -1,8 +1,8 @@ <?php -require_once __DIR__.'/../Base.php'; +require_once __DIR__.'/../../Base.php'; -use Kanboard\Core\Router; +use Kanboard\Core\Http\Router; class RouterTest extends Base { diff --git a/tests/units/Core/Security/TokenTest.php b/tests/units/Core/Security/TokenTest.php new file mode 100644 index 00000000..dbb7bd1a --- /dev/null +++ b/tests/units/Core/Security/TokenTest.php @@ -0,0 +1,29 @@ +<?php + +require_once __DIR__.'/../../Base.php'; + +use Kanboard\Core\Security\Token; + +class TokenTest extends Base +{ + public function testGenerateToken() + { + $t1 = Token::getToken(); + $t2 = Token::getToken(); + + $this->assertNotEmpty($t1); + $this->assertNotEmpty($t2); + + $this->assertNotEquals($t1, $t2); + } + + public function testCSRFTokens() + { + $token = new Token($this->container); + $t1 = $token->getCSRFToken(); + + $this->assertNotEmpty($t1); + $this->assertTrue($token->validateCSRFToken($t1)); + $this->assertFalse($token->validateCSRFToken($t1)); + } +} |