Update website

This commit is contained in:
Guilhem Lavaux 2024-11-23 20:45:29 +01:00
parent 41ce1aa076
commit ea0eb1c6e0
4222 changed files with 721797 additions and 14 deletions

View file

@ -0,0 +1,221 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use function array_key_exists;
use function array_replace;
use function count;
use function explode;
use function gmdate;
use function in_array;
use function is_array;
use function is_string;
use function preg_split;
use function rtrim;
use function strtolower;
use function strtotime;
use function urldecode;
use function urlencode;
class Cookies
{
/**
* Cookies from HTTP request
*
* @var array
*/
protected $requestCookies = [];
/**
* Cookies for HTTP response
*
* @var array
*/
protected $responseCookies = [];
/**
* Default cookie properties
*
* @var array
*/
protected $defaults = [
'value' => '',
'domain' => null,
'hostonly' => null,
'path' => null,
'expires' => null,
'secure' => false,
'httponly' => false,
'samesite' => null
];
/**
* @param array $cookies
*/
public function __construct(array $cookies = [])
{
$this->requestCookies = $cookies;
}
/**
* Set default cookie properties
*
* @param array $settings
*
* @return static
*/
public function setDefaults(array $settings): self
{
$this->defaults = array_replace($this->defaults, $settings);
return $this;
}
/**
* Get cookie
*
* @param string $name
* @param string|array|null $default
* @return mixed|null
*/
public function get(string $name, $default = null)
{
return array_key_exists($name, $this->requestCookies) ? $this->requestCookies[$name] : $default;
}
/**
* Set cookie
*
* @param string $name
* @param string|array $value
* @return static
*/
public function set(string $name, $value): self
{
if (!is_array($value)) {
$value = ['value' => $value];
}
$this->responseCookies[$name] = array_replace($this->defaults, $value);
return $this;
}
/**
* Convert all response cookies into an associate array of header values
*
* @return array
*/
public function toHeaders(): array
{
$headers = [];
foreach ($this->responseCookies as $name => $properties) {
$headers[] = $this->toHeader($name, $properties);
}
return $headers;
}
/**
* Convert to `Set-Cookie` header
*
* @param string $name Cookie name
* @param array $properties Cookie properties
*
* @return string
*/
protected function toHeader(string $name, array $properties): string
{
$result = urlencode($name) . '=' . urlencode($properties['value']);
if (isset($properties['domain'])) {
$result .= '; domain=' . $properties['domain'];
}
if (isset($properties['path'])) {
$result .= '; path=' . $properties['path'];
}
if (isset($properties['expires'])) {
if (is_string($properties['expires'])) {
$timestamp = strtotime($properties['expires']);
} else {
$timestamp = (int) $properties['expires'];
}
if ($timestamp && $timestamp !== 0) {
$result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp);
}
}
if (isset($properties['secure']) && $properties['secure']) {
$result .= '; secure';
}
if (isset($properties['hostonly']) && $properties['hostonly']) {
$result .= '; HostOnly';
}
if (isset($properties['httponly']) && $properties['httponly']) {
$result .= '; HttpOnly';
}
if (isset($properties['samesite']) && in_array(strtolower($properties['samesite']), ['lax', 'strict'], true)) {
// While strtolower is needed for correct comparison, the RFC doesn't care about case
$result .= '; SameSite=' . $properties['samesite'];
}
return $result;
}
/**
* Parse cookie values from header value
*
* Returns an associative array of cookie names and values
*
* @param string|array $header
*
* @return array
*/
public static function parseHeader($header): array
{
if (is_array($header)) {
$header = isset($header[0]) ? $header[0] : '';
}
if (!is_string($header)) {
throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.');
}
$header = rtrim($header, "\r\n");
$pieces = preg_split('@[;]\s*@', $header);
$cookies = [];
if (is_array($pieces)) {
foreach ($pieces as $cookie) {
$cookie = explode('=', $cookie, 2);
if (count($cookie) === 2) {
$key = urldecode($cookie[0]);
$value = urldecode($cookie[1]);
if (!isset($cookies[$key])) {
$cookies[$key] = $value;
}
}
}
}
return $cookies;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use function array_merge;
use function microtime;
use function time;
class Environment
{
/**
* @param array $data Array of custom environment keys and values
*
* @return array
*/
public static function mock(array $data = []): array
{
if (
(isset($data['HTTPS']) && $data['HTTPS'] !== 'off')
|| ((isset($data['REQUEST_SCHEME']) && $data['REQUEST_SCHEME'] === 'https'))
) {
$scheme = 'https';
$port = 443;
} else {
$scheme = 'http';
$port = 80;
}
return array_merge([
'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8',
'HTTP_USER_AGENT' => 'Slim Framework',
'QUERY_STRING' => '',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_METHOD' => 'GET',
'REQUEST_SCHEME' => $scheme,
'REQUEST_TIME' => time(),
'REQUEST_TIME_FLOAT' => microtime(true),
'REQUEST_URI' => '',
'SCRIPT_NAME' => '',
'SERVER_NAME' => 'localhost',
'SERVER_PORT' => $port,
'SERVER_PROTOCOL' => 'HTTP/1.1',
], $data);
}
}

View file

@ -0,0 +1,73 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Factory;
use InvalidArgumentException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Slim\Psr7\Headers;
use Slim\Psr7\Request;
use function is_string;
class RequestFactory implements RequestFactoryInterface
{
/**
* @var StreamFactoryInterface|StreamFactory
*/
protected $streamFactory;
/**
* @var UriFactoryInterface|UriFactory
*/
protected $uriFactory;
/**
* @param StreamFactoryInterface|null $streamFactory
* @param UriFactoryInterface|null $uriFactory
*/
public function __construct(?StreamFactoryInterface $streamFactory = null, ?UriFactoryInterface $uriFactory = null)
{
if (!isset($streamFactory)) {
$streamFactory = new StreamFactory();
}
if (!isset($uriFactory)) {
$uriFactory = new UriFactory();
}
$this->streamFactory = $streamFactory;
$this->uriFactory = $uriFactory;
}
/**
* {@inheritdoc}
*/
public function createRequest(string $method, $uri): RequestInterface
{
if (is_string($uri)) {
$uri = $this->uriFactory->createUri($uri);
}
if (!$uri instanceof UriInterface) {
throw new InvalidArgumentException(
'Parameter 2 of RequestFactory::createRequest() must be a string or a compatible UriInterface.'
);
}
$body = $this->streamFactory->createStream();
return new Request($method, $uri, new Headers(), [], [], $body);
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Factory;
use Fig\Http\Message\StatusCodeInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Slim\Psr7\Response;
class ResponseFactory implements ResponseFactoryInterface
{
/**
* {@inheritdoc}
*/
public function createResponse(
int $code = StatusCodeInterface::STATUS_OK,
string $reasonPhrase = ''
): ResponseInterface {
$res = new Response($code);
if ($reasonPhrase !== '') {
$res = $res->withStatus($code, $reasonPhrase);
}
return $res;
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Factory;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Slim\Psr7\Cookies;
use Slim\Psr7\Headers;
use Slim\Psr7\Request;
use Slim\Psr7\Stream;
use Slim\Psr7\UploadedFile;
use function current;
use function explode;
use function fopen;
use function in_array;
use function is_string;
class ServerRequestFactory implements ServerRequestFactoryInterface
{
/**
* @var StreamFactoryInterface|StreamFactory
*/
protected $streamFactory;
/**
* @var UriFactoryInterface|UriFactory
*/
protected $uriFactory;
/**
* @param StreamFactoryInterface|null $streamFactory
* @param UriFactoryInterface|null $uriFactory
*/
public function __construct(?StreamFactoryInterface $streamFactory = null, ?UriFactoryInterface $uriFactory = null)
{
if (!isset($streamFactory)) {
$streamFactory = new StreamFactory();
}
if (!isset($uriFactory)) {
$uriFactory = new UriFactory();
}
$this->streamFactory = $streamFactory;
$this->uriFactory = $uriFactory;
}
/**
* {@inheritdoc}
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
if (is_string($uri)) {
$uri = $this->uriFactory->createUri($uri);
}
if (!$uri instanceof UriInterface) {
throw new InvalidArgumentException('URI must either be string or instance of ' . UriInterface::class);
}
$body = $this->streamFactory->createStream();
$headers = new Headers();
$cookies = [];
if (!empty($serverParams)) {
$headers = Headers::createFromGlobals();
$cookies = Cookies::parseHeader($headers->getHeader('Cookie', []));
}
return new Request($method, $uri, $headers, $cookies, $serverParams, $body);
}
/**
* Create new ServerRequest from environment.
*
* @internal This method is not part of PSR-17
*
* @return Request
*/
public static function createFromGlobals(): Request
{
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
$uri = (new UriFactory())->createFromGlobals($_SERVER);
$headers = Headers::createFromGlobals();
$cookies = Cookies::parseHeader($headers->getHeader('Cookie', []));
// Cache the php://input stream as it cannot be re-read
$cacheResource = fopen('php://temp', 'wb+');
$cache = $cacheResource ? new Stream($cacheResource) : null;
$body = (new StreamFactory())->createStreamFromFile('php://input', 'r', $cache);
$uploadedFiles = UploadedFile::createFromGlobals($_SERVER);
$request = new Request($method, $uri, $headers, $cookies, $_SERVER, $body, $uploadedFiles);
$contentTypes = $request->getHeader('Content-Type') ?? [];
$parsedContentType = '';
foreach ($contentTypes as $contentType) {
$fragments = explode(';', $contentType);
$parsedContentType = current($fragments);
}
$contentTypesWithParsedBodies = ['application/x-www-form-urlencoded', 'multipart/form-data'];
if ($method === 'POST' && in_array($parsedContentType, $contentTypesWithParsedBodies)) {
return $request->withParsedBody($_POST);
}
return $request;
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Factory;
use InvalidArgumentException;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use Slim\Psr7\Stream;
use ValueError;
use function fopen;
use function fwrite;
use function is_resource;
use function restore_error_handler;
use function rewind;
use function set_error_handler;
class StreamFactory implements StreamFactoryInterface
{
/**
* {@inheritdoc}
*
* @throws RuntimeException
*/
public function createStream(string $content = ''): StreamInterface
{
$resource = fopen('php://temp', 'rw+');
if (!is_resource($resource)) {
throw new RuntimeException('StreamFactory::createStream() could not open temporary file stream.');
}
fwrite($resource, $content);
rewind($resource);
return $this->createStreamFromResource($resource);
}
/**
* {@inheritdoc}
*/
public function createStreamFromFile(
string $filename,
string $mode = 'r',
StreamInterface $cache = null
): StreamInterface {
set_error_handler(
static function (int $errno, string $errstr) use ($filename, $mode): void {
throw new RuntimeException(
"Unable to open $filename using mode $mode: $errstr",
$errno
);
}
);
try {
$resource = fopen($filename, $mode);
} catch (ValueError $exception) {
throw new RuntimeException("Unable to open $filename using mode $mode: " . $exception->getMessage());
} finally {
restore_error_handler();
}
if (!is_resource($resource)) {
throw new RuntimeException(
"StreamFactory::createStreamFromFile() could not create resource from file `$filename`"
);
}
return new Stream($resource, $cache);
}
/**
* {@inheritdoc}
*/
public function createStreamFromResource($resource, StreamInterface $cache = null): StreamInterface
{
if (!is_resource($resource)) {
throw new InvalidArgumentException(
'Parameter 1 of StreamFactory::createStreamFromResource() must be a resource.'
);
}
return new Stream($resource, $cache);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Factory;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
use Slim\Psr7\UploadedFile;
use function is_string;
use const UPLOAD_ERR_OK;
class UploadedFileFactory implements UploadedFileFactoryInterface
{
/**
* {@inheritdoc}
*/
public function createUploadedFile(
StreamInterface $stream,
?int $size = null,
int $error = UPLOAD_ERR_OK,
?string $clientFilename = null,
?string $clientMediaType = null
): UploadedFileInterface {
$file = $stream->getMetadata('uri');
if (!is_string($file) || !$stream->isReadable()) {
throw new InvalidArgumentException('File is not readable.');
}
if ($size === null) {
$size = $stream->getSize();
}
return new UploadedFile($stream, $clientFilename, $clientMediaType, $size, $error);
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Factory;
use InvalidArgumentException;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Slim\Psr7\Uri;
use function count;
use function explode;
use function parse_url;
use function preg_match;
use function strpos;
use function strstr;
use function substr;
use const PHP_URL_QUERY;
class UriFactory implements UriFactoryInterface
{
/**
* {@inheritdoc}
*/
public function createUri(string $uri = ''): UriInterface
{
$parts = parse_url($uri);
if ($parts === false) {
throw new InvalidArgumentException('URI cannot be parsed');
}
$scheme = $parts['scheme'] ?? '';
$user = $parts['user'] ?? '';
$pass = $parts['pass'] ?? '';
$host = $parts['host'] ?? '';
$port = $parts['port'] ?? null;
$path = $parts['path'] ?? '';
$query = $parts['query'] ?? '';
$fragment = $parts['fragment'] ?? '';
return new Uri($scheme, $host, $port, $path, $query, $fragment, $user, $pass);
}
/**
* Create new Uri from environment.
*
* @internal This method is not part of PSR-17
*
* @param array $globals The global server variables.
*
* @return Uri
*/
public function createFromGlobals(array $globals): Uri
{
// Scheme
$https = isset($globals['HTTPS']) ? $globals['HTTPS'] : false;
$scheme = !$https || $https === 'off' ? 'http' : 'https';
// Authority: Username and password
$username = isset($globals['PHP_AUTH_USER']) ? $globals['PHP_AUTH_USER'] : '';
$password = isset($globals['PHP_AUTH_PW']) ? $globals['PHP_AUTH_PW'] : '';
// Authority: Host
$host = '';
if (isset($globals['HTTP_HOST'])) {
$host = $globals['HTTP_HOST'];
} elseif (isset($globals['SERVER_NAME'])) {
$host = $globals['SERVER_NAME'];
}
// Authority: Port
$port = !empty($globals['SERVER_PORT']) ? (int)$globals['SERVER_PORT'] : ($scheme === 'https' ? 443 : 80);
if (preg_match('/^(\[[a-fA-F0-9:.]+])(:\d+)?\z/', $host, $matches)) {
$host = $matches[1];
if (isset($matches[2])) {
$port = (int) substr($matches[2], 1);
}
} else {
$pos = strpos($host, ':');
if ($pos !== false) {
$port = (int) substr($host, $pos + 1);
$host = strstr($host, ':', true);
}
}
// Query string
$queryString = '';
if (isset($globals['QUERY_STRING'])) {
$queryString = $globals['QUERY_STRING'];
}
// Request URI
$requestUri = '';
if (isset($globals['REQUEST_URI'])) {
$uriFragments = explode('?', $globals['REQUEST_URI']);
$requestUri = $uriFragments[0];
if ($queryString === '' && count($uriFragments) > 1) {
$queryString = parse_url('http://www.example.com' . $globals['REQUEST_URI'], PHP_URL_QUERY) ?? '';
}
}
// Build Uri and return
return new Uri($scheme, $host, $port, $requestUri, $queryString, '', $username, $password);
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use function array_merge;
use function is_array;
use function is_string;
class Header
{
/**
* @var string
*/
private $originalName;
/**
* @var string
*/
private $normalizedName;
/**
* @var array
*/
private $values;
/**
* Header constructor.
*
* @param string $originalName
* @param string $normalizedName
* @param array $values
*/
public function __construct(string $originalName, string $normalizedName, array $values)
{
$this->originalName = $originalName;
$this->normalizedName = $normalizedName;
$this->values = $values;
}
/**
* @return string
*/
public function getOriginalName(): string
{
return $this->originalName;
}
/**
* @return string
*/
public function getNormalizedName(): string
{
return $this->normalizedName;
}
/**
* @param string $value
*
* @return self
*/
public function addValue(string $value): self
{
$this->values[] = $value;
return $this;
}
/**
* @param array|string $values
*
* @return self
*/
public function addValues($values): self
{
if (is_string($values)) {
return $this->addValue($values);
}
if (!is_array($values)) {
throw new InvalidArgumentException('Parameter 1 of Header::addValues() should be a string or an array.');
}
$this->values = array_merge($this->values, $values);
return $this;
}
/**
* @return array
*/
public function getValues(): array
{
return $this->values;
}
}

View file

@ -0,0 +1,321 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use Slim\Psr7\Interfaces\HeadersInterface;
use function base64_encode;
use function function_exists;
use function getallheaders;
use function is_array;
use function is_numeric;
use function is_string;
use function preg_match;
use function strpos;
use function strtolower;
use function strtr;
use function substr;
use function trim;
class Headers implements HeadersInterface
{
/**
* @var array
*/
protected $globals;
/**
* @var Header[]
*/
protected $headers;
/**
* @param array $headers
* @param array $globals
*/
final public function __construct(array $headers = [], ?array $globals = null)
{
$this->globals = $globals ?? $_SERVER;
$this->setHeaders($headers);
}
/**
* {@inheritdoc}
*/
public function addHeader($name, $value): HeadersInterface
{
[$values, $originalName, $normalizedName] = $this->prepareHeader($name, $value);
if (isset($this->headers[$normalizedName])) {
$header = $this->headers[$normalizedName];
$header->addValues($values);
} else {
$this->headers[$normalizedName] = new Header($originalName, $normalizedName, $values);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function removeHeader(string $name): HeadersInterface
{
$name = $this->normalizeHeaderName($name);
unset($this->headers[$name]);
return $this;
}
/**
* {@inheritdoc}
*/
public function getHeader(string $name, $default = []): array
{
$name = $this->normalizeHeaderName($name);
if (isset($this->headers[$name])) {
$header = $this->headers[$name];
return $header->getValues();
}
if (empty($default)) {
return $default;
}
$this->validateHeader($name, $default);
return $this->trimHeaderValue($default);
}
/**
* {@inheritdoc}
*/
public function setHeader($name, $value): HeadersInterface
{
[$values, $originalName, $normalizedName] = $this->prepareHeader($name, $value);
// Ensure we preserve original case if the header already exists in the stack
if (isset($this->headers[$normalizedName])) {
$existingHeader = $this->headers[$normalizedName];
$originalName = $existingHeader->getOriginalName();
}
$this->headers[$normalizedName] = new Header($originalName, $normalizedName, $values);
return $this;
}
/**
* {@inheritdoc}
*/
public function setHeaders(array $headers): HeadersInterface
{
$this->headers = [];
foreach ($this->parseAuthorizationHeader($headers) as $name => $value) {
$this->addHeader($name, $value);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function hasHeader(string $name): bool
{
$name = $this->normalizeHeaderName($name);
return isset($this->headers[$name]);
}
/**
* {@inheritdoc}
*/
public function getHeaders(bool $originalCase = false): array
{
$headers = [];
foreach ($this->headers as $header) {
$name = $originalCase ? $header->getOriginalName() : $header->getNormalizedName();
$headers[$name] = $header->getValues();
}
return $headers;
}
/**
* @param string $name
* @param bool $preserveCase
* @return string
*/
protected function normalizeHeaderName(string $name, bool $preserveCase = false): string
{
$name = strtr($name, '_', '-');
if (!$preserveCase) {
$name = strtolower($name);
}
if (strpos(strtolower($name), 'http-') === 0) {
$name = substr($name, 5);
}
return $name;
}
/**
* Parse incoming headers and determine Authorization header from original headers
*
* @param array $headers
* @return array
*/
protected function parseAuthorizationHeader(array $headers): array
{
$hasAuthorizationHeader = false;
foreach ($headers as $name => $value) {
if (strtolower($name) === 'authorization') {
$hasAuthorizationHeader = true;
break;
}
}
if (!$hasAuthorizationHeader) {
if (isset($this->globals['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $this->globals['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (isset($this->globals['PHP_AUTH_USER'])) {
$pw = isset($this->globals['PHP_AUTH_PW']) ? $this->globals['PHP_AUTH_PW'] : '';
$headers['Authorization'] = 'Basic ' . base64_encode($this->globals['PHP_AUTH_USER'] . ':' . $pw);
} elseif (isset($this->globals['PHP_AUTH_DIGEST'])) {
$headers['Authorization'] = $this->globals['PHP_AUTH_DIGEST'];
}
}
return $headers;
}
/**
* @param array|string $value
*
* @return array
*/
protected function trimHeaderValue($value): array
{
$items = is_array($value) ? $value : [$value];
$result = [];
foreach ($items as $item) {
$result[] = trim((string) $item, " \t");
}
return $result;
}
/**
* @param string $name
* @param array|string $value
*
* @throws InvalidArgumentException
*
* @return array
*/
protected function prepareHeader($name, $value): array
{
$this->validateHeader($name, $value);
$values = $this->trimHeaderValue($value);
$originalName = $this->normalizeHeaderName($name, true);
$normalizedName = $this->normalizeHeaderName($name);
return [$values, $originalName, $normalizedName];
}
/**
* Make sure the header complies with RFC 7230.
*
* Header names must be a non-empty string consisting of token characters.
*
* Header values must be strings consisting of visible characters with all optional
* leading and trailing whitespace stripped. This method will always strip such
* optional whitespace. Note that the method does not allow folding whitespace within
* the values as this was deprecated for almost all instances by the RFC.
*
* header-field = field-name ":" OWS field-value OWS
* field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
* / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
* OWS = *( SP / HTAB )
* field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
*
* @see https://tools.ietf.org/html/rfc7230#section-3.2.4
*
* @param string $name
* @param array|string $value
*
* @throws InvalidArgumentException;
*/
protected function validateHeader($name, $value): void
{
$this->validateHeaderName($name);
$this->validateHeaderValue($value);
}
/**
* @param mixed $name
*
* @throws InvalidArgumentException
*/
protected function validateHeaderName($name): void
{
if (!is_string($name) || preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $name) !== 1) {
throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
}
}
/**
* @param mixed $value
*
* @throws InvalidArgumentException
*/
protected function validateHeaderValue($value): void
{
$items = is_array($value) ? $value : [$value];
if (empty($items)) {
throw new InvalidArgumentException(
'Header values must be a string or an array of strings, empty array given.'
);
}
$pattern = "@^[ \t\x21-\x7E\x80-\xFF]*$@";
foreach ($items as $item) {
$hasInvalidType = !is_numeric($item) && !is_string($item);
$rejected = $hasInvalidType || preg_match($pattern, (string) $item) !== 1;
if ($rejected) {
throw new InvalidArgumentException(
'Header values must be RFC 7230 compatible strings.'
);
}
}
}
/**
* @return static
*/
public static function createFromGlobals()
{
$headers = null;
if (function_exists('getallheaders')) {
$headers = getallheaders();
}
if (!is_array($headers)) {
$headers = [];
}
return new static($headers);
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7\Interfaces;
use InvalidArgumentException;
interface HeadersInterface
{
/**
* Add header value
*
* This method appends the value to the existing array of values
*
* @param string $name
* @param array|string $value
*
* @return HeadersInterface
*
* @throws InvalidArgumentException
*/
public function addHeader($name, $value): HeadersInterface;
/**
* Remove header value
*
* @param string $name
* @return HeadersInterface
*/
public function removeHeader(string $name): HeadersInterface;
/**
* Get header value or values.
* If the array has a single value it will return that single value.
* If the array has multiple values, it will return an array of values.
*
* @param string $name
* @param string[] $default
*
* @return array
*/
public function getHeader(string $name, $default = []): array;
/**
* Replaces the existing header value with the new value.
*
* @param string $name
* @param array|string $value
*
* @return HeadersInterface
*
* @throws InvalidArgumentException
*/
public function setHeader($name, $value): HeadersInterface;
/**
* Replaces all existing headers with the new values.
*
* @param array $headers
*
* @return HeadersInterface
*
* @throws InvalidArgumentException
*/
public function setHeaders(array $headers): HeadersInterface;
/**
* Is the header present in the stack.
*
* @param string $name
* @return bool
*/
public function hasHeader(string $name): bool;
/**
* Return all headers in the stack.
*
* @param bool $originalCase
*
* @return array
*/
public function getHeaders(bool $originalCase): array;
}

View file

@ -0,0 +1,191 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
use Slim\Psr7\Interfaces\HeadersInterface;
use function array_keys;
use function header;
use function header_remove;
use function implode;
use function sprintf;
abstract class Message implements MessageInterface
{
/**
* @var string
*/
protected $protocolVersion = '1.1';
/**
* @var array
*/
protected static $validProtocolVersions = [
'1.0' => true,
'1.1' => true,
'2.0' => true,
'2' => true,
];
/**
* @var HeadersInterface
*/
protected $headers;
/**
* @var StreamInterface
*/
protected $body;
/**
* Disable magic setter to ensure immutability
*
* @param string $name The property name
* @param mixed $value The property value
*
* @return void
*/
public function __set($name, $value): void
{
// Do nothing
}
/**
* {@inheritdoc}
*/
public function getProtocolVersion(): string
{
return $this->protocolVersion;
}
/**
* @return static
* {@inheritdoc}
*/
public function withProtocolVersion($version)
{
if (!isset(self::$validProtocolVersions[$version])) {
throw new InvalidArgumentException(
'Invalid HTTP version. Must be one of: '
. implode(', ', array_keys(self::$validProtocolVersions))
);
}
$clone = clone $this;
$clone->protocolVersion = $version;
return $clone;
}
/**
* {@inheritdoc}
*/
public function getHeaders(): array
{
return $this->headers->getHeaders(true);
}
/**
* {@inheritdoc}
*/
public function hasHeader($name): bool
{
return $this->headers->hasHeader($name);
}
/**
* {@inheritdoc}
*/
public function getHeader($name): array
{
return $this->headers->getHeader($name);
}
/**
* {@inheritdoc}
*/
public function getHeaderLine($name): string
{
$values = $this->headers->getHeader($name);
return implode(',', $values);
}
/**
* @return static
* {@inheritdoc}
*/
public function withHeader($name, $value)
{
$clone = clone $this;
$clone->headers->setHeader($name, $value);
if ($this instanceof Response && $this->body instanceof NonBufferedBody) {
header(sprintf('%s: %s', $name, $clone->getHeaderLine($name)));
}
return $clone;
}
/**
* @return static
* {@inheritdoc}
*/
public function withAddedHeader($name, $value)
{
$clone = clone $this;
$clone->headers->addHeader($name, $value);
if ($this instanceof Response && $this->body instanceof NonBufferedBody) {
header(sprintf('%s: %s', $name, $clone->getHeaderLine($name)));
}
return $clone;
}
/**
* @return static
* {@inheritdoc}
*/
public function withoutHeader($name)
{
$clone = clone $this;
$clone->headers->removeHeader($name);
if ($this instanceof Response && $this->body instanceof NonBufferedBody) {
header_remove($name);
}
return $clone;
}
/**
* {@inheritdoc}
*/
public function getBody(): StreamInterface
{
return $this->body;
}
/**
* @return static
* {@inheritdoc}
*/
public function withBody(StreamInterface $body)
{
$clone = clone $this;
$clone->body = $body;
return $clone;
}
}

View file

@ -0,0 +1,153 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use function flush;
use function ob_get_clean;
use function ob_get_level;
use function strlen;
use const SEEK_SET;
class NonBufferedBody implements StreamInterface
{
/**
* {@inheritdoc}
*/
public function __toString(): string
{
return '';
}
/**
* {@inheritdoc}
*/
public function close(): void
{
throw new RuntimeException('A NonBufferedBody is not closable.');
}
/**
* {@inheritdoc}
*/
public function detach()
{
return null;
}
/**
* {@inheritdoc}
*/
public function getSize(): ?int
{
return null;
}
/**
* {@inheritdoc}
*/
public function tell(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function eof(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function isSeekable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET): void
{
throw new RuntimeException('A NonBufferedBody is not seekable.');
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
throw new RuntimeException('A NonBufferedBody is not rewindable.');
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function write($string): int
{
$buffered = '';
while (0 < ob_get_level()) {
$buffered = ob_get_clean() . $buffered;
}
echo $buffered . $string;
flush();
return strlen($string) + strlen($buffered);
}
/**
* {@inheritdoc}
*/
public function isReadable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function read($length): string
{
throw new RuntimeException('A NonBufferedBody is not readable.');
}
/**
* {@inheritdoc}
*/
public function getContents(): string
{
return '';
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null): ?array
{
return null;
}
}

View file

@ -0,0 +1,383 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
use Slim\Psr7\Interfaces\HeadersInterface;
use function get_class;
use function gettype;
use function is_array;
use function is_null;
use function is_object;
use function is_string;
use function ltrim;
use function parse_str;
use function preg_match;
use function sprintf;
use function str_replace;
class Request extends Message implements ServerRequestInterface
{
/**
* @var string
*/
protected $method;
/**
* @var UriInterface
*/
protected $uri;
/**
* @var string
*/
protected $requestTarget;
/**
* @var ?array
*/
protected $queryParams;
/**
* @var array
*/
protected $cookies;
/**
* @var array
*/
protected $serverParams;
/**
* @var array
*/
protected $attributes;
/**
* @var null|array|object
*/
protected $parsedBody;
/**
* @var UploadedFileInterface[]
*/
protected $uploadedFiles;
/**
* @param string $method The request method
* @param UriInterface $uri The request URI object
* @param HeadersInterface $headers The request headers collection
* @param array $cookies The request cookies collection
* @param array $serverParams The server environment variables
* @param StreamInterface $body The request body object
* @param array $uploadedFiles The request uploadedFiles collection
* @throws InvalidArgumentException on invalid HTTP method
*/
public function __construct(
$method,
UriInterface $uri,
HeadersInterface $headers,
array $cookies,
array $serverParams,
StreamInterface $body,
array $uploadedFiles = []
) {
$this->method = $this->filterMethod($method);
$this->uri = $uri;
$this->headers = $headers;
$this->cookies = $cookies;
$this->serverParams = $serverParams;
$this->attributes = [];
$this->body = $body;
$this->uploadedFiles = $uploadedFiles;
if (isset($serverParams['SERVER_PROTOCOL'])) {
$this->protocolVersion = str_replace('HTTP/', '', $serverParams['SERVER_PROTOCOL']);
}
if (!$this->headers->hasHeader('Host') || $this->uri->getHost() !== '') {
$this->headers->setHeader('Host', $this->uri->getHost());
}
}
/**
* This method is applied to the cloned object after PHP performs an initial shallow-copy.
* This method completes a deep-copy by creating new objects for the cloned object's internal reference pointers.
*/
public function __clone()
{
$this->headers = clone $this->headers;
$this->body = clone $this->body;
}
/**
* {@inheritdoc}
*/
public function getMethod(): string
{
return $this->method;
}
/**
* {@inheritdoc}
*/
public function withMethod($method)
{
$method = $this->filterMethod($method);
$clone = clone $this;
$clone->method = $method;
return $clone;
}
/**
* Validate the HTTP method
*
* @param string $method
*
* @return string
*
* @throws InvalidArgumentException on invalid HTTP method.
*/
protected function filterMethod($method): string
{
/** @var mixed $method */
if (!is_string($method)) {
throw new InvalidArgumentException(sprintf(
'Unsupported HTTP method; must be a string, received %s',
(is_object($method) ? get_class($method) : gettype($method))
));
}
if (preg_match("/^[!#$%&'*+.^_`|~0-9a-z-]+$/i", $method) !== 1) {
throw new InvalidArgumentException(sprintf(
'Unsupported HTTP method "%s" provided',
$method
));
}
return $method;
}
/**
* {@inheritdoc}
*/
public function getRequestTarget(): string
{
if ($this->requestTarget) {
return $this->requestTarget;
}
if ($this->uri === null) {
return '/';
}
$path = $this->uri->getPath();
$path = '/' . ltrim($path, '/');
$query = $this->uri->getQuery();
if ($query) {
$path .= '?' . $query;
}
return $path;
}
/**
* {@inheritdoc}
*/
public function withRequestTarget($requestTarget)
{
if (preg_match('#\s#', $requestTarget)) {
throw new InvalidArgumentException(
'Invalid request target provided; must be a string and cannot contain whitespace'
);
}
$clone = clone $this;
$clone->requestTarget = $requestTarget;
return $clone;
}
/**
* {@inheritdoc}
*/
public function getUri(): UriInterface
{
return $this->uri;
}
/**
* {@inheritdoc}
*/
public function withUri(UriInterface $uri, $preserveHost = false)
{
$clone = clone $this;
$clone->uri = $uri;
if (!$preserveHost && $uri->getHost() !== '') {
$clone->headers->setHeader('Host', $uri->getHost());
return $clone;
}
if (($uri->getHost() !== '' && !$this->hasHeader('Host') || $this->getHeaderLine('Host') === '')) {
$clone->headers->setHeader('Host', $uri->getHost());
return $clone;
}
return $clone;
}
/**
* {@inheritdoc}
*/
public function getCookieParams(): array
{
return $this->cookies;
}
/**
* {@inheritdoc}
*/
public function withCookieParams(array $cookies)
{
$clone = clone $this;
$clone->cookies = $cookies;
return $clone;
}
/**
* {@inheritdoc}
*/
public function getQueryParams(): array
{
if (is_array($this->queryParams)) {
return $this->queryParams;
}
if ($this->uri === null) {
return [];
}
parse_str($this->uri->getQuery(), $this->queryParams); // <-- URL decodes data
assert(is_array($this->queryParams));
return $this->queryParams;
}
/**
* {@inheritdoc}
*/
public function withQueryParams(array $query)
{
$clone = clone $this;
$clone->queryParams = $query;
return $clone;
}
/**
* {@inheritdoc}
*/
public function getUploadedFiles(): array
{
return $this->uploadedFiles;
}
/**
* {@inheritdoc}
*/
public function withUploadedFiles(array $uploadedFiles)
{
$clone = clone $this;
$clone->uploadedFiles = $uploadedFiles;
return $clone;
}
/**
* {@inheritdoc}
*/
public function getServerParams(): array
{
return $this->serverParams;
}
/**
* {@inheritdoc}
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* {@inheritdoc}
*/
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
/**
* {@inheritdoc}
*/
public function withAttribute($name, $value)
{
$clone = clone $this;
$clone->attributes[$name] = $value;
return $clone;
}
/**
* {@inheritdoc}
*/
public function withoutAttribute($name)
{
$clone = clone $this;
unset($clone->attributes[$name]);
return $clone;
}
/**
* {@inheritdoc}
*/
public function getParsedBody()
{
return $this->parsedBody;
}
/**
* {@inheritdoc}
*/
public function withParsedBody($data)
{
/** @var mixed $data */
if (!is_null($data) && !is_object($data) && !is_array($data)) {
throw new InvalidArgumentException('Parsed body value must be an array, an object, or null');
}
$clone = clone $this;
$clone->parsedBody = $data;
return $clone;
}
}

View file

@ -0,0 +1,224 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use Fig\Http\Message\StatusCodeInterface;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Slim\Psr7\Factory\StreamFactory;
use Slim\Psr7\Interfaces\HeadersInterface;
use function is_integer;
use function is_object;
use function is_string;
use function method_exists;
class Response extends Message implements ResponseInterface
{
/**
* @var int
*/
protected $status = StatusCodeInterface::STATUS_OK;
/**
* @var string
*/
protected $reasonPhrase = '';
/**
* @var array
*/
protected static $messages = [
// Informational 1xx
StatusCodeInterface::STATUS_CONTINUE => 'Continue',
StatusCodeInterface::STATUS_SWITCHING_PROTOCOLS => 'Switching Protocols',
StatusCodeInterface::STATUS_PROCESSING => 'Processing',
// Successful 2xx
StatusCodeInterface::STATUS_OK => 'OK',
StatusCodeInterface::STATUS_CREATED => 'Created',
StatusCodeInterface::STATUS_ACCEPTED => 'Accepted',
StatusCodeInterface::STATUS_NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information',
StatusCodeInterface::STATUS_NO_CONTENT => 'No Content',
StatusCodeInterface::STATUS_RESET_CONTENT => 'Reset Content',
StatusCodeInterface::STATUS_PARTIAL_CONTENT => 'Partial Content',
StatusCodeInterface::STATUS_MULTI_STATUS => 'Multi-Status',
StatusCodeInterface::STATUS_ALREADY_REPORTED => 'Already Reported',
StatusCodeInterface::STATUS_IM_USED => 'IM Used',
// Redirection 3xx
StatusCodeInterface::STATUS_MULTIPLE_CHOICES => 'Multiple Choices',
StatusCodeInterface::STATUS_MOVED_PERMANENTLY => 'Moved Permanently',
StatusCodeInterface::STATUS_FOUND => 'Found',
StatusCodeInterface::STATUS_SEE_OTHER => 'See Other',
StatusCodeInterface::STATUS_NOT_MODIFIED => 'Not Modified',
StatusCodeInterface::STATUS_USE_PROXY => 'Use Proxy',
StatusCodeInterface::STATUS_RESERVED => '(Unused)',
StatusCodeInterface::STATUS_TEMPORARY_REDIRECT => 'Temporary Redirect',
StatusCodeInterface::STATUS_PERMANENT_REDIRECT => 'Permanent Redirect',
// Client Error 4xx
StatusCodeInterface::STATUS_BAD_REQUEST => 'Bad Request',
StatusCodeInterface::STATUS_UNAUTHORIZED => 'Unauthorized',
StatusCodeInterface::STATUS_PAYMENT_REQUIRED => 'Payment Required',
StatusCodeInterface::STATUS_FORBIDDEN => 'Forbidden',
StatusCodeInterface::STATUS_NOT_FOUND => 'Not Found',
StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED => 'Method Not Allowed',
StatusCodeInterface::STATUS_NOT_ACCEPTABLE => 'Not Acceptable',
StatusCodeInterface::STATUS_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',
StatusCodeInterface::STATUS_REQUEST_TIMEOUT => 'Request Timeout',
StatusCodeInterface::STATUS_CONFLICT => 'Conflict',
StatusCodeInterface::STATUS_GONE => 'Gone',
StatusCodeInterface::STATUS_LENGTH_REQUIRED => 'Length Required',
StatusCodeInterface::STATUS_PRECONDITION_FAILED => 'Precondition Failed',
StatusCodeInterface::STATUS_PAYLOAD_TOO_LARGE => 'Request Entity Too Large',
StatusCodeInterface::STATUS_URI_TOO_LONG => 'Request-URI Too Long',
StatusCodeInterface::STATUS_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',
StatusCodeInterface::STATUS_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable',
StatusCodeInterface::STATUS_EXPECTATION_FAILED => 'Expectation Failed',
StatusCodeInterface::STATUS_IM_A_TEAPOT => 'I\'m a teapot',
StatusCodeInterface::STATUS_MISDIRECTED_REQUEST => 'Misdirected Request',
StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY => 'Unprocessable Entity',
StatusCodeInterface::STATUS_LOCKED => 'Locked',
StatusCodeInterface::STATUS_FAILED_DEPENDENCY => 'Failed Dependency',
StatusCodeInterface::STATUS_UPGRADE_REQUIRED => 'Upgrade Required',
StatusCodeInterface::STATUS_PRECONDITION_REQUIRED => 'Precondition Required',
StatusCodeInterface::STATUS_TOO_MANY_REQUESTS => 'Too Many Requests',
StatusCodeInterface::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large',
444 => 'Connection Closed Without Response',
StatusCodeInterface::STATUS_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons',
499 => 'Client Closed Request',
// Server Error 5xx
StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR => 'Internal Server Error',
StatusCodeInterface::STATUS_NOT_IMPLEMENTED => 'Not Implemented',
StatusCodeInterface::STATUS_BAD_GATEWAY => 'Bad Gateway',
StatusCodeInterface::STATUS_SERVICE_UNAVAILABLE => 'Service Unavailable',
StatusCodeInterface::STATUS_GATEWAY_TIMEOUT => 'Gateway Timeout',
StatusCodeInterface::STATUS_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported',
StatusCodeInterface::STATUS_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates',
StatusCodeInterface::STATUS_INSUFFICIENT_STORAGE => 'Insufficient Storage',
StatusCodeInterface::STATUS_LOOP_DETECTED => 'Loop Detected',
StatusCodeInterface::STATUS_NOT_EXTENDED => 'Not Extended',
StatusCodeInterface::STATUS_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required',
599 => 'Network Connect Timeout Error',
];
/**
* @param int $status The response status code.
* @param HeadersInterface|null $headers The response headers.
* @param StreamInterface|null $body The response body.
*/
public function __construct(
int $status = StatusCodeInterface::STATUS_OK,
?HeadersInterface $headers = null,
?StreamInterface $body = null
) {
$this->status = $this->filterStatus($status);
$this->headers = $headers ? $headers : new Headers([], []);
$this->body = $body ? $body : (new StreamFactory())->createStream();
}
/**
* This method is applied to the cloned object after PHP performs an initial shallow-copy.
* This method completes a deep-copy by creating new objects for the cloned object's internal reference pointers.
*/
public function __clone()
{
$this->headers = clone $this->headers;
}
/**
* {@inheritdoc}
*/
public function getStatusCode(): int
{
return $this->status;
}
/**
* {@inheritdoc}
*/
public function withStatus($code, $reasonPhrase = '')
{
$code = $this->filterStatus($code);
$reasonPhrase = $this->filterReasonPhrase($reasonPhrase);
$clone = clone $this;
$clone->status = $code;
$clone->reasonPhrase = $reasonPhrase;
return $clone;
}
/**
* {@inheritdoc}
*/
public function getReasonPhrase(): string
{
if ($this->reasonPhrase !== '') {
return $this->reasonPhrase;
}
if (isset(static::$messages[$this->status])) {
return static::$messages[$this->status];
}
return '';
}
/**
* Filter HTTP status code.
*
* @param int $status HTTP status code.
*
* @return int
*
* @throws InvalidArgumentException If an invalid HTTP status code is provided.
*/
protected function filterStatus($status): int
{
if (!is_integer($status) || $status < StatusCodeInterface::STATUS_CONTINUE || $status > 599) {
throw new InvalidArgumentException('Invalid HTTP status code.');
}
return $status;
}
/**
* Filter Reason Phrase
*
* @param mixed $reasonPhrase
*
* @return string
*
* @throws InvalidArgumentException
*/
protected function filterReasonPhrase($reasonPhrase = ''): string
{
if (is_object($reasonPhrase) && method_exists($reasonPhrase, '__toString')) {
$reasonPhrase = (string) $reasonPhrase;
}
if (!is_string($reasonPhrase)) {
throw new InvalidArgumentException('Response reason phrase must be a string.');
}
if (strpos($reasonPhrase, "\r") || strpos($reasonPhrase, "\n")) {
throw new InvalidArgumentException(
'Reason phrase contains one of the following prohibited characters: \r \n'
);
}
return $reasonPhrase;
}
}

View file

@ -0,0 +1,420 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use function fclose;
use function feof;
use function fread;
use function fseek;
use function fstat;
use function ftell;
use function fwrite;
use function is_array;
use function is_resource;
use function is_string;
use function pclose;
use function rewind;
use function stream_get_contents;
use function stream_get_meta_data;
use function strstr;
use const SEEK_SET;
class Stream implements StreamInterface
{
/**
* Bit mask to determine if the stream is a pipe
*
* This is octal as per header stat.h
*/
public const FSTAT_MODE_S_IFIFO = 0010000;
/**
* The underlying stream resource
*
* @var resource|null
*/
protected $stream;
/**
* @var array|null
*/
protected $meta;
/**
* @var bool|null
*/
protected $readable;
/**
* @var bool|null
*/
protected $writable;
/**
* @var bool|null
*/
protected $seekable;
/**
* @var null|int
*/
protected $size;
/**
* @var bool|null
*/
protected $isPipe;
/**
* @var bool
*/
protected $finished = false;
/**
* @var StreamInterface | null
*/
protected $cache;
/**
* @param resource $stream A PHP resource handle.
* @param StreamInterface $cache A stream to cache $stream (useful for non-seekable streams)
*
* @throws InvalidArgumentException If argument is not a resource.
*/
public function __construct($stream, StreamInterface $cache = null)
{
$this->attach($stream);
if ($cache && (!$cache->isSeekable() || !$cache->isWritable())) {
throw new RuntimeException('Cache stream must be seekable and writable');
}
$this->cache = $cache;
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
if (!$this->stream) {
return null;
}
$this->meta = stream_get_meta_data($this->stream);
if (!$key) {
return $this->meta;
}
return isset($this->meta[$key]) ? $this->meta[$key] : null;
}
/**
* Attach new resource to this object.
*
* @internal This method is not part of the PSR-7 standard.
*
* @param resource $stream A PHP resource handle.
*
* @throws InvalidArgumentException If argument is not a valid PHP resource.
*
* @return void
*/
protected function attach($stream): void
{
if (!is_resource($stream)) {
throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource');
}
if ($this->stream) {
$this->detach();
}
$this->stream = $stream;
}
/**
* {@inheritdoc}
*/
public function detach()
{
$oldResource = $this->stream;
$this->stream = null;
$this->meta = null;
$this->readable = null;
$this->writable = null;
$this->seekable = null;
$this->size = null;
$this->isPipe = null;
$this->cache = null;
$this->finished = false;
return $oldResource;
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
if (!$this->stream) {
return '';
}
if ($this->cache && $this->finished) {
$this->cache->rewind();
return $this->cache->getContents();
}
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
}
/**
* {@inheritdoc}
*/
public function close(): void
{
if ($this->stream) {
if ($this->isPipe()) {
pclose($this->stream);
} else {
fclose($this->stream);
}
}
$this->detach();
}
/**
* {@inheritdoc}
*/
public function getSize(): ?int
{
if ($this->stream && !$this->size) {
$stats = fstat($this->stream);
if ($stats) {
$this->size = isset($stats['size']) && !$this->isPipe() ? $stats['size'] : null;
}
}
return $this->size;
}
/**
* {@inheritdoc}
*/
public function tell(): int
{
$position = false;
if ($this->stream) {
$position = ftell($this->stream);
}
if ($position === false || $this->isPipe()) {
throw new RuntimeException('Could not get the position of the pointer in stream.');
}
return $position;
}
/**
* {@inheritdoc}
*/
public function eof(): bool
{
return $this->stream ? feof($this->stream) : true;
}
/**
* {@inheritdoc}
*/
public function isReadable(): bool
{
if ($this->readable === null) {
if ($this->isPipe()) {
$this->readable = true;
} else {
$this->readable = false;
if ($this->stream) {
$mode = $this->getMetadata('mode');
if (strstr($mode, 'r') !== false || strstr($mode, '+') !== false) {
$this->readable = true;
}
}
}
}
return $this->readable;
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
if ($this->writable === null) {
$this->writable = false;
if ($this->stream) {
$mode = $this->getMetadata('mode');
if (strstr($mode, 'w') !== false || strstr($mode, '+') !== false) {
$this->writable = true;
}
}
}
return $this->writable;
}
/**
* {@inheritdoc}
*/
public function isSeekable(): bool
{
if ($this->seekable === null) {
$this->seekable = false;
if ($this->stream) {
$this->seekable = !$this->isPipe() && $this->getMetadata('seekable');
}
}
return $this->seekable;
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET): void
{
if (!$this->isSeekable() || $this->stream && fseek($this->stream, $offset, $whence) === -1) {
throw new RuntimeException('Could not seek in stream.');
}
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
if (!$this->isSeekable() || $this->stream && rewind($this->stream) === false) {
throw new RuntimeException('Could not rewind stream.');
}
}
/**
* {@inheritdoc}
*/
public function read($length): string
{
$data = false;
if ($this->isReadable() && $this->stream) {
$data = fread($this->stream, $length);
}
if (is_string($data)) {
if ($this->cache) {
$this->cache->write($data);
}
if ($this->eof()) {
$this->finished = true;
}
return $data;
}
throw new RuntimeException('Could not read from stream.');
}
/**
* {@inheritdoc}
*/
public function write($string)
{
$written = false;
if ($this->isWritable() && $this->stream) {
$written = fwrite($this->stream, $string);
}
if ($written !== false) {
$this->size = null;
return $written;
}
throw new RuntimeException('Could not write to stream.');
}
/**
* {@inheritdoc}
*/
public function getContents(): string
{
if ($this->cache && $this->finished) {
$this->cache->rewind();
return $this->cache->getContents();
}
$contents = false;
if ($this->stream) {
$contents = stream_get_contents($this->stream);
}
if (is_string($contents)) {
if ($this->cache) {
$this->cache->write($contents);
}
if ($this->eof()) {
$this->finished = true;
}
return $contents;
}
throw new RuntimeException('Could not get contents of stream.');
}
/**
* Returns whether or not the stream is a pipe.
*
* @internal This method is not part of the PSR-7 standard.
*
* @return bool
*/
public function isPipe(): bool
{
if ($this->isPipe === null) {
$this->isPipe = false;
if ($this->stream) {
$stats = fstat($this->stream);
if (is_array($stats)) {
$this->isPipe = isset($stats['mode']) && ($stats['mode'] & self::FSTAT_MODE_S_IFIFO) !== 0;
}
}
}
return $this->isPipe;
}
}

View file

@ -0,0 +1,293 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use RuntimeException;
use Slim\Psr7\Factory\StreamFactory;
use function copy;
use function dirname;
use function is_array;
use function is_string;
use function is_uploaded_file;
use function is_writable;
use function move_uploaded_file;
use function rename;
use function sprintf;
use function strpos;
use function unlink;
use const UPLOAD_ERR_OK;
class UploadedFile implements UploadedFileInterface
{
/**
* The client-provided full path to the file
*
* @var string
*/
protected $file;
/**
* The client-provided file name.
*
* @var string|null
*/
protected $name;
/**
* The client-provided media type of the file.
*
* @var string|null
*/
protected $type;
/**
* @var int|null
*/
protected $size;
/**
* A valid PHP UPLOAD_ERR_xxx code for the file upload.
*
* @var int
*/
protected $error = UPLOAD_ERR_OK;
/**
* Indicates if the upload is from a SAPI environment.
*
* @var bool
*/
protected $sapi = false;
/**
* @var StreamInterface|null
*/
protected $stream;
/**
* Indicates if the uploaded file has already been moved.
*
* @var bool
*/
protected $moved = false;
/**
* @param string|StreamInterface $fileNameOrStream The full path to the uploaded file provided by the client,
* or a StreamInterface instance.
* @param string|null $name The file name.
* @param string|null $type The file media type.
* @param int|null $size The file size in bytes.
* @param int $error The UPLOAD_ERR_XXX code representing the status of the upload.
* @param bool $sapi Indicates if the upload is in a SAPI environment.
*/
final public function __construct(
$fileNameOrStream,
?string $name = null,
?string $type = null,
?int $size = null,
int $error = UPLOAD_ERR_OK,
bool $sapi = false
) {
if ($fileNameOrStream instanceof StreamInterface) {
$file = $fileNameOrStream->getMetadata('uri');
if (!is_string($file)) {
throw new InvalidArgumentException('No URI associated with the stream.');
}
$this->file = $file;
$this->stream = $fileNameOrStream;
} elseif (is_string($fileNameOrStream)) {
$this->file = $fileNameOrStream;
} else {
throw new InvalidArgumentException(
'Please provide a string (full path to the uploaded file) or an instance of StreamInterface.'
);
}
$this->name = $name;
$this->type = $type;
$this->size = $size;
$this->error = $error;
$this->sapi = $sapi;
}
/**
* {@inheritdoc}
*/
public function getStream()
{
if ($this->moved) {
throw new RuntimeException(sprintf('Uploaded file %s has already been moved', $this->name));
}
if (!$this->stream) {
$this->stream = (new StreamFactory())->createStreamFromFile($this->file);
}
return $this->stream;
}
/**
* {@inheritdoc}
*/
public function moveTo($targetPath): void
{
if ($this->moved) {
throw new RuntimeException('Uploaded file already moved');
}
$targetIsStream = strpos($targetPath, '://') > 0;
if (!$targetIsStream && !is_writable(dirname($targetPath))) {
throw new InvalidArgumentException('Upload target path is not writable');
}
if ($targetIsStream) {
if (!copy($this->file, $targetPath)) {
throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath));
}
if (!unlink($this->file)) {
throw new RuntimeException(sprintf('Error removing uploaded file %s', $this->name));
}
} elseif ($this->sapi) {
if (!is_uploaded_file($this->file)) {
throw new RuntimeException(sprintf('%s is not a valid uploaded file', $this->file));
}
if (!move_uploaded_file($this->file, $targetPath)) {
throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath));
}
} else {
if (!rename($this->file, $targetPath)) {
throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath));
}
}
$this->moved = true;
}
/**
* {@inheritdoc}
*/
public function getError(): int
{
return $this->error;
}
/**
* {@inheritdoc}
*/
public function getClientFilename(): ?string
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getClientMediaType(): ?string
{
return $this->type;
}
/**
* {@inheritdoc}
*/
public function getSize(): ?int
{
return $this->size;
}
/**
* Returns the client-provided full path to the file
*
* @internal This method is not part of the PSR-7 standard
*
* @return string
*/
public function getFilePath(): string
{
return $this->file;
}
/**
* Create a normalized tree of UploadedFile instances from the Environment.
*
* @internal This method is not part of the PSR-7 standard.
*
* @param array $globals The global server variables.
*
* @return array A normalized tree of UploadedFile instances or null if none are provided.
*/
public static function createFromGlobals(array $globals): array
{
if (isset($globals['slim.files']) && is_array($globals['slim.files'])) {
return $globals['slim.files'];
}
if (!empty($_FILES)) {
return static::parseUploadedFiles($_FILES);
}
return [];
}
/**
* Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data.
*
* @internal This method is not part of the PSR-7 standard.
*
* @param array $uploadedFiles The non-normalized tree of uploaded file data.
*
* @return array A normalized tree of UploadedFile instances.
*/
private static function parseUploadedFiles(array $uploadedFiles): array
{
$parsed = [];
foreach ($uploadedFiles as $field => $uploadedFile) {
if (!isset($uploadedFile['error'])) {
if (is_array($uploadedFile)) {
$parsed[$field] = static::parseUploadedFiles($uploadedFile);
}
continue;
}
$parsed[$field] = [];
if (!is_array($uploadedFile['error'])) {
$parsed[$field] = new static(
$uploadedFile['tmp_name'],
isset($uploadedFile['name']) ? $uploadedFile['name'] : null,
isset($uploadedFile['type']) ? $uploadedFile['type'] : null,
isset($uploadedFile['size']) ? $uploadedFile['size'] : null,
$uploadedFile['error'],
true
);
} else {
$subArray = [];
foreach ($uploadedFile['error'] as $fileIdx => $error) {
// Normalize sub array and re-parse to move the input's key name up a level
$subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx];
$subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx];
$subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx];
$subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx];
$subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx];
$parsed[$field] = static::parseUploadedFiles($subArray);
}
}
}
return $parsed;
}
}

View file

@ -0,0 +1,495 @@
<?php
/**
* Slim Framework (https://slimframework.com)
*
* @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License)
*/
declare(strict_types=1);
namespace Slim\Psr7;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use function filter_var;
use function is_integer;
use function is_null;
use function is_object;
use function is_string;
use function ltrim;
use function method_exists;
use function preg_replace_callback;
use function rawurlencode;
use function str_replace;
use function strtolower;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
class Uri implements UriInterface
{
public const SUPPORTED_SCHEMES = [
'' => null,
'http' => 80,
'https' => 443
];
/**
* Uri scheme (without "://" suffix)
*
* @var string
*/
protected $scheme = '';
/**
* @var string
*/
protected $user = '';
/**
* @var string
*/
protected $password = '';
/**
* @var string
*/
protected $host = '';
/**
* @var null|int
*/
protected $port;
/**
* @var string
*/
protected $path = '';
/**
* Uri query string (without "?" prefix)
*
* @var string
*/
protected $query = '';
/**
* Uri fragment string (without "#" prefix)
*
* @var string
*/
protected $fragment = '';
/**
* @param string $scheme Uri scheme.
* @param string $host Uri host.
* @param int $port Uri port number.
* @param string $path Uri path.
* @param string $query Uri query string.
* @param string $fragment Uri fragment.
* @param string $user Uri user.
* @param string $password Uri password.
*/
public function __construct(
string $scheme,
string $host,
?int $port = null,
string $path = '/',
string $query = '',
string $fragment = '',
string $user = '',
string $password = ''
) {
$this->scheme = $this->filterScheme($scheme);
$this->host = $this->filterHost($host);
$this->port = $this->filterPort($port);
$this->path = $this->filterPath($path);
$this->query = $this->filterQuery($query);
$this->fragment = $this->filterFragment($fragment);
$this->user = $user;
$this->password = $password;
}
/**
* {@inheritdoc}
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* {@inheritdoc}
*/
public function withScheme($scheme)
{
$scheme = $this->filterScheme($scheme);
$clone = clone $this;
$clone->scheme = $scheme;
return $clone;
}
/**
* Filter Uri scheme.
*
* @param mixed $scheme Raw Uri scheme.
*
* @return string
*
* @throws InvalidArgumentException If the Uri scheme is not a string.
* @throws InvalidArgumentException If Uri scheme is not exists in SUPPORTED_SCHEMES
*/
protected function filterScheme($scheme): string
{
if (!is_string($scheme)) {
throw new InvalidArgumentException('Uri scheme must be a string.');
}
$scheme = str_replace('://', '', strtolower($scheme));
if (!key_exists($scheme, self::SUPPORTED_SCHEMES)) {
throw new InvalidArgumentException(
'Uri scheme must be one of: "' . implode('", "', array_keys(static::SUPPORTED_SCHEMES)) . '"'
);
}
return $scheme;
}
/**
* {@inheritdoc}
*/
public function getAuthority(): string
{
$userInfo = $this->getUserInfo();
$host = $this->getHost();
$port = $this->getPort();
return ($userInfo !== '' ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : '');
}
/**
* {@inheritdoc}
*/
public function getUserInfo(): string
{
$info = $this->user;
if (isset($this->password) && $this->password !== '') {
$info .= ':' . $this->password;
}
return $info;
}
/**
* {@inheritdoc}
*/
public function withUserInfo($user, $password = null)
{
$clone = clone $this;
$clone->user = $this->filterUserInfo($user);
if ($clone->user !== '') {
$clone->password = $this->filterUserInfo($password);
} else {
$clone->password = '';
}
return $clone;
}
/**
* Filters the user info string.
*
* Returns the percent-encoded query string.
*
* @param string|null $info The raw uri query string.
*
* @return string
*/
protected function filterUserInfo(?string $info = null): string
{
if (!is_string($info)) {
return '';
}
$match = preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=]+|%(?![A-Fa-f0-9]{2}))/u',
function ($match) {
return rawurlencode($match[0]);
},
$info
);
return is_string($match) ? $match : '';
}
/**
* {@inheritdoc}
*/
public function getHost(): string
{
return $this->host;
}
/**
* {@inheritdoc}
*/
public function withHost($host)
{
$clone = clone $this;
$clone->host = $this->filterHost($host);
return $clone;
}
/**
* Filter Uri host.
*
* If the supplied host is an IPv6 address, then it is converted to a reference
* as per RFC 2373.
*
* @param mixed $host The host to filter.
*
* @return string
*
* @throws InvalidArgumentException for invalid host names.
*/
protected function filterHost($host): string
{
if (is_object($host) && method_exists($host, '__toString')) {
$host = (string) $host;
}
if (!is_string($host)) {
throw new InvalidArgumentException('Uri host must be a string');
}
if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$host = '[' . $host . ']';
}
return strtolower($host);
}
/**
* {@inheritdoc}
*/
public function getPort(): ?int
{
return $this->port && !$this->hasStandardPort() ? $this->port : null;
}
/**
* {@inheritdoc}
*/
public function withPort($port)
{
$port = $this->filterPort($port);
$clone = clone $this;
$clone->port = $port;
return $clone;
}
/**
* Does this Uri use a standard port?
*
* @return bool
*/
protected function hasStandardPort(): bool
{
return static::SUPPORTED_SCHEMES[$this->scheme] === $this->port;
}
/**
* Filter Uri port.
*
* @param null|int $port The Uri port number.
*
* @return null|int
*
* @throws InvalidArgumentException If the port is invalid.
*/
protected function filterPort($port): ?int
{
if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) {
return $port;
}
throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)');
}
/**
* {@inheritdoc}
*/
public function getPath(): string
{
return $this->path;
}
/**
* {@inheritdoc}
*/
public function withPath($path)
{
if (!is_string($path)) {
throw new InvalidArgumentException('Uri path must be a string');
}
$clone = clone $this;
$clone->path = $this->filterPath($path);
return $clone;
}
/**
* Filter Uri path.
*
* This method percent-encodes all reserved characters in the provided path string.
* This method will NOT double-encode characters that are already percent-encoded.
*
* @param string $path The raw uri path.
*
* @return string The RFC 3986 percent-encoded uri path.
*
* @link http://www.faqs.org/rfcs/rfc3986.html
*/
protected function filterPath($path): string
{
$match = preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
function ($match) {
return rawurlencode($match[0]);
},
$path
);
return is_string($match) ? $match : '';
}
/**
* {@inheritdoc}
*/
public function getQuery(): string
{
return $this->query;
}
/**
* {@inheritdoc}
*/
public function withQuery($query)
{
$query = ltrim($this->filterQuery($query), '?');
$clone = clone $this;
$clone->query = $query;
return $clone;
}
/**
* Filters the query string of a URI.
*
* Returns the percent-encoded query string.
*
* @param mixed $query The raw uri query string.
*
* @return string
*/
protected function filterQuery($query): string
{
if (is_object($query) && method_exists($query, '__toString')) {
$query = (string) $query;
}
if (!is_string($query)) {
throw new InvalidArgumentException('Uri query must be a string.');
}
$match = preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
function ($match) {
return rawurlencode($match[0]);
},
$query
);
return is_string($match) ? $match : '';
}
/**
* {@inheritdoc}
*/
public function getFragment(): string
{
return $this->fragment;
}
/**
* {@inheritdoc}
*/
public function withFragment($fragment)
{
$fragment = $this->filterFragment($fragment);
$clone = clone $this;
$clone->fragment = $fragment;
return $clone;
}
/**
* Filters fragment of a URI.
*
* Returns the percent-encoded fragment.
*
* @param mixed $fragment The raw uri query string.
*
* @return string
*/
protected function filterFragment($fragment): string
{
if (is_object($fragment) && method_exists($fragment, '__toString')) {
$fragment = (string) $fragment;
}
if (!is_string($fragment)) {
throw new InvalidArgumentException('Uri fragment must be a string.');
}
$fragment = ltrim($fragment, '#');
$match = preg_replace_callback(
'/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/',
function ($match) {
return rawurlencode($match[0]);
},
$fragment
);
return is_string($match) ? $match : '';
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
$scheme = $this->getScheme();
$authority = $this->getAuthority();
$path = $this->getPath();
$query = $this->getQuery();
$fragment = $this->getFragment();
$path = '/' . ltrim($path, '/');
return ($scheme !== '' ? $scheme . ':' : '')
. ($authority !== '' ? '//' . $authority : '')
. $path
. ($query !== '' ? '?' . $query : '')
. ($fragment !== '' ? '#' . $fragment : '');
}
}