0byt3m1n1
Path:
/
data
/
applications
/
aps
/
typo3
/
12.4.7
/
standard
/
htdocs
/
typo3
/
sysext
/
core
/
Classes
/
Http
/
[
Home
]
File: 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; } }