看板初始化提交
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Kanboard\Core\Http;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ClientException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user