From a2ebc6c3b2ec3e420a03e36faf00c6e3bf3f25e7 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 25 Oct 2015 18:11:49 -0400 Subject: Move some classes to namespace Core\Http --- app/Auth/RememberMe.php | 2 +- app/Core/Base.php | 8 +- app/Core/Http/Client.php | 163 ++++++++++++++++++++ app/Core/Http/Request.php | 243 ++++++++++++++++++++++++++++++ app/Core/Http/Response.php | 273 ++++++++++++++++++++++++++++++++++ app/Core/Http/Router.php | 230 ++++++++++++++++++++++++++++ app/Core/HttpClient.php | 147 ------------------ app/Core/Request.php | 241 ------------------------------ app/Core/Response.php | 271 --------------------------------- app/Core/Router.php | 229 ---------------------------- app/Core/Session.php | 1 + app/Helper/Url.php | 2 +- app/Model/Authentication.php | 2 +- app/ServiceProvider/ClassProvider.php | 12 +- app/Subscriber/AuthSubscriber.php | 2 +- tests/units/Core/Http/RouterTest.php | 81 ++++++++++ tests/units/Core/RouterTest.php | 81 ---------- 17 files changed, 1009 insertions(+), 979 deletions(-) create mode 100644 app/Core/Http/Client.php create mode 100644 app/Core/Http/Request.php create mode 100644 app/Core/Http/Response.php create mode 100644 app/Core/Http/Router.php delete mode 100644 app/Core/HttpClient.php delete mode 100644 app/Core/Request.php delete mode 100644 app/Core/Response.php delete mode 100644 app/Core/Router.php create mode 100644 tests/units/Core/Http/RouterTest.php delete mode 100644 tests/units/Core/RouterTest.php diff --git a/app/Auth/RememberMe.php b/app/Auth/RememberMe.php index 24f30a2c..fd8ed8bb 100644 --- a/app/Auth/RememberMe.php +++ b/app/Auth/RememberMe.php @@ -3,7 +3,7 @@ namespace Kanboard\Auth; use Kanboard\Core\Base; -use Kanboard\Core\Request; +use Kanboard\Core\Http\Request; use Kanboard\Event\AuthEvent; use Kanboard\Core\Security\Token; 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/Http/Client.php b/app/Core/Http/Client.php new file mode 100644 index 00000000..c6bf36a6 --- /dev/null +++ b/app/Core/Http/Client.php @@ -0,0 +1,163 @@ +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/Request.php b/app/Core/Http/Request.php new file mode 100644 index 00000000..9f89a6e2 --- /dev/null +++ b/app/Core/Http/Request.php @@ -0,0 +1,243 @@ +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($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) { + unset($_POST['csrf_token']); + return $_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($_FILES[$name])) { + return file_get_contents($_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($_FILES[$name]['tmp_name']) ? $_FILES[$name]['tmp_name'] : ''; + } + + /** + * Return true if the HTTP request is sent with the POST method + * + * @access public + * @return bool + */ + public function isPost() + { + return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['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 + * + * @static + * @access public + * @return boolean + */ + public static function isHTTPS() + { + return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + } + + /** + * 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 isset($_SERVER[$name]) ? $_SERVER[$name] : ''; + } + + /** + * Returns current request's query string, useful for redirecting + * + * @access public + * @return string + */ + public function getQueryString() + { + return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; + } + + /** + * Returns uri + * + * @access public + * @return string + */ + public function getUri() + { + return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + } + + /** + * Get the user agent + * + * @static + * @access public + * @return string + */ + public static function getUserAgent() + { + return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT']; + } + + /** + * Get the real IP address of the user + * + * @static + * @access public + * @param bool $only_public Return only public IP address + * @return string + */ + public static function getIpAddress($only_public = false) + { + $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 (isset($_SERVER[$key])) { + foreach (explode(',', $_SERVER[$key]) as $ip_address) { + $ip_address = trim($ip_address); + + if ($only_public) { + + // Return only public IP address + if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { + return $ip_address; + } + } else { + return $ip_address; + } + } + } + } + + return t('Unknown'); + } +} diff --git a/app/Core/Http/Response.php b/app/Core/Http/Response.php new file mode 100644 index 00000000..a793e58b --- /dev/null +++ b/app/Core/Http/Response.php @@ -0,0 +1,273 @@ +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()) + { + $policies['default-src'] = "'self'"; + $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 (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/Router.php b/app/Core/Http/Router.php new file mode 100644 index 00000000..0080b23a --- /dev/null +++ b/app/Core/Http/Router.php @@ -0,0 +1,230 @@ +action; + } + + /** + * Get controller + * + * @access public + * @return string + */ + public function getController() + { + return $this->controller; + } + + /** + * Get the path to compare patterns + * + * @access public + * @param string $uri + * @param string $query_string + * @return string + */ + public function getPath($uri, $query_string = '') + { + $path = substr($uri, strlen($this->helper->url->dir())); + + if (! empty($query_string)) { + $path = substr($path, 0, - strlen($query_string) - 1); + } + + if (! empty($path) && $path{0} === '/') { + $path = substr($path, 1); + } + + return $path; + } + + /** + * Add route + * + * @access public + * @param string $path + * @param string $controller + * @param string $action + * @param array $params + */ + public function addRoute($path, $controller, $action, array $params = array()) + { + $pattern = explode('/', $path); + + $this->paths[] = array( + 'pattern' => $pattern, + 'count' => count($pattern), + 'controller' => $controller, + 'action' => $action, + ); + + $this->urls[$controller][$action][] = array( + 'path' => $path, + 'params' => array_flip($params), + 'count' => count($params), + ); + } + + /** + * Find a route according to the given path + * + * @access public + * @param string $path + * @return array + */ + public function findRoute($path) + { + $parts = explode('/', $path); + $count = count($parts); + + foreach ($this->paths as $route) { + if ($count === $route['count']) { + $params = array(); + + for ($i = 0; $i < $count; $i++) { + if ($route['pattern'][$i]{0} === ':') { + $params[substr($route['pattern'][$i], 1)] = $parts[$i]; + } elseif ($route['pattern'][$i] !== $parts[$i]) { + break; + } + } + + if ($i === $count) { + $_GET = array_merge($_GET, $params); + return array($route['controller'], $route['action']); + } + } + } + + return array('app', 'index'); + } + + /** + * Find route url + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @return string + */ + public function findUrl($controller, $action, array $params = array()) + { + if (! isset($this->urls[$controller][$action])) { + return ''; + } + + foreach ($this->urls[$controller][$action] as $pattern) { + if (array_diff_key($params, $pattern['params']) === array()) { + $url = $pattern['path']; + $i = 0; + + foreach ($params as $variable => $value) { + $url = str_replace(':'.$variable, $value, $url); + $i++; + } + + if ($i === $pattern['count']) { + return $url; + } + } + } + + return ''; + } + + /** + * Check controller and action parameter + * + * @access public + * @param string $value Controller or action name + * @param string $default_value Default value if validation fail + * @return string + */ + public function sanitize($value, $default_value) + { + return ! preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $default_value : $value; + } + + /** + * Find controller/action from the route table or from get arguments + * + * @access public + * @param string $uri + * @param string $query_string + */ + public function dispatch($uri, $query_string = '') + { + if (! empty($_GET['controller']) && ! empty($_GET['action'])) { + $this->controller = $this->sanitize($_GET['controller'], 'app'); + $this->action = $this->sanitize($_GET['action'], 'index'); + $plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : ''; + } else { + list($this->controller, $this->action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes + $plugin = ''; + } + + $class = '\Kanboard\\'; + $class .= empty($plugin) ? 'Controller\\'.ucfirst($this->controller) : 'Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($this->controller); + + if (! class_exists($class) || ! method_exists($class, $this->action)) { + throw new RuntimeException('Controller or method not found for the given url!'); + } + + $instance = new $class($this->container); + $instance->beforeAction($this->controller, $this->action); + $instance->{$this->action}(); + } +} diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php deleted file mode 100644 index 7f4ea47a..00000000 --- a/app/Core/HttpClient.php +++ /dev/null @@ -1,147 +0,0 @@ -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 ''; - } - - $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; - } - - $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; - } -} diff --git a/app/Core/Request.php b/app/Core/Request.php deleted file mode 100644 index 0398760e..00000000 --- a/app/Core/Request.php +++ /dev/null @@ -1,241 +0,0 @@ -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($_POST) && ! empty($_POST['csrf_token']) && $this->token->validateCSRFToken($_POST['csrf_token'])) { - unset($_POST['csrf_token']); - return $_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($_FILES[$name])) { - return file_get_contents($_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($_FILES[$name]['tmp_name']) ? $_FILES[$name]['tmp_name'] : ''; - } - - /** - * Return true if the HTTP request is sent with the POST method - * - * @access public - * @return bool - */ - public function isPost() - { - return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['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 - * - * @static - * @access public - * @return boolean - */ - public static function isHTTPS() - { - return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; - } - - /** - * 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 isset($_SERVER[$name]) ? $_SERVER[$name] : ''; - } - - /** - * Returns current request's query string, useful for redirecting - * - * @access public - * @return string - */ - public function getQueryString() - { - return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; - } - - /** - * Returns uri - * - * @access public - * @return string - */ - public function getUri() - { - return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; - } - - /** - * Get the user agent - * - * @static - * @access public - * @return string - */ - public static function getUserAgent() - { - return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT']; - } - - /** - * Get the real IP address of the user - * - * @static - * @access public - * @param bool $only_public Return only public IP address - * @return string - */ - public static function getIpAddress($only_public = false) - { - $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 (isset($_SERVER[$key])) { - foreach (explode(',', $_SERVER[$key]) as $ip_address) { - $ip_address = trim($ip_address); - - if ($only_public) { - - // Return only public IP address - if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { - return $ip_address; - } - } else { - return $ip_address; - } - } - } - } - - return t('Unknown'); - } -} diff --git a/app/Core/Response.php b/app/Core/Response.php deleted file mode 100644 index 6788473a..00000000 --- a/app/Core/Response.php +++ /dev/null @@ -1,271 +0,0 @@ -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()) - { - $policies['default-src'] = "'self'"; - $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 (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/Router.php b/app/Core/Router.php deleted file mode 100644 index 843f5139..00000000 --- a/app/Core/Router.php +++ /dev/null @@ -1,229 +0,0 @@ -action; - } - - /** - * Get controller - * - * @access public - * @return string - */ - public function getController() - { - return $this->controller; - } - - /** - * Get the path to compare patterns - * - * @access public - * @param string $uri - * @param string $query_string - * @return string - */ - public function getPath($uri, $query_string = '') - { - $path = substr($uri, strlen($this->helper->url->dir())); - - if (! empty($query_string)) { - $path = substr($path, 0, - strlen($query_string) - 1); - } - - if (! empty($path) && $path{0} === '/') { - $path = substr($path, 1); - } - - return $path; - } - - /** - * Add route - * - * @access public - * @param string $path - * @param string $controller - * @param string $action - * @param array $params - */ - public function addRoute($path, $controller, $action, array $params = array()) - { - $pattern = explode('/', $path); - - $this->paths[] = array( - 'pattern' => $pattern, - 'count' => count($pattern), - 'controller' => $controller, - 'action' => $action, - ); - - $this->urls[$controller][$action][] = array( - 'path' => $path, - 'params' => array_flip($params), - 'count' => count($params), - ); - } - - /** - * Find a route according to the given path - * - * @access public - * @param string $path - * @return array - */ - public function findRoute($path) - { - $parts = explode('/', $path); - $count = count($parts); - - foreach ($this->paths as $route) { - if ($count === $route['count']) { - $params = array(); - - for ($i = 0; $i < $count; $i++) { - if ($route['pattern'][$i]{0} === ':') { - $params[substr($route['pattern'][$i], 1)] = $parts[$i]; - } elseif ($route['pattern'][$i] !== $parts[$i]) { - break; - } - } - - if ($i === $count) { - $_GET = array_merge($_GET, $params); - return array($route['controller'], $route['action']); - } - } - } - - return array('app', 'index'); - } - - /** - * Find route url - * - * @access public - * @param string $controller - * @param string $action - * @param array $params - * @return string - */ - public function findUrl($controller, $action, array $params = array()) - { - if (! isset($this->urls[$controller][$action])) { - return ''; - } - - foreach ($this->urls[$controller][$action] as $pattern) { - if (array_diff_key($params, $pattern['params']) === array()) { - $url = $pattern['path']; - $i = 0; - - foreach ($params as $variable => $value) { - $url = str_replace(':'.$variable, $value, $url); - $i++; - } - - if ($i === $pattern['count']) { - return $url; - } - } - } - - return ''; - } - - /** - * Check controller and action parameter - * - * @access public - * @param string $value Controller or action name - * @param string $default_value Default value if validation fail - * @return string - */ - public function sanitize($value, $default_value) - { - return ! preg_match('/^[a-zA-Z_0-9]+$/', $value) ? $default_value : $value; - } - - /** - * Find controller/action from the route table or from get arguments - * - * @access public - * @param string $uri - * @param string $query_string - */ - public function dispatch($uri, $query_string = '') - { - if (! empty($_GET['controller']) && ! empty($_GET['action'])) { - $this->controller = $this->sanitize($_GET['controller'], 'app'); - $this->action = $this->sanitize($_GET['action'], 'index'); - $plugin = ! empty($_GET['plugin']) ? $this->sanitize($_GET['plugin'], '') : ''; - } else { - list($this->controller, $this->action) = $this->findRoute($this->getPath($uri, $query_string)); // TODO: add plugin for routes - $plugin = ''; - } - - $class = '\Kanboard\\'; - $class .= empty($plugin) ? 'Controller\\'.ucfirst($this->controller) : 'Plugin\\'.ucfirst($plugin).'\Controller\\'.ucfirst($this->controller); - - if (! class_exists($class) || ! method_exists($class, $this->action)) { - throw new RuntimeException('Controller or method not found for the given url!'); - } - - $instance = new $class($this->container); - $instance->beforeAction($this->controller, $this->action); - $instance->{$this->action}(); - } -} 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/Url.php b/app/Helper/Url.php index e47256ba..edb26841 100644 --- a/app/Helper/Url.php +++ b/app/Helper/Url.php @@ -2,7 +2,7 @@ namespace Kanboard\Helper; -use Kanboard\Core\Request; +use Kanboard\Core\Http\Request; use Kanboard\Core\Base; /** 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/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index c1a59f85..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,13 +82,14 @@ class ClassProvider implements ServiceProviderInterface 'Core' => array( 'DateParser', 'Helper', - 'HttpClient', 'Lexer', + 'Session', + 'Template', + ), + 'Core\Http' => array( 'Request', 'Response', 'Router', - 'Session', - 'Template', ), 'Core\Cache' => array( 'MemoryCache', @@ -117,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/Http/RouterTest.php b/tests/units/Core/Http/RouterTest.php new file mode 100644 index 00000000..c2380247 --- /dev/null +++ b/tests/units/Core/Http/RouterTest.php @@ -0,0 +1,81 @@ +container); + + $this->assertEquals('PloP', $r->sanitize('PloP', 'default')); + $this->assertEquals('default', $r->sanitize('', 'default')); + $this->assertEquals('default', $r->sanitize('123-AB', 'default')); + $this->assertEquals('default', $r->sanitize('R&D', 'default')); + $this->assertEquals('Test123', $r->sanitize('Test123', 'default')); + $this->assertEquals('Test_123', $r->sanitize('Test_123', 'default')); + $this->assertEquals('userImport', $r->sanitize('userImport', 'default')); + } + + public function testPath() + { + $r = new Router($this->container); + + $this->assertEquals('a/b/c', $r->getPath('/a/b/c')); + $this->assertEquals('a/b/something', $r->getPath('/a/b/something?test=a', 'test=a')); + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['PHP_SELF'] = '/a/index.php'; + + $this->assertEquals('b/c', $r->getPath('/a/b/c')); + $this->assertEquals('b/c', $r->getPath('/a/b/c?e=f', 'e=f')); + } + + public function testFindRouteWithEmptyTable() + { + $r = new Router($this->container); + $this->assertEquals(array('app', 'index'), $r->findRoute('')); + $this->assertEquals(array('app', 'index'), $r->findRoute('/')); + } + + public function testFindRouteWithoutPlaceholders() + { + $r = new Router($this->container); + $r->addRoute('a/b', 'controller', 'action'); + $this->assertEquals(array('app', 'index'), $r->findRoute('a/b/c')); + $this->assertEquals(array('controller', 'action'), $r->findRoute('a/b')); + } + + public function testFindRouteWithPlaceholders() + { + $r = new Router($this->container); + $r->addRoute('a/:myvar1/b/:myvar2', 'controller', 'action'); + $this->assertEquals(array('app', 'index'), $r->findRoute('a/123/b')); + $this->assertEquals(array('controller', 'action'), $r->findRoute('a/456/b/789')); + $this->assertEquals(array('myvar1' => 456, 'myvar2' => 789), $_GET); + } + + public function testFindMultipleRoutes() + { + $r = new Router($this->container); + $r->addRoute('a/b', 'controller1', 'action1'); + $r->addRoute('a/b', 'duplicate', 'duplicate'); + $r->addRoute('a', 'controller2', 'action2'); + $this->assertEquals(array('controller1', 'action1'), $r->findRoute('a/b')); + $this->assertEquals(array('controller2', 'action2'), $r->findRoute('a')); + } + + public function testFindUrl() + { + $r = new Router($this->container); + $r->addRoute('a/b', 'controller1', 'action1'); + $r->addRoute('a/:myvar1/b/:myvar2', 'controller2', 'action2', array('myvar1', 'myvar2')); + + $this->assertEquals('a/1/b/2', $r->findUrl('controller2', 'action2', array('myvar1' => 1, 'myvar2' => 2))); + $this->assertEquals('', $r->findUrl('controller2', 'action2', array('myvar1' => 1))); + $this->assertEquals('a/b', $r->findUrl('controller1', 'action1')); + $this->assertEquals('', $r->findUrl('controller1', 'action2')); + } +} diff --git a/tests/units/Core/RouterTest.php b/tests/units/Core/RouterTest.php deleted file mode 100644 index 753e1204..00000000 --- a/tests/units/Core/RouterTest.php +++ /dev/null @@ -1,81 +0,0 @@ -container); - - $this->assertEquals('PloP', $r->sanitize('PloP', 'default')); - $this->assertEquals('default', $r->sanitize('', 'default')); - $this->assertEquals('default', $r->sanitize('123-AB', 'default')); - $this->assertEquals('default', $r->sanitize('R&D', 'default')); - $this->assertEquals('Test123', $r->sanitize('Test123', 'default')); - $this->assertEquals('Test_123', $r->sanitize('Test_123', 'default')); - $this->assertEquals('userImport', $r->sanitize('userImport', 'default')); - } - - public function testPath() - { - $r = new Router($this->container); - - $this->assertEquals('a/b/c', $r->getPath('/a/b/c')); - $this->assertEquals('a/b/something', $r->getPath('/a/b/something?test=a', 'test=a')); - - $_SERVER['REQUEST_METHOD'] = 'GET'; - $_SERVER['PHP_SELF'] = '/a/index.php'; - - $this->assertEquals('b/c', $r->getPath('/a/b/c')); - $this->assertEquals('b/c', $r->getPath('/a/b/c?e=f', 'e=f')); - } - - public function testFindRouteWithEmptyTable() - { - $r = new Router($this->container); - $this->assertEquals(array('app', 'index'), $r->findRoute('')); - $this->assertEquals(array('app', 'index'), $r->findRoute('/')); - } - - public function testFindRouteWithoutPlaceholders() - { - $r = new Router($this->container); - $r->addRoute('a/b', 'controller', 'action'); - $this->assertEquals(array('app', 'index'), $r->findRoute('a/b/c')); - $this->assertEquals(array('controller', 'action'), $r->findRoute('a/b')); - } - - public function testFindRouteWithPlaceholders() - { - $r = new Router($this->container); - $r->addRoute('a/:myvar1/b/:myvar2', 'controller', 'action'); - $this->assertEquals(array('app', 'index'), $r->findRoute('a/123/b')); - $this->assertEquals(array('controller', 'action'), $r->findRoute('a/456/b/789')); - $this->assertEquals(array('myvar1' => 456, 'myvar2' => 789), $_GET); - } - - public function testFindMultipleRoutes() - { - $r = new Router($this->container); - $r->addRoute('a/b', 'controller1', 'action1'); - $r->addRoute('a/b', 'duplicate', 'duplicate'); - $r->addRoute('a', 'controller2', 'action2'); - $this->assertEquals(array('controller1', 'action1'), $r->findRoute('a/b')); - $this->assertEquals(array('controller2', 'action2'), $r->findRoute('a')); - } - - public function testFindUrl() - { - $r = new Router($this->container); - $r->addRoute('a/b', 'controller1', 'action1'); - $r->addRoute('a/:myvar1/b/:myvar2', 'controller2', 'action2', array('myvar1', 'myvar2')); - - $this->assertEquals('a/1/b/2', $r->findUrl('controller2', 'action2', array('myvar1' => 1, 'myvar2' => 2))); - $this->assertEquals('', $r->findUrl('controller2', 'action2', array('myvar1' => 1))); - $this->assertEquals('a/b', $r->findUrl('controller1', 'action1')); - $this->assertEquals('', $r->findUrl('controller1', 'action2')); - } -} -- cgit v1.2.3