summaryrefslogtreecommitdiff
path: root/app/Core/Http
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-10-25 18:11:49 -0400
committerFrederic Guillot <fred@kanboard.net>2015-10-25 18:11:49 -0400
commita2ebc6c3b2ec3e420a03e36faf00c6e3bf3f25e7 (patch)
tree55bbe24de52dc14591033cb3f9265acfa74cdbb8 /app/Core/Http
parent6756ef2301a5f624941b947ec9effd34b467de9a (diff)
Move some classes to namespace Core\Http
Diffstat (limited to 'app/Core/Http')
-rw-r--r--app/Core/Http/Client.php163
-rw-r--r--app/Core/Http/Request.php243
-rw-r--r--app/Core/Http/Response.php273
-rw-r--r--app/Core/Http/Router.php230
4 files changed, 909 insertions, 0 deletions
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 @@
+<?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 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/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 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
+
+/**
+ * Request class
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class Request extends Base
+{
+ /**
+ * Get URL 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($_GET[$name]) ? $_GET[$name] : $default_value;
+ }
+
+ /**
+ * Get URL 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($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_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($_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 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use Kanboard\Core\Base;
+
+/**
+ * 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.'"');
+ }
+
+ /**
+ * 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($_SERVER['SERVER_PROTOCOL'].' '.$status_code);
+ }
+
+ /**
+ * Redirect to another URL
+ *
+ * @access public
+ * @param string $url Redirection URL
+ */
+ public function redirect($url)
+ {
+ if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
+ header('X-Ajax-Redirect: '.$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())
+ {
+ $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 @@
+<?php
+
+namespace Kanboard\Core\Http;
+
+use RuntimeException;
+use Kanboard\Core\Base;
+
+/**
+ * Router class
+ *
+ * @package http
+ * @author Frederic Guillot
+ */
+class Router extends Base
+{
+ /**
+ * Controller
+ *
+ * @access private
+ * @var string
+ */
+ private $controller = '';
+
+ /**
+ * Action
+ *
+ * @access private
+ * @var string
+ */
+ private $action = '';
+
+ /**
+ * Store routes for path lookup
+ *
+ * @access private
+ * @var array
+ */
+ private $paths = array();
+
+ /**
+ * Store routes for url lookup
+ *
+ * @access private
+ * @var array
+ */
+ private $urls = array();
+
+ /**
+ * Get action
+ *
+ * @access public
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->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}();
+ }
+}