summaryrefslogtreecommitdiff
path: root/app/Core
diff options
context:
space:
mode:
Diffstat (limited to 'app/Core')
-rw-r--r--app/Core/Controller/AccessForbiddenException.php14
-rw-r--r--app/Core/Controller/BaseException.php52
-rw-r--r--app/Core/Controller/BaseMiddleware.php58
-rw-r--r--app/Core/Controller/PageNotFoundException.php14
-rw-r--r--app/Core/Controller/Runner.php102
-rw-r--r--app/Core/Http/Response.php355
-rw-r--r--app/Core/Http/Route.php4
-rw-r--r--app/Core/Http/Router.php62
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;
- }
}