diff options
Diffstat (limited to 'app/Core')
-rw-r--r-- | app/Core/Controller/AccessForbiddenException.php | 14 | ||||
-rw-r--r-- | app/Core/Controller/BaseException.php | 52 | ||||
-rw-r--r-- | app/Core/Controller/BaseMiddleware.php | 58 | ||||
-rw-r--r-- | app/Core/Controller/PageNotFoundException.php | 14 | ||||
-rw-r--r-- | app/Core/Controller/Runner.php | 102 | ||||
-rw-r--r-- | app/Core/Http/Response.php | 355 | ||||
-rw-r--r-- | app/Core/Http/Route.php | 4 | ||||
-rw-r--r-- | app/Core/Http/Router.php | 62 |
8 files changed, 463 insertions, 198 deletions
diff --git a/app/Core/Controller/AccessForbiddenException.php b/app/Core/Controller/AccessForbiddenException.php new file mode 100644 index 00000000..b5dccb78 --- /dev/null +++ b/app/Core/Controller/AccessForbiddenException.php @@ -0,0 +1,14 @@ +<?php + +namespace Kanboard\Core\Controller; + +/** + * Class AccessForbiddenException + * + * @package Kanboard\Core\Controller + * @author Frederic Guillot + */ +class AccessForbiddenException extends BaseException +{ + +} diff --git a/app/Core/Controller/BaseException.php b/app/Core/Controller/BaseException.php new file mode 100644 index 00000000..13836d2c --- /dev/null +++ b/app/Core/Controller/BaseException.php @@ -0,0 +1,52 @@ +<?php + +namespace Kanboard\Core\Controller; + +use Exception; + +/** + * Class AccessForbiddenException + * + * @package Kanboard\Core\Controller + * @author Frederic Guillot + */ +class BaseException extends Exception +{ + protected $withoutLayout = false; + + /** + * Get object instance + * + * @static + * @access public + * @param string $message + * @return static + */ + public static function getInstance($message = '') + { + return new static($message); + } + + /** + * There is no layout + * + * @access public + * @return BaseException + */ + public function withoutLayout() + { + $this->withoutLayout = true; + return $this; + } + + /** + * Return true if no layout + * + * @access public + * @return boolean + */ + public function hasLayout() + { + return $this->withoutLayout; + } +} diff --git a/app/Core/Controller/BaseMiddleware.php b/app/Core/Controller/BaseMiddleware.php new file mode 100644 index 00000000..f2862d13 --- /dev/null +++ b/app/Core/Controller/BaseMiddleware.php @@ -0,0 +1,58 @@ +<?php + +namespace Kanboard\Core\Controller; + +use Kanboard\Core\Base; + +/** + * Class BaseMiddleware + * + * @package Kanboard\Core\Controller + * @author Frederic Guillot + */ +abstract class BaseMiddleware extends Base +{ + /** + * @var BaseMiddleware + */ + protected $nextMiddleware = null; + + /** + * Execute middleware + */ + abstract public function execute(); + + /** + * Set next middleware + * + * @param BaseMiddleware $nextMiddleware + * @return BaseMiddleware + */ + public function setNextMiddleware($nextMiddleware) + { + $this->nextMiddleware = $nextMiddleware; + return $this; + } + + /** + * @return BaseMiddleware + */ + public function getNextMiddleware() + { + return $this->nextMiddleware; + } + + /** + * Move to next middleware + */ + public function next() + { + if ($this->nextMiddleware !== null) { + if (DEBUG) { + $this->logger->debug(__METHOD__.' => ' . get_class($this->nextMiddleware)); + } + + $this->nextMiddleware->execute(); + } + } +} diff --git a/app/Core/Controller/PageNotFoundException.php b/app/Core/Controller/PageNotFoundException.php new file mode 100644 index 00000000..e96a2057 --- /dev/null +++ b/app/Core/Controller/PageNotFoundException.php @@ -0,0 +1,14 @@ +<?php + +namespace Kanboard\Core\Controller; + +/** + * Class PageNotFoundException + * + * @package Kanboard\Core\Controller + * @author Frederic Guillot + */ +class PageNotFoundException extends BaseException +{ + +} diff --git a/app/Core/Controller/Runner.php b/app/Core/Controller/Runner.php new file mode 100644 index 00000000..b973c098 --- /dev/null +++ b/app/Core/Controller/Runner.php @@ -0,0 +1,102 @@ +<?php + +namespace Kanboard\Core\Controller; + +use Kanboard\Controller\AppController; +use Kanboard\Core\Base; +use Kanboard\Middleware\ApplicationAuthorizationMiddleware; +use Kanboard\Middleware\AuthenticationMiddleware; +use Kanboard\Middleware\BootstrapMiddleware; +use Kanboard\Middleware\PostAuthenticationMiddleware; +use Kanboard\Middleware\ProjectAuthorizationMiddleware; +use RuntimeException; + +/** + * Class Runner + * + * @package Kanboard\Core\Controller + * @author Frederic Guillot + */ +class Runner extends Base +{ + /** + * Execute middleware and controller + */ + public function execute() + { + try { + $this->executeMiddleware(); + $this->executeController(); + } catch (PageNotFoundException $e) { + $controllerObject = new AppController($this->container); + $controllerObject->notFound($e->hasLayout()); + } catch (AccessForbiddenException $e) { + $controllerObject = new AppController($this->container); + $controllerObject->accessForbidden($e->hasLayout()); + } + } + + /** + * Execute all middleware + */ + protected function executeMiddleware() + { + if (DEBUG) { + $this->logger->debug(__METHOD__); + } + + $bootstrapMiddleware = new BootstrapMiddleware($this->container); + $authenticationMiddleware = new AuthenticationMiddleware($this->container); + $postAuthenticationMiddleware = new PostAuthenticationMiddleware($this->container); + $appAuthorizationMiddleware = new ApplicationAuthorizationMiddleware($this->container); + $projectAuthorizationMiddleware = new ProjectAuthorizationMiddleware($this->container); + + $bootstrapMiddleware->setNextMiddleware($authenticationMiddleware); + $authenticationMiddleware->setNextMiddleware($postAuthenticationMiddleware); + $postAuthenticationMiddleware->setNextMiddleware($appAuthorizationMiddleware); + $appAuthorizationMiddleware->setNextMiddleware($projectAuthorizationMiddleware); + + $bootstrapMiddleware->execute(); + } + + /** + * Execute the controller + */ + protected function executeController() + { + $className = $this->getControllerClassName(); + + if (DEBUG) { + $this->logger->debug(__METHOD__.' => '.$className.'::'.$this->router->getAction()); + } + + $controllerObject = new $className($this->container); + $controllerObject->{$this->router->getAction()}(); + } + + /** + * Get controller class name + * + * @access protected + * @return string + * @throws RuntimeException + */ + protected function getControllerClassName() + { + if ($this->router->getPlugin() !== '') { + $className = '\Kanboard\Plugin\\'.$this->router->getPlugin().'\Controller\\'.$this->router->getController(); + } else { + $className = '\Kanboard\Controller\\'.$this->router->getController(); + } + + if (! class_exists($className)) { + throw new RuntimeException('Controller not found'); + } + + if (! method_exists($className, $this->router->getAction())) { + throw new RuntimeException('Action not implemented'); + } + + return $className; + } +} diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php index 996fc58d..fd67ec95 100644 --- a/app/Core/Http/Response.php +++ b/app/Core/Http/Response.php @@ -13,296 +13,359 @@ use Kanboard\Core\Csv; */ class Response extends Base { + private $httpStatusCode = 200; + private $httpHeaders = array(); + private $httpBody = ''; + /** - * Send headers to cache a resource + * Set HTTP status code * * @access public - * @param integer $duration - * @param string $etag + * @param integer $statusCode + * @return $this */ - public function cache($duration, $etag = '') + public function withStatusCode($statusCode) { - header('Pragma: cache'); - header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT'); - header('Cache-Control: public, max-age=' . $duration); - - if ($etag) { - header('ETag: "' . $etag . '"'); - } + $this->httpStatusCode = $statusCode; + return $this; } /** - * Send no cache headers + * Set HTTP header * * @access public + * @param string $header + * @param string $value + * @return $this */ - public function nocache() + public function withHeader($header, $value) { - header('Pragma: no-cache'); - header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); - - // Use no-store due to a Chrome bug: https://code.google.com/p/chromium/issues/detail?id=28035 - header('Cache-Control: no-store, must-revalidate'); + $this->httpHeaders[$header] = $value; + return $this; } /** - * Send a custom Content-Type header + * Set content type header * * @access public - * @param string $mimetype Mime-type + * @param string $value + * @return $this */ - public function contentType($mimetype) + public function withContentType($value) { - header('Content-Type: '.$mimetype); + $this->httpHeaders['Content-Type'] = $value; + return $this; } /** - * Force the browser to download an attachment + * Set default security headers * * @access public - * @param string $filename File name + * @return $this */ - public function forceDownload($filename) + public function withSecurityHeaders() { - header('Content-Disposition: attachment; filename="'.$filename.'"'); - header('Content-Transfer-Encoding: binary'); - header('Content-Type: application/octet-stream'); + $this->httpHeaders['X-Content-Type-Options'] = 'nosniff'; + $this->httpHeaders['X-XSS-Protection'] = '1; mode=block'; + return $this; } /** - * Send a custom HTTP status code + * Set header Content-Security-Policy * * @access public - * @param integer $status_code HTTP status code + * @param array $policies + * @return $this */ - public function status($status_code) + public function withContentSecurityPolicy(array $policies = array()) { - header('Status: '.$status_code); - header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code); + $values = ''; + + foreach ($policies as $policy => $acl) { + $values .= $policy.' '.trim($acl).'; '; + } + + $this->withHeader('Content-Security-Policy', $values); + return $this; } /** - * Redirect to another URL + * Set header X-Frame-Options * * @access public - * @param string $url Redirection URL - * @param boolean $self If Ajax request and true: refresh the current page + * @return $this */ - public function redirect($url, $self = false) + public function withXframe() { - if ($this->request->isAjax()) { - header('X-Ajax-Redirect: '.($self ? 'self' : $url)); - } else { - header('Location: '.$url); - } - - exit; + $this->withHeader('X-Frame-Options', 'DENY'); + return $this; } /** - * Send a CSV response + * Set header Strict-Transport-Security (only if we use HTTPS) * * @access public - * @param array $data Data to serialize in csv - * @param integer $status_code HTTP status code + * @return $this */ - public function csv(array $data, $status_code = 200) + public function withStrictTransportSecurity() { - $this->status($status_code); - $this->nocache(); + if ($this->request->isHTTPS()) { + $this->withHeader('Strict-Transport-Security', 'max-age=31536000'); + } - header('Content-Type: text/csv'); - Csv::output($data); - exit; + return $this; } /** - * Send a Json response + * Set HTTP response body * * @access public - * @param array $data Data to serialize in json - * @param integer $status_code HTTP status code + * @param string $body + * @return $this */ - public function json(array $data, $status_code = 200) + public function withBody($body) { - $this->status($status_code); - $this->nocache(); - header('Content-Type: application/json'); - echo json_encode($data); - exit; + $this->httpBody = $body; + return $this; } /** - * Send a text response + * Send headers to cache a resource * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param integer $duration + * @param string $etag + * @return $this */ - public function text($data, $status_code = 200) + public function withCache($duration, $etag = '') { - $this->status($status_code); - $this->nocache(); - header('Content-Type: text/plain; charset=utf-8'); - echo $data; - exit; + $this + ->withHeader('Pragma', 'cache') + ->withHeader('Expires', gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT') + ->withHeader('Cache-Control', 'public, max-age=' . $duration) + ; + + if ($etag) { + $this->withHeader('ETag', '"' . $etag . '"'); + } + + return $this; } /** - * Send a HTML response + * Send no cache headers * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @return $this */ - public function html($data, $status_code = 200) + public function withoutCache() { - $this->status($status_code); - $this->nocache(); - header('Content-Type: text/html; charset=utf-8'); - echo $data; - exit; + $this->withHeader('Pragma', 'no-cache'); + $this->withHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); + return $this; } /** - * Send a XML response + * Force the browser to download an attachment * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $filename + * @return $this */ - public function xml($data, $status_code = 200) + public function withDownload($filename) { - $this->status($status_code); - $this->nocache(); - header('Content-Type: text/xml; charset=utf-8'); - echo $data; - exit; + $this->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $this->withHeader('Content-Transfer-Encoding', 'binary'); + $this->withHeader('Content-Type', 'application/octet-stream'); + return $this; } /** - * Send a javascript response + * Send headers and body * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code */ - public function js($data, $status_code = 200) + public function send() { - $this->status($status_code); + if ($this->httpStatusCode !== 200) { + header('Status: '.$this->httpStatusCode); + header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$this->httpStatusCode); + } - header('Content-Type: text/javascript; charset=utf-8'); - echo $data; + foreach ($this->httpHeaders as $header => $value) { + header($header.': '.$value); + } - exit; + if (! empty($this->httpBody)) { + echo $this->httpBody; + } } /** - * Send a css response + * Send a custom HTTP status code * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param integer $statusCode */ - public function css($data, $status_code = 200) + public function status($statusCode) { - $this->status($status_code); + $this->withStatusCode($statusCode); + $this->send(); + } - header('Content-Type: text/css; charset=utf-8'); - echo $data; + /** + * Redirect to another URL + * + * @access public + * @param string $url Redirection URL + * @param boolean $self If Ajax request and true: refresh the current page + */ + public function redirect($url, $self = false) + { + if ($this->request->isAjax()) { + $this->withHeader('X-Ajax-Redirect', $self ? 'self' : $url); + } else { + $this->withHeader('Location', $url); + } - exit; + $this->send(); } /** - * Send a binary response + * Send a HTML response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function binary($data, $status_code = 200) + public function html($data, $statusCode = 200) { - $this->status($status_code); - $this->nocache(); - header('Content-Transfer-Encoding: binary'); - header('Content-Type: application/octet-stream'); - echo $data; - exit; + $this->withStatusCode($statusCode); + $this->withContentType('text/html; charset=utf-8'); + $this->withBody($data); + $this->send(); } /** - * Send a iCal response + * Send a text response * * @access public - * @param string $data Raw data - * @param integer $status_code HTTP status code + * @param string $data + * @param integer $statusCode */ - public function ical($data, $status_code = 200) + public function text($data, $statusCode = 200) { - $this->status($status_code); - $this->contentType('text/calendar; charset=utf-8'); - echo $data; + $this->withStatusCode($statusCode); + $this->withContentType('text/plain; charset=utf-8'); + $this->withBody($data); + $this->send(); } /** - * Send the security header: Content-Security-Policy + * Send a CSV response * * @access public - * @param array $policies CSP rules + * @param array $data Data to serialize in csv */ - public function csp(array $policies = array()) + public function csv(array $data) { - $values = ''; + $this->withoutCache(); + $this->withContentType('text/csv; charset=utf-8'); + $this->send(); + Csv::output($data); + } - foreach ($policies as $policy => $acl) { - $values .= $policy.' '.trim($acl).'; '; - } + /** + * Send a Json response + * + * @access public + * @param array $data Data to serialize in json + * @param integer $statusCode HTTP status code + */ + public function json(array $data, $statusCode = 200) + { + $this->withStatusCode($statusCode); + $this->withContentType('application/json'); + $this->withoutCache(); + $this->withBody(json_encode($data)); + $this->send(); + } - header('Content-Security-Policy: '.$values); + /** + * Send a XML response + * + * @access public + * @param string $data + * @param integer $statusCode + */ + public function xml($data, $statusCode = 200) + { + $this->withStatusCode($statusCode); + $this->withContentType('text/xml; charset=utf-8'); + $this->withoutCache(); + $this->withBody($data); + $this->send(); } /** - * Send the security header: X-Content-Type-Options + * Send a javascript response * * @access public + * @param string $data + * @param integer $statusCode */ - public function nosniff() + public function js($data, $statusCode = 200) { - header('X-Content-Type-Options: nosniff'); + $this->withStatusCode($statusCode); + $this->withContentType('text/javascript; charset=utf-8'); + $this->withBody($data); + $this->send(); } /** - * Send the security header: X-XSS-Protection + * Send a css response * * @access public + * @param string $data + * @param integer $statusCode */ - public function xss() + public function css($data, $statusCode = 200) { - header('X-XSS-Protection: 1; mode=block'); + $this->withStatusCode($statusCode); + $this->withContentType('text/css; charset=utf-8'); + $this->withBody($data); + $this->send(); } /** - * Send the security header: Strict-Transport-Security (only if we use HTTPS) + * Send a binary response * * @access public + * @param string $data + * @param integer $statusCode */ - public function hsts() + public function binary($data, $statusCode = 200) { - if ($this->request->isHTTPS()) { - header('Strict-Transport-Security: max-age=31536000'); - } + $this->withStatusCode($statusCode); + $this->withoutCache(); + $this->withHeader('Content-Transfer-Encoding', 'binary'); + $this->withContentType('application/octet-stream'); + $this->withBody($data); + $this->send(); } /** - * Send the security header: X-Frame-Options (deny by default) + * Send a iCal response * * @access public - * @param string $mode Frame option mode - * @param array $urls Allowed urls for the given mode + * @param string $data + * @param integer $statusCode */ - public function xframe($mode = 'DENY', array $urls = array()) + public function ical($data, $statusCode = 200) { - header('X-Frame-Options: '.$mode.' '.implode(' ', $urls)); + $this->withStatusCode($statusCode); + $this->withContentType('text/calendar; charset=utf-8'); + $this->withBody($data); + $this->send(); } } diff --git a/app/Core/Http/Route.php b/app/Core/Http/Route.php index 7836146d..9b45b725 100644 --- a/app/Core/Http/Route.php +++ b/app/Core/Http/Route.php @@ -119,8 +119,8 @@ class Route extends Base } return array( - 'controller' => 'app', - 'action' => 'index', + 'controller' => 'DashboardController', + 'action' => 'show', 'plugin' => '', ); } diff --git a/app/Core/Http/Router.php b/app/Core/Http/Router.php index 0fe80ecc..4de276a0 100644 --- a/app/Core/Http/Router.php +++ b/app/Core/Http/Router.php @@ -2,7 +2,6 @@ namespace Kanboard\Core\Http; -use RuntimeException; use Kanboard\Core\Base; /** @@ -13,13 +12,16 @@ use Kanboard\Core\Base; */ class Router extends Base { + const DEFAULT_CONTROLLER = 'DashboardController'; + const DEFAULT_METHOD = 'show'; + /** * Plugin name * * @access private * @var string */ - private $plugin = ''; + private $currentPluginName = ''; /** * Controller @@ -27,7 +29,7 @@ class Router extends Base * @access private * @var string */ - private $controller = ''; + private $currentControllerName = ''; /** * Action @@ -35,7 +37,7 @@ class Router extends Base * @access private * @var string */ - private $action = ''; + private $currentActionName = ''; /** * Get plugin name @@ -45,7 +47,7 @@ class Router extends Base */ public function getPlugin() { - return $this->plugin; + return $this->currentPluginName; } /** @@ -56,7 +58,7 @@ class Router extends Base */ public function getController() { - return $this->controller; + return $this->currentControllerName; } /** @@ -67,7 +69,7 @@ class Router extends Base */ public function getAction() { - return $this->action; + return $this->currentActionName; } /** @@ -109,11 +111,9 @@ class Router extends Base $plugin = $route['plugin']; } - $this->controller = ucfirst($this->sanitize($controller, 'app')); - $this->action = $this->sanitize($action, 'index'); - $this->plugin = ucfirst($this->sanitize($plugin)); - - return $this->executeAction(); + $this->currentControllerName = ucfirst($this->sanitize($controller, self::DEFAULT_CONTROLLER)); + $this->currentActionName = $this->sanitize($action, self::DEFAULT_METHOD); + $this->currentPluginName = ucfirst($this->sanitize($plugin)); } /** @@ -128,42 +128,4 @@ class Router extends Base { return preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $value : $default; } - - /** - * Execute controller action - * - * @access private - */ - private function executeAction() - { - $class = $this->getControllerClassName(); - - if (! class_exists($class)) { - throw new RuntimeException('Controller not found'); - } - - if (! method_exists($class, $this->action)) { - throw new RuntimeException('Action not implemented'); - } - - $instance = new $class($this->container); - $instance->beforeAction(); - $instance->{$this->action}(); - return $instance; - } - - /** - * Get controller class name - * - * @access private - * @return string - */ - private function getControllerClassName() - { - if ($this->plugin !== '') { - return '\Kanboard\Plugin\\'.$this->plugin.'\Controller\\'.$this->controller; - } - - return '\Kanboard\Controller\\'.$this->controller; - } } |