1
0

downgrade to kirby v3

This commit is contained in:
Philip Wagner
2024-09-01 10:47:15 +02:00
parent a4b2aece7b
commit af86acb7a1
1085 changed files with 54743 additions and 65042 deletions

View File

@@ -19,8 +19,9 @@ class Cookie
{
/**
* Key to use for cookie signing
* @var string
*/
public static string $key = 'KirbyHttpCookieKey';
public static $key = 'KirbyHttpCookieKey';
/**
* Set a new cookie
@@ -39,11 +40,8 @@ class Cookie
* @return bool true: cookie was created,
* false: cookie creation failed
*/
public static function set(
string $key,
string $value,
array $options = []
): bool {
public static function set(string $key, string $value, array $options = []): bool
{
// modify CMS caching behavior
static::trackUsage($key);
@@ -62,31 +60,27 @@ class Cookie
$_COOKIE[$key] = $value;
// store the cookie
return setcookie(
$key,
$value,
compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite')
);
$options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite');
return setcookie($key, $value, $options);
}
/**
* Calculates the lifetime for a cookie
*
* @param int $minutes Number of minutes or timestamp
* @return int
*/
public static function lifetime(int $minutes): int
{
// absolute timestamp
if ($minutes > 1000000000) {
// absolute timestamp
return $minutes;
}
// minutes from now
if ($minutes > 0) {
} elseif ($minutes > 0) {
// minutes from now
return time() + ($minutes * 60);
} else {
return 0;
}
return 0;
}
/**
@@ -106,11 +100,8 @@ class Cookie
* @return bool true: cookie was created,
* false: cookie creation failed
*/
public static function forever(
string $key,
string $value,
array $options = []
): bool {
public static function forever(string $key, string $value, array $options = []): bool
{
// 9999-12-31 if supported (lower on 32-bit servers)
$options['lifetime'] = min(253402214400, PHP_INT_MAX);
return static::set($key, $value, $options);
@@ -120,19 +111,19 @@ class Cookie
* 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 string|array|null The found value
* @return mixed The found value
*/
public static function get(
string|null $key = null,
string|null $default = null
): string|array|null {
public static function get(string $key = null, string $default = null)
{
if ($key === null) {
return $_COOKIE;
}
@@ -140,15 +131,15 @@ class Cookie
// modify CMS caching behavior
static::trackUsage($key);
if ($value = $_COOKIE[$key] ?? null) {
return static::parse($value);
}
return $default;
$value = $_COOKIE[$key] ?? null;
return empty($value) ? $default : static::parse($value);
}
/**
* Checks if a cookie exists
*
* @param string $key
* @return bool
*/
public static function exists(string $key): bool
{
@@ -158,6 +149,9 @@ class Cookie
/**
* 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
{
@@ -167,8 +161,11 @@ class Cookie
/**
* Parses the hashed value from a cookie
* and tries to extract the value
*
* @param string $string
* @return mixed
*/
protected static function parse(string $string): string|null
protected static function parse(string $string)
{
// if no hash-value separator is present, we can't parse the value
if (strpos($string, '+') === false) {
@@ -181,7 +178,7 @@ class Cookie
// if the hash or the value is missing at all return null
// $value can be an empty string, $hash can't be!
if ($hash === '') {
if (!is_string($hash) || $hash === '' || !is_string($value)) {
return null;
}
@@ -210,7 +207,7 @@ class Cookie
*/
public static function remove(string $key): bool
{
if (isset($_COOKIE[$key]) === true) {
if (isset($_COOKIE[$key])) {
unset($_COOKIE[$key]);
return setcookie($key, '', 1, '/') && setcookie($key, false);
}
@@ -224,11 +221,17 @@ class Cookie
* this ensures that the response is only cached for visitors who don't
* have this cookie set;
* https://github.com/getkirby/kirby/issues/4423#issuecomment-1166300526
*
* @param string $key
* @return void
*/
protected static function trackUsage(string $key): void
{
// lazily request the instance for non-CMS use cases
$kirby = App::instance(null, true);
$kirby?->response()->usesCookie($key);
if ($kirby) {
$kirby->response()->usesCookie($key);
}
}
}

View File

@@ -3,6 +3,7 @@
namespace Kirby\Http;
use Kirby\Cms\App;
use Kirby\Cms\Helpers;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Filesystem\F;
use Kirby\Toolkit\A;
@@ -25,110 +26,143 @@ class Environment
{
/**
* Full base URL object
*
* @var \Kirby\Http\Uri
*/
protected Uri $baseUri;
protected $baseUri;
/**
* Full base URL
*
* @var string
*/
protected string $baseUrl;
protected $baseUrl;
/**
* Whether the request is being served by the CLI
*
* @var bool
*/
protected bool $cli;
protected $cli;
/**
* Current host name
*
* @var string
*/
protected string|null $host;
protected $host;
/**
* Whether the HTTPS protocol is used
*
* @var bool
*/
protected bool $https;
protected $https;
/**
* Sanitized `$_SERVER` data
*
* @var array
*/
protected array $info;
protected $info;
/**
* Current server's IP address
*
* @var string
*/
protected string|null $ip;
protected $ip;
/**
* Whether the site is behind a reverse proxy;
* `null` if not known (fixed allowed URL setup)
*
* @var bool|null
*/
protected bool|null $isBehindProxy;
protected $isBehindProxy;
/**
* URI path to the base
*
* @var string
*/
protected string $path;
protected $path;
/**
* Port number in the site URL
*
* @var int|null
*/
protected int|null $port;
protected $port;
/**
* Intermediary value of the port
* extracted from the host name
*
* @var int|null
*/
protected int|null $portInHost = null;
protected $portInHost;
/**
* Uri object for the full request URI.
* It is a combination of the base URL and `REQUEST_URI`
*
* @var \Kirby\Http\Uri
*/
protected Uri $requestUri;
protected $requestUri;
/**
* Full request URL
*
* @var string
*/
protected string $requestUrl;
protected $requestUrl;
/**
* Path to the php script within the
* document root without the
* filename of the script
*
* @var string
*/
protected string $scriptPath;
protected $scriptPath;
/**
* Class constructor
*
* @param array|null $options
* @param array|null $info Optional override for `$_SERVER`
*/
public function __construct(
array|null $options = null,
array|null $info = null
) {
public function __construct(?array $options = null, ?array $info = null)
{
$this->detect($options, $info);
}
/**
* Returns the server's IP address
* @see ::ip
*
* @see static::ip
* @return string|null
*/
public function address(): string|null
public function address(): ?string
{
return $this->ip();
}
/**
* Returns the full base URL object
*
* @return \Kirby\Http\Uri
*/
public function baseUri(): Uri
public function baseUri()
{
return $this->baseUri;
}
/**
* Returns the full base URL
*
* @return string
*/
public function baseUrl(): string
{
@@ -137,6 +171,8 @@ class Environment
/**
* Checks if the request is being served by the CLI
*
* @return bool
*/
public function cli(): bool
{
@@ -150,19 +186,17 @@ class Environment
* the stored information and re-detect the
* environment if necessary.
*
* @param array|null $options
* @param array|null $info Optional override for `$_SERVER`
* @return array
*/
public function detect(
array $options = null,
array $info = null
): array {
$defaults = [
public function detect(array $options = null, array $info = null): array
{
$info ??= $_SERVER;
$options = array_merge([
'cli' => null,
'allowed' => null
];
$info ??= $_SERVER;
$options = array_merge($defaults, $options ?? []);
], $options ?? []);
$this->info = static::sanitize($info);
$this->cli = $this->detectCli($options['cli']);
@@ -174,6 +208,18 @@ class Environment
$this->path = $this->detectPath($this->scriptPath);
$this->port = null;
// keep Server flags compatible for now
// TODO: remove in 3.8.0
// @codeCoverageIgnoreStart
if (is_int($options['allowed']) === true) {
Helpers::deprecated('
Using `Server::` constants for the `allowed` option has been deprecated and support will be removed in 3.8.0. Use one of the following instead: a single fixed URL, an array of allowed URLs to match dynamically, `*` wildcard to match dynamically even from insecure headers, or `true` to match automtically from safe server variables.
');
$options['allowed'] = $this->detectAllowedFromFlag($options['allowed']);
}
// @codeCoverageIgnoreEnd
// insecure auto-detection
if ($options['allowed'] === '*' || $options['allowed'] === ['*']) {
$this->detectAuto(true);
@@ -200,8 +246,11 @@ class Environment
/**
* Sets the host name, port, path and protocol from the
* fixed list of allowed URLs
*
* @param array|string $allowed
* @return void
*/
protected function detectAllowed(array|string $allowed): void
protected function detectAllowed($allowed): void
{
$allowed = A::wrap($allowed);
@@ -241,9 +290,9 @@ class Environment
$uri = new Uri($url, ['slash' => false]);
// the current environment is allowed,
// stop before the exception below is thrown
if ($uri->toString() === $this->baseUrl) {
// the current environment is allowed,
// stop before the exception below is thrown
return;
}
}
@@ -251,10 +300,34 @@ class Environment
throw new InvalidArgumentException('The environment is not allowed');
}
/**
* The URL option receives a set of Server constant flags
*
* Server::HOST_FROM_SERVER
* Server::HOST_FROM_SERVER | Server::HOST_ALLOW_EMPTY
* Server::HOST_FROM_HEADER
* Server::HOST_FROM_HEADER | Server::HOST_ALLOW_EMPTY
* @todo Remove in 3.8.0
*
* @param int $flags
* @return string|null
*/
protected function detectAllowedFromFlag(int $flags): ?string
{
// allow host detection from host headers
if ($flags & Server::HOST_FROM_HEADER) {
return '*';
}
// detect host only from server name
return null;
}
/**
* Sets the host name, port and protocol without configuration
*
* @param bool $insecure Include the `Host`, `Forwarded` and `X-Forwarded-*` headers in the search
* @return void
*/
protected function detectAuto(bool $insecure = false): void
{
@@ -291,8 +364,10 @@ class Environment
/**
* Builds the base URL based on the
* given environment params
*
* @return \Kirby\Http\Uri
*/
protected function detectBaseUri(): Uri
protected function detectBaseUri()
{
$this->baseUri = new Uri([
'host' => $this->host,
@@ -310,8 +385,9 @@ class Environment
* Detects if the request is served by the CLI
*
* @param bool|null $override Set to a boolean to override detection (for testing)
* @return bool
*/
protected function detectCli(bool|null $override = null): bool
protected function detectCli(?bool $override = null): bool
{
if (is_bool($override) === true) {
return $override;
@@ -322,18 +398,9 @@ class Environment
}
// @codeCoverageIgnoreStart
$sapi = php_sapi_name();
if ($sapi === 'cli') {
return true;
}
$term = getenv('TERM');
if (
substr($sapi, 0, 3) === 'cgi' &&
$term &&
$term !== 'unknown'
) {
if (substr(PHP_SAPI, 0, 3) === 'cgi' && $term && $term !== 'unknown') {
return true;
}
@@ -344,6 +411,8 @@ class Environment
/**
* Detects the host, protocol, port and client IP
* from the `Forwarded` and `X-Forwarded-*` headers
*
* @return array
*/
protected function detectForwarded(): array
{
@@ -355,7 +424,8 @@ class Environment
];
// prefer the standardized `Forwarded` header if defined
if ($forwarded = $this->get('HTTP_FORWARDED')) {
$forwarded = $this->get('HTTP_FORWARDED');
if ($forwarded) {
// only use the first (outermost) proxy by using the first set of values
// before the first comma (but only a comma outside of quotes)
if (Str::contains($forwarded, ',') === true) {
@@ -391,8 +461,8 @@ class Environment
$data['https'] = $this->detectHttpsProtocol($fields['proto']);
}
if ($data['https'] === true) {
$data['port'] ??= 443;
if ($data['port'] === null && $data['https'] === true) {
$data['port'] = 443;
}
$data['for'] = $parts['for'] ?? null;
@@ -412,8 +482,10 @@ class Environment
/**
* Detects the host name of the reverse proxy
* from the `X-Forwarded-Host` header
*
* @return string|null
*/
protected function detectForwardedHost(): string|null
protected function detectForwardedHost(): ?string
{
$host = $this->get('HTTP_X_FORWARDED_HOST');
$parts = $this->detectPortInHost($host);
@@ -426,6 +498,8 @@ class Environment
/**
* Detects the protocol of the reverse proxy from the
* `X-Forwarded-SSL` or `X-Forwarded-Proto` header
*
* @return bool
*/
protected function detectForwardedHttps(): bool
{
@@ -445,8 +519,9 @@ class Environment
* `X-Forwarded-Host` or `X-Forwarded-Port` header
*
* @param bool $https Whether HTTPS was detected
* @return int|null
*/
protected function detectForwardedPort(bool $https): int|null
protected function detectForwardedPort(bool $https): ?int
{
// based on forwarded port
$port = $this->get('HTTP_X_FORWARDED_PORT');
@@ -472,11 +547,10 @@ class Environment
* Detects the host name from various headers
*
* @param bool $insecure Include the `Host` header in the search
* @return string|null
*/
protected function detectHost(bool $insecure = false): string|null
protected function detectHost(bool $insecure = false): ?string
{
$hosts = [];
if ($insecure === true) {
$hosts[] = $this->get('HTTP_HOST');
}
@@ -497,6 +571,8 @@ class Environment
/**
* Detects the HTTPS status
*
* @return bool
*/
protected function detectHttps(): bool
{
@@ -509,8 +585,11 @@ class Environment
/**
* Normalizes the HTTPS status into a boolean
*
* @param string|bool|null|int $value
* @return bool
*/
protected function detectHttpsOn(string|int|bool|null $value): bool
protected function detectHttpsOn($value): bool
{
// off can mean many things :)
$off = ['off', null, '', 0, '0', false, 'false', -1, '-1'];
@@ -520,30 +599,36 @@ class Environment
/**
* Detects the HTTPS status from a `X-Forwarded-Proto` string
*
* @param string|null $protocol
* @return bool
*/
protected function detectHttpsProtocol(string|null $protocol = null): bool
protected function detectHttpsProtocol(?string $protocol = null): bool
{
if ($protocol === null) {
return false;
}
$protocols = ['https', 'https, http'];
return in_array(strtolower($protocol), $protocols) === true;
return in_array(strtolower($protocol), ['https', 'https, http']) === true;
}
/**
* Detects the server's IP address
*
* @return string|null
*/
protected function detectIp(): string|null
protected function detectIp(): ?string
{
return $this->get('SERVER_ADDR');
}
/**
* Detects the URI path unless in CLI mode
*
* @param string|null $path
* @return string
*/
protected function detectPath(string|null $path = null): string
protected function detectPath(?string $path = null): string
{
if ($this->cli === true) {
return '';
@@ -554,8 +639,10 @@ class Environment
/**
* Detects the port from various sources
*
* @return int|null
*/
protected function detectPort(): int|null
protected function detectPort(): ?int
{
// based on server port
$port = $this->get('SERVER_PORT');
@@ -579,8 +666,11 @@ class Environment
/**
* Splits a hostname:port string into its components
*
* @param string|null $host
* @return array
*/
protected function detectPortInHost(string|null $host = null): array
protected function detectPortInHost(?string $host = null): array
{
if (empty($host) === true) {
return [
@@ -599,8 +689,11 @@ class Environment
/**
* Splits any URI into path and query
*
* @param string|null $requestUri
* @return \Kirby\Http\Uri
*/
protected function detectRequestUri(string|null $requestUri = null): Uri
protected function detectRequestUri(?string $requestUri = null)
{
// make sure the URL parser works properly when there's a
// colon in the request URI but the URI is relative
@@ -627,8 +720,11 @@ class Environment
/**
* Returns the sanitized script path unless in CLI mode
*
* @param string|null $scriptPath
* @return string
*/
protected function detectScriptPath(string|null $scriptPath = null): string
protected function detectScriptPath(?string $scriptPath = null): string
{
if ($this->cli === true) {
return '';
@@ -652,8 +748,9 @@ class Environment
* 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 function get(string|false|null $key = null, $default = null)
public function get($key = null, $default = null)
{
if (is_string($key) === false) {
return $this->info;
@@ -675,13 +772,13 @@ class Environment
* 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 getGlobally(
string|false|null $key = null,
$default = null
) {
public static function getGlobally($key = null, $default = null)
{
// first try the global `Environment` object if the CMS is running
if ($app = App::instance(null, true)) {
$app = App::instance(null, true);
if ($app) {
return $app->environment()->get($key, $default);
}
@@ -698,14 +795,18 @@ class Environment
/**
* Returns the current host name
*
* @return string|null
*/
public function host(): string|null
public function host(): ?string
{
return $this->host;
}
/**
* Returns whether the HTTPS protocol is used
*
* @return bool
*/
public function https(): bool
{
@@ -714,6 +815,8 @@ class Environment
/**
* Returns the sanitized `$_SERVER` array
*
* @return array
*/
public function info(): array
{
@@ -722,8 +825,10 @@ class Environment
/**
* Returns the server's IP address
*
* @return string|null
*/
public function ip(): string|null
public function ip(): ?string
{
return $this->ip;
}
@@ -731,8 +836,10 @@ class Environment
/**
* Returns if the server is behind a
* reverse proxy server
*
* @return bool|null
*/
public function isBehindProxy(): bool|null
public function isBehindProxy(): ?bool
{
return $this->isBehindProxy;
}
@@ -740,6 +847,8 @@ class Environment
/**
* Checks if this is a local installation;
* returns `false` if in doubt
*
* @return bool
*/
public function isLocal(): bool
{
@@ -758,10 +867,6 @@ class Environment
return true;
}
if (Str::endsWith($host, '.ddev.site') === true) {
return true;
}
// collect all possible visitor ips
$ips = [
$this->get('REMOTE_ADDR'),
@@ -793,69 +898,58 @@ class Environment
/**
* Loads and returns options from environment-specific
* PHP files (by host name and server IP address or CLI)
* PHP files (by host name and server IP address)
*
* @param string $root Root directory to load configs from
* @return array
*/
public function options(string $root): array
{
$configCli = [];
$configHost = [];
$configAddr = [];
$host = $this->host();
$addr = $this->ip();
// load the config for the cli
if ($this->cli() === true) {
$configCli = F::load(
file: $root . '/config.cli.php',
fallback: [],
allowOutput: false
);
}
// load the config for the host
if (empty($host) === false) {
$configHost = F::load(
file: $root . '/config.' . $host . '.php',
fallback: [],
allowOutput: false
);
$configHost = F::load($root . '/config.' . $host . '.php', []);
}
// load the config for the server IP
if (empty($addr) === false) {
$configAddr = F::load(
file: $root . '/config.' . $addr . '.php',
fallback: [],
allowOutput: false
);
$configAddr = F::load($root . '/config.' . $addr . '.php', []);
}
return array_replace_recursive($configCli, $configHost, $configAddr);
return array_replace_recursive($configHost, $configAddr);
}
/**
* Returns the detected path
*
* @return string|null
*/
public function path(): string|null
public function path(): ?string
{
return $this->path;
}
/**
* Returns the correct port number
*
* @return int|null
*/
public function port(): int|null
public function port(): ?int
{
return $this->port;
}
/**
* Returns an URI object for the requested URL
*
* @return \Kirby\Http\Uri
*/
public function requestUri(): Uri
public function requestUri()
{
return $this->requestUri;
}
@@ -863,6 +957,8 @@ class Environment
/**
* Returns the current URL, including the request path
* and query
*
* @return string
*/
public function requestUrl(): string
{
@@ -871,11 +967,13 @@ class Environment
/**
* Sanitizes some `$_SERVER` keys
*
* @param string|array $key
* @param mixed $value
* @return mixed
*/
public static function sanitize(
string|array $key,
$value = null
) {
public static function sanitize($key, $value = null)
{
if (is_array($key) === true) {
foreach ($key as $k => $v) {
$key[$k] = static::sanitize($k, $v);
@@ -884,25 +982,28 @@ class Environment
return $key;
}
return match ($key) {
'SERVER_ADDR',
'SERVER_NAME',
'HTTP_HOST',
'HTTP_X_FORWARDED_HOST' => static::sanitizeHost($value),
'SERVER_PORT',
'HTTP_X_FORWARDED_PORT' => static::sanitizePort($value),
default => $value
};
switch ($key) {
case 'SERVER_ADDR':
case 'SERVER_NAME':
case 'HTTP_HOST':
case 'HTTP_X_FORWARDED_HOST':
return static::sanitizeHost($value);
case 'SERVER_PORT':
case 'HTTP_X_FORWARDED_PORT':
return static::sanitizePort($value);
default:
return $value;
}
}
/**
* Sanitizes the given host name
*
* @param string|null $host
* @return string|null
*/
protected static function sanitizeHost(
string|null $host = null
): string|null {
protected static function sanitizeHost(?string $host = null): ?string
{
if (empty($host) === true) {
return null;
}
@@ -925,10 +1026,12 @@ class Environment
/**
* Sanitizes the given port number
*
* @param string|int|null $port
* @return int|null
*/
protected static function sanitizePort(
string|int|false|null $port = null
): int|null {
protected static function sanitizePort($port = null): ?int
{
// already fine
if (is_int($port) === true) {
return $port;
@@ -940,7 +1043,7 @@ class Environment
}
// remove any character that is not an integer
$port = preg_replace('![^0-9]+!', '', $port);
$port = preg_replace('![^0-9]+!', '', (string)($port ?? ''));
// no port
if ($port === '') {
@@ -953,11 +1056,14 @@ class Environment
/**
* Sanitizes the given script path
*
* @param string|null $scriptPath
* @return string
*/
protected function sanitizeScriptPath(string|null $scriptPath = null): string
protected function sanitizeScriptPath(?string $scriptPath = null): string
{
$scriptPath ??= '';
$scriptPath = trim($scriptPath);
$scriptPath = trim($scriptPath);
// skip all the sanitizing steps if the path is empty
if ($scriptPath === '') {
@@ -991,6 +1097,8 @@ class Environment
*
* This can be used to build the base baseUrl
* for subfolder installations
*
* @return string
*/
public function scriptPath(): string
{
@@ -999,6 +1107,8 @@ class Environment
/**
* Returns all environment data as array
*
* @return array
*/
public function toArray(): array
{

View File

@@ -17,7 +17,8 @@ use Kirby\Filesystem\F;
class Header
{
// configuration
public static array $codes = [
public static $codes = [
// successful
'_200' => 'OK',
'_201' => 'Created',
@@ -55,13 +56,13 @@ class Header
/**
* Sends a content type header
*
* @param string $mime
* @param string $charset
* @param bool $send
* @return string|void
*/
public static function contentType(
string $mime,
string $charset = 'UTF-8',
bool $send = true
) {
public static function contentType(string $mime, string $charset = 'UTF-8', bool $send = true)
{
if ($found = F::extensionToMime($mime)) {
$mime = $found;
}
@@ -81,11 +82,13 @@ class Header
/**
* Creates headers by key and value
*
* @param string|array $key
* @param string|null $value
* @return string
*/
public static function create(
string|array $key,
string|null $value = null
): string {
public static function create($key, string $value = null): string
{
if (is_array($key) === true) {
$headers = [];
@@ -96,21 +99,20 @@ class Header
return implode("\r\n", $headers);
}
// prevent header injection by stripping
// any newline characters from single 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 bool $send
* @return string|void
*/
public static function type(
string $mime,
string $charset = 'UTF-8',
bool $send = true
) {
public static function type(string $mime, string $charset = 'UTF-8', bool $send = true)
{
return static::contentType($mime, $charset, $send);
}
@@ -121,30 +123,21 @@ class Header
* 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|null $code The HTTP status code
* @param int|string $code The HTTP status code
* @param bool $send If set to false the header will be returned instead
* @return string|void
* @psalm-return ($send is false ? string : void)
*/
public static function status(
int|string|null $code = null,
bool $send = true
) {
public static function status($code = null, bool $send = true)
{
$codes = static::$codes;
$protocol = Environment::getGlobally('SERVER_PROTOCOL', 'HTTP/1.1');
// allow full control over code and message
if (
is_string($code) === true &&
preg_match('/^\d{3} \w.+$/', $code) === 1
) {
if (is_string($code) === true && preg_match('/^\d{3} \w.+$/', $code) === 1) {
$message = substr(rtrim($code), 4);
$code = substr($code, 0, 3);
} else {
if (array_key_exists('_' . $code, $codes) === false) {
$code = 500;
}
$code = array_key_exists('_' . $code, $codes) === false ? 500 : $code;
$message = $codes['_' . $code] ?? 'Something went wrong';
}
@@ -161,6 +154,7 @@ class Header
/**
* Sends a 200 header
*
* @param bool $send
* @return string|void
*/
public static function success(bool $send = true)
@@ -171,6 +165,7 @@ class Header
/**
* Sends a 201 header
*
* @param bool $send
* @return string|void
*/
public static function created(bool $send = true)
@@ -181,6 +176,7 @@ class Header
/**
* Sends a 202 header
*
* @param bool $send
* @return string|void
*/
public static function accepted(bool $send = true)
@@ -191,6 +187,7 @@ class Header
/**
* Sends a 400 header
*
* @param bool $send
* @return string|void
*/
public static function error(bool $send = true)
@@ -201,6 +198,7 @@ class Header
/**
* Sends a 403 header
*
* @param bool $send
* @return string|void
*/
public static function forbidden(bool $send = true)
@@ -211,6 +209,7 @@ class Header
/**
* Sends a 404 header
*
* @param bool $send
* @return string|void
*/
public static function notfound(bool $send = true)
@@ -221,6 +220,7 @@ class Header
/**
* Sends a 404 header
*
* @param bool $send
* @return string|void
*/
public static function missing(bool $send = true)
@@ -231,6 +231,7 @@ class Header
/**
* Sends a 410 header
*
* @param bool $send
* @return string|void
*/
public static function gone(bool $send = true)
@@ -241,6 +242,7 @@ class Header
/**
* Sends a 500 header
*
* @param bool $send
* @return string|void
*/
public static function panic(bool $send = true)
@@ -251,6 +253,7 @@ class Header
/**
* Sends a 503 header
*
* @param bool $send
* @return string|void
*/
public static function unavailable(bool $send = true)
@@ -261,13 +264,13 @@ class Header
/**
* Sends a redirect header
*
* @param string $url
* @param int $code
* @param bool $send
* @return string|void
*/
public static function redirect(
string $url,
int $code = 302,
bool $send = true
) {
public static function redirect(string $url, int $code = 302, bool $send = true)
{
$status = static::status($code, false);
$location = 'Location:' . Url::unIdn($url);
@@ -285,7 +288,7 @@ class Header
*
* @param array $params Check out the defaults array for available parameters
*/
public static function download(array $params = []): void
public static function download(array $params = [])
{
$defaults = [
'name' => 'download',

View File

@@ -17,22 +17,31 @@ class Idn
{
/**
* Convert domain name from IDNA ASCII to Unicode
*
* @param string $domain
* @return string|false
*/
public static function decode(string $domain): string|false
public static function decode(string $domain)
{
return idn_to_utf8($domain);
}
/**
* Convert domain name to IDNA ASCII form
*
* @param string $domain
* @return string|false
*/
public static function encode(string $domain): string|false
public static function encode(string $domain)
{
return idn_to_ascii($domain);
}
/**
* Decodes a email address to the Unicode format
*
* @param string $email
* @return string
*/
public static function decodeEmail(string $email): string
{
@@ -48,6 +57,9 @@ class Idn
/**
* Encodes a email address to the Punycode format
*
* @param string $email
* @return string
*/
public static function encodeEmail(string $email): string
{

View File

@@ -2,7 +2,6 @@
namespace Kirby\Http;
use Kirby\Toolkit\Obj;
use Kirby\Toolkit\Str;
/**
@@ -16,14 +15,19 @@ use Kirby\Toolkit\Str;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Params extends Obj
class Params extends Query
{
public static string|null $separator = null;
/**
* @var null|string
*/
public static $separator;
/**
* Creates a new params object
*
* @param array|string $params
*/
public function __construct(array|string|null $params)
public function __construct($params)
{
if (is_string($params) === true) {
$params = static::extract($params)['params'];
@@ -34,8 +38,11 @@ class Params extends Obj
/**
* Extract the params from a string or array
*
* @param string|array|null $path
* @return array
*/
public static function extract(string|array|null $path = null): array
public static function extract($path = null): array
{
if (empty($path) === true) {
return [
@@ -66,7 +73,7 @@ class Params extends Obj
$paramValue = $paramParts[1] ?? null;
if ($paramKey !== null) {
$params[rawurldecode($paramKey)] = $paramValue !== null ? rawurldecode($paramValue) : null;
$params[rawurldecode($paramKey)] = $paramValue ? rawurldecode($paramValue) : null;
}
unset($path[$index]);
@@ -86,22 +93,14 @@ class Params extends Obj
];
}
public function isEmpty(): bool
{
return empty((array)$this) === true;
}
public function isNotEmpty(): bool
{
return $this->isEmpty() === false;
}
/**
* Returns the param separator according
* to the operating system.
*
* Unix = ':'
* Windows = ';'
*
* @return string
*/
public static function separator(): string
{
@@ -111,19 +110,27 @@ class Params extends Obj
if (DIRECTORY_SEPARATOR === '/') {
return static::$separator = ':';
} else {
return static::$separator = ';';
}
return static::$separator = ';';
}
/**
* Converts the params object to a params string
* which can then be used in the URL builder again
*
* @param bool $leadingSlash
* @param bool $trailingSlash
* @return string|null
*
* @todo The argument $leadingSlash is incompatible with
* Query::toString($questionMark = false); the Query class
* should be extracted into a common parent class for both
* Query and Params
* @psalm-suppress ParamNameMismatch
*/
public function toString(
bool $leadingSlash = false,
bool $trailingSlash = false
): string {
public function toString($leadingSlash = false, $trailingSlash = false): string
{
if ($this->isEmpty() === true) {
return '';
}
@@ -148,9 +155,4 @@ class Params extends Obj
return $leadingSlash . $params . $trailingSlash;
}
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -17,7 +17,7 @@ use Kirby\Toolkit\Str;
*/
class Path extends Collection
{
public function __construct(string|array|null $items)
public function __construct($items)
{
if (is_string($items) === true) {
$items = Str::split($items, '/');
@@ -31,10 +31,8 @@ class Path extends Collection
return $this->toString();
}
public function toString(
bool $leadingSlash = false,
bool $trailingSlash = false
): string {
public function toString(bool $leadingSlash = false, bool $trailingSlash = false): string
{
if (empty($this->data) === true) {
return '';
}

View File

@@ -17,7 +17,7 @@ use Kirby\Toolkit\Obj;
*/
class Query extends Obj
{
public function __construct(string|array|null $query)
public function __construct($query)
{
if (is_string($query) === true) {
parse_str(ltrim($query, '?'), $query);
@@ -33,10 +33,15 @@ class Query extends Obj
public function isNotEmpty(): bool
{
return $this->isEmpty() === false;
return empty((array)$this) === false;
}
public function toString(bool $questionMark = false): string
public function __toString(): string
{
return $this->toString();
}
public function toString($questionMark = false): string
{
$query = http_build_query($this, '', '&', PHP_QUERY_RFC3986);
@@ -50,10 +55,4 @@ class Query extends Obj
return $query;
}
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -2,13 +2,11 @@
namespace Kirby\Http;
use CurlHandle;
use Exception;
use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Str;
use stdClass;
/**
* A handy little class to handle
@@ -25,7 +23,10 @@ class Remote
public const CA_INTERNAL = 1;
public const CA_SYSTEM = 2;
public static array $defaults = [
/**
* @var array
*/
public static $defaults = [
'agent' => null,
'basicAuth' => null,
'body' => true,
@@ -40,17 +41,64 @@ class Remote
'timeout' => 10,
];
public string|null $content = null;
public CurlHandle|false $curl;
public array $curlopt = [];
public int $errorCode;
public string $errorMessage;
public array $headers = [];
public array $info = [];
public array $options = [];
/**
* @var string
*/
public $content;
/**
* @throws \Exception when the curl request failed
* @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 = [])
{
@@ -59,15 +107,14 @@ class Remote
// use the system CA store by default if
// one has been configured in php.ini
$cainfo = ini_get('curl.cainfo');
// Suppress warnings e.g. if system CA is outside of open_basedir (See: issue #6236)
if (empty($cainfo) === false && @is_file($cainfo) === true) {
if (empty($cainfo) === false && is_file($cainfo) === true) {
$defaults['ca'] = self::CA_SYSTEM;
}
// update the defaults with App config if set;
// request the App instance lazily
if ($app = App::instance(null, true)) {
$app = App::instance(null, true);
if ($app !== null) {
$defaults = array_merge($defaults, $app->option('remote', []));
}
@@ -81,40 +128,27 @@ class Remote
$this->fetch();
}
/**
* Magic getter for request info data
*/
public function __call(string $method, array $arguments = [])
public static function __callStatic(string $method, array $arguments = [])
{
$method = str_replace('-', '_', Str::kebab($method));
return $this->info[$method] ?? null;
}
public static function __callStatic(
string $method,
array $arguments = []
): static {
return new static(
url: $arguments[0],
options: array_merge(
['method' => strtoupper($method)],
$arguments[1] ?? []
)
);
return new static($arguments[0], array_merge(['method' => strtoupper($method)], $arguments[1] ?? []));
}
/**
* Returns the http status code
*
* @return int|null
*/
public function code(): int|null
public function code(): ?int
{
return $this->info['http_code'] ?? null;
}
/**
* Returns the response content
*
* @return mixed
*/
public function content(): string|null
public function content()
{
return $this->content;
}
@@ -123,9 +157,8 @@ class Remote
* Sets up all curl options and sends the request
*
* @return $this
* @throws \Exception when the curl request failed
*/
public function fetch(): static
public function fetch()
{
// curl options
$this->curlopt = [
@@ -138,7 +171,7 @@ class Remote
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_HEADER => false,
CURLOPT_HEADERFUNCTION => function ($curl, $header): int {
CURLOPT_HEADERFUNCTION => function ($curl, $header) {
$parts = Str::split($header, ':');
if (empty($parts[0]) === false && empty($parts[1]) === false) {
@@ -186,10 +219,10 @@ class Remote
$headers = [];
foreach ($this->options['headers'] as $key => $value) {
if (is_string($key) === true) {
$value = $key . ': ' . $value;
$headers[] = $key . ': ' . $value;
} else {
$headers[] = $value;
}
$headers[] = $value;
}
$this->curlopt[CURLOPT_HTTPHEADER] = $headers;
@@ -263,9 +296,11 @@ class Remote
/**
* Static method to send a GET request
*
* @throws \Exception when the curl request failed
* @param string $url
* @param array $params
* @return static
*/
public static function get(string $url, array $params = []): static
public static function get(string $url, array $params = [])
{
$defaults = [
'method' => 'GET',
@@ -276,10 +311,7 @@ class Remote
$query = http_build_query($options['data']);
if (empty($query) === false) {
$url = match (Url::hasQuery($url)) {
true => $url . '&' . $query,
default => $url . '?' . $query
};
$url = Url::hasQuery($url) === true ? $url . '&' . $query : $url . '?' . $query;
}
// remove the data array from the options
@@ -290,6 +322,8 @@ class Remote
/**
* Returns all received headers
*
* @return array
*/
public function headers(): array
{
@@ -298,6 +332,8 @@ class Remote
/**
* Returns the request info
*
* @return array
*/
public function info(): array
{
@@ -308,15 +344,17 @@ class Remote
* Decode the response content
*
* @param bool $array decode as array or object
* @psalm-return ($array is true ? array|null : stdClass|null)
* @return array|\stdClass
*/
public function json(bool $array = true): array|stdClass|null
public function json(bool $array = true)
{
return json_decode($this->content(), $array);
}
/**
* Returns the request method
*
* @return string
*/
public function method(): string
{
@@ -326,6 +364,8 @@ class Remote
/**
* Returns all options which have been
* set for the current request
*
* @return array
*/
public function options(): array
{
@@ -334,28 +374,35 @@ class Remote
/**
* 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;
}
return $data;
}
/**
* Static method to init this class and send a request
*
* @throws \Exception when the curl request failed
* @param string $url
* @param array $params
* @return static
*/
public static function request(string $url, array $params = []): static
public static function request(string $url, array $params = [])
{
return new static($url, $params);
}
/**
* Returns the request Url
*
* @return string
*/
public function url(): string
{

View File

@@ -3,10 +3,6 @@
namespace Kirby\Http;
use Kirby\Cms\App;
use Kirby\Http\Request\Auth;
use Kirby\Http\Request\Auth\BasicAuth;
use Kirby\Http\Request\Auth\BearerAuth;
use Kirby\Http\Request\Auth\SessionAuth;
use Kirby\Http\Request\Body;
use Kirby\Http\Request\Files;
use Kirby\Http\Request\Query;
@@ -26,16 +22,18 @@ use Kirby\Toolkit\Str;
*/
class Request
{
public static array $authTypes = [
'basic' => BasicAuth::class,
'bearer' => BearerAuth::class,
'session' => SessionAuth::class,
public static $authTypes = [
'basic' => 'Kirby\Http\Request\Auth\BasicAuth',
'bearer' => 'Kirby\Http\Request\Auth\BearerAuth',
'session' => 'Kirby\Http\Request\Auth\SessionAuth',
];
/**
* The auth object if available
*
* @var \Kirby\Http\Request\Auth|false|null
*/
protected Auth|false|null $auth = null;
protected $auth;
/**
* The Body object is a wrapper around
@@ -46,8 +44,10 @@ class Request
* Examples:
*
* `$request->body()->get('foo')`
*
* @var Body
*/
protected Body|null $body = null;
protected $body;
/**
* The Files object is a wrapper around
@@ -59,19 +59,25 @@ class Request
*
* `$request->files()->get('upload')['size']`
* `$request->file('upload')['size']`
*
* @var Files
*/
protected Files|null $files = null;
protected $files;
/**
* The Method type
*
* @var string
*/
protected string $method;
protected $method;
/**
* All options that have been passed to
* the request in the constructor
*
* @var array
*/
protected array $options;
protected $options;
/**
* The Query object is a wrapper around
@@ -82,19 +88,25 @@ class Request
* Examples:
*
* `$request->query()->get('foo')`
*
* @var Query
*/
protected Query $query;
protected $query;
/**
* Request URL object
*
* @var Uri
*/
protected Uri $url;
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 = [])
{
@@ -102,37 +114,26 @@ class Request
$this->method = $this->detectRequestMethod($options['method'] ?? null);
if (isset($options['body']) === true) {
$this->body =
$options['body'] instanceof Body
? $options['body']
: new Body($options['body']);
$this->body = is_a($options['body'], Body::class) ? $options['body'] : new Body($options['body']);
}
if (isset($options['files']) === true) {
$this->files =
$options['files'] instanceof Files
? $options['files']
: new Files($options['files']);
$this->files = is_a($options['files'], Files::class) ? $options['files'] : new Files($options['files']);
}
if (isset($options['query']) === true) {
$this->query =
$options['query'] instanceof Query
? $options['query']
: new Query($options['query']);
$this->query = is_a($options['query'], Query::class) === true ? $options['query'] : new Query($options['query']);
}
if (isset($options['url']) === true) {
$this->url =
$options['url'] instanceof Uri
? $options['url']
: new Uri($options['url']);
$this->url = is_a($options['url'], Uri::class) === true ? $options['url'] : new Uri($options['url']);
}
}
/**
* Improved `var_dump` output
* @codeCoverageIgnore
*
* @return array
*/
public function __debugInfo(): array
{
@@ -147,8 +148,10 @@ class Request
/**
* Returns the Auth object if authentication is set
*
* @return \Kirby\Http\Request\Auth|null
*/
public function auth(): Auth|false|null
public function auth()
{
if ($this->auth !== null) {
return $this->auth;
@@ -163,9 +166,11 @@ class Request
// this ensures that the response is only cached for
// unauthenticated visitors;
// https://github.com/getkirby/kirby/issues/4423#issuecomment-1166300526
$kirby?->response()->usesAuth(true);
if ($kirby) {
$kirby->response()->usesAuth(true);
}
if ($auth = $this->authString()) {
if ($auth = $this->options['auth'] ?? $this->header('authorization')) {
$type = Str::lower(Str::before($auth, ' '));
$data = Str::after($auth, ' ');
@@ -184,14 +189,18 @@ class Request
/**
* Returns the Body object
*
* @return \Kirby\Http\Request\Body
*/
public function body(): Body
public function body()
{
return $this->body ??= new Body();
}
/**
* Checks if the request has been made from the command line
*
* @return bool
*/
public function cli(): bool
{
@@ -200,28 +209,32 @@ class Request
/**
* Returns a CSRF token if stored in a header or the query
*
* @return string|null
*/
public function csrf(): 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_replace(
$this->body()->toArray(),
$this->query()->toArray()
);
return array_merge($this->body()->toArray(), $this->query()->toArray());
}
/**
* Detect the request method from various
* options: given method, query string, server vars
*
* @param string $method
* @return string
*/
public function detectRequestMethod(string|null $method = null): string
public function detectRequestMethod(string $method = null): string
{
// all possible methods
$methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'];
@@ -229,12 +242,12 @@ class Request
// the request method can be overwritten with a header
$methodOverride = strtoupper(Environment::getGlobally('HTTP_X_HTTP_METHOD_OVERRIDE', ''));
if (in_array($methodOverride, $methods) === true) {
$method ??= $methodOverride;
if ($method === null && in_array($methodOverride, $methods) === true) {
$method = $methodOverride;
}
// final chain of options to detect the method
$method ??= Environment::getGlobally('REQUEST_METHOD', 'GET');
$method = $method ?? Environment::getGlobally('REQUEST_METHOD', 'GET');
// uppercase the shit out of it
$method = strtoupper($method);
@@ -249,6 +262,8 @@ class Request
/**
* Returns the domain
*
* @return string
*/
public function domain(): string
{
@@ -258,16 +273,21 @@ class Request
/**
* Fetches a single file array
* from the Files object by key
*
* @param string $key
* @return array|null
*/
public function file(string $key): array|null
public function file(string $key)
{
return $this->files()->get($key);
}
/**
* Returns the Files object
*
* @return \Kirby\Cms\Files
*/
public function files(): Files
public function files()
{
return $this->files ??= new Files();
}
@@ -275,8 +295,12 @@ class Request
/**
* Returns any data field from the request
* if it exists
*
* @param string|null|array $key
* @param mixed $fallback
* @return mixed
*/
public function get(string|array|null $key = null, $fallback = null)
public function get($key = null, $fallback = null)
{
return A::get($this->data(), $key, $fallback);
}
@@ -285,14 +309,22 @@ class Request
* Returns whether the request contains
* the `Authorization` header
* @since 3.7.0
*
* @return bool
*/
public function hasAuth(): bool
{
return $this->authString() !== null;
$header = $this->options['auth'] ?? $this->header('authorization');
return $header !== null;
}
/**
* Returns a header by key if it exists
*
* @param string $key
* @param mixed $fallback
* @return mixed
*/
public function header(string $key, $fallback = null)
{
@@ -303,16 +335,15 @@ class Request
/**
* Return all headers with polyfill for
* missing getallheaders function
*
* @return array
*/
public function headers(): array
{
$headers = [];
foreach (Environment::getGlobally() as $key => $value) {
if (
substr($key, 0, 5) !== 'HTTP_' &&
substr($key, 0, 14) !== 'REDIRECT_HTTP_'
) {
if (substr($key, 0, 5) !== 'HTTP_' && substr($key, 0, 14) !== 'REDIRECT_HTTP_') {
continue;
}
@@ -340,6 +371,9 @@ class Request
/**
* Checks if the given method name
* matches the name of the request method.
*
* @param string $method
* @return bool
*/
public function is(string $method): bool
{
@@ -348,6 +382,8 @@ class Request
/**
* Returns the request method
*
* @return string
*/
public function method(): string
{
@@ -357,7 +393,7 @@ class Request
/**
* Shortcut to the Params object
*/
public function params(): Params
public function params()
{
return $this->url()->params();
}
@@ -365,21 +401,25 @@ class Request
/**
* Shortcut to the Path object
*/
public function path(): Path
public function path()
{
return $this->url()->path();
}
/**
* Returns the Query object
*
* @return \Kirby\Http\Request\Query
*/
public function query(): Query
public function query()
{
return $this->query ??= new Query();
}
/**
* Checks for a valid SSL connection
*
* @return bool
*/
public function ssl(): bool
{
@@ -391,8 +431,11 @@ class Request
* If you pass props you can safely modify
* the Url with new parameters without destroying
* the original object.
*
* @param array $props
* @return \Kirby\Http\Uri
*/
public function url(array|null $props = null): Uri
public function url(array $props = null)
{
if ($props !== null) {
return $this->url()->clone($props);
@@ -400,27 +443,4 @@ class Request
return $this->url ??= Uri::current();
}
/**
* Returns the raw auth string from the `auth` option
* or `Authorization` header unless both are empty
*/
protected function authString(): string|null
{
// both variants need to be checked separately
// because empty strings are treated as invalid
// but the `??` operator wouldn't do the fallback
$option = $this->options['auth'] ?? null;
if (empty($option) === false) {
return $option;
}
$header = $this->header('authorization');
if (empty($header) === false) {
return $header;
}
return null;
}
}

View File

@@ -2,8 +2,6 @@
namespace Kirby\Http\Request;
use SensitiveParameter;
/**
* Base class for auth types
*
@@ -16,16 +14,27 @@ use SensitiveParameter;
abstract class Auth
{
/**
* @param string $data Raw authentication data after the first space in the `Authorization` header
* Raw authentication data after the first space
* in the `Authorization` header
*
* @var string
*/
public function __construct(
#[SensitiveParameter]
protected string $data
) {
protected $data;
/**
* Constructor
*
* @param string $data
*/
public function __construct(string $data)
{
$this->data = $data;
}
/**
* Converts the object to a string
*
* @return string
*/
public function __toString(): string
{
@@ -35,6 +44,8 @@ abstract class Auth
/**
* Returns the raw authentication data after the
* first space in the `Authorization` header
*
* @return string
*/
public function data(): string
{
@@ -43,6 +54,8 @@ abstract class Auth
/**
* Returns the name of the auth type (lowercase)
*
* @return string
*/
abstract public function type(): string;
}

View File

@@ -4,7 +4,6 @@ namespace Kirby\Http\Request\Auth;
use Kirby\Http\Request\Auth;
use Kirby\Toolkit\Str;
use SensitiveParameter;
/**
* HTTP basic authentication data
@@ -17,14 +16,26 @@ use SensitiveParameter;
*/
class BasicAuth extends Auth
{
protected string $credentials;
protected string|null $password;
protected string|null $username;
/**
* @var string
*/
protected $credentials;
public function __construct(
#[SensitiveParameter]
string $data
) {
/**
* @var string
*/
protected $password;
/**
* @var string
*/
protected $username;
/**
* @param string $token
*/
public function __construct(string $data)
{
parent::__construct($data);
$this->credentials = base64_decode($data);
@@ -34,6 +45,8 @@ class BasicAuth extends Auth
/**
* Returns the entire unencoded credentials string
*
* @return string
*/
public function credentials(): string
{
@@ -42,14 +55,18 @@ class BasicAuth extends Auth
/**
* Returns the password
*
* @return string|null
*/
public function password(): string|null
public function password(): ?string
{
return $this->password;
}
/**
* Returns the authentication type
*
* @return string
*/
public function type(): string
{
@@ -58,8 +75,10 @@ class BasicAuth extends Auth
/**
* Returns the username
*
* @return string|null
*/
public function username(): string|null
public function username(): ?string
{
return $this->username;
}

View File

@@ -17,6 +17,8 @@ class BearerAuth extends Auth
{
/**
* Returns the authentication token
*
* @return string
*/
public function token(): string
{
@@ -25,6 +27,8 @@ class BearerAuth extends Auth
/**
* Returns the auth type
*
* @return string
*/
public function type(): string
{

View File

@@ -4,7 +4,6 @@ namespace Kirby\Http\Request\Auth;
use Kirby\Cms\App;
use Kirby\Http\Request\Auth;
use Kirby\Session\Session;
/**
* Authentication data using Kirby's session
@@ -19,14 +18,18 @@ class SessionAuth extends Auth
{
/**
* Tries to return the session object
*
* @return \Kirby\Session\Session
*/
public function session(): Session
public function session()
{
return App::instance()->sessionHandler()->getManually($this->data);
}
/**
* Returns the session token
*
* @return string
*/
public function token(): string
{
@@ -35,6 +38,8 @@ class SessionAuth extends Auth
/**
* Returns the authentication type
*
* @return string
*/
public function type(): string
{

View File

@@ -20,13 +20,17 @@ class Body
/**
* The raw body content
*
* @var string|array
*/
protected string|array|null $contents;
protected $contents;
/**
* The parsed content as array
*
* @var array
*/
protected array|null $data = null;
protected $data;
/**
* Creates a new request body object.
@@ -34,8 +38,10 @@ class Body
* 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(array|string|null $contents = null)
public function __construct($contents = null)
{
$this->contents = $contents;
}
@@ -43,18 +49,20 @@ class Body
/**
* Fetches the raw contents for the body
* or uses the passed contents.
*
* @return string|array
*/
public function contents(): string|array
public function contents()
{
if ($this->contents !== null) {
return $this->contents;
if ($this->contents === null) {
if (empty($_POST) === false) {
$this->contents = $_POST;
} else {
$this->contents = file_get_contents('php://input');
}
}
if (empty($_POST) === false) {
return $this->contents = $_POST;
}
return $this->contents = file_get_contents('php://input');
return $this->contents;
}
/**
@@ -63,6 +71,8 @@ class Body
* 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
{
@@ -99,6 +109,8 @@ class Body
/**
* Converts the data array back
* to a http query string
*
* @return string
*/
public function toString(): string
{
@@ -107,6 +119,8 @@ class Body
/**
* Magic string converter
*
* @return string
*/
public function __toString(): string
{

View File

@@ -20,7 +20,8 @@ trait Data
{
/**
* Improved `var_dump` output
* @codeCoverageIgnore
*
* @return array
*/
public function __debugInfo(): array
{
@@ -32,6 +33,8 @@ trait Data
* 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;
@@ -40,8 +43,12 @@ trait Data
* 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(string|array $key, $default = null)
public function get($key, $default = null)
{
if (is_array($key) === true) {
$result = [];
@@ -57,6 +64,8 @@ trait Data
/**
* Returns the data array.
* This is basically an alias for Data::data()
*
* @return array
*/
public function toArray(): array
{
@@ -65,6 +74,8 @@ trait Data
/**
* Converts the data array to json
*
* @return string
*/
public function toJson(): string
{

View File

@@ -21,17 +21,25 @@ class Files
/**
* Sanitized array of all received files
*
* @var array
*/
protected array $files = [];
protected $files;
/**
* Creates a new Files object
* Pass your own array to mock
* uploads.
*
* @param array|null $files
*/
public function __construct(array|null $files = null)
public function __construct($files = null)
{
$files ??= $_FILES;
if ($files === null) {
$files = $_FILES;
}
$this->files = [];
foreach ($files as $key => $file) {
if (is_array($file['name'])) {
@@ -55,6 +63,8 @@ class Files
* array. This is only needed to make
* the Data trait work for the Files::get($key)
* method.
*
* @return array
*/
public function data(): array
{

View File

@@ -19,8 +19,10 @@ class Query
/**
* The Query data array
*
* @var array|null
*/
protected array|null $data = null;
protected $data;
/**
* Creates a new Query object.
@@ -28,12 +30,14 @@ class Query
* 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(array|string|null $data = null)
public function __construct($data = null)
{
if ($data === null) {
$this->data = $_GET;
} elseif (is_array($data) === true) {
} elseif (is_array($data)) {
$this->data = $data;
} else {
parse_str($data, $parsed);
@@ -43,6 +47,8 @@ class Query
/**
* Returns the Query data as array
*
* @return array
*/
public function data(): array
{
@@ -51,6 +57,8 @@ class Query
/**
* Returns `true` if the request doesn't contain query variables
*
* @return bool
*/
public function isEmpty(): bool
{
@@ -59,6 +67,8 @@ class Query
/**
* Returns `true` if the request contains query variables
*
* @return bool
*/
public function isNotEmpty(): bool
{
@@ -68,6 +78,8 @@ class Query
/**
* Converts the query data array
* back to a query string
*
* @return string
*/
public function toString(): string
{
@@ -76,6 +88,8 @@ class Query
/**
* Magic string converter
*
* @return string
*/
public function __toString(): string
{

View File

@@ -2,10 +2,9 @@
namespace Kirby\Http;
use Closure;
use Exception;
use Kirby\Exception\LogicException;
use Kirby\Filesystem\F;
use Throwable;
/**
* Representation of an Http response,
@@ -23,39 +22,50 @@ class Response
/**
* Store for all registered headers,
* which will be sent with the response
*
* @var array
*/
protected array $headers = [];
protected $headers = [];
/**
* The response body
*
* @var string
*/
protected string $body;
protected $body;
/**
* The HTTP response code
*
* @var int
*/
protected int $code;
protected $code;
/**
* The content type for the response
*
* @var string
*/
protected string $type;
protected $type;
/**
* The content type charset
*
* @var string
*/
protected string $charset = 'UTF-8';
protected $charset = 'UTF-8';
/**
* Creates a new response object
*
* @param string $body
* @param string $type
* @param int $code
* @param array $headers
* @param string $charset
*/
public function __construct(
string|array $body = '',
string|null $type = null,
int|null $code = null,
array|null $headers = null,
string|null $charset = null
) {
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;
@@ -81,7 +91,8 @@ class Response
/**
* Improved `var_dump` output
* @codeCoverageIgnore
*
* @return array
*/
public function __debugInfo(): array
{
@@ -92,14 +103,22 @@ class Response
* 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
{
return $this->send();
try {
return $this->send();
} catch (Throwable $e) {
return '';
}
}
/**
* Getter for the body
*
* @return string
*/
public function body(): string
{
@@ -108,6 +127,8 @@ class Response
/**
* Getter for the content type charset
*
* @return string
*/
public function charset(): string
{
@@ -116,6 +137,8 @@ class Response
/**
* Getter for the HTTP status code
*
* @return int
*/
public function code(): int
{
@@ -126,13 +149,13 @@ class Response
* Creates a response that triggers
* a file download for the given file
*
* @param string $file
* @param string $filename
* @param array $props Custom overrides for response props (e.g. headers)
* @return static
*/
public static function download(
string $file,
string|null $filename = null,
array $props = []
): static {
public static function download(string $file, string $filename = null, array $props = [])
{
if (file_exists($file) === false) {
throw new Exception('The file could not be found');
}
@@ -163,9 +186,11 @@ class Response
* Creates a response for a file and
* sends the file content to the browser
*
* @param string $file
* @param array $props Custom overrides for response props (e.g. headers)
* @return static
*/
public static function file(string $file, array $props = []): static
public static function file(string $file, array $props = [])
{
$props = array_merge([
'body' => F::read($file),
@@ -193,42 +218,32 @@ class Response
* Urls can be relative or absolute.
* @since 3.7.0
*
* @param string $url
* @param int $code
* @return void
*
* @codeCoverageIgnore
*/
public static function go(string $url = '/', int $code = 302): never
public static function go(string $url = '/', int $code = 302)
{
die(static::redirect($url, $code));
}
/**
* Ensures that the callback does not produce the first body output
* (used to show when loading a file creates side effects)
*/
public static function guardAgainstOutput(Closure $callback, ...$args): mixed
{
$before = headers_sent();
$result = $callback(...$args);
$after = headers_sent($file, $line);
if ($before === false && $after === true) {
throw new LogicException("Disallowed output from file $file:$line, possible accidental whitespace?");
}
return $result;
}
/**
* Getter for single headers
*
* @param string $key Name of the header
* @return string|null
*/
public function header(string $key): string|null
public function header(string $key): ?string
{
return $this->headers[$key] ?? null;
}
/**
* Getter for all headers
*
* @return array
*/
public function headers(): array
{
@@ -238,15 +253,17 @@ class Response
/**
* Creates a json response with appropriate
* header and automatic conversion of arrays.
*
* @param string|array $body
* @param int $code
* @param bool $pretty
* @param array $headers
* @return static
*/
public static function json(
string|array $body = '',
int|null $code = null,
bool|null $pretty = null,
array $headers = []
): static {
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 : 0);
$body = json_encode($body, $pretty === true ? JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES : 0);
}
return new static([
@@ -261,8 +278,12 @@ class Response
* Creates a redirect response,
* which will send the visitor to the
* given location.
*
* @param string $location
* @param int $code
* @return static
*/
public static function redirect(string $location = '/', int $code = 302): static
public static function redirect(string $location = '/', int $code = 302)
{
return new static([
'code' => $code,
@@ -275,6 +296,8 @@ class Response
/**
* Sends all registered headers and
* returns the response body
*
* @return string
*/
public function send(): string
{
@@ -297,6 +320,8 @@ class Response
* Converts all relevant response attributes
* to an associative array for debugging,
* testing or whatever.
*
* @return array
*/
public function toArray(): array
{
@@ -311,6 +336,8 @@ class Response
/**
* Getter for the content type
*
* @return string
*/
public function type(): string
{

View File

@@ -15,35 +15,47 @@ class Route
{
/**
* The callback action function
*
* @var Closure
*/
protected Closure $action;
protected $action;
/**
* Listed of parsed arguments
*
* @var array
*/
protected array $arguments = [];
protected $arguments = [];
/**
* An array of all passed attributes
*
* @var array
*/
protected array $attributes = [];
protected $attributes = [];
/**
* The registered request method
*
* @var string
*/
protected string $method;
protected $method;
/**
* The registered pattern
*
* @var string
*/
protected string $pattern;
protected $pattern;
/**
* Wildcards, which can be used in
* Route patterns to make regular expressions
* a little more human
*
* @var array
*/
protected array $wildcards = [
protected $wildcards = [
'required' => [
'(:num)' => '(-?[0-9]+)',
'(:alpha)' => '([a-zA-Z]+)',
@@ -62,8 +74,12 @@ class Route
/**
* Magic getter for route attributes
*
* @param string $key
* @param array $arguments
* @return mixed
*/
public function __call(string $key, array $args = null): mixed
public function __call(string $key, array $arguments = null)
{
return $this->attributes[$key] ?? null;
}
@@ -71,13 +87,14 @@ class Route
/**
* 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
* @param array $attributes
*/
public function __construct(
string $pattern,
string $method,
Closure $action,
array $attributes = []
) {
public function __construct($pattern, $method, Closure $action, array $attributes = [])
{
$this->action = $action;
$this->attributes = $attributes;
$this->method = $method;
@@ -86,14 +103,18 @@ class Route
/**
* Getter for the action callback
*
* @return Closure
*/
public function action(): Closure
public function action()
{
return $this->action;
}
/**
* Returns all parsed arguments
*
* @return array
*/
public function arguments(): array
{
@@ -102,6 +123,8 @@ class Route
/**
* Getter for additional attributes
*
* @return array
*/
public function attributes(): array
{
@@ -110,6 +133,8 @@ class Route
/**
* Getter for the method
*
* @return string
*/
public function method(): string
{
@@ -118,8 +143,10 @@ class Route
/**
* Returns the route name if set
*
* @return string|null
*/
public function name(): string|null
public function name(): ?string
{
return $this->attributes['name'] ?? null;
}
@@ -128,6 +155,8 @@ class Route
* Throws a specific exception to tell
* the router to jump to the next route
* @since 3.0.3
*
* @return void
*/
public static function next(): void
{
@@ -136,6 +165,8 @@ class Route
/**
* Getter for the pattern
*
* @return string
*/
public function pattern(): string
{
@@ -145,6 +176,9 @@ class Route
/**
* Converts the pattern into a full regular
* expression by replacing all the wildcards
*
* @param string $pattern
* @return string
*/
public function regex(string $pattern): string
{
@@ -166,8 +200,12 @@ class Route
/**
* 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): array|false
public function parse(string $pattern, string $path)
{
// check for direct matches
if ($pattern === $path) {

View File

@@ -18,27 +18,35 @@ class Router
{
/**
* Hook that is called after each route
*
* @var \Closure
*/
protected Closure|null $afterEach;
protected $afterEach;
/**
* Hook that is called before each route
*
* @var \Closure
*/
protected Closure|null $beforeEach;
protected $beforeEach;
/**
* Store for the current route,
* if one can be found
*
* @var \Kirby\Http\Route|null
*/
protected Route|null $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 array $routes = [
protected $routes = [
'GET' => [],
'HEAD' => [],
'POST' => [],
@@ -54,6 +62,7 @@ class Router
* Creates a new router object and
* registers all the given routes
*
* @param array $routes
* @param array<string, \Closure> $hooks Optional `beforeEach` and `afterEach` hooks
*/
public function __construct(array $routes = [], array $hooks = [])
@@ -95,12 +104,14 @@ class Router
* and then call the Route action with
* the appropriate arguments and a Result
* object.
*
* @param string $path
* @param string $method
* @param Closure|null $callback
* @return mixed
*/
public function call(
string|null $path = null,
string $method = 'GET',
Closure|null $callback = null
) {
public function call(string $path = null, string $method = 'GET', Closure $callback = null)
{
$path ??= '';
$ignore = [];
$result = null;
@@ -109,7 +120,7 @@ class Router
while ($loop === true) {
$route = $this->find($path, $method, $ignore);
if ($this->beforeEach instanceof Closure) {
if (is_a($this->beforeEach, 'Closure') === true) {
($this->beforeEach)($route, $path, $method);
}
@@ -117,18 +128,15 @@ class Router
if ($callback) {
$result = $callback($route);
} else {
$result = $route->action()->call(
$route,
...$route->arguments()
);
$result = $route->action()->call($route, ...$route->arguments());
}
$loop = false;
} catch (Exceptions\NextRouteException) {
} catch (Exceptions\NextRouteException $e) {
$ignore[] = $route;
}
if ($this->afterEach instanceof Closure) {
if (is_a($this->afterEach, 'Closure') === true) {
$final = $loop === false;
$result = ($this->afterEach)($route, $path, $method, $result, $final);
}
@@ -141,13 +149,15 @@ class Router
* Creates a micro-router and executes
* the routing action immediately
* @since 3.7.0
*
* @param string|null $path
* @param string $method
* @param array $routes
* @param \Closure|null $callback
* @return mixed
*/
public static function execute(
string|null $path = null,
string $method = 'GET',
array $routes = [],
Closure|null $callback = null
) {
public static function execute(?string $path = null, string $method = 'GET', array $routes = [], ?Closure $callback = null)
{
return (new static($routes))->call($path, $method, $callback);
}
@@ -156,12 +166,14 @@ class Router
* 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
* @param array $ignore
* @return \Kirby\Http\Route|null
*/
public function find(
string $path,
string $method,
array|null $ignore = null
): Route {
public function find(string $path, string $method, array $ignore = null)
{
if (isset($this->routes[$method]) === false) {
throw new InvalidArgumentException('Invalid routing method: ' . $method, 400);
}
@@ -173,10 +185,7 @@ class Router
$arguments = $route->parse($route->pattern(), $path);
if ($arguments !== false) {
if (
empty($ignore) === true ||
in_array($route, $ignore) === false
) {
if (empty($ignore) === true || in_array($route, $ignore) === false) {
return $this->route = $route;
}
}
@@ -190,8 +199,10 @@ class Router
* This will only return something,
* once Router::find() has been called
* and only if a route was found.
*
* @return \Kirby\Http\Route|null
*/
public function route(): Route|null
public function route()
{
return $this->route;
}

38
kirby/src/Http/Server.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace Kirby\Http;
use Kirby\Toolkit\Facade;
/**
* 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 https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
* @deprecated 3.7.0 Use `Kirby\Http\Environment` instead
* @todo Remove in 3.8.0
*/
class Server extends Facade
{
public const HOST_FROM_SERVER = 1;
public const HOST_FROM_HEADER = 2;
public const HOST_ALLOW_EMPTY = 4;
public static $cli;
public static $hosts;
/**
* @return \Kirby\Http\Environment
*/
public static function instance()
{
return new Environment([
'cli' => static::$cli,
'allowed' => static::$hosts
]);
}
}

View File

@@ -4,7 +4,7 @@ namespace Kirby\Http;
use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException;
use SensitiveParameter;
use Kirby\Toolkit\Properties;
use Throwable;
/**
@@ -18,74 +18,129 @@ use Throwable;
*/
class Uri
{
use Properties;
/**
* Cache for the current Uri object
*
* @var Uri|null
*/
public static Uri|null $current = null;
public static $current;
/**
* The fragment after the hash
*
* @var string|false
*/
protected string|false|null $fragment;
protected $fragment;
/**
* The host address
*
* @var string
*/
protected string|null $host;
protected $host;
/**
* The optional password for basic authentication
*
* @var string|false
*/
protected string|false|null $password;
protected $password;
/**
* The optional list of params
*
* @var Params
*/
protected Params $params;
protected $params;
/**
* The optional path
*
* @var Path
*/
protected Path $path;
protected $path;
/**
* The optional port number
*
* @var int|false
*/
protected int|false|null $port;
protected $port;
/**
* All original properties
*
* @var array
*/
protected array $props;
protected $props;
/**
* The optional query string without leading ?
*
* @var Query
*/
protected Query $query;
protected $query;
/**
* https or http
*
* @var string
*/
protected string|null $scheme;
protected $scheme = 'http';
/**
* Supported schemes
*
* @var array
*/
protected static array $schemes = ['http', 'https', 'ftp'];
protected static $schemes = ['http', 'https', 'ftp'];
protected bool $slash;
/**
* @var bool
*/
protected $slash = false;
/**
* The optional username for basic authentication
*
* @var string|false
*/
protected string|false|null $username = null;
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|string $props
* @param array $inject Additional props to inject if a URL string is passed
*/
public function __construct(array|string $props = [], array $inject = [])
public function __construct($props = [], array $inject = [])
{
if (is_string($props) === true) {
$props = parse_url($props);
@@ -100,40 +155,14 @@ class Uri
$props = static::parsePath($props);
}
$this->props = $props;
$this->setFragment($props['fragment'] ?? null);
$this->setHost($props['host'] ?? null);
$this->setParams($props['params'] ?? null);
$this->setPassword($props['password'] ?? null);
$this->setPath($props['path'] ?? null);
$this->setPort($props['port'] ?? null);
$this->setQuery($props['query'] ?? null);
$this->setScheme($props['scheme'] ?? 'http');
$this->setSlash($props['slash'] ?? false);
$this->setUsername($props['username'] ?? null);
}
/**
* Magic caller to access all properties
*/
public function __call(string $property, array $arguments = [])
{
return $this->$property ?? null;
}
/**
* Make sure that cloning also clones
* the path and query objects
*/
public function __clone()
{
$this->path = clone $this->path;
$this->query = clone $this->query;
$this->params = clone $this->params;
$this->setProperties($this->props = $props);
}
/**
* Magic getter
*
* @param string $property
* @return mixed
*/
public function __get(string $property)
{
@@ -142,6 +171,10 @@ class Uri
/**
* Magic setter
*
* @param string $property
* @param mixed $value
* @return void
*/
public function __set(string $property, $value): void
{
@@ -152,20 +185,24 @@ class Uri
/**
* Converts the URL object to string
*
* @return string
*/
public function __toString(): string
{
try {
return $this->toString();
} catch (Throwable) {
} catch (Throwable $e) {
return '';
}
}
/**
* Returns the auth details (username:password)
*
* @return string|null
*/
public function auth(): string|null
public function auth(): ?string
{
$auth = trim($this->username . ':' . $this->password);
return $auth !== ':' ? $auth : null;
@@ -174,8 +211,10 @@ class Uri
/**
* Returns the base url (scheme + host)
* without trailing slash
*
* @return string|null
*/
public function base(): string|null
public function base(): ?string
{
if ($domain = $this->domain()) {
return $this->scheme ? $this->scheme . '://' . $domain : $domain;
@@ -187,8 +226,11 @@ class Uri
/**
* Clones the Uri object and applies optional
* new props.
*
* @param array $props
* @return static
*/
public function clone(array $props = []): static
public function clone(array $props = [])
{
$clone = clone $this;
@@ -199,7 +241,11 @@ class Uri
return $clone;
}
public static function current(array $props = []): static
/**
* @param array $props
* @return static
*/
public static function current(array $props = [])
{
if (static::$current !== null) {
return static::$current;
@@ -215,11 +261,11 @@ class Uri
}
/**
* Returns the domain without scheme, path or query.
* Includes auth part when not empty.
* Includes port number when different from 80 or 443.
* Returns the domain without scheme, path or query
*
* @return string|null
*/
public function domain(): string|null
public function domain(): ?string
{
if (empty($this->host) === true || $this->host === '/') {
return null;
@@ -234,31 +280,40 @@ class Uri
$domain .= $this->host;
if (
$this->port !== null &&
in_array($this->port, [80, 443]) === false
) {
if ($this->port !== null && in_array($this->port, [80, 443]) === false) {
$domain .= ':' . $this->port;
}
return $domain;
}
/**
* @return bool
*/
public function hasFragment(): bool
{
return empty($this->fragment) === false;
}
/**
* @return bool
*/
public function hasPath(): bool
{
return $this->path()->isNotEmpty();
}
/**
* @return bool
*/
public function hasQuery(): bool
{
return $this->query()->isNotEmpty();
}
/**
* @return bool
*/
public function https(): bool
{
return $this->scheme() === 'https';
@@ -270,7 +325,7 @@ class Uri
*
* @return $this
*/
public function idn(): static
public function idn()
{
if (empty($this->host) === false) {
$this->setHost(Idn::decode($this->host));
@@ -281,8 +336,11 @@ class Uri
/**
* Creates an Uri object for the URL to the index.php
* or any other executed script.
*
* @param array $props
* @return static
*/
public static function index(array $props = []): static
public static function index(array $props = [])
{
if ($app = App::instance(null, true)) {
$url = $app->url('index');
@@ -295,6 +353,8 @@ class Uri
/**
* Checks if the host exists
*
* @return bool
*/
public function isAbsolute(): bool
{
@@ -302,27 +362,30 @@ class Uri
}
/**
* @param string|null $fragment
* @return $this
*/
public function setFragment(string|null $fragment = null): static
public function setFragment(string $fragment = null)
{
$this->fragment = $fragment ? ltrim($fragment, '#') : null;
return $this;
}
/**
* @param string $host
* @return $this
*/
public function setHost(string|null $host = null): static
public function setHost(string $host = null)
{
$this->host = $host;
return $this;
}
/**
* @param \Kirby\Http\Params|string|array|false|null $params
* @return $this
*/
public function setParams(Params|string|array|false|null $params = null): static
public function setParams($params = null)
{
// ensure that the special constructor value of `false`
// is never passed through as it's not supported by `Params`
@@ -330,34 +393,35 @@ class Uri
$params = [];
}
$this->params = $params instanceof Params ? $params : new Params($params);
$this->params = is_a($params, 'Kirby\Http\Params') === true ? $params : new Params($params);
return $this;
}
/**
* @param string|null $password
* @return $this
*/
public function setPassword(
#[SensitiveParameter]
string|null $password = null
): static {
public function setPassword(string $password = null)
{
$this->password = $password;
return $this;
}
/**
* @param \Kirby\Http\Path|string|array|null $path
* @return $this
*/
public function setPath(Path|string|array|null $path = null): static
public function setPath($path = null)
{
$this->path = $path instanceof Path ? $path : new Path($path);
$this->path = is_a($path, 'Kirby\Http\Path') === true ? $path : new Path($path);
return $this;
}
/**
* @param int|null $port
* @return $this
*/
public function setPort(int|null $port = null): static
public function setPort(int $port = null)
{
if ($port === 0) {
$port = null;
@@ -374,18 +438,20 @@ class Uri
}
/**
* @param \Kirby\Http\Query|string|array|null $query
* @return $this
*/
public function setQuery(Query|string|array|null $query = null): static
public function setQuery($query = null)
{
$this->query = $query instanceof Query ? $query : new Query($query);
$this->query = is_a($query, 'Kirby\Http\Query') === true ? $query : new Query($query);
return $this;
}
/**
* @param string $scheme
* @return $this
*/
public function setScheme(string|null $scheme = null): static
public function setScheme(string $scheme = null)
{
if ($scheme !== null && in_array($scheme, static::$schemes) === false) {
throw new InvalidArgumentException('Invalid URL scheme: ' . $scheme);
@@ -399,18 +465,20 @@ class Uri
* Set if a trailing slash should be added to
* the path when the URI is being built
*
* @param bool $slash
* @return $this
*/
public function setSlash(bool $slash = false): static
public function setSlash(bool $slash = false)
{
$this->slash = $slash;
return $this;
}
/**
* @param string|null $username
* @return $this
*/
public function setUsername(string|null $username = null): static
public function setUsername(string $username = null)
{
$this->username = $username;
return $this;
@@ -418,12 +486,14 @@ class Uri
/**
* Converts the Url object to an array
*
* @return array
*/
public function toArray(): array
{
$array = [];
foreach ($this->props as $key => $value) {
foreach ($this->propertyData as $key => $value) {
$value = $this->$key;
if (is_object($value) === true) {
@@ -443,6 +513,8 @@ class Uri
/**
* Returns the full URL as string
*
* @return string
*/
public function toString(): string
{
@@ -476,7 +548,7 @@ class Uri
*
* @return $this
*/
public function unIdn(): static
public function unIdn()
{
if (empty($this->host) === false) {
$this->setHost(Idn::encode($this->host));
@@ -488,6 +560,7 @@ class Uri
* Parses the path inside the props and extracts
* the params unless disabled
*
* @param array $props
* @return array Modified props array
*/
protected static function parsePath(array $props): array

View File

@@ -17,46 +17,57 @@ class Url
{
/**
* The base Url to build absolute Urls from
*
* @var string
*/
public static string|null $home = '/';
public static $home = '/';
/**
* The current Uri object as string
* The current Uri object
*
* @var Uri
*/
public static string|null $current = null;
public static $current = null;
/**
* Facade for all Uri object methods
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public static function __callStatic(string $method, array $arguments)
public static function __callStatic(string $method, $arguments)
{
$uri = new Uri($arguments[0] ?? static::current());
return $uri->$method(...array_slice($arguments, 1));
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|null $url = null
): string {
$url ??= static::current();
$uri = new Uri($url);
return $uri->clone($parts)->toString();
public static function build(array $parts = [], string $url = null): string
{
return (string)(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::toObject()->toString();
return static::$current = static::$current ?? static::toObject()->toString();
}
/**
* Returns the url for the current directory
*
* @return string
*/
public static function currentDir(): string
{
@@ -65,20 +76,20 @@ class Url
/**
* Tries to fix a broken url without protocol
* @psalm-return ($url is null ? string|null : string)
*
* @param string|null $url
* @return string
*/
public static function fix(string|null $url = null): string|null
public static function fix(string $url = null): string
{
// make sure to not touch absolute urls
if (!preg_match('!^(https|http|ftp)\:\/\/!i', $url ?? '')) {
return 'http://' . $url;
}
return $url;
return (!preg_match('!^(https|http|ftp)\:\/\/!i', $url ?? '')) ? 'http://' . $url : $url;
}
/**
* Returns the home url if defined
*
* @return string
*/
public static function home(): string
{
@@ -87,6 +98,9 @@ class Url
/**
* Returns the url to the executed script
*
* @param array $props
* @return string
*/
public static function index(array $props = []): string
{
@@ -95,22 +109,27 @@ class Url
/**
* Checks if an URL is absolute
*
* @param string|null $url
* @return bool
*/
public static function isAbsolute(string|null $url = null): bool
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, geo:49.0158,8.3239?z=11
return
$url !== null &&
preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:|geo:)!i', $url) === 1;
return $url !== null && preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:|geo:)!i', $url) === 1;
}
/**
* Convert a relative path into an absolute URL
*
* @param string|null $path
* @param string|null $home
* @return string
*/
public static function makeAbsolute(string|null $path = null, string|null $home = null): string
public static function makeAbsolute(string $path = null, string $home = null): string
{
if ($path === '' || $path === '/' || $path === null) {
return $home ?? static::home();
@@ -137,27 +156,32 @@ class Url
/**
* Returns the path for the given url
*
* @param string|array|null $url
* @param bool $leadingSlash
* @param bool $trailingSlash
* @return string
*/
public static function path(
string|array|null $url = null,
bool $leadingSlash = false,
bool $trailingSlash = false
): string {
return Url::toObject($url)
->path()
->toString($leadingSlash, $trailingSlash);
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 string
*/
public static function query(string|array|null $url = null): string
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
{
@@ -166,13 +190,15 @@ class Url
/**
* Shortens the Url by removing all unnecessary parts
*
* @param string $url
* @param int $length
* @param bool $base
* @param string $rep
* @return string
*/
public static function short(
string|null $url = null,
int $length = 0,
bool $base = false,
string $rep = '…'
): string {
public static function short($url = null, int $length = 0, bool $base = false, string $rep = '…'): string
{
$uri = static::toObject($url);
$uri->fragment = null;
@@ -186,42 +212,53 @@ class Url
$uri->slash = false;
$url = $base ? $uri->base() : $uri->toString();
$url = str_replace('www.', '', $url ?? '');
$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(string|null $url = null): 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(string|null $url = null): 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(string|null $url = null): 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 mixed $options
* @return string
*/
public static function to(
string|null $path = null,
array $options = null
): string {
public static function to(string $path = null, $options = null): string
{
// make sure $path is string
$path ??= '';
@@ -241,8 +278,11 @@ class Url
/**
* Converts the Url to a Uri object
*
* @param string $url
* @return \Kirby\Http\Uri
*/
public static function toObject(string|null $url = null): Uri
public static function toObject($url = null)
{
return $url === null ? Uri::current() : new Uri($url);
}

View File

@@ -20,10 +20,29 @@ use Kirby\Toolkit\Str;
*/
class Visitor
{
protected string|null $ip = null;
protected string|null $userAgent = null;
protected string|null $acceptedLanguage = null;
protected string|null $acceptedMimeType = null;
/**
* 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.
@@ -31,22 +50,15 @@ class Visitor
* modify the information about the visitor.
*
* By default everything is pulled from $_SERVER
*
* @param array $arguments
*/
public function __construct(array $arguments = [])
{
$ip = $arguments['ip'] ?? null;
$ip ??= Environment::getGlobally('REMOTE_ADDR', '');
$agent = $arguments['userAgent'] ?? null;
$agent ??= Environment::getGlobally('HTTP_USER_AGENT', '');
$language = $arguments['acceptedLanguage'] ?? null;
$language ??= Environment::getGlobally('HTTP_ACCEPT_LANGUAGE', '');
$mime = $arguments['acceptedMimeType'] ?? null;
$mime ??= Environment::getGlobally('HTTP_ACCEPT', '');
$this->ip($ip);
$this->userAgent($agent);
$this->acceptedLanguage($language);
$this->acceptedMimeType($mime);
$this->ip($arguments['ip'] ?? Environment::getGlobally('REMOTE_ADDR', ''));
$this->userAgent($arguments['userAgent'] ?? Environment::getGlobally('HTTP_USER_AGENT', ''));
$this->acceptedLanguage($arguments['acceptedLanguage'] ?? Environment::getGlobally('HTTP_ACCEPT_LANGUAGE', ''));
$this->acceptedMimeType($arguments['acceptedMimeType'] ?? Environment::getGlobally('HTTP_ACCEPT', ''));
}
/**
@@ -54,11 +66,11 @@ class Visitor
* provided or returns the user's
* accepted language otherwise
*
* @return $this|\Kirby\Toolkit\Obj|null
* @param string|null $acceptedLanguage
* @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor|null
*/
public function acceptedLanguage(
string|null $acceptedLanguage = null
): static|Obj|null {
public function acceptedLanguage(string $acceptedLanguage = null)
{
if ($acceptedLanguage === null) {
return $this->acceptedLanguages()->first();
}
@@ -70,8 +82,10 @@ class Visitor
/**
* Returns an array of all accepted languages
* including their quality and locale
*
* @return \Kirby\Toolkit\Collection
*/
public function acceptedLanguages(): Collection
public function acceptedLanguages()
{
$accepted = Str::accepted($this->acceptedLanguage);
$languages = [];
@@ -97,6 +111,9 @@ class Visitor
/**
* Checks if the user accepts the given language
*
* @param string $code
* @return bool
*/
public function acceptsLanguage(string $code): bool
{
@@ -116,11 +133,11 @@ class Visitor
* provided or returns the user's
* accepted mime type otherwise
*
* @return $this|\Kirby\Toolkit\Obj|null
* @param string|null $acceptedMimeType
* @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor
*/
public function acceptedMimeType(
string|null $acceptedMimeType = null
): static|Obj|null {
public function acceptedMimeType(string $acceptedMimeType = null)
{
if ($acceptedMimeType === null) {
return $this->acceptedMimeTypes()->first();
}
@@ -131,8 +148,10 @@ class Visitor
/**
* Returns a collection of all accepted mime types
*
* @return \Kirby\Toolkit\Collection
*/
public function acceptedMimeTypes(): Collection
public function acceptedMimeTypes()
{
$accepted = Str::accepted($this->acceptedMimeType);
$mimes = [];
@@ -149,6 +168,9 @@ class Visitor
/**
* Checks if the user accepts the given mime type
*
* @param string $mimeType
* @return bool
*/
public function acceptsMimeType(string $mimeType): bool
{
@@ -163,7 +185,7 @@ class Visitor
* @param string ...$mimeTypes MIME types to query for
* @return string|null Preferred MIME type
*/
public function preferredMimeType(string ...$mimeTypes): string|null
public function preferredMimeType(string ...$mimeTypes): ?string
{
foreach ($this->acceptedMimeTypes() as $acceptedMime) {
// look for direct matches
@@ -186,11 +208,12 @@ class Visitor
* Returns true if the visitor prefers a JSON response over
* an HTML response based on the `Accept` request header
* @since 3.3.0
*
* @return bool
*/
public function prefersJson(): bool
{
$preferred = $this->preferredMimeType('application/json', 'text/html');
return $preferred === 'application/json';
return $this->preferredMimeType('application/json', 'text/html') === 'application/json';
}
/**
@@ -198,14 +221,14 @@ class Visitor
* or returns the ip of the current
* visitor otherwise
*
* @return $this|string|null
* @param string|null $ip
* @return string|Visitor|null
*/
public function ip(string|null $ip = null): static|string|null
public function ip(string $ip = null)
{
if ($ip === null) {
return $this->ip;
}
$this->ip = $ip;
return $this;
}
@@ -215,14 +238,14 @@ class Visitor
* or returns the user agent string of
* the current visitor otherwise
*
* @return $this|string|null
* @param string|null $userAgent
* @return string|Visitor|null
*/
public function userAgent(string|null $userAgent = null): static|string|null
public function userAgent(string $userAgent = null)
{
if ($userAgent === null) {
return $this->userAgent;
}
$this->userAgent = $userAgent;
return $this;
}