xiaowang/kirby/src/Http/Request.php

408 lines
8.7 KiB
PHP
Raw Normal View History

2021-10-29 18:05:46 +02:00
<?php
namespace Kirby\Http;
2022-08-31 16:08:03 +02:00
use Kirby\Cms\App;
2022-12-19 16:26:24 +01:00
use Kirby\Http\Request\Auth;
2021-10-29 18:05:46 +02:00
use Kirby\Http\Request\Body;
use Kirby\Http\Request\Files;
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 https://getkirby.com
2022-03-22 15:39:39 +01:00
* @copyright Bastian Allgeier
2021-10-29 18:05:46 +02:00
* @license https://opensource.org/licenses/MIT
*/
class Request
{
2022-12-19 16:26:24 +01:00
public static array $authTypes = [
2022-08-31 16:08:03 +02:00
'basic' => 'Kirby\Http\Request\Auth\BasicAuth',
'bearer' => 'Kirby\Http\Request\Auth\BearerAuth',
'session' => 'Kirby\Http\Request\Auth\SessionAuth',
];
/**
* The auth object if available
*/
2022-12-19 16:26:24 +01:00
protected Auth|false|null $auth = null;
2022-08-31 16:08:03 +02:00
/**
* 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')`
*/
2022-12-19 16:26:24 +01:00
protected Body|null $body = null;
2022-08-31 16:08:03 +02:00
/**
* 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']`
*/
2022-12-19 16:26:24 +01:00
protected Files|null $files = null;
2022-08-31 16:08:03 +02:00
/**
* The Method type
*/
2022-12-19 16:26:24 +01:00
protected string $method;
2022-08-31 16:08:03 +02:00
/**
* All options that have been passed to
* the request in the constructor
*/
2022-12-19 16:26:24 +01:00
protected array $options;
2022-08-31 16:08:03 +02:00
/**
* 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')`
*/
2022-12-19 16:26:24 +01:00
protected Query $query;
2022-08-31 16:08:03 +02:00
/**
* Request URL object
*/
2022-12-19 16:26:24 +01:00
protected Uri $url;
2022-08-31 16:08:03 +02:00
/**
* 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.
*/
public function __construct(array $options = [])
{
$this->options = $options;
$this->method = $this->detectRequestMethod($options['method'] ?? null);
if (isset($options['body']) === true) {
2022-12-19 16:26:24 +01:00
$this->body = $options['body'] instanceof Body ? $options['body'] : new Body($options['body']);
2022-08-31 16:08:03 +02:00
}
if (isset($options['files']) === true) {
2022-12-19 16:26:24 +01:00
$this->files = $options['files'] instanceof Files ? $options['files'] : new Files($options['files']);
2022-08-31 16:08:03 +02:00
}
if (isset($options['query']) === true) {
2022-12-19 16:26:24 +01:00
$this->query = $options['query'] instanceof Query ? $options['query'] : new Query($options['query']);
2022-08-31 16:08:03 +02:00
}
if (isset($options['url']) === true) {
2022-12-19 16:26:24 +01:00
$this->url = $options['url'] instanceof Uri ? $options['url'] : new Uri($options['url']);
2022-08-31 16:08:03 +02:00
}
}
/**
* Improved `var_dump` output
*/
public function __debugInfo(): array
{
return [
'body' => $this->body(),
'files' => $this->files(),
'method' => $this->method(),
'query' => $this->query(),
'url' => $this->url()->toString()
];
}
/**
* Returns the Auth object if authentication is set
*/
2022-12-19 16:26:24 +01:00
public function auth(): Auth|false|null
2022-08-31 16:08:03 +02:00
{
if ($this->auth !== null) {
return $this->auth;
}
// lazily request the instance for non-CMS use cases
$kirby = App::instance(null, true);
// tell the CMS responder that the response relies on
// the `Authorization` header and its value (even if
// the header isn't set in the current request);
// this ensures that the response is only cached for
// unauthenticated visitors;
// https://github.com/getkirby/kirby/issues/4423#issuecomment-1166300526
2023-04-14 16:30:28 +02:00
$kirby?->response()->usesAuth(true);
2022-08-31 16:08:03 +02:00
2022-12-19 16:26:24 +01:00
if ($auth = $this->authString()) {
2022-08-31 16:08:03 +02:00
$type = Str::lower(Str::before($auth, ' '));
$data = Str::after($auth, ' ');
$class = static::$authTypes[$type] ?? null;
if (!$class || class_exists($class) === false) {
return $this->auth = false;
}
$object = new $class($data);
return $this->auth = $object;
}
return $this->auth = false;
}
/**
* Returns the Body object
*/
2022-12-19 16:26:24 +01:00
public function body(): Body
2022-08-31 16:08:03 +02:00
{
return $this->body ??= new Body();
}
/**
* Checks if the request has been made from the command line
*/
public function cli(): bool
{
return $this->options['cli'] ?? (new Environment())->cli();
}
/**
* Returns a CSRF token if stored in a header or the query
*/
2022-12-19 16:26:24 +01:00
public function csrf(): string|null
2022-08-31 16:08:03 +02:00
{
return $this->header('x-csrf') ?? $this->query()->get('csrf');
}
/**
* Returns the request input as array
*/
public function data(): array
{
2024-12-20 12:37:35 +01:00
return array_replace($this->body()->toArray(), $this->query()->toArray());
2022-08-31 16:08:03 +02:00
}
/**
* Detect the request method from various
* options: given method, query string, server vars
*/
2022-12-19 16:26:24 +01:00
public function detectRequestMethod(string|null $method = null): string
2022-08-31 16:08:03 +02:00
{
// all possible methods
$methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'];
// the request method can be overwritten with a header
$methodOverride = strtoupper(Environment::getGlobally('HTTP_X_HTTP_METHOD_OVERRIDE', ''));
2024-12-20 12:37:35 +01:00
if (in_array($methodOverride, $methods) === true) {
$method ??= $methodOverride;
2022-08-31 16:08:03 +02:00
}
// final chain of options to detect the method
$method = $method ?? Environment::getGlobally('REQUEST_METHOD', 'GET');
// uppercase the shit out of it
$method = strtoupper($method);
// sanitize the method
if (in_array($method, $methods) === false) {
$method = 'GET';
}
return $method;
}
/**
* Returns the domain
*/
public function domain(): string
{
return $this->url()->domain();
}
/**
* Fetches a single file array
* from the Files object by key
*/
2022-12-19 16:26:24 +01:00
public function file(string $key): array|null
2022-08-31 16:08:03 +02:00
{
return $this->files()->get($key);
}
/**
* Returns the Files object
*/
2022-12-19 16:26:24 +01:00
public function files(): Files
2022-08-31 16:08:03 +02:00
{
return $this->files ??= new Files();
}
/**
* Returns any data field from the request
* if it exists
*/
2022-12-19 16:26:24 +01:00
public function get(string|array|null $key = null, $fallback = null)
2022-08-31 16:08:03 +02:00
{
return A::get($this->data(), $key, $fallback);
}
/**
* Returns whether the request contains
* the `Authorization` header
* @since 3.7.0
*/
public function hasAuth(): bool
{
2022-12-19 16:26:24 +01:00
return $this->authString() !== null;
2022-08-31 16:08:03 +02:00
}
/**
* Returns a header by key if it exists
*/
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
*/
public function headers(): array
{
$headers = [];
foreach (Environment::getGlobally() as $key => $value) {
2023-04-14 16:30:28 +02:00
if (
substr($key, 0, 5) !== 'HTTP_' &&
substr($key, 0, 14) !== 'REDIRECT_HTTP_'
) {
2022-08-31 16:08:03 +02:00
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.
*/
public function is(string $method): bool
{
return strtoupper($this->method) === strtoupper($method);
}
/**
* Returns the request method
*/
public function method(): string
{
return $this->method;
}
/**
* Shortcut to the Params object
*/
2022-12-19 16:26:24 +01:00
public function params(): Params
2022-08-31 16:08:03 +02:00
{
return $this->url()->params();
}
/**
* Shortcut to the Path object
*/
2022-12-19 16:26:24 +01:00
public function path(): Path
2022-08-31 16:08:03 +02:00
{
return $this->url()->path();
}
/**
* Returns the Query object
*/
2022-12-19 16:26:24 +01:00
public function query(): Query
2022-08-31 16:08:03 +02:00
{
return $this->query ??= new Query();
}
/**
* Checks for a valid SSL connection
*/
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.
*/
2022-12-19 16:26:24 +01:00
public function url(array|null $props = null): Uri
2022-08-31 16:08:03 +02:00
{
if ($props !== null) {
return $this->url()->clone($props);
}
return $this->url ??= Uri::current();
}
2022-12-19 16:26:24 +01:00
/**
* 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;
}
2021-10-29 18:05:46 +02:00
}