first version

This commit is contained in:
Bastian Allgeier
2019-01-13 23:17:34 +01:00
commit 01277f79f2
595 changed files with 82913 additions and 0 deletions

206
kirby/src/Http/Cookie.php Executable file
View File

@@ -0,0 +1,206 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\Str;
/**
* This class makes cookie handling easy
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Cookie
{
/**
* Key to use for cookie signing
* @var string
*/
public static $key = 'KirbyHttpCookieKey';
/**
* Set a new cookie
*
* <code>
*
* cookie::set('mycookie', 'hello', ['lifetime' => 60]);
* // expires in 1 hour
*
* </code>
*
* @param string $key The name of the cookie
* @param string $value The cookie content
* @param array $options Array of options:
* lifetime, path, domain, secure, httpOnly
* @return boolean true: cookie was created,
* false: cookie creation failed
*/
public static function set(string $key, string $value, array $options = []): bool
{
// extract options
$lifetime = $options['lifetime'] ?? 0;
$path = $options['path'] ?? '/';
$domain = $options['domain'] ?? null;
$secure = $options['secure'] ?? false;
$httpOnly = $options['httpOnly'] ?? true;
// add an HMAC signature of the value
$value = static::hmac($value) . '+' . $value;
// store that thing in the cookie global
$_COOKIE[$key] = $value;
// store the cookie
return setcookie($key, $value, static::lifetime($lifetime), $path, $domain, $secure, $httpOnly);
}
/**
* Calculates the lifetime for a cookie
*
* @param int $minutes Number of minutes or timestamp
* @return int
*/
public static function lifetime(int $minutes): int
{
if ($minutes > 1000000000) {
// absolute timestamp
return $minutes;
} elseif ($minutes > 0) {
// minutes from now
return time() + ($minutes * 60);
} else {
return 0;
}
}
/**
* Stores a cookie forever
*
* <code>
*
* cookie::forever('mycookie', 'hello');
* // never expires
*
* </code>
*
* @param string $key The name of the cookie
* @param string $value The cookie content
* @param array $options Array of options:
* path, domain, secure, httpOnly
* @return boolean true: cookie was created,
* false: cookie creation failed
*/
public static function forever(string $key, string $value, array $options = []): bool
{
$options['lifetime'] = 253402214400; // 9999-12-31
return static::set($key, $value, $options);
}
/**
* Get a cookie value
*
* <code>
*
* cookie::get('mycookie', 'peter');
* // sample output: 'hello' or if the cookie is not set 'peter'
*
* </code>
*
* @param string|null $key The name of the cookie
* @param string|null $default The default value, which should be returned
* if the cookie has not been found
* @return mixed The found value
*/
public static function get(string $key = null, string $default = null)
{
if ($key === null) {
return $_COOKIE;
}
$value = $_COOKIE[$key] ?? null;
return empty($value) ? $default : static::parse($value);
}
/**
* Checks if a cookie exists
*
* @param string $key
* @return boolean
*/
public static function exists(string $key): bool
{
return static::get($key) !== null;
}
/**
* Creates a HMAC for the cookie value
* Used as a cookie signature to prevent easy tampering with cookie data
*
* @param string $value
* @return string
*/
protected static function hmac(string $value): string
{
return hash_hmac('sha1', $value, static::$key);
}
/**
* Parses the hashed value from a cookie
* and tries to extract the value
*
* @param string $string
* @return mixed
*/
protected static function parse(string $string)
{
// if no hash-value separator is present, we can't parse the value
if (strpos($string, '+') === false) {
return null;
}
// extract hash and value
$hash = Str::before($string, '+');
$value = Str::after($string, '+');
// if the hash or the value is missing at all return null
// $value can be an empty string, $hash can't be!
if (!is_string($hash) || $hash === '' || !is_string($value)) {
return null;
}
// compare the extracted hash with the hashed value
// don't accept value if the hash is invalid
if (hash_equals(static::hmac($value), $hash) !== true) {
return null;
}
return $value;
}
/**
* Remove a cookie
*
* <code>
*
* cookie::remove('mycookie');
* // mycookie is now gone
*
* </code>
*
* @param string $key The name of the cookie
* @return boolean true: the cookie has been removed,
* false: the cookie could not be removed
*/
public static function remove(string $key): bool
{
if (isset($_COOKIE[$key])) {
unset($_COOKIE[$key]);
return setcookie($key, '', 1, '/') && setcookie($key, false);
}
return false;
}
}

316
kirby/src/Http/Header.php Executable file
View File

@@ -0,0 +1,316 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\F;
/**
* Makes sending HTTP headers a breeze
*
* @package Kirby Toolkit
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
class Header
{
// configuration
public static $codes = [
// successful
'_200' => 'OK',
'_201' => 'Created',
'_202' => 'Accepted',
// redirection
'_300' => 'Multiple Choices',
'_301' => 'Moved Permanently',
'_302' => 'Found',
'_303' => 'See Other',
'_304' => 'Not Modified',
'_307' => 'Temporary Redirect',
'_308' => 'Permanent Redirect',
// client error
'_400' => 'Bad Request',
'_401' => 'Unauthorized',
'_402' => 'Payment Required',
'_403' => 'Forbidden',
'_404' => 'Not Found',
'_405' => 'Method Not Allowed',
'_406' => 'Not Acceptable',
'_410' => 'Gone',
'_418' => 'I\'m a teapot',
'_451' => 'Unavailable For Legal Reasons',
// server error
'_500' => 'Internal Server Error',
'_501' => 'Not Implemented',
'_502' => 'Bad Gateway',
'_503' => 'Service Unavailable',
'_504' => 'Gateway Time-out'
];
/**
* Sends a content type header
*
* @param string $mime
* @param string $charset
* @param boolean $send
* @return string|void
*/
public static function contentType(string $mime, string $charset = 'UTF-8', bool $send = true)
{
if ($found = F::extensionToMime($mime)) {
$mime = $found;
}
$header = 'Content-type: ' . $mime;
if (empty($charset) === false) {
$header .= '; charset=' . $charset;
}
if ($send === false) {
return $header;
}
header($header);
}
/**
* Creates headers by key and value
*
* @param string|array $key
* @param string|null $value
* @return string
*/
public static function create($key, string $value = null): string
{
if (is_array($key) === true) {
$headers = [];
foreach ($key as $k => $v) {
$headers[] = static::create($k, $v);
}
return implode("\r\n", $headers);
}
// prevent header injection by stripping any newline characters from single headers
return str_replace(["\r", "\n"], '', $key . ': ' . $value);
}
/**
* Shortcut for static::contentType()
*
* @param string $mime
* @param string $charset
* @param boolean $send
* @return string|void
*/
public static function type(string $mime, string $charset = 'UTF-8', bool $send = true)
{
return static::contentType($mime, $charset, $send);
}
/**
* Sends a status header
*
* Checks $code against a list of known status codes. To bypass this check
* and send a custom status code and message, use a $code string formatted
* as 3 digits followed by a space and a message, e.g. '999 Custom Status'.
*
* @param int|string $code The HTTP status code
* @param boolean $send If set to false the header will be returned instead
* @return string|void
*/
public static function status($code = null, bool $send = true)
{
$codes = static::$codes;
$protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
// allow full control over code and message
if (is_string($code) === true && preg_match('/^\d{3} \w.+$/', $code) === 1) {
$message = substr(rtrim($code), 4);
$code = substr($code, 0, 3);
} else {
$code = array_key_exists('_' . $code, $codes) === false ? 500 : $code;
$message = isset($codes['_' . $code]) ? $codes['_' . $code] : 'Something went wrong';
}
$header = $protocol . ' ' . $code . ' ' . $message;
if ($send === false) {
return $header;
}
// try to send the header
header($header);
}
/**
* Sends a 200 header
*
* @param boolean $send
* @return string|void
*/
public static function success(bool $send = true)
{
return static::status(200, $send);
}
/**
* Sends a 201 header
*
* @param boolean $send
* @return string|void
*/
public static function created(bool $send = true)
{
return static::status(201, $send);
}
/**
* Sends a 202 header
*
* @param boolean $send
* @return string|void
*/
public static function accepted(bool $send = true)
{
return static::status(202, $send);
}
/**
* Sends a 400 header
*
* @param boolean $send
* @return string|void
*/
public static function error(bool $send = true)
{
return static::status(400, $send);
}
/**
* Sends a 403 header
*
* @param boolean $send
* @return string|void
*/
public static function forbidden(bool $send = true)
{
return static::status(403, $send);
}
/**
* Sends a 404 header
*
* @param boolean $send
* @return string|void
*/
public static function notfound(bool $send = true)
{
return static::status(404, $send);
}
/**
* Sends a 404 header
*
* @param boolean $send
* @return string|void
*/
public static function missing(bool $send = true)
{
return static::status(404, $send);
}
/**
* Sends a 410 header
*
* @param boolean $send
* @return string|void
*/
public static function gone(bool $send = true)
{
return static::status(410, $send);
}
/**
* Sends a 500 header
*
* @param boolean $send
* @return string|void
*/
public static function panic(bool $send = true)
{
return static::status(500, $send);
}
/**
* Sends a 503 header
*
* @param boolean $send
* @return string|void
*/
public static function unavailable(bool $send = true)
{
return static::status(503, $send);
}
/**
* Sends a redirect header
*
* @param string $url
* @param int $code
* @param boolean $send
* @return string|void
*/
public static function redirect(string $url, int $code = 302, bool $send = true)
{
$status = static::status($code, false);
$location = 'Location:' . Url::unIdn($url);
if ($send !== true) {
return $status . "\r\n" . $location;
}
header($status);
header($location);
exit();
}
/**
* Sends download headers for anything that is downloadable
*
* @param array $params Check out the defaults array for available parameters
*/
public static function download(array $params = [])
{
$defaults = [
'name' => 'download',
'size' => false,
'mime' => 'application/force-download',
'modified' => time()
];
$options = array_merge($defaults, $params);
header('Pragma: public');
header('Expires: 0');
header('Last-Modified: '. gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT');
header('Content-Disposition: attachment; filename="' . $options['name'] . '"');
header('Content-Transfer-Encoding: binary');
static::contentType($options['mime']);
if ($options['size']) {
header('Content-Length: ' . $options['size']);
}
header('Connection: close');
}
}

