Move some classes to namespace Core\Http

This commit is contained in:
Frederic Guillot
2015-10-25 18:11:49 -04:00
parent 6756ef2301
commit a2ebc6c3b2
12 changed files with 68 additions and 38 deletions

163
app/Core/Http/Client.php Normal file
View File

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

243
app/Core/Http/Request.php Normal file
View File

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

273
app/Core/Http/Response.php Normal file
View File

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

230
app/Core/Http/Router.php Normal file
View File

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