看板初始化提交

This commit is contained in:
zephyr
2026-06-01 21:23:12 -07:00
commit 27411ebedc
1827 changed files with 192340 additions and 0 deletions
+451
View File
@@ -0,0 +1,451 @@
<?php
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
use Kanboard\Job\HttpAsyncJob;
/**
* HTTP client
*
* @package Kanboard\Core\Http
* @author Frederic Guillot
*/
class Client extends Base
{
/**
* HTTP client user agent
*
* @var string
*/
const HTTP_USER_AGENT = 'Kanboard';
/**
* Send a GET HTTP request
*
* @access public
* @param string $url
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return string
*/
public function get($url, array $headers = [], $raiseForErrors = false, $followRedirects = true)
{
return $this->doRequest('GET', $url, '', $headers, $raiseForErrors, $followRedirects);
}
/**
* Send a GET HTTP request and parse JSON response
*
* @access public
* @param string $url
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return array
*/
public function getJson($url, array $headers = [], $raiseForErrors = false, $followRedirects = true)
{
$response = $this->doRequest('GET', $url, '', array_merge(['Accept: application/json'], $headers), $raiseForErrors, $followRedirects);
return json_decode($response, true) ?: [];
}
/**
* Send a POST HTTP request encoded in JSON
*
* @access public
* @param string $url
* @param array $data
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return string
*/
public function postJson($url, array $data, array $headers = [], $raiseForErrors = false, $followRedirects = true)
{
return $this->doRequest(
'POST',
$url,
json_encode($data),
array_merge(['Content-type: application/json'], $headers),
$raiseForErrors,
$followRedirects
);
}
/**
* Send a POST HTTP request encoded in JSON (Fire and forget)
*
* @access public
* @param string $url
* @param array $data
* @param string[] $headers
* @param bool $raiseForErrors
*/
public function postJsonAsync($url, array $data, array $headers = [], $raiseForErrors = false)
{
$this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams(
'POST',
$url,
json_encode($data),
array_merge(['Content-type: application/json'], $headers),
$raiseForErrors
));
}
/**
* Send a POST HTTP request encoded in www-form-urlencoded
*
* @access public
* @param string $url
* @param array $data
* @param string[] $headers
* @param bool $raiseForErrors
* @return string
*/
public function postForm($url, array $data, array $headers = [], $raiseForErrors = false)
{
return $this->doRequest(
'POST',
$url,
http_build_query($data),
array_merge(['Content-type: application/x-www-form-urlencoded'], $headers),
$raiseForErrors
);
}
/**
* Send a POST HTTP request encoded in www-form-urlencoded (fire and forget)
*
* @access public
* @param string $url
* @param array $data
* @param string[] $headers
* @param bool $raiseForErrors
*/
public function postFormAsync($url, array $data, array $headers = [], $raiseForErrors = false)
{
$this->queueManager->push(HttpAsyncJob::getInstance($this->container)->withParams(
'POST',
$url,
http_build_query($data),
array_merge(['Content-type: application/x-www-form-urlencoded'], $headers),
$raiseForErrors
));
}
/**
* Make the HTTP request with cURL if detected, socket otherwise
*
* @access public
* @param string $method
* @param string $url
* @param string $content
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return string
*/
public function doRequest($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
{
$requestBody = '';
if (! empty($url)) {
if (function_exists('curl_version')) {
if (DEBUG) {
$this->logger->debug('HttpClient::doRequest: cURL detected');
}
$requestBody = $this->doRequestWithCurl($method, $url, $content, $headers, $raiseForErrors, $followRedirects);
} else {
if (DEBUG) {
$this->logger->debug('HttpClient::doRequest: using socket');
}
$requestBody = $this->doRequestWithSocket($method, $url, $content, $headers, $raiseForErrors, $followRedirects);
}
}
return $requestBody;
}
/**
* Make the HTTP request with socket
*
* @access private
* @param string $method
* @param string $url
* @param string $content
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return string
*/
private function doRequestWithSocket($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
{
$startTime = microtime(true);
$stream = @fopen(trim($url), 'r', false, stream_context_create($this->getContext($method, $content, $headers, $raiseForErrors, $followRedirects)));
if (! is_resource($stream)) {
$this->logger->error('HttpClient: request failed ('.$url.')');
if ($raiseForErrors) {
throw new ClientException('Unreachable URL: '.$url);
}
return '';
}
$body = stream_get_contents($stream);
$metadata = stream_get_meta_data($stream);
if ($raiseForErrors && array_key_exists('wrapper_data', $metadata)) {
$statusCode = $this->getStatusCode($metadata['wrapper_data']);
if ($statusCode >= 400) {
throw new InvalidStatusException('Request failed with status code '.$statusCode, $statusCode, $body);
}
}
if (DEBUG) {
$this->logger->debug('HttpClient: url='.$url);
$this->logger->debug('HttpClient: headers='.var_export($headers, true));
$this->logger->debug('HttpClient: payload='.$content);
$this->logger->debug('HttpClient: metadata='.var_export($metadata, true));
$this->logger->debug('HttpClient: body='.$body);
$this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime));
}
return $body;
}
/**
* Make the HTTP request with cURL
*
* @access private
* @param string $method
* @param string $url
* @param string $content
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return string
*/
private function doRequestWithCurl($method, $url, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
{
$startTime = microtime(true);
$curlSession = @curl_init();
curl_setopt($curlSession, CURLOPT_URL, trim($url));
curl_setopt($curlSession, CURLOPT_USERAGENT, self::HTTP_USER_AGENT);
curl_setopt($curlSession, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($curlSession, CURLOPT_TIMEOUT, HTTP_TIMEOUT);
curl_setopt($curlSession, CURLOPT_FORBID_REUSE, true);
curl_setopt($curlSession, CURLOPT_MAXREDIRS, $followRedirects ? HTTP_MAX_REDIRECTS : 0);
curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlSession, CURLOPT_FOLLOWLOCATION, $followRedirects);
if ('POST' === $method) {
curl_setopt($curlSession, CURLOPT_POST, true);
curl_setopt($curlSession, CURLOPT_POSTFIELDS, $content);
} elseif ('PUT' === $method) {
curl_setopt($curlSession, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($curlSession, CURLOPT_POST, true);
curl_setopt($curlSession, CURLOPT_POSTFIELDS, $content);
}
if (! empty($headers)) {
curl_setopt($curlSession, CURLOPT_HTTPHEADER, $headers);
}
if (HTTP_VERIFY_SSL_CERTIFICATE === false) {
curl_setopt($curlSession, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curlSession, CURLOPT_SSL_VERIFYPEER, false);
}
if (HTTP_PROXY_HOSTNAME) {
curl_setopt($curlSession, CURLOPT_PROXY, HTTP_PROXY_HOSTNAME);
curl_setopt($curlSession, CURLOPT_PROXYPORT, HTTP_PROXY_PORT);
curl_setopt($curlSession, CURLOPT_NOPROXY, HTTP_PROXY_EXCLUDE);
}
if (HTTP_PROXY_USERNAME) {
curl_setopt($curlSession, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
curl_setopt($curlSession, CURLOPT_PROXYUSERPWD, HTTP_PROXY_USERNAME.':'.HTTP_PROXY_PASSWORD);
}
$body = curl_exec($curlSession);
if ($body === false) {
$errorMsg = curl_error($curlSession);
curl_close($curlSession);
$this->logger->error('HttpClient: request failed ('.$url.' - '.$errorMsg.')');
if ($raiseForErrors) {
throw new ClientException('Unreachable URL: '.$url.' ('.$errorMsg.')');
}
return '';
}
if ($raiseForErrors) {
$statusCode = curl_getinfo($curlSession, CURLINFO_RESPONSE_CODE);
if ($statusCode >= 400) {
curl_close($curlSession);
throw new InvalidStatusException('Request failed with status code '.$statusCode, $statusCode, $body);
}
}
if (DEBUG) {
$this->logger->debug('HttpClient: url='.$url);
$this->logger->debug('HttpClient: headers='.var_export($headers, true));
$this->logger->debug('HttpClient: payload='.$content);
$this->logger->debug('HttpClient: metadata='.var_export(curl_getinfo($curlSession), true));
$this->logger->debug('HttpClient: body='.$body);
$this->logger->debug('HttpClient: executionTime='.(microtime(true) - $startTime));
}
curl_close($curlSession);
return $body;
}
/**
* Get stream context
*
* @access private
* @param string $method
* @param string $content
* @param string[] $headers
* @param bool $raiseForErrors
* @param bool $followRedirects
* @return array
*/
private function getContext($method, $content, array $headers, $raiseForErrors = false, $followRedirects = true)
{
$default_headers = [
'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 = [
'http' => [
'method' => $method,
'protocol_version' => 1.1,
'timeout' => HTTP_TIMEOUT,
'max_redirects' => $followRedirects ? HTTP_MAX_REDIRECTS : 0,
'follow_location' => $followRedirects ? 1 : 0,
'header' => implode("\r\n", $headers),
'content' => $content,
'ignore_errors' => $raiseForErrors,
]
];
if (HTTP_PROXY_HOSTNAME) {
$context['http']['proxy'] = 'tcp://'.HTTP_PROXY_HOSTNAME.':'.HTTP_PROXY_PORT;
$context['http']['request_fulluri'] = true;
}
if (HTTP_VERIFY_SSL_CERTIFICATE === false) {
$context['ssl'] = [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
];
}
return $context;
}
private function getStatusCode(array $lines)
{
$status = 200;
foreach ($lines as $line) {
if (strpos($line, 'HTTP/1') === 0) {
$status = (int) substr($line, 9, 3);
}
}
return $status;
}
/**
* Get backend used for making HTTP connections
*
* @access public
* @return string
*/
public static function backend()
{
return function_exists('curl_version') ? 'cURL' : 'socket';
}
/**
* Check if an IP address is private
*
* @access public
* @param string $ip
* @return bool
*/
public function isPrivateIpAddress($ip)
{
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
return false;
}
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
}
/**
* Check if a URL is private (RFC1918, localhost, etc.)
*
* @access public
* @param string $url
* @return bool
*/
public function isPrivateURL($url)
{
$parsedUrl = parse_url($url);
if (!isset($parsedUrl['scheme']) || !in_array(strtolower($parsedUrl['scheme']), ['http', 'https'], true)) {
return false;
}
if (!isset($parsedUrl['host'])) {
return false;
}
$host = trim($parsedUrl['host']);
if ($host === '') {
return false;
}
$ipv4Address = gethostbyname($host);
if ($this->isPrivateIpAddress($ipv4Address)) {
return true;
}
if (function_exists('dns_get_record')) {
$dnsRecords = @dns_get_record($host, DNS_AAAA);
if (is_array($dnsRecords)) {
foreach ($dnsRecords as $record) {
if (isset($record['type']) && $record['type'] === 'AAAA' && isset($record['ipv6'])) {
if ($this->isPrivateIpAddress($record['ipv6'])) {
return true;
}
}
}
}
}
return false;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Kanboard\Core\Http;
use Exception;
class ClientException extends Exception
{
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace Kanboard\Core\Http;
class InvalidStatusException extends ClientException
{
protected $statusCode = 0;
protected $body = '';
public function __construct($message, $statusCode, $body)
{
parent::__construct($message);
$this->statusCode = $statusCode;
$this->body = $body;
}
public function getStatusCode()
{
return $this->statusCode;
}
public function getBody()
{
return $this->body;
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
/**
* OAuth2 Client
*
* @package http
* @author Frederic Guillot
*/
class OAuth2 extends Base
{
protected $clientId;
protected $secret;
protected $callbackUrl;
protected $authUrl;
protected $tokenUrl;
protected $scopes;
protected $tokenType;
protected $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;
}
/**
* Generate OAuth2 state and return the token value
*
* @access public
* @return string
*/
public function getState()
{
if (! session_exists('oauthState')) {
session_set('oauthState', $this->token->getToken());
}
return session_get('oauthState');
}
/**
* Check the validity of the state (CSRF token)
*
* @access public
* @param string $state
* @return bool
*/
public function isValidateState($state)
{
return $state === $this->getState();
}
/**
* 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),
'state' => $this->getState(),
);
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',
'state' => $this->getState(),
);
$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 $this
*/
public function setAccessToken($token, $type = 'bearer')
{
$this->accessToken = $token;
$this->tokenType = $type;
return $this;
}
}
+120
View File
@@ -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(),
'',
$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(),
'',
$this->request->isHTTPS(),
true
);
}
}
+596
View File
@@ -0,0 +1,596 @@
<?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
* @param array $server
* @param array $get
* @param array $post
* @param array $files
* @param array $cookies
*/
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((string) $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->filterValues($this->post);
}
return array();
}
/**
* Get POST values without modification
*
* @return array
*/
public function getRawFormValues()
{
return $this->post;
}
/**
* Get POST value without modification
*
* @param $name
* @return mixed|null
*/
public function getRawValue($name)
{
return isset($this->post[$name]) ? $this->post[$name] : null;
}
/**
* 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
* @param bool $enforceContentType
* @return array
*/
public function getJson($enforceContentType = true)
{
if ($enforceContentType && ! $this->isJsonContentType()) {
return array();
}
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 request Content-Type is JSON
*
* @access public
* @return bool
*/
public function isJsonContentType()
{
$contentType = $this->getServerVariable('CONTENT_TYPE');
if ($contentType === '') {
$contentType = $this->getServerVariable('HTTP_CONTENT_TYPE');
}
return stripos($contentType, 'application/json') === 0;
}
/**
* 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()
{
if ($this->getServerVariable('HTTP_X_FORWARDED_PROTO') === 'https') {
return true;
}
return $this->getServerVariable('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
* @param array $trustedProxyNetworks
* @return string
*/
public function getRemoteUser(array $trustedProxyNetworks = [])
{
if (! $this->isTrustedProxy($trustedProxyNetworks)) {
return '';
}
return $this->getServerVariable(REVERSE_PROXY_USER_HEADER);
}
/**
* Get remote email
*
* @access public
* @param array $trustedProxyNetworks
* @return string
*/
public function getRemoteEmail(array $trustedProxyNetworks = [])
{
if (! $this->isTrustedProxy($trustedProxyNetworks)) {
return '';
}
return $this->getServerVariable(REVERSE_PROXY_EMAIL_HEADER);
}
/**
* Get remote user full name
*
* @access public
* @param array $trustedProxyNetworks
* @return string
*/
public function getRemoteName(array $trustedProxyNetworks = [])
{
if (! $this->isTrustedProxy($trustedProxyNetworks)) {
return '';
}
return $this->getServerVariable(REVERSE_PROXY_FULLNAME_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');
}
/**
* Check if a redirect URI is safe (relative path)
*
* @access public
* @param string $uri Redirect URI
* @return bool
*/
public function isSafeRedirectUri($uri)
{
$uri = trim($uri);
if ($uri === '') {
return false;
}
// Reject backslashes
if (str_contains($uri, '\\')) {
return false;
}
// Reject if it starts with // (protocol-relative)
if (str_starts_with($uri, '//')) {
return false;
}
// Reject if it does not start with a slash (relative path)
if (! str_starts_with($uri, '/')) {
return false;
}
$parsedUrl = parse_url($uri);
if ($parsedUrl === false) {
return false;
}
// Reject if it contains a scheme or host (partial or full URL)
if (isset($parsedUrl['scheme']) || isset($parsedUrl['host'])) {
return false;
}
return true;
}
/**
* 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 client IP address
*
* It returns the proxy IP address if the request is sent through a reverse proxy or the direct client IP address otherwise.
*
* @access public
* @return string
*/
public function getClientIpAddress()
{
return $this->getServerVariable('REMOTE_ADDR');
}
/**
* Get the real user IP address considering trusted proxy headers and networks
*
* @access public
* @param array $trustedProxyHeaders List of trusted proxy headers (default: TRUSTED_PROXY_HEADERS constant)
* @param array $trustedProxyNetworks List of trusted proxy networks (default: TRUSTED_PROXY_NETWORKS constant)
* @return string
*/
public function getIpAddress(array $trustedProxyHeaders = [], array $trustedProxyNetworks = [])
{
$trustedProxyHeaders = array_filter(array_map('trim', $trustedProxyHeaders ?: explode(',', TRUSTED_PROXY_HEADERS)));
$useProxyHeaders = ! empty($trustedProxyHeaders) && $this->isTrustedProxy($trustedProxyNetworks);
$keys = $useProxyHeaders ? $trustedProxyHeaders : [];
foreach ($keys as $key) {
if ($this->getServerVariable($key) !== '') {
foreach (explode(',', $this->server[$key]) as $ipAddress) {
$ipAddress = trim($ipAddress);
if (filter_var($ipAddress, FILTER_VALIDATE_IP)) {
return $ipAddress;
}
}
}
}
return $this->getClientIpAddress();
}
/**
* 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] : '';
}
protected function filterValues(array $values)
{
foreach ($values as $key => $value) {
// IE11 Workaround when submitting multipart/form-data
if (strpos($key, '-----------------------------') === 0) {
unset($values[$key]);
}
}
return $values;
}
/**
* Check if an IP address belongs to a trusted proxy network
*
* @access public
* @param array $trustedProxyNetworks
* @return bool
*/
public function isTrustedProxy(array $trustedProxyNetworks = [])
{
$ipAddress = $this->getClientIpAddress();
if ($ipAddress === '') {
return false;
}
$trustedProxyNetworks = array_filter(array_map('trim', $trustedProxyNetworks ?: explode(',', TRUSTED_PROXY_NETWORKS)));
if (empty($trustedProxyNetworks)) {
return false;
}
$this->logger->debug('Checking if IP address {ip} belongs to trusted proxy networks: {networks}', ['ip' => $ipAddress, 'networks' => implode(', ', $trustedProxyNetworks)]);
return $this->isIpInNetworks($ipAddress, $trustedProxyNetworks);
}
/**
* Check if an IP belongs to any of the provided networks
*
* @access protected
* @param string $ipAddress
* @param array $networks
* @return bool
*/
protected function isIpInNetworks($ipAddress, array $networks)
{
if (! filter_var($ipAddress, FILTER_VALIDATE_IP)) {
return false;
}
$ipBinary = inet_pton($ipAddress);
foreach ($networks as $network) {
if ($network === '') {
continue;
}
$mask = null;
if (strpos($network, '/') !== false) {
list($networkAddress, $mask) = explode('/', $network, 2);
} else {
$networkAddress = $network;
}
if (! filter_var($networkAddress, FILTER_VALIDATE_IP)) {
continue;
}
$networkBinary = inet_pton($networkAddress);
if ($networkBinary === false || strlen($networkBinary) !== strlen($ipBinary)) {
continue;
}
$maxMask = strlen($networkBinary) * 8;
$mask = ($mask === null || $mask === '') ? $maxMask : max(0, min((int) $mask, $maxMask));
if ($this->ipMatchesNetwork($ipBinary, $networkBinary, $mask)) {
return true;
}
}
return false;
}
/**
* Perform a binary comparison between an IP and a network mask
*
* @access protected
* @param string $ipBinary
* @param string $networkBinary
* @param int $mask
* @return bool
*/
protected function ipMatchesNetwork($ipBinary, $networkBinary, $mask)
{
if ($mask === 0) {
return true;
}
$bytes = (int) floor($mask / 8);
$bits = $mask % 8;
if ($bytes > 0 && strncmp($ipBinary, $networkBinary, $bytes) !== 0) {
return false;
}
if ($bits === 0) {
return true;
}
$maskByte = ~((1 << (8 - $bits)) - 1) & 0xFF;
$ipByte = ord($ipBinary[$bytes]);
$networkByte = ord($networkBinary[$bytes]);
return ($ipByte & $maskByte) === ($networkByte & $maskByte);
}
}
+419
View File
@@ -0,0 +1,419 @@
<?php
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
use Kanboard\Core\Csv;
/**
* Response class
*
* @package http
* @author Frederic Guillot
*/
class Response extends Base
{
private $httpStatusCode = 200;
private $httpHeaders = array();
private $httpBody = '';
private $responseSent = false;
/**
* Return true if the response have been sent to the user agent
*
* @access public
* @return bool
*/
public function isResponseAlreadySent()
{
return $this->responseSent;
}
/**
* Set HTTP status code
*
* @access public
* @param integer $statusCode
* @return $this
*/
public function withStatusCode($statusCode)
{
$this->httpStatusCode = $statusCode;
return $this;
}
/**
* Set HTTP header
*
* @access public
* @param string $header
* @param string $value
* @return $this
*/
public function withHeader($header, $value)
{
$this->httpHeaders[$header] = $value;
return $this;
}
/**
* Set content type header
*
* @access public
* @param string $value
* @return $this
*/
public function withContentType($value)
{
$this->httpHeaders['Content-Type'] = $value;
return $this;
}
/**
* Set default security headers
*
* @access public
* @return $this
*/
public function withSecurityHeaders()
{
$this->httpHeaders['X-Content-Type-Options'] = 'nosniff';
$this->httpHeaders['X-XSS-Protection'] = '1; mode=block';
return $this;
}
/**
* Set header Content-Security-Policy
*
* @access public
* @param array $policies
* @return $this
*/
public function withContentSecurityPolicy(array $policies = array())
{
$values = '';
foreach ($policies as $policy => $acl) {
$values .= $policy.' '.trim($acl).'; ';
}
$this->withHeader('Content-Security-Policy', $values);
return $this;
}
/**
* Set header X-Frame-Options
*
* @access public
* @return $this
*/
public function withXframe()
{
$this->withHeader('X-Frame-Options', 'DENY');
return $this;
}
/**
* Set header Strict-Transport-Security (only if we use HTTPS)
*
* @access public
* @return $this
*/
public function withStrictTransportSecurity()
{
if ($this->request->isHTTPS()) {
$this->withHeader('Strict-Transport-Security', 'max-age=31536000');
}
return $this;
}
/**
* Add P3P headers for Internet Explorer
*
* @access public
* @return $this
*/
public function withP3P()
{
$this->withHeader('P3P', 'CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');
return $this;
}
/**
* Set HTTP response body
*
* @access public
* @param string $body
* @return $this
*/
public function withBody($body)
{
$this->httpBody = $body;
return $this;
}
/**
* Send headers to cache a resource
*
* @access public
* @param integer $duration
* @param string $etag
* @return $this
*/
public function withCache($duration, $etag = '')
{
$this
->withHeader('Pragma', 'cache')
->withHeader('Expires', gmdate('D, d M Y H:i:s', time() + $duration) . ' GMT')
->withHeader('Cache-Control', 'public, max-age=' . $duration)
;
if ($etag) {
$this->withHeader('ETag', '"' . $etag . '"');
}
return $this;
}
/**
* Send no cache headers
*
* @access public
* @return $this
*/
public function withoutCache()
{
$this->withHeader('Pragma', 'no-cache');
$this->withHeader('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT');
return $this;
}
/**
* Force the browser to download an attachment
*
* @access public
* @param string $filename
* @return $this
*/
public function withFileDownload($filename)
{
$this->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
$this->withHeader('Content-Transfer-Encoding', 'binary');
$this->withHeader('Content-Type', 'application/octet-stream');
return $this;
}
/**
* Send headers and body
*
* @access public
*/
public function send()
{
$this->responseSent = true;
if ($this->httpStatusCode !== 200) {
header('Status: '.$this->httpStatusCode);
header($this->request->getServerVariable('SERVER_PROTOCOL').' '.$this->httpStatusCode);
}
foreach ($this->httpHeaders as $header => $value) {
header($header.': '.$value);
}
if (! empty($this->httpBody)) {
echo $this->httpBody;
}
}
/**
* Send a custom HTTP status code
*
* @access public
* @param integer $statusCode
*/
public function status($statusCode)
{
$this->withStatusCode($statusCode);
$this->send();
}
/**
* 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()) {
$this->withHeader('X-Ajax-Redirect', $self ? 'self' : $url);
} else {
$this->withHeader('Location', $url);
}
$this->send();
}
/**
* Send a HTML response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function html($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/html; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a text response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function text($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/plain; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a CSV response
*
* @access public
* @param array $data Data to serialize in csv
* @param bool $addBOM Add BOM header
*/
public function csv(array $data, $addBOM = false)
{
$this->withoutCache();
$this->withContentType('text/csv; charset=utf-8');
$this->send();
Csv::output($data, $addBOM);
}
/**
* Send a Json response
*
* @access public
* @param array $data Data to serialize in json
* @param integer $statusCode HTTP status code
*/
public function json(array $data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('application/json');
$this->withoutCache();
$this->withBody(json_encode($data));
$this->send();
}
/**
* Send a XML response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function xml($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/xml; charset=utf-8');
$this->withoutCache();
$this->withBody($data);
$this->send();
}
/**
* Send a javascript response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function js($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/javascript; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a css response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function css($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/css; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a binary response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function binary($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withoutCache();
$this->withHeader('Content-Transfer-Encoding', 'binary');
$this->withContentType('application/octet-stream');
$this->withBody($data);
$this->send();
}
/**
* Send a iCal response
*
* @access public
* @param string $data
* @param integer $statusCode
*/
public function ical($data, $statusCode = 200)
{
$this->withStatusCode($statusCode);
$this->withContentType('text/calendar; charset=utf-8');
$this->withBody($data);
$this->send();
}
/**
* Send a PDF response
*
* @access public
* @param string $data
* @param integer $statusCode
* @param string $fileName
*/
public function pdf($data, int $statusCode = 200, string $fileName = "")
{
$this->withStatusCode($statusCode);
$this->withContentType('application/pdf');
if (!empty($fileName)) {
$this->httpHeaders["content-disposition"] = "attachment; filename=".$fileName;
}
$this->withBody($data);
$this->send();
}
}
+188
View File
@@ -0,0 +1,188 @@
<?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)] = urldecode($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' => 'DashboardController',
'action' => 'show',
'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) {
$value = urlencode($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;
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace Kanboard\Core\Http;
use Kanboard\Core\Base;
/**
* Route Dispatcher
*
* @package http
* @author Frederic Guillot
*/
class Router extends Base
{
const DEFAULT_CONTROLLER = 'DashboardController';
const DEFAULT_METHOD = 'show';
/**
* Plugin name
*
* @access private
* @var string
*/
private $currentPluginName = '';
/**
* Controller
*
* @access private
* @var string
*/
private $currentControllerName = '';
/**
* Action
*
* @access private
* @var string
*/
private $currentActionName = '';
/**
* Get plugin name
*
* @access public
* @return string
*/
public function getPlugin()
{
return $this->currentPluginName;
}
/**
* Get controller
*
* @access public
* @return string
*/
public function getController()
{
return $this->currentControllerName;
}
/**
* Get action
*
* @access public
* @return string
*/
public function getAction()
{
return $this->currentActionName;
}
/**
* 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->currentControllerName = ucfirst($this->sanitize($controller, self::DEFAULT_CONTROLLER));
$this->currentActionName = $this->sanitize($action, self::DEFAULT_METHOD);
$this->currentPluginName = ucfirst($this->sanitize($plugin));
}
/**
* 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;
}
}