27
kirby/src/Http/Idn.php Executable file
View File

@@ -0,0 +1,27 @@
<?php
namespace Kirby\Http;
use TrueBV\Punycode;
/**
* Handles Internationalized Domain Names
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Idn
{
public static function decode(string $domain)
{
return (new Punycode())->decode($domain);
}
public static function encode(string $domain)
{
return (new Punycode())->encode($domain);
}
}

145
kirby/src/Http/Params.php Executable file
View File

@@ -0,0 +1,145 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\Obj;
use Kirby\Toolkit\Str;
/**
* A wrapper around a URL params
* that converts it into a Kirby Obj for easier
* access of each param.
*/
class Params extends Query
{
/**
* @var null|string
*/
public static $separator;
/**
* Creates a new params object
*
* @param array|string $params
*/
public function __construct($params)
{
if (is_string($params) === true) {
$params = static::extract($params)['params'];
}
parent::__construct($params ?? []);
}
/**
* Extract the params from a string or array
*
* @param string|array|null $path
* @return array
*/
public static function extract($path = null): array
{
if (empty($path) === true) {
return [
'path' => null,
'params' => null,
'slash' => false
];
}
$slash = false;
if (is_string($path) === true) {
$slash = substr($path, -1, 1) === '/';
$path = Str::split($path, '/');
}
if (is_array($path) === true) {
$params = [];
$separator = static::separator();
foreach ($path as $index => $p) {
if (strpos($p, $separator) === false) {
continue;
}
$paramParts = Str::split($p, $separator);
$paramKey = $paramParts[0];
$paramValue = $paramParts[1] ?? null;
$params[$paramKey] = $paramValue;
unset($path[$index]);
}
return [
'path' => $path,
'params' => $params,
'slash' => $slash
];
}
return [
'path' => null,
'params' => null,
'slash' => false
];
}
/**
* Returns the param separator according
* to the operating system.
*
* Unix = ':'
* Windows = ';'
*
* @return string
*/
public static function separator(): string
{
if (static::$separator !== null) {
return static::$separator;
}
if (DIRECTORY_SEPARATOR === '/') {
return static::$separator = ':';
} else {
return static::$separator = ';';
}
}
/**
* Converts the params object to a params string
* which can then be used in the URL builder again
*
* @param boolean $leadingSlash
* @param boolean $trailingSlash
* @return string|null
*/
public function toString($leadingSlash = false, $trailingSlash = false): string
{
if ($this->isEmpty() === true) {
return '';
}
$params = [];
$separator = static::separator();
foreach ($this as $key => $value) {
if ($value !== null && $value !== '') {
$params[] = $key . $separator . $value;
}
}
if (empty($params) === true) {
return '';
}
$params = implode('/', $params);
$leadingSlash = $leadingSlash === true ? '/' : null;
$trailingSlash = $trailingSlash === true ? '/' : null;
return $leadingSlash . $params . $trailingSlash;
}
}

41
kirby/src/Http/Path.php Executable file
View File

@@ -0,0 +1,41 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Str;
/**
* A wrapper around an URL path
* that converts the path into a Kirby stack
*/
class Path extends Collection
{
public function __construct($items)
{
if (is_string($items) === true) {
$items = Str::split($items, '/');
}
parent::__construct($items ?? []);
}
public function __toString(): string
{
return $this->toString();
}
public function toString(bool $leadingSlash = false, bool $trailingSlash = false): string
{
if (empty($this->data) === true) {
return '';
}
$path = implode('/', $this->data);
$leadingSlash = $leadingSlash === true ? '/' : null;
$trailingSlash = $trailingSlash === true ? '/' : null;
return $leadingSlash . $path . $trailingSlash;
}
}

52
kirby/src/Http/Query.php Executable file
View File

@@ -0,0 +1,52 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\Obj;
/**
* A wrapper around a URL query string
* that converts it into a Kirby Obj for easier
* access of each query attribute.
*/
class Query extends Obj
{
public function __construct($query)
{
if (is_string($query) === true) {
parse_str(ltrim($query, '?'), $query);
}
parent::__construct($query ?? []);
}
public function isEmpty(): bool
{
return empty((array)$this) === true;
}
public function isNotEmpty(): bool
{
return empty((array)$this) === false;
}
public function __toString(): string
{
return $this->toString();
}
public function toString($questionMark = false): string
{
$query = http_build_query($this);
if (empty($query) === true) {
return '';
}
if ($questionMark === true) {
$query = '?' . $query;
}
return $query;
}
}

352
kirby/src/Http/Remote.php Executable file
View File

