primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamInterface;
use function array_pop;
use function implode;
use function preg_match;
use function sprintf;
use function str_replace;
use function trim;
use function ucwords;
/**
* Provides base functionality for request and response de/serialization
* strategies, including functionality for retrieving a line at a time from
* the message, splitting headers from the body, and serializing headers.
*/
abstract class AbstractSerializer
{
public const CR = "\r";
public const EOL = "\r\n";
public const LF = "\n";
/**
* Retrieve a single line from the stream.
*
* Retrieves a line from the stream; a line is defined as a sequence of
* characters ending in a CRLF sequence.
*
* @throws Exception\DeserializationException If the sequence contains a CR
* or LF in isolation, or ends in a CR.
*/
protected static function getLine(StreamInterface $stream): string
{
$line = '';
$crFound = false;
while (! $stream->eof()) {
$char = $stream->read(1);
if ($crFound && $char === self::LF) {
$crFound = false;
break;
}
// CR NOT followed by LF
if ($crFound && $char !== self::LF) {
throw Exception\DeserializationException::forUnexpectedCarriageReturn();
}
// LF in isolation
if (! $crFound && $char === self::LF) {
throw Exception\DeserializationException::forUnexpectedLineFeed();
}
// CR found; do not append
if ($char === self::CR) {
$crFound = true;
continue;
}
// Any other character: append
$line .= $char;
}
// CR found at end of stream
if ($crFound) {
throw Exception\DeserializationException::forUnexpectedEndOfHeaders();
}
return $line;
}
/**
* Split the stream into headers and body content.
*
* Returns an array containing two elements
*
* - The first is an array of headers
* - The second is a StreamInterface containing the body content
*
* @throws Exception\DeserializationException For invalid headers.
*/
protected static function splitStream(StreamInterface $stream): array
{
$headers = [];
$currentHeader = false;
while ($line = self::getLine($stream)) {
if (preg_match(';^(?P<name>[!#$%&\'*+.^_`\|~0-9a-zA-Z-]+):(?P<value>.*)$;', $line, $matches)) {
$currentHeader = $matches['name'];
if (! isset($headers[$currentHeader])) {
$headers[$currentHeader] = [];
}
$headers[$currentHeader][] = trim($matches['value'], "\t ");
continue;
}
if (! $currentHeader) {
throw Exception\DeserializationException::forInvalidHeader();
}
if (! preg_match('#^[ \t]#', $line)) {
throw Exception\DeserializationException::forInvalidHeaderContinuation();
}
// Append continuation to last header value found
$value = array_pop($headers[$currentHeader]);
$headers[$currentHeader][] = $value . ' ' . trim($line, "\t ");
}
// use RelativeStream to avoid copying initial stream into memory
return [$headers, new RelativeStream($stream, $stream->tell())];
}
/**
* Serialize headers to string values.
*
* @psalm-param array<string, string[]> $headers
*/
protected static function serializeHeaders(array $headers): string
{
$lines = [];
foreach ($headers as $header => $values) {
$normalized = self::filterHeader($header);
foreach ($values as $value) {
$lines[] = sprintf('%s: %s', $normalized, $value);
}
}
return implode("\r\n", $lines);
}
/**
* Filter a header name to wordcase
*
* @param string $header
*/
protected static function filterHeader($header): string
{
$filtered = str_replace('-', ' ', $header);
$filtered = ucwords($filtered);
return str_replace(' ', '-', $filtered);
}
}

