| Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-core/Classes/Http/ |
| Current File : /var/www/surf/TYPO3/vendor/typo3/cms-core/Classes/Http/Message.php |
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Http;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* Default implementation for the MessageInterface of the PSR-7 standard
* It is the base for any request or response for PSR-7.
*
* Highly inspired by https://github.com/phly/http/
*
* @internal Note that this is not public API yet.
*/
class Message implements MessageInterface
{
/**
* The HTTP Protocol version, defaults to 1.1
*/
protected string $protocolVersion = '1.1';
/**
* Associative array containing all headers of this Message
* This is a mixed-case list of the headers (as due to the specification)
*/
protected array $headers = [];
/**
* Lowercased version of all headers, in order to check if a header is set or not
* this way a lot of checks are easier to be set
*/
protected array $lowercasedHeaderNames = [];
/**
* The body as a Stream object
*/
protected ?StreamInterface $body = null;
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion(): string
{
return $this->protocolVersion;
}
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion(string $version): MessageInterface
{
$clonedObject = clone $this;
$clonedObject->protocolVersion = $version;
return $clonedObject;
}
/**
* Retrieves all message header values.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* ```
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
* ```
*
* While header names are not case-sensitive, getHeaders() will preserve the
* exact case in which headers were originally specified.
*
* @return array Returns an associative array of the message's headers. Each
* key MUST be a header name, and each value MUST be an array of strings
* for that header.
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $name Case-insensitive header field name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader(string $name): bool
{
return isset($this->lowercasedHeaderNames[strtolower($name)]);
}
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method MUST return an
* empty array.
*
* @param string $name Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method MUST
* return an empty array.
*/
public function getHeader(string $name): array
{
if (!$this->hasHeader($name)) {
return [];
}
$header = $this->lowercasedHeaderNames[strtolower($name)];
$headerValue = $this->headers[$header];
if (is_array($headerValue)) {
return $headerValue;
}
return [$headerValue];
}
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine(string $name): string
{
$headerValue = $this->getHeader($name);
if (empty($headerValue)) {
return '';
}
return implode(',', $headerValue);
}
/**
* Return an instance with the provided value replacing the specified header.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new and/or updated header and value.
*
* @param string $name Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withHeader(string $name, $value): MessageInterface
{
if (is_string($value)) {
$value = [$value];
}
if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The value must be a string or an array of strings.', 1436717266);
}
$this->validateHeaderName($name);
$this->validateHeaderValues($value);
$lowercasedHeaderName = strtolower($name);
$clonedObject = clone $this;
$clonedObject->headers[$name] = $value;
$clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name;
return $clonedObject;
}
/**
* Return an instance with the specified header appended with the given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new header and/or value.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withAddedHeader(string $name, $value): MessageInterface
{
if (is_string($value)) {
$value = [$value];
}
if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267);
}
$this->validateHeaderName($name);
$this->validateHeaderValues($value);
if (!$this->hasHeader($name)) {
return $this->withHeader($name, $value);
}
$name = $this->lowercasedHeaderNames[strtolower($name)];
$clonedObject = clone $this;
$clonedObject->headers[$name] = array_merge($this->headers[$name], $value);
return $clonedObject;
}
/**
* Return an instance without the specified header.
*
* Header resolution MUST be done without case-sensitivity.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the named header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader(string $name): MessageInterface
{
if (!$this->hasHeader($name)) {
return clone $this;
}
// fetch the original header from the lowercased version
$lowercasedHeader = strtolower($name);
$name = $this->lowercasedHeaderNames[$lowercasedHeader];
$clonedObject = clone $this;
unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]);
return $clonedObject;
}
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody(): StreamInterface
{
if ($this->body === null) {
$this->body = new Stream('php://temp', 'r+');
}
return $this->body;
}
/**
* Return an instance with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* new body stream.
*
* @return static
* @throws \InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body): MessageInterface
{
$clonedObject = clone $this;
$clonedObject->body = $body;
return $clonedObject;
}
/**
* Ensure header names and values are valid.
*
* @throws \InvalidArgumentException
*/
protected function assertHeaders(array $headers): void
{
foreach ($headers as $name => $headerValues) {
$this->validateHeaderName($name);
// check if all values are correct
array_walk($headerValues, static function ($value, $key, Message $messageObject) {
if (!$messageObject->isValidHeaderValue($value)) {
throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
}
}, $this);
}
}
/**
* Filter a set of headers to ensure they are in the correct internal format.
*
* Used by message constructors to allow setting all initial headers at once.
*
* @param array $originalHeaders Headers to filter.
* @return array Filtered headers and names.
*/
protected function filterHeaders(array $originalHeaders): array
{
$headerNames = $headers = [];
foreach ($originalHeaders as $header => $value) {
if (!is_string($header) || (!is_array($value) && !is_scalar($value))) {
continue;
}
if (!is_array($value)) {
$value = [(string)$value];
}
$headerNames[strtolower($header)] = $header;
$headers[$header] = $value;
}
return [$headerNames, $headers];
}
/**
* Helper function to test if an array contains only strings
*/
protected function arrayContainsOnlyStrings(array $data): bool
{
return array_reduce($data, static function ($original, $item) {
return is_string($item) ? $original : false;
}, true);
}
/**
* Assert that the provided header values are valid.
*
* @see https://tools.ietf.org/html/rfc7230#section-3.2
* @param string[] $values
* @throws \InvalidArgumentException
*/
protected function validateHeaderValues(array $values): void
{
array_walk($values, static function ($value, $key, Message $messageObject) {
if (!$messageObject->isValidHeaderValue($value)) {
throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
}
}, $this);
}
/**
* Filter a header value
*
* Ensures CRLF header injection vectors are filtered.
*
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
* tabs are allowed in values; header continuations MUST consist of
* a single CRLF sequence followed by a space or horizontal tab.
*
* This method filters any values not allowed from the string, and is
* lossy.
*
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
* @todo: Unused? And why is this public? Maybe align with zend-diactoros again
*/
public function filter(string $value): string
{
$length = strlen($value);
$string = '';
for ($i = 0; $i < $length; $i += 1) {
$ascii = ord($value[$i]);
// Detect continuation sequences
if ($ascii === 13) {
$lf = ord($value[$i + 1]);
$ws = ord($value[$i + 2]);
if ($lf === 10 && in_array($ws, [9, 32], true)) {
$string .= $value[$i] . $value[$i + 1];
$i += 1;
}
continue;
}
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 32-126, 128-254 === visible
// 127 === DEL
// 255 === null byte
if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
continue;
}
$string .= $value[$i];
}
return $string;
}
/**
* Check whether a header name is valid and throw an exception.
*
* @see https://tools.ietf.org/html/rfc7230#section-3.2
* @throws \InvalidArgumentException
* @todo: Review. Should be protected / private, maybe align with zend-diactoros again
*/
public function validateHeaderName(string $name): void
{
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
}
}
/**
* Checks if an HTTP header value is valid.
*
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
* tabs are allowed in values; header continuations MUST consist of
* a single CRLF sequence followed by a space or horizontal tab.
*
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
* @todo: Review. Should be protected / private, maybe align with zend-diactoros again
*/
public function isValidHeaderValue(string $value): bool
{
// Any occurrence of \r or \n is invalid
if (strpbrk($value, "\r\n") !== false) {
return false;
}
foreach (unpack('C*', $value) as $ascii) {
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 32-126, 128-254 === visible
// 127 === DEL
// 255 === null byte
if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
return false;
}
}
return true;
}
}