@@ -0,0 +1,352 @@
<?php
namespace Kirby\Http;
use Exception;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
/**
* A handy little class to handle
* all kinds of remote requests
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
class Remote
{
/**
* @var array
*/
public static $defaults = [
'agent' => null,
'body' => true,
'data' => [],
'encoding' => 'utf-8',
'file' => null,
'headers' => [],
'method' => 'GET',
'progress' => null,
'test' => false,
'timeout' => 10,
];
/**
* @var string
*/
public $content;
/**
* @var resource
*/
public $curl;
/**
* @var array
*/
public $curlopt = [];
/**
* @var int
*/
public $errorCode;
/**
* @var string
*/
public $errorMessage;
/**
* @var array
*/
public $headers = [];
/**
* @var array
*/
public $info = [];
/**
* @var array
*/
public $options = [];
/**
* Magic getter for request info data
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public function __call(string $method, array $arguments = [])
{
$method = str_replace('-', '_', Str::kebab($method));
return $this->info[$method] ?? null;
}
/**
* Constructor
*
* @param string $url
* @param array $options
*/
public function __construct(string $url, array $options = [])
{
// set all options
$this->options = array_merge(static::$defaults, $options);
// add the url
$this->options['url'] = $url;
// send the request
$this->fetch();
}
public static function __callStatic(string $method, array $arguments = [])
{
return new static($arguments[0], array_merge(['method' => strtoupper($method)], $arguments[1] ?? []));
}
/**
* Returns the http status code
*
* @return integer|null
*/
public function code(): ?int
{
return $this->info['http_code'] ?? null;
}
/**
* Returns the response content
*
* @return mixed
*/
public function content()
{
return $this->content;
}
/**
* Sets up all curl options and sends the request
*
* @return self
*/
public function fetch()
{
// curl options
$this->curlopt = [
CURLOPT_URL => $this->options['url'],
CURLOPT_ENCODING => $this->options['encoding'],
CURLOPT_CONNECTTIMEOUT => $this->options['timeout'],
CURLOPT_TIMEOUT => $this->options['timeout'],
CURLOPT_AUTOREFERER => true,
CURLOPT_RETURNTRANSFER => $this->options['body'],
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HEADER => false,
CURLOPT_HEADERFUNCTION => function ($curl, $header) {
$parts = Str::split($header, ':');
if (empty($parts[0]) === false && empty($parts[1]) === false) {
$key = array_shift($parts);
$this->headers[$key] = implode(':', $parts);
}
return strlen($header);
}
];
// add the progress
if (is_callable($this->options['progress']) === true) {
$this->curlopt[CURLOPT_NOPROGRESS] = false;
$this->curlopt[CURLOPT_PROGRESSFUNCTION] = $this->options['progress'];
}
// add all headers
if (empty($this->options['headers']) === false) {
$this->curlopt[CURLOPT_HTTPHEADER] = $this->options['headers'];
}
// add the user agent
if (empty($this->options['agent']) === false) {
$this->curlopt[CURLOPT_USERAGENT] = $this->options['agent'];
}
// do some request specific stuff
switch ($action = strtoupper($this->options['method'])) {
case 'POST':
$this->curlopt[CURLOPT_POST] = true;
$this->curlopt[CURLOPT_CUSTOMREQUEST] = 'POST';
$this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']);
break;
case 'PUT':
$this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PUT';
$this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']);
// put a file
if ($this->options['file']) {
$this->curlopt[CURLOPT_INFILE] = fopen($this->options['file'], 'r');
$this->curlopt[CURLOPT_INFILESIZE] = F::size($this->options['file']);
}
break;
case 'PATCH':
$this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PATCH';
$this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']);
break;
case 'DELETE':
$this->curlopt[CURLOPT_CUSTOMREQUEST] = 'DELETE';
$this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']);
break;
case 'HEAD':
$this->curlopt[CURLOPT_CUSTOMREQUEST] = 'HEAD';
$this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']);
$this->curlopt[CURLOPT_NOBODY] = true;
break;
}
if ($this->options['test'] === true) {
return $this;
}
// start a curl request
$this->curl = curl_init();
curl_setopt_array($this->curl, $this->curlopt);
$this->content = curl_exec($this->curl);
$this->info = curl_getinfo($this->curl);
$this->errorCode = curl_errno($this->curl);
$this->errorMessage = curl_error($this->curl);
if ($this->errorCode) {
throw new Exception($this->errorMessage, $this->errorCode);
}
curl_close($this->curl);
return $this;
}
/**
* Static method to send a GET request
*
* @param string $url
* @param array $params
* @return self
*/
public static function get(string $url, array $params = [])
{
$defaults = [
'method' => 'GET',
'data' => [],
];
$options = array_merge($defaults, $params);
$query = http_build_query($options['data']);
if (empty($query) === false) {
$url = Url::hasQuery($url) === true ? $url . '&' . $query : $url . '?' . $query;
}
// remove the data array from the options
unset($options['data']);
return new static($url, $options);
}
/**
* Returns all received headers
*
* @return array
*/
public function headers(): array
{
return $this->headers;
}
/**
* Returns the request info
*
* @return array
*/
public function info(): array
{
return $this->info;
}
/**
* Decode the response content
*
* @param bool $array decode as array or object
* @return array|stdClass
*/
public function json(bool $array = true)
{
return json_decode($this->content(), $array);
}
/**
* Returns the request method
*
* @return string
*/
public function method(): string
{
return $this->options['method'];
}
/**
* Returns all options which have been
* set for the current request
*
* @return array
*/
public function options(): array
{
return $this->options;
}
/**
* Internal method to handle post field data
*
* @param mixed $data
* @return mixed
*/
protected function postfields($data)
{
if (is_object($data) || is_array($data)) {
return http_build_query($data);
} else {
return $data;
}
}
/**
* Static method to init this class and send a request
*
* @param string $url
* @param array $params
* @return self
*/
public static function request(string $url, array $params = [])
{
return new static($url, $params);
}
/**
* Returns the request Url
*
* @return string
*/
public function url(): string
{
return $this->options['url'];
}
}

382
kirby/src/Http/Request.php Executable file
View File