View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamInterface;
use Stringable;
use function array_key_exists;
use const SEEK_SET;
/**
* Implementation of PSR HTTP streams
*/
class CallbackStream implements StreamInterface, Stringable
{
/** @var callable|null */
protected $callback;
/**
* @throws Exception\InvalidArgumentException
*/
public function __construct(callable $callback)
{
$this->attach($callback);
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
return $this->getContents();
}
/**
* {@inheritdoc}
*/
public function close(): void
{
$this->callback = null;
}
/**
* {@inheritdoc}
*
* @return null|callable
*/
public function detach(): ?callable
{
$callback = $this->callback;
$this->callback = null;
return $callback;
}
/**
* Attach a new callback to the instance.
*/
public function attach(callable $callback): void
{
$this->callback = $callback;
}
/**
* {@inheritdoc}
*/
public function getSize(): ?int
{
return null;
}
/**
* {@inheritdoc}
*/
public function tell(): int
{
throw Exception\UntellableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
public function eof(): bool
{
return empty($this->callback);
}
/**
* {@inheritdoc}
*/
public function isSeekable(): bool
{
return false;
}
/**
* {@inheritdoc}
*
* @param int $offset
* @param int $whence
* @return void
*/
public function seek($offset, $whence = SEEK_SET)
{
throw Exception\UnseekableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
throw Exception\UnrewindableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function write($string): void
{
throw Exception\UnwritableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
public function isReadable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function read($length): string
{
throw Exception\UnreadableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
public function getContents(): string
{
$callback = $this->detach();
$contents = $callback ? $callback() : '';
return (string) $contents;
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
$metadata = [
'eof' => $this->eof(),
'stream_type' => 'callback',
'seekable' => false,
];
if (null === $key) {
return $metadata;
}
if (! array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
class ConfigProvider
{
public const CONFIG_KEY = 'laminas-diactoros';
public const X_FORWARDED = 'x-forwarded-request-filter';
public const X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies';
public const X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers';
/**
* Retrieve configuration for laminas-diactoros.
*
* @return array
*/
public function __invoke(): array
{
return [
'dependencies' => $this->getDependencies(),
self::CONFIG_KEY => $this->getComponentConfig(),
];
}
/**
* Returns the container dependencies.
* Maps factory interfaces to factories.
*/
public function getDependencies(): array
{
// @codingStandardsIgnoreStart
return [
'invokables' => [
RequestFactoryInterface::class => RequestFactory::class,
ResponseFactoryInterface::class => ResponseFactory::class,
StreamFactoryInterface::class => StreamFactory::class,
ServerRequestFactoryInterface::class => ServerRequestFactory::class,
UploadedFileFactoryInterface::class => UploadedFileFactory::class,
UriFactoryInterface::class => UriFactory::class
],
];
// @codingStandardsIgnoreEnd
}
public function getComponentConfig(): array
{
return [
self::X_FORWARDED => [
self::X_FORWARDED_TRUSTED_PROXIES => '',
self::X_FORWARDED_TRUSTED_HEADERS => [],
],
];
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use Throwable;
use UnexpectedValueException;
class DeserializationException extends UnexpectedValueException implements ExceptionInterface
{
public static function forInvalidHeader(): self
{
throw new self('Invalid header detected');
}
public static function forInvalidHeaderContinuation(): self
{
throw new self('Invalid header continuation');
}
public static function forRequestFromArray(Throwable $previous): self
{
return new self('Cannot deserialize request', $previous->getCode(), $previous);
}
public static function forResponseFromArray(Throwable $previous): self
{
return new self('Cannot deserialize response', $previous->getCode(), $previous);
}
public static function forUnexpectedCarriageReturn(): self
{
throw new self('Unexpected carriage return detected');
}
public static function forUnexpectedEndOfHeaders(): self
{
throw new self('Unexpected end of headers');
}
public static function forUnexpectedLineFeed(): self
{
throw new self('Unexpected line feed detected');
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use Throwable;
/**
* Marker interface for package-specific exceptions.
*/
interface ExceptionInterface extends Throwable
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
class InvalidForwardedHeaderNameException extends RuntimeException implements ExceptionInterface
{
public static function forHeader(mixed $name): self
{
if (! is_string($name)) {
$name = sprintf('(value of type %s)', is_object($name) ? $name::class : gettype($name));
}
return new self(sprintf(
'Invalid X-Forwarded-* header name "%s" provided to %s',
$name,
FilterUsingXForwardedHeaders::class
));
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use function gettype;
use function is_object;
use function sprintf;
class InvalidProxyAddressException extends RuntimeException implements ExceptionInterface
{
public static function forInvalidProxyArgument(mixed $proxy): self
{
$type = is_object($proxy) ? $proxy::class : gettype($proxy);
return new self(sprintf(
'Invalid proxy of type "%s" provided;'
. ' must be a valid IPv4 or IPv6 address, optionally with a subnet mask provided'
. ' or an array of such values',
$type,
));
}
public static function forAddress(string $address): self
{
return new self(sprintf(
'Invalid proxy address "%s" provided;'
. ' must be a valid IPv4 or IPv6 address, optionally with a subnet mask provided',
$address,
));
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
use Throwable;
class InvalidStreamPointerPositionException extends RuntimeException implements ExceptionInterface
{
/** {@inheritDoc} */
public function __construct(
string $message = 'Invalid pointer position',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use UnexpectedValueException;
class SerializationException extends UnexpectedValueException implements ExceptionInterface
{
public static function forInvalidRequestLine(): self
{
return new self('Invalid request line detected');
}
public static function forInvalidStatusLine(): self
{
return new self('No status line detected');
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnreadableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToConfiguration(): self
{
return new self('Stream is not readable');
}
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot read');
}
public static function dueToPhpError(): self
{
return new self('Error reading stream');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot read');
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use UnexpectedValueException;
use function sprintf;
class UnrecognizedProtocolVersionException extends UnexpectedValueException implements ExceptionInterface
{
public static function forVersion(string $version): self
{
return new self(sprintf('Unrecognized protocol version (%s)', $version));
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnrewindableStreamException extends RuntimeException implements ExceptionInterface
{
public static function forCallbackStream(): self
{
return new self('Callback streams cannot rewind position');
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnseekableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToConfiguration(): self
{
return new self('Stream is not seekable');
}
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot seek position');
}
public static function dueToPhpError(): self
{
return new self('Error seeking within stream');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot seek position');
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UntellableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot tell position');
}
public static function dueToPhpError(): self
{
return new self('Error occurred during tell operation');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot tell position');
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnwritableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToConfiguration(): self
{
return new self('Stream is not writable');
}
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot write');
}
public static function dueToPhpError(): self
{
return new self('Error writing to stream');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot write');
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
use Throwable;
class UploadedFileAlreadyMovedException extends RuntimeException implements ExceptionInterface
{
/** {@inheritDoc} */
public function __construct(
string $message = 'Cannot retrieve stream after it has already moved',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
use function sprintf;
class UploadedFileErrorException extends RuntimeException implements ExceptionInterface
{
public static function forUnmovableFile(): self
{
return new self('Error occurred while moving uploaded file');
}
public static function dueToStreamUploadError(string $error): self
{
return new self(sprintf(
'Cannot retrieve stream due to upload error: %s',
$error
));
}
public static function dueToUnwritablePath(): self
{
return new self('Unable to write to designated path');
}
public static function dueToUnwritableTarget(string $targetDirectory): self
{
return new self(sprintf(
'The target directory `%s` does not exist or is not writable',
$targetDirectory
));
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function gettype;
use function in_array;
use function is_numeric;
use function is_object;
use function is_string;
use function ord;
use function preg_match;
use function sprintf;
use function strlen;
/**
* Provide security tools around HTTP headers to prevent common injection vectors.
*/
final class HeaderSecurity
{
/**
* Private constructor; non-instantiable.
*
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* 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
*/
public static 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;
}
/**
* Validate a header value.
*
* 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
*
* @param string|int|float $value
*/
public static function isValid($value): bool
{
$value = (string) $value;
// Look for:
// \n not preceded by \r, OR
// \r not followed by \n, OR
// \r\n not followed by space or horizontal tab; these are all CRLF attacks
if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
return false;
}
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 10 === line feed
// 13 === carriage return
// 32-126, 128-254 === visible
// 127 === DEL (disallowed)
// 255 === null byte (disallowed)
if (preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)) {
return false;
}
return true;
}
/**
* Assert a header value is valid.
*
* @param mixed $value Value to be tested. This method asserts it is a string or number.
* @throws Exception\InvalidArgumentException For invalid values.
*/
public static function assertValid(mixed $value): void
{
if (! is_string($value) && ! is_numeric($value)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header value type; must be a string or numeric; received %s',
is_object($value) ? $value::class : gettype($value)
));
}
if (! self::isValid($value)) {
throw new Exception\InvalidArgumentException(sprintf(
'"%s" is not valid header value',
$value
));
}
}
/**
* Assert whether or not a header name is valid.
*
* @see http://tools.ietf.org/html/rfc7230#section-3.2
*
* @throws Exception\InvalidArgumentException
*/
public static function assertValidName(mixed $name): void
{
if (! is_string($name)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header name type; expected string; received %s',
is_object($name) ? $name::class : gettype($name)
));
}
if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name)) {
throw new Exception\InvalidArgumentException(sprintf(
'"%s" is not valid header name',
$name
));
}
}
}

View File

@ -0,0 +1,414 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
use function array_map;
use function array_merge;
use function array_values;
use function gettype;
use function implode;
use function is_array;
use function is_object;
use function is_resource;
use function is_string;
use function preg_match;
use function sprintf;
use function str_replace;
use function strtolower;
use function trim;
/**
* Trait implementing the various methods defined in MessageInterface.
*
* @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php
*/
trait MessageTrait
{
/**
* List of all registered headers, as key => array of values.
*
* @var array
* @psalm-var array<non-empty-string, list<string>>
*/
protected $headers = [];
/**
* Map of normalized header name to original name used to register header.
*
* @var array
* @psalm-var array<non-empty-string, non-empty-string>
*/
protected $headerNames = [];
/** @var string */
private $protocol = '1.1';
/** @var StreamInterface */
private $stream;
/**
* 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->protocol;
}
/**
* 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($version): MessageInterface
{
$this->validateProtocolVersion($version);
$new = clone $this;
$new->protocol = $version;
return $new;
}
/**
* Retrieves all message headers.
*
* 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);
* }
* }
*
* @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.
* @psalm-return array<non-empty-string, list<string>>
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $header Case-insensitive header 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($header): bool
{
return isset($this->headerNames[strtolower($header)]);
}
/**
* 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 $header 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($header): array
{
if (! $this->hasHeader($header)) {
return [];
}
$header = $this->headerNames[strtolower($header)];
return $this->headers[$header];
}
/**
* 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($name): string
{
$value = $this->getHeader($name);
if (empty($value)) {
return '';
}
return implode(',', $value);
}
/**
* Return an instance with the provided header, replacing any existing
* values of any headers with the same case-insensitive name.
*
* 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 Exception\InvalidArgumentException For invalid header names or values.
*/
public function withHeader($name, $value): MessageInterface
{
$this->assertHeader($name);
$normalized = strtolower($name);
$new = clone $this;
if ($new->hasHeader($name)) {
unset($new->headers[$new->headerNames[$normalized]]);
}
$value = $this->filterHeaderValue($value);
$new->headerNames[$normalized] = $name;
$new->headers[$name] = $value;
return $new;
}
/**
* 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 Exception\InvalidArgumentException For invalid header names or values.
*/
public function withAddedHeader($name, $value): MessageInterface
{
$this->assertHeader($name);
if (! $this->hasHeader($name)) {
return $this->withHeader($name, $value);
}
$header = $this->headerNames[strtolower($name)];
$new = clone $this;
$value = $this->filterHeaderValue($value);
$new->headers[$header] = array_merge($this->headers[$header], $value);
return $new;
}
/**
* 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($name): MessageInterface
{
if (! is_string($name) || $name === '' || ! $this->hasHeader($name)) {
return clone $this;
}
$normalized = strtolower($name);
$original = $this->headerNames[$normalized];
$new = clone $this;
unset($new->headers[$original], $new->headerNames[$normalized]);
return $new;
}
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody(): StreamInterface
{
return $this->stream;
}
/**
* 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.
*
* @param StreamInterface $body Body.
* @return static
* @throws Exception\InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body): MessageInterface
{
$new = clone $this;
$new->stream = $body;
return $new;
}
/** @param StreamInterface|string|resource $stream */
private function getStream($stream, string $modeIfNotInstance): StreamInterface
{
if ($stream instanceof StreamInterface) {
return $stream;
}
if (! is_string($stream) && ! is_resource($stream)) {
throw new Exception\InvalidArgumentException(
'Stream must be a string stream resource identifier, '
. 'an actual stream resource, '
. 'or a Psr\Http\Message\StreamInterface implementation'
);
}
return new Stream($stream, $modeIfNotInstance);
}
/**
* 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.
*/
private function setHeaders(array $originalHeaders): void
{
$headerNames = $headers = [];
foreach ($originalHeaders as $header => $value) {
$value = $this->filterHeaderValue($value);
$this->assertHeader($header);
$headerNames[strtolower($header)] = $header;
$headers[$header] = $value;
}
$this->headerNames = $headerNames;
$this->headers = $headers;
}
/**
* Validate the HTTP protocol version
*
* @param string $version
* @throws Exception\InvalidArgumentException On invalid HTTP protocol version.
*/
private function validateProtocolVersion($version): void
{
if (empty($version)) {
throw new Exception\InvalidArgumentException(
'HTTP protocol version can not be empty'
);
}
if (! is_string($version)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported HTTP protocol version; must be a string, received %s',
is_object($version) ? $version::class : gettype($version)
));
}
// HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate
// versions of the protocol, while HTTP/2 does not.
if (! preg_match('#^(1\.[01]|2(\.0)?)$#', $version)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported HTTP protocol version "%s" provided',
$version
));
}
}
/** @return list<string> */
private function filterHeaderValue(mixed $values): array
{
if (! is_array($values)) {
$values = [$values];
}
if ([] === $values) {
throw new Exception\InvalidArgumentException(
'Invalid header value: must be a string or array of strings; '
. 'cannot be an empty array'
);
}
return array_map(static function ($value): string {
HeaderSecurity::assertValid($value);
$value = (string) $value;
// Normalize line folding to a single space (RFC 7230#3.2.4).
$value = str_replace(["\r\n\t", "\r\n "], ' ', $value);
// Remove optional whitespace (OWS, RFC 7230#3.2.3) around the header value.
return trim($value, "\t ");
}, array_values($values));
}
/**
* Ensure header name and values are valid.
*
* @param string $name
* @throws Exception\InvalidArgumentException
*/
private function assertHeader($name): void
{
HeaderSecurity::assertValidName($name);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
class Module
{
public function getConfig(): array
{
return [
'service_manager' => (new ConfigProvider())->getDependencies(),
];
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Stringable;
use function stream_get_contents;
/**
* Caching version of php://input
*/
class PhpInputStream extends Stream implements Stringable
{
private string $cache = '';
private bool $reachedEof = false;
/**
* @param string|resource $stream
*/
public function __construct($stream = 'php://input')
{
parent::__construct($stream, 'r');
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
if ($this->reachedEof) {
return $this->cache;
}
$this->getContents();
return $this->cache;
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function read($length): string
{
$content = parent::read($length);
if (! $this->reachedEof) {
$this->cache .= $content;
}
if ($this->eof()) {
$this->reachedEof = true;
}
return $content;
}
/**
* {@inheritdoc}
*/
public function getContents($maxLength = -1): string
{
if ($this->reachedEof) {
return $this->cache;
}
$contents = stream_get_contents($this->resource, $maxLength);
$this->cache .= $contents;
if ($maxLength === -1 || $this->eof()) {
$this->reachedEof = true;
}
return $contents;
}
}

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamInterface;
use Stringable;
use const SEEK_SET;
/**
* Wrapper for default Stream class, representing subpart (starting from given offset) of initial stream.
* It can be used to avoid copying full stream, conserving memory.
*
* @see AbstractSerializer::splitStream()
*/
final class RelativeStream implements StreamInterface, Stringable
{
private int $offset;
public function __construct(private StreamInterface $decoratedStream, ?int $offset)
{
$this->offset = (int) $offset;
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
/**
* {@inheritdoc}
*/
public function close(): void
{
$this->decoratedStream->close();
}
/**
* {@inheritdoc}
*/
public function detach()
{
return $this->decoratedStream->detach();
}
/**
* {@inheritdoc}
*/
public function getSize(): int
{
return $this->decoratedStream->getSize() - $this->offset;
}
/**
* {@inheritdoc}
*/
public function tell(): int
{
return $this->decoratedStream->tell() - $this->offset;
}
/**
* {@inheritdoc}
*/
public function eof(): bool
{
return $this->decoratedStream->eof();
}
/**
* {@inheritdoc}
*/
public function isSeekable(): bool
{
return $this->decoratedStream->isSeekable();
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET): void
{
if ($whence === SEEK_SET) {
$this->decoratedStream->seek($offset + $this->offset, $whence);
return;
}
$this->decoratedStream->seek($offset, $whence);
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
return $this->decoratedStream->isWritable();
}
/**
* {@inheritdoc}
*/
public function write($string): int
{
if ($this->tell() < 0) {
throw new Exception\InvalidStreamPointerPositionException();
}
return $this->decoratedStream->write($string);
}
/**
* {@inheritdoc}
*/
public function isReadable(): bool
{
return $this->decoratedStream->isReadable();
}
/**
* {@inheritdoc}
*/
public function read($length): string
{
if ($this->tell() < 0) {
throw new Exception\InvalidStreamPointerPositionException();
}
return $this->decoratedStream->read($length);
}
/**
* {@inheritdoc}
*/
public function getContents(): string
{
if ($this->tell() < 0) {
throw new Exception\InvalidStreamPointerPositionException();
}
return $this->decoratedStream->getContents();
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
return $this->decoratedStream->getMetadata($key);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use function strtolower;
/**
* HTTP Request encapsulation
*
* Requests are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class Request implements RequestInterface
{
use RequestTrait;
/**
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @throws Exception\InvalidArgumentException For any invalid value.
*/
public function __construct($uri = null, ?string $method = null, $body = 'php://temp', array $headers = [])
{
$this->initialize($uri, $method, $body, $headers);
}
/**
* {@inheritdoc}
*/
public function getHeaders(): array
{
$headers = $this->headers;
if (
! $this->hasHeader('host')
&& $this->uri->getHost()
) {
$headers['Host'] = [$this->getHostFromUri()];
}
return $headers;
}
/**
* {@inheritdoc}
*/
public function getHeader($name): array
{
if (empty($name) || ! $this->hasHeader($name)) {
if (
strtolower($name) === 'host'
&& $this->uri->getHost()
) {
return [$this->getHostFromUri()];
}
return [];
}
$header = $this->headerNames[strtolower($name)];
return $this->headers[$header];
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Request;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Request;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\RequestInterface;
use Throwable;
use function sprintf;
/**
* Serialize or deserialize request messages to/from arrays.
*
* This class provides functionality for serializing a RequestInterface instance
* to an array, as well as the reverse operation of creating a Request instance
* from an array representing a message.
*/
final class ArraySerializer
{
/**
* Serialize a request message to an array.
*
* @return array{
* method: string,
* request_target: string,
* uri: string,
* protocol_version: string,
* headers: array<array<string>>,
* body: string
* }
*/
public static function toArray(RequestInterface $request): array
{
return [
'method' => $request->getMethod(),
'request_target' => $request->getRequestTarget(),
'uri' => (string) $request->getUri(),
'protocol_version' => $request->getProtocolVersion(),
'headers' => $request->getHeaders(),
'body' => (string) $request->getBody(),
];
}
/**
* Deserialize a request array to a request instance.
*
* @throws Exception\DeserializationException When the response cannot be deserialized.
*/
public static function fromArray(array $serializedRequest): Request
{
try {
$uri = self::getValueFromKey($serializedRequest, 'uri');
$method = self::getValueFromKey($serializedRequest, 'method');
$body = new Stream('php://memory', 'wb+');
$body->write(self::getValueFromKey($serializedRequest, 'body'));
$headers = self::getValueFromKey($serializedRequest, 'headers');
$requestTarget = self::getValueFromKey($serializedRequest, 'request_target');
$protocolVersion = self::getValueFromKey($serializedRequest, 'protocol_version');
return (new Request($uri, $method, $body, $headers))
->withRequestTarget($requestTarget)
->withProtocolVersion($protocolVersion);
} catch (Throwable $exception) {
throw Exception\DeserializationException::forRequestFromArray($exception);
}
}
/**
* @return mixed
* @throws Exception\DeserializationException
*/
private static function getValueFromKey(array $data, string $key, ?string $message = null)
{
if (isset($data[$key])) {
return $data[$key];
}
if ($message === null) {
$message = sprintf('Missing "%s" key in serialized request', $key);
}
throw new Exception\DeserializationException($message);
}
}

View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Request;
use Laminas\Diactoros\AbstractSerializer;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Request;
use Laminas\Diactoros\Stream;
use Laminas\Diactoros\Uri;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use function preg_match;
use function sprintf;
/**
* Serialize (cast to string) or deserialize (cast string to Request) messages.
*
* This class provides functionality for serializing a RequestInterface instance
* to a string, as well as the reverse operation of creating a Request instance
* from a string/stream representing a message.
*/
final class Serializer extends AbstractSerializer
{
/**
* Deserialize a request string to a request instance.
*
* Internally, casts the message to a stream and invokes fromStream().
*
* @throws Exception\SerializationException When errors occur parsing the message.
*/
public static function fromString(string $message): Request
{
$stream = new Stream('php://temp', 'wb+');
$stream->write($message);
return self::fromStream($stream);
}
/**
* Deserialize a request stream to a request instance.
*
* @throws Exception\InvalidArgumentException If the message stream is not readable or seekable.
* @throws Exception\SerializationException If an invalid request line is detected.
*/
public static function fromStream(StreamInterface $stream): Request
{
if (! $stream->isReadable() || ! $stream->isSeekable()) {
throw new Exception\InvalidArgumentException('Message stream must be both readable and seekable');
}
$stream->rewind();
[$method, $requestTarget, $version] = self::getRequestLine($stream);
$uri = self::createUriFromRequestTarget($requestTarget);
[$headers, $body] = self::splitStream($stream);
return (new Request($uri, $method, $body, $headers))
->withProtocolVersion($version)
->withRequestTarget($requestTarget);
}
/**
* Serialize a request message to a string.
*/
public static function toString(RequestInterface $request): string
{
$httpMethod = $request->getMethod();
$headers = self::serializeHeaders($request->getHeaders());
$body = (string) $request->getBody();
$format = '%s %s HTTP/%s%s%s';
if (! empty($headers)) {
$headers = "\r\n" . $headers;
}
if (! empty($body)) {
$headers .= "\r\n\r\n";
}
return sprintf(
$format,
$httpMethod,
$request->getRequestTarget(),
$request->getProtocolVersion(),
$headers,
$body
);
}
/**
* Retrieve the components of the request line.
*
* Retrieves the first line of the stream and parses it, raising an
* exception if it does not follow specifications; if valid, returns a list
* with the method, target, and version, in that order.
*
* @throws Exception\SerializationException
*/
private static function getRequestLine(StreamInterface $stream): array
{
$requestLine = self::getLine($stream);
if (
! preg_match(
'#^(?P<method>[!\#$%&\'*+.^_`|~a-zA-Z0-9-]+) (?P<target>[^\s]+) HTTP/(?P<version>[1-9]\d*\.\d+)$#',
$requestLine,
$matches
)
) {
throw Exception\SerializationException::forInvalidRequestLine();
}
return [$matches['method'], $matches['target'], $matches['version']];
}
/**
* Create and return a Uri instance based on the provided request target.
*
* If the request target is of authority or asterisk form, an empty Uri
* instance is returned; otherwise, the value is used to create and return
* a new Uri instance.
*/
private static function createUriFromRequestTarget(string $requestTarget): Uri
{
if (preg_match('#^https?://#', $requestTarget)) {
return new Uri($requestTarget);
}
if (preg_match('#^(\*|[^/])#', $requestTarget)) {
return new Uri();
}
return new Uri($requestTarget);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
class RequestFactory implements RequestFactoryInterface
{
/**
* {@inheritDoc}
*/
public function createRequest(string $method, $uri): RequestInterface
{
return new Request($uri, $method);
}
}

View File

@ -0,0 +1,315 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use function array_keys;
use function gettype;
use function is_object;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
/**
* Trait with common request behaviors.
*
* Server and client-side requests differ slightly in how the Host header is
* handled; on client-side, it should be calculated on-the-fly from the
* composed URI (if present), while on server-side, it will be calculated from
* the environment. As such, this trait exists to provide the common code
* between both client-side and server-side requests, and each can then
* use the headers functionality required by their implementations.
*/
trait RequestTrait
{
use MessageTrait;
/** @var string */
private $method = 'GET';
/**
* The request-target, if it has been provided or calculated.
*
* @var null|string
*/
private $requestTarget;
/** @var UriInterface */
private $uri;
/**
* Initialize request state.
*
* Used by constructors.
*
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @throws Exception\InvalidArgumentException For any invalid value.
*/
private function initialize(
$uri = null,
?string $method = null,
$body = 'php://memory',
array $headers = []
): void {
if ($method !== null) {
$this->setMethod($method);
}
$this->uri = $this->createUri($uri);
$this->stream = $this->getStream($body, 'wb+');
$this->setHeaders($headers);
// per PSR-7: attempt to set the Host header from a provided URI if no
// Host header is provided
if (! $this->hasHeader('Host') && $this->uri->getHost()) {
$this->headerNames['host'] = 'Host';
$this->headers['Host'] = [$this->getHostFromUri()];
}
}
/**
* Create and return a URI instance.
*
* If `$uri` is a already a `UriInterface` instance, returns it.
*
* If `$uri` is a string, passes it to the `Uri` constructor to return an
* instance.
*
* If `$uri is null, creates and returns an empty `Uri` instance.
*
* Otherwise, it raises an exception.
*
* @param null|string|UriInterface $uri
* @throws Exception\InvalidArgumentException
*/
private function createUri($uri): UriInterface
{
if ($uri instanceof UriInterface) {
return $uri;
}
if (is_string($uri)) {
return new Uri($uri);
}
if ($uri === null) {
return new Uri();
}
throw new Exception\InvalidArgumentException(
'Invalid URI provided; must be null, a string, or a Psr\Http\Message\UriInterface instance'
);
}
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method MUST return the string "/".
*/
public function getRequestTarget(): string
{
if (null !== $this->requestTarget) {
return $this->requestTarget;
}
$target = $this->uri->getPath();
if ($this->uri->getQuery()) {
$target .= '?' . $this->uri->getQuery();
}
if (empty($target)) {
$target = '/';
}
return $target;
}
/**
* Create a new instance with a specific request-target.
*
* If the request needs a non-origin-form request-target — e.g., for
* specifying an absolute-form, authority-form, or asterisk-form —
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* 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
* changed request target.
*
* @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
* request-target forms allowed in request messages)
*
* @param string $requestTarget
* @throws Exception\InvalidArgumentException If the request target is invalid.
* @return static
*/
public function withRequestTarget($requestTarget): RequestInterface
{
if (preg_match('#\s#', $requestTarget)) {
throw new Exception\InvalidArgumentException(
'Invalid request target provided; cannot contain whitespace'
);
}
$new = clone $this;
$new->requestTarget = $requestTarget;
return $new;
}
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod(): string
{
return $this->method;
}
/**
* Return an instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive and thus implementations SHOULD NOT
* modify the given string.
*
* 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
* changed request method.
*
* @param string $method Case-insensitive method.
* @throws Exception\InvalidArgumentException For invalid HTTP methods.
* @return static
*/
public function withMethod($method): RequestInterface
{
$new = clone $this;
$new->setMethod($method);
return $new;
}
/**
* Retrieves the URI instance.
*
* This method MUST return a UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
*
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request, if any.
*/
public function getUri(): UriInterface
{
return $this->uri;
}
/**
* Returns an instance with the provided URI.
*
* This method will update the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header will be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, the returned request will not update the Host header of the
* returned message -- even if the message contains no Host header. This
* means that a call to `getHeader('Host')` on the original request MUST
* equal the return value of a call to `getHeader('Host')` on the returned
* request.
*
* 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 UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
*
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
{
$new = clone $this;
$new->uri = $uri;
if ($preserveHost && $this->hasHeader('Host')) {
return $new;
}
if (! $uri->getHost()) {
return $new;
}
$host = $uri->getHost();
if ($uri->getPort()) {
$host .= ':' . $uri->getPort();
}
$new->headerNames['host'] = 'Host';
// Remove an existing host header if present, regardless of current
// de-normalization of the header name.
// @see https://github.com/zendframework/zend-diactoros/issues/91
foreach (array_keys($new->headers) as $header) {
if (strtolower($header) === 'host') {
unset($new->headers[$header]);
}
}
$new->headers['Host'] = [$host];
return $new;
}
/**
* Set and validate the HTTP method
*
* @param string $method
* @throws Exception\InvalidArgumentException On invalid HTTP method.
*/
private function setMethod($method): void
{
if (! is_string($method)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported HTTP method; must be a string, received %s',
is_object($method) ? $method::class : gettype($method)
));
}
if (! preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported HTTP method "%s" provided',
$method
));
}
$this->method = $method;
}
/**
* Retrieve the host from the URI instance
*/
private function getHostFromUri(): string
{
$host = $this->uri->getHost();
$host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
return $host;
}
}

View File

@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use function gettype;
use function is_float;
use function is_numeric;
use function is_object;
use function is_scalar;
use function is_string;
use function sprintf;
/**
* HTTP response encapsulation.
*
* Responses are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class Response implements ResponseInterface
{
use MessageTrait;
public const MIN_STATUS_CODE_VALUE = 100;
public const MAX_STATUS_CODE_VALUE = 599;
/**
* Map of standard HTTP status code/reason phrases
*
* @psalm-var array<positive-int, non-empty-string>
*/
private array $phrases = [
// INFORMATIONAL CODES
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
// SUCCESS CODES
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
// REDIRECTION CODES
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Switch Proxy', // Deprecated to 306 => '(Unused)'
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
// CLIENT ERROR
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Content Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
421 => 'Misdirected Request',
422 => 'Unprocessable Content',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
444 => 'Connection Closed Without Response',
451 => 'Unavailable For Legal Reasons',
// SERVER ERROR
499 => 'Client Closed Request',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended (OBSOLETED)',
511 => 'Network Authentication Required',
599 => 'Network Connect Timeout Error',
];
private string $reasonPhrase;
private int $statusCode;
/**
* @param string|resource|StreamInterface $body Stream identifier and/or actual stream resource
* @param int $status Status code for the response, if any.
* @param array $headers Headers for the response, if any.
* @throws Exception\InvalidArgumentException On any invalid element.
*/
public function __construct($body = 'php://memory', int $status = 200, array $headers = [])
{
$this->setStatusCode($status);
$this->stream = $this->getStream($body, 'wb+');
$this->setHeaders($headers);
}
/**
* {@inheritdoc}
*/
public function getStatusCode(): int
{
return $this->statusCode;
}
/**
* {@inheritdoc}
*/
public function getReasonPhrase(): string
{
return $this->reasonPhrase;
}
/**
* {@inheritdoc}
*/
public function withStatus($code, $reasonPhrase = ''): Response
{
$new = clone $this;
$new->setStatusCode($code, $reasonPhrase);
return $new;
}
/**
* Set a valid status code.
*
* @param int $code
* @param string $reasonPhrase
* @throws Exception\InvalidArgumentException On an invalid status code.
*/
private function setStatusCode($code, $reasonPhrase = ''): void
{
if (
! is_numeric($code)
|| is_float($code)
|| $code < static::MIN_STATUS_CODE_VALUE
|| $code > static::MAX_STATUS_CODE_VALUE
) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid status code "%s"; must be an integer between %d and %d, inclusive',
is_scalar($code) ? $code : gettype($code),
static::MIN_STATUS_CODE_VALUE,
static::MAX_STATUS_CODE_VALUE
));
}
if (! is_string($reasonPhrase)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported response reason phrase; must be a string, received %s',
is_object($reasonPhrase) ? $reasonPhrase::class : gettype($reasonPhrase)
));
}
if ($reasonPhrase === '' && isset($this->phrases[$code])) {
$reasonPhrase = $this->phrases[$code];
}
$this->reasonPhrase = $reasonPhrase;
$this->statusCode = (int) $code;
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function sprintf;
/**
* Serialize or deserialize response messages to/from arrays.
*
* This class provides functionality for serializing a ResponseInterface instance
* to an array, as well as the reverse operation of creating a Response instance
* from an array representing a message.
*/
final class ArraySerializer
{
/**
* Serialize a response message to an array.
*
* @return array{
* status_code: int,
* reason_phrase: string,
* protocol_version: string,
* headers: array<array<string>>,
* body: string
* }
*/
public static function toArray(ResponseInterface $response): array
{
return [
'status_code' => $response->getStatusCode(),
'reason_phrase' => $response->getReasonPhrase(),
'protocol_version' => $response->getProtocolVersion(),
'headers' => $response->getHeaders(),
'body' => (string) $response->getBody(),
];
}
/**
* Deserialize a response array to a response instance.
*
* @throws Exception\DeserializationException When cannot deserialize response.
*/
public static function fromArray(array $serializedResponse): Response
{
try {
$body = new Stream('php://memory', 'wb+');
$body->write(self::getValueFromKey($serializedResponse, 'body'));
$statusCode = self::getValueFromKey($serializedResponse, 'status_code');
$headers = self::getValueFromKey($serializedResponse, 'headers');
$protocolVersion = self::getValueFromKey($serializedResponse, 'protocol_version');
$reasonPhrase = self::getValueFromKey($serializedResponse, 'reason_phrase');
return (new Response($body, $statusCode, $headers))
->withProtocolVersion($protocolVersion)
->withStatus($statusCode, $reasonPhrase);
} catch (Throwable $exception) {
throw Exception\DeserializationException::forResponseFromArray($exception);
}
}
/**
* @return mixed
* @throws Exception\DeserializationException
*/
private static function getValueFromKey(array $data, string $key, ?string $message = null)
{
if (isset($data[$key])) {
return $data[$key];
}
if ($message === null) {
$message = sprintf('Missing "%s" key in serialized response', $key);
}
throw new Exception\DeserializationException($message);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
/**
* A class representing empty HTTP responses.
*/
class EmptyResponse extends Response
{
/**
* Create an empty response with the given status code.
*
* @param int $status Status code for the response, if any.
* @param array $headers Headers for the response, if any.
*/
public function __construct(int $status = 204, array $headers = [])
{
$body = new Stream('php://temp', 'r');
parent::__construct($body, $status, $headers);
}
/**
* Create an empty response with the given headers.
*
* @param array $headers Headers for the response.
*/
public static function withHeaders(array $headers): EmptyResponse
{
return new static(204, $headers);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\StreamInterface;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* HTML response.
*
* Allows creating a response by passing an HTML string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/html.
*/
class HtmlResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create an HTML response.
*
* Produces an HTML response with a Content-Type of text/html and a default
* status of 200.
*
* @param string|StreamInterface $html HTML or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws Exception\InvalidArgumentException If $html is neither a string or stream.
*/
public function __construct($html, int $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($html),
$status,
$this->injectContentType('text/html; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $html
* @throws Exception\InvalidArgumentException If $html is neither a string or stream.
*/
private function createBody($html): StreamInterface
{
if ($html instanceof StreamInterface) {
return $html;
}
if (! is_string($html)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
is_object($html) ? $html::class : gettype($html),
self::class
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($html);
$body->rewind();
return $body;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use function array_keys;
use function array_reduce;
use function strtolower;
trait InjectContentTypeTrait
{
/**
* Inject the provided Content-Type, if none is already present.
*
* @return array Headers with injected Content-Type
*/
private function injectContentType(string $contentType, array $headers): array
{
$hasContentType = array_reduce(
array_keys($headers),
static fn($carry, $item) => $carry ?: strtolower($item) === 'content-type',
false
);
if (! $hasContentType) {
$headers['content-type'] = [$contentType];
}
return $headers;
}
}

View File

@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use function is_object;
use function is_resource;
use function json_encode;
use function json_last_error;
use function json_last_error_msg;
use function sprintf;
use const JSON_ERROR_NONE;
use const JSON_HEX_AMP;
use const JSON_HEX_APOS;
use const JSON_HEX_QUOT;
use const JSON_HEX_TAG;
use const JSON_UNESCAPED_SLASHES;
/**
* JSON response.
*
* Allows creating a response by passing data to the constructor; by default,
* serializes the data to JSON, sets a status code of 200 and sets the
* Content-Type header to application/json.
*/
class JsonResponse extends Response
{
use InjectContentTypeTrait;
/**
* Default flags for json_encode
*
* @const int
*/
public const DEFAULT_JSON_FLAGS = JSON_HEX_TAG
| JSON_HEX_APOS
| JSON_HEX_AMP
| JSON_HEX_QUOT
| JSON_UNESCAPED_SLASHES;
/** @var mixed */
private $payload;
/**
* Create a JSON response with the given data.
*
* Default JSON encoding is performed with the following options, which
* produces RFC4627-compliant JSON, capable of embedding into HTML.
*
* - JSON_HEX_TAG
* - JSON_HEX_APOS
* - JSON_HEX_AMP
* - JSON_HEX_QUOT
* - JSON_UNESCAPED_SLASHES
*
* @param mixed $data Data to convert to JSON.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @param int $encodingOptions JSON encoding options to use.
* @throws Exception\InvalidArgumentException If unable to encode the $data to JSON.
*/
public function __construct(
$data,
int $status = 200,
array $headers = [],
private int $encodingOptions = self::DEFAULT_JSON_FLAGS
) {
$this->setPayload($data);
$json = $this->jsonEncode($data, $this->encodingOptions);
$body = $this->createBodyFromJson($json);
$headers = $this->injectContentType('application/json', $headers);
parent::__construct($body, $status, $headers);
}
/**
* @return mixed
*/
public function getPayload()
{
return $this->payload;
}
public function withPayload(mixed $data): JsonResponse
{
$new = clone $this;
$new->setPayload($data);
return $this->updateBodyFor($new);
}
public function getEncodingOptions(): int
{
return $this->encodingOptions;
}
public function withEncodingOptions(int $encodingOptions): JsonResponse
{
$new = clone $this;
$new->encodingOptions = $encodingOptions;
return $this->updateBodyFor($new);
}
private function createBodyFromJson(string $json): Stream
{
$body = new Stream('php://temp', 'wb+');
$body->write($json);
$body->rewind();
return $body;
}
/**
* Encode the provided data to JSON.
*
* @throws Exception\InvalidArgumentException If unable to encode the $data to JSON.
*/
private function jsonEncode(mixed $data, int $encodingOptions): string
{
if (is_resource($data)) {
throw new Exception\InvalidArgumentException('Cannot JSON encode resources');
}
// Clear json_last_error()
json_encode(null);
$json = json_encode($data, $encodingOptions);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new Exception\InvalidArgumentException(sprintf(
'Unable to encode data to JSON in %s: %s',
self::class,
json_last_error_msg()
));
}
return $json;
}
private function setPayload(mixed $data): void
{
if (is_object($data)) {
$data = clone $data;
}
$this->payload = $data;
}
/**
* Update the response body for the given instance.
*
* @param self $toUpdate Instance to update.
* @return JsonResponse Returns a new instance with an updated body.
*/
private function updateBodyFor(JsonResponse $toUpdate): JsonResponse
{
$json = $this->jsonEncode($toUpdate->payload, $toUpdate->encodingOptions);
$body = $this->createBodyFromJson($json);
return $toUpdate->withBody($body);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Psr\Http\Message\UriInterface;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* Produce a redirect response.
*/
class RedirectResponse extends Response
{
/**
* Create a redirect response.
*
* Produces a redirect response with a Location header and the given status
* (302 by default).
*
* Note: this method overwrites the `location` $headers value.
*
* @param string|UriInterface $uri URI for the Location header.
* @param int $status Integer status code for the redirect; 302 by default.
* @param array $headers Array of headers to use at initialization.
*/
public function __construct($uri, int $status = 302, array $headers = [])
{
if (! is_string($uri) && ! $uri instanceof UriInterface) {
throw new Exception\InvalidArgumentException(sprintf(
'Uri provided to %s MUST be a string or Psr\Http\Message\UriInterface instance; received "%s"',
self::class,
is_object($uri) ? $uri::class : gettype($uri)
));
}
$headers['location'] = [(string) $uri];
parent::__construct('php://temp', $status, $headers);
}
}

View File

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\AbstractSerializer;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use function preg_match;
use function sprintf;
final class Serializer extends AbstractSerializer
{
/**
* Deserialize a response string to a response instance.
*
* @throws Exception\SerializationException When errors occur parsing the message.
*/
public static function fromString(string $message): Response
{
$stream = new Stream('php://temp', 'wb+');
$stream->write($message);
return static::fromStream($stream);
}
/**
* Parse a response from a stream.
*
* @throws Exception\InvalidArgumentException When the stream is not readable.
* @throws Exception\SerializationException When errors occur parsing the message.
*/
public static function fromStream(StreamInterface $stream): Response
{
if (! $stream->isReadable() || ! $stream->isSeekable()) {
throw new Exception\InvalidArgumentException('Message stream must be both readable and seekable');
}
$stream->rewind();
[$version, $status, $reasonPhrase] = self::getStatusLine($stream);
[$headers, $body] = self::splitStream($stream);
return (new Response($body, $status, $headers))
->withProtocolVersion($version)
->withStatus((int) $status, $reasonPhrase);
}
/**
* Create a string representation of a response.
*/
public static function toString(ResponseInterface $response): string
{
$reasonPhrase = $response->getReasonPhrase();
$headers = self::serializeHeaders($response->getHeaders());
$body = (string) $response->getBody();
$format = 'HTTP/%s %d%s%s%s';
if (! empty($headers)) {
$headers = "\r\n" . $headers;
}
$headers .= "\r\n\r\n";
return sprintf(
$format,
$response->getProtocolVersion(),
$response->getStatusCode(),
$reasonPhrase ? ' ' . $reasonPhrase : '',
$headers,
$body
);
}
/**
* Retrieve the status line for the message.
*
* @return array Array with three elements: 0 => version, 1 => status, 2 => reason
* @throws Exception\SerializationException If line is malformed.
*/
private static function getStatusLine(StreamInterface $stream): array
{
$line = self::getLine($stream);
if (
! preg_match(
'#^HTTP/(?P<version>[1-9]\d*\.\d) (?P<status>[1-5]\d{2})(\s+(?P<reason>.+))?$#',
$line,
$matches
)
) {
throw Exception\SerializationException::forInvalidStatusLine();
}
return [$matches['version'], (int) $matches['status'], $matches['reason'] ?? ''];
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\StreamInterface;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* Plain text response.
*
* Allows creating a response by passing a string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/plain.
*/
class TextResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create a plain text response.
*
* Produces a text response with a Content-Type of text/plain and a default
* status of 200.
*
* @param string|StreamInterface $text String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws Exception\InvalidArgumentException If $text is neither a string or stream.
*/
public function __construct($text, int $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($text),
$status,
$this->injectContentType('text/plain; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $text
* @throws Exception\InvalidArgumentException If $text is neither a string or stream.
*/
private function createBody($text): StreamInterface
{
if ($text instanceof StreamInterface) {
return $text;
}
if (! is_string($text)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
is_object($text) ? $text::class : gettype($text),
self::class
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($text);
$body->rewind();
return $body;
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\StreamInterface;
use function gettype;
use function is_object;
use function is_string;
use function sprintf;
/**
* XML response.
*
* Allows creating a response by passing an XML string to the constructor; by default,
* sets a status code of 200 and sets the Content-Type header to application/xml.
*/
class XmlResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create an XML response.
*
* Produces an XML response with a Content-Type of application/xml and a default
* status of 200.
*
* @param string|StreamInterface $xml String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array $headers Array of headers to use at initialization.
* @throws Exception\InvalidArgumentException If $text is neither a string or stream.
*/
public function __construct(
$xml,
int $status = 200,
array $headers = []
) {
parent::__construct(
$this->createBody($xml),
$status,
$this->injectContentType('application/xml; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $xml
* @throws Exception\InvalidArgumentException If $xml is neither a string or stream.
*/
private function createBody($xml): StreamInterface
{
if ($xml instanceof StreamInterface) {
return $xml;
}
if (! is_string($xml)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
is_object($xml) ? $xml::class : gettype($xml),
self::class
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($xml);
$body->rewind();
return $body;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class ResponseFactory implements ResponseFactoryInterface
{
/**
* {@inheritDoc}
*/
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
{
return (new Response())
->withStatus($code, $reasonPhrase);
}
}

View File

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
use function array_key_exists;
use function gettype;
use function is_array;
use function is_object;
use function sprintf;
/**
* Server-side HTTP request
*
* Extends the Request definition to add methods for accessing incoming data,
* specifically server parameters, cookies, matched path parameters, query
* string arguments, body parameters, and upload file information.
*
* "Attributes" are discovered via decomposing the request (and usually
* specifically the URI path), and typically will be injected by the application.
*
* Requests are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class ServerRequest implements ServerRequestInterface
{
use RequestTrait;
private array $attributes = [];
private array $uploadedFiles;
/**
* @param array $serverParams Server parameters, typically from $_SERVER
* @param array $uploadedFiles Upload file information, a tree of UploadedFiles
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array $headers Headers for the message, if any.
* @param array $cookieParams Cookies for the message, if any.
* @param array $queryParams Query params for the message, if any.
* @param null|array|object $parsedBody The deserialized body parameters, if any.
* @param string $protocol HTTP protocol version.
* @throws Exception\InvalidArgumentException For any invalid value.
*/
public function __construct(
private array $serverParams = [],
array $uploadedFiles = [],
$uri = null,
?string $method = null,
$body = 'php://input',
array $headers = [],
private array $cookieParams = [],
private array $queryParams = [],
private $parsedBody = null,
string $protocol = '1.1'
) {
$this->validateUploadedFiles($uploadedFiles);
if ($body === 'php://input') {
$body = new PhpInputStream();
}
$this->initialize($uri, $method, $body, $headers);
$this->uploadedFiles = $uploadedFiles;
$this->protocol = $protocol;
}
/**
* {@inheritdoc}
*/
public function getServerParams(): array
{
return $this->serverParams;
}
/**
* {@inheritdoc}
*/
public function getUploadedFiles(): array
{
return $this->uploadedFiles;
}
/**
* {@inheritdoc}
*/
public function withUploadedFiles(array $uploadedFiles): ServerRequest
{
$this->validateUploadedFiles($uploadedFiles);
$new = clone $this;
$new->uploadedFiles = $uploadedFiles;
return $new;
}
/**
* {@inheritdoc}
*/
public function getCookieParams(): array
{
return $this->cookieParams;
}
/**
* {@inheritdoc}
*/
public function withCookieParams(array $cookies): ServerRequest
{
$new = clone $this;
$new->cookieParams = $cookies;
return $new;
}
/**
* {@inheritdoc}
*/
public function getQueryParams(): array
{
return $this->queryParams;
}
/**
* {@inheritdoc}
*/
public function withQueryParams(array $query): ServerRequest
{
$new = clone $this;
$new->queryParams = $query;
return $new;
}
/**
* {@inheritdoc}
*/
public function getParsedBody()
{
return $this->parsedBody;
}
/**
* {@inheritdoc}
*/
public function withParsedBody($data): ServerRequest
{
if (! is_array($data) && ! is_object($data) && null !== $data) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a null, array, or object argument; received %s',
__METHOD__,
gettype($data)
));
}
$new = clone $this;
$new->parsedBody = $data;
return $new;
}
/**
* {@inheritdoc}
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* {@inheritdoc}
*/
public function getAttribute($attribute, $default = null)
{
if (! array_key_exists($attribute, $this->attributes)) {
return $default;
}
return $this->attributes[$attribute];
}
/**
* {@inheritdoc}
*/
public function withAttribute($attribute, $value): ServerRequest
{
$new = clone $this;
$new->attributes[$attribute] = $value;
return $new;
}
/**
* {@inheritdoc}
*/
public function withoutAttribute($name): ServerRequest
{
$new = clone $this;
unset($new->attributes[$name]);
return $new;
}
/**
* Recursively validate the structure in an uploaded files array.
*
* @throws Exception\InvalidArgumentException If any leaf is not an UploadedFileInterface instance.
*/
private function validateUploadedFiles(array $uploadedFiles): void
{
foreach ($uploadedFiles as $file) {
if (is_array($file)) {
$this->validateUploadedFiles($file);
continue;
}
if (! $file instanceof UploadedFileInterface) {
throw new Exception\InvalidArgumentException('Invalid leaf in uploaded files structure');
}
}
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface;
use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use function array_key_exists;
use function is_callable;
/**
* Class for marshaling a request object from the current PHP environment.
*/
class ServerRequestFactory implements ServerRequestFactoryInterface
{
/**
* Function to use to get apache request headers; present only to simplify mocking.
*
* @var callable
*/
private static $apacheRequestHeaders = 'apache_request_headers';
/**
* Create a request from the supplied superglobal values.
*
* If any argument is not supplied, the corresponding superglobal value will
* be used.
*
* The ServerRequest created is then passed to the fromServer() method in
* order to marshal the request URI and headers.
*
* @see fromServer()
*
* @param array $server $_SERVER superglobal
* @param array $query $_GET superglobal
* @param array $body $_POST superglobal
* @param array $cookies $_COOKIE superglobal
* @param array $files $_FILES superglobal
* @param null|FilterServerRequestInterface $requestFilter If present, the
* generated request will be passed to this instance and the result
* returned by this method. When not present, a default instance of
* FilterUsingXForwardedHeaders is created, using the `trustReservedSubnets()`
* constructor.
*/
public static function fromGlobals(
?array $server = null,
?array $query = null,
?array $body = null,
?array $cookies = null,
?array $files = null,
?FilterServerRequestInterface $requestFilter = null
): ServerRequest {
$requestFilter = $requestFilter ?: FilterUsingXForwardedHeaders::trustReservedSubnets();
$server = normalizeServer(
$server ?: $_SERVER,
is_callable(self::$apacheRequestHeaders) ? self::$apacheRequestHeaders : null
);
$files = normalizeUploadedFiles($files ?: $_FILES);
$headers = marshalHeadersFromSapi($server);
if (null === $cookies && array_key_exists('cookie', $headers)) {
$cookies = parseCookieHeader($headers['cookie']);
}
return $requestFilter(new ServerRequest(
$server,
$files,
UriFactory::createFromSapi($server, $headers),
marshalMethodFromSapi($server),
'php://input',
$headers,
$cookies ?: $_COOKIE,
$query ?: $_GET,
$body ?: $_POST,
marshalProtocolVersionFromSapi($server)
));
}
/**
* {@inheritDoc}
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
$uploadedFiles = [];
return new ServerRequest(
$serverParams,
$uploadedFiles,
$uri,
$method,
'php://temp'
);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Psr\Http\Message\ServerRequestInterface;
final class DoNotFilter implements FilterServerRequestInterface
{
public function __invoke(ServerRequestInterface $request): ServerRequestInterface
{
return $request;
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Psr\Http\Message\ServerRequestInterface;
/**
* Filter/initialize a server request.
*
* Implementations of this interface will take an incoming request, and
* decide if additional modifications are necessary. As examples:
*
* - Injecting a unique request identifier header.
* - Using the X-Forwarded-* headers to rewrite the URI to reflect the original request.
* - Using the Forwarded header to rewrite the URI to reflect the original request.
*
* This functionality is consumed by the ServerRequestFactory using the request
* instance it generates, just prior to returning a request.
*/
interface FilterServerRequestInterface
{
/**
* Determine if a request needs further modification, and if so, return a
* new instance reflecting those modifications.
*/
public function __invoke(ServerRequestInterface $request): ServerRequestInterface;
}

View File

@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException;
use Laminas\Diactoros\Exception\InvalidProxyAddressException;
use Laminas\Diactoros\UriFactory;
use Psr\Http\Message\ServerRequestInterface;
use function assert;
use function count;
use function explode;
use function filter_var;
use function in_array;
use function is_string;
use function str_contains;
use function strtolower;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* Modify the URI to reflect the X-Forwarded-* headers.
*
* If the request comes from a trusted proxy, this filter will analyze the
* various X-Forwarded-* headers, if any, and if they are marked as trusted,
* in order to return a new request that composes a URI instance that reflects
* those headers.
*
* @psalm-immutable
*/
final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface
{
public const HEADER_HOST = 'X-FORWARDED-HOST';
public const HEADER_PORT = 'X-FORWARDED-PORT';
public const HEADER_PROTO = 'X-FORWARDED-PROTO';
private const X_FORWARDED_HEADERS = [
self::HEADER_HOST,
self::HEADER_PORT,
self::HEADER_PROTO,
];
/**
* Only allow construction via named constructors
*
* @param list<non-empty-string> $trustedProxies
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders
*/
private function __construct(private array $trustedProxies = [], private array $trustedHeaders = [])
{
}
public function __invoke(ServerRequestInterface $request): ServerRequestInterface
{
$remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? '';
if ('' === $remoteAddress || ! is_string($remoteAddress)) {
// Should we trigger a warning here?
return $request;
}
if (! $this->isFromTrustedProxy($remoteAddress)) {
// Do nothing
return $request;
}
// Update the URI based on the trusted headers
$uri = $originalUri = $request->getUri();
foreach ($this->trustedHeaders as $headerName) {
$header = $request->getHeaderLine($headerName);
if ('' === $header || str_contains($header, ',')) {
// Reject empty headers and/or headers with multiple values
continue;
}
switch ($headerName) {
case self::HEADER_HOST:
[$host, $port] = UriFactory::marshalHostAndPortFromHeader($header);
$uri = $uri
->withHost($host);
if ($port !== null) {
$uri = $uri->withPort($port);
}
break;
case self::HEADER_PORT:
$uri = $uri->withPort((int) $header);
break;
case self::HEADER_PROTO:
$scheme = strtolower($header) === 'https' ? 'https' : 'http';
$uri = $uri->withScheme($scheme);
break;
}
}
if ($uri !== $originalUri) {
return $request->withUri($uri);
}
return $request;
}
/**
* Indicate which proxies and which X-Forwarded headers to trust.
*
* @param list<non-empty-string> $proxyCIDRList Each element may
* be an IP address or a subnet specified using CIDR notation; both IPv4
* and IPv6 are supported. The special string "*" will be translated to
* two entries, "0.0.0.0/0" and "::/0". An empty list indicates no
* proxies are trusted.
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidProxyAddressException
* @throws InvalidForwardedHeaderNameException
*/
public static function trustProxies(
array $proxyCIDRList,
array $trustedHeaders = self::X_FORWARDED_HEADERS
): self {
$proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
self::validateTrustedHeaders($trustedHeaders);
return new self($proxyCIDRList, $trustedHeaders);
}
/**
* Trust any X-FORWARDED-* headers from any address.
*
* This is functionally equivalent to calling `trustProxies(['*'])`.
*
* WARNING: Only do this if you know for certain that your application
* sits behind a trusted proxy that cannot be spoofed. This should only
* be the case if your server is not publicly addressable, and all requests
* are routed via a reverse proxy (e.g., a load balancer, a server such as
* Caddy, when using Traefik, etc.).
*/
public static function trustAny(): self
{
return self::trustProxies(['*']);
}
/**
* Trust X-Forwarded headers from reserved subnetworks.
*
* This is functionally equivalent to calling `trustProxies()` where the
* `$proxcyCIDRList` argument is a list with the following:
*
* - 10.0.0.0/8
* - 127.0.0.0/8
* - 172.16.0.0/12
* - 192.168.0.0/16
* - ::1/128 (IPv6 localhost)
* - fc00::/7 (IPv6 private networks)
* - fe80::/10 (IPv6 local-link addresses)
*
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidForwardedHeaderNameException
*/
public static function trustReservedSubnets(array $trustedHeaders = self::X_FORWARDED_HEADERS): self
{
return self::trustProxies([
'10.0.0.0/8',
'127.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'::1/128', // ipv6 localhost
'fc00::/7', // ipv6 private networks
'fe80::/10', // ipv6 local-link addresses
], $trustedHeaders);
}
private function isFromTrustedProxy(string $remoteAddress): bool
{
foreach ($this->trustedProxies as $proxy) {
if (IPRange::matches($remoteAddress, $proxy)) {
return true;
}
}
return false;
}
/** @throws InvalidForwardedHeaderNameException */
private static function validateTrustedHeaders(array $headers): void
{
foreach ($headers as $header) {
if (! in_array($header, self::X_FORWARDED_HEADERS, true)) {
throw InvalidForwardedHeaderNameException::forHeader($header);
}
}
}
/**
* @param list<non-empty-string> $proxyCIDRList
* @return list<non-empty-string>
* @throws InvalidProxyAddressException
*/
private static function normalizeProxiesList(array $proxyCIDRList): array
{
$foundWildcard = false;
foreach ($proxyCIDRList as $index => $cidr) {
if ($cidr === '*') {
unset($proxyCIDRList[$index]);
$foundWildcard = true;
continue;
}
if (! self::validateProxyCIDR($cidr)) {
throw InvalidProxyAddressException::forAddress($cidr);
}
}
if ($foundWildcard) {
$proxyCIDRList[] = '0.0.0.0/0';
$proxyCIDRList[] = '::/0';
}
return $proxyCIDRList;
}
private static function validateProxyCIDR(mixed $cidr): bool
{
if (! is_string($cidr) || '' === $cidr) {
return false;
}
$address = $cidr;
$mask = null;
if (str_contains($cidr, '/')) {
$parts = explode('/', $cidr, 2);
assert(count($parts) >= 2);
[$address, $mask] = $parts;
$mask = (int) $mask;
}
if (str_contains($address, ':')) {
// is IPV6
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& (
$mask === null
|| (
$mask <= 128
&& $mask >= 0
)
);
}
// is IPV4
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
&& (
$mask === null
|| (
$mask <= 32
&& $mask >= 0
)
);
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use function assert;
use function count;
use function explode;
use function inet_pton;
use function intval;
use function ip2long;
use function pack;
use function sprintf;
use function str_contains;
use function str_pad;
use function str_repeat;
use function substr_compare;
use function unpack;
/** @internal */
final class IPRange
{
/**
* Disable instantiation
*/
private function __construct()
{
}
/** @psalm-pure */
public static function matches(string $ip, string $cidr): bool
{
if (str_contains($ip, ':')) {
return self::matchesIPv6($ip, $cidr);
}
return self::matchesIPv4($ip, $cidr);
}
/** @psalm-pure */
public static function matchesIPv4(string $ip, string $cidr): bool
{
$mask = 32;
$subnet = $cidr;
if (str_contains($cidr, '/')) {
$parts = explode('/', $cidr, 2);
assert(count($parts) >= 2);
[$subnet, $mask] = $parts;
$mask = (int) $mask;
}
if ($mask < 0 || $mask > 32) {
return false;
}
$ip = ip2long($ip);
$subnet = ip2long($subnet);
if (false === $ip || false === $subnet) {
// Invalid data
return false;
}
return 0 === substr_compare(
sprintf("%032b", $ip),
sprintf("%032b", $subnet),
0,
$mask
);
}
/** @psalm-pure */
public static function matchesIPv6(string $ip, string $cidr): bool
{
$mask = 128;
$subnet = $cidr;
if (str_contains($cidr, '/')) {
$parts = explode('/', $cidr, 2);
assert(count($parts) >= 2);
[$subnet, $mask] = $parts;
$mask = (int) $mask;
}
if ($mask < 0 || $mask > 128) {
return false;
}
$ip = inet_pton($ip);
$subnet = inet_pton($subnet);
if (false === $ip || false === $subnet) {
// Invalid data
return false;
}
// mask 0: if it's a valid IP, it's valid
if ($mask === 0) {
return (bool) unpack('n*', $ip);
}
// @see http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet, MW answer
$binMask = str_repeat("f", intval($mask / 4));
switch ($mask % 4) {
case 0:
break;
case 1:
$binMask .= "8";
break;
case 2:
$binMask .= "c";
break;
case 3:
$binMask .= "e";
break;
}
$binMask = str_pad($binMask, 32, '0');
$binMask = pack("H*", $binMask);
return ($ip & $binMask) === $subnet;
}
}

View File

@ -0,0 +1,367 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use GdImage;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use Stringable;
use Throwable;
use function array_key_exists;
use function fclose;
use function feof;
use function fopen;
use function fread;
use function fseek;
use function fstat;
use function ftell;
use function fwrite;
use function get_resource_type;
use function in_array;
use function is_int;
use function is_resource;
use function is_string;
use function sprintf;
use function stream_get_contents;
use function stream_get_meta_data;
use function strstr;
use const SEEK_SET;
/**
* Implementation of PSR HTTP streams
*/
class Stream implements StreamInterface, Stringable
{
/**
* A list of allowed stream resource types that are allowed to instantiate a Stream
*/
private const ALLOWED_STREAM_RESOURCE_TYPES = ['gd', 'stream'];
/** @var resource|null */
protected $resource;
/** @var string|object|resource|null */
protected $stream;
/**
* @param string|object|resource $stream
* @param string $mode Mode with which to open stream
* @throws Exception\InvalidArgumentException
*/
public function __construct($stream, string $mode = 'r')
{
$this->setStream($stream, $mode);
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
if (! $this->isReadable()) {
return '';
}
try {
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
} catch (RuntimeException) {
return '';
}
}
/**
* {@inheritdoc}
*/
public function close(): void
{
if (! $this->resource) {
return;
}
$resource = $this->detach();
fclose($resource);
}
/**
* {@inheritdoc}
*/
public function detach()
{
$resource = $this->resource;
$this->resource = null;
return $resource;
}
/**
* Attach a new stream/resource to the instance.
*
* @param string|object|resource $resource
* @throws Exception\InvalidArgumentException For stream identifier that cannot be cast to a resource.
* @throws Exception\InvalidArgumentException For non-resource stream.
*/
public function attach($resource, string $mode = 'r'): void
{
$this->setStream($resource, $mode);
}
/**
* {@inheritdoc}
*/
public function getSize(): ?int
{
if (null === $this->resource) {
return null;
}
$stats = fstat($this->resource);
if ($stats !== false) {
return $stats['size'];
}
return null;
}
/**
* {@inheritdoc}
*/
public function tell(): int
{
if (! $this->resource) {
throw Exception\UntellableStreamException::dueToMissingResource();
}
$result = ftell($this->resource);
if (! is_int($result)) {
throw Exception\UntellableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function eof(): bool
{
if (! $this->resource) {
return true;
}
return feof($this->resource);
}
/**
* {@inheritdoc}
*/
public function isSeekable(): bool
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
return $meta['seekable'];
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET): void
{
if (! $this->resource) {
throw Exception\UnseekableStreamException::dueToMissingResource();
}
if (! $this->isSeekable()) {
throw Exception\UnseekableStreamException::dueToConfiguration();
}
$result = fseek($this->resource, $offset, $whence);
if (0 !== $result) {
throw Exception\UnseekableStreamException::dueToPhpError();
}
}
/**
* {@inheritdoc}
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* {@inheritdoc}
*/
public function isWritable(): bool
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return strstr($mode, 'x')
|| strstr($mode, 'w')
|| strstr($mode, 'c')
|| strstr($mode, 'a')
|| strstr($mode, '+');
}
/**
* {@inheritdoc}
*/
public function write($string): int
{
if (! $this->resource) {
throw Exception\UnwritableStreamException::dueToMissingResource();
}
if (! $this->isWritable()) {
throw Exception\UnwritableStreamException::dueToConfiguration();
}
$result = fwrite($this->resource, $string);
if (false === $result) {
throw Exception\UnwritableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function isReadable(): bool
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return strstr($mode, 'r') || strstr($mode, '+');
}
/**
* {@inheritdoc}
*/
public function read($length): string
{
if (! $this->resource) {
throw Exception\UnreadableStreamException::dueToMissingResource();
}
if (! $this->isReadable()) {
throw Exception\UnreadableStreamException::dueToConfiguration();
}
$result = fread($this->resource, $length);
if (false === $result) {
throw Exception\UnreadableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getContents(): string
{
if (! $this->isReadable()) {
throw Exception\UnreadableStreamException::dueToConfiguration();
}
$result = stream_get_contents($this->resource);
if (false === $result) {
throw Exception\UnreadableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
if (null === $key) {
return stream_get_meta_data($this->resource);
}
$metadata = stream_get_meta_data($this->resource);
if (! array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
/**
* Set the internal stream resource.
*
* @param string|object|resource $stream String stream target or stream resource.
* @param string $mode Resource mode for stream target.
* @throws Exception\InvalidArgumentException For invalid streams or resources.
*/
private function setStream($stream, string $mode = 'r'): void
{
$resource = $stream;
if (is_string($stream)) {
try {
$resource = fopen($stream, $mode);
} catch (Throwable $error) {
throw new Exception\RuntimeException(
sprintf('Invalid stream reference provided: %s', $error->getMessage()),
0,
$error
);
}
}
if (! $this->isValidStreamResourceType($resource)) {
throw new Exception\InvalidArgumentException(
'Invalid stream provided; must be a string stream identifier or stream resource'
);
}
if ($stream !== $resource) {
$this->stream = $stream;
}
$this->resource = $resource;
}
/**
* Determine if a resource is one of the resource types allowed to instantiate a Stream
*
* @param mixed $resource Stream resource.
* @psalm-assert-if-true resource $resource
*/
private function isValidStreamResourceType(mixed $resource): bool
{
if (is_resource($resource)) {
return in_array(get_resource_type($resource), self::ALLOWED_STREAM_RESOURCE_TYPES, true);
}
if ($resource instanceof GdImage) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use function fopen;
use function fwrite;
use function rewind;
class StreamFactory implements StreamFactoryInterface
{
/**
* {@inheritDoc}
*/
public function createStream(string $content = ''): StreamInterface
{
$resource = fopen('php://temp', 'r+');
fwrite($resource, $content);
rewind($resource);
return $this->createStreamFromResource($resource);
}
/**
* {@inheritDoc}
*/
public function createStreamFromFile(string $file, string $mode = 'r'): StreamInterface
{
return new Stream($file, $mode);
}
/**
* {@inheritDoc}
*/
public function createStreamFromResource($resource): StreamInterface
{
return new Stream($resource);
}
}

View File

@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use function dirname;
use function fclose;
use function file_exists;
use function fopen;
use function fwrite;
use function is_dir;
use function is_resource;
use function is_string;
use function is_writable;
use function move_uploaded_file;
use function str_starts_with;
use function unlink;
use const PHP_SAPI;
use const UPLOAD_ERR_CANT_WRITE;
use const UPLOAD_ERR_EXTENSION;
use const UPLOAD_ERR_FORM_SIZE;
use const UPLOAD_ERR_INI_SIZE;
use const UPLOAD_ERR_NO_FILE;
use const UPLOAD_ERR_NO_TMP_DIR;
use const UPLOAD_ERR_OK;
use const UPLOAD_ERR_PARTIAL;
class UploadedFile implements UploadedFileInterface
{
public const ERROR_MESSAGES = [
UPLOAD_ERR_OK => 'There is no error, the file uploaded with success',
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was '
. 'specified in the HTML form',
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
];
private int $error;
private ?string $file = null;
private bool $moved = false;
/** @var null|StreamInterface */
private $stream;
/**
* @param string|resource|StreamInterface $streamOrFile
* @throws Exception\InvalidArgumentException
*/
public function __construct(
$streamOrFile,
private int $size,
int $errorStatus,
private ?string $clientFilename = null,
private ?string $clientMediaType = null
) {
if ($errorStatus === UPLOAD_ERR_OK) {
if (is_string($streamOrFile)) {
$this->file = $streamOrFile;
}
if (is_resource($streamOrFile)) {
$this->stream = new Stream($streamOrFile);
}
if (! $this->file && ! $this->stream) {
if (! $streamOrFile instanceof StreamInterface) {
throw new Exception\InvalidArgumentException('Invalid stream or file provided for UploadedFile');
}
$this->stream = $streamOrFile;
}
}
if (0 > $errorStatus || 8 < $errorStatus) {
throw new Exception\InvalidArgumentException(
'Invalid error status for UploadedFile; must be an UPLOAD_ERR_* constant'
);
}
$this->error = $errorStatus;
}
/**
* {@inheritdoc}
*
* @throws Exception\UploadedFileAlreadyMovedException If the upload was not successful.
*/
public function getStream(): StreamInterface
{
if ($this->error !== UPLOAD_ERR_OK) {
throw Exception\UploadedFileErrorException::dueToStreamUploadError(
self::ERROR_MESSAGES[$this->error]
);
}
if ($this->moved) {
throw new Exception\UploadedFileAlreadyMovedException();
}
if ($this->stream instanceof StreamInterface) {
return $this->stream;
}
$this->stream = new Stream($this->file);
return $this->stream;
}
/**
* {@inheritdoc}
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
*
* @param string $targetPath Path to which to move the uploaded file.
* @throws Exception\UploadedFileErrorException If the upload was not successful.
* @throws Exception\InvalidArgumentException If the $path specified is invalid.
* @throws Exception\UploadedFileErrorException On any error during the
* move operation, or on the second or subsequent call to the method.
*/
public function moveTo($targetPath): void
{
if ($this->moved) {
throw new Exception\UploadedFileAlreadyMovedException('Cannot move file; already moved!');
}
if ($this->error !== UPLOAD_ERR_OK) {
throw Exception\UploadedFileErrorException::dueToStreamUploadError(
self::ERROR_MESSAGES[$this->error]
);
}
if (! is_string($targetPath) || empty($targetPath)) {
throw new Exception\InvalidArgumentException(
'Invalid path provided for move operation; must be a non-empty string'
);
}
$targetDirectory = dirname($targetPath);
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
throw Exception\UploadedFileErrorException::dueToUnwritableTarget($targetDirectory);
}
$sapi = PHP_SAPI;
switch (true) {
case empty($sapi) || str_starts_with($sapi, 'cli') || str_starts_with($sapi, 'phpdbg') || ! $this->file:
// Non-SAPI environment, or no filename present
$this->writeFile($targetPath);
if ($this->stream instanceof StreamInterface) {
$this->stream->close();
}
if (is_string($this->file) && file_exists($this->file)) {
unlink($this->file);
}
break;
default:
// SAPI environment, with file present
if (false === move_uploaded_file($this->file, $targetPath)) {
throw Exception\UploadedFileErrorException::forUnmovableFile();
}
break;
}
$this->moved = true;
}
/**
* {@inheritdoc}
*
* @return int|null The file size in bytes or null if unknown.
*/
public function getSize(): ?int
{
return $this->size;
}
/**
* {@inheritdoc}
*
* @see http://php.net/manual/en/features.file-upload.errors.php
*
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
public function getError(): int
{
return $this->error;
}
/**
* {@inheritdoc}
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
public function getClientFilename(): ?string
{
return $this->clientFilename;
}
/**
* {@inheritdoc}
*/
public function getClientMediaType(): ?string
{
return $this->clientMediaType;
}
/**
* Write internal stream to given path
*/
private function writeFile(string $path): void
{
$handle = fopen($path, 'wb+');
if (false === $handle) {
throw Exception\UploadedFileErrorException::dueToUnwritablePath();
}
$stream = $this->getStream();
$stream->rewind();
while (! $stream->eof()) {
fwrite($handle, $stream->read(4096));
}
fclose($handle);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
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 {
if ($size === null) {
$size = $stream->getSize();
}
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
}
}

View File

@ -0,0 +1,690 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\UriInterface;
use SensitiveParameter;
use Stringable;
use function array_keys;
use function explode;
use function gettype;
use function implode;
use function is_float;
use function is_numeric;
use function is_object;
use function is_string;
use function ltrim;
use function parse_url;
use function preg_match;
use function preg_replace;
use function preg_replace_callback;
use function rawurlencode;
use function sprintf;
use function str_contains;
use function str_split;
use function str_starts_with;
use function strtolower;
use function substr;
/**
* Implementation of Psr\Http\UriInterface.
*
* Provides a value object representing a URI for HTTP requests.
*
* Instances of this class are considered immutable; all methods that
* might change state are implemented such that they retain the internal
* state of the current instance and return a new instance that contains the
* changed state.
*
* @psalm-immutable
*/
class Uri implements UriInterface, Stringable
{
/**
* Sub-delimiters used in user info, query strings and fragments.
*
* @const string
*/
public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
/**
* Unreserved characters used in user info, paths, query strings, and fragments.
*
* @const string
*/
public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
/** @var int[] Array indexed by valid scheme names to their corresponding ports. */
protected $allowedSchemes = [
'http' => 80,
'https' => 443,
];
private string $scheme = '';
private string $userInfo = '';
private string $host = '';
private ?int $port = null;
private string $path = '';
private string $query = '';
private string $fragment = '';
/**
* generated uri string cache
*/
private ?string $uriString = null;
public function __construct(string $uri = '')
{
if ('' === $uri) {
return;
}
$this->parseUri($uri);
}
/**
* Operations to perform on clone.
*
* Since cloning usually is for purposes of mutation, we reset the
* $uriString property so it will be re-calculated.
*/
public function __clone()
{
$this->uriString = null;
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
if (null !== $this->uriString) {
return $this->uriString;
}
/** @psalm-suppress ImpureMethodCall, InaccessibleProperty */
$this->uriString = static::createUriString(
$this->scheme,
$this->getAuthority(),
$this->path, // Absolute URIs should use a "/" for an empty path
$this->query,
$this->fragment
);
return $this->uriString;
}
/**
* {@inheritdoc}
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* {@inheritdoc}
*/
public function getAuthority(): string
{
if ('' === $this->host) {
return '';
}
$authority = $this->host;
if ('' !== $this->userInfo) {
$authority = $this->userInfo . '@' . $authority;
}
if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
$authority .= ':' . $this->port;
}
return $authority;
}
/**
* Retrieve the user-info part of the URI.
*
* This value is percent-encoded, per RFC 3986 Section 3.2.1.
*
* {@inheritdoc}
*/
public function getUserInfo(): string
{
return $this->userInfo;
}
/**
* {@inheritdoc}
*/
public function getHost(): string
{
return $this->host;
}
/**
* {@inheritdoc}
*/
public function getPort(): ?int
{
return $this->isNonStandardPort($this->scheme, $this->host, $this->port)
? $this->port
: null;
}
/**
* {@inheritdoc}
*/
public function getPath(): string
{
if ('' === $this->path) {
// No path
return $this->path;
}
if ($this->path[0] !== '/') {
// Relative path
return $this->path;
}
// Ensure only one leading slash, to prevent XSS attempts.
return '/' . ltrim($this->path, '/');
}
/**
* {@inheritdoc}
*/
public function getQuery(): string
{
return $this->query;
}
/**
* {@inheritdoc}
*/
public function getFragment(): string
{
return $this->fragment;
}
/**
* {@inheritdoc}
*/
public function withScheme($scheme): UriInterface
{
if (! is_string($scheme)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
is_object($scheme) ? $scheme::class : gettype($scheme)
));
}
$scheme = $this->filterScheme($scheme);
if ($scheme === $this->scheme) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->scheme = $scheme;
return $new;
}
// The following rule is buggy for parameters attributes
// phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter
/**
* Create and return a new instance containing the provided user credentials.
*
* The value will be percent-encoded in the new instance, but with measures
* taken to prevent double-encoding.
*
* {@inheritdoc}
*/
public function withUserInfo(
$user,
#[SensitiveParameter]
$password = null
): UriInterface {
if (! is_string($user)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string user argument; received %s',
__METHOD__,
is_object($user) ? $user::class : gettype($user)
));
}
if (null !== $password && ! is_string($password)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string or null password argument; received %s',
__METHOD__,
is_object($password) ? $password::class : gettype($password)
));
}
$info = $this->filterUserInfoPart($user);
if (null !== $password) {
$info .= ':' . $this->filterUserInfoPart($password);
}
if ($info === $this->userInfo) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->userInfo = $info;
return $new;
}
// phpcs:enable SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter
/**
* {@inheritdoc}
*/
public function withHost($host): UriInterface
{
if (! is_string($host)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
is_object($host) ? $host::class : gettype($host)
));
}
if ($host === $this->host) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->host = strtolower($host);
return $new;
}
/**
* {@inheritdoc}
*/
public function withPort($port): UriInterface
{
if ($port !== null) {
if (! is_numeric($port) || is_float($port)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid port "%s" specified; must be an integer, an integer string, or null',
is_object($port) ? $port::class : gettype($port)
));
}
$port = (int) $port;
}
if ($port === $this->port) {
// Do nothing if no change was made.
return $this;
}
if ($port !== null && ($port < 1 || $port > 65535)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid port "%d" specified; must be a valid TCP/UDP port',
$port
));
}
$new = clone $this;
$new->port = $port;
return $new;
}
/**
* {@inheritdoc}
*/
public function withPath($path): UriInterface
{
if (! is_string($path)) {
throw new Exception\InvalidArgumentException(
'Invalid path provided; must be a string'
);
}
if (str_contains($path, '?')) {
throw new Exception\InvalidArgumentException(
'Invalid path provided; must not contain a query string'
);
}
if (str_contains($path, '#')) {
throw new Exception\InvalidArgumentException(
'Invalid path provided; must not contain a URI fragment'
);
}
$path = $this->filterPath($path);
if ($path === $this->path) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* {@inheritdoc}
*/
public function withQuery($query): UriInterface
{
if (! is_string($query)) {
throw new Exception\InvalidArgumentException(
'Query string must be a string'
);
}
if (str_contains($query, '#')) {
throw new Exception\InvalidArgumentException(
'Query string must not include a URI fragment'
);
}
$query = $this->filterQuery($query);
if ($query === $this->query) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->query = $query;
return $new;
}
/**
* {@inheritdoc}
*/
public function withFragment($fragment): UriInterface
{
if (! is_string($fragment)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
is_object($fragment) ? $fragment::class : gettype($fragment)
));
}
$fragment = $this->filterFragment($fragment);
if ($fragment === $this->fragment) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
/**
* Parse a URI into its parts, and set the properties
*
* @psalm-suppress InaccessibleProperty Method is only called in {@see Uri::__construct} and thus immutability is
* still given.
*/
private function parseUri(string $uri): void
{
$parts = parse_url($uri);
if (false === $parts) {
throw new Exception\InvalidArgumentException(
'The source URI string appears to be malformed'
);
}
$this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
$this->userInfo = isset($parts['user']) ? $this->filterUserInfoPart($parts['user']) : '';
$this->host = isset($parts['host']) ? strtolower($parts['host']) : '';
$this->port = $parts['port'] ?? null;
$this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
$this->query = isset($parts['query']) ? $this->filterQuery($parts['query']) : '';
$this->fragment = isset($parts['fragment']) ? $this->filterFragment($parts['fragment']) : '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $parts['pass'];
}
}
/**
* Create a URI string from its various parts
*/
private static function createUriString(
string $scheme,
string $authority,
string $path,
string $query,
string $fragment
): string {
$uri = '';
if ('' !== $scheme) {
$uri .= sprintf('%s:', $scheme);
}
if ('' !== $authority) {
$uri .= '//' . $authority;
}
if ('' !== $path && ! str_starts_with($path, '/')) {
$path = '/' . $path;
}
$uri .= $path;
if ('' !== $query) {
$uri .= sprintf('?%s', $query);
}
if ('' !== $fragment) {
$uri .= sprintf('#%s', $fragment);
}
return $uri;
}
/**
* Is a given port non-standard for the current scheme?
*/
private function isNonStandardPort(string $scheme, string $host, ?int $port): bool
{
if ('' === $scheme) {
return '' === $host || null !== $port;
}
if ('' === $host || null === $port) {
return false;
}
return ! isset($this->allowedSchemes[$scheme]) || $port !== $this->allowedSchemes[$scheme];
}
/**
* Filters the scheme to ensure it is a valid scheme.
*
* @param string $scheme Scheme name.
* @return string Filtered scheme.
*/
private function filterScheme(string $scheme): string
{
$scheme = strtolower($scheme);
$scheme = preg_replace('#:(//)?$#', '', $scheme);
if ('' === $scheme) {
return '';
}
if (! isset($this->allowedSchemes[$scheme])) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
$scheme,
implode(', ', array_keys($this->allowedSchemes))
));
}
return $scheme;
}
/**
* Filters a part of user info in a URI to ensure it is properly encoded.
*/
private function filterUserInfoPart(string $part): string
{
$part = $this->filterInvalidUtf8($part);
/**
* @psalm-suppress ImpureFunctionCall Even tho the callback targets this immutable class,
* psalm reports an issue here.
* Note the addition of `%` to initial charset; this allows `|` portion
* to match and thus prevent double-encoding.
*/
return preg_replace_callback(
'/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$part
);
}
/**
* Filters the path of a URI to ensure it is properly encoded.
*/
private function filterPath(string $path): string
{
$path = $this->filterInvalidUtf8($path);
/**
* @psalm-suppress ImpureFunctionCall Even tho the callback targets this immutable class,
* psalm reports an issue here.
*/
return preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . ')(:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$path
);
}
/**
* Encode invalid UTF-8 characters in given string. All other characters are unchanged.
*/
private function filterInvalidUtf8(string $string): string
{
// check if given string contains only valid UTF-8 characters
if (preg_match('//u', $string)) {
return $string;
}
$letters = str_split($string);
foreach ($letters as $i => $letter) {
if (! preg_match('//u', $letter)) {
$letters[$i] = $this->urlEncodeChar([$letter]);
}
}
return implode('', $letters);
}
/**
* Filter a query string to ensure it is propertly encoded.
*
* Ensures that the values in the query string are properly urlencoded.
*/
private function filterQuery(string $query): string
{
if ('' !== $query && str_starts_with($query, '?')) {
$query = substr($query, 1);
}
$parts = explode('&', $query);
foreach ($parts as $index => $part) {
[$key, $value] = $this->splitQueryValue($part);
if ($value === null) {
$parts[$index] = $this->filterQueryOrFragment($key);
continue;
}
$parts[$index] = sprintf(
'%s=%s',
$this->filterQueryOrFragment($key),
$this->filterQueryOrFragment($value)
);
}
return implode('&', $parts);
}
/**
* Split a query value into a key/value tuple.
*
* @return array A value with exactly two elements, key and value
*/
private function splitQueryValue(string $value): array
{
$data = explode('=', $value, 2);
if (! isset($data[1])) {
$data[] = null;
}
return $data;
}
/**
* Filter a fragment value to ensure it is properly encoded.
*/
private function filterFragment(string $fragment): string
{
if ('' !== $fragment && str_starts_with($fragment, '#')) {
$fragment = '%23' . substr($fragment, 1);
}
return $this->filterQueryOrFragment($fragment);
}
/**
* Filter a query string key or value, or a fragment.
*/
private function filterQueryOrFragment(string $value): string
{
$value = $this->filterInvalidUtf8($value);
/**
* @psalm-suppress ImpureFunctionCall Even tho the callback targets this immutable class,
* psalm reports an issue here.
*/
return preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$value
);
}
/**
* URL encode a character returned by a regex.
*/
private function urlEncodeChar(array $matches): string
{
return rawurlencode($matches[0]);
}
}

View File

@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use function array_change_key_case;
use function array_key_exists;
use function assert;
use function count;
use function explode;
use function gettype;
use function implode;
use function is_bool;
use function is_scalar;
use function is_string;
use function ltrim;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_contains;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;
use const CASE_LOWER;
class UriFactory implements UriFactoryInterface
{
/**
* {@inheritDoc}
*/
public function createUri(string $uri = ''): UriInterface
{
return new Uri($uri);
}
/**
* Create a Uri instance based on the headers and $_SERVER data.
*
* @param array<non-empty-string, list<string>|int|float|string> $server SAPI parameters
* @param array<string, string|list<string>> $headers
*/
public static function createFromSapi(array $server, array $headers): Uri
{
$uri = new Uri('');
$isHttps = false;
if (array_key_exists('HTTPS', $server)) {
$isHttps = self::marshalHttpsValue($server['HTTPS']);
} elseif (array_key_exists('https', $server)) {
$isHttps = self::marshalHttpsValue($server['https']);
}
$uri = $uri->withScheme($isHttps ? 'https' : 'http');
[$host, $port] = self::marshalHostAndPort($server, $headers);
if (! empty($host)) {
$uri = $uri->withHost($host);
if (! empty($port)) {
$uri = $uri->withPort($port);
}
}
$path = self::marshalRequestPath($server);
// Strip query string
$path = explode('?', $path, 2)[0];
$query = '';
if (isset($server['QUERY_STRING']) && is_scalar($server['QUERY_STRING'])) {
$query = ltrim((string) $server['QUERY_STRING'], '?');
}
$fragment = '';
if (str_contains($path, '#')) {
$parts = explode('#', $path, 2);
assert(count($parts) >= 2);
[$path, $fragment] = $parts;
}
return $uri
->withPath($path)
->withFragment($fragment)
->withQuery($query);
}
/**
* Retrieve a header value from an array of headers using a case-insensitive lookup.
*
* @template T
* @param array<string, string|list<string>> $headers Key/value header pairs
* @param T $default Default value to return if header not found
* @return string|T
*/
private static function getHeaderFromArray(string $name, array $headers, $default = null)
{
$header = strtolower($name);
$headers = array_change_key_case($headers, CASE_LOWER);
if (! array_key_exists($header, $headers)) {
return $default;
}
if (is_string($headers[$header])) {
return $headers[$header];
}
return implode(', ', $headers[$header]);
}
/**
* Marshal the host and port from the PHP environment.
*
* @param array<string, string|list<string>> $headers
* @return array{string, int|null} Array of two items, host and port,
* in that order (can be passed to a list() operation).
*/
private static function marshalHostAndPort(array $server, array $headers): array
{
/** @var array{string, null} $defaults */
static $defaults = ['', null];
$host = self::getHeaderFromArray('host', $headers, false);
if ($host !== false) {
// Ignore obviously malformed host headers:
// - Whitespace is invalid within a hostname and break the URI representation within HTTP.
// non-printable characters other than SPACE and TAB are already rejected by HeaderSecurity.
// - A comma indicates that multiple host headers have been sent which is not legal
// and might be used in an attack where a load balancer sees a different host header
// than Diactoros.
if (! preg_match('/[\\t ,]/', $host)) {
return self::marshalHostAndPortFromHeader($host);
}
}
if (! isset($server['SERVER_NAME'])) {
return $defaults;
}
$host = (string) $server['SERVER_NAME'];
$port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null;
if (
! isset($server['SERVER_ADDR'])
|| ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host)
) {
return [$host, $port];
}
// Misinterpreted IPv6-Address
// Reported for Safari on Windows
return self::marshalIpv6HostAndPort($server, $port);
}
/**
* @return array{string, int|null} Array of two items, host and port,
* in that order (can be passed to a list() operation).
*/
private static function marshalIpv6HostAndPort(array $server, ?int $port): array
{
$host = '[' . (string) $server['SERVER_ADDR'] . ']';
$port = $port ?: 80;
$portSeparatorPos = strrpos($host, ':');
if (false === $portSeparatorPos) {
return [$host, $port];
}
if ($port . ']' === substr($host, $portSeparatorPos + 1)) {
// The last digit of the IPv6-Address has been taken as port
// Unset the port so the default port can be used
$port = null;
}
return [$host, $port];
}
/**
* Detect the path for the request
*
* Looks at a variety of criteria in order to attempt to autodetect the base
* request path, including:
*
* - IIS7 UrlRewrite environment
* - REQUEST_URI
* - ORIG_PATH_INFO
*/
private static function marshalRequestPath(array $server): string
{
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
/** @var string|array<string>|null $iisUrlRewritten */
$iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null;
/** @var string|array<string> $unencodedUrl */
$unencodedUrl = $server['UNENCODED_URL'] ?? '';
if ('1' === $iisUrlRewritten && is_string($unencodedUrl) && '' !== $unencodedUrl) {
return $unencodedUrl;
}
/** @var string|array<string>|null $requestUri */
$requestUri = $server['REQUEST_URI'] ?? null;
if (is_string($requestUri)) {
return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
}
$origPathInfo = $server['ORIG_PATH_INFO'] ?? '';
if (! is_string($origPathInfo) || '' === $origPathInfo) {
return '/';
}
return $origPathInfo;
}
private static function marshalHttpsValue(mixed $https): bool
{
if (is_bool($https)) {
return $https;
}
if (! is_string($https)) {
throw new Exception\InvalidArgumentException(sprintf(
'SAPI HTTPS value MUST be a string or boolean; received %s',
gettype($https)
));
}
return 'on' === strtolower($https);
}
/**
* @internal
*
* @return array{string, int|null} Array of two items, host and port, in that order (can be
* passed to a list() operation).
* @psalm-mutation-free
*/
public static function marshalHostAndPortFromHeader(string $host): array
{
$port = null;
// works for regname, IPv4 & IPv6
if (preg_match('|\:(\d+)$|', $host, $matches)) {
$host = substr($host, 0, -1 * (strlen($matches[1]) + 1));
$port = (int) $matches[1];
}
return [$host, $port];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use Laminas\Diactoros\UploadedFile;
use function func_get_args;
use function Laminas\Diactoros\createUploadedFile as laminas_createUploadedFile;
/**
* @deprecated Use Laminas\Diactoros\createUploadedFile instead
*/
function createUploadedFile(array $spec): UploadedFile
{
return laminas_createUploadedFile(...func_get_args());
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function sprintf;
/**
* Create an uploaded file instance from an array of values.
*
* @param array $spec A single $_FILES entry.
* @throws Exception\InvalidArgumentException If one or more of the tmp_name,
* size, or error keys are missing from $spec.
*/
function createUploadedFile(array $spec): UploadedFile
{
if (
! isset($spec['tmp_name'])
|| ! isset($spec['size'])
|| ! isset($spec['error'])
) {
throw new Exception\InvalidArgumentException(sprintf(
'$spec provided to %s MUST contain each of the keys "tmp_name",'
. ' "size", and "error"; one or more were missing',
__FUNCTION__
));
}
return new UploadedFile(
$spec['tmp_name'],
(int) $spec['size'],
$spec['error'],
$spec['name'] ?? null,
$spec['type'] ?? null
);
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use function func_get_args;
use function Laminas\Diactoros\marshalHeadersFromSapi as laminas_marshalHeadersFromSapi;
/**
* @deprecated Use Laminas\Diactoros\marshalHeadersFromSapi instead
*/
function marshalHeadersFromSapi(array $server): array
{
return laminas_marshalHeadersFromSapi(...func_get_args());
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function array_key_exists;
use function is_string;
use function str_starts_with;
use function strtolower;
use function strtr;
use function substr;
/**
* @param array $server Values obtained from the SAPI (generally `$_SERVER`).
* @return array Header/value pairs
*/
function marshalHeadersFromSapi(array $server): array
{
$contentHeaderLookup = isset($server['LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP'])
? static function (string $key): bool {
static $contentHeaders = [
'CONTENT_TYPE' => true,
'CONTENT_LENGTH' => true,
'CONTENT_MD5' => true,
];
return isset($contentHeaders[$key]);
}
: static fn(string $key): bool => str_starts_with($key, 'CONTENT_');
$headers = [];
foreach ($server as $key => $value) {
if (! is_string($key)) {
continue;
}
if ($value === '') {
continue;
}
// Apache prefixes environment variables with REDIRECT_
// if they are added by rewrite rules
if (str_starts_with($key, 'REDIRECT_')) {
$key = substr($key, 9);
// We will not overwrite existing variables with the
// prefixed versions, though
if (array_key_exists($key, $server)) {
continue;
}
}
if (str_starts_with($key, 'HTTP_')) {
$name = strtr(strtolower(substr($key, 5)), '_', '-');
$headers[$name] = $value;
continue;
}
if ($contentHeaderLookup($key)) {
$name = strtr(strtolower($key), '_', '-');
$headers[$name] = $value;
continue;
}
}
return $headers;
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use function func_get_args;
use function Laminas\Diactoros\marshalMethodFromSapi as laminas_marshalMethodFromSapi;
/**
* @deprecated Use Laminas\Diactoros\marshalMethodFromSapi instead
*/
function marshalMethodFromSapi(array $server): string
{
return laminas_marshalMethodFromSapi(...func_get_args());
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
/**
* Retrieve the request method from the SAPI parameters.
*/
function marshalMethodFromSapi(array $server): string
{
return $server['REQUEST_METHOD'] ?? 'GET';
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use function func_get_args;
use function Laminas\Diactoros\marshalProtocolVersionFromSapi as laminas_marshalProtocolVersionFromSapi;
/**
* @deprecated Use Laminas\Diactoros\marshalProtocolVersionFromSapi instead
*/
function marshalProtocolVersionFromSapi(array $server): string
{
return laminas_marshalProtocolVersionFromSapi(...func_get_args());
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function preg_match;
/**
* Return HTTP protocol version (X.Y) as discovered within a `$_SERVER` array.
*
* @throws Exception\UnrecognizedProtocolVersionException If the
* $server['SERVER_PROTOCOL'] value is malformed.
*/
function marshalProtocolVersionFromSapi(array $server): string
{
if (! isset($server['SERVER_PROTOCOL'])) {
return '1.1';
}
if (! preg_match('#^(HTTP/)?(?P<version>[1-9]\d*(?:\.\d)?)$#', $server['SERVER_PROTOCOL'], $matches)) {
throw Exception\UnrecognizedProtocolVersionException::forVersion(
(string) $server['SERVER_PROTOCOL']
);
}
return $matches['version'];
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use Laminas\Diactoros\Uri;
use function func_get_args;
use function Laminas\Diactoros\marshalUriFromSapi as laminas_marshalUriFromSapi;
/**
* @deprecated Use Laminas\Diactoros\marshalUriFromSapi instead
*/
function marshalUriFromSapi(array $server, array $headers): Uri
{
return laminas_marshalUriFromSapi(...func_get_args());
}

View File

@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function array_change_key_case;
use function array_key_exists;
use function assert;
use function count;
use function explode;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_string;
use function ltrim;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_contains;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;
use const CASE_LOWER;
/**
* Marshal a Uri instance based on the values presnt in the $_SERVER array and headers.
*
* @deprecated This function is deprecated as of 2.11.1, and will be removed in
* 3.0.0. As of 2.11.1, it is no longer used internally.
*
* @param array $server SAPI parameters
* @param array $headers HTTP request headers
*/
function marshalUriFromSapi(array $server, array $headers): Uri
{
/**
* Retrieve a header value from an array of headers using a case-insensitive lookup.
*
* @param array $headers Key/value header pairs
* @param mixed $default Default value to return if header not found
* @return mixed
*/
$getHeaderFromArray = static function (string $name, array $headers, $default = null) {
$header = strtolower($name);
$headers = array_change_key_case($headers, CASE_LOWER);
if (array_key_exists($header, $headers)) {
return is_array($headers[$header]) ? implode(', ', $headers[$header]) : $headers[$header];
}
return $default;
};
/**
* Marshal the host and port from HTTP headers and/or the PHP environment.
*
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
$marshalHostAndPort = static function (array $headers, array $server) use ($getHeaderFromArray): array {
/**
* @param string|array $host
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
$marshalHostAndPortFromHeader = static function ($host) {
if (is_array($host)) {
$host = implode(', ', $host);
}
$port = null;
// works for regname, IPv4 & IPv6
if (preg_match('|\:(\d+)$|', $host, $matches)) {
$host = substr($host, 0, -1 * (strlen($matches[1]) + 1));
$port = (int) $matches[1];
}
return [$host, $port];
};
/**
* @return array Array of two items, host and port, in that order (can be
* passed to a list() operation).
*/
$marshalIpv6HostAndPort = static function (array $server, ?int $port): array {
$host = '[' . $server['SERVER_ADDR'] . ']';
$port = $port ?: 80;
if ($port . ']' === substr($host, strrpos($host, ':') + 1)) {
// The last digit of the IPv6-Address has been taken as port
// Unset the port so the default port can be used
$port = null;
}
return [$host, $port];
};
static $defaults = ['', null];
$forwardedHost = $getHeaderFromArray('x-forwarded-host', $headers, false);
if ($forwardedHost !== false) {
return $marshalHostAndPortFromHeader($forwardedHost);
}
$host = $getHeaderFromArray('host', $headers, false);
if ($host !== false) {
return $marshalHostAndPortFromHeader($host);
}
if (! isset($server['SERVER_NAME'])) {
return $defaults;
}
$host = $server['SERVER_NAME'];
$port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null;
if (
! isset($server['SERVER_ADDR'])
|| ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host)
) {
return [$host, $port];
}
// Misinterpreted IPv6-Address
// Reported for Safari on Windows
return $marshalIpv6HostAndPort($server, $port);
};
/**
* Detect the path for the request
*
* Looks at a variety of criteria in order to attempt to autodetect the base
* request path, including:
*
* - IIS7 UrlRewrite environment
* - REQUEST_URI
* - ORIG_PATH_INFO
*
* From Laminas\Http\PhpEnvironment\Request class
*/
$marshalRequestPath = static function (array $server): string {
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
$iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null;
$unencodedUrl = $server['UNENCODED_URL'] ?? '';
if ('1' === $iisUrlRewritten && ! empty($unencodedUrl)) {
return $unencodedUrl;
}
$requestUri = $server['REQUEST_URI'] ?? null;
if ($requestUri !== null) {
return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
}
$origPathInfo = $server['ORIG_PATH_INFO'] ?? null;
if (empty($origPathInfo)) {
return '/';
}
return $origPathInfo;
};
$uri = new Uri('');
// URI scheme
$scheme = 'http';
$marshalHttpsValue = static function ($https): bool {
if (is_bool($https)) {
return $https;
}
if (! is_string($https)) {
throw new Exception\InvalidArgumentException(sprintf(
'SAPI HTTPS value MUST be a string or boolean; received %s',
gettype($https)
));
}
return 'on' === strtolower($https);
};
if (array_key_exists('HTTPS', $server)) {
$https = $marshalHttpsValue($server['HTTPS']);
} elseif (array_key_exists('https', $server)) {
$https = $marshalHttpsValue($server['https']);
} else {
$https = false;
}
if (
$https
|| strtolower($getHeaderFromArray('x-forwarded-proto', $headers, '')) === 'https'
) {
$scheme = 'https';
}
$uri = $uri->withScheme($scheme);
// Set the host
[$host, $port] = $marshalHostAndPort($headers, $server);
if (! empty($host)) {
$uri = $uri->withHost($host);
if (! empty($port)) {
$uri = $uri->withPort($port);
}
}
// URI path
$path = $marshalRequestPath($server);
// Strip query string
$path = explode('?', $path, 2)[0];
// URI query
$query = '';
if (isset($server['QUERY_STRING'])) {
$query = ltrim($server['QUERY_STRING'], '?');
}
// URI fragment
$fragment = '';
if (str_contains($path, '#')) {
$parts = explode('#', $path, 2);
assert(count($parts) >= 2);
[$path, $fragment] = $parts;
}
return $uri
->withPath($path)
->withFragment($fragment)
->withQuery($query);
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use function func_get_args;
use function Laminas\Diactoros\normalizeServer as laminas_normalizeServer;
/**
* @deprecated Use Laminas\Diactoros\normalizeServer instead
*/
function normalizeServer(array $server, ?callable $apacheRequestHeaderCallback = null): array
{
return laminas_normalizeServer(...func_get_args());
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function is_callable;
/**
* Marshal the $_SERVER array
*
* Pre-processes and returns the $_SERVER superglobal. In particularly, it
* attempts to detect the Authorization header, which is often not aggregated
* correctly under various SAPI/httpd combinations.
*
* @param null|callable $apacheRequestHeaderCallback Callback that can be used to
* retrieve Apache request headers. This defaults to
* `apache_request_headers` under the Apache mod_php.
* @return array Either $server verbatim, or with an added HTTP_AUTHORIZATION header.
*/
function normalizeServer(array $server, ?callable $apacheRequestHeaderCallback = null): array
{
if (null === $apacheRequestHeaderCallback && is_callable('apache_request_headers')) {
$apacheRequestHeaderCallback = 'apache_request_headers';
}
// If the HTTP_AUTHORIZATION value is already set, or the callback is not
// callable, we return verbatim
if (
isset($server['HTTP_AUTHORIZATION'])
|| ! is_callable($apacheRequestHeaderCallback)
) {
return $server;
}
$apacheRequestHeaders = $apacheRequestHeaderCallback();
if (isset($apacheRequestHeaders['Authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization'];
return $server;
}
if (isset($apacheRequestHeaders['authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization'];
return $server;
}
return $server;
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use function func_get_args;
use function Laminas\Diactoros\normalizeUploadedFiles as laminas_normalizeUploadedFiles;
/**
* @deprecated Use Laminas\Diactoros\normalizeUploadedFiles instead
*/
function normalizeUploadedFiles(array $files): array
{
return laminas_normalizeUploadedFiles(...func_get_args());
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\UploadedFileInterface;
use function is_array;
use function sprintf;
/**
* Normalize uploaded files
*
* Transforms each value into an UploadedFile instance, and ensures that nested
* arrays are normalized.
*
* @return UploadedFileInterface[]
* @throws Exception\InvalidArgumentException For unrecognized values.
*/
function normalizeUploadedFiles(array $files): array
{
/**
* Traverse a nested tree of uploaded file specifications.
*
* @param string[]|array[] $tmpNameTree
* @param int[]|array[] $sizeTree
* @param int[]|array[] $errorTree
* @param string[]|array[]|null $nameTree
* @param string[]|array[]|null $typeTree
* @return UploadedFile[]|array[]
*/
$recursiveNormalize = static function (
array $tmpNameTree,
array $sizeTree,
array $errorTree,
?array $nameTree = null,
?array $typeTree = null
) use (&$recursiveNormalize): array {
$normalized = [];
foreach ($tmpNameTree as $key => $value) {
if (is_array($value)) {
// Traverse
$normalized[$key] = $recursiveNormalize(
$tmpNameTree[$key],
$sizeTree[$key],
$errorTree[$key],
$nameTree[$key] ?? null,
$typeTree[$key] ?? null
);
continue;
}
$normalized[$key] = createUploadedFile([
'tmp_name' => $tmpNameTree[$key],
'size' => $sizeTree[$key],
'error' => $errorTree[$key],
'name' => $nameTree[$key] ?? null,
'type' => $typeTree[$key] ?? null,
]);
}
return $normalized;
};
/**
* Normalize an array of file specifications.
*
* Loops through all nested files (as determined by receiving an array to the
* `tmp_name` key of a `$_FILES` specification) and returns a normalized array
* of UploadedFile instances.
*
* This function normalizes a `$_FILES` array representing a nested set of
* uploaded files as produced by the php-fpm SAPI, CGI SAPI, or mod_php
* SAPI.
*
* @param array $files
* @return UploadedFile[]
*/
$normalizeUploadedFileSpecification = static function (array $files = []) use (&$recursiveNormalize): array {
if (
! isset($files['tmp_name']) || ! is_array($files['tmp_name'])
|| ! isset($files['size']) || ! is_array($files['size'])
|| ! isset($files['error']) || ! is_array($files['error'])
) {
throw new Exception\InvalidArgumentException(sprintf(
'$files provided to %s MUST contain each of the keys "tmp_name",'
. ' "size", and "error", with each represented as an array;'
. ' one or more were missing or non-array values',
__FUNCTION__
));
}
return $recursiveNormalize(
$files['tmp_name'],
$files['size'],
$files['error'],
$files['name'] ?? null,
$files['type'] ?? null
);
};
$normalized = [];
foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
continue;
}
if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) {
$normalized[$key] = $normalizeUploadedFileSpecification($value);
continue;
}
if (is_array($value) && isset($value['tmp_name'])) {
$normalized[$key] = createUploadedFile($value);
continue;
}
if (is_array($value)) {
$normalized[$key] = normalizeUploadedFiles($value);
continue;
}
throw new Exception\InvalidArgumentException('Invalid value in files specification');
}
return $normalized;
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Zend\Diactoros;
use function func_get_args;
use function Laminas\Diactoros\parseCookieHeader as laminas_parseCookieHeader;
/**
* @deprecated Use {@see \Laminas\Diactoros\parseCookieHeader} instead
*
* @param string $cookieHeader A string cookie header value.
* @return array<non-empty-string, string> key/value cookie pairs.
*/
function parseCookieHeader($cookieHeader): array
{
return laminas_parseCookieHeader(...func_get_args());
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function preg_match_all;
use function urldecode;
use const PREG_SET_ORDER;
/**
* Parse a cookie header according to RFC 6265.
*
* PHP will replace special characters in cookie names, which results in other cookies not being available due to
* overwriting. Thus, the server request should take the cookies from the request header instead.
*
* @param string $cookieHeader A string cookie header value.
* @return array<non-empty-string, string> key/value cookie pairs.
*/
function parseCookieHeader($cookieHeader): array
{
preg_match_all('(
(?:^\\n?[ \t]*|;[ ])
(?P<name>[!#$%&\'*+-.0-9A-Z^_`a-z|~]+)
=
(?P<DQUOTE>"?)
(?P<value>[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*)
(?P=DQUOTE)
(?=\\n?[ \t]*$|;[ ])
)x', $cookieHeader, $matches, PREG_SET_ORDER);
$cookies = [];
foreach ($matches as $match) {
$cookies[$match['name']] = urldecode($match['value']);
}
return $cookies;
}