Refactoring: added controlled middleware and changed response class

This commit is contained in:
Frederic Guillot
2016-05-15 18:31:47 -04:00
parent 108e867605
commit 67b8361649
105 changed files with 1586 additions and 1147 deletions

View File

@@ -13,72 +13,204 @@ use Kanboard\Core\Csv;
*/
class Response extends Base
{
private $httpStatusCode = 200;
private $httpHeaders = array();
private $httpBody = '';
/**
* Set HTTP status code
*
* @access public
* @param integer $statusCode
* @return $this
*/
public function withStatusCode($statusCode)
{
$this->httpStatusCode = $statusCode;
return $this;
}
/**
* Set HTTP header
*
* @access public
* @param string $header
* @param string $value
* @return $this
*/
public function withHeader($header, $value)
{
$this->httpHeaders[$header] = $value;
return $this;
}
/**
* Set content type header
*
* @access public
* @param string $value
* @return $this
*/
public function withContentType($value)
{
$this->httpHeaders['Content-Type'] = $value;
return $this;
}
/**
* Set default security headers
*
* @access public
* @return $this
*/
public function withSecurityHeaders()
{
$this->httpHeaders['X-Content-Type-Options'] = 'nosniff';
$this->httpHeaders['X-XSS-Protection'] = '1; mode=block';
return $this;
}
/**
* Set header Content-Security-Policy
*
* @access public
* @param array $policies
* @return $this
*/
public function withContentSecurityPolicy(array $policies = array())
{
$values = '';
foreach ($policies as $policy => $acl) {
$values .= $policy.' '.trim($acl).'; ';
}
$this->withHeader('Content-Security-Policy', $values);
return $this;
}
/**
* Set header X-Frame-Options
*
* @access public
* @return $this
*/
public function withXframe()
{
$this->withHeader('X-Frame-Options', 'DENY');
return $this;
}
/**
* Set header Strict-Transport-Security (only if we use HTTPS)
*
* @access public
* @return $this
*/
public function withStrictTransportSecurity()
{
if ($this->request->isHTTPS()) {
$this->withHeader('Strict-Transport-Security', 'max-age=31536000');
}
return $this;
}
/**
* Set HTTP response body
*
* @access public
* @param string $body
* @return $this
*/
public function withBody($body)
{
$this->httpBody = $body;
return $this;
}
/**
* Send headers to cache a resource
*
* @access public
* @param integer $duration
* @param string $etag
* @return $this
*/
public function cache($duration, $etag = '')
public function withCache($duration, $etag = '')
{
header('Pragma: cache');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT');
header('Cache-Control: public, max-age=' . $duration);
$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) {
header('ETag: "' . $etag . '"');
$this->withHeader('ETag', '"' . $etag . '"');
}
return $this;
}
/**
* Send no cache headers
*
* @access public
* @return $this
*/
public function nocache()
public function withoutCache()
{
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');
}
/**
* Send a custom Content-Type header
*
* @access public
* @param string $mimetype Mime-type
*/
public function contentType($mimetype)
{
header('Content-Type: '.$mimetype);
$this->withHeader('Pragma', 'no-cache');
$this->withHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT');
return $this;
}
/**
* Force the browser to download an attachment
*
* @access public
* @param string $filename File name
* @param string $filename
* @return $this
*/
public function forceDownload($filename)
public function withDownload($filename)
{
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Content-Transfer-Encoding: binary');
header('Content-Type: application/octet-stream');
$this->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
$this->withHeader('Content-Transfer-Encoding', 'binary');
$this->withHeader('Content-Type', 'application/octet-stream');
return $this;
}
/**
* Send headers and body
*
* @access public
*/
public function send()
{
if ($this->httpStatusCode !== 200) {
header('Status: '.$this->httpStatusCode);
header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$this->httpStatusCode);
}
foreach ($this->httpHeaders as $header => $value) {
header($header.': '.$value);
}
if (! empty($this->httpBody)) {
echo $this->httpBody;
}
}
/**
* Send a custom HTTP status code
*
* @access public
* @param integer $status_code HTTP status code
* @param integer $statusCode
*/
public function status($status_code)
public function status($statusCode)
{
header('Status: '.$status_code);
header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code);
$this->withStatusCode($statusCode);
$this->send();
}
/**
@@ -91,218 +223,149 @@ class Response extends Base
public function redirect($url, $self = false)
{
if ($this->request->isAjax()) {
header('X-Ajax-Redirect: '.($self ? 'self' : $url));
$this->withHeader('X-Ajax-Redirect', $self ? 'self' : $url);
} else {
header('Location: '.$url);
$this->withHeader('Location', $url);
}
exit;
}
/**
* Send a CSV response
*
* @access public
* @param array $data Data to serialize in csv
* @param integer $status_code HTTP status code
*/
public function csv(array $data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/csv');
Csv::output($data);
exit;
}
/**
* Send a Json response
*
* @access public
* @param array $data Data to serialize in json
* @param integer $status_code HTTP status code
*/
public function json(array $data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
/**
* Send a text response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
*/
public function text($data, $status_code = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/plain; charset=utf-8');
echo $data;
exit;
$this->send();
}
/**
* 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 html($data, $status_code = 200)
public function html($data, $statusCode = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/html; charset=utf-8');
echo $data;
exit;
$this->withStatusCode($statusCode);
$this->withContentType('text/html; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a text response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function text($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/plain; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a CSV response
*
* @access public
* @param array $data Data to serialize in csv
*/
public function csv(array $data)
{
$this->withoutCache();
$this->withContentType('text/csv; charset=utf-8');
$this->send();
Csv::output($data);
}
/**
* 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();
}
/**
* Send a XML response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
* @param string $data
* @param integer $statusCode
*/
public function xml($data, $status_code = 200)
public function xml($data, $statusCode = 200)
{
$this->status($status_code);
$this->nocache();
header('Content-Type: text/xml; charset=utf-8');
echo $data;
exit;
$this->withStatusCode($statusCode);
$this->withContentType('text/xml; charset=utf-8');
$this->withoutCache();
$this->withBody($data);
$this->send();
}
/**
* Send a javascript response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
* @param string $data
* @param integer $statusCode
*/
public function js($data, $status_code = 200)
public function js($data, $statusCode = 200)
{
$this->status($status_code);
header('Content-Type: text/javascript; charset=utf-8');
echo $data;
exit;
$this->withStatusCode($statusCode);
$this->withContentType('text/javascript; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a css response
*
* @access public
* @param string $data Raw data
* @param integer $status_code HTTP status code
* @param string $data
* @param integer $statusCode
*/
public function css($data, $status_code = 200)
public function css($data, $statusCode = 200)
{
$this->status($status_code);
header('Content-Type: text/css; charset=utf-8');
echo $data;
exit;
$this->withStatusCode($statusCode);
$this->withContentType('text/css; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a binary 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 binary($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->withoutCache();
$this->withHeader('Content-Transfer-Encoding', 'binary');
$this->withContentType('application/octet-stream');
$this->withBody($data);
$this->send();
}
/**
* Send a iCal 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 ical($data, $statusCode = 200)
{
$this->status($status_code);
$this->contentType('text/calendar; charset=utf-8');
echo $data;
}
/**
* Send the security header: Content-Security-Policy
*
* @access public
* @param array $policies CSP rules
*/
public function csp(array $policies = array())
{
$values = '';
foreach ($policies as $policy => $acl) {
$values .= $policy.' '.trim($acl).'; ';
}
header('Content-Security-Policy: '.$values);
}
/**
* Send the security header: X-Content-Type-Options
*
* @access public
*/
public function nosniff()
{
header('X-Content-Type-Options: nosniff');
}
/**
* Send the security header: X-XSS-Protection
*
* @access public
*/
public function xss()
{
header('X-XSS-Protection: 1; mode=block');
}
/**
* Send the security header: Strict-Transport-Security (only if we use HTTPS)
*
* @access public
*/
public function hsts()
{
if ($this->request->isHTTPS()) {
header('Strict-Transport-Security: max-age=31536000');
}
}
/**
* Send the security header: X-Frame-Options (deny by default)
*
* @access public
* @param string $mode Frame option mode
* @param array $urls Allowed urls for the given mode
*/
public function xframe($mode = 'DENY', array $urls = array())
{
header('X-Frame-Options: '.$mode.' '.implode(' ', $urls));
$this->withStatusCode($statusCode);
$this->withContentType('text/calendar; charset=utf-8');
$this->withBody($data);
$this->send();
}
}

View File

@@ -119,8 +119,8 @@ class Route extends Base
}
return array(
'controller' => 'app',
'action' => 'index',
'controller' => 'DashboardController',
'action' => 'show',
'plugin' => '',
);
}

View File

@@ -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;
}
}