@@ -0,0 +1,382 @@
<?php
namespace Kirby\Http;
use Kirby\Http\Request\Auth\BasicAuth;
use Kirby\Http\Request\Auth\BearerAuth;
use Kirby\Http\Request\Body;
use Kirby\Http\Request\Files;
use Kirby\Http\Request\Method;
use Kirby\Http\Request\Query;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
/**
* The Request class provides
* a simple API to inspect incoming
* requests.
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Request
{
/**
* The auth object if available
*
* @var BearerAuth|BasicAuth|false|null
*/
protected $auth;
/**
* The Body object is a wrapper around
* the request body, which parses the contents
* of the body and provides an API to fetch
* particular parts of the body
*
* Examples:
*
* `$request->body()->get('foo')`
*
* @var Body
*/
protected $body;
/**
* The Files object is a wrapper around
* the $_FILES global. It sanitizes the
* $_FILES array and provides an API to fetch
* individual files by key
*
* Examples:
*
* `$request->files()->get('upload')['size']`
* `$request->file('upload')['size']`
*
* @var Files
*/
protected $files;
/**
* The Method object is a tiny
* wrapper around the request method
* name, which will validate and sanitize
* the given name and always return
* its uppercase version.
*
* Examples:
*
* `$request->method()->name()`
* `$request->method()->is('post')`
*
* @var Method
*/
protected $method;
/**
* All options that have been passed to
* the request in the constructor
*
* @var array
*/
protected $options;
/**
* The Query object is a wrapper around
* the URL query string, which parses the
* string and provides a clean API to fetch
* particular parts of the query
*
* Examples:
*
* `$request->query()->get('foo')`
*
* @var Query
*/
protected $query;
/**
* Request URL object
*
* @var Uri
*/
protected $url;
/**
* Creates a new Request object
* You can either pass your own request
* data via the $options array or use
* the data from the incoming request.
*
* @param array $options
*/
public function __construct(array $options = [])
{
$this->options = $options;
$this->method = $options['method'] ?? $_SERVER['REQUEST_METHOD'] ?? 'GET';
if (isset($options['body']) === true) {
$this->body = new Body($options['body']);
}
if (isset($options['files']) === true) {
$this->files = new Files($options['files']);
}
if (isset($options['query']) === true) {
$this->query = new Query($options['query']);
}
if (isset($options['url']) === true) {
$this->url = new Uri($options['url']);
}
}
/**
* Improved var_dump output
*
* @return array
*/
public function __debuginfo(): array
{
return [
'body' => $this->body(),
'files' => $this->files(),
'method' => $this->method(),
'query' => $this->query(),
'url' => $this->url()->toString()
];
}
/**
* Detects ajax requests
*
* @return boolean
*/
public function ajax(): bool
{
return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
}
/**
* Returns the Auth object if authentication is set
*
* @return BasicAuth|BearerAuth|null
*/
public function auth()
{
if ($this->auth !== null) {
return $this->auth;
}
if ($auth = $this->options['auth'] ?? $this->header('authorization')) {
$type = Str::before($auth, ' ');
$token = Str::after($auth, ' ');
$class = 'Kirby\\Http\\Request\\Auth\\' . ucfirst($type) . 'Auth';
if (class_exists($class) === false) {
return $this->auth = false;
}
return $this->auth = new $class($token);
}
return $this->auth = false;
}
/**
* Returns the Body object
*
* @return Body
*/
public function body(): Body
{
return $this->body = $this->body ?? new Body();
}
/**
* Checks if the request has been made from the command line
*
* @return boolean
*/
public function cli(): bool
{
return Server::cli();
}
/**
* Returns a CSRF token if stored in a header or the query
*
* @return string|null
*/
public function csrf(): ?string
{
return $this->header('x-csrf') ?? $this->query()->get('csrf');
}
/**
* Returns the request input as array
*
* @return array
*/
public function data(): array
{
return array_merge($this->body()->toArray(), $this->query()->toArray());
}
/**
* Fetches a single file array
* from the Files object by key
*
* @param string $key
* @return array|null
*/
public function file(string $key)
{
return $this->files()->get($key);
}
/**
* Returns the Files object
*
* @return Files
*/
public function files(): Files
{
return $this->files = $this->files ?? new Files();
}
/**
* Returns any data field from the request
* if it exists
*
* @param string|null|array $key
* @param mixed $fallback
* @return mixed
*/
public function get($key = null, $fallback = null)
{
return A::get($this->data(), $key, $fallback);
}
/**
* Returns a header by key if it exists
*
* @param string $key
* @param mixed $fallback
* @return mixed
*/
public function header(string $key, $fallback = null)
{
$headers = array_change_key_case($this->headers());
return $headers[strtolower($key)] ?? $fallback;
}
/**
* Return all headers with polyfill for
* missing getallheaders function
*
* @return array
*/
public function headers(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) !== 'HTTP_' && substr($key, 0, 14) !== 'REDIRECT_HTTP_') {
continue;
}
// remove HTTP_
$key = str_replace(['REDIRECT_HTTP_', 'HTTP_'], '', $key);
// convert to lowercase
$key = strtolower($key);
// replace _ with spaces
$key = str_replace('_', ' ', $key);
// uppercase first char in each word
$key = ucwords($key);
// convert spaces to dashes
$key = str_replace(' ', '-', $key);
$headers[$key] = $value;
}
return $headers;
}
/**
* Checks if the given method name
* matches the name of the request method.
*
* @param string $method
* @return boolean
*/
public function is(string $method): bool
{
return strtoupper($this->method) === strtoupper($method);
}
/**
* Returns the request method
*
* @return string
*/
public function method(): string
{
return $this->method;
}
/**
* Shortcut to the Params object
*/
public function params()
{
return $this->url()->params();
}
/**
* Returns the Query object
*
* @return Query
*/
public function query(): Query
{
return $this->query = $this->query ?? new Query();
}
/**
* Checks for a valid SSL connection
*
* @return boolean
*/
public function ssl(): bool
{
return $this->url()->scheme() === 'https';
}
/**
* Returns the current Uri object.
* If you pass props you can safely modify
* the Url with new parameters without destroying
* the original object.
*
* @param array $props
* @return Uri
*/
public function url(array $props = null): Uri
{
if ($props !== null) {
return $this->url()->clone($props);
}
return $this->url = $this->url ?? Uri::current();
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Kirby\Http\Request\Auth;
use Kirby\Toolkit\Str;
/**
* Basic Authentication
*/
class BasicAuth extends BearerAuth
{
/**
* @var string
*/
protected $credentials;
/**
* @var string
*/
protected $password;
/**
* @var string
*/
protected $username;
/**
* @param string $token
*/
public function __construct(string $token)
{
parent::__construct($token);
$this->credentials = base64_decode($token);
$this->username = Str::before($this->credentials, ':');
$this->password = Str::after($this->credentials, ':');
}
/**
* Returns the entire unencoded credentials string
*
* @return string
*/
public function credentials(): string
{
return $this->credentials;
}
/**
* Returns the password
*
* @return string|null
*/
public function password(): ?string
{
return $this->password;
}
/**
* Returns the authentication type
*
* @return string
*/
public function type(): string
{
return 'basic';
}
/**
* Returns the username
*
* @return string|null
*/
public function username(): ?string
{
return $this->username;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Kirby\Http\Request\Auth;
/**
* Bearer Auth
*/
class BearerAuth
{
/**
* @var string
*/
protected $token;
/**
* Creates a new Bearer Auth object
*
* @param string $token
*/
public function __construct(string $token)
{
$this->token = $token;
}
/**
* Converts the object to a string
*
* @return string
*/
public function __toString(): string
{
return ucfirst($this->type()) . ' ' . $this->token();
}
/**
* Returns the authentication token
*
* @return string
*/
public function token(): string
{
return $this->token;
}
/**
* Returns the auth type
*
* @return string
*/
public function type(): string
{
return 'bearer';
}
}

129
kirby/src/Http/Request/Body.php Executable file
View File

@@ -0,0 +1,129 @@
<?php
namespace Kirby\Http\Request;
/**
* The Body class parses the
* request body and provides a nice
* interface to get values from
* structured bodies (json encoded or form data)
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Body
{
use Data;
/**
* The raw body content
*
* @var string|array
*/
protected $contents;
/**
* The parsed content as array
*
* @var array
*/
protected $data;
/**
* Creates a new request body object.
* You can pass your own array or string.
* If null is being passed, the class will
* fetch the body either from the $_POST global
* or from php://input.
*
* @param array|string|null $contents
*/
public function __construct($contents = null)
{
$this->contents = $contents;
}
/**
* Fetches the raw contents for the body
* or uses the passed contents.
*
* @return string|array
*/
public function contents()
{
if ($this->contents === null) {
if (empty($_POST) === false) {
$this->contents = $_POST;
} else {
$this->contents = file_get_contents('php://input');
}
}
return $this->contents;
}
/**
* Parses the raw contents once and caches
* the result. The parser will try to convert
* the body with the json decoder first and
* then run parse_str to get some results
* if the json decoder failed.
*
* @return array
*/
public function data(): array
{
if (is_array($this->data) === true) {
return $this->data;
}
$contents = $this->contents();
// return content which is already in array form
if (is_array($contents) === true) {
return $this->data = $contents;
}
// try to convert the body from json
$json = json_decode($contents, true);
if (is_array($json) === true) {
return $this->data = $json;
}
if (strstr($contents, '=') !== false) {
// try to parse the body as query string
parse_str($contents, $parsed);
if (is_array($parsed)) {
return $this->data = $parsed;
}
}
return $this->data = [];
}
/**
* Converts the data array back
* to a http query string
*
* @return string
*/
public function toString(): string
{
return http_build_query($this->data());
}
/**
* Magic string converter
*
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
}

85
kirby/src/Http/Request/Data.php Executable file
View File

@@ -0,0 +1,85 @@
<?php
namespace Kirby\Http\Request;
/**
* The Data Trait is being used in
* Query, Files and Body classes to
* provide unified get and data methods.
* Especially the get method is a bit more
* complex with the option to fetch single keys
* or entire arrays.
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
trait Data
{
/**
* Improved var_dump output
*
* @return array
*/
public function __debuginfo(): array
{
return $this->toArray();
}
/**
* The data provider method has to be
* implemented by each class using this Trait
* and has to return an associative array
* for the get method
*
* @return array
*/
abstract public function data(): array;
/**
* The get method is the heart and soul of this
* Trait. You can use it to fetch a single value
* of the data array by key or multiple values by
* passing an array of keys.
*
* @param string|array $key
* @param mixed|null $default
* @return mixed
*/
public function get($key, $default = null)
{
if (is_array($key) === true) {
$result = [];
foreach ($key as $k) {
$result[$k] = $this->get($k);
}
return $result;
}
return $this->data()[$key] ?? $default;
}
/**
* Returns the data array.
* This is basically an alias for Data::data()
*
* @return array
*/
public function toArray(): array
{
return $this->data();
}
/**
* Converts the data array to json
*
* @return string
*/
public function toJson(): string
{
return json_encode($this->data());
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Kirby\Http\Request;
/**
* The Files object sanitizes
* the input coming from the $_FILES
* global. Especially for multiple uploads
* for the same key, it will produce a more
* usable array.
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Files
{
use Data;
/**
* Sanitized array of all received files
*
* @var array
*/
protected $files;
/**
* Creates a new Files object
* Pass your own array to mock
* uploads.
*
* @param array|null $files
*/
public function __construct($files = null)
{
if ($files === null) {
$files = $_FILES;
}
$this->files = [];
foreach ($files as $key => $file) {
if (is_array($file['name'])) {
foreach ($file['name'] as $i => $name) {
$this->files[$key][] = [
'name' => $file['name'][$i] ?? null,
'type' => $file['type'][$i] ?? null,
'tmp_name' => $file['tmp_name'][$i] ?? null,
'error' => $file['error'][$i] ?? null,
'size' => $file['size'][$i] ?? null,
];
}
} else {
$this->files[$key] = $file;
}
}
}
/**
* The data method returns the files
* array. This is only needed to make
* the Data trait work for the Files::get($key)
* method.
*
* @return array
*/
public function data(): array
{
return $this->files;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Kirby\Http\Request;
/**
* The Query class helps to
* parse and inspect URL queries
* as part of the Request object
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Query
{
use Data;
/**
* The Query data array
*
* @var array|null
*/
protected $data;
/**
* Creates a new Query object.
* The passed data can be an array
* or a parsable query string. If
* null is passed, the current Query
* will be taken from $_GET
*
* @param array|string|null $data
*/
public function __construct($data = null)
{
if ($data === null) {
$this->data = $_GET;
} elseif (is_array($data)) {
$this->data = $data;
} else {
parse_str($data, $parsed);
$this->data = $parsed;
}
}
/**
* Returns the Query data as array
*
* @return array
*/
public function data(): array
{
return $this->data;
}
/**
* Converts the query data array
* back to a query string
*
* @return string
*/
public function toString(): string
{
return http_build_query($this->data());
}
/**
* Magic string converter
*
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
}

307
kirby/src/Http/Response.php Executable file
View File

@@ -0,0 +1,307 @@
<?php
namespace Kirby\Http;
use Exception;
use Throwable;
use Kirby\Toolkit\F;
/**
* Representation of an Http response,
* to simplify sending correct headers
* and Http status codes.
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Response
{
/**
* Store for all registered headers,
* which will be sent with the response
*
* @var array
*/
protected $headers = [];
/**
* The response body
*
* @var string
*/
protected $body;
/**
* The HTTP response code
*
* @var int
*/
protected $code;
/**
* The content type for the response
*
* @var string
*/
protected $type;
/**
* The content type charset
*
* @var string
*/
protected $charset = 'UTF-8';
/**
* Creates a new response object
*
* @param string $body
* @param string $type
* @param integer $code
*/
public function __construct($body = '', ?string $type = null, ?int $code = null, ?array $headers = null, ?string $charset = null)
{
// array construction
if (is_array($body) === true) {
$params = $body;
$body = $params['body'] ?? '';
$type = $params['type'] ?? $type;
$code = $params['code'] ?? $code;
$headers = $params['headers'] ?? $headers;
$charset = $params['charset'] ?? $charset;
}
// regular construction
$this->body = $body;
$this->type = $type ?? 'text/html';
$this->code = $code ?? 200;
$this->headers = $headers ?? [];
$this->charset = $charset ?? 'UTF-8';
// automatic mime type detection
if (strpos($this->type, '/') === false) {
$this->type = F::extensionToMime($this->type) ?? 'text/html';
}
}
/**
* Improved var_dump() output
*
* @return array
*/
public function __debuginfo(): array
{
return $this->toArray();
}
/**
* Makes it possible to convert the
* entire response object to a string
* to send the headers and print the body
*
* @return string
*/
public function __toString(): string
{
try {
return $this->send();
} catch (Throwable $e) {
return '';
}
}
/**
* Getter for the body
*
* @return string
*/
public function body(): string
{
return $this->body;
}
/**
* Getter for the content type charset
*
* @return string
*/
public function charset(): string
{
return $this->charset;
}
/**
* Getter for the HTTP status code
*
* @return int
*/
public function code(): int
{
return $this->code;
}
/**
* Creates a response that triggers
* a file download for the given file
*
* @param string $file
* @param string $filename
* @return self
*/
public static function download(string $file, string $filename = null)
{
if (file_exists($file) === false) {
throw new Exception('The file could not be found');
}
$filename = $filename ?? basename($file);
$modified = filemtime($file);
$body = file_get_contents($file);
$size = strlen($body);
return new static([
'body' => $body,
'type' => 'application/force-download',
'headers' => [
'Pragma' => 'public',
'Expires' => '0',
'Last-Modified' => gmdate('D, d M Y H:i:s', $modified) . ' GMT',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Content-Transfer-Encoding' => 'binary',
'Content-Length' => $size,
'Connection' => 'close'
]
]);
}
/**
* Creates a response for a file and
* sends the file content to the browser
*
* @return self
*/
public static function file(string $file)
{
return new static(F::read($file), F::mime($file));
}
/**
* Getter for single headers
*
* @param string $key Name of the header
* @return string|null
*/
public function header(string $key): ?string
{
return $this->headers[$key] ?? null;
}
/**
* Getter for all headers
*
* @return array
*/
public function headers(): array
{
return $this->headers;
}
/**
* Creates a json response with appropriate
* header and automatic conversion of arrays.
*
* @param string|array $body
* @param integer $code
* @param boolean $pretty
* @param array $headers
* @return self
*/
public static function json($body = '', ?int $code = null, ?bool $pretty = null, array $headers = [])
{
if (is_array($body) === true) {
$body = json_encode($body, $pretty === true ? JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES : null);
}
return new static([
'body' => $body,
'code' => $code,
'type' => 'application/json',
'headers' => $headers
]);
}
/**
* Creates a redirect response,
* which will send the visitor to the
* given location.
*
* @param string $location
* @param integer $code
* @return self
*/
public static function redirect(?string $location = null, ?int $code = null)
{
return new static([
'code' => $code ?? 302,
'headers' => [
'Location' => Url::unIdn($location ?? '/')
]
]);
}
/**
* Sends all registered headers and
* returns the response body
*
* @return string
*/
public function send(): string
{
// send the status response code
http_response_code($this->code());
// send all custom headers
foreach ($this->headers() as $key => $value) {
header($key . ': ' . $value);
}
// send the content type header
header('Content-Type:' . $this->type() . '; charset=' . $this->charset());
// print the response body
return $this->body();
}
/**
* Converts all relevant response attributes
* to an associative array for debugging,
* testing or whatever.
*
* @return array
*/
public function toArray(): array
{
return [
'type' => $this->type(),
'charset' => $this->charset(),
'code' => $this->code(),
'headers' => $this->headers(),
'body' => $this->body()
];
}
/**
* Getter for the content type
*
* @return string
*/
public function type(): string
{
return $this->type;
}
}

219
kirby/src/Http/Route.php Executable file
View File

@@ -0,0 +1,219 @@
<?php
namespace Kirby\Http;
use Closure;
use Exception;
/**
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Route
{
/**
* The callback action function
*
* @var Closure
*/
protected $action;
/**
* Listed of parsed arguments
*
* @var array
*/
protected $arguments = [];
/**
* An array of all passed attributes
*
* @var array
*/
protected $attributes = [];
/**
* The registered request method
*
* @var string
*/
protected $method;
/**
* The registered pattern
*
* @var string
*/
protected $pattern;
/**
* Wildcards, which can be used in
* Route patterns to make regular expressions
* a little more human
*
* @var array
*/
protected $wildcards = [
'required' => [
'(:num)' => '(-?[0-9]+)',
'(:alpha)' => '([a-zA-Z]+)',
'(:alphanum)' => '([a-zA-Z0-9]+)',
'(:any)' => '([a-zA-Z0-9\.\-_%= \+\@\(\)]+)',
'(:all)' => '(.*)',
],
'optional' => [
'/(:num?)' => '(?:/(-?[0-9]+)',
'/(:alpha?)' => '(?:/([a-zA-Z]+)',
'/(:alphanum?)' => '(?:/([a-zA-Z0-9]+)',
'/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%= \+\@\(\)]+)',
'/(:all?)' => '(?:/(.*)',
],
];
/**
* Magic getter for route attributes
*
* @param string $key
* @param array $arguments
* @return mixed
*/
public function __call(string $key, array $arguments = null)
{
return $this->attributes[$key] ?? null;
}
/**
* Creates a new Route object for the given
* pattern(s), method(s) and the callback action
*
* @param string|array $pattern
* @param string|array $method
* @param Closure $action
*/
public function __construct($pattern, $method = 'GET', Closure $action, array $attributes = [])
{
$this->action = $action;
$this->attributes = $attributes;
$this->method = $method;
$this->pattern = $this->regex(ltrim($pattern, '/'));
}
/**
* Getter for the action callback
*
* @return Closure
*/
public function action(): Closure
{
return $this->action;
}
/**
* Returns all parsed arguments
*
* @return array
*/
public function arguments()
{
return $this->arguments;
}
/**
* Getter for additional attributes
*
* @return array
*/
public function attributes(): array
{
return $this->attributes;
}
/**
* Getter for the method
*
* @return string
*/
public function method(): string
{
return $this->method;
}
/**
* Returns the route name if set
*
* @return string|null
*/
public function name(): ?string
{
return $this->attributes['name'] ?? null;
}
/**
* Getter for the pattern
*
* @return string
*/
public function pattern(): string
{
return $this->pattern;
}
/**
* Converts the pattern into a full regular
* expression by replacing all the wildcards
*
* @param string $pattern
* @return string
*/
public function regex(string $pattern): string
{
$search = array_keys($this->wildcards['optional']);
$replace = array_values($this->wildcards['optional']);
// For optional parameters, first translate the wildcards to their
// regex equivalent, sans the ")?" ending. We'll add the endings
// back on when we know the replacement count.
$pattern = str_replace($search, $replace, $pattern, $count);
if ($count > 0) {
$pattern .= str_repeat(')?', $count);
}
return strtr($pattern, $this->wildcards['required']);
}
/**
* Tries to match the path with the regular expression and
* extracts all arguments for the Route action
*
* @param string $pattern
* @param string $path
* @return array|false
*/
public function parse(string $pattern, string $path)
{
// check for direct matches
if ($pattern === $path) {
return $this->arguments = [];
}
// We only need to check routes with regular expression since all others
// would have been able to be matched by the search for literal matches
// we just did before we started searching.
if (strpos($pattern, '(') === false) {
return false;
}
// If we have a match we'll return all results
// from the preg without the full first match.
if (preg_match('#^' . $this->regex($pattern) . '$#u', $path, $parameters)) {
return $this->arguments = array_slice($parameters, 1);
}
return false;
}
}

135
kirby/src/Http/Router.php Executable file
View File

@@ -0,0 +1,135 @@
<?php
namespace Kirby\Http;
use Exception;
use InvalidArgumentException;
/**
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Router
{
/**
* Store for the current route,
* if one can be found
*
* @var Route|null
*/
protected $route;
/**
* All registered routes, sorted by
* their request method. This makes
* it faster to find the right route
* later.
*
* @var array
*/
protected $routes = [
'GET' => [],
'HEAD' => [],
'POST' => [],
'PUT' => [],
'DELETE' => [],
'CONNECT' => [],
'OPTIONS' => [],
'TRACE' => [],
'PATCH' => [],
];
/**
* Creates a new router object and
* registers all the given routes
*
* @param array $routes
*/
public function __construct(array $routes = [])
{
foreach ($routes as $props) {
if (isset($props['pattern'], $props['action']) === false) {
throw new InvalidArgumentException('Invalid route parameters');
}
$methods = array_map('trim', explode('|', strtoupper($props['method'] ?? 'GET')));
$patterns = is_array($props['pattern']) === false ? [$props['pattern']] : $props['pattern'];
if ($methods === ['ALL']) {
$methods = array_keys($this->routes);
}
foreach ($methods as $method) {
foreach ($patterns as $pattern) {
$this->routes[$method][] = new Route($pattern, $method, $props['action'], $props);
}
}
}
}
/**
* Calls the Router by path and method.
* This will try to find a Route object
* and then call the Route action with
* the appropriate arguments and a Result
* object.
*
* @param string $path
* @param string $method
* @return mixed
*/
public function call(string $path = '', string $method = 'GET')
{
return $this
->find($path, $method)
->action()
->call($this->route, ...$this->route->arguments());
}
/**
* Finds a Route object by path and method
* The Route's arguments method is used to
* find matches and return all the found
* arguments in the path.
*
* @param string $path
* @param string $method
* @return Route|null
*/
public function find(string $path, string $method)
{
if (isset($this->routes[$method]) === false) {
throw new InvalidArgumentException('Invalid routing method: ' . $method, 400);
}
// remove leading and trailing slashes
$path = trim($path, '/');
foreach ($this->routes[$method] as $route) {
$arguments = $route->parse($route->pattern(), $path);
if ($arguments !== false) {
return $this->route = $route;
}
}
throw new Exception('No route found for path: "' . $path . '" and request method: "' . $method . '"', 404);
}
/**
* Returns the current route.
* This will only return something,
* once Router::find() has been called
* and only if a route was found.
*
* @return Route|null
*/
public function route()
{
return $this->route;
}
}

170
kirby/src/Http/Server.php Executable file
View File

@@ -0,0 +1,170 @@
<?php
namespace Kirby\Http;
/**
* A set of methods that make it more convenient to get variables
* from the global server array
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Server
{
/**
* Cache for the cli status
*
* @var bool|null
*/
public static $cli;
/**
* Returns the server's IP address
*
* @return string
*/
public static function address(): string
{
return static::get('SERVER_ADDR');
}
/**
* Checks if the request is being served by the CLI
*
* @return boolean
*/
public static function cli(): bool
{
if (static::$cli !== null) {
return static::$cli;
}
if (defined('STDIN') === true) {
return static::$cli = true;
}
$term = getenv('TERM');
if (substr(PHP_SAPI, 0, 3) === 'cgi' && $term && $term !== 'unknown') {
return static::$cli = true;
}
return static::$cli = false;
}
/**
* Gets a value from the _SERVER array
*
* <code>
* Server::get('document_root');
* // sample output: /var/www/kirby
*
* Server::get();
* // returns the whole server array
* </code>
*
* @param mixed $key The key to look for. Pass false or null to
* return the entire server array.
* @param mixed $default Optional default value, which should be
* returned if no element has been found
* @return mixed
*/
public static function get($key = null, $default = null)
{
if ($key === null) {
return $_SERVER;
}
$key = strtoupper($key);
$value = $_SERVER[$key] ?? $default;
return static::sanitize($key, $value);
}
/**
* Help to sanitize some _SERVER keys
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public static function sanitize(string $key, $value)
{
switch ($key) {
case 'SERVER_ADDR':
case 'SERVER_NAME':
case 'HTTP_HOST':
case 'HTTP_X_FORWARDED_HOST':
$value = strip_tags($value);
$value = preg_replace('![^\w.:-]+!iu', '', $value);
$value = trim($value, '-');
$value = htmlspecialchars($value);
break;
case 'SERVER_PORT':
case 'HTTP_X_FORWARDED_PORT':
$value = intval(preg_replace('![^0-9]+!', '', $value));
break;
}
return $value;
}
/**
* Returns the correct port number
*
* @param bool $forwarded
* @return int
*/
public static function port(bool $forwarded = false): int
{
$port = $forwarded === true ? static::get('HTTP_X_FORWARDED_PORT') : null;
if (empty($port) === true) {
$port = static::get('SERVER_PORT');
}
return $port;
}
/**
* Checks for a https request
*
* @return boolean
*/
public static function https(): bool
{
if (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') {
return true;
} elseif (static::port() === 443) {
return true;
} elseif (in_array(static::get('HTTP_X_FORWARDED_PROTO'), ['https', 'https, http'])) {
return true;
} else {
return false;
}
}
/**
* Returns the correct host
*
* @param bool $forwarded
* @return string
*/
public static function host(bool $forwarded = false): string
{
$host = $forwarded === true ? static::get('HTTP_X_FORWARDED_HOST') : null;
if (empty($host) === true) {
$host = static::get('SERVER_NAME');
}
if (empty($host) === true) {
$host = static::get('SERVER_ADDR');
}
return explode(':', $host)[0];
}
}

548
kirby/src/Http/Uri.php Executable file
View File

@@ -0,0 +1,548 @@
<?php
namespace Kirby\Http;
use Throwable;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Properties;
use Kirby\Toolkit\Str;
/**
* Uri builder class
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Uri
{
use Properties;
/**
* Cache for the current Uri object
*
* @var Uri|null
*/
public static $current;
/**
* The fragment after the hash
*
* @var string|false
*/
protected $fragment;
/**
* The host address
*
* @var string
*/
protected $host;
/**
* The optional password for basic authentication
*
* @var string|false
*/
protected $password;
/**
* The optional list of params
*
* @var Params
*/
protected $params;
/**
* The optional path
*
* @var Path
*/
protected $path;
/**
* The optional port number
*
* @var int|false
*/
protected $port;
/**
* All original properties
*
* @var array
*/
protected $props;
/**
* The optional query string without leading ?
*
* @var Query
*/
protected $query;
/**
* https or http
*
* @var string
*/
protected $scheme = 'http';
/**
* @var boolean
*/
protected $slash = false;
/**
* The optional username for basic authentication
*
* @var string|false
*/
protected $username;
/**
* Magic caller to access all properties
*
* @param string $property
* @param array $arguments
* @return mixed
*/
public function __call(string $property, array $arguments = [])
{
return $this->$property ?? null;
}
/**
* Make sure that cloning also clones
* the path and query objects
*
* @return void
*/
public function __clone()
{
$this->path = clone $this->path;
$this->query = clone $this->query;
$this->params = clone $this->params;
}
/**
* Creates a new URI object
*
* @param array $props
* @param array $inject
*/
public function __construct($props = [], array $inject = [])
{
if (is_string($props) === true) {
$props = parse_url($props);
$props['username'] = $props['user'] ?? null;
$props['password'] = $props['pass'] ?? null;
$props = array_merge($props, $inject);
}
// parse the path and extract params
if (empty($props['path']) === false) {
$extract = Params::extract($props['path']);
$props['params'] = $props['params'] ?? $extract['params'];
$props['path'] = $extract['path'];
$props['slash'] = $props['slash'] ?? $extract['slash'];
}
$this->setProperties($this->props = $props);
}
/**
* Magic getter
*
* @param string $property
* @return mixed
*/
public function __get(string $property)
{
return $this->$property ?? null;
}
/**
* Magic setter
*
* @param string $property
* @param mixed $value
*/
public function __set(string $property, $value)
{
if (method_exists($this, 'set' . $property) === true) {
$this->{'set' . $property}($value);
}
}
/**
* Converts the URL object to string
*
* @return string
*/
public function __toString(): string
{
try {
return $this->toString();
} catch (Throwable $e) {
return '';
}
}
/**
* Returns the auth details (username:password)
*
* @return string|null
*/
public function auth()
{
$auth = trim($this->username . ':' . $this->password);
return $auth !== ':' ? $auth : null;
}
/**
* Returns the base url (scheme + host)
* without trailing slash
*
* @return string
*/
public function base()
{
if (empty($this->host) === true || $this->host === '/') {
return null;
}
$auth = $this->auth();
$base = $this->scheme ? $this->scheme . '://' : '';
if ($auth !== null) {
$base .= $auth . '@';
}
$base .= $this->host;
if ($this->port !== null && in_array($this->port, [80, 443]) === false) {
$base .= ':' . $this->port;
}
return $base;
}
/**
* Clones the Uri object and applies optional
* new props.
*
* @param array $props
* @return self
*/
public function clone(array $props = []): self
{
$clone = clone $this;
foreach ($props as $key => $value) {
$clone->__set($key, $value);
}
return $clone;
}
/**
* @param array $props
* @param boolean $forwarded
* @return self
*/
public static function current(array $props = [], bool $forwarded = false): self
{
if (static::$current !== null) {
return static::$current;
}
$uri = parse_url('http://getkirby.com' . Server::get('REQUEST_URI'));
$url = new static(array_merge([
'scheme' => Server::https() === true ? 'https' : 'http',
'host' => Server::host($forwarded),
'port' => Server::port($forwarded),
'path' => $uri['path'] ?? null,
'query' => $uri['query'] ?? null,
], $props));
return $url;
}
/**
* @return boolean
*/
public function hasFragment(): bool
{
return empty($this->fragment) === false;
}
/**
* @return boolean
*/
public function hasPath(): bool
{
return $this->path()->isNotEmpty();
}
/**
* @return boolean
*/
public function hasQuery(): bool
{
return $this->query()->isNotEmpty();
}
/**
* Tries to convert the internationalized host
* name to the human-readable UTF8 representation
*
* @return self
*/
public function idn(): self
{
if (empty($this->host) === false) {
$this->setHost(Idn::decode($this->host));
}
return $this;
}
/**
* Creates an Uri object for the URL to the index.php
* or any other executed script.
*
* @param array $props
* @param bool $forwarded
* @return string
*/
public static function index(array $props = [], bool $forwarded = false): self
{
if (Server::cli() === true) {
$path = null;
} else {
$path = Server::get('SCRIPT_NAME');
// replace Windows backslashes
$path = str_replace('\\', '/', $path);
// remove the script
$path = dirname($path);
// replace those fucking backslashes again
$path = str_replace('\\', '/', $path);
// remove the leading and trailing slashes
$path = trim($path, '/');
}
if ($path === '.') {
$path = null;
}
return static::current(array_merge($props, [
'path' => $path,
'query' => null,
'fragment' => null,
]), $forwarded);
}
/**
* Checks if the host exists
*
* @return bool
*/
public function isAbsolute(): bool
{
return empty($this->host) === false;
}
/**
* @param string|null $fragment
* @return self
*/
public function setFragment(string $fragment = null)
{
$this->fragment = $fragment ? ltrim($fragment, '#') : null;
return $this;
}
/**
* @param string $host
* @return self
*/
public function setHost(string $host = null): self
{
$this->host = $host;
return $this;
}
/**
* @param Params|string|array|null $path
* @return self
*/
public function setParams($params = null): self
{
$this->params = is_a($params, 'Kirby\Http\Params') === true ? $params : new Params($params);
return $this;
}
/**
* @param string|null $password
* @return self
*/
public function setPassword(string $password = null): self
{
$this->password = $password;
return $this;
}
/**
* @param Path|string|array|null $path
* @return self
*/
public function setPath($path = null): self
{
$this->path = is_a($path, 'Kirby\Http\Path') === true ? $path : new Path($path);
return $this;
}
/**
* @param int|null $port
* @return self
*/
public function setPort(int $port = null): self
{
if ($port === 0) {
$port = null;
}
if ($port !== null) {
if ($port < 1 || $port > 65535) {
throw new InvalidArgumentException('Invalid port format: ' . $port);
}
}
$this->port = $port;
return $this;
}
/**
* @param string|array|null $query
* @return self
*/
public function setQuery($query = null): self
{
$this->query = is_a($query, 'Kirby\Http\Query') === true ? $query : new Query($query);
return $this;
}
/**
* @param string $scheme
* @return self
*/
public function setScheme(string $scheme = null): self
{
if ($scheme !== null && in_array($scheme, ['http', 'https', 'ftp']) === false) {
throw new InvalidArgumentException('Invalid URL scheme: ' . $scheme);
}
$this->scheme = $scheme;
return $this;
}
/**
* Set if a trailing slash should be added to
* the path when the URI is being built
*
* @param bool $slash
* @return self
*/
public function setSlash(bool $slash = false): self
{
$this->slash = $slash;
return $this;
}
/**
* @param string|null $username
* @return self
*/
public function setUsername(string $username = null): self
{
$this->username = $username;
return $this;
}
/**
* Converts the Url object to an array
*
* @return array
*/
public function toArray(): array
{
$array = [];
foreach ($this->propertyData as $key => $value) {
$value = $this->$key;
if (is_object($value) === true) {
$value = $value->toArray();
}
$array[$key] = $value;
}
return $array;
}
public function toJson(...$arguments): string
{
return json_encode($this->toArray(), ...$arguments);
}
/**
* Returns the full URL as string
*
* @return string
*/
public function toString(): string
{
$url = $this->base();
$slash = true;
if (empty($url) === true) {
$url = '/';
$slash = false;
}
$path = $this->path->toString($slash) . $this->params->toString($slash);
if ($this->slash && $slash === true) {
$path .= '/';
}
$url .= $path;
$url .= $this->query->toString(true);
if (empty($this->fragment) === false) {
$url .= '#' . $this->fragment;
}
return $url;
}
/**
* Tries to convert a URL with an internationalized host
* name to the machine-readable Punycode representation
*
* @return self
*/
public function unIdn(): self
{
if (empty($this->host) === false) {
$this->setHost(Idn::encode($this->host));
}
return $this;
}
}

