diff options
Diffstat (limited to 'app/Core/Http')
-rw-r--r-- | app/Core/Http/Client.php | 176 | ||||
-rw-r--r-- | app/Core/Http/OAuth2.php | 121 | ||||
-rw-r--r-- | app/Core/Http/RememberMeCookie.php | 120 | ||||
-rw-r--r-- | app/Core/Http/Request.php | 337 | ||||
-rw-r--r-- | app/Core/Http/Response.php | 276 | ||||
-rw-r--r-- | app/Core/Http/Route.php | 187 | ||||
-rw-r--r-- | app/Core/Http/Router.php | 169 |
7 files changed, 1386 insertions, 0 deletions
diff --git a/app/Core/Http/Client.php b/app/Core/Http/Client.php new file mode 100644 index 00000000..12b0a1cb --- /dev/null +++ b/app/Core/Http/Client.php @@ -0,0 +1,176 @@ +<?php + +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; + +/** + * HTTP client + * + * @package http + * @author Frederic Guillot + */ +class Client extends Base +{ + /** + * HTTP connection timeout in seconds + * + * @var integer + */ + const HTTP_TIMEOUT = 5; + + /** + * Number of maximum redirections for the HTTP client + * + * @var integer + */ + const HTTP_MAX_REDIRECTS = 2; + + /** + * HTTP client user agent + * + * @var string + */ + const HTTP_USER_AGENT = 'Kanboard'; + + /** + * Send a GET HTTP request + * + * @access public + * @param string $url + * @param string[] $headers + * @return string + */ + public function get($url, array $headers = array()) + { + return $this->doRequest('GET', $url, '', $headers); + } + + /** + * Send a GET HTTP request and parse JSON response + * + * @access public + * @param string $url + * @param string[] $headers + * @return array + */ + public function getJson($url, array $headers = array()) + { + $response = $this->doRequest('GET', $url, '', array_merge(array('Accept: application/json'), $headers)); + return json_decode($response, true) ?: array(); + } + + /** + * Send a POST HTTP request encoded in JSON + * + * @access public + * @param string $url + * @param array $data + * @param string[] $headers + * @return string + */ + public function postJson($url, array $data, array $headers = array()) + { + return $this->doRequest( + 'POST', + $url, + json_encode($data), + array_merge(array('Content-type: application/json'), $headers) + ); + } + + /** + * Send a POST HTTP request encoded in www-form-urlencoded + * + * @access public + * @param string $url + * @param array $data + * @param string[] $headers + * @return string + */ + public function postForm($url, array $data, array $headers = array()) + { + return $this->doRequest( + 'POST', + $url, + http_build_query($data), + array_merge(array('Content-type: application/x-www-form-urlencoded'), $headers) + ); + } + + /** + * Make the HTTP request + * + * @access private + * @param string $method + * @param string $url + * @param string $content + * @param string[] $headers + * @return string + */ + private function doRequest($method, $url, $content, array $headers) + { + if (empty($url)) { + 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', + ); + + if (HTTP_PROXY_USERNAME) { + $default_headers[] = 'Proxy-Authorization: Basic '.base64_encode(HTTP_PROXY_USERNAME.':'.HTTP_PROXY_PASSWORD); + } + + $headers = array_merge($default_headers, $headers); + + $context = array( + 'http' => array( + 'method' => $method, + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => $content + ) + ); + + if (HTTP_PROXY_HOSTNAME) { + $context['http']['proxy'] = 'tcp://'.HTTP_PROXY_HOSTNAME.':'.HTTP_PROXY_PORT; + $context['http']['request_fulluri'] = true; + } + + return $context; + } +} diff --git a/app/Core/Http/OAuth2.php b/app/Core/Http/OAuth2.php new file mode 100644 index 00000000..6fa1fb0a --- /dev/null +++ b/app/Core/Http/OAuth2.php @@ -0,0 +1,121 @@ +<?php + +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; + +/** + * OAuth2 Client + * + * @package http + * @author Frederic Guillot + */ +class OAuth2 extends Base +{ + private $clientId; + private $secret; + private $callbackUrl; + private $authUrl; + private $tokenUrl; + private $scopes; + private $tokenType; + private $accessToken; + + /** + * Create OAuth2 service + * + * @access public + * @param string $clientId + * @param string $secret + * @param string $callbackUrl + * @param string $authUrl + * @param string $tokenUrl + * @param array $scopes + * @return OAuth2 + */ + public function createService($clientId, $secret, $callbackUrl, $authUrl, $tokenUrl, array $scopes) + { + $this->clientId = $clientId; + $this->secret = $secret; + $this->callbackUrl = $callbackUrl; + $this->authUrl = $authUrl; + $this->tokenUrl = $tokenUrl; + $this->scopes = $scopes; + + return $this; + } + + /** + * Get authorization url + * + * @access public + * @return string + */ + public function getAuthorizationUrl() + { + $params = array( + 'response_type' => 'code', + 'client_id' => $this->clientId, + 'redirect_uri' => $this->callbackUrl, + 'scope' => implode(' ', $this->scopes), + ); + + return $this->authUrl.'?'.http_build_query($params); + } + + /** + * Get authorization header + * + * @access public + * @return string + */ + public function getAuthorizationHeader() + { + if (strtolower($this->tokenType) === 'bearer') { + return 'Authorization: Bearer '.$this->accessToken; + } + + return ''; + } + + /** + * Get access token + * + * @access public + * @param string $code + * @return string + */ + public function getAccessToken($code) + { + if (empty($this->accessToken) && ! empty($code)) { + $params = array( + 'code' => $code, + 'client_id' => $this->clientId, + 'client_secret' => $this->secret, + 'redirect_uri' => $this->callbackUrl, + 'grant_type' => 'authorization_code', + ); + + $response = json_decode($this->httpClient->postForm($this->tokenUrl, $params, array('Accept: application/json')), true); + + $this->tokenType = isset($response['token_type']) ? $response['token_type'] : ''; + $this->accessToken = isset($response['access_token']) ? $response['access_token'] : ''; + } + + return $this->accessToken; + } + + /** + * Set access token + * + * @access public + * @param string $token + * @param string $type + * @return string + */ + public function setAccessToken($token, $type = 'bearer') + { + $this->accessToken = $token; + $this->tokenType = $type; + } +} diff --git a/app/Core/Http/RememberMeCookie.php b/app/Core/Http/RememberMeCookie.php new file mode 100644 index 00000000..a32b35f3 --- /dev/null +++ b/app/Core/Http/RememberMeCookie.php @@ -0,0 +1,120 @@ +<?php + +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; + +/** + * Remember Me Cookie + * + * @package http + * @author Frederic Guillot + */ +class RememberMeCookie extends Base +{ + /** + * Cookie name + * + * @var string + */ + const COOKIE_NAME = 'KB_RM'; + + /** + * Encode the cookie + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @return string + */ + public function encode($token, $sequence) + { + return implode('|', array($token, $sequence)); + } + + /** + * Decode the value of a cookie + * + * @access public + * @param string $value Raw cookie data + * @return array + */ + public function decode($value) + { + list($token, $sequence) = explode('|', $value); + + return array( + 'token' => $token, + 'sequence' => $sequence, + ); + } + + /** + * Return true if the current user has a RememberMe cookie + * + * @access public + * @return bool + */ + public function hasCookie() + { + return $this->request->getCookie(self::COOKIE_NAME) !== ''; + } + + /** + * Write and encode the cookie + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @param string $expiration Cookie expiration + * @return boolean + */ + public function write($token, $sequence, $expiration) + { + return setcookie( + self::COOKIE_NAME, + $this->encode($token, $sequence), + $expiration, + $this->helper->url->dir(), + null, + $this->request->isHTTPS(), + true + ); + } + + /** + * Read and decode the cookie + * + * @access public + * @return mixed + */ + public function read() + { + $cookie = $this->request->getCookie(self::COOKIE_NAME); + + if (empty($cookie)) { + return false; + } + + return $this->decode($cookie); + } + + /** + * Remove the cookie + * + * @access public + * @return boolean + */ + public function remove() + { + return setcookie( + self::COOKIE_NAME, + '', + time() - 3600, + $this->helper->url->dir(), + null, + $this->request->isHTTPS(), + true + ); + } +} diff --git a/app/Core/Http/Request.php b/app/Core/Http/Request.php new file mode 100644 index 00000000..1b3036d5 --- /dev/null +++ b/app/Core/Http/Request.php @@ -0,0 +1,337 @@ +<?php + +namespace Kanboard\Core\Http; + +use Pimple\Container; +use Kanboard\Core\Base; + +/** + * Request class + * + * @package http + * @author Frederic Guillot + */ +class Request extends Base +{ + /** + * Pointer to PHP environment variables + * + * @access private + * @var array + */ + private $server; + private $get; + private $post; + private $files; + private $cookies; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container, array $server = array(), array $get = array(), array $post = array(), array $files = array(), array $cookies = array()) + { + parent::__construct($container); + $this->server = empty($server) ? $_SERVER : $server; + $this->get = empty($get) ? $_GET : $get; + $this->post = empty($post) ? $_POST : $post; + $this->files = empty($files) ? $_FILES : $files; + $this->cookies = empty($cookies) ? $_COOKIE : $cookies; + } + + /** + * Set GET parameters + * + * @param array $params + */ + public function setParams(array $params) + { + $this->get = array_merge($this->get, $params); + } + + /** + * Get query string string parameter + * + * @access public + * @param string $name Parameter name + * @param string $default_value Default value + * @return string + */ + public function getStringParam($name, $default_value = '') + { + return isset($this->get[$name]) ? $this->get[$name] : $default_value; + } + + /** + * Get query string integer parameter + * + * @access public + * @param string $name Parameter name + * @param integer $default_value Default value + * @return integer + */ + public function getIntegerParam($name, $default_value = 0) + { + return isset($this->get[$name]) && ctype_digit($this->get[$name]) ? (int) $this->get[$name] : $default_value; + } + + /** + * Get a form value + * + * @access public + * @param string $name Form field name + * @return string|null + */ + public function getValue($name) + { + $values = $this->getValues(); + return isset($values[$name]) ? $values[$name] : null; + } + + /** + * Get form values and check for CSRF token + * + * @access public + * @return array + */ + public function getValues() + { + if (! empty($this->post) && ! empty($this->post['csrf_token']) && $this->token->validateCSRFToken($this->post['csrf_token'])) { + unset($this->post['csrf_token']); + return $this->post; + } + + return array(); + } + + /** + * Get the raw body of the HTTP request + * + * @access public + * @return string + */ + public function getBody() + { + return file_get_contents('php://input'); + } + + /** + * Get the Json request body + * + * @access public + * @return array + */ + public function getJson() + { + return json_decode($this->getBody(), true) ?: array(); + } + + /** + * Get the content of an uploaded file + * + * @access public + * @param string $name Form file name + * @return string + */ + public function getFileContent($name) + { + if (isset($this->files[$name]['tmp_name'])) { + return file_get_contents($this->files[$name]['tmp_name']); + } + + return ''; + } + + /** + * Get the path of an uploaded file + * + * @access public + * @param string $name Form file name + * @return string + */ + public function getFilePath($name) + { + return isset($this->files[$name]['tmp_name']) ? $this->files[$name]['tmp_name'] : ''; + } + + /** + * Get info of an uploaded file + * + * @access public + * @param string $name Form file name + * @return array + */ + public function getFileInfo($name) + { + return isset($this->files[$name]) ? $this->files[$name] : array(); + } + + /** + * Return HTTP method + * + * @access public + * @return bool + */ + public function getMethod() + { + return $this->getServerVariable('REQUEST_METHOD'); + } + + /** + * Return true if the HTTP request is sent with the POST method + * + * @access public + * @return bool + */ + public function isPost() + { + return $this->getServerVariable('REQUEST_METHOD') === 'POST'; + } + + /** + * Return true if the HTTP request is an Ajax request + * + * @access public + * @return bool + */ + public function isAjax() + { + return $this->getHeader('X-Requested-With') === 'XMLHttpRequest'; + } + + /** + * Check if the page is requested through HTTPS + * + * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS + * + * @access public + * @return boolean + */ + public function isHTTPS() + { + return isset($this->server['HTTPS']) && $this->server['HTTPS'] !== '' && $this->server['HTTPS'] !== 'off'; + } + + /** + * Get cookie value + * + * @access public + * @param string $name + * @return string + */ + public function getCookie($name) + { + return isset($this->cookies[$name]) ? $this->cookies[$name] : ''; + } + + /** + * Return a HTTP header value + * + * @access public + * @param string $name Header name + * @return string + */ + public function getHeader($name) + { + $name = 'HTTP_'.str_replace('-', '_', strtoupper($name)); + return $this->getServerVariable($name); + } + + /** + * Get remote user + * + * @access public + * @return string + */ + public function getRemoteUser() + { + return $this->getServerVariable(REVERSE_PROXY_USER_HEADER); + } + + /** + * Returns query string + * + * @access public + * @return string + */ + public function getQueryString() + { + return $this->getServerVariable('QUERY_STRING'); + } + + /** + * Return URI + * + * @access public + * @return string + */ + public function getUri() + { + return $this->getServerVariable('REQUEST_URI'); + } + + /** + * Get the user agent + * + * @access public + * @return string + */ + public function getUserAgent() + { + return empty($this->server['HTTP_USER_AGENT']) ? t('Unknown') : $this->server['HTTP_USER_AGENT']; + } + + /** + * Get the IP address of the user + * + * @access public + * @return string + */ + public function getIpAddress() + { + $keys = array( + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', + 'HTTP_FORWARDED', + 'REMOTE_ADDR' + ); + + foreach ($keys as $key) { + if ($this->getServerVariable($key) !== '') { + foreach (explode(',', $this->server[$key]) as $ipAddress) { + return trim($ipAddress); + } + } + } + + return t('Unknown'); + } + + /** + * Get start time + * + * @access public + * @return float + */ + public function getStartTime() + { + return $this->getServerVariable('REQUEST_TIME_FLOAT') ?: 0; + } + + /** + * Get server variable + * + * @access public + * @param string $variable + * @return string + */ + public function getServerVariable($variable) + { + return isset($this->server[$variable]) ? $this->server[$variable] : ''; + } +} diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php new file mode 100644 index 00000000..d098f519 --- /dev/null +++ b/app/Core/Http/Response.php @@ -0,0 +1,276 @@ +<?php + +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; +use Kanboard\Core\Csv; + +/** + * Response class + * + * @package http + * @author Frederic Guillot + */ +class Response extends Base +{ + /** + * Send no cache headers + * + * @access public + */ + public function nocache() + { + 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); + } + + /** + * Force the browser to download an attachment + * + * @access public + * @param string $filename File name + */ + public function forceDownload($filename) + { + header('Content-Disposition: attachment; filename="'.$filename.'"'); + header('Content-Transfer-Encoding: binary'); + header('Content-Type: application/octet-stream'); + } + + /** + * Send a custom HTTP status code + * + * @access public + * @param integer $status_code HTTP status code + */ + public function status($status_code) + { + header('Status: '.$status_code); + header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$status_code); + } + + /** + * 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()) { + header('X-Ajax-Redirect: '.($self ? 'self' : $url)); + } else { + header('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; + } + + /** + * Send a HTML response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function html($data, $status_code = 200) + { + $this->status($status_code); + $this->nocache(); + header('Content-Type: text/html; charset=utf-8'); + echo $data; + exit; + } + + /** + * Send a XML response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function xml($data, $status_code = 200) + { + $this->status($status_code); + $this->nocache(); + header('Content-Type: text/xml; charset=utf-8'); + echo $data; + exit; + } + + /** + * Send a javascript response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function js($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/javascript; charset=utf-8'); + echo $data; + + exit; + } + + /** + * Send a css response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function css($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/css; charset=utf-8'); + echo $data; + + exit; + } + + /** + * Send a binary response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function binary($data, $status_code = 200) + { + $this->status($status_code); + $this->nocache(); + header('Content-Transfer-Encoding: binary'); + header('Content-Type: application/octet-stream'); + echo $data; + exit; + } + + /** + * 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)); + } +} diff --git a/app/Core/Http/Route.php b/app/Core/Http/Route.php new file mode 100644 index 00000000..7836146d --- /dev/null +++ b/app/Core/Http/Route.php @@ -0,0 +1,187 @@ +<?php + +namespace Kanboard\Core\Http; + +use Kanboard\Core\Base; + +/** + * Route Handler + * + * @package http + * @author Frederic Guillot + */ +class Route extends Base +{ + /** + * Flag that enable the routing table + * + * @access private + * @var boolean + */ + private $activated = false; + + /** + * Store routes for path lookup + * + * @access private + * @var array + */ + private $paths = array(); + + /** + * Store routes for url lookup + * + * @access private + * @var array + */ + private $urls = array(); + + /** + * Enable routing table + * + * @access public + * @return Route + */ + public function enable() + { + $this->activated = true; + return $this; + } + + /** + * Add route + * + * @access public + * @param string $path + * @param string $controller + * @param string $action + * @param string $plugin + * @return Route + */ + public function addRoute($path, $controller, $action, $plugin = '') + { + if ($this->activated) { + $path = ltrim($path, '/'); + $items = explode('/', $path); + $params = $this->findParams($items); + + $this->paths[] = array( + 'items' => $items, + 'count' => count($items), + 'controller' => $controller, + 'action' => $action, + 'plugin' => $plugin, + ); + + $this->urls[$plugin][$controller][$action][] = array( + 'path' => $path, + 'params' => $params, + 'count' => count($params), + ); + } + + return $this; + } + + /** + * Find a route according to the given path + * + * @access public + * @param string $path + * @return array + */ + public function findRoute($path) + { + $items = explode('/', ltrim($path, '/')); + $count = count($items); + + foreach ($this->paths as $route) { + if ($count === $route['count']) { + $params = array(); + + for ($i = 0; $i < $count; $i++) { + if ($route['items'][$i]{0} === ':') { + $params[substr($route['items'][$i], 1)] = $items[$i]; + } elseif ($route['items'][$i] !== $items[$i]) { + break; + } + } + + if ($i === $count) { + $this->request->setParams($params); + return array( + 'controller' => $route['controller'], + 'action' => $route['action'], + 'plugin' => $route['plugin'], + ); + } + } + } + + return array( + 'controller' => 'app', + 'action' => 'index', + 'plugin' => '', + ); + } + + /** + * Find route url + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @param string $plugin + * @return string + */ + public function findUrl($controller, $action, array $params = array(), $plugin = '') + { + if ($plugin === '' && isset($params['plugin'])) { + $plugin = $params['plugin']; + unset($params['plugin']); + } + + if (! isset($this->urls[$plugin][$controller][$action])) { + return ''; + } + + foreach ($this->urls[$plugin][$controller][$action] as $route) { + if (array_diff_key($params, $route['params']) === array()) { + $url = $route['path']; + $i = 0; + + foreach ($params as $variable => $value) { + $url = str_replace(':'.$variable, $value, $url); + $i++; + } + + if ($i === $route['count']) { + return $url; + } + } + } + + return ''; + } + + /** + * Find url params + * + * @access public + * @param array $items + * @return array + */ + public function findParams(array $items) + { + $params = array(); + + foreach ($items as $item) { + if ($item !== '' && $item{0} === ':') { + $params[substr($item, 1)] = true; + } + } + + return $params; + } +} diff --git a/app/Core/Http/Router.php b/app/Core/Http/Router.php new file mode 100644 index 00000000..0fe80ecc --- /dev/null +++ b/app/Core/Http/Router.php @@ -0,0 +1,169 @@ +<?php + +namespace Kanboard\Core\Http; + +use RuntimeException; +use Kanboard\Core\Base; + +/** + * Route Dispatcher + * + * @package http + * @author Frederic Guillot + */ +class Router extends Base +{ + /** + * Plugin name + * + * @access private + * @var string + */ + private $plugin = ''; + + /** + * Controller + * + * @access private + * @var string + */ + private $controller = ''; + + /** + * Action + * + * @access private + * @var string + */ + private $action = ''; + + /** + * Get plugin name + * + * @access public + * @return string + */ + public function getPlugin() + { + return $this->plugin; + } + + /** + * Get controller + * + * @access public + * @return string + */ + public function getController() + { + return $this->controller; + } + + /** + * Get action + * + * @access public + * @return string + */ + public function getAction() + { + return $this->action; + } + + /** + * Get the path to compare patterns + * + * @access public + * @return string + */ + public function getPath() + { + $path = substr($this->request->getUri(), strlen($this->helper->url->dir())); + + if ($this->request->getQueryString() !== '') { + $path = substr($path, 0, - strlen($this->request->getQueryString()) - 1); + } + + if ($path !== '' && $path{0} === '/') { + $path = substr($path, 1); + } + + return $path; + } + + /** + * Find controller/action from the route table or from get arguments + * + * @access public + */ + public function dispatch() + { + $controller = $this->request->getStringParam('controller'); + $action = $this->request->getStringParam('action'); + $plugin = $this->request->getStringParam('plugin'); + + if ($controller === '') { + $route = $this->route->findRoute($this->getPath()); + $controller = $route['controller']; + $action = $route['action']; + $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(); + } + + /** + * Check controller and action parameter + * + * @access public + * @param string $value + * @param string $default + * @return string + */ + public function sanitize($value, $default = '') + { + 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; + } +} |