282
kirby/src/Http/Url.php Executable file
View File

@@ -0,0 +1,282 @@
<?php
namespace Kirby\Http;
use Exception;
use Kirby\Toolkit\Str;
/**
* Static URL tools
*/
class Url
{
/**
* The base Url to build absolute Urls from
*
* @var string
*/
public static $home = '/';
/**
* The current Uri object
*
* @var Uri
*/
public static $current = null;
/**
* Facade for all Uri object methods
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public static function __callStatic(string $method, $arguments)
{
return (new Uri($arguments[0] ?? static::current()))->$method(...array_slice($arguments, 1));
}
/**
* Url Builder
* Actually just a factory for `new Uri($parts)`
*
* @param array $parts
* @param string|null $url
* @return string
*/
public static function build(array $parts = [], string $url = null): string
{
return (new Uri($url ?? static::current()))->clone($parts);
}
/**
* Returns the current url with all bells and whistles
*
* @return string
*/
public static function current(): string
{
return static::$current = static::$current ?? static::toObject()->toString();
}
/**
* Returns the url for the current directory
*
* @return string
*/
public static function currentDir(): string
{
return dirname(static::current());
}
/**
* Tries to fix a broken url without protocol
*
* @param string $url
* @return string
*/
public static function fix(string $url = null)
{
// make sure to not touch absolute urls
return (!preg_match('!^(https|http|ftp)\:\/\/!i', $url)) ? 'http://' . $url : $url;
}
/**
* Returns the home url if defined
*
* @return string
*/
public static function home(): string
{
return static::$home;
}
/**
* Returns the url to the executed script
*
* @param array $props
* @param bool $forwarded
* @return string
*/
public static function index(array $props = [], bool $forwarded = false): string
{
return Uri::index($props, $forwarded)->toString();
}
/**
* Checks if an URL is absolute
*
* @return boolean
*/
public static function isAbsolute(string $url = null): bool
{
// matches the following groups of URLs:
// //example.com/uri
// http://example.com/uri, https://example.com/uri, ftp://example.com/uri
// mailto:example@example.com
return preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:)!i', $url) === 1;
}
/**
* Convert a relative path into an absolute URL
*
* @param string $path
* @param string $home
* @return string
*/
public static function makeAbsolute(string $path = null, string $home = null)
{
if ($path === '' || $path === '/' || $path === null) {
return $home ?? static::home();
}
if (substr($path, 0, 1) === '#') {
return $path;
}
if (static::isAbsolute($path)) {
return $path;
}
// build the full url
$path = ltrim($path, '/');
$home = $home ?? static::home();
if (empty($path) === true) {
return $home;
}
return $home === '/' ? '/' . $path : $home . '/' . $path;
}
/**
* Returns the path for the given url
*
* @param string|array|null $url
* @param bool $leadingSlash
* @param bool $trailingSlash
* @return mixed
*/
public static function path($url = null, bool $leadingSlash = false, bool $trailingSlash = false): string
{
return Url::toObject($url)->path()->toString($leadingSlash, $trailingSlash);
}
/**
* Returns the query for the given url
*
* @param string|array|null $url
* @return mixed
*/
public static function query($url = null): string
{
return Url::toObject($url)->query()->toString();
}
/**
* Return the last url the user has been on if detectable
*
* @return string
*/
public static function last(): string
{
return $_SERVER['HTTP_REFERER'] ?? '';
}
/**
* Shortens the Url by removing all unnecessary parts
*
* @param string $url
* @param boolean $length
* @param boolean $base
* @param string $rep
* @return string
*/
public static function short($url = null, $length = false, bool $base = false, string $rep = '…'): string
{
$uri = static::toObject($url);
$uri->fragment = null;
$uri->query = null;
$uri->password = null;
$uri->port = null;
$uri->scheme = null;
$uri->username = null;
// remove the trailing slash from the path
$uri->slash = false;
$url = $base ? $uri->base() : $uri->toString();
$url = str_replace('www.', '', $url);
return Str::short($url, $length, $rep);
}
/**
* Removes the path from the Url
*
* @param string $url
* @return string
*/
public static function stripPath($url = null): string
{
return static::toObject($url)->setPath(null)->toString();
}
/**
* Removes the query string from the Url
*
* @param string $url
* @return string
*/
public static function stripQuery($url = null): string
{
return static::toObject($url)->setQuery(null)->toString();
}
/**
* Removes the fragment (hash) from the Url
*
* @param string $url
* @return string
*/
public static function stripFragment($url = null): string
{
return static::toObject($url)->setFragment(null)->toString();
}
/**
* Smart resolver for internal and external urls
*
* @param string $path
* @param array $options
* @return string
*/
public static function to(string $path = null, array $options = null): string
{
// keep relative urls
if (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') {
return $path;
}
$url = static::makeAbsolute($path);
if ($options === null) {
return $url;
}
return (new Uri($url, $options))->toString();
}
/**
* Converts the Url to a Uri object
*
* @param string $url
* @return Uri
*/
public static function toObject($url = null)
{
return $url === null ? Uri::current() : new Uri($url);
}
}

215
kirby/src/Http/Visitor.php Executable file
View File

@@ -0,0 +1,215 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Mime;
use Kirby\Toolkit\Obj;
use Kirby\Toolkit\Str;
/**
* The Visitor class makes it easy to inspect information
* like the ip address, language, user agent and more
* of the current visitor
*
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link http://getkirby.com
* @copyright Bastian Allgeier
* @license MIT
*/
class Visitor
{
/**
* IP address
* @var string|null
*/
protected $ip;
/**
* user agent
* @var string|null
*/
protected $userAgent;
/**
* accepted language
* @var string|null
*/
protected $acceptedLanguage;
/**
* accepted mime type
* @var string|null
*/
protected $acceptedMimeType;
/**
* Creates a new visitor object.
* Optional arguments can be passed to
* modify the information about the visitor.
*
* By default everything is pulled from $_SERVER
*
* @param array $arguments
*/
public function __construct(array $arguments = [])
{
$this->ip($arguments['ip'] ?? getenv('REMOTE_ADDR'));
$this->userAgent($arguments['userAgent'] ?? $_SERVER['HTTP_USER_AGENT'] ?? '');
$this->acceptedLanguage($arguments['acceptedLanguage'] ?? $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
$this->acceptedMimeType($arguments['acceptedMimeType'] ?? $_SERVER['HTTP_ACCEPT'] ?? '');
}
/**
* Sets the accepted language if
* provided or returns the user's
* accepted language otherwise
*
* @param string|null $acceptedLanguage
* @return Obj|Visitor|null
*/
public function acceptedLanguage(string $acceptedLanguage = null)
{
if ($acceptedLanguage === null) {
return $this->acceptedLanguages()->first();
}
$this->acceptedLanguage = $acceptedLanguage;
return $this;
}
/**
* Returns an array of all accepted languages
* including their quality and locale
*
* @return Collection
*/
public function acceptedLanguages()
{
$accepted = Str::accepted($this->acceptedLanguage);
$languages = [];
foreach ($accepted as $language) {
$value = $language['value'];
$parts = Str::split($value, '-');
$code = isset($parts[0]) ? Str::lower($parts[0]) : null;
$region = isset($parts[1]) ? Str::upper($parts[1]) : null;
$locale = $region ? $code . '_' . $region : $code;
$languages[$locale] = new Obj([
'code' => $code,
'locale' => $locale,
'original' => $value,
'quality' => $language['quality'],
'region' => $region,
]);
}
return new Collection($languages);
}
/**
* Checks if the user accepts the given language
*
* @param string $code
* @return bool
*/
public function acceptsLanguage(string $code): bool
{
$mode = Str::contains($code, '_') === true ? 'locale' : 'code';
foreach ($this->acceptedLanguages() as $language) {
if ($language->$mode() === $code) {
return true;
}
}
return false;
}
/**
* Sets the accepted mime type if
* provided or returns the user's
* accepted mime type otherwise
*
* @param string|null $acceptedMimeType
* @return Obj|Visitor
*/
public function acceptedMimeType(string $acceptedMimeType = null)
{
if ($acceptedMimeType === null) {
return $this->acceptedMimeTypes()->first();
}
$this->acceptedMimeType = $acceptedMimeType;
return $this;
}
/**
* Returns a collection of all accepted mime types
*
* @return Collection
*/
public function acceptedMimeTypes()
{
$accepted = Str::accepted($this->acceptedMimeType);
$mimes = [];
foreach ($accepted as $mime) {
$mimes[$mime['value']] = new Obj([
'type' => $mime['value'],
'quality' => $mime['quality'],
]);
}
return new Collection($mimes);
}
/**
* Checks if the user accepts the given mime type
*
* @param string $mimeType
* @return boolean
*/
public function acceptsMimeType(string $mimeType): bool
{
return Mime::isAccepted($mimeType, $this->acceptedMimeType);
}
/**
* Sets the ip address if provided
* or returns the ip of the current
* visitor otherwise
*
* @param string|null $ip
* @return string|Visitor|null
*/
public function ip(string $ip = null)
{
if ($ip === null) {
return $this->ip;
}
$this->ip = $ip;
return $this;
}
/**
* Sets the user agent if provided
* or returns the user agent string of
* the current visitor otherwise
*
* @param string|null $userAgent
* @return string|Visitor|null
*/
public function userAgent(string $userAgent = null)
{
if ($userAgent === null) {
return $this->userAgent;
}
$this->userAgent = $userAgent;
return $this;
}
}