From 6f95ecb1f948da1eac296a3de196d5a2338eec2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:18:22 +0100 Subject: [PATCH 01/16] all http-message packages are merged; improved code --- src/Exception/ExceptionInterface.php | 24 + .../FailedStreamOperationException.php | 19 + .../FailedUploadedFileOperationException.php | 19 + src/Exception/InvalidArgumentException.php | 24 + src/Exception/InvalidHeaderException.php | 19 + src/Exception/InvalidHeaderNameException.php | 19 + src/Exception/InvalidHeaderValueException.php | 19 + .../InvalidHeaderValueParameterException.php | 19 + src/Exception/InvalidStreamException.php | 27 + .../InvalidStreamOperationException.php | 19 + .../InvalidUploadedFileException.php | 19 + .../InvalidUploadedFileOperationException.php | 19 + .../InvalidUriComponentException.php | 19 + src/Exception/InvalidUriException.php | 19 + src/Exception/RuntimeException.php | 24 + src/Header.php | 258 +++++++++ .../AccessControlAllowCredentialsHeader.php | 40 ++ .../AccessControlAllowHeadersHeader.php | 68 +++ .../AccessControlAllowMethodsHeader.php | 72 +++ src/Header/AccessControlAllowOriginHeader.php | 107 ++++ .../AccessControlExposeHeadersHeader.php | 68 +++ src/Header/AccessControlMaxAgeHeader.php | 87 +++ src/Header/AgeHeader.php | 87 +++ src/Header/AllowHeader.php | 72 +++ src/Header/CacheControlHeader.php | 80 +++ src/Header/ClearSiteDataHeader.php | 74 +++ src/Header/ConnectionHeader.php | 71 +++ src/Header/ContentDispositionHeader.php | 84 +++ src/Header/ContentEncodingHeader.php | 78 +++ src/Header/ContentLanguageHeader.php | 77 +++ src/Header/ContentLengthHeader.php | 87 +++ src/Header/ContentLocationHeader.php | 63 +++ src/Header/ContentMD5Header.php | 70 +++ src/Header/ContentRangeHeader.php | 114 ++++ src/Header/ContentSecurityPolicyHeader.php | 101 ++++ .../ContentSecurityPolicyReportOnlyHeader.php | 27 + src/Header/ContentTypeHeader.php | 93 +++ src/Header/CookieHeader.php | 65 +++ src/Header/DateHeader.php | 57 ++ src/Header/EtagHeader.php | 66 +++ src/Header/ExpiresHeader.php | 57 ++ src/Header/KeepAliveHeader.php | 80 +++ src/Header/LastModifiedHeader.php | 57 ++ src/Header/LinkHeader.php | 86 +++ src/Header/LocationHeader.php | 63 +++ src/Header/RefreshHeader.php | 102 ++++ src/Header/RetryAfterHeader.php | 57 ++ src/Header/SetCookieHeader.php | 277 +++++++++ src/Header/SunsetHeader.php | 57 ++ src/Header/TrailerHeader.php | 61 ++ src/Header/TransferEncodingHeader.php | 78 +++ src/Header/VaryHeader.php | 68 +++ src/Header/WWWAuthenticateHeader.php | 107 ++++ src/Header/WarningHeader.php | 130 +++++ src/HeaderInterface.php | 51 ++ src/Message.php | 382 ++++++++----- src/Request.php | 268 +++++---- src/RequestFactory.php | 51 +- src/Response.php | 203 +++++-- src/Response/HtmlResponse.php | 90 +++ src/Response/JsonResponse.php | 100 ++++ src/ResponseFactory.php | 76 +-- src/ServerRequest.php | 388 +++++++++++++ src/ServerRequestFactory.php | 84 +++ src/Stream.php | 445 +++++++++++++++ src/Stream/FileStream.php | 77 +++ src/Stream/PhpInputStream.php | 45 ++ src/Stream/PhpMemoryStream.php | 39 ++ src/Stream/PhpTempStream.php | 48 ++ src/Stream/TmpfileStream.php | 57 ++ src/StreamFactory.php | 61 ++ src/UploadedFile.php | 292 ++++++++++ src/UploadedFileFactory.php | 52 ++ src/Uri.php | 534 ++++++++++++++++++ src/Uri/Component/ComponentInterface.php | 26 + src/Uri/Component/Fragment.php | 82 +++ src/Uri/Component/Host.php | 86 +++ src/Uri/Component/Password.php | 82 +++ src/Uri/Component/Path.php | 82 +++ src/Uri/Component/Port.php | 76 +++ src/Uri/Component/Query.php | 82 +++ src/Uri/Component/Scheme.php | 83 +++ src/Uri/Component/User.php | 82 +++ src/Uri/Component/UserInfo.php | 66 +++ src/UriFactory.php | 35 ++ 85 files changed, 7345 insertions(+), 434 deletions(-) create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/FailedStreamOperationException.php create mode 100644 src/Exception/FailedUploadedFileOperationException.php create mode 100644 src/Exception/InvalidArgumentException.php create mode 100644 src/Exception/InvalidHeaderException.php create mode 100644 src/Exception/InvalidHeaderNameException.php create mode 100644 src/Exception/InvalidHeaderValueException.php create mode 100644 src/Exception/InvalidHeaderValueParameterException.php create mode 100644 src/Exception/InvalidStreamException.php create mode 100644 src/Exception/InvalidStreamOperationException.php create mode 100644 src/Exception/InvalidUploadedFileException.php create mode 100644 src/Exception/InvalidUploadedFileOperationException.php create mode 100644 src/Exception/InvalidUriComponentException.php create mode 100644 src/Exception/InvalidUriException.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Header.php create mode 100644 src/Header/AccessControlAllowCredentialsHeader.php create mode 100644 src/Header/AccessControlAllowHeadersHeader.php create mode 100644 src/Header/AccessControlAllowMethodsHeader.php create mode 100644 src/Header/AccessControlAllowOriginHeader.php create mode 100644 src/Header/AccessControlExposeHeadersHeader.php create mode 100644 src/Header/AccessControlMaxAgeHeader.php create mode 100644 src/Header/AgeHeader.php create mode 100644 src/Header/AllowHeader.php create mode 100644 src/Header/CacheControlHeader.php create mode 100644 src/Header/ClearSiteDataHeader.php create mode 100644 src/Header/ConnectionHeader.php create mode 100644 src/Header/ContentDispositionHeader.php create mode 100644 src/Header/ContentEncodingHeader.php create mode 100644 src/Header/ContentLanguageHeader.php create mode 100644 src/Header/ContentLengthHeader.php create mode 100644 src/Header/ContentLocationHeader.php create mode 100644 src/Header/ContentMD5Header.php create mode 100644 src/Header/ContentRangeHeader.php create mode 100644 src/Header/ContentSecurityPolicyHeader.php create mode 100644 src/Header/ContentSecurityPolicyReportOnlyHeader.php create mode 100644 src/Header/ContentTypeHeader.php create mode 100644 src/Header/CookieHeader.php create mode 100644 src/Header/DateHeader.php create mode 100644 src/Header/EtagHeader.php create mode 100644 src/Header/ExpiresHeader.php create mode 100644 src/Header/KeepAliveHeader.php create mode 100644 src/Header/LastModifiedHeader.php create mode 100644 src/Header/LinkHeader.php create mode 100644 src/Header/LocationHeader.php create mode 100644 src/Header/RefreshHeader.php create mode 100644 src/Header/RetryAfterHeader.php create mode 100644 src/Header/SetCookieHeader.php create mode 100644 src/Header/SunsetHeader.php create mode 100644 src/Header/TrailerHeader.php create mode 100644 src/Header/TransferEncodingHeader.php create mode 100644 src/Header/VaryHeader.php create mode 100644 src/Header/WWWAuthenticateHeader.php create mode 100644 src/Header/WarningHeader.php create mode 100644 src/HeaderInterface.php create mode 100644 src/Response/HtmlResponse.php create mode 100644 src/Response/JsonResponse.php create mode 100644 src/ServerRequest.php create mode 100644 src/ServerRequestFactory.php create mode 100644 src/Stream.php create mode 100644 src/Stream/FileStream.php create mode 100644 src/Stream/PhpInputStream.php create mode 100644 src/Stream/PhpMemoryStream.php create mode 100644 src/Stream/PhpTempStream.php create mode 100644 src/Stream/TmpfileStream.php create mode 100644 src/StreamFactory.php create mode 100644 src/UploadedFile.php create mode 100644 src/UploadedFileFactory.php create mode 100644 src/Uri.php create mode 100644 src/Uri/Component/ComponentInterface.php create mode 100644 src/Uri/Component/Fragment.php create mode 100644 src/Uri/Component/Host.php create mode 100644 src/Uri/Component/Password.php create mode 100644 src/Uri/Component/Path.php create mode 100644 src/Uri/Component/Port.php create mode 100644 src/Uri/Component/Query.php create mode 100644 src/Uri/Component/Scheme.php create mode 100644 src/Uri/Component/User.php create mode 100644 src/Uri/Component/UserInfo.php create mode 100644 src/UriFactory.php diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..2a94002 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * Import classes + */ +use Throwable; + +/** + * ExceptionInterface + */ +interface ExceptionInterface extends Throwable +{ +} diff --git a/src/Exception/FailedStreamOperationException.php b/src/Exception/FailedStreamOperationException.php new file mode 100644 index 0000000..30e6369 --- /dev/null +++ b/src/Exception/FailedStreamOperationException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * FailedStreamOperationException + */ +class FailedStreamOperationException extends RuntimeException +{ +} diff --git a/src/Exception/FailedUploadedFileOperationException.php b/src/Exception/FailedUploadedFileOperationException.php new file mode 100644 index 0000000..9046ded --- /dev/null +++ b/src/Exception/FailedUploadedFileOperationException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * FailedUploadedFileOperationException + */ +class FailedUploadedFileOperationException extends RuntimeException +{ +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..9626d8a --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,24 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * Import classes + */ +use InvalidArgumentException as BaseInvalidArgumentException; + +/** + * InvalidArgumentException + */ +class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Exception/InvalidHeaderException.php b/src/Exception/InvalidHeaderException.php new file mode 100644 index 0000000..5a60100 --- /dev/null +++ b/src/Exception/InvalidHeaderException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidHeaderException + */ +class InvalidHeaderException extends InvalidArgumentException +{ +} diff --git a/src/Exception/InvalidHeaderNameException.php b/src/Exception/InvalidHeaderNameException.php new file mode 100644 index 0000000..8e3b545 --- /dev/null +++ b/src/Exception/InvalidHeaderNameException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidHeaderNameException + */ +class InvalidHeaderNameException extends InvalidHeaderException +{ +} diff --git a/src/Exception/InvalidHeaderValueException.php b/src/Exception/InvalidHeaderValueException.php new file mode 100644 index 0000000..571182d --- /dev/null +++ b/src/Exception/InvalidHeaderValueException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidHeaderValueException + */ +class InvalidHeaderValueException extends InvalidHeaderException +{ +} diff --git a/src/Exception/InvalidHeaderValueParameterException.php b/src/Exception/InvalidHeaderValueParameterException.php new file mode 100644 index 0000000..b537bba --- /dev/null +++ b/src/Exception/InvalidHeaderValueParameterException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidHeaderValueParameterException + */ +class InvalidHeaderValueParameterException extends InvalidHeaderValueException +{ +} diff --git a/src/Exception/InvalidStreamException.php b/src/Exception/InvalidStreamException.php new file mode 100644 index 0000000..bada40e --- /dev/null +++ b/src/Exception/InvalidStreamException.php @@ -0,0 +1,27 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidStreamException + */ +class InvalidStreamException extends RuntimeException +{ + + /** + * @return self + */ + final public static function noResource(): self + { + return new self('The stream without a resource so the operation is not possible'); + } +} diff --git a/src/Exception/InvalidStreamOperationException.php b/src/Exception/InvalidStreamOperationException.php new file mode 100644 index 0000000..093af94 --- /dev/null +++ b/src/Exception/InvalidStreamOperationException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidStreamOperationException + */ +class InvalidStreamOperationException extends RuntimeException +{ +} diff --git a/src/Exception/InvalidUploadedFileException.php b/src/Exception/InvalidUploadedFileException.php new file mode 100644 index 0000000..38293e5 --- /dev/null +++ b/src/Exception/InvalidUploadedFileException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidUploadedFileException + */ +class InvalidUploadedFileException extends RuntimeException +{ +} diff --git a/src/Exception/InvalidUploadedFileOperationException.php b/src/Exception/InvalidUploadedFileOperationException.php new file mode 100644 index 0000000..cf481db --- /dev/null +++ b/src/Exception/InvalidUploadedFileOperationException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidUploadedFileOperationException + */ +class InvalidUploadedFileOperationException extends RuntimeException +{ +} diff --git a/src/Exception/InvalidUriComponentException.php b/src/Exception/InvalidUriComponentException.php new file mode 100644 index 0000000..faa43a6 --- /dev/null +++ b/src/Exception/InvalidUriComponentException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidUriComponentException + */ +class InvalidUriComponentException extends InvalidUriException +{ +} diff --git a/src/Exception/InvalidUriException.php b/src/Exception/InvalidUriException.php new file mode 100644 index 0000000..707d3db --- /dev/null +++ b/src/Exception/InvalidUriException.php @@ -0,0 +1,19 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * InvalidUriException + */ +class InvalidUriException extends InvalidArgumentException +{ +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 0000000..2c3a86d --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,24 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Exception; + +/** + * Import classes + */ +use RuntimeException as BaseRuntimeException; + +/** + * RuntimeException + */ +class RuntimeException extends BaseRuntimeException implements ExceptionInterface +{ +} diff --git a/src/Header.php b/src/Header.php new file mode 100644 index 0000000..abd8846 --- /dev/null +++ b/src/Header.php @@ -0,0 +1,258 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use ArrayIterator; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Traversable; + +/** + * Import functions + */ +use function gettype; +use function is_int; +use function is_string; +use function preg_match; +use function sprintf; + +/** + * HTTP Header Field + */ +abstract class Header implements HeaderInterface +{ + + /** + * Regular Expression for a token validation + * + * @link https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @var string + */ + public const RFC7230_VALID_TOKEN = '/^[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]+$/'; + + /** + * Regular Expression for a field value validation + * + * @link https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @var string + */ + public const RFC7230_VALID_FIELD_VALUE = '/^[\x09\x20-\x7E\x80-\xFF]*$/'; + + /** + * Regular Expression for a quoted string validation + * + * @link https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @var string + */ + public const RFC7230_VALID_QUOTED_STRING = '/^[\x09\x20\x21\x23-\x5B\x5D-\x7E\x80-\xFF]*$/'; + + /** + * Date and time format + * + * @link https://www.rfc-editor.org/rfc/rfc822#section-5 + * + * @var string + */ + public const RFC822_DATE_TIME_FORMAT = 'D, d M y H:i:s O'; + + /** + * {@inheritdoc} + */ + final public function getIterator(): Traversable + { + return new ArrayIterator([$this->getFieldName(), $this->getFieldValue()]); + } + + /** + * {@inheritdoc} + */ + final public function __toString(): string + { + return sprintf('%s: %s', $this->getFieldName(), $this->getFieldValue()); + } + + /** + * Checks if the given string is a token + * + * @param string $token + * + * @return bool + */ + final protected function isToken(string $token): bool + { + return preg_match(self::RFC7230_VALID_TOKEN, $token) === 1; + } + + /** + * Validates the given token(s) + * + * @param string ...$tokens + * + * @return void + * + * @throws InvalidHeaderValueException + * If one of the tokens isn't valid. + */ + final protected function validateToken(string ...$tokens): void + { + $this->validateValueByRegex(self::RFC7230_VALID_TOKEN, ...$tokens); + } + + /** + * Validates the given field value(s) + * + * @param string ...$fieldValues + * + * @return void + * + * @throws InvalidHeaderValueException + * If one of the field values isn't valid. + */ + final protected function validateFieldValue(string ...$fieldValues): void + { + $this->validateValueByRegex(self::RFC7230_VALID_FIELD_VALUE, ...$fieldValues); + } + + /** + * Validates the given quoted string(s) + * + * @param string ...$quotedStrings + * + * @return void + * + * @throws InvalidHeaderValueException + * If one of the quoted strings isn't valid. + */ + final protected function validateQuotedString(string ...$quotedStrings): void + { + $this->validateValueByRegex(self::RFC7230_VALID_QUOTED_STRING, ...$quotedStrings); + } + + /** + * Validates and normalizes the given parameters + * + * @param array $parameters + * + * @return array + * The normalized parameters. + * + * @throws InvalidHeaderValueParameterException + * If one of the parameters isn't valid. + */ + final protected function validateParameters(array $parameters): array + { + return $this->validateParametersByRegex( + $parameters, + self::RFC7230_VALID_TOKEN, + self::RFC7230_VALID_QUOTED_STRING + ); + } + + /** + * Validates the given value(s) by the given regular expression + * + * @param string $regex + * @param string ...$values + * + * @return void + * + * @throws InvalidHeaderValueException + * If one of the values isn't valid. + */ + final protected function validateValueByRegex(string $regex, string ...$values): void + { + foreach ($values as $value) { + if (!preg_match($regex, $value)) { + throw new InvalidHeaderValueException(sprintf( + 'The value "%2$s" for the header "%1$s" is not valid', + $this->getFieldName(), + $value + )); + } + } + } + + /** + * Validates and normalizes the given parameters by the given regular expressions + * + * @param array $parameters + * @param string $nameRegex + * @param string $valueRegex + * + * @return array + * The normalized parameters. + * + * @throws InvalidHeaderValueParameterException + * If one of the parameters isn't valid. + */ + final protected function validateParametersByRegex(array $parameters, string $nameRegex, string $valueRegex): array + { + foreach ($parameters as $name => &$value) { + if (!is_string($name) || !preg_match($nameRegex, $name)) { + throw new InvalidHeaderValueParameterException(sprintf( + 'The parameter name "%2$s" for the header "%1$s" is not valid', + $this->getFieldName(), + (is_string($name) ? $name : ('<' . gettype($name) . '>')) + )); + } + + // e.g. Cache-Control: max-age=31536000 + if (is_int($value)) { + $value = (string) $value; + } + + if (!is_string($value) || !preg_match($valueRegex, $value)) { + throw new InvalidHeaderValueParameterException(sprintf( + 'The parameter value "%2$s" for the header "%1$s" is not valid', + $this->getFieldName(), + (is_string($value) ? $value : ('<' . gettype($value) . '>')) + )); + } + } + + /** @var array $parameters */ + + return $parameters; + } + + /** + * Formats the given date-time object + * + * @link https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @param DateTimeInterface $dateTime + * + * @return string + */ + final protected function formatDateTime(DateTimeInterface $dateTime): string + { + if ($dateTime instanceof DateTime) { + return (clone $dateTime) + ->setTimezone(new DateTimeZone('GMT')) + ->format(self::RFC822_DATE_TIME_FORMAT); + } + + return $dateTime + ->setTimezone(new DateTimeZone('GMT')) + ->format(self::RFC822_DATE_TIME_FORMAT); + } +} diff --git a/src/Header/AccessControlAllowCredentialsHeader.php b/src/Header/AccessControlAllowCredentialsHeader.php new file mode 100644 index 0000000..c96232a --- /dev/null +++ b/src/Header/AccessControlAllowCredentialsHeader.php @@ -0,0 +1,40 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Header; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + */ +class AccessControlAllowCredentialsHeader extends Header +{ + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Access-Control-Allow-Credentials'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return 'true'; + } +} diff --git a/src/Header/AccessControlAllowHeadersHeader.php b/src/Header/AccessControlAllowHeadersHeader.php new file mode 100644 index 0000000..70d3d7e --- /dev/null +++ b/src/Header/AccessControlAllowHeadersHeader.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + */ +class AccessControlAllowHeadersHeader extends Header +{ + + /** + * @var list + */ + private array $headers; + + /** + * Constructor of the class + * + * @param string ...$headers + * + * @throws InvalidHeaderValueException + * If one of the header names isn't valid. + */ + public function __construct(string ...$headers) + { + /** @var list $headers */ + + $this->validateToken(...$headers); + + $this->headers = $headers; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Access-Control-Allow-Headers'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->headers); + } +} diff --git a/src/Header/AccessControlAllowMethodsHeader.php b/src/Header/AccessControlAllowMethodsHeader.php new file mode 100644 index 0000000..ed8ab4b --- /dev/null +++ b/src/Header/AccessControlAllowMethodsHeader.php @@ -0,0 +1,72 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function strtoupper; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + */ +class AccessControlAllowMethodsHeader extends Header +{ + + /** + * @var list + */ + private array $methods = []; + + /** + * Constructor of the class + * + * @param string ...$methods + * + * @throws InvalidHeaderValueException + * If one of the methods isn't valid. + */ + public function __construct(string ...$methods) + { + /** @var list $methods */ + + $this->validateToken(...$methods); + + // normalize the list of methods... + foreach ($methods as $method) { + $this->methods[] = strtoupper($method); + } + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Access-Control-Allow-Methods'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->methods); + } +} diff --git a/src/Header/AccessControlAllowOriginHeader.php b/src/Header/AccessControlAllowOriginHeader.php new file mode 100644 index 0000000..02defb6 --- /dev/null +++ b/src/Header/AccessControlAllowOriginHeader.php @@ -0,0 +1,107 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Exception\InvalidUriException; +use Sunrise\Http\Message\Header; +use Sunrise\Http\Message\Uri; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + */ +class AccessControlAllowOriginHeader extends Header +{ + + /** + * @var UriInterface|null + */ + private ?UriInterface $uri = null; + + /** + * Constructor of the class + * + * @param mixed $uri + * + * @throws InvalidUriException + * If the URI isn't valid. + * + * @throws InvalidHeaderValueException + * If the URI isn't valid. + */ + public function __construct($uri = null) + { + if (isset($uri)) { + $uri = Uri::create($uri); + $this->validateUri($uri); + $this->uri = $uri; + } + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Access-Control-Allow-Origin'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + if (!isset($this->uri)) { + return '*'; + } + + $origin = $this->uri->getScheme() . ':'; + $origin .= '//' . $this->uri->getHost(); + + $port = $this->uri->getPort(); + if (isset($port)) { + $origin .= ':' . $port; + } + + return $origin; + } + + /** + * Validates the given URI + * + * @param UriInterface $uri + * + * @return void + * + * @throws InvalidHeaderValueException + * If the URI isn't valid. + */ + private function validateUri(UriInterface $uri): void + { + if ($uri->getScheme() === '' || $uri->getHost() === '') { + throw new InvalidHeaderValueException(sprintf( + 'The URI "%2$s" for the header "%1$s" is not valid', + $this->getFieldName(), + $uri->__toString() + )); + } + } +} diff --git a/src/Header/AccessControlExposeHeadersHeader.php b/src/Header/AccessControlExposeHeadersHeader.php new file mode 100644 index 0000000..999c974 --- /dev/null +++ b/src/Header/AccessControlExposeHeadersHeader.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + */ +class AccessControlExposeHeadersHeader extends Header +{ + + /** + * @var list + */ + private array $headers; + + /** + * Constructor of the class + * + * @param string ...$headers + * + * @throws InvalidHeaderValueException + * If one of the header names isn't valid. + */ + public function __construct(string ...$headers) + { + /** @var list $headers */ + + $this->validateToken(...$headers); + + $this->headers = $headers; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Access-Control-Expose-Headers'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->headers); + } +} diff --git a/src/Header/AccessControlMaxAgeHeader.php b/src/Header/AccessControlMaxAgeHeader.php new file mode 100644 index 0000000..fa6d361 --- /dev/null +++ b/src/Header/AccessControlMaxAgeHeader.php @@ -0,0 +1,87 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + */ +class AccessControlMaxAgeHeader extends Header +{ + + /** + * @var int + */ + private int $value; + + /** + * Constructor of the class + * + * @param int $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(int $value) + { + $this->validateValue($value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Access-Control-Max-Age'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return sprintf('%d', $this->value); + } + + /** + * Validates the given value + * + * @param int $value + * + * @return void + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + private function validateValue(int $value): void + { + if (! ($value === -1 || $value >= 1)) { + throw new InvalidHeaderValueException(sprintf( + 'The value "%2$d" for the header "%1$s" is not valid.', + $this->getFieldName(), + $value + )); + } + } +} diff --git a/src/Header/AgeHeader.php b/src/Header/AgeHeader.php new file mode 100644 index 0000000..faa4a60 --- /dev/null +++ b/src/Header/AgeHeader.php @@ -0,0 +1,87 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.6 + */ +class AgeHeader extends Header +{ + + /** + * @var int + */ + private int $value; + + /** + * Constructor of the class + * + * @param int $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(int $value) + { + $this->validateValue($value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Age'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return sprintf('%d', $this->value); + } + + /** + * Validates the given value + * + * @param int $value + * + * @return void + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + private function validateValue(int $value): void + { + if (! ($value >= 0)) { + throw new InvalidHeaderValueException(sprintf( + 'The value "%2$d" for the header "%1$s" is not valid', + $this->getFieldName(), + $value + )); + } + } +} diff --git a/src/Header/AllowHeader.php b/src/Header/AllowHeader.php new file mode 100644 index 0000000..fc943c4 --- /dev/null +++ b/src/Header/AllowHeader.php @@ -0,0 +1,72 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function strtoupper; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.7 + */ +class AllowHeader extends Header +{ + + /** + * @var list + */ + private array $methods = []; + + /** + * Constructor of the class + * + * @param string ...$methods + * + * @throws InvalidHeaderValueException + * If one of the methods isn't valid. + */ + public function __construct(string ...$methods) + { + /** @var list $methods */ + + $this->validateToken(...$methods); + + // normalize the list of methods... + foreach ($methods as $method) { + $this->methods[] = strtoupper($method); + } + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Allow'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->methods); + } +} diff --git a/src/Header/CacheControlHeader.php b/src/Header/CacheControlHeader.php new file mode 100644 index 0000000..1dfe3e0 --- /dev/null +++ b/src/Header/CacheControlHeader.php @@ -0,0 +1,80 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.9 + */ +class CacheControlHeader extends Header +{ + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param array $parameters + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct(array $parameters) + { + $parameters = $this->validateParameters($parameters); + + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Cache-Control'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $segments = []; + foreach ($this->parameters as $name => $value) { + // the construction isn't valid... + if ($value === '') { + $segments[] = $name; + continue; + } + + $format = $this->isToken($value) ? '%s=%s' : '%s="%s"'; + + $segments[] = sprintf($format, $name, $value); + } + + return implode(', ', $segments); + } +} diff --git a/src/Header/ClearSiteDataHeader.php b/src/Header/ClearSiteDataHeader.php new file mode 100644 index 0000000..664f65b --- /dev/null +++ b/src/Header/ClearSiteDataHeader.php @@ -0,0 +1,74 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function sprintf; + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data + */ +class ClearSiteDataHeader extends Header +{ + + /** + * @var list + */ + private array $directives; + + /** + * Constructor of the class + * + * @param string ...$directives + * + * @throws InvalidHeaderValueException + * If one of the directives isn't valid. + */ + public function __construct(string ...$directives) + { + /** @var list $directives */ + + $this->validateQuotedString(...$directives); + + $this->directives = $directives; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Clear-Site-Data'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $segments = []; + foreach ($this->directives as $directive) { + $segments[] = sprintf('"%s"', $directive); + } + + return implode(', ', $segments); + } +} diff --git a/src/Header/ConnectionHeader.php b/src/Header/ConnectionHeader.php new file mode 100644 index 0000000..171fe8d --- /dev/null +++ b/src/Header/ConnectionHeader.php @@ -0,0 +1,71 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.10 + */ +class ConnectionHeader extends Header +{ + + /** + * @var string + */ + public const CONNECTION_CLOSE = 'close'; + + /** + * @var string + */ + public const CONNECTION_KEEP_ALIVE = 'keep-alive'; + + /** + * @var string + */ + private string $value; + + /** + * Constructor of the class + * + * @param string $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(string $value) + { + $this->validateToken($value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Connection'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->value; + } +} diff --git a/src/Header/ContentDispositionHeader.php b/src/Header/ContentDispositionHeader.php new file mode 100644 index 0000000..60d5bbf --- /dev/null +++ b/src/Header/ContentDispositionHeader.php @@ -0,0 +1,84 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-19.5.1 + */ +class ContentDispositionHeader extends Header +{ + + /** + * @var string + */ + private string $type; + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param string $type + * @param array $parameters + * + * @throws InvalidHeaderValueException + * If the type isn't valid. + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct(string $type, array $parameters = []) + { + $this->validateToken($type); + + $parameters = $this->validateParameters($parameters); + + $this->type = $type; + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Disposition'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $v = $this->type; + foreach ($this->parameters as $name => $value) { + $v .= sprintf('; %s="%s"', $name, $value); + } + + return $v; + } +} diff --git a/src/Header/ContentEncodingHeader.php b/src/Header/ContentEncodingHeader.php new file mode 100644 index 0000000..af51152 --- /dev/null +++ b/src/Header/ContentEncodingHeader.php @@ -0,0 +1,78 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.11 + */ +class ContentEncodingHeader extends Header +{ + + /** + * Directives + * + * @var string + */ + public const GZIP = 'gzip'; + public const COMPRESS = 'compress'; + public const DEFLATE = 'deflate'; + public const BR = 'br'; + + /** + * @var list + */ + private array $directives; + + /** + * Constructor of the class + * + * @param string ...$directives + * + * @throws InvalidHeaderValueException + * If one of the directives isn't valid. + */ + public function __construct(string ...$directives) + { + /** @var list $directives */ + + $this->validateToken(...$directives); + + $this->directives = $directives; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Encoding'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->directives); + } +} diff --git a/src/Header/ContentLanguageHeader.php b/src/Header/ContentLanguageHeader.php new file mode 100644 index 0000000..f81c33a --- /dev/null +++ b/src/Header/ContentLanguageHeader.php @@ -0,0 +1,77 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.12 + */ +class ContentLanguageHeader extends Header +{ + + /** + * Regular Expression for a language tag validation + * + * @link https://tools.ietf.org/html/rfc2616#section-3.10 + * + * @var string + */ + public const RFC2616_LANGUAGE_TAG = '/^[a-zA-Z]{1,8}(?:\-[a-zA-Z]{1,8})?$/'; + + /** + * @var list + */ + private array $languages; + + /** + * Constructor of the class + * + * @param string ...$languages + * + * @throws InvalidHeaderValueException + * If one of the language codes isn't valid. + */ + public function __construct(string ...$languages) + { + /** @var list $languages */ + + $this->validateValueByRegex(self::RFC2616_LANGUAGE_TAG, ...$languages); + + $this->languages = $languages; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Language'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->languages); + } +} diff --git a/src/Header/ContentLengthHeader.php b/src/Header/ContentLengthHeader.php new file mode 100644 index 0000000..c0a8393 --- /dev/null +++ b/src/Header/ContentLengthHeader.php @@ -0,0 +1,87 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.13 + */ +class ContentLengthHeader extends Header +{ + + /** + * @var int + */ + private int $value; + + /** + * Constructor of the class + * + * @param int $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(int $value) + { + $this->validateValue($value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Length'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return sprintf('%d', $this->value); + } + + /** + * Validates the given value + * + * @param int $value + * + * @return void + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + private function validateValue(int $value): void + { + if (! ($value >= 0)) { + throw new InvalidHeaderValueException(sprintf( + 'The value "%2$d" for the header "%1$s" is not valid', + $this->getFieldName(), + $value + )); + } + } +} diff --git a/src/Header/ContentLocationHeader.php b/src/Header/ContentLocationHeader.php new file mode 100644 index 0000000..b2fd64e --- /dev/null +++ b/src/Header/ContentLocationHeader.php @@ -0,0 +1,63 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidUriException; +use Sunrise\Http\Message\Header; +use Sunrise\Http\Message\Uri; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.14 + */ +class ContentLocationHeader extends Header +{ + + /** + * @var UriInterface + */ + private UriInterface $uri; + + /** + * Constructor of the class + * + * @param mixed $uri + * + * @throws InvalidUriException + * If the URI isn't valid. + */ + public function __construct($uri) + { + $uri = Uri::create($uri); + + $this->uri = $uri; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Location'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->uri->__toString(); + } +} diff --git a/src/Header/ContentMD5Header.php b/src/Header/ContentMD5Header.php new file mode 100644 index 0000000..2ed1133 --- /dev/null +++ b/src/Header/ContentMD5Header.php @@ -0,0 +1,70 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.15 + */ +class ContentMD5Header extends Header +{ + + /** + * Regular Expression for a MD5 digest validation + * + * @link https://tools.ietf.org/html/rfc2045#section-6.8 + * + * @var string + */ + public const RFC2045_MD5_DIGEST = '/^[A-Za-z0-9\+\/]+=*$/'; + + /** + * @var string + */ + private string $value; + + /** + * Constructor of the class + * + * @param string $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(string $value) + { + $this->validateValueByRegex(self::RFC2045_MD5_DIGEST, $value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-MD5'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->value; + } +} diff --git a/src/Header/ContentRangeHeader.php b/src/Header/ContentRangeHeader.php new file mode 100644 index 0000000..3f6884f --- /dev/null +++ b/src/Header/ContentRangeHeader.php @@ -0,0 +1,114 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.16 + */ +class ContentRangeHeader extends Header +{ + + /** + * @var int + */ + private int $firstBytePosition; + + /** + * @var int + */ + private int $lastBytePosition; + + /** + * @var int + */ + private int $instanceLength; + + /** + * Constructor of the class + * + * @param int $firstBytePosition + * @param int $lastBytePosition + * @param int $instanceLength + * + * @throws InvalidHeaderValueException + * If the range isn't valid. + */ + public function __construct(int $firstBytePosition, int $lastBytePosition, int $instanceLength) + { + $this->validateRange($firstBytePosition, $lastBytePosition, $instanceLength); + + $this->firstBytePosition = $firstBytePosition; + $this->lastBytePosition = $lastBytePosition; + $this->instanceLength = $instanceLength; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Range'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return sprintf( + 'bytes %d-%d/%d', + $this->firstBytePosition, + $this->lastBytePosition, + $this->instanceLength + ); + } + + /** + * Validates the given range + * + * @param int $firstBytePosition + * @param int $lastBytePosition + * @param int $instanceLength + * + * @return void + * + * @throws InvalidHeaderValueException + * If the range isn't valid. + */ + private function validateRange(int $firstBytePosition, int $lastBytePosition, int $instanceLength): void + { + if (! ($firstBytePosition <= $lastBytePosition)) { + throw new InvalidHeaderValueException( + 'The "first-byte-pos" value of the content range ' . + 'must be less than or equal to the "last-byte-pos" value' + ); + } + + if (! ($lastBytePosition < $instanceLength)) { + throw new InvalidHeaderValueException( + 'The "last-byte-pos" value of the content range ' . + 'must be less than the "instance-length" value' + ); + } + } +} diff --git a/src/Header/ContentSecurityPolicyHeader.php b/src/Header/ContentSecurityPolicyHeader.php new file mode 100644 index 0000000..620bb3f --- /dev/null +++ b/src/Header/ContentSecurityPolicyHeader.php @@ -0,0 +1,101 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function sprintf; + +/** + * @link https://www.w3.org/TR/CSP3/#csp-header + */ +class ContentSecurityPolicyHeader extends Header +{ + + /** + * Regular Expression for a directive name validation + * + * @link https://www.w3.org/TR/CSP3/#framework-directives + * + * @var string + */ + public const VALID_DIRECTIVE_NAME = '/^[0-9A-Za-z\-]+$/'; + + /** + * Regular Expression for a directive value validation + * + * @link https://www.w3.org/TR/CSP3/#framework-directives + * + * @var string + */ + public const VALID_DIRECTIVE_VALUE = '/^[\x09\x20-\x2B\x2D-\x3A\x3C-\x7E]*$/'; + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param array $parameters + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct(array $parameters = []) + { + $parameters = $this->validateParametersByRegex( + $parameters, + self::VALID_DIRECTIVE_NAME, + self::VALID_DIRECTIVE_VALUE + ); + + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Security-Policy'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $directives = []; + foreach ($this->parameters as $directive => $value) { + // the directive can be without value... + // e.g. sandbox, upgrade-insecure-requests, etc. + if ($value === '') { + $directives[] = $directive; + continue; + } + + $directives[] = sprintf('%s %s', $directive, $value); + } + + return implode('; ', $directives); + } +} diff --git a/src/Header/ContentSecurityPolicyReportOnlyHeader.php b/src/Header/ContentSecurityPolicyReportOnlyHeader.php new file mode 100644 index 0000000..3b80446 --- /dev/null +++ b/src/Header/ContentSecurityPolicyReportOnlyHeader.php @@ -0,0 +1,27 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * @link https://www.w3.org/TR/CSP3/#cspro-header + */ +class ContentSecurityPolicyReportOnlyHeader extends ContentSecurityPolicyHeader +{ + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Security-Policy-Report-Only'; + } +} diff --git a/src/Header/ContentTypeHeader.php b/src/Header/ContentTypeHeader.php new file mode 100644 index 0000000..8964004 --- /dev/null +++ b/src/Header/ContentTypeHeader.php @@ -0,0 +1,93 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.17 + */ +class ContentTypeHeader extends Header +{ + + /** + * Regular Expression for a content type validation + * + * @link https://tools.ietf.org/html/rfc6838#section-4.2 + * + * @var string + */ + public const RFC6838_CONTENT_TYPE = '/^[\dA-Za-z][\d\w\!#\$&\+\-\.\^]*(?:\/[\dA-Za-z][\d\w\!#\$&\+\-\.\^]*)?$/'; + + /** + * @var string + */ + private string $type; + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param string $type + * @param array $parameters + * + * @throws InvalidHeaderValueException + * If the type isn't valid. + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct(string $type, array $parameters = []) + { + $this->validateValueByRegex(self::RFC6838_CONTENT_TYPE, $type); + + $parameters = $this->validateParameters($parameters); + + $this->type = $type; + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Content-Type'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $v = $this->type; + foreach ($this->parameters as $name => $value) { + $v .= sprintf('; %s="%s"', $name, $value); + } + + return $v; + } +} diff --git a/src/Header/CookieHeader.php b/src/Header/CookieHeader.php new file mode 100644 index 0000000..511255b --- /dev/null +++ b/src/Header/CookieHeader.php @@ -0,0 +1,65 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function http_build_query; + +/** + * Import constants + */ +use const PHP_QUERY_RFC3986; + +/** + * @link https://tools.ietf.org/html/rfc6265.html#section-5.4 + */ +class CookieHeader extends Header +{ + + /** + * @var array + */ + private array $value; + + /** + * Constructor of the class + * + * @param array $value + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Cookie'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return http_build_query($this->value, '', '; ', PHP_QUERY_RFC3986); + } +} diff --git a/src/Header/DateHeader.php b/src/Header/DateHeader.php new file mode 100644 index 0000000..1af8b44 --- /dev/null +++ b/src/Header/DateHeader.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeInterface; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.18 + * @link https://tools.ietf.org/html/rfc822#section-5 + */ +class DateHeader extends Header +{ + + /** + * @var DateTimeInterface + */ + private DateTimeInterface $timestamp; + + /** + * Constructor of the class + * + * @param DateTimeInterface $timestamp + */ + public function __construct(DateTimeInterface $timestamp) + { + $this->timestamp = $timestamp; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Date'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->formatDateTime($this->timestamp); + } +} diff --git a/src/Header/EtagHeader.php b/src/Header/EtagHeader.php new file mode 100644 index 0000000..d5d421f --- /dev/null +++ b/src/Header/EtagHeader.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.19 + */ +class EtagHeader extends Header +{ + + /** + * @var string + */ + private string $value; + + /** + * Constructor of the class + * + * @param string $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(string $value) + { + $this->validateQuotedString($value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'ETag'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return sprintf('"%s"', $this->value); + } +} diff --git a/src/Header/ExpiresHeader.php b/src/Header/ExpiresHeader.php new file mode 100644 index 0000000..339b95e --- /dev/null +++ b/src/Header/ExpiresHeader.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeInterface; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.21 + * @link https://tools.ietf.org/html/rfc822#section-5 + */ +class ExpiresHeader extends Header +{ + + /** + * @var DateTimeInterface + */ + private DateTimeInterface $timestamp; + + /** + * Constructor of the class + * + * @param DateTimeInterface $timestamp + */ + public function __construct(DateTimeInterface $timestamp) + { + $this->timestamp = $timestamp; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Expires'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->formatDateTime($this->timestamp); + } +} diff --git a/src/Header/KeepAliveHeader.php b/src/Header/KeepAliveHeader.php new file mode 100644 index 0000000..fbbc205 --- /dev/null +++ b/src/Header/KeepAliveHeader.php @@ -0,0 +1,80 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2068#section-19.7.1.1 + */ +class KeepAliveHeader extends Header +{ + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param array $parameters + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct(array $parameters = []) + { + $parameters = $this->validateParameters($parameters); + + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Keep-Alive'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $segments = []; + foreach ($this->parameters as $name => $value) { + // the construction isn't valid... + if ($value === '') { + $segments[] = $name; + continue; + } + + $format = $this->isToken($value) ? '%s=%s' : '%s="%s"'; + + $segments[] = sprintf($format, $name, $value); + } + + return implode(', ', $segments); + } +} diff --git a/src/Header/LastModifiedHeader.php b/src/Header/LastModifiedHeader.php new file mode 100644 index 0000000..72c2fbb --- /dev/null +++ b/src/Header/LastModifiedHeader.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeInterface; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.29 + * @link https://tools.ietf.org/html/rfc822#section-5 + */ +class LastModifiedHeader extends Header +{ + + /** + * @var DateTimeInterface + */ + private DateTimeInterface $timestamp; + + /** + * Constructor of the class + * + * @param DateTimeInterface $timestamp + */ + public function __construct(DateTimeInterface $timestamp) + { + $this->timestamp = $timestamp; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Last-Modified'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->formatDateTime($this->timestamp); + } +} diff --git a/src/Header/LinkHeader.php b/src/Header/LinkHeader.php new file mode 100644 index 0000000..506f54c --- /dev/null +++ b/src/Header/LinkHeader.php @@ -0,0 +1,86 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Exception\InvalidUriException; +use Sunrise\Http\Message\Header; +use Sunrise\Http\Message\Uri; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc5988 + */ +class LinkHeader extends Header +{ + + /** + * @var UriInterface + */ + private UriInterface $uri; + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param mixed $uri + * @param array $parameters + * + * @throws InvalidUriException + * If the URI isn't valid. + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct($uri, array $parameters = []) + { + $uri = Uri::create($uri); + + $parameters = $this->validateParameters($parameters); + + $this->uri = $uri; + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Link'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $v = sprintf('<%s>', $this->uri->__toString()); + foreach ($this->parameters as $name => $value) { + $v .= sprintf('; %s="%s"', $name, $value); + } + + return $v; + } +} diff --git a/src/Header/LocationHeader.php b/src/Header/LocationHeader.php new file mode 100644 index 0000000..0eef409 --- /dev/null +++ b/src/Header/LocationHeader.php @@ -0,0 +1,63 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidUriException; +use Sunrise\Http\Message\Header; +use Sunrise\Http\Message\Uri; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.30 + */ +class LocationHeader extends Header +{ + + /** + * @var UriInterface + */ + private UriInterface $uri; + + /** + * Constructor of the class + * + * @param mixed $uri + * + * @throws InvalidUriException + * If the URI isn't valid. + */ + public function __construct($uri) + { + $uri = Uri::create($uri); + + $this->uri = $uri; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Location'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->uri->__toString(); + } +} diff --git a/src/Header/RefreshHeader.php b/src/Header/RefreshHeader.php new file mode 100644 index 0000000..4065b94 --- /dev/null +++ b/src/Header/RefreshHeader.php @@ -0,0 +1,102 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Exception\InvalidUriException; +use Sunrise\Http\Message\Header; +use Sunrise\Http\Message\Uri; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://en.wikipedia.org/wiki/Meta_refresh + */ +class RefreshHeader extends Header +{ + + /** + * @var int + */ + private int $delay; + + /** + * @var UriInterface + */ + private UriInterface $uri; + + /** + * Constructor of the class + * + * @param int $delay + * @param mixed $uri + * + * @throws InvalidUriException + * If the URI isn't valid. + * + * @throws InvalidHeaderValueException + * If the delay isn't valid. + */ + public function __construct(int $delay, $uri) + { + $this->validateDelay($delay); + + $uri = Uri::create($uri); + + $this->delay = $delay; + $this->uri = $uri; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Refresh'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return sprintf('%d; url=%s', $this->delay, $this->uri->__toString()); + } + + /** + * Validates the redirection delay + * + * @param int $delay + * + * @return void + * + * @throws InvalidHeaderValueException + * If the delay isn't valid. + */ + private function validateDelay(int $delay): void + { + if (! ($delay >= 0)) { + throw new InvalidHeaderValueException(sprintf( + 'The delay "%2$d" for the header "%1$s" is not valid', + $this->getFieldName(), + $delay + )); + } + } +} diff --git a/src/Header/RetryAfterHeader.php b/src/Header/RetryAfterHeader.php new file mode 100644 index 0000000..9dd7a20 --- /dev/null +++ b/src/Header/RetryAfterHeader.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeInterface; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.37 + * @link https://tools.ietf.org/html/rfc822#section-5 + */ +class RetryAfterHeader extends Header +{ + + /** + * @var DateTimeInterface + */ + private DateTimeInterface $timestamp; + + /** + * Constructor of the class + * + * @param DateTimeInterface $timestamp + */ + public function __construct(DateTimeInterface $timestamp) + { + $this->timestamp = $timestamp; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Retry-After'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->formatDateTime($this->timestamp); + } +} diff --git a/src/Header/SetCookieHeader.php b/src/Header/SetCookieHeader.php new file mode 100644 index 0000000..cfcc990 --- /dev/null +++ b/src/Header/SetCookieHeader.php @@ -0,0 +1,277 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeImmutable; +use DateTimeInterface; +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function max; +use function rawurlencode; +use function sprintf; +use function strpbrk; +use function time; + +/** + * @link https://tools.ietf.org/html/rfc6265#section-4.1 + * @link https://github.com/php/php-src/blob/master/ext/standard/head.c + */ +class SetCookieHeader extends Header +{ + + /** + * Cookies are not sent on normal cross-site subrequests, but + * are sent when a user is navigating to the origin site. + * + * This is the default cookie value if SameSite has not been + * explicitly specified in recent browser versions.. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#lax + * + * @var string + */ + public const SAME_SITE_LAX = 'Lax'; + + /** + * Cookies will only be sent in a first-party context and not + * be sent along with requests initiated by third party websites. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#strict + * + * @var string + */ + public const SAME_SITE_STRICT = 'Strict'; + + /** + * Cookies will be sent in all contexts, i.e. in responses to + * both first-party and cross-site requests. + * + * If SameSite=None is set, the cookie Secure attribute must + * also be set (or the cookie will be blocked). + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#none + * + * @var string + */ + public const SAME_SITE_NONE = 'None'; + + /** + * Cookie option keys + * + * @var string + */ + public const OPTION_KEY_PATH = 'path'; + public const OPTION_KEY_DOMAIN = 'domain'; + public const OPTION_KEY_SECURE = 'secure'; + public const OPTION_KEY_HTTP_ONLY = 'httpOnly'; + public const OPTION_KEY_SAMESITE = 'sameSite'; + + /** + * Default cookie options + * + * @var array{ + * path?: ?string, + * domain?: ?string, + * secure?: ?bool, + * httpOnly?: ?bool, + * sameSite?: ?string + * } + */ + protected static array $defaultOptions = [ + self::OPTION_KEY_PATH => '/', + self::OPTION_KEY_DOMAIN => null, + self::OPTION_KEY_SECURE => null, + self::OPTION_KEY_HTTP_ONLY => true, + self::OPTION_KEY_SAMESITE => self::SAME_SITE_LAX, + ]; + + /** + * The cookie name + * + * @var string + */ + private string $name; + + /** + * The cookie value + * + * @var string + */ + private string $value; + + /** + * The cookie expiration date + * + * @var DateTimeInterface|null + */ + private ?DateTimeInterface $expires; + + /** + * The cookie options + * + * @var array{ + * path?: ?string, + * domain?: ?string, + * secure?: ?bool, + * httpOnly?: ?bool, + * sameSite?: ?string + * } + */ + private array $options; + + /** + * Constructor of the class + * + * @param string $name + * @param string $value + * @param DateTimeInterface|null $expires + * @param array{path?: ?string, domain?: ?string, secure?: ?bool, httpOnly?: ?bool, sameSite?: ?string} $options + * + * @throws InvalidHeaderValueException + * If one of the parameters isn't valid. + */ + public function __construct(string $name, string $value, ?DateTimeInterface $expires = null, array $options = []) + { + $this->validateCookieName($name); + + if (isset($options[self::OPTION_KEY_PATH])) { + $this->validateCookieOption(self::OPTION_KEY_PATH, $options[self::OPTION_KEY_PATH]); + } + + if (isset($options[self::OPTION_KEY_DOMAIN])) { + $this->validateCookieOption(self::OPTION_KEY_DOMAIN, $options[self::OPTION_KEY_DOMAIN]); + } + + if (isset($options[self::OPTION_KEY_SAMESITE])) { + $this->validateCookieOption(self::OPTION_KEY_SAMESITE, $options[self::OPTION_KEY_SAMESITE]); + } + + if ($value === '') { + $value = 'deleted'; + $expires = new DateTimeImmutable('1 year ago'); + } + + $options += static::$defaultOptions; + + $this->name = $name; + $this->value = $value; + $this->expires = $expires; + $this->options = $options; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Set-Cookie'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $name = rawurlencode($this->name); + $value = rawurlencode($this->value); + $result = sprintf('%s=%s', $name, $value); + + if (isset($this->expires)) { + $result .= '; Expires=' . $this->formatDateTime($this->expires); + $result .= '; Max-Age=' . max($this->expires->getTimestamp() - time(), 0); + } + + if (isset($this->options[self::OPTION_KEY_PATH])) { + $result .= '; Path=' . $this->options[self::OPTION_KEY_PATH]; + } + + if (isset($this->options[self::OPTION_KEY_DOMAIN])) { + $result .= '; Domain=' . $this->options[self::OPTION_KEY_DOMAIN]; + } + + if (isset($this->options[self::OPTION_KEY_SECURE]) && $this->options[self::OPTION_KEY_SECURE]) { + $result .= '; Secure'; + } + + if (isset($this->options[self::OPTION_KEY_HTTP_ONLY]) && $this->options[self::OPTION_KEY_HTTP_ONLY]) { + $result .= '; HttpOnly'; + } + + if (isset($this->options[self::OPTION_KEY_SAMESITE])) { + $result .= '; SameSite=' . $this->options[self::OPTION_KEY_SAMESITE]; + } + + return $result; + } + + /** + * Validates the given cookie name + * + * @param string $name + * + * @return void + * + * @throws InvalidHeaderValueException + * If the cookie name isn't valid. + */ + private function validateCookieName(string $name): void + { + if ('' === $name) { + throw new InvalidHeaderValueException('Cookie name cannot be empty'); + } + + // https://github.com/php/php-src/blob/02a5335b710aa36cd0c3108bfb9c6f7a57d40000/ext/standard/head.c#L93 + if (strpbrk($name, "=,; \t\r\n\013\014") !== false) { + throw new InvalidHeaderValueException(sprintf( + 'The cookie name "%s" contains prohibited characters', + $name + )); + } + } + + /** + * Validates the given cookie option + * + * @param string $validKey + * @param mixed $value + * + * @return void + * + * @throws InvalidHeaderValueException + * If the cookie option isn't valid. + */ + private function validateCookieOption(string $validKey, $value): void + { + if (!is_string($value)) { + throw new InvalidHeaderValueException(sprintf( + 'The cookie option "%s" must be a string', + $validKey + )); + } + + // https://github.com/php/php-src/blob/02a5335b710aa36cd0c3108bfb9c6f7a57d40000/ext/standard/head.c#L103 + // https://github.com/php/php-src/blob/02a5335b710aa36cd0c3108bfb9c6f7a57d40000/ext/standard/head.c#L108 + if (strpbrk($value, ",; \t\r\n\013\014") !== false) { + throw new InvalidHeaderValueException(sprintf( + 'The cookie option "%s" contains prohibited characters', + $validKey + )); + } + } +} diff --git a/src/Header/SunsetHeader.php b/src/Header/SunsetHeader.php new file mode 100644 index 0000000..b8682ae --- /dev/null +++ b/src/Header/SunsetHeader.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeInterface; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/id/draft-wilde-sunset-header-03.html + * @link https://github.com/sunrise-php/http-header-kit/issues/1#issuecomment-457043527 + */ +class SunsetHeader extends Header +{ + + /** + * @var DateTimeInterface + */ + private DateTimeInterface $timestamp; + + /** + * Constructor of the class + * + * @param DateTimeInterface $timestamp + */ + public function __construct(DateTimeInterface $timestamp) + { + $this->timestamp = $timestamp; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Sunset'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->formatDateTime($this->timestamp); + } +} diff --git a/src/Header/TrailerHeader.php b/src/Header/TrailerHeader.php new file mode 100644 index 0000000..684621a --- /dev/null +++ b/src/Header/TrailerHeader.php @@ -0,0 +1,61 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.40 + */ +class TrailerHeader extends Header +{ + + /** + * @var string + */ + private string $value; + + /** + * Constructor of the class + * + * @param string $value + * + * @throws InvalidHeaderValueException + * If the value isn't valid. + */ + public function __construct(string $value) + { + $this->validateToken($value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Trailer'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return $this->value; + } +} diff --git a/src/Header/TransferEncodingHeader.php b/src/Header/TransferEncodingHeader.php new file mode 100644 index 0000000..dabeda2 --- /dev/null +++ b/src/Header/TransferEncodingHeader.php @@ -0,0 +1,78 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.41 + */ +class TransferEncodingHeader extends Header +{ + + /** + * Directives + * + * @var string + */ + public const CHUNKED = 'chunked'; + public const COMPRESS = 'compress'; + public const DEFLATE = 'deflate'; + public const GZIP = 'gzip'; + + /** + * @var list + */ + private array $directives; + + /** + * Constructor of the class + * + * @param string ...$directives + * + * @throws InvalidHeaderValueException + * If one of the directives isn't valid. + */ + public function __construct(string ...$directives) + { + /** @var list $directives */ + + $this->validateToken(...$directives); + + $this->directives = $directives; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Transfer-Encoding'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->directives); + } +} diff --git a/src/Header/VaryHeader.php b/src/Header/VaryHeader.php new file mode 100644 index 0000000..dd12ef3 --- /dev/null +++ b/src/Header/VaryHeader.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.44 + */ +class VaryHeader extends Header +{ + + /** + * @var list + */ + private array $value; + + /** + * Constructor of the class + * + * @param string ...$value + * + * @throws InvalidHeaderValueException + * If one of the values isn't valid. + */ + public function __construct(string ...$value) + { + /** @var list $value */ + + $this->validateToken(...$value); + + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Vary'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + return implode(', ', $this->value); + } +} diff --git a/src/Header/WWWAuthenticateHeader.php b/src/Header/WWWAuthenticateHeader.php new file mode 100644 index 0000000..c6c475b --- /dev/null +++ b/src/Header/WWWAuthenticateHeader.php @@ -0,0 +1,107 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Exception\InvalidHeaderValueParameterException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function implode; +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc7235#section-4.1 + */ +class WWWAuthenticateHeader extends Header +{ + + /** + * HTTP Authentication Schemes + * + * @link https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml + */ + public const HTTP_AUTHENTICATE_SCHEME_BASIC = 'Basic'; + public const HTTP_AUTHENTICATE_SCHEME_BEARER = 'Bearer'; + public const HTTP_AUTHENTICATE_SCHEME_DIGEST = 'Digest'; + public const HTTP_AUTHENTICATE_SCHEME_HOBA = 'HOBA'; + public const HTTP_AUTHENTICATE_SCHEME_MUTUAL = 'Mutual'; + public const HTTP_AUTHENTICATE_SCHEME_NEGOTIATE = 'Negotiate'; + public const HTTP_AUTHENTICATE_SCHEME_OAUTH = 'OAuth'; + public const HTTP_AUTHENTICATE_SCHEME_SCRAM_SHA_1 = 'SCRAM-SHA-1'; + public const HTTP_AUTHENTICATE_SCHEME_SCRAM_SHA_256 = 'SCRAM-SHA-256'; + public const HTTP_AUTHENTICATE_SCHEME_VAPID = 'vapid'; + + /** + * @var string + */ + private string $scheme; + + /** + * @var array + */ + private array $parameters; + + /** + * Constructor of the class + * + * @param string $scheme + * @param array $parameters + * + * @throws InvalidHeaderValueException + * If the scheme isn't valid. + * + * @throws InvalidHeaderValueParameterException + * If the parameters aren't valid. + */ + public function __construct(string $scheme, array $parameters = []) + { + $this->validateToken($scheme); + + $parameters = $this->validateParameters($parameters); + + $this->scheme = $scheme; + $this->parameters = $parameters; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'WWW-Authenticate'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $v = $this->scheme; + + $challenge = []; + foreach ($this->parameters as $name => $value) { + $challenge[] = sprintf(' %s="%s"', $name, $value); + } + + if (!empty($challenge)) { + $v .= implode(',', $challenge); + } + + return $v; + } +} diff --git a/src/Header/WarningHeader.php b/src/Header/WarningHeader.php new file mode 100644 index 0000000..7132def --- /dev/null +++ b/src/Header/WarningHeader.php @@ -0,0 +1,130 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Header; + +/** + * Import classes + */ +use DateTimeInterface; +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Header; + +/** + * Import functions + */ +use function sprintf; + +/** + * @link https://tools.ietf.org/html/rfc2616#section-14.46 + */ +class WarningHeader extends Header +{ + + /** + * HTTP Warning Codes + * + * @link https://www.iana.org/assignments/http-warn-codes/http-warn-codes.xhtml + */ + public const HTTP_WARNING_CODE_RESPONSE_IS_STALE = 110; + public const HTTP_WARNING_CODE_REVALIDATION_FAILED = 111; + public const HTTP_WARNING_CODE_DISCONNECTED_OPERATION = 112; + public const HTTP_WARNING_CODE_HEURISTIC_EXPIRATION = 113; + public const HTTP_WARNING_CODE_MISCELLANEOUS_WARNING = 199; + public const HTTP_WARNING_CODE_TRANSFORMATION_APPLIED = 214; + public const HTTP_WARNING_CODE_MISCELLANEOUS_PERSISTENT_WARNING = 299; + + /** + * @var int + */ + private int $code; + + /** + * @var string + */ + private string $agent; + + /** + * @var string + */ + private string $text; + + /** + * @var DateTimeInterface|null + */ + private ?DateTimeInterface $date; + + /** + * Constructor of the class + * + * @param int $code + * @param string $agent + * @param string $text + * @param DateTimeInterface|null $date + * + * @throws InvalidHeaderValueException + * If one of parameters isn't valid. + */ + public function __construct(int $code, string $agent, string $text, ?DateTimeInterface $date = null) + { + $this->validateCode($code); + $this->validateToken($agent); + $this->validateQuotedString($text); + + $this->code = $code; + $this->agent = $agent; + $this->text = $text; + $this->date = $date; + } + + /** + * {@inheritdoc} + */ + public function getFieldName(): string + { + return 'Warning'; + } + + /** + * {@inheritdoc} + */ + public function getFieldValue(): string + { + $value = sprintf('%s %s "%s"', $this->code, $this->agent, $this->text); + + if (isset($this->date)) { + $value .= sprintf(' "%s"', $this->formatDateTime($this->date)); + } + + return $value; + } + + /** + * Validates the given code + * + * @param int $code + * + * @return void + * + * @throws InvalidHeaderValueException + * If the code isn't valid. + */ + private function validateCode(int $code): void + { + if (! ($code >= 100 && $code <= 999)) { + throw new InvalidHeaderValueException(sprintf( + 'The code "%2$d" for the header "%1$s" is not valid', + $this->getFieldName(), + $code + )); + } + } +} diff --git a/src/HeaderInterface.php b/src/HeaderInterface.php new file mode 100644 index 0000000..67afa98 --- /dev/null +++ b/src/HeaderInterface.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use IteratorAggregate; + +/** + * + * $response->withHeader(...new SetCookieHeader('foo', 'bar')) + * + * + * @template-implements IteratorAggregate + */ +interface HeaderInterface extends IteratorAggregate +{ + + /** + * Gets the header field name + * + * @return string + */ + public function getFieldName(): string; + + /** + * Gets the header field value + * + * @return string + */ + public function getFieldValue(): string; + + /** + * Converts the header field to a string + * + * @link http://php.net/manual/en/language.oop5.magic.php#object.tostring + * + * @return string + */ + public function __toString(): string; +} diff --git a/src/Message.php b/src/Message.php index 2988cd5..443cffc 100644 --- a/src/Message.php +++ b/src/Message.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE * @link https://github.com/sunrise-php/http-message */ @@ -14,21 +14,24 @@ /** * Import classes */ -use InvalidArgumentException; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\StreamInterface; -use Sunrise\Http\Header\HeaderInterface; -use Sunrise\Stream\StreamFactory; +use Sunrise\Http\Message\Exception\InvalidArgumentException; +use Sunrise\Http\Message\Exception\InvalidHeaderException; +use Sunrise\Http\Message\Exception\InvalidHeaderNameException; +use Sunrise\Http\Message\Exception\InvalidHeaderValueException; +use Sunrise\Http\Message\Stream\PhpTempStream; /** * Import functions */ use function implode; +use function in_array; +use function is_array; use function is_string; use function preg_match; use function sprintf; use function strtolower; -use function ucwords; /** * Hypertext Transfer Protocol Message @@ -36,69 +39,72 @@ * @link https://tools.ietf.org/html/rfc7230 * @link https://www.php-fig.org/psr/psr-7/ */ -class Message implements MessageInterface +abstract class Message implements MessageInterface { /** - * HTTP version + * Default HTTP version * * @var string */ - protected $protocolVersion = '1.1'; + public const DEFAULT_HTTP_VERSION = '1.1'; /** - * The message headers + * Supported HTTP versions * - * @var array + * @var list */ - protected $headers = []; + public const SUPPORTED_HTTP_VERSIONS = ['1.0', '1.1', '2.0', '2']; /** - * The message body + * The message HTTP version * - * @var StreamInterface|null + * @var string */ - protected $body = null; + private string $protocolVersion = self::DEFAULT_HTTP_VERSION; /** - * Constructor of the class + * The message headers * - * @param array|null $headers - * @param StreamInterface|null $body - * @param string|null $protocolVersion + * @var array> */ - public function __construct( - ?array $headers = null, - ?StreamInterface $body = null, - ?string $protocolVersion = null - ) { - if (isset($protocolVersion)) { - $this->setProtocolVersion($protocolVersion); - } + private array $headers = []; - if (isset($headers)) { - foreach ($headers as $name => $value) { - $this->addHeader($name, $value); - } - } + /** + * Original header names (see $headers) + * + * @var array + */ + private array $headerNames = []; - if (isset($body)) { - $this->body = $body; - } - } + /** + * The message body + * + * @var StreamInterface|null + */ + private ?StreamInterface $body = null; /** - * {@inheritdoc} + * Gets the message HTTP version + * + * @return string */ - public function getProtocolVersion() : string + public function getProtocolVersion(): string { return $this->protocolVersion; } /** - * {@inheritdoc} + * Creates a new instance of the message with the given HTTP version + * + * @param string $version + * + * @return static + * + * @throws InvalidArgumentException + * If the HTTP version isn't valid. */ - public function withProtocolVersion($version) : MessageInterface + public function withProtocolVersion($version): MessageInterface { $clone = clone $this; $clone->setProtocolVersion($version); @@ -107,105 +113,140 @@ public function withProtocolVersion($version) : MessageInterface } /** - * {@inheritdoc} + * Gets the message headers + * + * @return array> */ - public function getHeaders() : array + public function getHeaders(): array { return $this->headers; } /** - * {@inheritdoc} + * Checks if a header exists in the message by the given name + * + * @param string $name + * + * @return bool */ - public function hasHeader($name) : bool + public function hasHeader($name): bool { - $name = $this->normalizeHeaderName($name); + $key = strtolower($name); - return ! empty($this->headers[$name]); + return isset($this->headerNames[$key]); } /** - * {@inheritdoc} + * Gets a header value from the message by the given name + * + * @param string $name + * + * @return list */ - public function getHeader($name) : array + public function getHeader($name): array { - $name = $this->normalizeHeaderName($name); - - if (empty($this->headers[$name])) { + if (!$this->hasHeader($name)) { return []; } - return $this->headers[$name]; + $key = strtolower($name); + $originalName = $this->headerNames[$key]; + $value = $this->headers[$originalName]; + + return $value; } /** - * {@inheritdoc} + * Gets a header value as a string from the message by the given name + * + * @param string $name + * + * @return string */ - public function getHeaderLine($name) : string + public function getHeaderLine($name): string { - $name = $this->normalizeHeaderName($name); - - if (empty($this->headers[$name])) { + $value = $this->getHeader($name); + if ([] === $value) { return ''; } - return implode(', ', $this->headers[$name]); + return implode(',', $value); } /** - * {@inheritdoc} + * Creates a new instance of the message with the given header overwriting the old header + * + * @param string $name + * @param string|string[] $value + * + * @return static + * + * @throws InvalidHeaderException + * If the header isn't valid. */ - public function withHeader($name, $value) : MessageInterface + public function withHeader($name, $value): MessageInterface { $clone = clone $this; - $clone->addHeader($name, $value); + $clone->setHeader($name, $value, true); return $clone; } /** - * {@inheritdoc} + * Creates a new instance of the message with the given header NOT overwriting the old header + * + * @param string $name + * @param string|string[] $value + * + * @return static + * + * @throws InvalidHeaderException + * If the header isn't valid. */ - public function withAddedHeader($name, $value) : MessageInterface + public function withAddedHeader($name, $value): MessageInterface { $clone = clone $this; - $clone->addHeader($name, $value, false); + $clone->setHeader($name, $value, false); return $clone; } /** - * {@inheritdoc} + * Creates a new instance of the message without a header by the given name + * + * @param string $name + * + * @return static */ - public function withoutHeader($name) : MessageInterface + public function withoutHeader($name): MessageInterface { - $name = $this->normalizeHeaderName($name); - $clone = clone $this; - unset($clone->headers[$name]); + $clone->deleteHeader($name); return $clone; } /** - * {@inheritdoc} + * Gets the message body + * + * @return StreamInterface */ - public function getBody() : StreamInterface + public function getBody(): StreamInterface { - if (null === $this->body) { - $this->body = (new StreamFactory)->createStream(); - } - - return $this->body; + return $this->body ??= new PhpTempStream(); } /** - * {@inheritdoc} + * Creates a new instance of the message with the given body + * + * @param StreamInterface $body + * + * @return static */ - public function withBody(StreamInterface $body) : MessageInterface + public function withBody(StreamInterface $body): MessageInterface { $clone = clone $this; - $clone->body = $body; + $clone->setBody($body); return $clone; } @@ -213,70 +254,115 @@ public function withBody(StreamInterface $body) : MessageInterface /** * Sets the given HTTP version to the message * - * @param string $version + * @param string $protocolVersion * * @return void + * + * @throws InvalidArgumentException + * If the HTTP version isn't valid. */ - protected function setProtocolVersion($version) : void + final protected function setProtocolVersion($protocolVersion): void { - $this->validateProtocolVersion($version); + $this->validateProtocolVersion($protocolVersion); - $this->protocolVersion = $version; + $this->protocolVersion = $protocolVersion; } /** - * Adds the given header field to the message + * Sets a new header to the message with the given name and value(s) * * @param string $name * @param string|string[] $value * @param bool $replace * * @return void + * + * @throws InvalidHeaderException + * If the header isn't valid. */ - protected function addHeader($name, $value, bool $replace = true) : void + final protected function setHeader($name, $value, bool $replace = true): void { - $this->validateHeaderName($name); - $this->validateHeaderValue($value, $name); + if (!is_array($value)) { + $value = [$value]; + } - $name = $this->normalizeHeaderName($name); - $value = (array) $value; + $this->validateHeaderName($name); + $this->validateHeaderValue($name, $value); if ($replace) { - $this->headers[$name] = $value; - return; + $this->deleteHeader($name); } - foreach ($value as $item) { - $this->headers[$name][] = $item; + $key = strtolower($name); + + $this->headerNames[$key] ??= $name; + $this->headers[$this->headerNames[$key]] ??= []; + + foreach ($value as $subvalue) { + $this->headers[$this->headerNames[$key]][] = $subvalue; } } /** - * Validates the given HTTP version + * Sets the given headers to the message * - * @param mixed $version + * @param array $headers * * @return void * - * @throws InvalidArgumentException + * @throws InvalidHeaderException + * If one of the headers isn't valid. + */ + final protected function setHeaders(array $headers): void + { + foreach ($headers as $name => $value) { + $this->setHeader($name, $value, false); + } + } + + /** + * Deletes a header from the message by the given name + * + * @param string $name * - * @link https://tools.ietf.org/html/rfc2145 - * @link https://tools.ietf.org/html/rfc7230#section-2.6 - * @link https://tools.ietf.org/html/rfc7540 + * @return void */ - protected function validateProtocolVersion($version) : void + final protected function deleteHeader($name): void { - static $allowed = ['1.0' => true, '1.1' => true, '2.0' => true, '2' => true]; + $key = strtolower($name); - if (!is_string($version)) { - throw new InvalidArgumentException('HTTP version must be a string'); + if (isset($this->headerNames[$key])) { + unset($this->headers[$this->headerNames[$key]]); + unset($this->headerNames[$key]); } + } - if (!isset($allowed[$version])) { - throw new InvalidArgumentException(sprintf( - 'The HTTP version "%s" is not valid, use only: 1.0, 1.1, 2{.0}', - $version - )); + /** + * Sets the given body to the message + * + * @param StreamInterface $body + * + * @return void + */ + final protected function setBody(StreamInterface $body): void + { + $this->body = $body; + } + + /** + * Validates the given HTTP version + * + * @param mixed $protocolVersion + * + * @return void + * + * @throws InvalidArgumentException + * If the HTTP version isn't valid. + */ + private function validateProtocolVersion($protocolVersion): void + { + if (!in_array($protocolVersion, self::SUPPORTED_HTTP_VERSIONS, true)) { + throw new InvalidArgumentException('Invalid or unsupported HTTP version'); } } @@ -287,76 +373,64 @@ protected function validateProtocolVersion($version) : void * * @return void * - * @throws InvalidArgumentException - * - * @link https://tools.ietf.org/html/rfc7230#section-3.2 + * @throws InvalidHeaderNameException + * If the header name isn't valid. */ - protected function validateHeaderName($name) : void + private function validateHeaderName($name): void { + if ($name === '') { + throw new InvalidHeaderNameException('HTTP header name cannot be an empty'); + } + if (!is_string($name)) { - throw new InvalidArgumentException('Header name must be a string'); + throw new InvalidHeaderNameException('HTTP header name must be a string'); } - if (!preg_match(HeaderInterface::RFC7230_TOKEN, $name)) { - throw new InvalidArgumentException(sprintf( - 'The header name "%s" is not valid', - $name - )); + if (!preg_match(Header::RFC7230_VALID_TOKEN, $name)) { + throw new InvalidHeaderNameException('HTTP header name is invalid'); } } /** * Validates the given header value * - * @param mixed $value - * @param string $name + * @param string $validName + * @param array $value * * @return void * - * @throws InvalidArgumentException - * - * @link https://tools.ietf.org/html/rfc7230#section-3.2 + * @throws InvalidHeaderValueException + * If the header value isn't valid. */ - protected function validateHeaderValue($value, string $name) : void + private function validateHeaderValue(string $validName, array $value): void { - $items = (array) $value; - - if ([] === $items) { - throw new InvalidArgumentException(sprintf( - 'The header "%s" value must be a string or a non-empty array', - $name + if ([] === $value) { + throw new InvalidHeaderValueException(sprintf( + 'The "%s" HTTP header value cannot be an empty array', + $validName, )); } - foreach ($items as $item) { - if (!is_string($item)) { - throw new InvalidArgumentException(sprintf( - 'The header "%s" value must be a string or an array with strings only', - $name + foreach ($value as $i => $subvalue) { + if ('' === $subvalue) { + continue; + } + + if (!is_string($subvalue)) { + throw new InvalidHeaderValueException(sprintf( + 'The "%s[%d]" HTTP header value must be a string', + $validName, + $i )); } - if (!preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $item)) { - throw new InvalidArgumentException(sprintf( - 'The header "%s" value "%s" is not valid', - $name, - $item + if (!preg_match(Header::RFC7230_VALID_FIELD_VALUE, $subvalue)) { + throw new InvalidHeaderValueException(sprintf( + 'The "%s[%d]" HTTP header value is invalid', + $validName, + $i )); } } } - - /** - * Normalizes the given header name - * - * @param string $name - * - * @return string - * - * @link https://tools.ietf.org/html/rfc7230#section-3.2 - */ - protected function normalizeHeaderName($name) : string - { - return ucwords(strtolower($name), '-'); - } } diff --git a/src/Request.php b/src/Request.php index e4b1a0f..54a3751 100644 --- a/src/Request.php +++ b/src/Request.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE * @link https://github.com/sunrise-php/http-message */ @@ -15,21 +15,17 @@ * Import classes */ use Fig\Http\Message\RequestMethodInterface; -use InvalidArgumentException; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -use Sunrise\Http\Header\HeaderInterface; -use Sunrise\Uri\UriFactory; +use Sunrise\Http\Message\Exception\InvalidArgumentException; /** * Import functions */ use function is_string; use function preg_match; -use function sprintf; use function strncmp; -use function strtoupper; /** * HTTP Request Message @@ -41,75 +37,102 @@ class Request extends Message implements RequestInterface, RequestMethodInterfac { /** - * The request method (aka verb) + * Regular Expression for a request target validation + * + * @link https://tools.ietf.org/html/rfc7230#section-5.3 * * @var string */ - protected $method = self::METHOD_GET; + public const RFC7230_VALID_REQUEST_TARGET = '/^[\x21-\x7E\x80-\xFF]+$/'; /** - * The request target + * Default request method * - * @var string|null + * @var string */ - protected $requestTarget = null; + public const DEFAULT_METHOD = self::METHOD_GET; + + /** + * Default request URI + * + * @var string + */ + public const DEFAULT_URI = '/'; + + /** + * The request method (aka verb) + * + * @var string + */ + private string $method = self::DEFAULT_METHOD; /** * The request URI * - * @var UriInterface|null + * @var UriInterface + */ + private UriInterface $uri; + + /** + * The request target + * + * @var string|null */ - protected $uri = null; + private ?string $requestTarget = null; /** * Constructor of the class * * @param string|null $method - * @param string|UriInterface|null $uri + * @param mixed $uri * @param array|null $headers * @param StreamInterface|null $body - * @param string|null $requestTarget - * @param string|null $protocolVersion + * + * @throws InvalidArgumentException + * If one of the parameters isn't valid. */ public function __construct( ?string $method = null, $uri = null, ?array $headers = null, - ?StreamInterface $body = null, - ?string $requestTarget = null, - ?string $protocolVersion = null + ?StreamInterface $body = null ) { - parent::__construct( - $headers, - $body, - $protocolVersion - ); - if (isset($method)) { $this->setMethod($method); } - if (isset($requestTarget)) { - $this->setRequestTarget($requestTarget); + $this->setUri($uri ?? self::DEFAULT_URI); + + if (isset($headers)) { + $this->setHeaders($headers); } - if (isset($uri)) { - $this->setUri($uri); + if (isset($body)) { + $this->setBody($body); } } /** - * {@inheritdoc} + * Gets the request method + * + * @return string */ - public function getMethod() : string + public function getMethod(): string { return $this->method; } /** - * {@inheritdoc} + * Creates a new instance of the request with the given method + * + * @param string $method + * + * @return static + * + * @throws InvalidArgumentException + * If the method isn't valid. */ - public function withMethod($method) : RequestInterface + public function withMethod($method): RequestInterface { $clone = clone $this; $clone->setMethod($method); @@ -118,40 +141,72 @@ public function withMethod($method) : RequestInterface } /** - * {@inheritdoc} + * Gets the request URI + * + * @return UriInterface */ - public function getRequestTarget() : string + public function getUri(): UriInterface + { + return $this->uri; + } + + /** + * Creates a new instance of the request with the given URI + * + * @param UriInterface $uri + * @param bool $preserveHost + * + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface + { + $clone = clone $this; + $clone->setUri($uri, $preserveHost); + + return $clone; + } + + /** + * Gets the request target + * + * @return string + */ + public function getRequestTarget(): string { if (isset($this->requestTarget)) { return $this->requestTarget; } - $uri = $this->getUri(); - $path = $uri->getPath(); + $requestTarget = $this->uri->getPath(); // https://tools.ietf.org/html/rfc7230#section-5.3.1 // https://tools.ietf.org/html/rfc7230#section-2.7 // // origin-form = absolute-path [ "?" query ] // absolute-path = 1*( "/" segment ) - if (0 !== strncmp($path, '/', 1)) { + if (strncmp($requestTarget, '/', 1) !== 0) { return '/'; } - $requestTarget = $path; - - $query = $uri->getQuery(); - if ('' !== $query) { - $requestTarget .= '?' . $query; + $queryString = $this->uri->getQuery(); + if ($queryString !== '') { + $requestTarget .= '?' . $queryString; } return $requestTarget; } /** - * {@inheritdoc} + * Creates a new instance of the request with the given request target + * + * @param mixed $requestTarget + * + * @return static + * + * @throws InvalidArgumentException + * If the request target isn't valid. */ - public function withRequestTarget($requestTarget) : RequestInterface + public function withRequestTarget($requestTarget): RequestInterface { $clone = clone $this; $clone->setRequestTarget($requestTarget); @@ -159,132 +214,123 @@ public function withRequestTarget($requestTarget) : RequestInterface return $clone; } - /** - * {@inheritdoc} - */ - public function getUri() : UriInterface - { - if (null === $this->uri) { - $this->uri = (new UriFactory)->createUri(); - } - - return $this->uri; - } - - /** - * {@inheritdoc} - */ - public function withUri(UriInterface $uri, $preserveHost = false) : RequestInterface - { - $clone = clone $this; - $clone->setUri($uri, $preserveHost); - - return $clone; - } - /** * Sets the given method to the request * * @param string $method * * @return void + * + * @throws InvalidArgumentException + * If the method isn't valid. */ - protected function setMethod($method) : void + final protected function setMethod($method): void { $this->validateMethod($method); - $this->method = strtoupper($method); + $this->method = $method; } /** - * Sets the given request-target to the request + * Sets the given URI to the request * - * @param mixed $requestTarget + * @param mixed $uri + * @param bool $preserveHost * * @return void + * + * @throws InvalidArgumentException + * If the URI isn't valid. */ - protected function setRequestTarget($requestTarget) : void + final protected function setUri($uri, $preserveHost = false): void { - $this->validateRequestTarget($requestTarget); + $this->uri = Uri::create($uri); - /** - * @var string $requestTarget - */ + if ($preserveHost && $this->hasHeader('Host')) { + return; + } - $this->requestTarget = $requestTarget; + $host = $this->uri->getHost(); + if ($host === '') { + return; + } + + $port = $this->uri->getPort(); + if (isset($port)) { + $host .= ':' . $port; + } + + $this->setHeader('Host', $host, true); } /** - * Sets the given URI to the request + * Sets the given request target to the request * - * @param string|UriInterface $uri - * @param bool $preserveHost + * @param mixed $requestTarget * * @return void + * + * @throws InvalidArgumentException + * If the request target isn't valid. */ - protected function setUri($uri, $preserveHost = false) : void + final protected function setRequestTarget($requestTarget): void { - if (! ($uri instanceof UriInterface)) { - $uri = (new UriFactory)->createUri($uri); - } - - $this->uri = $uri; - - if ('' === $uri->getHost() || ($preserveHost && $this->hasHeader('Host'))) { - return; - } - - $newHost = $uri->getHost(); + $this->validateRequestTarget($requestTarget); - $port = $uri->getPort(); - if (null !== $port) { - $newHost .= ':' . $port; - } + /** @var string $requestTarget */ - $this->addHeader('Host', $newHost); + $this->requestTarget = $requestTarget; } /** * Validates the given method * + * @link https://tools.ietf.org/html/rfc7230#section-3.1.1 + * * @param mixed $method * * @return void * * @throws InvalidArgumentException - * - * @link https://tools.ietf.org/html/rfc7230#section-3.1.1 + * If the method isn't valid. */ - protected function validateMethod($method) : void + private function validateMethod($method): void { + if ('' === $method) { + throw new InvalidArgumentException('HTTP method cannot be an empty'); + } + if (!is_string($method)) { throw new InvalidArgumentException('HTTP method must be a string'); } - if (!preg_match(HeaderInterface::RFC7230_TOKEN, $method)) { - throw new InvalidArgumentException(sprintf('HTTP method "%s" is not valid', $method)); + if (!preg_match(Header::RFC7230_VALID_TOKEN, $method)) { + throw new InvalidArgumentException('Invalid HTTP method'); } } /** - * Validates the given request-target + * Validates the given request target * * @param mixed $requestTarget * * @return void * * @throws InvalidArgumentException - * - * @link https://tools.ietf.org/html/rfc7230#section-5.3 + * If the request target isn't valid. */ - protected function validateRequestTarget($requestTarget) : void + private function validateRequestTarget($requestTarget): void { + if ('' === $requestTarget) { + throw new InvalidArgumentException('HTTP request target cannot be an empty'); + } + if (!is_string($requestTarget)) { - throw new InvalidArgumentException('HTTP request-target must be a string'); + throw new InvalidArgumentException('HTTP request target must be a string'); } - if (!preg_match('/^[\x21-\x7E\x80-\xFF]+$/', $requestTarget)) { - throw new InvalidArgumentException(sprintf('HTTP request-target "%s" is not valid', $requestTarget)); + if (!preg_match(self::RFC7230_VALID_REQUEST_TARGET, $requestTarget)) { + throw new InvalidArgumentException('Invalid HTTP request target'); } } } diff --git a/src/RequestFactory.php b/src/RequestFactory.php index 8969708..1e49412 100644 --- a/src/RequestFactory.php +++ b/src/RequestFactory.php @@ -14,22 +14,8 @@ /** * Import classes */ -use InvalidArgumentException; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; - -/** - * Import functions - */ -use function json_encode; -use function json_last_error; -use function json_last_error_msg; - -/** - * Import constants - */ -use const JSON_ERROR_NONE; /** * HTTP Request Message Factory @@ -42,43 +28,8 @@ class RequestFactory implements RequestFactoryInterface /** * {@inheritdoc} */ - public function createRequest(string $method, $uri) : RequestInterface + public function createRequest(string $method, $uri): RequestInterface { return new Request($method, $uri); } - - /** - * Creates a JSON request - * - * @param string $method - * @param string|UriInterface|null $uri - * @param mixed $data - * @param int $flags - * @param int $depth - * - * @return RequestInterface - * - * @throws InvalidArgumentException - * If the data cannot be encoded. - */ - public function createJsonRequest(string $method, $uri, $data, int $flags = 0, int $depth = 512) : RequestInterface - { - /** - * @psalm-suppress UnusedFunctionCall - */ - json_encode(''); - - $json = json_encode($data, $flags, $depth); - if (JSON_ERROR_NONE <> json_last_error()) { - throw new InvalidArgumentException(json_last_error_msg()); - } - - $request = new Request($method, $uri, [ - 'Content-Type' => 'application/json; charset=UTF-8', - ]); - - $request->getBody()->write($json); - - return $request; - } } diff --git a/src/Response.php b/src/Response.php index 2a0fe9d..d0c99cd 100644 --- a/src/Response.php +++ b/src/Response.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE * @link https://github.com/sunrise-php/http-message */ @@ -15,10 +15,9 @@ * Import classes */ use Fig\Http\Message\StatusCodeInterface; -use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; -use Sunrise\Http\Header\HeaderInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; /** * Import functions @@ -26,12 +25,6 @@ use function is_int; use function is_string; use function preg_match; -use function sprintf; - -/** - * Import constants - */ -use const Sunrise\Http\Message\REASON_PHRASES; /** * HTTP Response Message @@ -42,19 +35,121 @@ class Response extends Message implements ResponseInterface, StatusCodeInterface { + /** + * List of Reason Phrases + * + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * + * @var array + */ + public const REASON_PHRASES = [ + + // 1xx + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 103 => 'Early Hints', + + // 2xx + 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', + + // 3xx + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + + // 4xx + 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 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Too Early', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + + // 5xx + 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', + 511 => 'Network Authentication Required', + ]; + + /** + * Default response status code + * + * @var int + */ + public const DEFAULT_STATUS_CODE = self::STATUS_OK; + + /** + * Default response reason phrase + * + * @var string + */ + public const DEFAULT_REASON_PHRASE = self::REASON_PHRASES[self::DEFAULT_STATUS_CODE]; + + /** + * Reason phrase for unknown status code + * + * @var string + */ + public const UNKNOWN_STATUS_CODE_REASON_PHRASE = 'Unknown Status Code'; + /** * The response's status code * * @var int */ - protected $statusCode = self::STATUS_OK; + private int $statusCode = self::DEFAULT_STATUS_CODE; /** * The response's reason phrase * * @var string */ - protected $reasonPhrase = REASON_PHRASES[self::STATUS_OK]; + private string $reasonPhrase = self::DEFAULT_REASON_PHRASE; /** * Constrictor of the class @@ -63,70 +158,86 @@ class Response extends Message implements ResponseInterface, StatusCodeInterface * @param string|null $reasonPhrase * @param array|null $headers * @param StreamInterface|null $body - * @param string|null $protocolVersion + * + * @throws InvalidArgumentException + * If one of the parameters isn't valid. */ public function __construct( ?int $statusCode = null, ?string $reasonPhrase = null, ?array $headers = null, - ?StreamInterface $body = null, - ?string $protocolVersion = null + ?StreamInterface $body = null ) { - parent::__construct( - $headers, - $body, - $protocolVersion - ); - if (isset($statusCode)) { $this->setStatus($statusCode, $reasonPhrase ?? ''); } + + if (isset($headers)) { + $this->setHeaders($headers); + } + + if (isset($body)) { + $this->setBody($body); + } } /** - * {@inheritdoc} + * Gets the response's status code + * + * @return int */ - public function getStatusCode() : int + public function getStatusCode(): int { return $this->statusCode; } /** - * {@inheritdoc} + * Gets the response's reason phrase + * + * @return string */ - public function getReasonPhrase() : string + public function getReasonPhrase(): string { return $this->reasonPhrase; } /** - * {@inheritdoc} + * Creates a new instance of the response with the given status code + * + * @param int $code + * @param string $reasonPhrase * - * @psalm-suppress ParamNameMismatch + * @return static + * + * @throws InvalidArgumentException + * If the status isn't valid. */ - public function withStatus($statusCode, $reasonPhrase = '') : ResponseInterface + public function withStatus($code, $reasonPhrase = ''): ResponseInterface { $clone = clone $this; - $clone->setStatus($statusCode, $reasonPhrase); + $clone->setStatus($code, $reasonPhrase); return $clone; } /** - * Sets the given status to the response + * Sets the given status code to the response * * @param int $statusCode * @param string $reasonPhrase * * @return void + * + * @throws InvalidArgumentException + * If the status isn't valid. */ - protected function setStatus($statusCode, $reasonPhrase) : void + final protected function setStatus($statusCode, $reasonPhrase): void { $this->validateStatusCode($statusCode); $this->validateReasonPhrase($reasonPhrase); if ('' === $reasonPhrase) { - $reasonPhrase = REASON_PHRASES[$statusCode] ?? 'Unknown Status Code'; + $reasonPhrase = self::REASON_PHRASES[$statusCode] ?? self::UNKNOWN_STATUS_CODE_REASON_PHRASE; } $this->statusCode = $statusCode; @@ -134,46 +245,48 @@ protected function setStatus($statusCode, $reasonPhrase) : void } /** - * Validates the given status-code + * Validates the given status code + * + * @link https://tools.ietf.org/html/rfc7230#section-3.1.2 * * @param mixed $statusCode * * @return void * * @throws InvalidArgumentException - * - * @link https://tools.ietf.org/html/rfc7230#section-3.1.2 + * If the status code isn't valid. */ - protected function validateStatusCode($statusCode) : void + private function validateStatusCode($statusCode): void { if (!is_int($statusCode)) { - throw new InvalidArgumentException('HTTP status-code must be an integer'); + throw new InvalidArgumentException('HTTP status code must be an integer'); } if (! ($statusCode >= 100 && $statusCode <= 599)) { - throw new InvalidArgumentException(sprintf('HTTP status-code "%d" is not valid', $statusCode)); + throw new InvalidArgumentException('Invalid HTTP status code'); } } /** - * Validates the given reason-phrase + * Validates the given reason phrase + * + * @link https://tools.ietf.org/html/rfc7230#section-3.1.2 * * @param mixed $reasonPhrase * * @return void * * @throws InvalidArgumentException - * - * @link https://tools.ietf.org/html/rfc7230#section-3.1.2 + * If the reason phrase isn't valid. */ - protected function validateReasonPhrase($reasonPhrase) : void + private function validateReasonPhrase($reasonPhrase): void { if (!is_string($reasonPhrase)) { - throw new InvalidArgumentException('HTTP reason-phrase must be a string'); + throw new InvalidArgumentException('HTTP reason phrase must be a string'); } - if (!preg_match(HeaderInterface::RFC7230_FIELD_VALUE, $reasonPhrase)) { - throw new InvalidArgumentException(sprintf('HTTP reason-phrase "%s" is not valid', $reasonPhrase)); + if (!preg_match(Header::RFC7230_VALID_FIELD_VALUE, $reasonPhrase)) { + throw new InvalidArgumentException('Invalid HTTP reason phrase'); } } } diff --git a/src/Response/HtmlResponse.php b/src/Response/HtmlResponse.php new file mode 100644 index 0000000..1c71b96 --- /dev/null +++ b/src/Response/HtmlResponse.php @@ -0,0 +1,90 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Response; + +/** + * Import classes + */ +use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; +use Sunrise\Http\Message\Response; +use Sunrise\Http\Message\Stream\PhpTempStream; + +/** + * Import functions + */ +use function is_object; +use function is_string; +use function method_exists; + +/** + * HTML Response + */ +class HtmlResponse extends Response +{ + + /** + * The response content type + * + * @var string + */ + public const CONTENT_TYPE = 'text/html; charset=utf-8'; + + /** + * Constructor of the class + * + * @param int $statusCode + * @param mixed $html + * + * @throws InvalidArgumentException + */ + public function __construct(int $statusCode, $html) + { + $body = $this->createBody($html); + + $headers = ['Content-Type' => self::CONTENT_TYPE]; + + parent::__construct($statusCode, null, $headers, $body); + } + + /** + * Creates the response body from the given HTML + * + * @param mixed $html + * + * @return StreamInterface + * + * @throws InvalidArgumentException + * If the response body cannot be created from the given HTML. + */ + private function createBody($html): StreamInterface + { + if ($html instanceof StreamInterface) { + return $html; + } + + if (is_object($html) && method_exists($html, '__toString')) { + /** @var string */ + $html = $html->__toString(); + } + + if (!is_string($html)) { + throw new InvalidArgumentException('Unable to create HTML response due to invalid body'); + } + + $stream = new PhpTempStream('r+b'); + $stream->write($html); + $stream->rewind(); + + return $stream; + } +} diff --git a/src/Response/JsonResponse.php b/src/Response/JsonResponse.php new file mode 100644 index 0000000..4057f4f --- /dev/null +++ b/src/Response/JsonResponse.php @@ -0,0 +1,100 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Response; + +/** + * Import classes + */ +use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; +use Sunrise\Http\Message\Response; +use Sunrise\Http\Message\Stream\PhpTempStream; +use JsonException; + +/** + * Import functions + */ +use function json_encode; + +/** + * Import constants + */ +use const JSON_THROW_ON_ERROR; + +/** + * JSON Response + */ +class JsonResponse extends Response +{ + + /** + * The response content type + * + * @var string + */ + public const CONTENT_TYPE = 'application/json; charset=utf-8'; + + /** + * Constructor of the class + * + * @param int $statusCode + * @param mixed $data + * @param int $flags + * @param int $depth + * + * @throws InvalidArgumentException + */ + public function __construct(int $statusCode, $data, int $flags = 0, int $depth = 512) + { + $body = $this->createBody($data, $flags, $depth); + + $headers = ['Content-Type' => self::CONTENT_TYPE]; + + parent::__construct($statusCode, null, $headers, $body); + } + + /** + * Creates the response body from the given JSON data + * + * @param mixed $data + * @param int $flags + * @param int $depth + * + * @return StreamInterface + * + * @throws InvalidArgumentException + * If the response body cannot be created from the given JSON data. + */ + private function createBody($data, int $flags, int $depth): StreamInterface + { + if ($data instanceof StreamInterface) { + return $data; + } + + $flags |= JSON_THROW_ON_ERROR; + + try { + $payload = json_encode($data, $flags, $depth); + } catch (JsonException $e) { + throw new InvalidArgumentException(sprintf( + 'Unable to create JSON response due to invalid JSON data: %s', + $e->getMessage() + ), 0, $e); + } + + $stream = new PhpTempStream('r+b'); + $stream->write($payload); + $stream->rewind(); + + return $stream; + } +} diff --git a/src/ResponseFactory.php b/src/ResponseFactory.php index a1c0a52..88a8d6f 100644 --- a/src/ResponseFactory.php +++ b/src/ResponseFactory.php @@ -14,23 +14,8 @@ /** * Import classes */ -use InvalidArgumentException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; -use Stringable; - -/** - * Import functions - */ -use function json_encode; -use function json_last_error; -use function json_last_error_msg; - -/** - * Import constants - */ -use const JSON_ERROR_NONE; /** * HTTP Response Message Factory @@ -42,66 +27,9 @@ class ResponseFactory implements ResponseFactoryInterface /** * {@inheritdoc} - * - * @psalm-suppress ParamNameMismatch */ - public function createResponse(int $statusCode = 200, string $reasonPhrase = '') : ResponseInterface + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface { - return new Response($statusCode, $reasonPhrase); - } - - /** - * Creates a HTML response - * - * @param int $statusCode - * @param string|StreamInterface|Stringable $html - * - * @return ResponseInterface - */ - public function createHtmlResponse(int $statusCode, $html) : ResponseInterface - { - $html = (string) $html; - - $response = new Response($statusCode, null, [ - 'Content-Type' => 'text/html; charset=UTF-8', - ]); - - $response->getBody()->write($html); - - return $response; - } - - /** - * Creates a JSON response - * - * @param int $statusCode - * @param mixed $data - * @param int $flags - * @param int $depth - * - * @return ResponseInterface - * - * @throws InvalidArgumentException - * If the data cannot be encoded. - */ - public function createJsonResponse(int $statusCode, $data, int $flags = 0, int $depth = 512) : ResponseInterface - { - /** - * @psalm-suppress UnusedFunctionCall - */ - json_encode(''); - - $json = json_encode($data, $flags, $depth); - if (JSON_ERROR_NONE <> json_last_error()) { - throw new InvalidArgumentException(json_last_error_msg()); - } - - $response = new Response($statusCode, null, [ - 'Content-Type' => 'application/json; charset=UTF-8', - ]); - - $response->getBody()->write($json); - - return $response; + return new Response($code, $reasonPhrase); } } diff --git a/src/ServerRequest.php b/src/ServerRequest.php new file mode 100644 index 0000000..0d9503c --- /dev/null +++ b/src/ServerRequest.php @@ -0,0 +1,388 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; + +/** + * Import functions + */ +use function array_key_exists; +use function array_walk_recursive; +use function is_array; +use function is_object; + +/** + * ServerRequest + * + * @link https://www.php-fig.org/psr/psr-7/ + */ +class ServerRequest extends Request implements ServerRequestInterface +{ + + /** + * The server parameters + * + * @var array + */ + private array $serverParams; + + /** + * The request's query parameters + * + * @var array + */ + private array $queryParams; + + /** + * The request's cookie parameters + * + * @var array + */ + private array $cookieParams; + + /** + * The request's uploaded files + * + * @var array + */ + private array $uploadedFiles; + + /** + * The request's parsed body + * + * @var array|object|null + */ + private $parsedBody; + + /** + * The request attributes + * + * @var array + */ + private array $attributes; + + /** + * Constructor of the class + * + * @param string|null $protocolVersion + * @param string|null $method + * @param mixed $uri + * @param array|null $headers + * @param StreamInterface|null $body + * + * @param array $serverParams + * @param array $queryParams + * @param array $cookieParams + * @param array $uploadedFiles + * @param array|object|null $parsedBody + * @param array $attributes + * + * @throws InvalidArgumentException + * If one of the parameters isn't valid. + */ + public function __construct( + ?string $protocolVersion = null, + ?string $method = null, + $uri = null, + ?array $headers = null, + ?StreamInterface $body = null, + array $serverParams = [], + array $queryParams = [], + array $cookieParams = [], + array $uploadedFiles = [], + $parsedBody = null, + array $attributes = [] + ) { + if (isset($protocolVersion)) { + $this->setProtocolVersion($protocolVersion); + } + + parent::__construct($method, $uri, $headers, $body); + + $this->serverParams = $serverParams; + $this->queryParams = $queryParams; + $this->cookieParams = $cookieParams; + $this->setUploadedFiles($uploadedFiles); + $this->setParsedBody($parsedBody); + $this->attributes = $attributes; + } + + /** + * Gets the server parameters + * + * @return array + */ + public function getServerParams(): array + { + return $this->serverParams; + } + + /** + * Gets the request's query parameters + * + * @return array + */ + public function getQueryParams(): array + { + return $this->queryParams; + } + + /** + * Creates a new instance of the request with the given query parameters + * + * @param array $query + * + * @return static + */ + public function withQueryParams(array $query): ServerRequestInterface + { + $clone = clone $this; + $clone->queryParams = $query; + + return $clone; + } + + /** + * Gets the request's cookie parameters + * + * @return array + */ + public function getCookieParams(): array + { + return $this->cookieParams; + } + + /** + * Creates a new instance of the request with the given cookie parameters + * + * @param array $cookies + * + * @return static + */ + public function withCookieParams(array $cookies): ServerRequestInterface + { + $clone = clone $this; + $clone->cookieParams = $cookies; + + return $clone; + } + + /** + * Gets the request's uploaded files + * + * @return array + */ + public function getUploadedFiles(): array + { + return $this->uploadedFiles; + } + + /** + * Creates a new instance of the request with the given uploaded files + * + * @param array $uploadedFiles + * + * @return static + * + * @throws InvalidArgumentException + * If one of the files isn't valid. + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + $clone = clone $this; + $clone->setUploadedFiles($uploadedFiles); + + return $clone; + } + + /** + * Gets the request's parsed body + * + * @return array|object|null + */ + public function getParsedBody() + { + return $this->parsedBody; + } + + /** + * Creates a new instance of the request with the given parsed body + * + * @param array|object|null $data + * + * @return static + * + * @throws InvalidArgumentException + * If the data isn't valid. + */ + public function withParsedBody($data): ServerRequestInterface + { + $clone = clone $this; + $clone->setParsedBody($data); + + return $clone; + } + + /** + * Gets the request attributes + * + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * Gets the request's attribute value by the given name + * + * Returns the default value if the attribute wasn't found. + * + * @param array-key $name + * @param mixed $default + * + * @return mixed + */ + public function getAttribute($name, $default = null) + { + if (!array_key_exists($name, $this->attributes)) { + return $default; + } + + return $this->attributes[$name]; + } + + /** + * Creates a new instance of the request with the given attribute + * + * @param array-key $name + * @param mixed $value + * + * @return static + */ + public function withAttribute($name, $value): ServerRequestInterface + { + $clone = clone $this; + $clone->attributes[$name] = $value; + + return $clone; + } + + /** + * Creates a new instance of the request without an attribute with the given name + * + * @param array-key $name + * + * @return static + */ + public function withoutAttribute($name): ServerRequestInterface + { + $clone = clone $this; + unset($clone->attributes[$name]); + + return $clone; + } + + /** + * Sets the given uploaded files to the request + * + * @param array $files + * + * @return void + * + * @throws InvalidArgumentException + * If one of the files isn't valid. + */ + final protected function setUploadedFiles(array $files): void + { + $this->validateUploadedFiles($files); + + $this->uploadedFiles = $files; + } + + /** + * Sets the given parsed body to the request + * + * @param array|object|null $data + * + * @return void + * + * @throws InvalidArgumentException + * If the parsed body isn't valid. + */ + final protected function setParsedBody($data): void + { + $this->validateParsedBody($data); + + $this->parsedBody = $data; + } + + /** + * Validates the given uploaded files + * + * @param array $files + * + * @return void + * + * @throws InvalidArgumentException + * If one of the files isn't valid. + */ + private function validateUploadedFiles(array $files): void + { + if ([] === $files) { + return; + } + + /** + * @param mixed $file + * + * @return void + * + * @throws InvalidArgumentException + * + * @psalm-suppress MissingClosureParamType + */ + array_walk_recursive($files, static function ($file): void { + if (! ($file instanceof UploadedFileInterface)) { + throw new InvalidArgumentException('Invalid uploaded files'); + } + }); + } + + /** + * Validates the given parsed body + * + * @param mixed $data + * + * @return void + * + * @throws InvalidArgumentException + * If the parsed body isn't valid. + */ + private function validateParsedBody($data): void + { + if (null === $data) { + return; + } + + if (!is_array($data) && !is_object($data)) { + throw new InvalidArgumentException('Invalid parsed body'); + } + } +} diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php new file mode 100644 index 0000000..9d33a51 --- /dev/null +++ b/src/ServerRequestFactory.php @@ -0,0 +1,84 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Message\Stream\PhpInputStream; + +/** + * ServerRequestFactory + * + * @link https://www.php-fig.org/psr/psr-17/ + */ +class ServerRequestFactory implements ServerRequestFactoryInterface +{ + + /** + * Creates a new request from superglobals variables + * + * @param array|null $serverParams + * @param array|null $queryParams + * @param array|null $cookieParams + * @param array|null $uploadedFiles + * @param array|null $parsedBody + * + * @return ServerRequestInterface + * + * @link http://php.net/manual/en/language.variables.superglobals.php + * @link https://www.php-fig.org/psr/psr-15/meta/ + */ + public static function fromGlobals( + ?array $serverParams = null, + ?array $queryParams = null, + ?array $cookieParams = null, + ?array $uploadedFiles = null, + ?array $parsedBody = null + ): ServerRequestInterface { + $serverParams = $serverParams ?? $_SERVER; + $queryParams = $queryParams ?? $_GET; + $cookieParams = $cookieParams ?? $_COOKIE; + $uploadedFiles = $uploadedFiles ?? $_FILES; + $parsedBody = $parsedBody ?? $_POST; + + return new ServerRequest( + server_request_protocol_version($serverParams), + server_request_method($serverParams), + server_request_uri($serverParams), + server_request_headers($serverParams), + new PhpInputStream(), + $serverParams, + $queryParams, + $cookieParams, + server_request_files($uploadedFiles), + $parsedBody + ); + } + + /** + * {@inheritdoc} + */ + public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface + { + return new ServerRequest( + server_request_protocol_version($serverParams), + $method, + $uri, + server_request_headers($serverParams), + null, // body + $serverParams + ); + } +} diff --git a/src/Stream.php b/src/Stream.php new file mode 100644 index 0000000..22a0e5c --- /dev/null +++ b/src/Stream.php @@ -0,0 +1,445 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Exception\FailedStreamOperationException; +use Sunrise\Http\Message\Exception\InvalidArgumentException; +use Sunrise\Http\Message\Exception\InvalidStreamException; +use Sunrise\Http\Message\Exception\InvalidStreamOperationException; +use Throwable; + +/** + * Import functions + */ +use function fclose; +use function feof; +use function fread; +use function fseek; +use function fstat; +use function ftell; +use function fwrite; +use function is_resource; +use function stream_get_contents; +use function stream_get_meta_data; +use function strpbrk; + +/** + * Import constants + */ +use const SEEK_SET; + +/** + * Stream + * + * @link https://www.php-fig.org/psr/psr-7/ + */ +class Stream implements StreamInterface +{ + + /** + * The stream resource + * + * @var resource|null + */ + private $resource; + + /** + * Signals to close the stream on destruction + * + * @var bool + */ + private $autoClose; + + /** + * Constructor of the class + * + * @param mixed $resource + * @param bool $autoClose + * + * @throws InvalidArgumentException + * If the stream cannot be initialized with the resource. + */ + public function __construct($resource, bool $autoClose = true) + { + if (!is_resource($resource)) { + throw new InvalidArgumentException('Unexpected stream resource'); + } + + $this->resource = $resource; + $this->autoClose = $autoClose; + } + + /** + * Creates a stream + * + * @param mixed $resource + * + * @return StreamInterface + * + * @throws InvalidArgumentException + * If a stream cannot be created. + */ + public static function create($resource): StreamInterface + { + if ($resource instanceof StreamInterface) { + return $resource; + } + + return new self($resource); + } + + /** + * Destructor of the class + */ + public function __destruct() + { + if ($this->autoClose) { + $this->close(); + } + } + + /** + * Detaches a resource from the stream + * + * Returns NULL if the stream already without a resource. + * + * @return resource|null + */ + public function detach() + { + $resource = $this->resource; + $this->resource = null; + + return $resource; + } + + /** + * Closes the stream + * + * @link http://php.net/manual/en/function.fclose.php + * + * @return void + */ + public function close(): void + { + $resource = $this->detach(); + if (!is_resource($resource)) { + return; + } + + fclose($resource); + } + + /** + * Checks if the end of the stream is reached + * + * @link http://php.net/manual/en/function.feof.php + * + * @return bool + */ + public function eof(): bool + { + if (!is_resource($this->resource)) { + return true; + } + + return feof($this->resource); + } + + /** + * Gets the stream pointer position + * + * @link http://php.net/manual/en/function.ftell.php + * + * @return int + * + * @throws InvalidStreamException + * @throws FailedStreamOperationException + */ + public function tell(): int + { + if (!is_resource($this->resource)) { + throw InvalidStreamException::noResource(); + } + + $result = ftell($this->resource); + if ($result === false) { + throw new FailedStreamOperationException('Unable to get the stream pointer position'); + } + + return $result; + } + + /** + * Checks if the stream is seekable + * + * @return bool + */ + public function isSeekable(): bool + { + if (!is_resource($this->resource)) { + return false; + } + + /** @var array{seekable: bool} */ + $metadata = stream_get_meta_data($this->resource); + + return $metadata['seekable']; + } + + /** + * Moves the stream pointer to the beginning + * + * @return void + * + * @throws InvalidStreamException + * @throws InvalidStreamOperationException + * @throws FailedStreamOperationException + */ + public function rewind(): void + { + $this->seek(0); + } + + /** + * Moves the stream pointer to the given position + * + * @link http://php.net/manual/en/function.fseek.php + * + * @param int $offset + * @param int $whence + * + * @return void + * + * @throws InvalidStreamException + * @throws InvalidStreamOperationException + * @throws FailedStreamOperationException + */ + public function seek($offset, $whence = SEEK_SET): void + { + if (!is_resource($this->resource)) { + throw InvalidStreamException::noResource(); + } + + if (!$this->isSeekable()) { + throw new InvalidStreamOperationException('Stream is not seekable'); + } + + $result = fseek($this->resource, $offset, $whence); + if ($result !== 0) { + throw new FailedStreamOperationException('Unable to move the stream pointer position'); + } + } + + /** + * Checks if the stream is writable + * + * @return bool + */ + public function isWritable(): bool + { + if (!is_resource($this->resource)) { + return false; + } + + /** @var array{mode: string} */ + $metadata = stream_get_meta_data($this->resource); + + return strpbrk($metadata['mode'], '+acwx') !== false; + } + + /** + * Writes the given string to the stream + * + * Returns the number of bytes written to the stream. + * + * @link http://php.net/manual/en/function.fwrite.php + * + * @param string $string + * + * @return int + * + * @throws InvalidStreamException + * @throws InvalidStreamOperationException + * @throws FailedStreamOperationException + */ + public function write($string): int + { + if (!is_resource($this->resource)) { + throw InvalidStreamException::noResource(); + } + + if (!$this->isWritable()) { + throw new InvalidStreamOperationException('Stream is not writable'); + } + + $result = fwrite($this->resource, $string); + if ($result === false) { + throw new FailedStreamOperationException('Unable to write to the stream'); + } + + return $result; + } + + /** + * Checks if the stream is readable + * + * @return bool + */ + public function isReadable(): bool + { + if (!is_resource($this->resource)) { + return false; + } + + /** @var array{mode: string} */ + $metadata = stream_get_meta_data($this->resource); + + return strpbrk($metadata['mode'], '+r') !== false; + } + + /** + * Reads the given number of bytes from the stream + * + * @link http://php.net/manual/en/function.fread.php + * + * @param int $length + * + * @return string + * + * @throws InvalidStreamException + * @throws InvalidStreamOperationException + * @throws FailedStreamOperationException + */ + public function read($length): string + { + if (!is_resource($this->resource)) { + throw InvalidStreamException::noResource(); + } + + if (!$this->isReadable()) { + throw new InvalidStreamOperationException('Stream is not readable'); + } + + $result = fread($this->resource, $length); + if ($result === false) { + throw new FailedStreamOperationException('Unable to read from the stream'); + } + + return $result; + } + + /** + * Reads the remainder of the stream + * + * @link http://php.net/manual/en/function.stream-get-contents.php + * + * @return string + * + * @throws InvalidStreamException + * @throws InvalidStreamOperationException + * @throws FailedStreamOperationException + */ + public function getContents(): string + { + if (!is_resource($this->resource)) { + throw InvalidStreamException::noResource(); + } + + if (!$this->isReadable()) { + throw new InvalidStreamOperationException('Stream is not readable'); + } + + $result = stream_get_contents($this->resource); + if ($result === false) { + throw new FailedStreamOperationException('Unable to read the remainder of the stream'); + } + + return $result; + } + + /** + * Gets the stream metadata + * + * @link http://php.net/manual/en/function.stream-get-meta-data.php + * + * @param string|null $key + * + * @return mixed + */ + public function getMetadata($key = null) + { + if (!is_resource($this->resource)) { + return null; + } + + $metadata = stream_get_meta_data($this->resource); + if ($key === null) { + return $metadata; + } + + return $metadata[$key] ?? null; + } + + /** + * Gets the stream size + * + * Returns NULL if the stream without a resource, + * or if the stream size cannot be determined. + * + * @link http://php.net/manual/en/function.fstat.php + * + * @return int|null + */ + public function getSize(): ?int + { + if (!is_resource($this->resource)) { + return null; + } + + /** @var array{size: int}|false */ + $stats = fstat($this->resource); + if ($stats === false) { + return null; + } + + return $stats['size']; + } + + /** + * Converts the stream to a string + * + * @link http://php.net/manual/en/language.oop5.magic.php#object.tostring + * + * @return string + */ + public function __toString(): string + { + if (!$this->isReadable()) { + return ''; + } + + try { + if ($this->isSeekable()) { + $this->rewind(); + } + + return $this->getContents(); + } catch (Throwable $e) { + return ''; + } + } +} diff --git a/src/Stream/FileStream.php b/src/Stream/FileStream.php new file mode 100644 index 0000000..5e01e62 --- /dev/null +++ b/src/Stream/FileStream.php @@ -0,0 +1,77 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Stream; + +/** + * Import classes + */ +use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Exception\RuntimeException; +use Sunrise\Http\Message\Stream; +use Throwable; + +/** + * Import functions + */ +use function fopen; +use function is_resource; +use function sprintf; +use function sys_get_temp_dir; +use function tempnam; + +/** + * FileStream + */ +class FileStream extends Stream +{ + + /** + * Constructor of the class + * + * @param string $filename + * @param string $mode + * + * @throws RuntimeException + */ + public function __construct(string $filename, string $mode) + { + try { + $resource = fopen($filename, $mode); + } catch (Throwable $e) { + $resource = false; + } + + if (!is_resource($resource)) { + throw new RuntimeException(sprintf( + 'Unable to open the file "%s" in the mode "%s"', + $filename, + $mode + )); + } + + parent::__construct($resource); + } + + /** + * Creates a new temporary file in the temporary directory + * + * @return StreamInterface + * + * @throws RuntimeException + */ + public static function tempFile(): StreamInterface + { + $filename = tempnam(sys_get_temp_dir(), 'sunrisephp'); + + return new self($filename, 'w+b'); + } +} diff --git a/src/Stream/PhpInputStream.php b/src/Stream/PhpInputStream.php new file mode 100644 index 0000000..018213b --- /dev/null +++ b/src/Stream/PhpInputStream.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Stream; + +/** + * Import classes + */ +use Sunrise\Http\Message\Stream; + +/** + * Import functions + */ +use function fopen; +use function stream_copy_to_stream; + +/** + * @link https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input + */ +class PhpInputStream extends Stream +{ + + /** + * Constructor of the class + */ + public function __construct() + { + $input = fopen('php://input', 'rb'); + $resource = fopen('php://temp', 'r+b'); + + stream_copy_to_stream($input, $resource); + + parent::__construct($resource); + + $this->rewind(); + } +} diff --git a/src/Stream/PhpMemoryStream.php b/src/Stream/PhpMemoryStream.php new file mode 100644 index 0000000..0ae1b46 --- /dev/null +++ b/src/Stream/PhpMemoryStream.php @@ -0,0 +1,39 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Stream; + +/** + * Import classes + */ +use Sunrise\Http\Message\Stream; + +/** + * Import functions + */ +use function fopen; + +/** + * @link https://www.php.net/manual/en/wrappers.php.php#wrappers.php.memory + */ +class PhpMemoryStream extends Stream +{ + + /** + * Constructor of the class + * + * @param string $mode + */ + public function __construct(string $mode = 'r+b') + { + parent::__construct(fopen('php://memory', $mode)); + } +} diff --git a/src/Stream/PhpTempStream.php b/src/Stream/PhpTempStream.php new file mode 100644 index 0000000..d16b889 --- /dev/null +++ b/src/Stream/PhpTempStream.php @@ -0,0 +1,48 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Stream; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidArgumentException; +use Sunrise\Http\Message\Stream; + +/** + * Import functions + */ +use function fopen; +use function sprintf; + +/** + * @link https://www.php.net/manual/en/wrappers.php.php#wrappers.php.memory + */ +class PhpTempStream extends Stream +{ + + /** + * Constructor of the class + * + * @param string $mode + * @param int $maxmemory + * + * @throws InvalidArgumentException + */ + public function __construct(string $mode = 'r+b', int $maxmemory = 2097152) + { + if ($maxmemory < 0) { + throw new InvalidArgumentException('Argument #2 ($maxmemory) must be greater than or equal to 0'); + } + + parent::__construct(fopen(sprintf('php://temp/maxmemory:%d', $maxmemory), $mode)); + } +} diff --git a/src/Stream/TmpfileStream.php b/src/Stream/TmpfileStream.php new file mode 100644 index 0000000..b6e217b --- /dev/null +++ b/src/Stream/TmpfileStream.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Stream; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\RuntimeException; +use Sunrise\Http\Message\Stream; + +/** + * Import functions + */ +use function is_resource; +use function is_writable; +use function sys_get_temp_dir; +use function tmpfile; + +/** + * The tmpfile() function opens a unique temporary file in binary + * read/write (w+b) mode. The file will be automatically deleted + * when it is closed or the program terminates. + * + * @link https://www.php.net/tmpfile + */ +class TmpfileStream extends Stream +{ + + /** + * Constructor of the class + * + * @throws RuntimeException + */ + public function __construct() + { + $tmpdir = sys_get_temp_dir(); + if (!is_writable($tmpdir)) { + throw new RuntimeException('Temporary files directory is not writable'); + } + + $tmpfile = tmpfile(); + if (!is_resource($tmpfile)) { + throw new RuntimeException('Temporary file cannot be created or opened'); + } + + parent::__construct($tmpfile); + } +} diff --git a/src/StreamFactory.php b/src/StreamFactory.php new file mode 100644 index 0000000..bcfaf7d --- /dev/null +++ b/src/StreamFactory.php @@ -0,0 +1,61 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Stream\FileStream; +use Sunrise\Http\Message\Stream\PhpTempStream; + +/** + * StreamFactory + * + * @link https://www.php-fig.org/psr/psr-17/ + */ +class StreamFactory implements StreamFactoryInterface +{ + + /** + * {@inheritdoc} + */ + public function createStream(string $content = ''): StreamInterface + { + $stream = new PhpTempStream(); + if ($content === '') { + return $stream; + } + + $stream->write($content); + $stream->rewind(); + + return $stream; + } + + /** + * {@inheritdoc} + */ + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + return new FileStream($filename, $mode); + } + + /** + * {@inheritdoc} + */ + public function createStreamFromResource($resource): StreamInterface + { + return new Stream($resource); + } +} diff --git a/src/UploadedFile.php b/src/UploadedFile.php new file mode 100644 index 0000000..fedaaeb --- /dev/null +++ b/src/UploadedFile.php @@ -0,0 +1,292 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileInterface; +use Sunrise\Http\Message\Exception\FailedUploadedFileOperationException; +use Sunrise\Http\Message\Exception\InvalidUploadedFileException; +use Sunrise\Http\Message\Exception\InvalidUploadedFileOperationException; +use Sunrise\Http\Message\Stream\FileStream; + +/** + * Import functions + */ +use function dirname; +use function is_dir; +use function is_file; +use function is_writable; +use function sprintf; +use function unlink; + +/** + * Import constants + */ +use const UPLOAD_ERR_OK; +use const UPLOAD_ERR_INI_SIZE; +use const UPLOAD_ERR_FORM_SIZE; +use const UPLOAD_ERR_PARTIAL; +use const UPLOAD_ERR_NO_FILE; +use const UPLOAD_ERR_NO_TMP_DIR; +use const UPLOAD_ERR_CANT_WRITE; +use const UPLOAD_ERR_EXTENSION; + +/** + * UploadedFile + * + * @link https://www.php-fig.org/psr/psr-7/ + */ +class UploadedFile implements UploadedFileInterface +{ + + /** + * List of upload errors + * + * @link https://www.php.net/manual/en/features.file-upload.errors.php + * + * @var array + */ + public const UPLOAD_ERRORS = [ + UPLOAD_ERR_OK => 'No error', + 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 => 'File upload stopped by extension', + ]; + + /** + * Unknown error description + * + * @var string + */ + public const UNKNOWN_ERROR_TEXT = 'Unknown error'; + + /** + * The file stream + * + * @var StreamInterface|null + */ + private ?StreamInterface $stream = null; + + /** + * The file size + * + * @var int|null + */ + private ?int $size; + + /** + * The file's error code + * + * @var int + */ + private int $errorCode; + + /** + * The file's error message + * + * @var string + */ + private string $errorMessage; + + /** + * The file name + * + * @var string|null + */ + private ?string $clientFilename; + + /** + * The file type + * + * @var string|null + */ + private ?string $clientMediaType; + + /** + * Constructor of the class + * + * @param StreamInterface $stream + * @param int|null $size + * @param int $error + * @param string|null $clientFilename + * @param string|null $clientMediaType + */ + public function __construct( + StreamInterface $stream, + ?int $size = null, + int $error = UPLOAD_ERR_OK, + ?string $clientFilename = null, + ?string $clientMediaType = null + ) { + if (UPLOAD_ERR_OK === $error) { + $this->stream = $stream; + } + + $errorMessage = self::UPLOAD_ERRORS[$error] ?? self::UNKNOWN_ERROR_TEXT; + + $this->size = $size; + $this->errorCode = $error; + $this->errorMessage = $errorMessage; + $this->clientFilename = $clientFilename; + $this->clientMediaType = $clientMediaType; + } + + /** + * Gets the file stream + * + * @return StreamInterface + * + * @throws InvalidUploadedFileException + * If the file has no a stream due to an error or + * if the file was already moved. + */ + public function getStream(): StreamInterface + { + if (UPLOAD_ERR_OK <> $this->errorCode) { + throw new InvalidUploadedFileException(sprintf( + 'The uploaded file has no a stream due to the error #%d (%s)', + $this->errorCode, + $this->errorMessage + )); + } + + if (!isset($this->stream)) { + throw new InvalidUploadedFileException( + 'The uploaded file has no a stream because it was already moved' + ); + } + + return $this->stream; + } + + /** + * Moves the file to the given path + * + * @param string $targetPath + * + * @return void + * + * @throws InvalidUploadedFileException + * If the file has no a stream due to an error or + * if the file was already moved. + * + * @throws InvalidUploadedFileOperationException + * If the file cannot be read. + * + * @throws FailedUploadedFileOperationException + * If the target path cannot be used. + */ + public function moveTo($targetPath): void + { + if (UPLOAD_ERR_OK <> $this->errorCode) { + throw new InvalidUploadedFileException(sprintf( + 'The uploaded file cannot be moved due to the error #%d (%s)', + $this->errorCode, + $this->errorMessage + )); + } + + if (!isset($this->stream)) { + throw new InvalidUploadedFileException( + 'The uploaded file cannot be moved because it was already moved' + ); + } + + if (!$this->stream->isReadable()) { + throw new InvalidUploadedFileOperationException( + 'The uploaded file cannot be moved because it is not readable' + ); + } + + $targetDir = dirname($targetPath); + if (!is_dir($targetDir) || !is_writable($targetDir)) { + throw new FailedUploadedFileOperationException(sprintf( + 'The uploaded file cannot be moved because the directory "%s" is not writable', + $targetDir + )); + } + + $targetStream = new FileStream($targetPath, 'wb'); + + if ($this->stream->isSeekable()) { + $this->stream->rewind(); + } + + while (!$this->stream->eof()) { + $piece = $this->stream->read(4096); + $targetStream->write($piece); + } + + $targetStream->close(); + + /** @var string|null */ + $sourcePath = $this->stream->getMetadata('uri'); + + $this->stream->close(); + $this->stream = null; + + if (isset($sourcePath) && is_file($sourcePath)) { + $sourceDir = dirname($sourcePath); + if (is_writable($sourceDir)) { + unlink($sourcePath); + } + } + } + + /** + * Gets the file size + * + * @return int|null + */ + public function getSize(): ?int + { + return $this->size; + } + + /** + * Gets the file's error code + * + * @return int + */ + public function getError(): int + { + return $this->errorCode; + } + + /** + * Gets the file name + * + * @return string|null + */ + public function getClientFilename(): ?string + { + return $this->clientFilename; + } + + /** + * Gets the file type + * + * @return string|null + */ + public function getClientMediaType(): ?string + { + return $this->clientMediaType; + } +} diff --git a/src/UploadedFileFactory.php b/src/UploadedFileFactory.php new file mode 100644 index 0000000..6deae8e --- /dev/null +++ b/src/UploadedFileFactory.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileFactoryInterface; +use Psr\Http\Message\UploadedFileInterface; + +/** + * Import constants + */ +use const UPLOAD_ERR_OK; + +/** + * UploadedFileFactory + * + * @link https://www.php-fig.org/psr/psr-17/ + */ +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 { + return new UploadedFile( + $stream, + $size, + $error, + $clientFilename, + $clientMediaType + ); + } +} diff --git a/src/Uri.php b/src/Uri.php new file mode 100644 index 0000000..5059c37 --- /dev/null +++ b/src/Uri.php @@ -0,0 +1,534 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; +use Sunrise\Http\Message\Exception\InvalidUriComponentException; +use Sunrise\Http\Message\Exception\InvalidUriException; +use Sunrise\Http\Message\Uri\Component\Fragment; +use Sunrise\Http\Message\Uri\Component\Host; +use Sunrise\Http\Message\Uri\Component\Path; +use Sunrise\Http\Message\Uri\Component\Port; +use Sunrise\Http\Message\Uri\Component\Query; +use Sunrise\Http\Message\Uri\Component\Scheme; +use Sunrise\Http\Message\Uri\Component\UserInfo; + +/** + * Import functions + */ +use function is_string; +use function ltrim; +use function parse_url; +use function strncmp; + +/** + * Uniform Resource Identifier + * + * @link https://tools.ietf.org/html/rfc3986 + * @link https://www.php-fig.org/psr/psr-7/ + */ +class Uri implements UriInterface +{ + + /** + * Scheme of the URI + * + * @var string + */ + private string $scheme = ''; + + /** + * User Information of the URI + * + * @var string + */ + private string $userInfo = ''; + + /** + * Host of the URI + * + * @var string + */ + private string $host = ''; + + /** + * Port of the URI + * + * @var int|null + */ + private ?int $port = null; + + /** + * Path of the URI + * + * @var string + */ + private string $path = ''; + + /** + * Query of the URI + * + * @var string + */ + private string $query = ''; + + /** + * Fragment of the URI + * + * @var string + */ + private string $fragment = ''; + + /** + * Constructor of the class + * + * @param string $uri + * + * @throws InvalidUriException + * If the URI isn't valid. + */ + public function __construct(string $uri = '') + { + if ($uri === '') { + return; + } + + $components = parse_url($uri); + if ($components === false) { + throw new InvalidUriException('Unable to parse URI'); + } + + if (isset($components['scheme'])) { + $this->setScheme($components['scheme']); + } + + if (isset($components['user'])) { + $this->setUserInfo( + $components['user'], + $components['pass'] ?? null + ); + } + + if (isset($components['host'])) { + $this->setHost($components['host']); + } + + if (isset($components['port'])) { + $this->setPort($components['port']); + } + + if (isset($components['path'])) { + $this->setPath($components['path']); + } + + if (isset($components['query'])) { + $this->setQuery($components['query']); + } + + if (isset($components['fragment'])) { + $this->setFragment($components['fragment']); + } + } + + /** + * Creates a URI + * + * @param mixed $uri + * + * @return UriInterface + * + * @throws InvalidArgumentException + * If a URI cannot be created. + */ + public static function create($uri): UriInterface + { + if ($uri instanceof UriInterface) { + return $uri; + } + + if (!is_string($uri)) { + throw new InvalidArgumentException('URI should be a string'); + } + + return new self($uri); + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the scheme isn't valid. + */ + public function withScheme($scheme): UriInterface + { + $clone = clone $this; + $clone->setScheme($scheme); + + return $clone; + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the user information isn't valid. + */ + public function withUserInfo($user, $password = null): UriInterface + { + $clone = clone $this; + $clone->setUserInfo($user, $password); + + return $clone; + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the host isn't valid. + */ + public function withHost($host): UriInterface + { + $clone = clone $this; + $clone->setHost($host); + + return $clone; + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the port isn't valid. + */ + public function withPort($port): UriInterface + { + $clone = clone $this; + $clone->setPort($port); + + return $clone; + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the path isn't valid. + */ + public function withPath($path): UriInterface + { + $clone = clone $this; + $clone->setPath($path); + + return $clone; + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the query isn't valid. + */ + public function withQuery($query): UriInterface + { + $clone = clone $this; + $clone->setQuery($query); + + return $clone; + } + + /** + * {@inheritdoc} + * + * @throws InvalidUriComponentException + * If the fragment isn't valid. + */ + public function withFragment($fragment): UriInterface + { + $clone = clone $this; + $clone->setFragment($fragment); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getScheme(): string + { + return $this->scheme; + } + + /** + * {@inheritdoc} + */ + public function getUserInfo(): string + { + return $this->userInfo; + } + + /** + * {@inheritdoc} + */ + public function getHost(): string + { + return $this->host; + } + + /** + * {@inheritdoc} + */ + public function getPort(): ?int + { + // The 80 is the default port number for the HTTP protocol. + if ($this->port === 80 && $this->scheme === 'http') { + return null; + } + + // The 443 is the default port number for the HTTPS protocol. + if ($this->port === 443 && $this->scheme === 'https') { + return null; + } + + return $this->port; + } + + /** + * {@inheritdoc} + */ + public function getPath(): string + { + // CVE-2015-3257 + if (strncmp($this->path, '//', 2) === 0) { + return '/' . ltrim($this->path, '/'); + } + + return $this->path; + } + + /** + * {@inheritdoc} + */ + public function getQuery(): string + { + return $this->query; + } + + /** + * {@inheritdoc} + */ + public function getFragment(): string + { + return $this->fragment; + } + + /** + * {@inheritdoc} + */ + public function getAuthority(): string + { + // The host is the basic subcomponent. + if ($this->host === '') { + return ''; + } + + $authority = $this->host; + if ($this->userInfo !== '') { + $authority = $this->userInfo . '@' . $authority; + } + + $port = $this->getPort(); + if ($port !== null) { + $authority = $authority . ':' . $port; + } + + return $authority; + } + + /** + * {@inheritdoc} + */ + public function __toString(): string + { + $uri = ''; + + $scheme = $this->scheme; + if ($scheme !== '') { + $uri .= $scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== '') { + $uri .= '//' . $authority; + } + + $path = $this->path; + if ($path !== '') { + // https://github.com/sunrise-php/uri/issues/31 + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + // + // If a URI contains an authority component, + // then the path component must either be empty + // or begin with a slash ("/") character. + if ($authority !== '' && strncmp($path, '/', 1) !== 0) { + $path = '/' . $path; + } + + // https://github.com/sunrise-php/uri/issues/31 + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + // + // If a URI does not contain an authority component, + // then the path cannot begin with two slash characters ("//"). + if ($authority === '' && strncmp($path, '//', 2) === 0) { + $path = '/' . ltrim($path, '/'); + } + + $uri .= $path; + } + + $query = $this->query; + if ($query !== '') { + $uri .= '?' . $query; + } + + $fragment = $this->fragment; + if ($fragment !== '') { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Sets the given scheme to the URI + * + * @param mixed $scheme + * + * @return void + * + * @throws InvalidUriComponentException + * If the scheme isn't valid. + */ + final protected function setScheme($scheme): void + { + $component = new Scheme($scheme); + + $this->scheme = $component->getValue(); + } + + /** + * Sets the given user information to the URI + * + * @param mixed $user + * @param mixed $password + * + * @return void + * + * @throws InvalidUriComponentException + * If the user information isn't valid. + */ + final protected function setUserInfo($user, $password): void + { + $component = new UserInfo($user, $password); + + $this->userInfo = $component->getValue(); + } + + /** + * Sets the given host to the URI + * + * @param mixed $host + * + * @return void + * + * @throws InvalidUriComponentException + * If the host isn't valid. + */ + final protected function setHost($host): void + { + $component = new Host($host); + + $this->host = $component->getValue(); + } + + /** + * Sets the given port to the URI + * + * @param mixed $port + * + * @return void + * + * @throws InvalidUriComponentException + * If the port isn't valid. + */ + final protected function setPort($port): void + { + $component = new Port($port); + + $this->port = $component->getValue(); + } + + /** + * Sets the given path to the URI + * + * @param mixed $path + * + * @return void + * + * @throws InvalidUriComponentException + * If the path isn't valid. + */ + final protected function setPath($path): void + { + $component = new Path($path); + + $this->path = $component->getValue(); + } + + /** + * Sets the given query to the URI + * + * @param mixed $query + * + * @return void + * + * @throws InvalidUriComponentException + * If the query isn't valid. + */ + final protected function setQuery($query): void + { + $component = new Query($query); + + $this->query = $component->getValue(); + } + + /** + * Sets the given fragment to the URI + * + * @param mixed $fragment + * + * @return void + * + * @throws InvalidUriComponentException + * If the fragment isn't valid. + */ + final protected function setFragment($fragment): void + { + $component = new Fragment($fragment); + + $this->fragment = $component->getValue(); + } +} diff --git a/src/Uri/Component/ComponentInterface.php b/src/Uri/Component/ComponentInterface.php new file mode 100644 index 0000000..0060522 --- /dev/null +++ b/src/Uri/Component/ComponentInterface.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * ComponentInterface + */ +interface ComponentInterface +{ + + /** + * Gets the component value + * + * @return mixed + */ + public function getValue(); +} diff --git a/src/Uri/Component/Fragment.php b/src/Uri/Component/Fragment.php new file mode 100644 index 0000000..8c89e70 --- /dev/null +++ b/src/Uri/Component/Fragment.php @@ -0,0 +1,82 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_replace_callback; +use function rawurlencode; + +/** + * URI component "fragment" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.5 + */ +final class Fragment implements ComponentInterface +{ + + /** + * Regular expression to normalize the component value + * + * @var string + */ + private const NORMALIZE_REGEX = '/(?:(?:%[0-9A-Fa-f]{2}|[0-9A-Za-z\-\._~\!\$&\'\(\)\*\+,;\=\:@\/\?]+)|(.?))/u'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "fragment" must be a string'); + } + + $this->value = preg_replace_callback(self::NORMALIZE_REGEX, function (array $match): string { + /** @var array{0: string, 1?: string} $match */ + + return isset($match[1]) ? rawurlencode($match[1]) : $match[0]; + }, $value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/Host.php b/src/Uri/Component/Host.php new file mode 100644 index 0000000..c5b8d69 --- /dev/null +++ b/src/Uri/Component/Host.php @@ -0,0 +1,86 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_replace_callback; +use function rawurlencode; +use function strtolower; + +/** + * URI component "host" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 + */ +final class Host implements ComponentInterface +{ + + /** + * Regular expression to normalize the component value + * + * @var string + */ + private const NORMALIZE_REGEX = '/(?:(?:%[0-9A-Fa-f]{2}|[0-9A-Za-z\-\._~\!\$&\'\(\)\*\+,;\=]+)|(.?))/u'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "host" must be a string'); + } + + $this->value = preg_replace_callback(self::NORMALIZE_REGEX, function (array $match): string { + /** @var array{0: string, 1?: string} $match */ + + return isset($match[1]) ? rawurlencode($match[1]) : $match[0]; + }, $value); + + // the component is case-insensitive... + $this->value = strtolower($this->value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/Password.php b/src/Uri/Component/Password.php new file mode 100644 index 0000000..11be4ea --- /dev/null +++ b/src/Uri/Component/Password.php @@ -0,0 +1,82 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_replace_callback; +use function rawurlencode; + +/** + * URI component "password" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.1 + */ +final class Password implements ComponentInterface +{ + + /** + * Regular expression to normalize the component value + * + * @var string + */ + private const NORMALIZE_REGEX = '/(?:(?:%[0-9A-Fa-f]{2}|[0-9A-Za-z\-\._~\!\$&\'\(\)\*\+,;\=]+)|(.?))/u'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "password" must be a string'); + } + + $this->value = preg_replace_callback(self::NORMALIZE_REGEX, function (array $match): string { + /** @var array{0: string, 1?: string} $match */ + + return isset($match[1]) ? rawurlencode($match[1]) : $match[0]; + }, $value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/Path.php b/src/Uri/Component/Path.php new file mode 100644 index 0000000..5bafae1 --- /dev/null +++ b/src/Uri/Component/Path.php @@ -0,0 +1,82 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_replace_callback; +use function rawurlencode; + +/** + * URI component "path" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.3 + */ +final class Path implements ComponentInterface +{ + + /** + * Regular expression to normalize the component value + * + * @var string + */ + private const NORMALIZE_REGEX = '/(?:(?:%[0-9A-Fa-f]{2}|[0-9A-Za-z\-\._~\!\$&\'\(\)\*\+,;\=\:@\/]+)|(.?))/u'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "path" must be a string'); + } + + $this->value = preg_replace_callback(self::NORMALIZE_REGEX, function (array $match): string { + /** @var array{0: string, 1?: string} $match */ + + return isset($match[1]) ? rawurlencode($match[1]) : $match[0]; + }, $value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/Port.php b/src/Uri/Component/Port.php new file mode 100644 index 0000000..d46e981 --- /dev/null +++ b/src/Uri/Component/Port.php @@ -0,0 +1,76 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_int; + +/** + * URI component "port" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.3 + */ +final class Port implements ComponentInterface +{ + + /** + * The component value + * + * @var int|null + */ + private ?int $value = null; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + $min = 1; + $max = (2 ** 16) - 1; + + if ($value === null) { + return; + } + + if (!is_int($value)) { + throw new InvalidUriComponentException('URI component "port" must be an integer'); + } + + if (!($value >= $min && $value <= $max)) { + throw new InvalidUriComponentException('Invalid URI component "port"'); + } + + $this->value = $value; + } + + /** + * {@inheritdoc} + * + * @return int|null + */ + public function getValue(): ?int + { + return $this->value; + } +} diff --git a/src/Uri/Component/Query.php b/src/Uri/Component/Query.php new file mode 100644 index 0000000..09aeaf3 --- /dev/null +++ b/src/Uri/Component/Query.php @@ -0,0 +1,82 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_replace_callback; +use function rawurlencode; + +/** + * URI component "query" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.4 + */ +final class Query implements ComponentInterface +{ + + /** + * Regular expression to normalize the component value + * + * @var string + */ + private const NORMALIZE_REGEX = '/(?:(?:%[0-9A-Fa-f]{2}|[0-9A-Za-z\-\._~\!\$&\'\(\)\*\+,;\=\:@\/\?]+)|(.?))/u'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "query" must be a string'); + } + + $this->value = preg_replace_callback(self::NORMALIZE_REGEX, function (array $match): string { + /** @var array{0: string, 1?: string} $match */ + + return isset($match[1]) ? rawurlencode($match[1]) : $match[0]; + }, $value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/Scheme.php b/src/Uri/Component/Scheme.php new file mode 100644 index 0000000..fb4a24c --- /dev/null +++ b/src/Uri/Component/Scheme.php @@ -0,0 +1,83 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_match; +use function strtolower; + +/** + * URI component "scheme" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.1 + */ +final class Scheme implements ComponentInterface +{ + + /** + * Regular expression to validate the component value + * + * @var string + */ + private const VALIDATE_REGEX = '/^(?:[A-Za-z][0-9A-Za-z\+\-\.]*)?$/'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "scheme" must be a string'); + } + + if (!preg_match(self::VALIDATE_REGEX, $value)) { + throw new InvalidUriComponentException('Invalid URI component "scheme"'); + } + + // the component is case-insensitive... + $this->value = strtolower($value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/User.php b/src/Uri/Component/User.php new file mode 100644 index 0000000..9d84249 --- /dev/null +++ b/src/Uri/Component/User.php @@ -0,0 +1,82 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * Import classes + */ +use Sunrise\Http\Message\Exception\InvalidUriComponentException; + +/** + * Import functions + */ +use function is_string; +use function preg_replace_callback; +use function rawurlencode; + +/** + * URI component "user" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.1 + */ +final class User implements ComponentInterface +{ + + /** + * Regular expression to normalize the component value + * + * @var string + */ + private const NORMALIZE_REGEX = '/(?:(?:%[0-9A-Fa-f]{2}|[0-9A-Za-z\-\._~\!\$&\'\(\)\*\+,;\=]+)|(.?))/u'; + + /** + * The component value + * + * @var string + */ + private string $value = ''; + + /** + * Constructor of the class + * + * @param mixed $value + * + * @throws InvalidUriComponentException + * If the component isn't valid. + */ + public function __construct($value) + { + if ($value === '') { + return; + } + + if (!is_string($value)) { + throw new InvalidUriComponentException('URI component "user" must be a string'); + } + + $this->value = preg_replace_callback(self::NORMALIZE_REGEX, function (array $match): string { + /** @var array{0: string, 1?: string} $match */ + + return isset($match[1]) ? rawurlencode($match[1]) : $match[0]; + }, $value); + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/src/Uri/Component/UserInfo.php b/src/Uri/Component/UserInfo.php new file mode 100644 index 0000000..1bf6e4d --- /dev/null +++ b/src/Uri/Component/UserInfo.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message\Uri\Component; + +/** + * URI component "User Information" + * + * @link https://tools.ietf.org/html/rfc3986#section-3.2.1 + */ +final class UserInfo implements ComponentInterface +{ + + /** + * URI component "user" + * + * @var User + */ + private User $user; + + /** + * URI component "password" + * + * @var Password|null + */ + private ?Password $password = null; + + /** + * Constructor of the class + * + * @param mixed $user + * @param mixed $password + */ + public function __construct($user, $password = null) + { + $this->user = $user instanceof User ? $user : new User($user); + + if (isset($password)) { + $this->password = $password instanceof Password ? $password : new Password($password); + } + } + + /** + * {@inheritdoc} + * + * @return string + */ + public function getValue(): string + { + $value = $this->user->getValue(); + + if (isset($this->password)) { + $value .= ':' . $this->password->getValue(); + } + + return $value; + } +} diff --git a/src/UriFactory.php b/src/UriFactory.php new file mode 100644 index 0000000..f663d40 --- /dev/null +++ b/src/UriFactory.php @@ -0,0 +1,35 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\UriFactoryInterface; +use Psr\Http\Message\UriInterface; + +/** + * UriFactory + * + * @link https://www.php-fig.org/psr/psr-17/ + */ +class UriFactory implements UriFactoryInterface +{ + + /** + * {@inheritdoc} + */ + public function createUri(string $uri = ''): UriInterface + { + return new Uri($uri); + } +} From 85ebe4b54c44f00162c41f59889c58bfa721926e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:18:43 +0100 Subject: [PATCH 02/16] improved tests --- tests/BaseMessageTest.php | 904 ++++++++++++++++++ tests/BaseRequestTest.php | 409 ++++++++ tests/BaseResponseTest.php | 246 +++++ tests/BaseServerRequestTest.php | 361 +++++++ ...ccessControlAllowCredentialsHeaderTest.php | 55 ++ .../AccessControlAllowHeadersHeaderTest.php | 96 ++ .../AccessControlAllowMethodsHeaderTest.php | 103 ++ .../AccessControlAllowOriginHeaderTest.php | 106 ++ .../AccessControlExposeHeadersHeaderTest.php | 96 ++ .../Header/AccessControlMaxAgeHeaderTest.php | 90 ++ tests/Header/AgeHeaderTest.php | 90 ++ tests/Header/AllowHeaderTest.php | 103 ++ tests/Header/CacheControlHeaderTest.php | 147 +++ tests/Header/ClearSiteDataHeaderTest.php | 94 ++ tests/Header/ConnectionHeaderTest.php | 78 ++ tests/Header/ContentDispositionHeaderTest.php | 166 ++++ tests/Header/ContentEncodingHeaderTest.php | 104 ++ tests/Header/ContentLanguageHeaderTest.php | 114 +++ tests/Header/ContentLengthHeaderTest.php | 63 ++ tests/Header/ContentLocationHeaderTest.php | 61 ++ tests/Header/ContentMD5HeaderTest.php | 79 ++ tests/Header/ContentRangeHeaderTest.php | 91 ++ .../ContentSecurityPolicyHeaderTest.php | 137 +++ ...tentSecurityPolicyReportOnlyHeaderTest.php | 137 +++ tests/Header/ContentTypeHeaderTest.php | 168 ++++ tests/Header/CookieHeaderTest.php | 61 ++ tests/Header/DateHeaderTest.php | 82 ++ tests/Header/EtagHeaderTest.php | 71 ++ tests/Header/ExpiresHeaderTest.php | 82 ++ tests/Header/KeepAliveHeaderTest.php | 149 +++ tests/Header/LastModifiedHeaderTest.php | 82 ++ tests/Header/LinkHeaderTest.php | 174 ++++ tests/Header/LocationHeaderTest.php | 61 ++ tests/Header/RefreshHeaderTest.php | 71 ++ tests/Header/RetryAfterHeaderTest.php | 82 ++ tests/Header/SetCookieHeaderTest.php | 257 +++++ tests/Header/SunsetHeaderTest.php | 82 ++ tests/Header/TrailerHeaderTest.php | 78 ++ tests/Header/TransferEncodingHeaderTest.php | 104 ++ tests/Header/VaryHeaderTest.php | 96 ++ tests/Header/WWWAuthenticateHeaderTest.php | 180 ++++ tests/Header/WarningHeaderTest.php | 134 +++ tests/Integration/RequestIntegrationTest.php | 21 + tests/Integration/ResponseIntegrationTest.php | 21 + .../ServerRequestIntegrationTest.php | 33 + tests/Integration/StreamIntegrationTest.php | 32 + .../UploadedFileIntegrationTest.php | 22 + tests/Integration/UriIntegrationTest.php | 21 + tests/MessageTest.php | 509 ---------- tests/RequestFactoryTest.php | 103 +- tests/RequestTest.php | 371 +------ tests/Response/HtmlResponseTest.php | 54 ++ tests/Response/JsonResponseTest.php | 53 + tests/ResponseFactoryTest.php | 129 +-- tests/ResponseTest.php | 162 +--- tests/ServerRequestFactoryTest.php | 571 +++++++++++ tests/ServerRequestTest.php | 72 ++ tests/StreamFactoryTest.php | 81 ++ tests/StreamTest.php | 515 ++++++++++ tests/UploadedFileFactoryTest.php | 46 + tests/UploadedFileTest.php | 185 ++++ tests/UriFactoryTest.php | 40 + tests/UriTest.php | 676 +++++++++++++ 63 files changed, 8517 insertions(+), 1144 deletions(-) create mode 100644 tests/BaseMessageTest.php create mode 100644 tests/BaseRequestTest.php create mode 100644 tests/BaseResponseTest.php create mode 100644 tests/BaseServerRequestTest.php create mode 100644 tests/Header/AccessControlAllowCredentialsHeaderTest.php create mode 100644 tests/Header/AccessControlAllowHeadersHeaderTest.php create mode 100644 tests/Header/AccessControlAllowMethodsHeaderTest.php create mode 100644 tests/Header/AccessControlAllowOriginHeaderTest.php create mode 100644 tests/Header/AccessControlExposeHeadersHeaderTest.php create mode 100644 tests/Header/AccessControlMaxAgeHeaderTest.php create mode 100644 tests/Header/AgeHeaderTest.php create mode 100644 tests/Header/AllowHeaderTest.php create mode 100644 tests/Header/CacheControlHeaderTest.php create mode 100644 tests/Header/ClearSiteDataHeaderTest.php create mode 100644 tests/Header/ConnectionHeaderTest.php create mode 100644 tests/Header/ContentDispositionHeaderTest.php create mode 100644 tests/Header/ContentEncodingHeaderTest.php create mode 100644 tests/Header/ContentLanguageHeaderTest.php create mode 100644 tests/Header/ContentLengthHeaderTest.php create mode 100644 tests/Header/ContentLocationHeaderTest.php create mode 100644 tests/Header/ContentMD5HeaderTest.php create mode 100644 tests/Header/ContentRangeHeaderTest.php create mode 100644 tests/Header/ContentSecurityPolicyHeaderTest.php create mode 100644 tests/Header/ContentSecurityPolicyReportOnlyHeaderTest.php create mode 100644 tests/Header/ContentTypeHeaderTest.php create mode 100644 tests/Header/CookieHeaderTest.php create mode 100644 tests/Header/DateHeaderTest.php create mode 100644 tests/Header/EtagHeaderTest.php create mode 100644 tests/Header/ExpiresHeaderTest.php create mode 100644 tests/Header/KeepAliveHeaderTest.php create mode 100644 tests/Header/LastModifiedHeaderTest.php create mode 100644 tests/Header/LinkHeaderTest.php create mode 100644 tests/Header/LocationHeaderTest.php create mode 100644 tests/Header/RefreshHeaderTest.php create mode 100644 tests/Header/RetryAfterHeaderTest.php create mode 100644 tests/Header/SetCookieHeaderTest.php create mode 100644 tests/Header/SunsetHeaderTest.php create mode 100644 tests/Header/TrailerHeaderTest.php create mode 100644 tests/Header/TransferEncodingHeaderTest.php create mode 100644 tests/Header/VaryHeaderTest.php create mode 100644 tests/Header/WWWAuthenticateHeaderTest.php create mode 100644 tests/Header/WarningHeaderTest.php create mode 100644 tests/Integration/RequestIntegrationTest.php create mode 100644 tests/Integration/ResponseIntegrationTest.php create mode 100644 tests/Integration/ServerRequestIntegrationTest.php create mode 100644 tests/Integration/StreamIntegrationTest.php create mode 100644 tests/Integration/UploadedFileIntegrationTest.php create mode 100644 tests/Integration/UriIntegrationTest.php delete mode 100644 tests/MessageTest.php create mode 100644 tests/Response/HtmlResponseTest.php create mode 100644 tests/Response/JsonResponseTest.php create mode 100644 tests/ServerRequestFactoryTest.php create mode 100644 tests/ServerRequestTest.php create mode 100644 tests/StreamFactoryTest.php create mode 100644 tests/StreamTest.php create mode 100644 tests/UploadedFileFactoryTest.php create mode 100644 tests/UploadedFileTest.php create mode 100644 tests/UriFactoryTest.php create mode 100644 tests/UriTest.php diff --git a/tests/BaseMessageTest.php b/tests/BaseMessageTest.php new file mode 100644 index 0000000..2b75e1f --- /dev/null +++ b/tests/BaseMessageTest.php @@ -0,0 +1,904 @@ +createSubject(); + + $this->assertSame('1.1', $subject->getProtocolVersion()); + } + + public function testDefaultHeaders(): void + { + $subject = $this->createSubject(); + + $this->assertSame([], $subject->getHeaders()); + } + + public function testDefaultBody(): void + { + $subject = $this->createSubject(); + $body = $subject->getBody(); + + $this->assertSame('php://temp/maxmemory:2097152', $body->getMetadata('uri')); + $this->assertTrue($body->isSeekable()); + $this->assertTrue($body->isReadable()); + $this->assertTrue($body->isWritable()); + $this->assertSame(0, $body->getSize()); + } + + /** + * @dataProvider protocolVersionProvider + */ + public function testSetProtocolVersion($protocolVersion): void + { + $subject = $this->createSubject(); + $clone = $subject->withProtocolVersion($protocolVersion); + + $this->assertNotSame($clone, $subject); + $this->assertSame($protocolVersion, $clone->getProtocolVersion()); + $this->assertSame('1.1', $subject->getProtocolVersion()); + } + + public function testSetProtocolVersionAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or unsupported HTTP version'); + + $this->createSubject()->withProtocolVersion(null); + } + + public function testSetProtocolVersionAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or unsupported HTTP version'); + + $this->createSubject()->withProtocolVersion(1.1); + } + + /** + * @dataProvider invalidProtocolVersionProvider + */ + public function testSetInvalidProtocolVersion($protocolVersion): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or unsupported HTTP version'); + + $this->createSubject()->withProtocolVersion($protocolVersion); + } + + public function testSetHeader(): void + { + $subject = $this->createSubject(); + $clone = $subject->withHeader('X-Foo', 'bar'); + + $this->assertNotSame($clone, $subject); + + $this->assertSame([ + 'X-Foo' => ['bar'], + ], $clone->getHeaders()); + + $this->assertSame([], $subject->getHeaders()); + } + + public function testSetHeaderWithSeveralValues(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', ['bar', 'baz']); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + ], $subject->getHeaders()); + } + + public function testSetSeveralHeaders(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', 'bar') + ->withHeader('X-Bar', 'baz'); + + $this->assertSame([ + 'X-Foo' => ['bar'], + 'X-Bar' => ['baz'], + ], $subject->getHeaders()); + } + + public function testSetSeveralHeadersWithSeveralValues(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', ['bar', 'baz']) + ->withHeader('X-Bar', ['baz', 'bat']); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + 'X-Bar' => ['baz', 'bat'], + ], $subject->getHeaders()); + } + + public function testSetHeaderWithEmptyValue(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', ''); + + $this->assertSame([ + 'X-Foo' => [''], + ], $subject->getHeaders()); + } + + public function testSetHeaderWithEmptyName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name cannot be an empty'); + + $this->createSubject()->withHeader('', 'bar'); + } + + public function testSetHeaderWithNameAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name must be a string'); + + $this->createSubject()->withHeader(null, 'foo'); + } + + public function testSetHeaderWithNameAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name must be a string'); + + $this->createSubject()->withHeader(42, 'foo'); + } + + public function testSetHeaderWithInvalidName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name is invalid'); + + $this->createSubject()->withHeader('X-Foo:', 'bar'); + } + + public function testSetHeaderWithValueAsEmptyArray(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo" HTTP header value cannot be an empty array'); + + $this->createSubject()->withHeader('X-Foo', []); + } + + public function testSetHeaderWithValueAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value must be a string'); + + $this->createSubject()->withHeader('X-Foo', null); + } + + public function testSetHeaderWithValueAsNullAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value must be a string'); + + $this->createSubject()->withHeader('X-Foo', ['bar', null, 'baz']); + } + + public function testSetHeaderWithValueAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value must be a string'); + + $this->createSubject()->withHeader('X-Foo', 42); + } + + public function testSetHeaderWithValueAsNumberAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value must be a string'); + + $this->createSubject()->withHeader('X-Foo', ['bar', 42, 'baz']); + } + + public function testSetHeaderWithInvalidValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value is invalid'); + + $this->createSubject()->withHeader('X-Foo', "\0"); + } + + public function testSetHeaderWithInvalidValueAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value is invalid'); + + $this->createSubject()->withHeader('X-Foo', ['bar', "\0", 'baz']); + } + + public function testAddHeader(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + $clone = $subject->withAddedHeader('X-Foo', 'baz'); + + $this->assertNotSame($clone, $subject); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + ], $clone->getHeaders()); + + $this->assertSame([ + 'X-Foo' => ['bar'], + ], $subject->getHeaders()); + } + + public function testAddHeaderWithSeveralValues(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', 'bar') + ->withAddedHeader('X-Foo', ['baz', 'bat']); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz', 'bat'], + ], $subject->getHeaders()); + } + + public function testAddSeveralHeaders(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', 'bar') + ->withAddedHeader('X-Foo', 'baz') + ->withAddedHeader('X-Foo', 'bat'); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz', 'bat'], + ], $subject->getHeaders()); + } + + public function testAddSeveralHeadersWithSeveralValues(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', 'bar') + ->withAddedHeader('X-Foo', ['baz', 'bat']) + ->withAddedHeader('X-Foo', ['qux', 'qaz']); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz', 'bat', 'qux', 'qaz'], + ], $subject->getHeaders()); + } + + public function testAddHeaderWithEmptyValue(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + $clone = $subject->withAddedHeader('X-Foo', ''); + + $this->assertSame([ + 'X-Foo' => ['bar', ''], + ], $clone->getHeaders()); + + $this->assertSame([ + 'X-Foo' => ['bar'], + ], $subject->getHeaders()); + } + + public function testAddHeaderWithEmptyName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name cannot be an empty'); + + $this->createSubject()->withAddedHeader('', 'bar'); + } + + public function testAddHeaderWithNameAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name must be a string'); + + $this->createSubject()->withAddedHeader(null, 'foo'); + } + + public function testAddHeaderWithNameAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name must be a string'); + + $this->createSubject()->withAddedHeader(42, 'foo'); + } + + public function testAddHeaderWithInvalidName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name is invalid'); + + $this->createSubject()->withAddedHeader('X-Foo:', 'bar'); + } + + public function testAddHeaderWithValueAsEmptyArray(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo" HTTP header value cannot be an empty array'); + + $this->createSubject()->withAddedHeader('X-Foo', []); + } + + public function testAddHeaderWithValueAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value must be a string'); + + $this->createSubject()->withAddedHeader('X-Foo', null); + } + + public function testAddHeaderWithValueAsNullAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value must be a string'); + + $this->createSubject()->withAddedHeader('X-Foo', ['bar', null, 'baz']); + } + + public function testAddHeaderWithValueAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value must be a string'); + + $this->createSubject()->withAddedHeader('X-Foo', 42); + } + + public function testAddHeaderWithValueAsNumberAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value must be a string'); + + $this->createSubject()->withAddedHeader('X-Foo', ['bar', 42, 'baz']); + } + + public function testAddHeaderWithInvalidValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value is invalid'); + + $this->createSubject()->withAddedHeader('X-Foo', "\0"); + } + + public function testAddHeaderWithInvalidValueAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value is invalid'); + + $this->createSubject()->withAddedHeader('X-Foo', ['bar', "\0", 'baz']); + } + + public function testAddNewHeader(): void + { + $subject = $this->createSubject(); + $clone = $subject->withAddedHeader('X-Foo', 'bar'); + + $this->assertSame([ + 'X-Foo' => ['bar'], + ], $clone->getHeaders()); + + $this->assertSame([], $subject->getHeaders()); + } + + public function testAddNewHeaderWithSeveralValues(): void + { + $subject = $this->createSubject() + ->withAddedHeader('X-Foo', ['bar', 'baz']); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + ], $subject->getHeaders()); + } + + public function testAddNewSeveralHeaders(): void + { + $subject = $this->createSubject() + ->withAddedHeader('X-Foo', 'bar') + ->withAddedHeader('X-Foo', 'baz'); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + ], $subject->getHeaders()); + } + + public function testAddNewSeveralHeadersWithSeveralValues(): void + { + $subject = $this->createSubject() + ->withAddedHeader('X-Foo', ['bar', 'baz']) + ->withAddedHeader('X-Foo', ['bat', 'qux']); + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz', 'bat', 'qux'], + ], $subject->getHeaders()); + } + + public function testAddNewHeaderWithEmptyValue(): void + { + $subject = $this->createSubject(); + $clone = $subject->withAddedHeader('X-Foo', ''); + + $this->assertSame([ + 'X-Foo' => [''], + ], $clone->getHeaders()); + + $this->assertSame([], $subject->getHeaders()); + } + + public function testAddHeaderCaseInsensitive(): void + { + $subject = $this->createSubject()->withHeader('x-foo', 'bar'); + $clone = $subject->withAddedHeader('X-Foo', 'baz'); + + $this->assertSame([ + 'x-foo' => ['bar', 'baz'], + ], $clone->getHeaders()); + + $this->assertSame([ + 'x-foo' => ['bar'], + ], $subject->getHeaders()); + } + + public function testAddHeaderWithSeveralValuesCaseInsensitive(): void + { + $subject = $this->createSubject() + ->withHeader('x-foo', 'bar') + ->withAddedHeader('X-Foo', ['baz', 'bat']); + + $this->assertSame([ + 'x-foo' => ['bar', 'baz', 'bat'], + ], $subject->getHeaders()); + } + + public function testAddSeveralHeadersCaseInsensitive(): void + { + $subject = $this->createSubject() + ->withHeader('x-foo', 'bar') + ->withAddedHeader('X-Foo', 'baz') + ->withAddedHeader('X-FOO', 'bat'); + + $this->assertSame([ + 'x-foo' => ['bar', 'baz', 'bat'], + ], $subject->getHeaders()); + } + + public function testAddSeveralHeadersWithSeveralValuesCaseInsensitive(): void + { + $subject = $this->createSubject() + ->withHeader('x-foo', 'bar') + ->withAddedHeader('X-Foo', ['baz', 'bat']) + ->withAddedHeader('X-Foo', ['qux', 'qaz']); + + $this->assertSame([ + 'x-foo' => ['bar', 'baz', 'bat', 'qux', 'qaz'], + ], $subject->getHeaders()); + } + + public function testReplaceHeader(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', 'bar') + ->withHeader('X-Foo', 'baz'); + + $this->assertSame([ + 'X-Foo' => ['baz'], + ], $subject->getHeaders()); + } + + public function testReplaceHeaderCaseInsensitive(): void + { + $subject = $this->createSubject() + ->withHeader('X-Foo', 'bar') + ->withHeader('x-foo', 'baz'); + + $this->assertSame([ + 'x-foo' => ['baz'], + ], $subject->getHeaders()); + } + + public function testDeleteHeader(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + $clone = $subject->withoutHeader('X-Foo'); + + $this->assertNotSame($clone, $subject); + + $this->assertSame([], $clone->getHeaders()); + + $this->assertSame([ + 'X-Foo' => ['bar'], + ], $subject->getHeaders()); + } + + public function testDeleteHeaderCaseInsensitive(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + $clone = $subject->withoutHeader('x-foo'); + + $this->assertSame([], $clone->getHeaders()); + + $this->assertSame([ + 'X-Foo' => ['bar'], + ], $subject->getHeaders()); + } + + public function testHasHeader(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + + $this->assertTrue($subject->hasHeader('X-Foo')); + $this->assertFalse($subject->hasHeader('X-Bar')); + } + + public function testHasHeaderCaseInsensitive(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + + $this->assertTrue($subject->hasHeader('x-foo')); + $this->assertTrue($subject->hasHeader('X-FOO')); + } + + public function testGetHeader(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + + $this->assertSame(['bar'], $subject->getHeader('X-Foo')); + $this->assertSame([], $subject->getHeader('X-Bar')); + } + + public function testGetHeaderCaseInsensitive(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + + $this->assertSame(['bar'], $subject->getHeader('x-foo')); + $this->assertSame(['bar'], $subject->getHeader('X-FOO')); + } + + public function testGetHeaderWithSeveralValues(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', ['bar', 'baz', 'bat']); + + $this->assertSame(['bar', 'baz', 'bat'], $subject->getHeader('X-Foo')); + } + + public function testGetHeaderLine(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + + $this->assertSame('bar', $subject->getHeaderLine('X-Foo')); + $this->assertSame('', $subject->getHeaderLine('X-Bar')); + } + + public function testGetHeaderLineCaseInsensitive(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', 'bar'); + + $this->assertSame('bar', $subject->getHeaderLine('x-foo')); + $this->assertSame('bar', $subject->getHeaderLine('X-FOO')); + } + + public function testGetHeaderLineWithSeveralValues(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', ['bar', 'baz']); + + $this->assertSame('bar,baz', $subject->getHeaderLine('X-Foo')); + } + + public function testGetHeaderLineWithEmptyValue(): void + { + $subject = $this->createSubject()->withHeader('X-Foo', ''); + + $this->assertSame('', $subject->getHeaderLine('X-Foo')); + } + + public function testSetBody(): void + { + $body = $this->createMock(StreamInterface::class); + $subject = $this->createSubject(); + $clone = $subject->withBody($body); + + $this->assertNotSame($clone, $subject); + $this->assertSame($body, $clone->getBody()); + $this->assertNotSame($body, $subject->getBody()); + } + + /** + * @dataProvider protocolVersionProvider + */ + public function testConstructorWithProtocolVersion(string $protocolVersion): void + { + $subject = $this->createSubjectWithProtocolVersion($protocolVersion); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame($protocolVersion, $subject->getProtocolVersion()); + } + + /** + * @dataProvider invalidProtocolVersionProvider + */ + public function testConstructorWithInvalidProtocolVersion(string $protocolVersion): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or unsupported HTTP version'); + + $subject = $this->createSubjectWithProtocolVersion($protocolVersion); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeaders(): void + { + $subject = $this->createSubjectWithHeaders(['X-Foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['X-Foo' => ['bar']], $subject->getHeaders()); + } + + public function testConstructorWithHeadersWithSeveralValues(): void + { + $subject = $this->createSubjectWithHeaders(['X-Foo' => ['bar', 'baz']]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['X-Foo' => ['bar', 'baz']], $subject->getHeaders()); + } + + public function testConstructorWithSeveralHeaders(): void + { + $subject = $this->createSubjectWithHeaders([ + 'X-Foo' => 'bar', + 'X-Bar' => 'baz', + ]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame([ + 'X-Foo' => ['bar'], + 'X-Bar' => ['baz'], + ], $subject->getHeaders()); + } + + public function testConstructorWithSeveralHeadersWithSeveralValues(): void + { + $subject = $this->createSubjectWithHeaders([ + 'X-Foo' => ['bar', 'baz'], + 'X-Bar' => ['baz', 'bat'], + ]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + 'X-Bar' => ['baz', 'bat'], + ], $subject->getHeaders()); + } + + public function testConstructorWithSeveralHeadersWithSameName(): void + { + $subject = $this->createSubjectWithHeaders([ + 'X-Foo' => 'bar', + 'x-foo' => 'baz', + ]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame([ + 'X-Foo' => ['bar', 'baz'], + ], $subject->getHeaders()); + } + + public function testConstructorWithHeadersWithEmptyValue(): void + { + $subject = $this->createSubjectWithHeaders(['X-Foo' => '']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['X-Foo' => ['']], $subject->getHeaders()); + } + + public function testConstructorWithHeadersWithEmptyName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name cannot be an empty'); + + $subject = $this->createSubjectWithHeaders(['' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithNameAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name must be a string'); + + $subject = $this->createSubjectWithHeaders([42 => 'foo']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithInvalidName(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP header name is invalid'); + + $subject = $this->createSubjectWithHeaders(['X-Foo:' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithValueAsEmptyArray(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo" HTTP header value cannot be an empty array'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => []]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithValueAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value must be a string'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => null]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithValueAsNullAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value must be a string'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => ['bar', null, 'baz']]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithValueAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value must be a string'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => 42]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithValueAsNumberAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value must be a string'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => ['bar', 42, 'baz']]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithInvalidValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value is invalid'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => "\0"]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithHeadersWithInvalidValueAmongOthers(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[1]" HTTP header value is invalid'); + + $subject = $this->createSubjectWithHeaders(['X-Foo' => ['bar', "\0", 'baz']]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithBody(): void + { + $body = $this->createMock(StreamInterface::class); + $subject = $this->createSubjectWithBody($body); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame($body, $subject->getBody()); + } + + public function protocolVersionProvider(): array + { + return [ + ['1.0'], + ['1.1'], + ['2.0'], + ['2'], + ]; + } + + public function invalidProtocolVersionProvider(): array + { + return [ + [''], + ['.'], + ['1.'], + ['.1'], + ['1.1.'], + ['.1.1'], + [' 1.1'], + ['1.1 '], + ['1.1.1'], + ['-1.1'], + ['a'], + ['a.'], + ['.a'], + ['a.a'], + ['HTTP/1.1'], + ['2.1'], + ['3'], + ]; + } +} diff --git a/tests/BaseRequestTest.php b/tests/BaseRequestTest.php new file mode 100644 index 0000000..326ab3a --- /dev/null +++ b/tests/BaseRequestTest.php @@ -0,0 +1,409 @@ +createSubject(); + + $this->assertInstanceOf(RequestMethodInterface::class, $subject); + } + + public function testDefaultMethod(): void + { + $subject = $this->createSubject(); + + $this->assertSame('GET', $subject->getMethod()); + } + + public function testDefaultUri(): void + { + $subject = $this->createSubject(); + + $this->assertSame('/', $subject->getUri()->__toString()); + } + + public function testDefaultRequestTarget(): void + { + $subject = $this->createSubject(); + + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testSetMethod(): void + { + $subject = $this->createSubject(); + $clone = $subject->withMethod('POST'); + + $this->assertNotSame($clone, $subject); + $this->assertSame('POST', $clone->getMethod()); + $this->assertSame('GET', $subject->getMethod()); + } + + public function testSetLowerCaseMethod(): void + { + $subject = $this->createSubject()->withMethod('post'); + + $this->assertSame('post', $subject->getMethod()); + } + + public function testSetNonStandardMethod(): void + { + $subject = $this->createSubject()->withMethod('CUSTOM'); + + $this->assertSame('CUSTOM', $subject->getMethod()); + } + + public function testSetMethodAsEmptyString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP method cannot be an empty'); + + $this->createSubject()->withMethod(''); + } + + public function testSetMethodAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP method must be a string'); + + $this->createSubject()->withMethod(null); + } + + public function testSetInvalidMethod(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP method'); + + $this->createSubject()->withMethod("GET\0"); + } + + public function testSetUri(): void + { + $uri = new Uri(); + $subject = $this->createSubject(); + $clone = $subject->withUri($uri); + + $this->assertNotSame($clone, $subject); + $this->assertSame($uri, $clone->getUri()); + $this->assertNotSame($uri, $subject->getUri()); + } + + public function testSetUriAndHostHeader(): void + { + $uri = new Uri('//localhost'); + $subject = $this->createSubject(); + $clone = $subject->withUri($uri); + + $this->assertSame('localhost', $clone->getHeaderLine('Host')); + $this->assertFalse($subject->hasHeader('Host')); + } + + public function testSetUriWithPreservationHostHeader(): void + { + $uri = new Uri('//www2.localhost'); + $subject = $this->createSubject()->withHeader('Host', 'www1.localhost'); + $clone = $subject->withUri($uri, true); + + $this->assertNotSame($clone, $subject); + $this->assertSame('www1.localhost', $clone->getHeaderLine('Host')); + $this->assertSame('www1.localhost', $subject->getHeaderLine('Host')); + } + + public function testSetUriWithoutPreservationHostHeader(): void + { + $uri = new Uri('//www2.localhost'); + $subject = $this->createSubject()->withHeader('Host', 'www1.localhost'); + $clone = $subject->withUri($uri, false); + + $this->assertNotSame($clone, $subject); + $this->assertSame('www2.localhost', $clone->getHeaderLine('Host')); + $this->assertSame('www1.localhost', $subject->getHeaderLine('Host')); + } + + public function testSetUriWithPortWithoutPreservationHostHeader(): void + { + $uri = new Uri('//www2.localhost:8000'); + $subject = $this->createSubject()->withHeader('Host', 'www1.localhost'); + $clone = $subject->withUri($uri, false); + + $this->assertNotSame($clone, $subject); + $this->assertSame('www2.localhost:8000', $clone->getHeaderLine('Host')); + $this->assertSame('www1.localhost', $subject->getHeaderLine('Host')); + } + + public function testSetUriWithoutHostWithoutPreservationHostHeader(): void + { + $uri = new Uri(); + $subject = $this->createSubject()->withHeader('Host', 'www1.localhost'); + $clone = $subject->withUri($uri, false); + + $this->assertNotSame($clone, $subject); + $this->assertSame('www1.localhost', $clone->getHeaderLine('Host')); + $this->assertSame('www1.localhost', $subject->getHeaderLine('Host')); + } + + public function testSetUriWithDefaultBehaviourForPreservationHostHeader(): void + { + $uri = new Uri('//www2.localhost'); + $subject = $this->createSubject()->withHeader('Host', 'www1.localhost'); + $clone = $subject->withUri($uri, /* must be false */); + + $this->assertNotSame($clone, $subject); + $this->assertSame('www2.localhost', $clone->getHeaderLine('Host')); + $this->assertSame('www1.localhost', $subject->getHeaderLine('Host')); + } + + public function testSetRequestTargetAsAbsoluteForm(): void + { + $subject = $this->createSubject(); + $clone = $subject->withRequestTarget('http://localhost/path?query'); + + $this->assertNotSame($clone, $subject); + $this->assertSame('http://localhost/path?query', $clone->getRequestTarget()); + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testSetRequestTargetAsAuthorityForm(): void + { + $subject = $this->createSubject(); + $clone = $subject->withRequestTarget('localhost:3000'); + + $this->assertNotSame($clone, $subject); + $this->assertSame('localhost:3000', $clone->getRequestTarget()); + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testSetRequestTargetAsOriginForm(): void + { + $subject = $this->createSubject(); + $clone = $subject->withRequestTarget('/path?query'); + + $this->assertNotSame($clone, $subject); + $this->assertSame('/path?query', $clone->getRequestTarget()); + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testSetRequestTargetAsAsteriskForm(): void + { + $subject = $this->createSubject(); + $clone = $subject->withRequestTarget('*'); + + $this->assertNotSame($clone, $subject); + $this->assertSame('*', $clone->getRequestTarget()); + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testSetRequestTargetAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP request target must be a string'); + + $this->createSubject()->withRequestTarget(null); + } + + public function testSetRequestTargetAsEmptyString(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP request target cannot be an empty'); + + $this->createSubject()->withRequestTarget(''); + } + + public function testSetInvalidRequestTarget(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP request target'); + + $this->createSubject()->withRequestTarget("/\0"); + } + + public function testGetRequestTargetByFullUri(): void + { + $uri = new Uri('http://user:password@localhost:8000/path?query#fragment'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/path?query', $subject->getRequestTarget()); + } + + public function testGetRequestTargetByUriWithPathAndQueryOnly(): void + { + $uri = new Uri('/path?query'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/path?query', $subject->getRequestTarget()); + } + + public function testGetRequestTargetByUriWithPathOnly(): void + { + $uri = new Uri('/path'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/path', $subject->getRequestTarget()); + } + + public function testGetRequestTargetByUriWithQueryOnly(): void + { + $uri = new Uri('?query'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testGetRequestTargetByUriWithRelativePathOnly(): void + { + $uri = new Uri('path'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/', $subject->getRequestTarget()); + } + + public function testGetRequestTargetByUriWithPathThatContainsTwoLeadingSlashes(): void + { + $uri = new Uri('//localhost//path'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/path', $subject->getRequestTarget()); + } + + public function testGetRequestTargetByUriWithPathThatContainsThreeLeadingSlashes(): void + { + $uri = new Uri('//localhost///path'); + + $subject = $this->createSubject()->withUri($uri); + + $this->assertSame('/path', $subject->getRequestTarget()); + } + + public function testGetRequestTargetIgnoringNewUri(): void + { + $subject = $this->createSubject() + ->withRequestTarget('/foo') + ->withUri(new Uri('/bar')); + + $this->assertSame('/foo', $subject->getRequestTarget()); + } + + public function testConstructorWithMethod(): void + { + $subject = $this->createSubjectWithMethod('POST'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame('POST', $subject->getMethod()); + } + + public function testConstructorWithLowerCaseMethod(): void + { + $subject = $this->createSubjectWithMethod('post'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame('post', $subject->getMethod()); + } + + public function testConstructorWithNonStandardMethod(): void + { + $subject = $this->createSubjectWithMethod('CUSTOM'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame('CUSTOM', $subject->getMethod()); + } + + public function testConstructorWithEmptyMethod(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP method cannot be an empty'); + + $subject = $this->createSubjectWithMethod(''); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithInvalidMethod(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP method'); + + $subject = $this->createSubjectWithMethod("GET\0"); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithUri(): void + { + $uri = new Uri('//foo/bar'); + $subject = $this->createSubjectWithUri($uri); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame($uri, $subject->getUri()); + $this->assertSame('foo', $subject->getHeaderLine('Host')); + $this->assertSame('/bar', $subject->getRequestTarget()); + } + + public function testConstructorWithStringUri(): void + { + $subject = $this->createSubjectWithUri('//foo/bar'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame('//foo/bar', $subject->getUri()->__toString()); + $this->assertSame('foo', $subject->getHeaderLine('Host')); + $this->assertSame('/bar', $subject->getRequestTarget()); + } + + public function testConstructorWithInvalidUri(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to parse URI'); + + $subject = $this->createSubjectWithUri(':'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } +} diff --git a/tests/BaseResponseTest.php b/tests/BaseResponseTest.php new file mode 100644 index 0000000..fd34f15 --- /dev/null +++ b/tests/BaseResponseTest.php @@ -0,0 +1,246 @@ +createSubject(); + + $this->assertInstanceOf(StatusCodeInterface::class, $subject); + } + + public function testDefaultStatusCode(): void + { + $subject = $this->createSubject(); + + $this->assertSame(200, $subject->getStatusCode()); + } + + public function testDefaultReasonPhrase(): void + { + $subject = $this->createSubject(); + + $this->assertSame('OK', $subject->getReasonPhrase()); + } + + public function testSetStatusCode(): void + { + $subject = $this->createSubject(); + $clone = $subject->withStatus(202); + + $this->assertNotSame($clone, $subject); + + $this->assertSame(202, $clone->getStatusCode()); + $this->assertSame('Accepted', $clone->getReasonPhrase()); + + $this->assertSame(200, $subject->getStatusCode()); + $this->assertSame('OK', $subject->getReasonPhrase()); + } + + public function testSetStatusCodeWithReasonPhrase(): void + { + $subject = $this->createSubject(); + $clone = $subject->withStatus(202, 'Custom Reason Phrase'); + + $this->assertNotSame($clone, $subject); + + $this->assertSame(202, $clone->getStatusCode()); + $this->assertSame('Custom Reason Phrase', $clone->getReasonPhrase()); + + $this->assertSame(200, $subject->getStatusCode()); + $this->assertSame('OK', $subject->getReasonPhrase()); + } + + public function testSetStatusCodeWithEmptyReasonPhrase(): void + { + $subject = $this->createSubject()->withStatus(202, ''); + + $this->assertSame('Accepted', $subject->getReasonPhrase()); + } + + public function testSetStatusCodeThatHasNoReasonPhrase(): void + { + $subject = $this->createSubject()->withStatus(599); + + $this->assertSame('Unknown Status Code', $subject->getReasonPhrase()); + } + + public function testSetStatusCodeAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP status code must be an integer'); + + $this->createSubject()->withStatus(null); + } + + public function testSetStatusCodeAsStringNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP status code must be an integer'); + + $this->createSubject()->withStatus('200'); + } + + public function testSetStatusCodeAsNumberLessThan100(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP status code'); + + $this->createSubject()->withStatus(99); + } + + public function testSetStatusCodeAsNumberGreaterThan599(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP status code'); + + $this->createSubject()->withStatus(600); + } + + public function testSetReasonPhraseAsNull(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP reason phrase must be a string'); + + $this->createSubject()->withStatus(200, null); + } + + public function testSetReasonPhraseAsNumber(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP reason phrase must be a string'); + + $this->createSubject()->withStatus(200, 42); + } + + public function testSetInvalidReasonPhrase(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP reason phrase'); + + $this->createSubject()->withStatus(200, "\0"); + } + + public function testConstructorWithStatusCodeWithoutReasonPhrase(): void + { + $subject = $this->createSubjectWithStatus(200); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(200, $subject->getStatusCode()); + $this->assertSame('OK', $subject->getReasonPhrase()); + } + + public function testConstructorWithStatusCodeWithEmptyReasonPhrase(): void + { + $subject = $this->createSubjectWithStatus(200, ''); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(200, $subject->getStatusCode()); + $this->assertSame('OK', $subject->getReasonPhrase()); + } + + public function testConstructorWithStatusCodeWithCustomReasonPhrase(): void + { + $subject = $this->createSubjectWithStatus(200, 'Custom Reason Phrase'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(200, $subject->getStatusCode()); + $this->assertSame('Custom Reason Phrase', $subject->getReasonPhrase()); + } + + public function testConstructorWithUnknownStatusCodeWithoutReasonPhrase(): void + { + $subject = $this->createSubjectWithStatus(599); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(599, $subject->getStatusCode()); + $this->assertSame('Unknown Status Code', $subject->getReasonPhrase()); + } + + public function testConstructorWithUnknownStatusCodeWithEmptyReasonPhrase(): void + { + $subject = $this->createSubjectWithStatus(599, ''); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(599, $subject->getStatusCode()); + $this->assertSame('Unknown Status Code', $subject->getReasonPhrase()); + } + + public function testConstructorWithUnknownStatusCodeWithReasonPhrase(): void + { + $subject = $this->createSubjectWithStatus(599, 'Custom Reason Phrase'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(599, $subject->getStatusCode()); + $this->assertSame('Custom Reason Phrase', $subject->getReasonPhrase()); + } + + public function testConstructorWithStatusCodeLessThan100(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP status code'); + + $subject = $this->createSubjectWithStatus(99); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithStatusCodeGreaterThan599(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP status code'); + + $subject = $this->createSubjectWithStatus(600); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithInvalidReasonPhrase(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP reason phrase'); + + $subject = $this->createSubjectWithStatus(200, "\0"); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } +} diff --git a/tests/BaseServerRequestTest.php b/tests/BaseServerRequestTest.php new file mode 100644 index 0000000..b3304ef --- /dev/null +++ b/tests/BaseServerRequestTest.php @@ -0,0 +1,361 @@ +createSubject(); + + $this->assertSame([], $subject->getServerParams()); + } + + public function testDefaultQueryParams(): void + { + $subject = $this->createSubject(); + + $this->assertSame([], $subject->getQueryParams()); + } + + public function testDefaultCookieParams(): void + { + $subject = $this->createSubject(); + + $this->assertSame([], $subject->getCookieParams()); + } + + public function testDefaultUploadedFiles(): void + { + $subject = $this->createSubject(); + + $this->assertSame([], $subject->getUploadedFiles()); + } + + public function testDefaultParsedBody(): void + { + $subject = $this->createSubject(); + + $this->assertNull($subject->getParsedBody()); + } + + public function testDefaultAttributes(): void + { + $subject = $this->createSubject(); + + $this->assertSame([], $subject->getAttributes()); + } + + public function testSetQueryParams(): void + { + $subject = $this->createSubject(); + $clone = $subject->withQueryParams(['foo' => 'bar']); + + $this->assertNotSame($clone, $subject); + $this->assertSame(['foo' => 'bar'], $clone->getQueryParams()); + $this->assertSame([], $subject->getQueryParams()); + } + + public function testSetEmptyQueryParams(): void + { + $this->assertSame([], $this->createSubject() + ->withQueryParams(['foo' => 'bar']) + ->withQueryParams([]) + ->getQueryParams()); + } + + public function testSetCookieParams(): void + { + $subject = $this->createSubject(); + $clone = $subject->withCookieParams(['foo' => 'bar']); + + $this->assertNotSame($clone, $subject); + $this->assertSame(['foo' => 'bar'], $clone->getCookieParams()); + $this->assertSame([], $subject->getCookieParams()); + } + + public function testSetEmptyCookieParams(): void + { + $this->assertSame([], $this->createSubject() + ->withCookieParams(['foo' => 'bar']) + ->withCookieParams([]) + ->getCookieParams()); + } + + public function testSetUploadedFiles(): void + { + $file = $this->createMock(UploadedFileInterface::class); + $subject = $this->createSubject(); + $clone = $subject->withUploadedFiles(['foo' => $file]); + + $this->assertNotSame($clone, $subject); + $this->assertSame(['foo' => $file], $clone->getUploadedFiles()); + $this->assertSame([], $subject->getUploadedFiles()); + } + + public function testSetEmptyUploadedFiles(): void + { + $this->assertSame([], $this->createSubject() + ->withUploadedFiles([ + 'foo' => $this->createMock(UploadedFileInterface::class), + ]) + ->withUploadedFiles([]) + ->getUploadedFiles()); + } + + public function testSetInvalidUploadedFiles(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid uploaded files'); + + $this->createSubject()->withUploadedFiles(['foo' => 'bar']); + } + + public function testSetParsedBodyAsNull(): void + { + $subject = $this->createSubject(); + $clone = $subject->withParsedBody(null); + + $this->assertNotSame($clone, $subject); + $this->assertNull($clone->getParsedBody()); + $this->assertNull($subject->getParsedBody()); + } + + public function testSetParsedBodyAsArray(): void + { + $subject = $this->createSubject(); + $clone = $subject->withParsedBody(['foo' => 'bar']); + + $this->assertNotSame($clone, $subject); + $this->assertSame(['foo' => 'bar'], $clone->getParsedBody()); + $this->assertNull($subject->getParsedBody()); + } + + public function testSetParsedBodyAsEmptyArray(): void + { + $this->assertSame([], $this->createSubject() + ->withParsedBody(['foo' => 'bar']) + ->withParsedBody([]) + ->getParsedBody()); + } + + public function testSetParsedBodyAsObject(): void + { + $object = new \stdClass; + $subject = $this->createSubject(); + $clone = $subject->withParsedBody($object); + + $this->assertNotSame($clone, $subject); + $this->assertSame($object, $clone->getParsedBody()); + $this->assertNull($subject->getParsedBody()); + } + + public function testSetInvalidParsedBody(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid parsed body'); + + $this->createSubject()->withParsedBody('foo'); + } + + public function testSetAttribute(): void + { + $subject = $this->createSubject(); + $clone = $subject->withAttribute('foo', 'bar'); + + $this->assertNotSame($clone, $subject); + $this->assertSame(['foo' => 'bar'], $clone->getAttributes()); + $this->assertSame([], $subject->getAttributes()); + } + + public function testGetAttribute(): void + { + $this->assertSame('bar', $this->createSubject() + ->withAttribute('foo', 'bar') + ->getAttribute('foo')); + } + + public function testGetUnknownAttribute(): void + { + $this->assertNull($this->createSubject() + ->getAttribute('foo')); + } + + public function testGetUnknownAttributeWithDefaultValue(): void + { + $this->assertFalse($this->createSubject() + ->getAttribute('foo', false)); + } + + public function testReplaceAttribute(): void + { + $this->assertSame('baz', $this->createSubject() + ->withAttribute('foo', 'bar') + ->withAttribute('foo', 'baz') + ->getAttribute('foo')); + } + + public function testDeleteAttribute(): void + { + $subject = $this->createSubject() + ->withAttribute('foo', 'bar'); + + $clone = $subject->withoutAttribute('foo'); + + $this->assertNotSame($clone, $subject); + $this->assertSame([], $clone->getAttributes()); + $this->assertSame(['foo' => 'bar'], $subject->getAttributes()); + } + + public function testConstructorWithServerParams(): void + { + $subject = $this->createSubjectWithServerParams(['foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['foo' => 'bar'], $subject->getServerParams()); + } + + public function testConstructorWithQueryParams(): void + { + $subject = $this->createSubjectWithQueryParams(['foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['foo' => 'bar'], $subject->getQueryParams()); + } + + public function testConstructorWithCookieParams(): void + { + $subject = $this->createSubjectWithCookieParams(['foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['foo' => 'bar'], $subject->getCookieParams()); + } + + public function testConstructorWithUploadedFiles(): void + { + $file = $this->createMock(UploadedFileInterface::class); + $subject = $this->createSubjectWithUploadedFiles(['foo' => $file]); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['foo' => $file], $subject->getUploadedFiles()); + } + + public function testConstructorWithInvalidUploadedFiles(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid uploaded files'); + + $subject = $this->createSubjectWithUploadedFiles(['foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithParsedBodyAsNull(): void + { + $subject = $this->createSubjectWithParsedBody(null); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertNull($subject->getParsedBody()); + } + + public function testConstructorWithParsedBodyAsArray(): void + { + $subject = $this->createSubjectWithParsedBody(['foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['foo' => 'bar'], $subject->getParsedBody()); + } + + public function testConstructorWithParsedBodyAsObject(): void + { + $object = new \stdClass; + $subject = $this->createSubjectWithParsedBody($object); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame($object, $subject->getParsedBody()); + } + + public function testConstructorWithInvalidParsedBody(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid parsed body'); + + $subject = $this->createSubjectWithParsedBody('foo'); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + } + + public function testConstructorWithAttributes(): void + { + $subject = $this->createSubjectWithAttributes(['foo' => 'bar']); + + if (!isset($subject)) { + $this->markTestSkipped(__FUNCTION__); + } + + $this->assertSame(['foo' => 'bar'], $subject->getAttributes()); + } +} diff --git a/tests/Header/AccessControlAllowCredentialsHeaderTest.php b/tests/Header/AccessControlAllowCredentialsHeaderTest.php new file mode 100644 index 0000000..b195b5b --- /dev/null +++ b/tests/Header/AccessControlAllowCredentialsHeaderTest.php @@ -0,0 +1,55 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AccessControlAllowCredentialsHeader(); + + $this->assertSame('Access-Control-Allow-Credentials', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AccessControlAllowCredentialsHeader(); + + $this->assertSame('true', $header->getFieldValue()); + } + + public function testBuild() + { + $header = new AccessControlAllowCredentialsHeader(); + + $expected = 'Access-Control-Allow-Credentials: true'; + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AccessControlAllowCredentialsHeader(); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AccessControlAllowHeadersHeaderTest.php b/tests/Header/AccessControlAllowHeadersHeaderTest.php new file mode 100644 index 0000000..3881b77 --- /dev/null +++ b/tests/Header/AccessControlAllowHeadersHeaderTest.php @@ -0,0 +1,96 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AccessControlAllowHeadersHeader('x-foo'); + + $this->assertSame('Access-Control-Allow-Headers', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AccessControlAllowHeadersHeader('x-foo'); + + $this->assertSame('x-foo', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new AccessControlAllowHeadersHeader('x-foo', 'x-bar', 'x-baz'); + + $this->assertSame('x-foo, x-bar, x-baz', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Access-Control-Allow-Headers" is not valid'); + + new AccessControlAllowHeadersHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Access-Control-Allow-Headers" is not valid'); + + new AccessControlAllowHeadersHeader('x-foo', '', 'x-bar'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "x-foo=" for the header "Access-Control-Allow-Headers" is not valid'); + + // a token cannot contain the "=" character... + new AccessControlAllowHeadersHeader('x-foo='); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "x-bar=" for the header "Access-Control-Allow-Headers" is not valid'); + + // a token cannot contain the "=" character... + new AccessControlAllowHeadersHeader('x-foo', 'x-bar=', 'x-bar'); + } + + public function testBuild() + { + $header = new AccessControlAllowHeadersHeader('x-foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AccessControlAllowHeadersHeader('x-foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AccessControlAllowMethodsHeaderTest.php b/tests/Header/AccessControlAllowMethodsHeaderTest.php new file mode 100644 index 0000000..fdb63e4 --- /dev/null +++ b/tests/Header/AccessControlAllowMethodsHeaderTest.php @@ -0,0 +1,103 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AccessControlAllowMethodsHeader('GET'); + + $this->assertSame('Access-Control-Allow-Methods', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AccessControlAllowMethodsHeader('GET'); + + $this->assertSame('GET', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new AccessControlAllowMethodsHeader('HEAD', 'GET', 'POST'); + + $this->assertSame('HEAD, GET, POST', $header->getFieldValue()); + } + + public function testValueCapitalizing() + { + $header = new AccessControlAllowMethodsHeader('head', 'get', 'post'); + + $this->assertSame('HEAD, GET, POST', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Access-Control-Allow-Methods" is not valid'); + + new AccessControlAllowMethodsHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Access-Control-Allow-Methods" is not valid'); + + new AccessControlAllowMethodsHeader('head', '', 'post'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Access-Control-Allow-Methods" is not valid'); + + // isn't a token... + new AccessControlAllowMethodsHeader('@'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Access-Control-Allow-Methods" is not valid'); + + // isn't a token... + new AccessControlAllowMethodsHeader('head', '@', 'post'); + } + + public function testBuild() + { + $header = new AccessControlAllowMethodsHeader('GET'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AccessControlAllowMethodsHeader('GET'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AccessControlAllowOriginHeaderTest.php b/tests/Header/AccessControlAllowOriginHeaderTest.php new file mode 100644 index 0000000..c75c181 --- /dev/null +++ b/tests/Header/AccessControlAllowOriginHeaderTest.php @@ -0,0 +1,106 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $uri = new Uri('http://localhost'); + $header = new AccessControlAllowOriginHeader($uri); + + $this->assertSame('Access-Control-Allow-Origin', $header->getFieldName()); + } + + public function testFieldValue() + { + $uri = new Uri('http://localhost'); + $header = new AccessControlAllowOriginHeader($uri); + + $this->assertSame('http://localhost', $header->getFieldValue()); + } + + public function testFieldValueWithoutUri() + { + $header = new AccessControlAllowOriginHeader(null); + + $this->assertSame('*', $header->getFieldValue()); + } + + public function testIgnoringUnnecessaryUriComponents() + { + $uri = new Uri('http://user:pass@localhost:3000/index.php?q#h'); + $header = new AccessControlAllowOriginHeader($uri); + + $this->assertSame('http://localhost:3000', $header->getFieldValue()); + } + + public function testUriWithPort() + { + $uri = new Uri('http://localhost:3000'); + $header = new AccessControlAllowOriginHeader($uri); + + $this->assertSame('http://localhost:3000', $header->getFieldValue()); + } + + public function testUriWithoutScheme() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The URI "//localhost" for the header "Access-Control-Allow-Origin" is not valid' + ); + + new AccessControlAllowOriginHeader(new Uri('//localhost')); + } + + public function testUriWithoutHost() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The URI "http:" for the header "Access-Control-Allow-Origin" is not valid' + ); + + new AccessControlAllowOriginHeader(new Uri('http:')); + } + + public function testBuild() + { + $uri = new Uri('http://localhost'); + $header = new AccessControlAllowOriginHeader($uri); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $uri = new Uri('http://localhost'); + $header = new AccessControlAllowOriginHeader($uri); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AccessControlExposeHeadersHeaderTest.php b/tests/Header/AccessControlExposeHeadersHeaderTest.php new file mode 100644 index 0000000..271a1db --- /dev/null +++ b/tests/Header/AccessControlExposeHeadersHeaderTest.php @@ -0,0 +1,96 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AccessControlExposeHeadersHeader('x-foo'); + + $this->assertSame('Access-Control-Expose-Headers', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AccessControlExposeHeadersHeader('x-foo'); + + $this->assertSame('x-foo', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new AccessControlExposeHeadersHeader('x-foo', 'x-bar', 'x-baz'); + + $this->assertSame('x-foo, x-bar, x-baz', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Access-Control-Expose-Headers" is not valid'); + + new AccessControlExposeHeadersHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Access-Control-Expose-Headers" is not valid'); + + new AccessControlExposeHeadersHeader('x-foo', '', 'x-baz'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Access-Control-Expose-Headers" is not valid'); + + // isn't a token... + new AccessControlExposeHeadersHeader('@'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Access-Control-Expose-Headers" is not valid'); + + // isn't a token... + new AccessControlExposeHeadersHeader('x-foo', '@', 'x-baz'); + } + + public function testBuild() + { + $header = new AccessControlExposeHeadersHeader('x-foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AccessControlExposeHeadersHeader('x-foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AccessControlMaxAgeHeaderTest.php b/tests/Header/AccessControlMaxAgeHeaderTest.php new file mode 100644 index 0000000..7520624 --- /dev/null +++ b/tests/Header/AccessControlMaxAgeHeaderTest.php @@ -0,0 +1,90 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AccessControlMaxAgeHeader(-1); + + $this->assertSame('Access-Control-Max-Age', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AccessControlMaxAgeHeader(-1); + + $this->assertSame('-1', $header->getFieldValue()); + } + + /** + * @dataProvider validValueDataProvider + */ + public function testValidValue(int $validValue) + { + $header = new AccessControlMaxAgeHeader($validValue); + + $this->assertEquals($validValue, $header->getFieldValue()); + } + + public function validValueDataProvider(): array + { + return [[-1], [1], [2]]; + } + + /** + * @dataProvider invalidValueDataProvider + */ + public function testInvalidValue(int $invalidValue) + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage(sprintf( + 'The value "%d" for the header "Access-Control-Max-Age" is not valid', + $invalidValue + )); + + new AccessControlMaxAgeHeader($invalidValue); + } + + public function invalidValueDataProvider(): array + { + return [[-3], [-2], [0]]; + } + + public function testBuild() + { + $header = new AccessControlMaxAgeHeader(-1); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AccessControlMaxAgeHeader(-1); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AgeHeaderTest.php b/tests/Header/AgeHeaderTest.php new file mode 100644 index 0000000..f3289fb --- /dev/null +++ b/tests/Header/AgeHeaderTest.php @@ -0,0 +1,90 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AgeHeader(0); + + $this->assertSame('Age', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AgeHeader(0); + + $this->assertSame('0', $header->getFieldValue()); + } + + /** + * @dataProvider validValueDataProvider + */ + public function testValidValue(int $validValue) + { + $header = new AgeHeader($validValue); + + $this->assertEquals($validValue, $header->getFieldValue()); + } + + public function validValueDataProvider(): array + { + return [[0], [1], [2]]; + } + + /** + * @dataProvider invalidValueDataProvider + */ + public function testInvalidValue(int $invalidValue) + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage(sprintf( + 'The value "%d" for the header "Age" is not valid', + $invalidValue + )); + + new AgeHeader($invalidValue); + } + + public function invalidValueDataProvider(): array + { + return [[-3], [-2], [-1]]; + } + + public function testBuild() + { + $header = new AgeHeader(0); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AgeHeader(0); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/AllowHeaderTest.php b/tests/Header/AllowHeaderTest.php new file mode 100644 index 0000000..9c325be --- /dev/null +++ b/tests/Header/AllowHeaderTest.php @@ -0,0 +1,103 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new AllowHeader('GET'); + + $this->assertSame('Allow', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new AllowHeader('GET'); + + $this->assertSame('GET', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new AllowHeader('HEAD', 'GET', 'POST'); + + $this->assertSame('HEAD, GET, POST', $header->getFieldValue()); + } + + public function testValueCapitalizing() + { + $header = new AllowHeader('head', 'get', 'post'); + + $this->assertSame('HEAD, GET, POST', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Allow" is not valid'); + + new AllowHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Allow" is not valid'); + + new AllowHeader('head', '', 'post'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Allow" is not valid'); + + // isn't a token... + new AllowHeader('@'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Allow" is not valid'); + + // isn't a token... + new AllowHeader('head', '@', 'post'); + } + + public function testBuild() + { + $header = new AllowHeader('GET'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new AllowHeader('GET'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/CacheControlHeaderTest.php b/tests/Header/CacheControlHeaderTest.php new file mode 100644 index 0000000..7d83961 --- /dev/null +++ b/tests/Header/CacheControlHeaderTest.php @@ -0,0 +1,147 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new CacheControlHeader([]); + + $this->assertSame('Cache-Control', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new CacheControlHeader([]); + + $this->assertSame('', $header->getFieldValue()); + } + + public function testParameterWithEmptyValue() + { + $header = new CacheControlHeader([ + 'foo' => '', + ]); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithToken() + { + $header = new CacheControlHeader([ + 'foo' => 'token', + ]); + + $this->assertSame('foo=token', $header->getFieldValue()); + } + + public function testParameterWithQuotedString() + { + $header = new CacheControlHeader([ + 'foo' => 'quoted string', + ]); + + $this->assertSame('foo="quoted string"', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new CacheControlHeader([ + 'foo' => 1, + ]); + + $this->assertSame('foo=1', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new CacheControlHeader([ + 'foo' => '', + 'bar' => 'token', + 'baz' => 'quoted string', + 'qux' => 1, + ]); + + $this->assertSame('foo, bar=token, baz="quoted string", qux=1', $header->getFieldValue()); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "invalid name" for the header "Cache-Control" is not valid' + ); + + new CacheControlHeader(['invalid name' => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ""invalid value"" for the header "Cache-Control" is not valid' + ); + + new CacheControlHeader(['name' => '"invalid value"']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Cache-Control" is not valid' + ); + + new CacheControlHeader([0 => 'value']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Cache-Control" is not valid' + ); + + new CacheControlHeader(['name' => []]); + } + + public function testBuild() + { + $header = new CacheControlHeader(['foo' => 'bar']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new CacheControlHeader(['foo' => 'bar']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ClearSiteDataHeaderTest.php b/tests/Header/ClearSiteDataHeaderTest.php new file mode 100644 index 0000000..5e7d508 --- /dev/null +++ b/tests/Header/ClearSiteDataHeaderTest.php @@ -0,0 +1,94 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ClearSiteDataHeader('foo'); + + $this->assertSame('Clear-Site-Data', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ClearSiteDataHeader('foo'); + + $this->assertSame('"foo"', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new ClearSiteDataHeader('foo', 'bar', 'baz'); + + $this->assertSame('"foo", "bar", "baz"', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $header = new ClearSiteDataHeader(''); + + $this->assertSame('""', $header->getFieldValue()); + } + + public function testEmptyValueAmongOthers() + { + $header = new ClearSiteDataHeader('foo', '', 'baz'); + + $this->assertSame('"foo", "", "baz"', $header->getFieldValue()); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value ""invalid value"" for the header "Clear-Site-Data" is not valid'); + + // cannot contain quotes... + new ClearSiteDataHeader('"invalid value"'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value ""bar"" for the header "Clear-Site-Data" is not valid'); + + // cannot contain quotes... + new ClearSiteDataHeader('foo', '"bar"', 'baz'); + } + + public function testBuild() + { + $header = new ClearSiteDataHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ClearSiteDataHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ConnectionHeaderTest.php b/tests/Header/ConnectionHeaderTest.php new file mode 100644 index 0000000..036b9ed --- /dev/null +++ b/tests/Header/ConnectionHeaderTest.php @@ -0,0 +1,78 @@ +assertSame('close', ConnectionHeader::CONNECTION_CLOSE); + $this->assertSame('keep-alive', ConnectionHeader::CONNECTION_KEEP_ALIVE); + } + + public function testContracts() + { + $header = new ConnectionHeader('foo'); + + $this->assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ConnectionHeader('foo'); + + $this->assertSame('Connection', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ConnectionHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Connection" is not valid'); + + new ConnectionHeader(''); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Connection" is not valid'); + + // isn't a token... + new ConnectionHeader('@'); + } + + public function testBuild() + { + $header = new ConnectionHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ConnectionHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentDispositionHeaderTest.php b/tests/Header/ContentDispositionHeaderTest.php new file mode 100644 index 0000000..17ff90f --- /dev/null +++ b/tests/Header/ContentDispositionHeaderTest.php @@ -0,0 +1,166 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentDispositionHeader('foo'); + + $this->assertSame('Content-Disposition', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentDispositionHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithEmptyValue() + { + $header = new ContentDispositionHeader('foo', [ + 'bar' => '', + ]); + + $this->assertSame('foo; bar=""', $header->getFieldValue()); + } + + public function testParameterWithToken() + { + $header = new ContentDispositionHeader('foo', [ + 'bar' => 'token', + ]); + + $this->assertSame('foo; bar="token"', $header->getFieldValue()); + } + + public function testParameterWithQuotedString() + { + $header = new ContentDispositionHeader('foo', [ + 'bar' => 'quoted string', + ]); + + $this->assertSame('foo; bar="quoted string"', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new ContentDispositionHeader('foo', [ + 'bar' => 1, + ]); + + $this->assertSame('foo; bar="1"', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new ContentDispositionHeader('foo', [ + 'bar' => '', + 'baz' => 'token', + 'bat' => 'quoted string', + 'qux' => 1, + ]); + + $this->assertSame('foo; bar=""; baz="token"; bat="quoted string"; qux="1"', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Content-Disposition" is not valid'); + + new ContentDispositionHeader(''); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Content-Disposition" is not valid'); + + // isn't a token... + new ContentDispositionHeader('@'); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "invalid name" for the header "Content-Disposition" is not valid' + ); + + // cannot contain spaces... + new ContentDispositionHeader('foo', ['invalid name' => 'value']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Content-Disposition" is not valid' + ); + + new ContentDispositionHeader('foo', [0 => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ""invalid value"" for the header "Content-Disposition" is not valid' + ); + + // cannot contain quotes... + new ContentDispositionHeader('foo', ['name' => '"invalid value"']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Content-Disposition" is not valid' + ); + + new ContentDispositionHeader('foo', ['name' => []]); + } + + public function testBuild() + { + $header = new ContentDispositionHeader('foo', ['bar' => 'baz']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentDispositionHeader('foo', ['bar' => 'baz']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentEncodingHeaderTest.php b/tests/Header/ContentEncodingHeaderTest.php new file mode 100644 index 0000000..3325846 --- /dev/null +++ b/tests/Header/ContentEncodingHeaderTest.php @@ -0,0 +1,104 @@ +assertSame('gzip', ContentEncodingHeader::GZIP); + $this->assertSame('compress', ContentEncodingHeader::COMPRESS); + $this->assertSame('deflate', ContentEncodingHeader::DEFLATE); + $this->assertSame('br', ContentEncodingHeader::BR); + } + + public function testContracts() + { + $header = new ContentEncodingHeader('foo'); + + $this->assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentEncodingHeader('foo'); + + $this->assertSame('Content-Encoding', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentEncodingHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new ContentEncodingHeader('foo', 'bar', 'baz'); + + $this->assertSame('foo, bar, baz', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Content-Encoding" is not valid'); + + new ContentEncodingHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Content-Encoding" is not valid'); + + new ContentEncodingHeader('foo', '', 'bar'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "foo=" for the header "Content-Encoding" is not valid'); + + // a token cannot contain the "=" character... + new ContentEncodingHeader('foo='); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "bar=" for the header "Content-Encoding" is not valid'); + + // a token cannot contain the "=" character... + new ContentEncodingHeader('foo', 'bar=', 'bar'); + } + + public function testBuild() + { + $header = new ContentEncodingHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentEncodingHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentLanguageHeaderTest.php b/tests/Header/ContentLanguageHeaderTest.php new file mode 100644 index 0000000..e789827 --- /dev/null +++ b/tests/Header/ContentLanguageHeaderTest.php @@ -0,0 +1,114 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentLanguageHeader('foo'); + + $this->assertSame('Content-Language', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentLanguageHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new ContentLanguageHeader('foo', 'bar', 'baz'); + + $this->assertSame('foo, bar, baz', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Content-Language" is not valid'); + + new ContentLanguageHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Content-Language" is not valid'); + + new ContentLanguageHeader('foo', '', 'baz'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Content-Language" is not valid'); + + // isn't a token... + new ContentLanguageHeader('@'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Content-Language" is not valid'); + + // isn't a token... + new ContentLanguageHeader('foo', '@', 'baz'); + } + + public function testInvalidValueLength() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "VERYLONGWORD" for the header "Content-Language" is not valid'); + + // isn't a token... + new ContentLanguageHeader('VERYLONGWORD'); + } + + public function testInvalidValueLengthAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "VERYLONGWORD" for the header "Content-Language" is not valid'); + + // isn't a token... + new ContentLanguageHeader('foo', 'VERYLONGWORD', 'baz'); + } + + public function testBuild() + { + $header = new ContentLanguageHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentLanguageHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentLengthHeaderTest.php b/tests/Header/ContentLengthHeaderTest.php new file mode 100644 index 0000000..7eb9b1f --- /dev/null +++ b/tests/Header/ContentLengthHeaderTest.php @@ -0,0 +1,63 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentLengthHeader(0); + + $this->assertSame('Content-Length', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentLengthHeader(0); + + $this->assertSame('0', $header->getFieldValue()); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "-1" for the header "Content-Length" is not valid'); + + new ContentLengthHeader(-1); + } + + public function testBuild() + { + $header = new ContentLengthHeader(0); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentLengthHeader(0); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentLocationHeaderTest.php b/tests/Header/ContentLocationHeaderTest.php new file mode 100644 index 0000000..0234399 --- /dev/null +++ b/tests/Header/ContentLocationHeaderTest.php @@ -0,0 +1,61 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $uri = new Uri('/'); + $header = new ContentLocationHeader($uri); + + $this->assertSame('Content-Location', $header->getFieldName()); + } + + public function testFieldValue() + { + $uri = new Uri('/'); + $header = new ContentLocationHeader($uri); + + $this->assertSame('/', $header->getFieldValue()); + } + + public function testBuild() + { + $uri = new Uri('/'); + $header = new ContentLocationHeader($uri); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $uri = new Uri('/'); + $header = new ContentLocationHeader($uri); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentMD5HeaderTest.php b/tests/Header/ContentMD5HeaderTest.php new file mode 100644 index 0000000..0bca2d8 --- /dev/null +++ b/tests/Header/ContentMD5HeaderTest.php @@ -0,0 +1,79 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentMD5Header(self::TEST_MD5_DIGEST); + + $this->assertSame('Content-MD5', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentMD5Header(self::TEST_MD5_DIGEST); + + $this->assertSame(self::TEST_MD5_DIGEST, $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The value "" for the header "Content-MD5" is not valid' + ); + + new ContentMD5Header(''); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The value "=invalid md5 digest=" for the header "Content-MD5" is not valid' + ); + + new ContentMD5Header('=invalid md5 digest='); + } + + public function testBuild() + { + $header = new ContentMD5Header(self::TEST_MD5_DIGEST); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentMD5Header(self::TEST_MD5_DIGEST); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentRangeHeaderTest.php b/tests/Header/ContentRangeHeaderTest.php new file mode 100644 index 0000000..cb04bd5 --- /dev/null +++ b/tests/Header/ContentRangeHeaderTest.php @@ -0,0 +1,91 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentRangeHeader(0, 1, 2); + + $this->assertSame('Content-Range', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentRangeHeader(0, 1, 2); + + $this->assertSame('bytes 0-1/2', $header->getFieldValue()); + } + + public function testInvalidFirstBytePosition() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The "first-byte-pos" value of the content range ' . + 'must be less than or equal to the "last-byte-pos" value' + ); + + new ContentRangeHeader(2, 1, 2); + } + + public function testInvalidLastBytePosition() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The "last-byte-pos" value of the content range ' . + 'must be less than the "instance-length" value' + ); + + new ContentRangeHeader(0, 2, 2); + } + + public function testInvalidInstanceLength() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The "last-byte-pos" value of the content range ' . + 'must be less than the "instance-length" value' + ); + + new ContentRangeHeader(0, 1, 1); + } + + public function testBuild() + { + $header = new ContentRangeHeader(0, 1, 2); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentRangeHeader(0, 1, 2); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentSecurityPolicyHeaderTest.php b/tests/Header/ContentSecurityPolicyHeaderTest.php new file mode 100644 index 0000000..44b20d0 --- /dev/null +++ b/tests/Header/ContentSecurityPolicyHeaderTest.php @@ -0,0 +1,137 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentSecurityPolicyHeader([]); + + $this->assertSame('Content-Security-Policy', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentSecurityPolicyHeader([]); + + $this->assertSame('', $header->getFieldValue()); + } + + public function testParameterWithoutValue() + { + $header = new ContentSecurityPolicyHeader([ + 'foo' => '', + ]); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithValue() + { + $header = new ContentSecurityPolicyHeader([ + 'foo' => 'bar', + ]); + + $this->assertSame('foo bar', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new ContentSecurityPolicyHeader([ + 'foo' => 1, + ]); + + $this->assertSame('foo 1', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new ContentSecurityPolicyHeader([ + 'foo' => '', + 'bar' => 'bat', + 'baz' => 1, + ]); + + $this->assertSame('foo; bar bat; baz 1', $header->getFieldValue()); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "name=" for the header "Content-Security-Policy" is not valid' + ); + + new ContentSecurityPolicyHeader(['name=' => 'value']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Content-Security-Policy" is not valid' + ); + + new ContentSecurityPolicyHeader([0 => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ";value" for the header "Content-Security-Policy" is not valid' + ); + + new ContentSecurityPolicyHeader(['name' => ';value']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Content-Security-Policy" is not valid' + ); + + new ContentSecurityPolicyHeader(['name' => []]); + } + + public function testBuild() + { + $header = new ContentSecurityPolicyHeader(['foo' => 'bar']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentSecurityPolicyHeader(['foo' => 'bar']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentSecurityPolicyReportOnlyHeaderTest.php b/tests/Header/ContentSecurityPolicyReportOnlyHeaderTest.php new file mode 100644 index 0000000..a521715 --- /dev/null +++ b/tests/Header/ContentSecurityPolicyReportOnlyHeaderTest.php @@ -0,0 +1,137 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentSecurityPolicyReportOnlyHeader([]); + + $this->assertSame('Content-Security-Policy-Report-Only', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentSecurityPolicyReportOnlyHeader([]); + + $this->assertSame('', $header->getFieldValue()); + } + + public function testParameterWithoutValue() + { + $header = new ContentSecurityPolicyReportOnlyHeader([ + 'foo' => '', + ]); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithValue() + { + $header = new ContentSecurityPolicyReportOnlyHeader([ + 'foo' => 'bar', + ]); + + $this->assertSame('foo bar', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new ContentSecurityPolicyReportOnlyHeader([ + 'foo' => 1, + ]); + + $this->assertSame('foo 1', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new ContentSecurityPolicyReportOnlyHeader([ + 'foo' => '', + 'bar' => 'bat', + 'baz' => 1, + ]); + + $this->assertSame('foo; bar bat; baz 1', $header->getFieldValue()); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "name=" for the header "Content-Security-Policy-Report-Only" is not valid' + ); + + new ContentSecurityPolicyReportOnlyHeader(['name=' => 'value']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Content-Security-Policy-Report-Only" is not valid' + ); + + new ContentSecurityPolicyReportOnlyHeader([0 => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ";value" for the header "Content-Security-Policy-Report-Only" is not valid' + ); + + new ContentSecurityPolicyReportOnlyHeader(['name' => ';value']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Content-Security-Policy-Report-Only" is not valid' + ); + + new ContentSecurityPolicyReportOnlyHeader(['name' => []]); + } + + public function testBuild() + { + $header = new ContentSecurityPolicyReportOnlyHeader(['foo' => 'bar']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentSecurityPolicyReportOnlyHeader(['foo' => 'bar']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ContentTypeHeaderTest.php b/tests/Header/ContentTypeHeaderTest.php new file mode 100644 index 0000000..3a617e7 --- /dev/null +++ b/tests/Header/ContentTypeHeaderTest.php @@ -0,0 +1,168 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new ContentTypeHeader('foo'); + + $this->assertSame('Content-Type', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new ContentTypeHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithEmptyValue() + { + $header = new ContentTypeHeader('foo', [ + 'bar' => '', + ]); + + $this->assertSame('foo; bar=""', $header->getFieldValue()); + } + + public function testParameterWithToken() + { + $header = new ContentTypeHeader('foo', [ + 'bar' => 'token', + ]); + + $this->assertSame('foo; bar="token"', $header->getFieldValue()); + } + + public function testParameterWithQuotedString() + { + $header = new ContentTypeHeader('foo', [ + 'bar' => 'quoted string', + ]); + + $this->assertSame('foo; bar="quoted string"', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new ContentTypeHeader('foo', [ + 'bar' => 1, + ]); + + $this->assertSame('foo; bar="1"', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new ContentTypeHeader('foo', [ + 'bar' => '', + 'baz' => 'token', + 'bat' => 'quoted string', + 'qux' => 1, + ]); + + $this->assertSame('foo; bar=""; baz="token"; bat="quoted string"; qux="1"', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Content-Type" is not valid'); + + new ContentTypeHeader(''); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Content-Type" is not valid'); + + // isn't a token... + new ContentTypeHeader('@'); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "invalid name" for the header "Content-Type" is not valid' + ); + + // cannot contain spaces... + new ContentTypeHeader('foo', ['invalid name' => 'value']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Content-Type" is not valid' + ); + + // cannot contain spaces... + new ContentTypeHeader('foo', [0 => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ""invalid value"" for the header "Content-Type" is not valid' + ); + + // cannot contain quotes... + new ContentTypeHeader('foo', ['name' => '"invalid value"']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Content-Type" is not valid' + ); + + // cannot contain quotes... + new ContentTypeHeader('foo', ['name' => []]); + } + + public function testBuild() + { + $header = new ContentTypeHeader('foo', ['bar' => 'baz']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new ContentTypeHeader('foo', ['bar' => 'baz']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/CookieHeaderTest.php b/tests/Header/CookieHeaderTest.php new file mode 100644 index 0000000..870edf4 --- /dev/null +++ b/tests/Header/CookieHeaderTest.php @@ -0,0 +1,61 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new CookieHeader(); + + $this->assertSame('Cookie', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new CookieHeader([ + 'foo' => 'bar', + 'bar' => 'baz', + 'baz' => [ + 'qux', + ], + ]); + + $this->assertSame('foo=bar; bar=baz; baz%5B0%5D=qux', $header->getFieldValue()); + } + + public function testBuild() + { + $header = new CookieHeader(['foo' => 'bar']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new CookieHeader(['foo' => 'bar']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/DateHeaderTest.php b/tests/Header/DateHeaderTest.php new file mode 100644 index 0000000..0b95d4c --- /dev/null +++ b/tests/Header/DateHeaderTest.php @@ -0,0 +1,82 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $utc = new \DateTime('utc'); + $header = new DateHeader($utc); + + $this->assertSame('Date', $header->getFieldName()); + } + + public function testFieldValue() + { + $utc = new \DateTime('utc'); + $header = new DateHeader($utc); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + } + + public function testFieldValueWithMutableDateTime() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new DateHeader($now); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testFieldValueWithImmutableDateTime() + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $header = new DateHeader($now); + + $this->assertSame($utc->format(\DateTimeImmutable::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testBuild() + { + $now = new \DateTime('now'); + $header = new DateHeader($now); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $now = new \DateTime('now'); + $header = new DateHeader($now); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/EtagHeaderTest.php b/tests/Header/EtagHeaderTest.php new file mode 100644 index 0000000..71fa8b7 --- /dev/null +++ b/tests/Header/EtagHeaderTest.php @@ -0,0 +1,71 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new EtagHeader('foo'); + + $this->assertSame('ETag', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new EtagHeader('foo'); + + $this->assertSame('"foo"', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $header = new EtagHeader(''); + + $this->assertSame('""', $header->getFieldValue()); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value ""invalid value"" for the header "ETag" is not valid'); + + // cannot contain quotes... + new EtagHeader('"invalid value"'); + } + + public function testBuild() + { + $header = new EtagHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new EtagHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/ExpiresHeaderTest.php b/tests/Header/ExpiresHeaderTest.php new file mode 100644 index 0000000..f06a795 --- /dev/null +++ b/tests/Header/ExpiresHeaderTest.php @@ -0,0 +1,82 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $utc = new \DateTime('utc'); + $header = new ExpiresHeader($utc); + + $this->assertSame('Expires', $header->getFieldName()); + } + + public function testFieldValue() + { + $utc = new \DateTime('utc'); + $header = new ExpiresHeader($utc); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + } + + public function testFieldValueWithMutableDateTime() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new ExpiresHeader($now); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testFieldValueWithImmutableDateTime() + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $header = new ExpiresHeader($now); + + $this->assertSame($utc->format(\DateTimeImmutable::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testBuild() + { + $now = new \DateTime('now'); + $header = new ExpiresHeader($now); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $now = new \DateTime('now'); + $header = new ExpiresHeader($now); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/KeepAliveHeaderTest.php b/tests/Header/KeepAliveHeaderTest.php new file mode 100644 index 0000000..2625b47 --- /dev/null +++ b/tests/Header/KeepAliveHeaderTest.php @@ -0,0 +1,149 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new KeepAliveHeader(); + + $this->assertSame('Keep-Alive', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new KeepAliveHeader(); + + $this->assertSame('', $header->getFieldValue()); + } + + public function testParameterWithEmptyValue() + { + $header = new KeepAliveHeader([ + 'foo' => '', + ]); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithToken() + { + $header = new KeepAliveHeader([ + 'foo' => 'token', + ]); + + $this->assertSame('foo=token', $header->getFieldValue()); + } + + public function testParameterWithQuotedString() + { + $header = new KeepAliveHeader([ + 'foo' => 'quoted string', + ]); + + $this->assertSame('foo="quoted string"', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new KeepAliveHeader([ + 'foo' => 1, + ]); + + $this->assertSame('foo=1', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new KeepAliveHeader([ + 'foo' => '', + 'bar' => 'token', + 'baz' => 'quoted string', + 'qux' => 1, + ]); + + $this->assertSame('foo, bar=token, baz="quoted string", qux=1', $header->getFieldValue()); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "invalid name" for the header "Keep-Alive" is not valid' + ); + + // cannot contain spaces... + new KeepAliveHeader(['invalid name' => 'value']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Keep-Alive" is not valid' + ); + + new KeepAliveHeader([0 => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ""invalid value"" for the header "Keep-Alive" is not valid' + ); + + // cannot contain quotes... + new KeepAliveHeader(['name' => '"invalid value"']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Keep-Alive" is not valid' + ); + + new KeepAliveHeader(['name' => []]); + } + + public function testBuild() + { + $header = new KeepAliveHeader(['foo' => 'bar']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new KeepAliveHeader(['foo' => 'bar']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/LastModifiedHeaderTest.php b/tests/Header/LastModifiedHeaderTest.php new file mode 100644 index 0000000..a85ec4c --- /dev/null +++ b/tests/Header/LastModifiedHeaderTest.php @@ -0,0 +1,82 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $utc = new \DateTime('utc'); + $header = new LastModifiedHeader($utc); + + $this->assertSame('Last-Modified', $header->getFieldName()); + } + + public function testFieldValue() + { + $utc = new \DateTime('utc'); + $header = new LastModifiedHeader($utc); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + } + + public function testFieldValueWithMutableDateTime() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new LastModifiedHeader($now); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testFieldValueWithImmutableDateTime() + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $header = new LastModifiedHeader($now); + + $this->assertSame($utc->format(\DateTimeImmutable::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testBuild() + { + $now = new \DateTime('now'); + $header = new LastModifiedHeader($now); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $now = new \DateTime('now'); + $header = new LastModifiedHeader($now); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/LinkHeaderTest.php b/tests/Header/LinkHeaderTest.php new file mode 100644 index 0000000..57d86aa --- /dev/null +++ b/tests/Header/LinkHeaderTest.php @@ -0,0 +1,174 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $uri = new Uri('/'); + $header = new LinkHeader($uri); + + $this->assertSame('Link', $header->getFieldName()); + } + + public function testFieldValue() + { + $uri = new Uri('/'); + $header = new LinkHeader($uri); + + $this->assertSame('', $header->getFieldValue()); + } + + public function testParameterWithEmptyValue() + { + $uri = new Uri('/'); + + $header = new LinkHeader($uri, [ + 'foo' => '', + ]); + + $this->assertSame('; foo=""', $header->getFieldValue()); + } + + public function testParameterWithToken() + { + $uri = new Uri('/'); + + $header = new LinkHeader($uri, [ + 'foo' => 'token', + ]); + + $this->assertSame('; foo="token"', $header->getFieldValue()); + } + + public function testParameterWithQuotedString() + { + $uri = new Uri('/'); + + $header = new LinkHeader($uri, [ + 'foo' => 'quoted string', + ]); + + $this->assertSame('; foo="quoted string"', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $uri = new Uri('/'); + + $header = new LinkHeader($uri, [ + 'foo' => 1, + ]); + + $this->assertSame('; foo="1"', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $uri = new Uri('/'); + + $header = new LinkHeader($uri, [ + 'foo' => '', + 'bar' => 'token', + 'baz' => 'quoted string', + 'qux' => 1, + ]); + + $this->assertSame('; foo=""; bar="token"; baz="quoted string"; qux="1"', $header->getFieldValue()); + } + + public function testInvalidParameterName() + { + $uri = new Uri('/'); + + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "invalid name" for the header "Link" is not valid' + ); + + // cannot contain spaces... + new LinkHeader($uri, ['invalid name' => 'value']); + } + + public function testInvalidParameterNameType() + { + $uri = new Uri('/'); + + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "Link" is not valid' + ); + + new LinkHeader($uri, [0 => 'value']); + } + + public function testInvalidParameterValue() + { + $uri = new Uri('/'); + + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ""invalid value"" for the header "Link" is not valid' + ); + + // cannot contain quotes... + new LinkHeader($uri, ['name' => '"invalid value"']); + } + + public function testInvalidParameterValueType() + { + $uri = new Uri('/'); + + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "Link" is not valid' + ); + + // cannot contain quotes... + new LinkHeader($uri, ['name' => []]); + } + + public function testBuild() + { + $uri = new Uri('/'); + $header = new LinkHeader($uri, ['foo' => 'bar']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $uri = new Uri('/'); + $header = new LinkHeader($uri, ['foo' => 'bar']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/LocationHeaderTest.php b/tests/Header/LocationHeaderTest.php new file mode 100644 index 0000000..c684f26 --- /dev/null +++ b/tests/Header/LocationHeaderTest.php @@ -0,0 +1,61 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $uri = new Uri('/'); + $header = new LocationHeader($uri); + + $this->assertSame('Location', $header->getFieldName()); + } + + public function testFieldValue() + { + $uri = new Uri('/'); + $header = new LocationHeader($uri); + + $this->assertSame('/', $header->getFieldValue()); + } + + public function testBuild() + { + $uri = new Uri('/'); + $header = new LocationHeader($uri); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $uri = new Uri('/'); + $header = new LocationHeader($uri); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/RefreshHeaderTest.php b/tests/Header/RefreshHeaderTest.php new file mode 100644 index 0000000..fa9ad0c --- /dev/null +++ b/tests/Header/RefreshHeaderTest.php @@ -0,0 +1,71 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $uri = new Uri('/'); + $header = new RefreshHeader(0, $uri); + + $this->assertSame('Refresh', $header->getFieldName()); + } + + public function testFieldValue() + { + $uri = new Uri('/'); + $header = new RefreshHeader(0, $uri); + + $this->assertSame('0; url=/', $header->getFieldValue()); + } + + public function testInvalidDelay() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The delay "-1" for the header "Refresh" is not valid'); + + $uri = new Uri('/'); + + new RefreshHeader(-1, $uri); + } + + public function testBuild() + { + $uri = new Uri('/'); + $header = new RefreshHeader(0, $uri); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $uri = new Uri('/'); + $header = new RefreshHeader(0, $uri); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/RetryAfterHeaderTest.php b/tests/Header/RetryAfterHeaderTest.php new file mode 100644 index 0000000..643f180 --- /dev/null +++ b/tests/Header/RetryAfterHeaderTest.php @@ -0,0 +1,82 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $utc = new \DateTime('utc'); + $header = new RetryAfterHeader($utc); + + $this->assertSame('Retry-After', $header->getFieldName()); + } + + public function testFieldValue() + { + $utc = new \DateTime('utc'); + $header = new RetryAfterHeader($utc); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + } + + public function testFieldValueWithMutableDateTime() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new RetryAfterHeader($now); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testFieldValueWithImmutableDateTime() + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $header = new RetryAfterHeader($now); + + $this->assertSame($utc->format(\DateTimeImmutable::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testBuild() + { + $now = new \DateTime('now'); + $header = new RetryAfterHeader($now); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $now = new \DateTime('now'); + $header = new RetryAfterHeader($now); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/SetCookieHeaderTest.php b/tests/Header/SetCookieHeaderTest.php new file mode 100644 index 0000000..9abfa85 --- /dev/null +++ b/tests/Header/SetCookieHeaderTest.php @@ -0,0 +1,257 @@ +assertSame('Lax', SetCookieHeader::SAME_SITE_LAX); + $this->assertSame('Strict', SetCookieHeader::SAME_SITE_STRICT); + $this->assertSame('None', SetCookieHeader::SAME_SITE_NONE); + } + + public function testContracts() + { + $header = new SetCookieHeader('name', 'value'); + + $this->assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new SetCookieHeader('name', 'value'); + + $this->assertSame('Set-Cookie', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new SetCookieHeader('name', 'value'); + + $this->assertSame('name=value; Path=/; HttpOnly; SameSite=Lax', $header->getFieldValue()); + } + + public function testEncodingName() + { + $header = new SetCookieHeader('@foo', 'bar'); + + $this->assertSame('%40foo=bar; Path=/; HttpOnly; SameSite=Lax', $header->getFieldValue()); + } + + public function testEncodingValue() + { + $header = new SetCookieHeader('foo', '@bar'); + + $this->assertSame('foo=%40bar; Path=/; HttpOnly; SameSite=Lax', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $dt = new \DateTime('-1 year', new \DateTimeZone('UTC')); + $header = new SetCookieHeader('name', ''); + + $expected = \sprintf( + 'name=deleted; Expires=%s; Max-Age=0; Path=/; HttpOnly; SameSite=Lax', + $dt->format(\DateTime::RFC822) + ); + + $this->assertSame($expected, $header->getFieldValue()); + } + + public function testExpiresWithMutableDateTime() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new SetCookieHeader('name', 'value', $now); + + $expected = \sprintf( + 'name=value; Expires=%s; Max-Age=0; Path=/; HttpOnly; SameSite=Lax', + $utc->format(\DateTime::RFC822) + ); + + $this->assertSame($expected, $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testExpiresWithImmutableDateTime() + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $header = new SetCookieHeader('name', 'value', $now); + + $expected = \sprintf( + 'name=value; Expires=%s; Max-Age=0; Path=/; HttpOnly; SameSite=Lax', + $utc->format(\DateTimeImmutable::RFC822) + ); + + $this->assertSame($expected, $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testNegativeExpires() + { + $utc = new \DateTime('-30 seconds', new \DateTimeZone('UTC')); + $header = new SetCookieHeader('name', 'value', $utc); + + // the max-age attribute cannot be negative... + $expected = \sprintf( + 'name=value; Expires=%s; Max-Age=0; Path=/; HttpOnly; SameSite=Lax', + $utc->format(\DateTime::RFC822) + ); + + $this->assertSame($expected, $header->getFieldValue()); + } + + public function testPositiveExpires() + { + $utc = new \DateTime('+30 seconds', new \DateTimeZone('UTC')); + $header = new SetCookieHeader('name', 'value', $utc); + + $expected = \sprintf( + 'name=value; Expires=%s; Max-Age=30; Path=/; HttpOnly; SameSite=Lax', + $utc->format(\DateTime::RFC822) + ); + + $this->assertSame($expected, $header->getFieldValue()); + } + + public function testPath() + { + $header = new SetCookieHeader('name', 'value', null, [ + 'path' => '/assets/', + ]); + + $this->assertSame('name=value; Path=/assets/; HttpOnly; SameSite=Lax', $header->getFieldValue()); + } + + public function testDomain() + { + $header = new SetCookieHeader('name', 'value', null, [ + 'domain' => 'acme.com', + ]); + + $this->assertSame('name=value; Path=/; Domain=acme.com; HttpOnly; SameSite=Lax', $header->getFieldValue()); + } + + public function testSecure() + { + $header = new SetCookieHeader('name', 'value', null, [ + 'secure' => true, + ]); + + $this->assertSame('name=value; Path=/; Secure; HttpOnly; SameSite=Lax', $header->getFieldValue()); + } + + public function testHttpOnly() + { + $header = new SetCookieHeader('name', 'value', null, [ + 'httpOnly' => false, + ]); + + $this->assertSame('name=value; Path=/; SameSite=Lax', $header->getFieldValue()); + } + + public function testSameSite() + { + $header = new SetCookieHeader('name', 'value', null, [ + 'sameSite' => 'Strict', + ]); + + $this->assertSame('name=value; Path=/; HttpOnly; SameSite=Strict', $header->getFieldValue()); + } + + public function testEmptyName() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cookie name cannot be empty'); + + new SetCookieHeader('', 'value'); + } + + public function testInvalidName() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie name "name=" contains prohibited characters'); + + new SetCookieHeader('name=', 'value'); + } + + public function testInvalidPath() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie option "path" contains prohibited characters'); + + new SetCookieHeader('name', 'value', null, ['path' => ';']); + } + + public function testInvalidPathDataType() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie option "path" must be a string'); + + new SetCookieHeader('name', 'value', null, ['path' => []]); + } + + public function testInvalidDomain() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie option "domain" contains prohibited characters'); + + new SetCookieHeader('name', 'value', null, ['domain' => ';']); + } + + public function testInvalidDomainDataType() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie option "domain" must be a string'); + + new SetCookieHeader('name', 'value', null, ['domain' => []]); + } + + public function testInvalidSamesite() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie option "sameSite" contains prohibited characters'); + + new SetCookieHeader('name', 'value', null, ['sameSite' => ';']); + } + + public function testInvalidSamesiteDataType() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The cookie option "sameSite" must be a string'); + + new SetCookieHeader('name', 'value', null, ['sameSite' => []]); + } + + public function testBuild() + { + $header = new SetCookieHeader('name', 'value'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new SetCookieHeader('name', 'value'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/SunsetHeaderTest.php b/tests/Header/SunsetHeaderTest.php new file mode 100644 index 0000000..02df8bf --- /dev/null +++ b/tests/Header/SunsetHeaderTest.php @@ -0,0 +1,82 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $utc = new \DateTime('utc'); + $header = new SunsetHeader($utc); + + $this->assertSame('Sunset', $header->getFieldName()); + } + + public function testFieldValue() + { + $utc = new \DateTime('utc'); + $header = new SunsetHeader($utc); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + } + + public function testFieldValueWithMutableDateTime() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new SunsetHeader($now); + + $this->assertSame($utc->format(\DateTime::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testFieldValueWithImmutableDateTime() + { + $now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $header = new SunsetHeader($now); + + $this->assertSame($utc->format(\DateTimeImmutable::RFC822), $header->getFieldValue()); + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testBuild() + { + $now = new \DateTime('now'); + $header = new SunsetHeader($now); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $now = new \DateTime('now'); + $header = new SunsetHeader($now); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/TrailerHeaderTest.php b/tests/Header/TrailerHeaderTest.php new file mode 100644 index 0000000..1614191 --- /dev/null +++ b/tests/Header/TrailerHeaderTest.php @@ -0,0 +1,78 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new TrailerHeader('foo'); + + $this->assertSame('Trailer', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new TrailerHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The value "" for the header "Trailer" is not valid' + ); + + new TrailerHeader(''); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The value "@" for the header "Trailer" is not valid' + ); + + // isn't a token... + new TrailerHeader('@'); + } + + public function testBuild() + { + $header = new TrailerHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new TrailerHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/TransferEncodingHeaderTest.php b/tests/Header/TransferEncodingHeaderTest.php new file mode 100644 index 0000000..b5bd0a4 --- /dev/null +++ b/tests/Header/TransferEncodingHeaderTest.php @@ -0,0 +1,104 @@ +assertSame('chunked', TransferEncodingHeader::CHUNKED); + $this->assertSame('compress', TransferEncodingHeader::COMPRESS); + $this->assertSame('deflate', TransferEncodingHeader::DEFLATE); + $this->assertSame('gzip', TransferEncodingHeader::GZIP); + } + + public function testContracts() + { + $header = new TransferEncodingHeader('foo'); + + $this->assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new TransferEncodingHeader('foo'); + + $this->assertSame('Transfer-Encoding', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new TransferEncodingHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new TransferEncodingHeader('foo', 'bar', 'baz'); + + $this->assertSame('foo, bar, baz', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Transfer-Encoding" is not valid'); + + new TransferEncodingHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Transfer-Encoding" is not valid'); + + new TransferEncodingHeader('foo', '', 'baz'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Transfer-Encoding" is not valid'); + + // isn't a token... + new TransferEncodingHeader('@'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Transfer-Encoding" is not valid'); + + // isn't a token... + new TransferEncodingHeader('foo', '@', 'baz'); + } + + public function testBuild() + { + $header = new TransferEncodingHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new TransferEncodingHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/VaryHeaderTest.php b/tests/Header/VaryHeaderTest.php new file mode 100644 index 0000000..8e37012 --- /dev/null +++ b/tests/Header/VaryHeaderTest.php @@ -0,0 +1,96 @@ +assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new VaryHeader('foo'); + + $this->assertSame('Vary', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new VaryHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testSeveralValues() + { + $header = new VaryHeader('foo', 'bar', 'baz'); + + $this->assertSame('foo, bar, baz', $header->getFieldValue()); + } + + public function testEmptyValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Vary" is not valid'); + + new VaryHeader(''); + } + + public function testEmptyValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Vary" is not valid'); + + new VaryHeader('foo', '', 'baz'); + } + + public function testInvalidValue() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Vary" is not valid'); + + // isn't a token... + new VaryHeader('@'); + } + + public function testInvalidValueAmongOthers() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Vary" is not valid'); + + // isn't a token... + new VaryHeader('foo', '@', 'baz'); + } + + public function testBuild() + { + $header = new VaryHeader('foo'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new VaryHeader('foo'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/WWWAuthenticateHeaderTest.php b/tests/Header/WWWAuthenticateHeaderTest.php new file mode 100644 index 0000000..388dc82 --- /dev/null +++ b/tests/Header/WWWAuthenticateHeaderTest.php @@ -0,0 +1,180 @@ +assertSame('Basic', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_BASIC); + $this->assertSame('Bearer', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_BEARER); + $this->assertSame('Digest', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_DIGEST); + $this->assertSame('HOBA', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_HOBA); + $this->assertSame('Mutual', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_MUTUAL); + $this->assertSame('Negotiate', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_NEGOTIATE); + $this->assertSame('OAuth', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_OAUTH); + $this->assertSame('SCRAM-SHA-1', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_SCRAM_SHA_1); + $this->assertSame('SCRAM-SHA-256', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_SCRAM_SHA_256); + $this->assertSame('vapid', WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_VAPID); + } + + public function testContracts() + { + $header = new WWWAuthenticateHeader('foo'); + + $this->assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new WWWAuthenticateHeader('foo'); + + $this->assertSame('WWW-Authenticate', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new WWWAuthenticateHeader('foo'); + + $this->assertSame('foo', $header->getFieldValue()); + } + + public function testParameterWithEmptyValue() + { + $header = new WWWAuthenticateHeader('foo', [ + 'bar' => '', + ]); + + $this->assertSame('foo bar=""', $header->getFieldValue()); + } + + public function testParameterWithToken() + { + $header = new WWWAuthenticateHeader('foo', [ + 'bar' => 'token', + ]); + + $this->assertSame('foo bar="token"', $header->getFieldValue()); + } + + public function testParameterWithQuotedString() + { + $header = new WWWAuthenticateHeader('foo', [ + 'bar' => 'quoted string', + ]); + + $this->assertSame('foo bar="quoted string"', $header->getFieldValue()); + } + + public function testParameterWithInteger() + { + $header = new WWWAuthenticateHeader('foo', [ + 'bar' => 1, + ]); + + $this->assertSame('foo bar="1"', $header->getFieldValue()); + } + + public function testSeveralParameters() + { + $header = new WWWAuthenticateHeader('foo', [ + 'bar' => '', + 'baz' => 'token', + 'bat' => 'quoted string', + 'qux' => 1, + ]); + + $this->assertSame('foo bar="", baz="token", bat="quoted string", qux="1"', $header->getFieldValue()); + } + + public function testEmptyScheme() + { + $this->expectException(\InvalidArgumentException::class); + + new WWWAuthenticateHeader(''); + } + + public function testInvalidScheme() + { + $this->expectException(\InvalidArgumentException::class); + + // isn't a token... + new WWWAuthenticateHeader('@'); + } + + public function testInvalidParameterName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "invalid name" for the header "WWW-Authenticate" is not valid' + ); + + // cannot contain spaces... + new WWWAuthenticateHeader('foo', ['invalid name' => 'value']); + } + + public function testInvalidParameterNameType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter name "" for the header "WWW-Authenticate" is not valid' + ); + + // cannot contain spaces... + new WWWAuthenticateHeader('foo', [0 => 'value']); + } + + public function testInvalidParameterValue() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value ""invalid value"" for the header "WWW-Authenticate" is not valid' + ); + + // cannot contain quotes... + new WWWAuthenticateHeader('foo', ['name' => '"invalid value"']); + } + + public function testInvalidParameterValueType() + { + $this->expectException(\InvalidArgumentException::class); + + $this->expectExceptionMessage( + 'The parameter value "" for the header "WWW-Authenticate" is not valid' + ); + + // cannot contain quotes... + new WWWAuthenticateHeader('foo', ['name' => []]); + } + + public function testBuild() + { + $header = new WWWAuthenticateHeader('foo', ['bar' => 'baz']); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new WWWAuthenticateHeader('foo', ['bar' => 'baz']); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Header/WarningHeaderTest.php b/tests/Header/WarningHeaderTest.php new file mode 100644 index 0000000..fb884f5 --- /dev/null +++ b/tests/Header/WarningHeaderTest.php @@ -0,0 +1,134 @@ +assertSame(110, WarningHeader::HTTP_WARNING_CODE_RESPONSE_IS_STALE); + $this->assertSame(111, WarningHeader::HTTP_WARNING_CODE_REVALIDATION_FAILED); + $this->assertSame(112, WarningHeader::HTTP_WARNING_CODE_DISCONNECTED_OPERATION); + $this->assertSame(113, WarningHeader::HTTP_WARNING_CODE_HEURISTIC_EXPIRATION); + $this->assertSame(199, WarningHeader::HTTP_WARNING_CODE_MISCELLANEOUS_WARNING); + $this->assertSame(214, WarningHeader::HTTP_WARNING_CODE_TRANSFORMATION_APPLIED); + $this->assertSame(299, WarningHeader::HTTP_WARNING_CODE_MISCELLANEOUS_PERSISTENT_WARNING); + } + + public function testContracts() + { + $header = new WarningHeader(199, 'agent', 'text'); + + $this->assertInstanceOf(HeaderInterface::class, $header); + } + + public function testFieldName() + { + $header = new WarningHeader(199, 'agent', 'text'); + + $this->assertSame('Warning', $header->getFieldName()); + } + + public function testFieldValue() + { + $header = new WarningHeader(199, 'agent', 'text'); + + $this->assertSame('199 agent "text"', $header->getFieldValue()); + } + + public function testFieldValueWithDate() + { + $now = new \DateTime('now', new \DateTimeZone('Europe/Moscow')); + $utc = new \DateTime('now', new \DateTimeZone('UTC')); + + $header = new WarningHeader(199, 'agent', 'text', $now); + + $this->assertSame( + \sprintf( + '199 agent "text" "%s"', + $utc->format(\DateTime::RFC822) + ), + $header->getFieldValue() + ); + + // cannot be modified... + $this->assertSame('Europe/Moscow', $now->getTimezone()->getName()); + } + + public function testCodeLessThat100() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The code "99" for the header "Warning" is not valid'); + + new WarningHeader(99, 'agent', 'text'); + } + + public function testCodeGreaterThat999() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The code "1000" for the header "Warning" is not valid'); + + new WarningHeader(1000, 'agent', 'text'); + } + + public function testEmptyAgent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "" for the header "Warning" is not valid'); + + new WarningHeader(199, '', 'text'); + } + + public function testInvalidAgent() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value "@" for the header "Warning" is not valid'); + + // isn't a token... + new WarningHeader(199, '@', 'text'); + } + + public function testEmptyText() + { + $header = new WarningHeader(199, 'agent', ''); + + $this->assertSame('199 agent ""', $header->getFieldValue()); + } + + public function testInvalidText() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value ""text"" for the header "Warning" is not valid'); + + // cannot contain quotes... + new WarningHeader(199, 'agent', '"text"'); + } + + public function testBuild() + { + $header = new WarningHeader(199, 'agent', 'text'); + + $expected = \sprintf('%s: %s', $header->getFieldName(), $header->getFieldValue()); + + $this->assertSame($expected, $header->__toString()); + } + + public function testIterator() + { + $header = new WarningHeader(199, 'agent', 'text'); + + $this->assertSame( + [ + $header->getFieldName(), + $header->getFieldValue(), + ], + \iterator_to_array($header->getIterator()) + ); + } +} diff --git a/tests/Integration/RequestIntegrationTest.php b/tests/Integration/RequestIntegrationTest.php new file mode 100644 index 0000000..8544ae1 --- /dev/null +++ b/tests/Integration/RequestIntegrationTest.php @@ -0,0 +1,21 @@ +assertInstanceOf(MessageInterface::class, $mess); - } - - /** - * @return void - */ - public function testConstructor() : void - { - $headers = ['X-Foo' => ['bar', 'baz'], 'X-Bar' => ['baz']]; - $body = (new StreamFactory)->createStreamFromResource(\STDOUT); - $protocol = '2.0'; - - $mess = new Message($headers, $body, $protocol); - - $this->assertSame($headers, $mess->getHeaders()); - $this->assertSame($body, $mess->getBody()); - $this->assertSame($protocol, $mess->getProtocolVersion()); - } - - /** - * @return void - */ - public function testProtocolVersion() : void - { - $mess = new Message(); - $copy = $mess->withProtocolVersion('2.0'); - - $this->assertInstanceOf(MessageInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - - // default value - $this->assertSame('1.1', $mess->getProtocolVersion()); - // assigned value - $this->assertSame('2.0', $copy->getProtocolVersion()); - } - - /** - * @dataProvider invalidProtocolVersionProvider - * - * @return void - */ - public function testInvalidProtocolVersion($protocolVersion) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withProtocolVersion($protocolVersion); - } - - /** - * @return void - */ - public function testSetHeader() : void - { - $mess = new Message(); - $copy = $mess->withHeader('X-Foo', 'bar'); - - $this->assertInstanceOf(MessageInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - $this->assertSame([], $mess->getHeaders()); - $this->assertSame(['X-Foo' => ['bar']], $copy->getHeaders()); - } - - /** - * @return void - */ - public function testSetHeaderWithSeveralValues() : void - { - $mess = (new Message)->withHeader('X-Foo', ['bar', 'baz']); - - $this->assertSame(['X-Foo' => ['bar', 'baz']], $mess->getHeaders()); - } - - /** - * @return void - */ - public function testSetSeveralHeaders() : void - { - $mess = (new Message) - ->withHeader('X-Foo', ['bar', 'baz']) - ->withHeader('X-Quux', ['quuux', 'quuuux']); - - $this->assertSame([ - 'X-Foo' => ['bar', 'baz'], - 'X-Quux' => ['quuux', 'quuuux'], - ], $mess->getHeaders()); - } - - /** - * @return void - */ - public function testSetHeaderLowercase() : void - { - $mess = (new Message)->withHeader('x-foo', 'bar'); - - $this->assertSame(['X-Foo' => ['bar']], $mess->getHeaders()); - } - - /** - * @dataProvider invalidHeaderNameProvider - * - * @return void - */ - public function testSetInvalidHeaderName($headerName) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withHeader($headerName, 'bar'); - } - - /** - * @dataProvider invalidHeaderValueProvider - * - * @return void - */ - public function testSetInvalidHeaderValue($headerValue) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withHeader('X-Foo', $headerValue); - } - - /** - * @dataProvider invalidHeaderValueProvider - * - * @return void - */ - public function testSetInvalidHeaderValueItem($headerValue) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withHeader('X-Foo', ['bar', $headerValue, 'baz']); - } - - /** - * @return void - */ - public function testAddHeader() : void - { - $mess = (new Message)->withHeader('X-Foo', 'bar'); - $copy = $mess->withAddedHeader('X-Foo', 'baz'); - - $this->assertInstanceOf(MessageInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - $this->assertSame(['X-Foo' => ['bar']], $mess->getHeaders()); - $this->assertSame(['X-Foo' => ['bar', 'baz']], $copy->getHeaders()); - } - - /** - * @return void - */ - public function testAddHeaderWithSeveralValues() : void - { - $mess = (new Message) - ->withHeader('X-Foo', ['bar', 'baz']) - ->withAddedHeader('X-Foo', ['quux', 'quuux']); - - $this->assertSame(['X-Foo' => ['bar', 'baz', 'quux', 'quuux']], $mess->getHeaders()); - } - - /** - * @return void - */ - public function testAddSeveralHeaders() : void - { - $mess = (new Message) - ->withHeader('X-Foo', 'bar') - ->withHeader('X-Baz', 'quux') - ->withAddedHeader('X-Foo', 'quuux') - ->withAddedHeader('X-Baz', 'quuuux'); - - $this->assertSame([ - 'X-Foo' => ['bar', 'quuux'], - 'X-Baz' => ['quux', 'quuuux'], - ], $mess->getHeaders()); - } - - /** - * @return void - */ - public function testAddHeaderLowercase() : void - { - $mess = (new Message) - ->withHeader('x-foo', 'bar') - ->withAddedHeader('x-foo', 'baz'); - - $this->assertSame(['X-Foo' => ['bar', 'baz']], $mess->getHeaders()); - } - - /** - * @dataProvider invalidHeaderNameProvider - * - * @return void - */ - public function testAddInvalidHeaderName($headerName) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withAddedHeader($headerName, 'bar'); - } - - /** - * @dataProvider invalidHeaderValueProvider - * - * @return void - */ - public function testAddInvalidHeaderValue($headerValue) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withAddedHeader('X-Foo', $headerValue); - } - - /** - * @dataProvider invalidHeaderValueProvider - * - * @return void - */ - public function testAddInvalidHeaderValueItem($headerValue) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Message)->withAddedHeader('X-Foo', ['bar', $headerValue, 'baz']); - } - - /** - * @return void - */ - public function testDeleteHeader() : void - { - $mess = (new Message)->withHeader('X-Foo', 'bar'); - $copy = $mess->withoutHeader('X-Foo'); - - $this->assertInstanceOf(MessageInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - $this->assertSame(['X-Foo' => ['bar']], $mess->getHeaders()); - $this->assertSame([], $copy->getHeaders()); - } - - /** - * @return void - */ - public function testDeleteHeaderCaseInsensitive() : void - { - $mess = (new Message) - ->withHeader('x-foo', 'bar') - ->withoutHeader('X-Foo'); - - $this->assertSame([], $mess->getHeaders()); - } - - /** - * @return void - */ - public function testReplaceHeader() : void - { - $mess = (new Message)->withHeader('X-Foo', 'bar'); - $copy = $mess->withHeader('X-Foo', 'baz'); - - $this->assertSame(['X-Foo' => ['bar']], $mess->getHeaders()); - $this->assertSame(['X-Foo' => ['baz']], $copy->getHeaders()); - } - - /** - * @return void - */ - public function testReplaceHeaderCaseInsensitive() : void - { - $mess = (new Message) - ->withHeader('x-foo', 'bar') - ->withHeader('X-Foo', 'baz'); - - $this->assertSame(['X-Foo' => ['baz']], $mess->getHeaders()); - } - - /** - * @return void - */ - public function testHasHeader() : void - { - $mess = (new Message)->withHeader('X-Foo', 'bar'); - - $this->assertTrue($mess->hasHeader('X-Foo')); - $this->assertFalse($mess->hasHeader('X-Bar')); - } - - /** - * @return void - */ - public function testHasHeaderCaseInsensitive() : void - { - $mess = (new Message)->withHeader('x-foo', 'bar'); - - $this->assertTrue($mess->hasHeader('x-foo')); - $this->assertTrue($mess->hasHeader('X-Foo')); - $this->assertTrue($mess->hasHeader('X-FOO')); - } - - /** - * @return void - */ - public function testGetHeader() : void - { - $mess = (new Message)->withHeader('X-Foo', 'bar'); - - $this->assertSame(['bar'], $mess->getHeader('X-Foo')); - $this->assertSame([], $mess->getHeader('X-Bar')); - } - - /** - * @return void - */ - public function testGetHeaderCaseInsensitive() : void - { - $mess = (new Message)->withHeader('x-foo', 'bar'); - - $this->assertSame(['bar'], $mess->getHeader('x-foo')); - $this->assertSame(['bar'], $mess->getHeader('X-Foo')); - $this->assertSame(['bar'], $mess->getHeader('X-FOO')); - } - - /** - * @return void - */ - public function testGetHeaderWithSeveralValues() : void - { - $mess = (new Message)->withHeader('X-Foo', ['bar', 'baz', 'quux']); - - $this->assertSame(['bar', 'baz', 'quux'], $mess->getHeader('X-Foo')); - } - - /** - * @return void - */ - public function testGetHeaderLine() : void - { - $mess = (new Message)->withHeader('X-Foo', 'bar'); - - $this->assertSame('bar', $mess->getHeaderLine('X-Foo')); - $this->assertSame('', $mess->getHeaderLine('X-Bar')); - } - - /** - * @return void - */ - public function testGetHeaderLineCaseInsensitive() : void - { - $mess = (new Message)->withHeader('x-foo', 'bar'); - - $this->assertSame('bar', $mess->getHeaderLine('x-foo')); - $this->assertSame('bar', $mess->getHeaderLine('X-Foo')); - $this->assertSame('bar', $mess->getHeaderLine('X-FOO')); - } - - /** - * @return void - */ - public function testGetHeaderLineWithSeveralValues() : void - { - $mess = (new Message)->withHeader('X-Foo', ['bar', 'baz', 'quux']); - - $this->assertSame('bar, baz, quux', $mess->getHeaderLine('X-Foo')); - } - - /** - * @return void - */ - public function testBody() : void - { - $body = (new StreamFactory)->createStreamFromResource(\STDOUT); - $mess = new Message(); - $copy = $mess->withBody($body); - - $this->assertInstanceOf(MessageInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - - // default value - $this->assertNotSame($body, $mess->getBody()); - // assigned value - $this->assertSame($body, $copy->getBody()); - } - - // Providers... - - /** - * @return array - */ - public function invalidProtocolVersionProvider() : array - { - return [ - [''], - ['.'], - ['1.'], - ['.1'], - ['1.1.'], - ['.1.1'], - ['1.1.1'], - ['a'], - ['a.'], - ['.a'], - ['a.a'], - ['HTTP/1.1'], - - // other types - [true], - [false], - [1], - [1.1], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - ]; - } - - /** - * @return array - */ - public function invalidHeaderNameProvider() : array - { - return [ - [''], - ['x foo'], - ['x-foo:'], - ["x\0foo"], - ["x\tfoo"], - ["x\rfoo"], - ["x\nfoo"], - - // other types - [true], - [false], - [1], - [1.1], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - ]; - } - - /** - * @return array - */ - public function invalidHeaderValueProvider() : array - { - return [ - ["field \0 value"], - ["field \r value"], - ["field \n value"], - [["field \0 value"]], - [["field \r value"]], - [["field \n value"]], - - // other types - [true], - [false], - [1], - [1.1], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - - [[true]], - [[false]], - [[1]], - [[1.1]], - [[[]]], - [[new \stdClass]], - [[\STDOUT]], - [[null]], - [[function () { - }]], - ]; - } -} diff --git a/tests/RequestFactoryTest.php b/tests/RequestFactoryTest.php index db4474b..8672209 100644 --- a/tests/RequestFactoryTest.php +++ b/tests/RequestFactoryTest.php @@ -4,91 +4,74 @@ namespace Sunrise\Http\Message\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamInterface; -use Psr\Http\Message\UriInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; use Sunrise\Http\Message\RequestFactory; -use Sunrise\Uri\UriFactory; +use Sunrise\Http\Message\Uri; -/** - * RequestFactoryTest - */ class RequestFactoryTest extends TestCase { - - /** - * @return void - */ - public function testConstructor() : void + public function testContracts(): void { $factory = new RequestFactory(); $this->assertInstanceOf(RequestFactoryInterface::class, $factory); } - /** - * @return void - */ - public function testCreateRequest() : void + public function testCreateRequest(): void { - $method = 'POST'; - $uri = (new UriFactory)->createUri('/'); - $request = (new RequestFactory)->createRequest($method, $uri); - - $this->assertInstanceOf(RequestInterface::class, $request); - $this->assertSame($method, $request->getMethod()); - $this->assertSame($uri, $request->getUri()); - - // default body of the request... - $this->assertInstanceOf(StreamInterface::class, $request->getBody()); - $this->assertTrue($request->getBody()->isSeekable()); - $this->assertTrue($request->getBody()->isWritable()); - $this->assertTrue($request->getBody()->isReadable()); - $this->assertSame('php://temp', $request->getBody()->getMetadata('uri')); + $uri = new Uri(); + + $subject = (new RequestFactory)->createRequest('POST', $uri); + + $this->assertSame('POST', $subject->getMethod()); + $this->assertSame($uri, $subject->getUri()); } - /** - * @return void - */ - public function testCreateRequestWithUriAsString() : void + public function testCreateRequestWithLowerCaseMethod(): void { - $uri = 'http://user:password@localhost:3000/path?query#fragment'; - $request = (new RequestFactory)->createRequest('GET', $uri); + $this->assertSame('post', (new RequestFactory) + ->createRequest('post', new Uri()) + ->getMethod()); + } - $this->assertInstanceOf(UriInterface::class, $request->getUri()); - $this->assertSame($uri, (string) $request->getUri()); + public function testCreateRequestWithNonStandardMethod(): void + { + $this->assertSame('CUSTOM', (new RequestFactory) + ->createRequest('CUSTOM', new Uri()) + ->getMethod()); } - /** - * @return void - */ - public function testCreateJsonRequest() : void + public function testCreateRequestWithEmptyMethod(): void { - $payload = ['foo' => '']; - $options = \JSON_HEX_TAG; + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP method cannot be an empty'); - $request = (new RequestFactory)->createJsonRequest('GET', '/foo', $payload, $options); + (new RequestFactory)->createRequest('', new Uri()); + } - $this->assertInstanceOf(RequestInterface::class, $request); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame('/foo', (string) $request->getUri()); - $this->assertSame('application/json; charset=UTF-8', $request->getHeaderLine('Content-Type')); - $this->assertSame(\json_encode($payload, $options), (string) $request->getBody()); + public function testCreateRequestWithInvalidMethod(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP method'); + + (new RequestFactory)->createRequest("GET\0", '/'); + } + + public function testCreateRequestWithStringUri(): void + { + $this->assertSame('/foo', (new RequestFactory) + ->createRequest('GET', '/foo') + ->getUri() + ->__toString()); } - /** - * @return void - */ - public function testCreateJsonRequestWithInvalidJson() : void + public function testCreateRequestWithInvalidUri(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Maximum stack depth exceeded'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to parse URI'); - $request = (new RequestFactory)->createJsonRequest('GET', '/', [[]], 0, 1); + (new RequestFactory)->createRequest('GET', ':'); } } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index f56a2ab..1e96cc4 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -4,381 +4,34 @@ namespace Sunrise\Http\Message\Tests; -/** - * Import classes - */ -use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; -use Sunrise\Http\Message\Message; +use Psr\Http\Message\StreamInterface; use Sunrise\Http\Message\Request; -use Sunrise\Stream\StreamFactory; -use Sunrise\Uri\UriFactory; -/** - * RequestTest - */ -class RequestTest extends TestCase +class RequestTest extends BaseRequestTest { - - /** - * @return void - */ - public function testContracts() : void - { - $mess = new Request(); - - $this->assertInstanceOf(Message::class, $mess); - $this->assertInstanceOf(RequestInterface::class, $mess); - } - - /** - * @return void - */ - public function testConstructor() : void - { - $method = 'POST'; - $uri = '/foo?bar'; - $headers = ['X-Foo' => ['bar', 'baz'], 'X-Bar' => ['baz']]; - $body = (new StreamFactory)->createStreamFromResource(\STDOUT); - $target = '/bar?baz'; - $protocol = '2.0'; - - $mess = new Request( - $method, - $uri, - $headers, - $body, - $target, - $protocol - ); - - $this->assertSame($method, $mess->getMethod()); - $this->assertSame('/foo', $mess->getUri()->getPath()); - $this->assertSame('bar', $mess->getUri()->getQuery()); - $this->assertSame($headers, $mess->getHeaders()); - $this->assertSame($body, $mess->getBody()); - $this->assertSame($target, $mess->getRequestTarget()); - $this->assertSame($protocol, $mess->getProtocolVersion()); - } - - /** - * @return void - */ - public function testMethod() : void - { - $mess = new Request(); - $copy = $mess->withMethod('POST'); - - $this->assertInstanceOf(Message::class, $copy); - $this->assertInstanceOf(RequestInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - - // default value - $this->assertSame('GET', $mess->getMethod()); - // assigned value - $this->assertSame('POST', $copy->getMethod()); - } - - /** - * @return void - */ - public function testLowercasedMethod() : void - { - $mess = (new Request)->withMethod('post'); - - $this->assertSame('POST', $mess->getMethod()); - } - - /** - * @dataProvider figMethodProvider - * - * @return void - */ - public function testFigMethod($method) : void - { - $mess = (new Request)->withMethod($method); - - $this->assertSame($method, $mess->getMethod()); - } - - /** - * @dataProvider invalidMethodProvider - * - * @return void - */ - public function testInvalidMethod($method) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Request)->withMethod($method); - } - - /** - * @return void - */ - public function testRequestTarget() : void - { - $mess = new Request(); - $copy = $mess->withRequestTarget('/path?query'); - - $this->assertInstanceOf(Message::class, $copy); - $this->assertInstanceOf(RequestInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - - // default value - $this->assertSame('/', $mess->getRequestTarget()); - // assigned value - $this->assertSame('/path?query', $copy->getRequestTarget()); - } - - /** - * @dataProvider uriFormProvider - * - * @return void - */ - public function testRequestTargetWithDifferentUriForms($requestTarget) : void - { - $mess = (new Request)->withRequestTarget($requestTarget); - - $this->assertSame($requestTarget, $mess->getRequestTarget()); - } - - /** - * @dataProvider invalidRequestTargetProvider - * - * @return void - */ - public function testInvalidRequestTarget($requestTarget) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Request)->withRequestTarget($requestTarget); - } - - /** - * @return void - */ - public function testGetRequestTargetHavingUriWithoutPath() : void - { - $uri = (new UriFactory)->createUri('http://localhost'); - $mess = (new Request)->withUri($uri); - - // returns "/" as default path - $this->assertSame('/', $mess->getRequestTarget()); - } - - /** - * @return void - */ - public function testGetRequestTargetHavingUriWithNotAbsolutePath() : void + protected function createSubject(): RequestInterface { - $uri = (new UriFactory)->createUri('not/absolute/path?query'); - $mess = (new Request)->withUri($uri); - - // returns "/" as default path - $this->assertSame('/', $mess->getRequestTarget()); - } - - /** - * @return void - */ - public function testGetRequestTargetHavingUriWithAbsolutePath() : void - { - $uri = (new UriFactory)->createUri('/path'); - $mess = (new Request)->withUri($uri); - - $this->assertSame('/path', $mess->getRequestTarget()); - } - - /** - * @return void - */ - public function testGetRequestTargetHavingUriWithAbsolutePathAndQuery() : void - { - $uri = (new UriFactory)->createUri('/path?query'); - $mess = (new Request)->withUri($uri); - - $this->assertSame('/path?query', $mess->getRequestTarget()); - } - - /** - * @return void - */ - public function testGetRequestTargetIgnoringNewUri() : void - { - $uri = (new UriFactory)->createUri('/new'); - $mess = (new Request)->withRequestTarget('/primary')->withUri($uri); - - $this->assertSame('/primary', $mess->getRequestTarget()); - } - - /** - * @return void - */ - public function testUri() : void - { - $uri = (new UriFactory)->createUri('/'); - $mess = new Request(); - $copy = $mess->withUri($uri); - - $this->assertInstanceOf(Message::class, $copy); - $this->assertInstanceOf(RequestInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - - // default value - $this->assertNotSame($uri, $mess->getUri()); - // assigned value - $this->assertSame($uri, $copy->getUri()); - } - - /** - * @return void - */ - public function testUriWithAssigningHostHeaderFromUriHost() : void - { - $uri = (new UriFactory)->createUri('http://localhost'); - $mess = (new Request)->withUri($uri); - - $this->assertSame($uri->getHost(), $mess->getHeaderLine('host')); + return new Request(); } - /** - * @return void - */ - public function testUriWithAssigningHostHeaderFromUriHostAndPort() : void + protected function createSubjectWithMethod(string $method): RequestInterface { - $uri = (new UriFactory)->createUri('http://localhost:3000'); - $mess = (new Request)->withUri($uri); - - $this->assertSame($uri->getHost() . ':' . $uri->getPort(), $mess->getHeaderLine('host')); - } - - /** - * @return void - */ - public function testUriWithReplacingHostHeaderFromUri() : void - { - $uri = (new UriFactory)->createUri('http://localhost'); - $mess = (new Request)->withHeader('host', 'example.com')->withUri($uri); - - $this->assertSame($uri->getHost(), $mess->getHeaderLine('host')); - } - - /** - * @return void - */ - public function testUriWithPreservingHostHeader() : void - { - $uri = (new UriFactory)->createUri('http://localhost'); - $mess = (new Request)->withHeader('host', 'example.com')->withUri($uri, true); - - $this->assertSame('example.com', $mess->getHeaderLine('host')); - } - - /** - * @return void - */ - public function testUriWithPreservingHostHeaderIfItIsEmpty() : void - { - $uri = (new UriFactory)->createUri('http://localhost'); - $mess = (new Request)->withUri($uri, true); - - $this->assertSame($uri->getHost(), $mess->getHeaderLine('host')); + return new Request($method); } - // Providers... - - /** - * @return array - */ - public function figMethodProvider() : array + protected function createSubjectWithUri($uri): RequestInterface { - return [ - ['HEAD'], - ['GET'], - ['POST'], - ['PUT'], - ['PATCH'], - ['DELETE'], - ['PURGE'], - ['OPTIONS'], - ['TRACE'], - ['CONNECT'], - ]; + return new Request(null, $uri); } - /** - * @return array - */ - public function invalidMethodProvider() : array + protected function createSubjectWithHeaders(array $headers): RequestInterface { - return [ - [''], - ["BAR\0BAZ"], - ["BAR\tBAZ"], - ["BAR\nBAZ"], - ["BAR\rBAZ"], - ["BAR BAZ"], - ["BAR,BAZ"], - - // other types - [true], - [false], - [1], - [1.1], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - ]; + return new Request(null, null, $headers); } - /** - * @return array - */ - public function invalidRequestTargetProvider() : array + protected function createSubjectWithBody(StreamInterface $body): RequestInterface { - return [ - [''], - ["/path\0/"], - ["/path\t/"], - ["/path\n/"], - ["/path\r/"], - ["/path /"], - - // other types - [true], - [false], - [1], - [1.1], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - ]; - } - - /** - * @return array - */ - public function uriFormProvider() : array - { - return [ - // https://tools.ietf.org/html/rfc7230#section-5.3.1 - ['/path?query'], - - // https://tools.ietf.org/html/rfc7230#section-5.3.2 - ['http://localhost/path?query'], - - // https://tools.ietf.org/html/rfc7230#section-5.3.3 - ['localhost:3000'], - - // https://tools.ietf.org/html/rfc7230#section-5.3.4 - ['*'], - ]; + return new Request(null, null, null, $body); } } diff --git a/tests/Response/HtmlResponseTest.php b/tests/Response/HtmlResponseTest.php new file mode 100644 index 0000000..84a15ba --- /dev/null +++ b/tests/Response/HtmlResponseTest.php @@ -0,0 +1,54 @@ +assertSame(400, $response->getStatusCode()); + $this->assertSame('text/html; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringStartsWith('php://temp', $response->getBody()->getMetadata('uri')); + $this->assertTrue($response->getBody()->isReadable()); + $this->assertTrue($response->getBody()->isWritable()); + $this->assertSame('foo', $response->getBody()->__toString()); + } + + public function testConstructorWithStringableHtml(): void + { + $response = new HtmlResponse(200, new class + { + public function __toString(): string + { + return 'foo'; + } + }); + + $this->assertSame('foo', $response->getBody()->__toString()); + } + + public function testConstructorWithUnexpectedHtml(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to create HTML response due to invalid body'); + + new HtmlResponse(200, null); + } + + public function testConstructorWithStreamBody(): void + { + $html = $this->createMock(StreamInterface::class); + $response = new HtmlResponse(200, $html); + + $this->assertSame($html, $response->getBody()); + } +} diff --git a/tests/Response/JsonResponseTest.php b/tests/Response/JsonResponseTest.php new file mode 100644 index 0000000..3d2dc9e --- /dev/null +++ b/tests/Response/JsonResponseTest.php @@ -0,0 +1,53 @@ +assertSame(400, $response->getStatusCode()); + $this->assertSame('application/json; charset=utf-8', $response->getHeaderLine('Content-Type')); + $this->assertStringStartsWith('php://temp', $response->getBody()->getMetadata('uri')); + $this->assertTrue($response->getBody()->isReadable()); + $this->assertTrue($response->getBody()->isWritable()); + $this->assertSame('[]', $response->getBody()->__toString()); + } + + public function testConstructorWithJsonFlags(): void + { + $response = new JsonResponse(200, [], JSON_FORCE_OBJECT); + + $this->assertSame('{}', $response->getBody()->__toString()); + } + + public function testConstructorWithInvalidJson(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Unable to create JSON response due to invalid JSON data: ' . + 'Maximum stack depth exceeded' + ); + + new JsonResponse(200, [], 0, 0); + } + + public function testConstructorWithStreamBody(): void + { + $body = $this->createMock(StreamInterface::class); + $response = new JsonResponse(200, $body); + + $this->assertSame($body, $response->getBody()); + } +} diff --git a/tests/ResponseFactoryTest.php b/tests/ResponseFactoryTest.php index a592709..2be7cc9 100644 --- a/tests/ResponseFactoryTest.php +++ b/tests/ResponseFactoryTest.php @@ -4,96 +4,97 @@ namespace Sunrise\Http\Message\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; +use Sunrise\Http\Message\Exception\InvalidArgumentException; use Sunrise\Http\Message\ResponseFactory; -/** - * ResponseFactoryTest - */ class ResponseFactoryTest extends TestCase { - - /** - * @return void - */ - public function testConstructor() : void + public function testContracts(): void { $factory = new ResponseFactory(); $this->assertInstanceOf(ResponseFactoryInterface::class, $factory); } - /** - * @return void - */ - public function testCreateResponse() : void + public function testCreateResponse(): void + { + $response = (new ResponseFactory)->createResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('OK', $response->getReasonPhrase()); + } + + public function testCreateResponseWithStatusCode(): void + { + $response = (new ResponseFactory)->createResponse(202); + + $this->assertSame(202, $response->getStatusCode()); + $this->assertSame('Accepted', $response->getReasonPhrase()); + } + + public function testCreateResponseWithStatusCodeAndEmptyReasonPhrase(): void + { + $response = (new ResponseFactory)->createResponse(202, ''); + + $this->assertSame(202, $response->getStatusCode()); + $this->assertSame('Accepted', $response->getReasonPhrase()); + } + + public function testCreateResponseWithStatusCodeAndCustomReasonPhrase(): void + { + $response = (new ResponseFactory)->createResponse(202, 'Custom Reason Phrase'); + + $this->assertSame(202, $response->getStatusCode()); + $this->assertSame('Custom Reason Phrase', $response->getReasonPhrase()); + } + + public function testCreateResponseWithUnknownStatusCode(): void { - $statusCode = 204; - $reasonPhrase = 'No Content'; - - $response = (new ResponseFactory) - ->createResponse($statusCode, $reasonPhrase); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertSame($statusCode, $response->getStatusCode()); - $this->assertSame($reasonPhrase, $response->getReasonPhrase()); - - // default body of the response... - $this->assertInstanceOf(StreamInterface::class, $response->getBody()); - $this->assertTrue($response->getBody()->isSeekable()); - $this->assertTrue($response->getBody()->isWritable()); - $this->assertTrue($response->getBody()->isReadable()); - $this->assertSame('php://temp', $response->getBody()->getMetadata('uri')); + $response = (new ResponseFactory)->createResponse(599); + + $this->assertSame(599, $response->getStatusCode()); + $this->assertSame('Unknown Status Code', $response->getReasonPhrase()); } - /** - * @return void - */ - public function testCreateHtmlResponse() : void + public function testCreateResponseWithUnknownStatusCodeAndEmptyReasonPhrase(): void { - $content = '
foo bar
'; + $response = (new ResponseFactory)->createResponse(599, ''); - $response = (new ResponseFactory) - ->createHtmlResponse(400, $content); + $this->assertSame(599, $response->getStatusCode()); + $this->assertSame('Unknown Status Code', $response->getReasonPhrase()); + } + + public function testCreateResponseWithUnknownStatusCodeAndReasonPhrase(): void + { + $response = (new ResponseFactory)->createResponse(599, 'Custom Reason Phrase'); - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('text/html; charset=UTF-8', $response->getHeaderLine('Content-Type')); - $this->assertSame($content, (string) $response->getBody()); + $this->assertSame(599, $response->getStatusCode()); + $this->assertSame('Custom Reason Phrase', $response->getReasonPhrase()); } - /** - * @return void - */ - public function testCreateJsonResponse() : void + public function testCreateResponseWithStatusCodeLessThan100(): void { - $payload = ['foo' => '']; - $options = \JSON_HEX_TAG; + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP status code'); - $response = (new ResponseFactory) - ->createJsonResponse(400, $payload, $options); + (new ResponseFactory)->createResponse(99); + } + + public function testCreateResponseWithStatusCodeGreaterThan599(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP status code'); - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); - $this->assertSame(\json_encode($payload, $options), (string) $response->getBody()); + (new ResponseFactory)->createResponse(600); } - /** - * @return void - */ - public function testCreateResponseWithInvalidJson() : void + public function testCreateResponseWithStatusCodeAndInvalidReasonPhrase(): void { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Maximum stack depth exceeded'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP reason phrase'); - $response = (new ResponseFactory) - ->createJsonResponse(200, [[]], 0, 1); + (new ResponseFactory)->createResponse(200, "\0"); } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 0dfabf7..9d6bad4 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -4,171 +4,29 @@ namespace Sunrise\Http\Message\Tests; -/** - * Import classes - */ -use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Message\Message; +use Psr\Http\Message\StreamInterface; use Sunrise\Http\Message\Response; -/** - * Import constants - */ -use const Sunrise\Http\Message\REASON_PHRASES; - -/** - * ResponseTest - */ -class ResponseTest extends TestCase +class ResponseTest extends BaseResponseTest { - - /** - * @return void - */ - public function testConstructor() : void - { - $mess = new Response(); - - $this->assertInstanceOf(Message::class, $mess); - $this->assertInstanceOf(ResponseInterface::class, $mess); - } - - /** - * @return void - */ - public function testStatus() : void - { - $mess = new Response(); - $copy = $mess->withStatus(204); - - $this->assertInstanceOf(Message::class, $copy); - $this->assertInstanceOf(ResponseInterface::class, $copy); - $this->assertNotEquals($mess, $copy); - - // default values - $this->assertSame(200, $mess->getStatusCode()); - $this->assertSame(REASON_PHRASES[200], $mess->getReasonPhrase()); - - // assigned values - $this->assertSame(204, $copy->getStatusCode()); - $this->assertSame(REASON_PHRASES[204], $copy->getReasonPhrase()); - } - - /** - * @dataProvider figStatusProvider - * - * @return void - */ - public function testFigStatus($statusCode, $reasonPhrase) : void - { - $mess = (new Response)->withStatus($statusCode); - - $this->assertSame($statusCode, $mess->getStatusCode()); - $this->assertSame($reasonPhrase, $mess->getReasonPhrase()); - } - - /** - * @dataProvider invalidStatusCodeProvider - * - * @return void - */ - public function testInvalidStatusCode($statusCode) : void + protected function createSubject(): ResponseInterface { - $this->expectException(\InvalidArgumentException::class); - - (new Response)->withStatus($statusCode); - } - - /** - * @return void - */ - public function testUnknownStatusCode() : void - { - $mess = (new Response)->withStatus(599); - - $this->assertSame('Unknown Status Code', $mess->getReasonPhrase()); - } - - /** - * @dataProvider invalidReasonPhraseProvider - * - * @return void - */ - public function testInvalidReasonPhrase($reasonPhrase) : void - { - $this->expectException(\InvalidArgumentException::class); - - (new Response)->withStatus(200, $reasonPhrase); - } - - /** - * @return void - */ - public function testCustomReasonPhrase() : void - { - $mess = (new Response)->withStatus(200, 'test'); - - $this->assertSame('test', $mess->getReasonPhrase()); + return new Response(); } - // Providers... - - /** - * @return array - */ - public function figStatusProvider() : array + protected function createSubjectWithStatus(int $statusCode, string $reasonPhrase = ''): ResponseInterface { - return [ - [200, REASON_PHRASES[200] ?? ''], - ]; + return new Response($statusCode, $reasonPhrase); } - /** - * @return array - */ - public function invalidStatusCodeProvider() : array + protected function createSubjectWithHeaders(array $headers): ResponseInterface { - return [ - [0], - [99], - [600], - - // other types - [true], - [false], - ['100'], - [100.0], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - ]; + return new Response(null, null, $headers); } - /** - * @return array - */ - public function invalidReasonPhraseProvider() : array + protected function createSubjectWithBody(StreamInterface $body): ResponseInterface { - return [ - ["bar\0baz"], - ["bar\nbaz"], - ["bar\rbaz"], - - // other types - [true], - [false], - [1], - [1.1], - [[]], - [new \stdClass], - [\STDOUT], - [null], - [function () { - }], - ]; + return new Response(null, null, null, $body); } } diff --git a/tests/ServerRequestFactoryTest.php b/tests/ServerRequestFactoryTest.php new file mode 100644 index 0000000..c3f2b71 --- /dev/null +++ b/tests/ServerRequestFactoryTest.php @@ -0,0 +1,571 @@ +assertInstanceOf(ServerRequestFactoryInterface::class, $factory); + } + + public function testCreateServerRequest(): void + { + $uri = new Uri(); + + $request = (new ServerRequestFactory)->createServerRequest('POST', $uri); + + $this->assertSame('POST', $request->getMethod()); + $this->assertSame($uri, $request->getUri()); + } + + public function testCreateServerRequestWithLowerCaseMethod(): void + { + $this->assertSame('post', (new ServerRequestFactory) + ->createServerRequest('post', new Uri()) + ->getMethod()); + } + + public function testCreateServerRequestWithNonStandardMethod(): void + { + $this->assertSame('CUSTOM', (new ServerRequestFactory) + ->createServerRequest('CUSTOM', new Uri()) + ->getMethod()); + } + + public function testCreateServerRequestWithEmptyMethod(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP method cannot be an empty'); + + (new ServerRequestFactory)->createServerRequest('', new Uri()); + } + + public function testCreateServerRequestWithInvalidMethod(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP method'); + + (new ServerRequestFactory)->createServerRequest("GET\0", new Uri()); + } + + public function testCreateServerRequestWithStringUri(): void + { + $this->assertSame('/foo', (new ServerRequestFactory) + ->createServerRequest('GET', '/foo') + ->getUri() + ->__toString()); + } + + public function testCreateServerRequestWithInvalidUri(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to parse URI'); + + (new ServerRequestFactory)->createServerRequest('GET', ':'); + } + + public function testCreateServerRequestWithServerParams(): void + { + $this->assertSame(['foo' => 'bar'], (new ServerRequestFactory) + ->createServerRequest('GET', new Uri(), ['foo' => 'bar']) + ->getServerParams()); + } + + /** + * @dataProvider serverParamsWithProtocolVersionProvider + */ + public function testCreateServerRequestWithServerParamsWithProtocolVersion( + array $serverParams, + string $expectedProtocolVersion + ): void { + $this->assertSame($expectedProtocolVersion, (new ServerRequestFactory) + ->createServerRequest('GET', new Uri(), $serverParams) + ->getProtocolVersion()); + } + + public function testCreateServerRequestWithServerParamsWithUnsupportedProtocolVersion(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or unsupported HTTP version'); + + (new ServerRequestFactory)->createServerRequest('GET', new Uri(), ['SERVER_PROTOCOL' => 'HTTP/3']); + } + + /** + * @dataProvider serverParamsProviderWithHeaders + */ + public function testCreateServerRequestWithServerParamsWithHeaders( + array $serverParams, + array $expectedHeaders + ): void { + $this->assertSame($expectedHeaders, (new ServerRequestFactory) + ->createServerRequest('GET', new Uri(), $serverParams) + ->getHeaders()); + } + + public function testCreateServerRequestWithServerParamsWithInvalidHeader(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value is invalid'); + + (new ServerRequestFactory)->createServerRequest('GET', new Uri(), ['HTTP_X_FOO' => "\0"]); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithServerParams(): void + { + $_SERVER = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(); + $this->assertSame($_SERVER, $request->getServerParams()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithQueryParams(): void + { + $_GET = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(); + $this->assertSame($_GET, $request->getQueryParams()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithCookieParams(): void + { + $_COOKIE = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(); + $this->assertSame($_COOKIE, $request->getCookieParams()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithParsedBody(): void + { + $_POST = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(); + $this->assertSame($_POST, $request->getParsedBody()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + * @dataProvider serverParamsWithProtocolVersionProvider + */ + public function testCreateServerRequestFromGlobalsWithProtocolVersion( + array $serverParams, + string $expectedProtocolVersion + ): void { + $_SERVER = $serverParams; + $request = ServerRequestFactory::fromGlobals(); + + $this->assertSame($serverParams, $request->getServerParams()); + $this->assertSame($expectedProtocolVersion, $request->getProtocolVersion()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithUnsupportedProtocolVersion(): void + { + $_SERVER = ['SERVER_PROTOCOL' => 'HTTP/3']; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid or unsupported HTTP version'); + + ServerRequestFactory::fromGlobals(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + * @dataProvider serverParamsWithMethodProvider + */ + public function testCreateServerRequestFromGlobalsWithMethod( + array $serverParams, + string $expectedMethod + ): void { + $_SERVER = $serverParams; + $request = ServerRequestFactory::fromGlobals(); + + $this->assertSame($serverParams, $request->getServerParams()); + $this->assertSame($expectedMethod, $request->getMethod()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithEmptyMethod(): void + { + $_SERVER = ['REQUEST_METHOD' => '']; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('HTTP method cannot be an empty'); + + ServerRequestFactory::fromGlobals(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithInvalidMethod(): void + { + $_SERVER = ['REQUEST_METHOD' => "GET\0"]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid HTTP method'); + + ServerRequestFactory::fromGlobals(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + * @dataProvider serverParamsWithUriProvider + */ + public function testCreateServerRequestFromGlobalsWithUri(array $serverParams, string $expectedUri): void + { + $_SERVER = $serverParams; + $request = ServerRequestFactory::fromGlobals(); + + $this->assertSame($serverParams, $request->getServerParams()); + $this->assertSame($expectedUri, $request->getUri()->__toString()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithInvalidUri(): void + { + $_SERVER = ['HTTP_HOST' => 'localhost:65536']; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to parse URI'); + + ServerRequestFactory::fromGlobals(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + * @dataProvider serverParamsProviderWithHeaders + */ + public function testCreateServerRequestFromGlobalsWithHeaders(array $serverParams, array $expectedHeaders): void + { + $expectedHeaders = ['Host' => ['localhost']] + $expectedHeaders; + + $_SERVER = $serverParams; + $request = ServerRequestFactory::fromGlobals(); + + $this->assertSame($serverParams, $request->getServerParams()); + $this->assertSame($expectedHeaders, $request->getHeaders()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithInvalidHeader(): void + { + $_SERVER = ['HTTP_X_FOO' => "\0"]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The "X-Foo[0]" HTTP header value is invalid'); + + ServerRequestFactory::fromGlobals(); + } + + public function testCreateServerRequestFromGlobalsWithCustomServerParams(): void + { + $serverParams = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals($serverParams); + $this->assertSame($serverParams, $request->getServerParams()); + } + + public function testCreateServerRequestFromGlobalsWithCustomQueryParams(): void + { + $queryParams = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(null, $queryParams); + $this->assertSame($queryParams, $request->getQueryParams()); + } + + public function testCreateServerRequestFromGlobalsWithCustomCookieParams(): void + { + $cookieParams = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(null, null, $cookieParams); + $this->assertSame($cookieParams, $request->getCookieParams()); + } + + public function testCreateServerRequestFromGlobalsWithCustomParsedBody(): void + { + $parsedBody = ['foo' => 'bar']; + $request = ServerRequestFactory::fromGlobals(null, null, null, null, $parsedBody); + $this->assertSame($parsedBody, $request->getParsedBody()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState + */ + public function testCreateServerRequestFromGlobalsWithUploadedFiles(): void + { + $tmpfile = new TmpfileStream(); + $filename = $tmpfile->getMetadata('uri'); + + $_FILES['foo']['tmp_name'] = $filename; + $_FILES['foo']['size'] = 42; + $_FILES['foo']['error'] = UPLOAD_ERR_OK; + $_FILES['foo']['name'] = 'foo.txt'; + $_FILES['foo']['type'] = 'text/foo'; + + $_FILES['bar']['tmp_name'][0] = $filename; + $_FILES['bar']['size'][0] = 42; + $_FILES['bar']['error'][0] = UPLOAD_ERR_OK; + $_FILES['bar']['name'][0] = 'bar.txt'; + $_FILES['bar']['type'][0] = 'text/bar'; + + // must be ignored + $_FILES['baz']['tmp_name'] = $filename; + $_FILES['baz']['size'] = 0; + $_FILES['baz']['error'] = UPLOAD_ERR_NO_FILE; + $_FILES['baz']['name'] = 'baz.txt'; + $_FILES['baz']['type'] = 'text/baz'; + + $uploadedFiles = ServerRequestFactory::fromGlobals()->getUploadedFiles(); + + $this->assertArrayHasKey('foo', $uploadedFiles); + $this->assertSame($_FILES['foo']['tmp_name'], $uploadedFiles['foo']->getStream()->getMetadata('uri')); + $this->assertSame($_FILES['foo']['size'], $uploadedFiles['foo']->getSize()); + $this->assertSame($_FILES['foo']['error'], $uploadedFiles['foo']->getError()); + $this->assertSame($_FILES['foo']['name'], $uploadedFiles['foo']->getClientFilename()); + $this->assertSame($_FILES['foo']['type'], $uploadedFiles['foo']->getClientMediaType()); + + $this->assertArrayHasKey('bar', $uploadedFiles); + $this->assertArrayHasKey(0, $uploadedFiles['bar']); + $this->assertSame($_FILES['bar']['tmp_name'][0], $uploadedFiles['bar'][0]->getStream()->getMetadata('uri')); + $this->assertSame($_FILES['bar']['size'][0], $uploadedFiles['bar'][0]->getSize()); + $this->assertSame($_FILES['bar']['error'][0], $uploadedFiles['bar'][0]->getError()); + $this->assertSame($_FILES['bar']['name'][0], $uploadedFiles['bar'][0]->getClientFilename()); + $this->assertSame($_FILES['bar']['type'][0], $uploadedFiles['bar'][0]->getClientMediaType()); + + $this->assertArrayNotHasKey('baz', $uploadedFiles); + } + + public function testCreateServerRequestFromGlobalsWithCustomUploadedFiles(): void + { + $tmpfile = new TmpfileStream(); + $filename = $tmpfile->getMetadata('uri'); + + $files = [ + 'foo' => [ + 'tmp_name' => $filename, + 'size' => 42, + 'error' => UPLOAD_ERR_OK, + 'name' => 'foo.txt', + 'type' => 'text/foo', + ], + ]; + + $uploadedFiles = ServerRequestFactory::fromGlobals(null, null, null, $files)->getUploadedFiles(); + + $this->assertArrayHasKey('foo', $uploadedFiles); + $this->assertSame($files['foo']['tmp_name'], $uploadedFiles['foo']->getStream()->getMetadata('uri')); + $this->assertSame($files['foo']['size'], $uploadedFiles['foo']->getSize()); + $this->assertSame($files['foo']['error'], $uploadedFiles['foo']->getError()); + $this->assertSame($files['foo']['name'], $uploadedFiles['foo']->getClientFilename()); + $this->assertSame($files['foo']['type'], $uploadedFiles['foo']->getClientMediaType()); + } + + public function serverParamsWithProtocolVersionProvider(): array + { + return [ + [ + ['SERVER_PROTOCOL' => 'HTTP/1.0'], + '1.0', + ], + [ + ['SERVER_PROTOCOL' => 'HTTP/1.1'], + '1.1', + ], + [ + ['SERVER_PROTOCOL' => 'HTTP/2.0'], + '2.0', + ], + [ + ['SERVER_PROTOCOL' => 'HTTP/2'], + '2', + ], + [ + ['SERVER_PROTOCOL' => 'oO'], + '1.1', + ], + ]; + } + + public function serverParamsWithMethodProvider(): array + { + return [ + [ + ['REQUEST_METHOD' => 'POST'], + 'POST', + ], + [ + ['REQUEST_METHOD' => 'post'], + 'post', + ], + [ + ['REQUEST_METHOD' => 'CUSTOM'], + 'CUSTOM', + ], + ]; + } + + public function serverParamsWithUriProvider(): array + { + return [ + [ + [ + ], + 'http://localhost/', + ], + [ + [ + 'HTTPS' => 'off', + ], + 'http://localhost/', + ], + [ + [ + 'HTTPS' => 'on', + ], + 'https://localhost/', + ], + [ + [ + 'HTTP_HOST' => 'example.com', + ], + 'http://example.com/', + ], + [ + [ + 'HTTP_HOST' => 'example.com:3000', + ], + 'http://example.com:3000/', + ], + [ + [ + 'SERVER_NAME' => 'example.com', + ], + 'http://example.com/', + ], + [ + [ + 'SERVER_NAME' => 'example.com', + 'SERVER_PORT' => 3000, + ], + 'http://example.com:3000/', + ], + [ + [ + 'SERVER_PORT' => 3000, + ], + 'http://localhost/', + ], + [ + [ + 'REQUEST_URI' => '/path', + ], + 'http://localhost/path', + ], + [ + [ + 'REQUEST_URI' => '/path?query', + ], + 'http://localhost/path?query', + ], + [ + [ + 'PHP_SELF' => '/path', + ], + 'http://localhost/path', + ], + [ + [ + 'PHP_SELF' => '/path', + 'QUERY_STRING' => 'query', + ], + 'http://localhost/path?query', + ], + [ + [ + 'QUERY_STRING' => 'query', + ], + 'http://localhost/', + ], + ]; + } + + public function serverParamsProviderWithHeaders(): array + { + return [ + [ + [ + 'HTTP_X_FOO' => 'bar', + ], + [ + 'X-Foo' => ['bar'], + ], + ], + [ + [ + 'CONTENT_LENGTH' => '100', + ], + [ + 'Content-Length' => ['100'], + ], + ], + [ + [ + 'CONTENT_TYPE' => 'application/json', + ], + [ + 'Content-Type' => ['application/json'], + ], + ], + [ + [ + 'NON_HEADER_HTTP_TEST' => '', + ], + [ + ], + ], + ]; + } +} diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php new file mode 100644 index 0000000..c0d2bd2 --- /dev/null +++ b/tests/ServerRequestTest.php @@ -0,0 +1,72 @@ +assertInstanceOf(StreamFactoryInterface::class, $factory); + } + + public function testCreateStream(): void + { + $stream = (new StreamFactory)->createStream(); + $this->assertStringStartsWith('php://temp', $stream->getMetadata('uri')); + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->isWritable()); + } + + public function testCreateStreamWithContent(): void + { + $stream = (new StreamFactory)->createStream('foo'); + $this->assertSame(0, $stream->tell()); + $this->assertSame('foo', $stream->getContents()); + } + + public function testCreateStreamFromFile(): void + { + $stream = (new StreamFactory)->createStreamFromFile('php://memory'); + $this->assertSame('php://memory', $stream->getMetadata('uri')); + $this->assertTrue($stream->isReadable()); + $this->assertFalse($stream->isWritable()); + } + + public function testCreateStreamFromFileWithMode(): void + { + $stream = (new StreamFactory)->createStreamFromFile('php://memory', 'r+'); + $this->assertSame('php://memory', $stream->getMetadata('uri')); + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->isWritable()); + } + + public function testCreateStreamFromInvalidFile(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'Unable to open the file "/55EF8096-7A6A-4C85-9BCD-6A5958376AB8" in the mode "r"' + ); + + (new StreamFactory)->createStreamFromFile('/55EF8096-7A6A-4C85-9BCD-6A5958376AB8', 'r'); + } + + public function testCreateStreamFromResource(): void + { + $resource = fopen('php://memory', 'r+b'); + $stream = (new StreamFactory)->createStreamFromResource($resource); + $this->assertSame($resource, $stream->detach()); + fclose($resource); + } + + public function testCreateStreamFromInvalidResource(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unexpected stream resource'); + + (new StreamFactory)->createStreamFromResource(null); + } +} diff --git a/tests/StreamTest.php b/tests/StreamTest.php new file mode 100644 index 0000000..7127f43 --- /dev/null +++ b/tests/StreamTest.php @@ -0,0 +1,515 @@ +testResource = fopen('php://memory', 'r+b'); + $this->testStream = new Stream($this->testResource); + } + + protected function tearDown(): void + { + if (isset($this->testStream)) { + $this->testStream->close(); + } + + if (is_resource($this->testResource)) { + fclose($this->testResource); + } + } + + public function testCreateWithStream(): void + { + $stream = $this->createMock(StreamInterface::class); + $this->assertSame($stream, Stream::create($stream)); + } + + public function testCreateWithResource(): void + { + $this->assertSame($this->testResource, Stream::create($this->testResource)->detach()); + } + + public function testCreateWithUnexpectedOperand(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unexpected stream resource'); + + Stream::create(null); + } + + public function testContracts(): void + { + $this->assertInstanceOf(StreamInterface::class, $this->testStream); + } + + public function testConstructorWithClosedResource(): void + { + fclose($this->testResource); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unexpected stream resource'); + + new Stream($this->testResource); + } + + public function testConstructorWithNotResource(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unexpected stream resource'); + + new Stream(null); + } + + public function testAutoClose(): void + { + $this->testStream = null; + + $this->assertIsClosedResource($this->testResource); + } + + public function testAutoCloseDisabled(): void + { + // no object references, destruction must occur immediately... + new Stream($this->testResource, false); + + $this->assertTrue(is_resource($this->testResource)); + } + + public function testDetach(): void + { + $this->assertSame($this->testResource, $this->testStream->detach()); + $this->assertNull($this->testStream->detach()); + } + + public function testClose(): void + { + $this->testStream->close(); + $this->assertIsClosedResource($this->testResource); + $this->assertNull($this->testStream->detach()); + } + + public function testEof(): void + { + $this->assertFalse($this->testStream->eof()); + + while (!$this->testStream->eof()) { + $this->testStream->read(4096); + } + + $this->assertTrue($this->testStream->eof()); + } + + public function testEofAfterDetach(): void + { + $this->testStream->detach(); + $this->assertTrue($this->testStream->eof()); + } + + public function testEofAfterClose(): void + { + $this->testStream->close(); + $this->assertTrue($this->testStream->eof()); + } + + public function testTell(): void + { + $this->assertSame(0, $this->testStream->tell()); + $this->testStream->write('foo'); + $this->assertSame(3, $this->testStream->tell()); + $this->testStream->seek(1); + $this->assertSame(1, $this->testStream->tell()); + $this->testStream->rewind(); + $this->assertSame(0, $this->testStream->tell()); + } + + public function testTellAfterDetach(): void + { + $this->testStream->detach(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->tell(); + } + + public function testTellAfterClose(): void + { + $this->testStream->close(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->tell(); + } + + public function testFailedTell(): void + { + $testStream = new Stream(STDIN, false); + + $this->expectException(FailedStreamOperationException::class); + $this->expectExceptionMessage('Unable to get the stream pointer position'); + + $testStream->tell(); + } + + public function testIsSeekable(): void + { + $this->assertTrue($this->testStream->isSeekable()); + } + + public function testIsSeekableUnseekableResource(): void + { + $testStream = new Stream(STDIN, false); + $this->assertFalse($testStream->isSeekable()); + } + + public function testIsSeekableAfterDetach(): void + { + $this->testStream->detach(); + $this->assertFalse($this->testStream->isSeekable()); + } + + public function testIsSeekableAfterClose(): void + { + $this->testStream->close(); + $this->assertFalse($this->testStream->isSeekable()); + } + + public function testRewind(): void + { + $this->testStream->write('foo'); + $this->assertSame(3, $this->testStream->tell()); + $this->testStream->rewind(); + $this->assertSame(0, $this->testStream->tell()); + } + + public function testRewindAfterDetach(): void + { + $this->testStream->detach(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->rewind(); + } + + public function testRewindAfterClose(): void + { + $this->testStream->close(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->rewind(); + } + + public function testRewindInUnseekableResource(): void + { + $testStream = new Stream(STDIN, false); + + $this->expectException(InvalidStreamOperationException::class); + $this->expectExceptionMessage('Stream is not seekable'); + + $testStream->rewind(); + } + + public function testSeek(): void + { + $this->testStream->write('foo'); + $this->testStream->seek(1); + $this->assertSame(1, $this->testStream->tell()); + } + + public function testSeekAfterDetach(): void + { + $this->testStream->detach(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->seek(0); + } + + public function testSeekAfterClose(): void + { + $this->testStream->close(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->seek(0); + } + + public function testSeekInUnseekableResource(): void + { + $testStream = new Stream(STDIN, false); + + $this->expectException(InvalidStreamOperationException::class); + $this->expectExceptionMessage('Stream is not seekable'); + + $testStream->seek(0); + } + + public function testFailedSeek(): void + { + $this->expectException(FailedStreamOperationException::class); + $this->expectExceptionMessage('Unable to move the stream pointer position'); + + $this->testStream->seek(1); + } + + public function testIsWritable(): void + { + $this->assertTrue($this->testStream->isWritable()); + } + + public function testIsWritableUnwritableResource(): void + { + $testStream = new Stream(STDIN, false); + $this->assertFalse($testStream->isWritable()); + } + + public function testIsWritableAfterDetach(): void + { + $this->testStream->detach(); + $this->assertFalse($this->testStream->isWritable()); + } + + public function testIsWritableAfterClose(): void + { + $this->testStream->close(); + $this->assertFalse($this->testStream->isWritable()); + } + + public function testWrite(): void + { + $this->assertSame(3, $this->testStream->write('foo')); + $this->testStream->rewind(); + $this->assertSame('foo', $this->testStream->read(3)); + } + + public function testWriteAfterDetach(): void + { + $this->testStream->detach(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->write('foo'); + } + + public function testWriteAfterClose(): void + { + $this->testStream->close(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->write('foo'); + } + + public function testWriteToUnwritableResource(): void + { + $testStream = new Stream(STDIN, false); + + $this->expectException(InvalidStreamOperationException::class); + $this->expectExceptionMessage('Stream is not writable'); + + $testStream->write('foo'); + } + + public function testIsReadable(): void + { + $this->assertTrue($this->testStream->isReadable()); + } + + public function testIsReadableUnreadableResource(): void + { + $testStream = new Stream(STDOUT, false); + $this->assertFalse($testStream->isReadable()); + } + + public function testIsReadableAfterDetach(): void + { + $this->testStream->detach(); + $this->assertFalse($this->testStream->isReadable()); + } + + public function testIsReadableAfterClose(): void + { + $this->testStream->close(); + $this->assertFalse($this->testStream->isReadable()); + } + + public function testRead(): void + { + $this->testStream->write('foo'); + $this->testStream->rewind(); + $this->assertSame('foo', $this->testStream->read(3)); + } + + public function testReadAfterDetach(): void + { + $this->testStream->detach(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->read(1); + } + + public function testReadAfterClose(): void + { + $this->testStream->close(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->read(1); + } + + public function testReadFromUnreadableResource(): void + { + $testStream = new Stream(STDOUT, false); + + $this->expectException(InvalidStreamOperationException::class); + $this->expectExceptionMessage('Stream is not readable'); + + $testStream->read(1); + } + + public function testGetContents(): void + { + $this->testStream->write('foo'); + $this->testStream->rewind(); + $this->assertSame('foo', $this->testStream->getContents()); + } + + public function testGetContentsAfterDetach(): void + { + $this->testStream->detach(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->getContents(); + } + + public function testGetContentsAfterClose(): void + { + $this->testStream->close(); + + $this->expectException(InvalidStreamException::class); + $this->expectExceptionMessage('The stream without a resource so the operation is not possible'); + + $this->testStream->getContents(); + } + + public function testGetContentsFromUnreadableResource(): void + { + $testStream = new Stream(STDOUT, false); + + $this->expectException(InvalidStreamOperationException::class); + $this->expectExceptionMessage('Stream is not readable'); + + $testStream->getContents(); + } + + public function testGetMetaData(): void + { + $this->assertSame( + stream_get_meta_data($this->testResource), + $this->testStream->getMetadata() + ); + } + + public function testGetMetaDataWithKey(): void + { + $this->assertSame( + stream_get_meta_data($this->testResource)['uri'], + $this->testStream->getMetadata('uri') + ); + } + + public function testGetMetaDataWithUnknownKey(): void + { + $this->assertNull($this->testStream->getMetadata('unknown')); + } + + public function testGetMetaDataAfterDetach(): void + { + $this->testStream->detach(); + $this->assertNull($this->testStream->getMetadata()); + } + + public function testGetMetaDataAfterClose(): void + { + $this->testStream->close(); + $this->assertNull($this->testStream->getMetadata()); + } + + public function testGetSize(): void + { + $this->assertSame($this->testStream->write('foo'), $this->testStream->getSize()); + } + + public function testGetSizeAfterDetach(): void + { + $this->testStream->detach(); + $this->assertNull($this->testStream->getSize()); + } + + public function testGetSizeAfterClose(): void + { + $this->testStream->close(); + $this->assertNull($this->testStream->getSize()); + } + + public function testStringify(): void + { + $this->testStream->write('foo'); + $this->assertSame('foo', $this->testStream->__toString()); + } + + public function testStringifyAfterDetach(): void + { + $this->testStream->detach(); + $this->assertSame('', $this->testStream->__toString()); + } + + public function testStringifyAfterClose(): void + { + $this->testStream->close(); + $this->assertSame('', $this->testStream->__toString()); + } + + public function testStringifyForInvalidResource(): void + { + $testStream = new Stream(STDOUT, false); + $this->assertSame('', $testStream->__toString()); + } +} diff --git a/tests/UploadedFileFactoryTest.php b/tests/UploadedFileFactoryTest.php new file mode 100644 index 0000000..e5952b0 --- /dev/null +++ b/tests/UploadedFileFactoryTest.php @@ -0,0 +1,46 @@ +assertInstanceOf(UploadedFileFactoryInterface::class, $factory); + } + + public function testCreateUploadedFileWithAllParameters(): void + { + $stream = new PhpTempStream(); + $file = (new UploadedFileFactory)->createUploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo', 'bar'); + + $this->assertSame($stream, $file->getStream()); + $this->assertSame(0, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('foo', $file->getClientFilename()); + $this->assertSame('bar', $file->getClientMediaType()); + } + + public function testCreateUploadedFileWithRequiredParametersOnly(): void + { + $stream = new PhpTempStream(); + $file = (new UploadedFileFactory)->createUploadedFile($stream); + + $this->assertSame($stream, $file->getStream()); + $this->assertNull($file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertNull($file->getClientFilename()); + $this->assertNull($file->getClientMediaType()); + } +} diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php new file mode 100644 index 0000000..ab6f160 --- /dev/null +++ b/tests/UploadedFileTest.php @@ -0,0 +1,185 @@ +assertInstanceOf(UploadedFileInterface::class, $file); + } + + public function testConstructorWithAllParameters(): void + { + $stream = new PhpTempStream(); + $file = new UploadedFile($stream, 0, UPLOAD_ERR_OK, 'foo', 'bar'); + + $this->assertSame($stream, $file->getStream()); + $this->assertSame(0, $file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertSame('foo', $file->getClientFilename()); + $this->assertSame('bar', $file->getClientMediaType()); + } + + public function testConstructorWithRequiredParametersOnly(): void + { + $stream = new PhpTempStream(); + $file = new UploadedFile($stream); + + $this->assertSame($stream, $file->getStream()); + $this->assertNull($file->getSize()); + $this->assertSame(UPLOAD_ERR_OK, $file->getError()); + $this->assertNull($file->getClientFilename()); + $this->assertNull($file->getClientMediaType()); + } + + /** + * @dataProvider uploadErrorCodeProvider + */ + public function testGetsStreamWithError(int $errorCode): void + { + $file = new UploadedFile(new PhpTempStream(), null, $errorCode); + + $errorMessage = UploadedFile::UPLOAD_ERRORS[$errorCode] ?? UploadedFile::UNKNOWN_ERROR_TEXT; + + $this->expectException(InvalidUploadedFileException::class); + $this->expectExceptionMessage(sprintf( + 'The uploaded file has no a stream due to the error #%d (%s)', + $errorCode, + $errorMessage + )); + + $file->getStream(); + } + + public function testGetsStreamAfterMove(): void + { + $tmpfile = new TmpfileStream(); + + $file = new UploadedFile(new PhpTempStream()); + $file->moveTo($tmpfile->getMetadata('uri')); + + $this->expectException(InvalidUploadedFileException::class); + $this->expectExceptionMessage( + 'The uploaded file has no a stream because it was already moved' + ); + + $file->getStream(); + } + + public function testMove(): void + { + // will be deleted after the move + $srcStream = FileStream::tempFile(); + $srcStream->write('foo'); + $srcPath = $srcStream->getMetadata('uri'); + + // will be deleted automatically + $destStream = new TmpfileStream(); + $destStream->write('bar'); + $destPath = $destStream->getMetadata('uri'); + + $file = new UploadedFile($srcStream); + $file->moveTo($destPath); + $this->assertStringEqualsFile($destPath, 'foo'); + $this->assertFileDoesNotExist($srcPath); + } + + /** + * @dataProvider uploadErrorCodeProvider + */ + public function testMoveWithError(int $errorCode): void + { + $file = new UploadedFile(new PhpTempStream(), null, $errorCode); + + $errorMessage = UploadedFile::UPLOAD_ERRORS[$errorCode] ?? UploadedFile::UNKNOWN_ERROR_TEXT; + + $this->expectException(InvalidUploadedFileException::class); + $this->expectExceptionMessage(sprintf( + 'The uploaded file cannot be moved due to the error #%d (%s)', + $errorCode, + $errorMessage + )); + + $file->moveTo('/foo'); + } + + public function testMoveAfterMove(): void + { + $tmpfile = new TmpfileStream(); + + $file = new UploadedFile(new PhpTempStream()); + $file->moveTo($tmpfile->getMetadata('uri')); + + $this->expectException(InvalidUploadedFileException::class); + $this->expectExceptionMessage( + 'The uploaded file cannot be moved because it was already moved' + ); + + $file->moveTo('/foo'); + } + + public function testMoveUnreadableFile(): void + { + $tmpfile = new TmpfileStream(); + + $file = new UploadedFile(new FileStream($tmpfile->getMetadata('uri'), 'w')); + + $this->expectException(InvalidUploadedFileOperationException::class); + $this->expectExceptionMessage( + 'The uploaded file cannot be moved because it is not readable' + ); + + $file->moveTo('/foo'); + } + + public function testMoveUnwritableDirectory(): void + { + $file = new UploadedFile(new PhpTempStream()); + + $this->expectException(FailedUploadedFileOperationException::class); + $this->expectExceptionMessage( + 'The uploaded file cannot be moved because ' . + 'the directory "/4c32dad5-181f-46b7-a86a-15568e11fdf9" is not writable' + ); + + $file->moveTo('/4c32dad5-181f-46b7-a86a-15568e11fdf9/foo'); + } + + public function uploadErrorCodeProvider(): array + { + return [ + [UPLOAD_ERR_CANT_WRITE], + [UPLOAD_ERR_EXTENSION], + [UPLOAD_ERR_FORM_SIZE], + [UPLOAD_ERR_INI_SIZE], + [UPLOAD_ERR_NO_FILE], + [UPLOAD_ERR_NO_TMP_DIR], + [UPLOAD_ERR_PARTIAL], + [-1], // unknown error... + ]; + } +} diff --git a/tests/UriFactoryTest.php b/tests/UriFactoryTest.php new file mode 100644 index 0000000..c0fcdf8 --- /dev/null +++ b/tests/UriFactoryTest.php @@ -0,0 +1,40 @@ +assertInstanceOf(UriFactoryInterface::class, $factory); + } + + public function testCreateUriWithUri(): void + { + $uri = (new UriFactory)->createUri('/'); + + $this->assertSame('/', $uri->getPath()); + } + + public function testCreateUriWithoutUri(): void + { + $uri = (new UriFactory)->createUri(); + + $this->assertSame('', $uri->getPath()); + } + + public function testCreateUriWithEmptyUri(): void + { + $uri = (new UriFactory)->createUri(''); + + $this->assertSame('', $uri->getPath()); + } +} diff --git a/tests/UriTest.php b/tests/UriTest.php new file mode 100644 index 0000000..a870730 --- /dev/null +++ b/tests/UriTest.php @@ -0,0 +1,676 @@ +assertInstanceOf(UriInterface::class, $uri); + } + + // Constructor... + + public function testConstructorWithUri(): void + { + $uri = new Uri('/'); + + $this->assertSame('/', $uri->__toString()); + } + + public function testConstructorWithoutUri(): void + { + $uri = new Uri(); + + $this->assertSame('', $uri->__toString()); + } + + public function testConstructorWithEmptyUri(): void + { + $uri = new Uri(''); + + $this->assertSame('', $uri->__toString()); + } + + public function testConstructorWithInvalidUri(): void + { + $this->expectException(InvalidUriException::class); + $this->expectExceptionMessage('Unable to parse URI'); + + new Uri(':'); + } + + public function testCreateWithUri(): void + { + $uri = $this->createMock(UriInterface::class); + + $this->assertSame($uri, Uri::create($uri)); + } + + public function testCreateWithStringUri(): void + { + $uri = Uri::create(self::TEST_URI); + + $this->assertSame(self::TEST_URI, $uri->__toString()); + } + + public function testCreateWithUnknownType(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('URI should be a string'); + + Uri::create(null); + } + + // Getters... + + public function testGetScheme(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('scheme', $uri->getScheme()); + } + + public function testGetUserInfo(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('user:password', $uri->getUserInfo()); + } + + public function testGetHost(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('host', $uri->getHost()); + } + + public function testGetPort(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame(3000, $uri->getPort()); + } + + public function testGetPath(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('/path', $uri->getPath()); + } + + public function testGetPathWithoutTwoLeadingSlashes(): void + { + $uri = new Uri('//localhost//path'); + + $this->assertSame('/path', $uri->getPath()); + } + + public function testGetQuery(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('query', $uri->getQuery()); + } + + public function testGetFragment(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('fragment', $uri->getFragment()); + } + + // Withers... + + public function testWithScheme(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withScheme('new-scheme'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('scheme', $uri->getScheme()); + $this->assertSame('new-scheme', $copy->getScheme()); + } + + public function testWithUserInfo(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withUserInfo('new-user', 'new-password'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('user:password', $uri->getUserInfo()); + $this->assertSame('new-user:new-password', $copy->getUserInfo()); + } + + public function testWithUserInfoWithoutPassword(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withUserInfo('new-user'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('user:password', $uri->getUserInfo()); + $this->assertSame('new-user', $copy->getUserInfo()); + } + + public function testWithHost(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withHost('new-host'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('host', $uri->getHost()); + $this->assertSame('new-host', $copy->getHost()); + } + + public function testWithPort(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withPort(80); + + $this->assertNotSame($uri, $copy); + $this->assertSame(3000, $uri->getPort()); + $this->assertSame(80, $copy->getPort()); + } + + public function testWithPath(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withPath('/new-path'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('/path', $uri->getPath()); + $this->assertSame('/new-path', $copy->getPath()); + } + + public function testWithQuery(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withQuery('new-query'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('query', $uri->getQuery()); + $this->assertSame('new-query', $copy->getQuery()); + } + + public function testWithFragment(): void + { + $uri = new Uri(self::TEST_URI); + $copy = $uri->withFragment('new-fragment'); + + $this->assertNotSame($uri, $copy); + $this->assertSame('fragment', $uri->getFragment()); + $this->assertSame('new-fragment', $copy->getFragment()); + } + + // Withers with empty data... + + public function testWithEmptyScheme(): void + { + $uri = (new Uri(self::TEST_URI))->withScheme(''); + + $this->assertSame('', $uri->getScheme()); + } + + public function testWithEmptyUserInfo(): void + { + $uri = (new Uri(self::TEST_URI))->withUserInfo(''); + + $this->assertSame('', $uri->getUserInfo()); + } + + public function testWithEmptyHost(): void + { + $uri = (new Uri(self::TEST_URI))->withHost(''); + + $this->assertSame('', $uri->getHost()); + } + + public function testWithEmptyPort(): void + { + $uri = (new Uri(self::TEST_URI))->withPort(null); + + $this->assertNull($uri->getPort()); + } + + public function testWithEmptyPath(): void + { + $uri = (new Uri(self::TEST_URI))->withPath(''); + + $this->assertSame('', $uri->getPath()); + } + + public function testWithEmptyQuery(): void + { + $uri = (new Uri(self::TEST_URI))->withQuery(''); + + $this->assertSame('', $uri->getQuery()); + } + + public function testWithEmptyFragment(): void + { + $uri = (new Uri(self::TEST_URI))->withFragment(''); + + $this->assertSame('', $uri->getFragment()); + } + + // Withers with invalid data... + + public function testWithInvalidScheme(): void + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('Invalid URI component "scheme"'); + + (new Uri(self::TEST_URI))->withScheme('scheme:'); + } + + public function testWithInvalidUserInfo(): void + { + $uri = (new Uri(self::TEST_URI))->withUserInfo('user:password', 'user:password'); + + $this->assertSame('user%3Apassword:user%3Apassword', $uri->getUserInfo(), '', 0.0, 10, false, true); + } + + public function testWithInvalidHost(): void + { + $uri = (new Uri(self::TEST_URI))->withHost('host:80'); + + // %3A or %3a + $expected = strtolower('host%3A80'); + + $this->assertSame($expected, $uri->getHost(), '', 0.0, 10, false, true); + } + + public function testWithPortLessThanZero(): void + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('Invalid URI component "port"'); + + (new Uri(self::TEST_URI))->withPort(-1); + } + + public function testWithPortEqualsZero(): void + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('Invalid URI component "port"'); + + (new Uri(self::TEST_URI))->withPort(0); + } + + public function testWithPortGreaterThan65535(): void + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('Invalid URI component "port"'); + + (new Uri(self::TEST_URI))->withPort(2 ** 16); + } + + public function testWithInvalidPath(): void + { + $uri = (new Uri(self::TEST_URI))->withPath('/path?query'); + + $this->assertSame('/path%3Fquery', $uri->getPath(), '', 0.0, 10, false, true); + } + + public function testWithInvalidQuery(): void + { + $uri = (new Uri(self::TEST_URI))->withQuery('query#fragment'); + + $this->assertSame('query%23fragment', $uri->getQuery(), '', 0.0, 10, false, true); + } + + public function testWithInvalidFragment(): void + { + $uri = (new Uri(self::TEST_URI))->withFragment('fragment#fragment'); + + $this->assertSame('fragment%23fragment', $uri->getFragment(), '', 0.0, 10, false, true); + } + + // Withers with invalid data types... + + /** + * @dataProvider schemeInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForScheme($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "scheme" must be a string'); + + (new Uri)->withScheme($value); + } + + /** + * @dataProvider userInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForUser($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "user" must be a string'); + + (new Uri)->withUserInfo($value); + } + + /** + * @dataProvider passwordInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForPass($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "password" must be a string'); + + (new Uri)->withUserInfo('user', $value); + } + + /** + * @dataProvider hostInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForHost($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "host" must be a string'); + + (new Uri)->withHost($value); + } + + /** + * @dataProvider portInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForPort($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "port" must be an integer'); + + (new Uri)->withPort($value); + } + + /** + * @dataProvider pathInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForPath($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "path" must be a string'); + + (new Uri)->withPath($value); + } + + /** + * @dataProvider queryInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForQuery($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "query" must be a string'); + + (new Uri)->withQuery($value); + } + + /** + * @dataProvider fragmentInvalidDataTypeProvider + */ + public function testWithInvalidDataTypeForFragment($value) + { + $this->expectException(InvalidUriComponentException::class); + $this->expectExceptionMessage('URI component "fragment" must be a string'); + + (new Uri)->withFragment($value); + } + + // Builders... + + public function testGetAuthority(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame('user:password@host:3000', $uri->getAuthority()); + $this->assertSame('', $uri->withHost('')->getAuthority()); + $this->assertSame('host:3000', $uri->withUserInfo('')->getAuthority()); + $this->assertSame('user@host:3000', $uri->withUserInfo('user')->getAuthority()); + $this->assertSame('user:password@host', $uri->withPort(null)->getAuthority()); + } + + public function testToString(): void + { + $uri = new Uri(self::TEST_URI); + + $this->assertSame(self::TEST_URI, (string) $uri); + } + + // Normalizes... + + public function testNormalizeScheme(): void + { + $uri = new Uri(self::TEST_URI); + + $uri = $uri->withScheme('UPPERCASED-SCHEME'); + + $this->assertSame('uppercased-scheme', $uri->getScheme()); + } + + public function testNormalizeHost(): void + { + $uri = new Uri(self::TEST_URI); + + $uri = $uri->withHost('UPPERCASED-HOST'); + + $this->assertSame('uppercased-host', $uri->getHost()); + } + + // Ignoring the standard ports + + public function testIgnoringStandardPorts(): void + { + $uri = new Uri('http://example.com:80/'); + $this->assertNull($uri->getPort()); + $this->assertSame('example.com', $uri->getAuthority()); + $this->assertSame('http://example.com/', (string) $uri); + + $uri = new Uri('https://example.com:443/'); + $this->assertNull($uri->getPort()); + $this->assertSame('example.com', $uri->getAuthority()); + $this->assertSame('https://example.com/', (string) $uri); + + $uri = new Uri('http://example.com:443/'); + $this->assertSame(443, $uri->getPort()); + $this->assertSame('example.com:443', $uri->getAuthority()); + $this->assertSame('http://example.com:443/', (string) $uri); + + $uri = new Uri('https://example.com:80/'); + $this->assertSame(80, $uri->getPort()); + $this->assertSame('example.com:80', $uri->getAuthority()); + $this->assertSame('https://example.com:80/', (string) $uri); + } + + // Another schemes... + + public function testMailtoScheme(): void + { + $uri = new Uri('mailto:test@example.com'); + + $this->assertSame('mailto', $uri->getScheme()); + $this->assertSame('test@example.com', $uri->getPath()); + } + + public function testMapsScheme(): void + { + $uri = new Uri('maps:?q=112+E+Chapman+Ave+Orange,+CA+92866'); + + $this->assertSame('maps', $uri->getScheme()); + $this->assertSame('q=112+E+Chapman+Ave+Orange,+CA+92866', $uri->getQuery()); + } + + public function testTelScheme(): void + { + $uri = new Uri('tel:+1-816-555-1212'); + + $this->assertSame('tel', $uri->getScheme()); + $this->assertSame('+1-816-555-1212', $uri->getPath()); + } + + public function testUrnScheme(): void + { + $uri = new Uri('urn:oasis:names:specification:docbook:dtd:xml:4.1.2'); + + $this->assertSame('urn', $uri->getScheme()); + $this->assertSame('oasis:names:specification:docbook:dtd:xml:4.1.2', $uri->getPath()); + } + + // Providers... + + public function schemeInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [null], + [function () { + }], + ]; + } + + public function userInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [null], + [function () { + }], + ]; + } + + public function passwordInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [function () { + }], + ]; + } + + public function hostInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [null], + [function () { + }], + ]; + } + + public function portInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + ['a'], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [function () { + }], + ]; + } + + public function pathInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [null], + [function () { + }], + ]; + } + + public function queryInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [null], + [function () { + }], + ]; + } + + public function fragmentInvalidDataTypeProvider(): array + { + return [ + [true], + [false], + [0], + [0.0], + [[]], + [new \stdClass], + [\STDOUT], + [null], + [function () { + }], + ]; + } + + // Issues.. + + public function testIssue31(): void + { + $uri = new Uri('//username@hostname'); + $uri = $uri->withPath('pathname'); + $this->assertSame('//username@hostname/pathname', $uri->__toString()); + + $uri = new Uri('scheme:'); + $uri = $uri->withPath('//pathname'); + $this->assertSame('scheme:/pathname', $uri->__toString()); + + $uri = new Uri('scheme:'); + $uri = $uri->withPath('///pathname'); + $this->assertSame('scheme:/pathname', $uri->__toString()); + } +} From 64ec3819d3bca47b743a4123f36319133d88c63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:18:57 +0100 Subject: [PATCH 03/16] deleted --- .circleci/config.yml | 64 -------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 706d640..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,64 +0,0 @@ -# PHP CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-php/ for more details -# -version: 2 -jobs: - php71: - docker: - - image: cimg/php:7.1 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php72: - docker: - - image: cimg/php:7.2 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php73: - docker: - - image: cimg/php:7.3 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php74: - docker: - - image: cimg/php:7.4 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php80: - docker: - - image: cimg/php:8.0 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php81: - docker: - - image: cimg/php:8.1 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text -workflows: - version: 2 - build: - jobs: - - php71 - - php72 - - php73 - - php74 - - php80 - - php81 From 049f76fa7af03a31c0294f2e00d88ee3af2ca633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:19:32 +0100 Subject: [PATCH 04/16] helpers --- functions/server_request_files.php | 70 +++++++++++++++++++ functions/server_request_headers.php | 60 ++++++++++++++++ functions/server_request_method.php | 29 ++++++++ functions/server_request_protocol_version.php | 52 ++++++++++++++ functions/server_request_uri.php | 66 +++++++++++++++++ 5 files changed, 277 insertions(+) create mode 100644 functions/server_request_files.php create mode 100644 functions/server_request_headers.php create mode 100644 functions/server_request_method.php create mode 100644 functions/server_request_protocol_version.php create mode 100644 functions/server_request_uri.php diff --git a/functions/server_request_files.php b/functions/server_request_files.php new file mode 100644 index 0000000..ce8d050 --- /dev/null +++ b/functions/server_request_files.php @@ -0,0 +1,70 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Sunrise\Http\Message\Stream\FileStream; + +/** + * Import functions + */ +use function is_array; + +/** + * Import constants + */ +use const UPLOAD_ERR_NO_FILE; + +/** + * Gets the request uploaded files + * + * Note that not sent files will not be handled. + * + * @param array|null $rawUploadedFiles + * + * @return array + * + * @link http://php.net/manual/en/reserved.variables.files.php + * @link https://www.php.net/manual/ru/features.file-upload.post-method.php + * @link https://www.php.net/manual/ru/features.file-upload.multiple.php + * @link https://github.com/php/php-src/blob/8c5b41cefb88b753c630b731956ede8d9da30c5d/main/rfc1867.c + */ +function server_request_files(?array $rawUploadedFiles = null): array +{ + $rawUploadedFiles ??= $_FILES; + + $walker = function ($path, $size, $error, $name, $type) use (&$walker) { + if (! is_array($path)) { + return new UploadedFile(new FileStream($path, 'rb'), $size, $error, $name, $type); + } + + $result = []; + foreach ($path as $key => $_) { + if (UPLOAD_ERR_NO_FILE <> $error[$key]) { + $result[$key] = $walker($path[$key], $size[$key], $error[$key], $name[$key], $type[$key]); + } + } + + return $result; + }; + + $result = []; + foreach ($rawUploadedFiles as $key => $file) { + if (UPLOAD_ERR_NO_FILE <> $file['error']) { + $result[$key] = $walker($file['tmp_name'], $file['size'], $file['error'], $file['name'], $file['type']); + } + } + + return $result; +} diff --git a/functions/server_request_headers.php b/functions/server_request_headers.php new file mode 100644 index 0000000..75f461e --- /dev/null +++ b/functions/server_request_headers.php @@ -0,0 +1,60 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import functions + */ +use function strncmp; +use function strtolower; +use function strtr; +use function substr; +use function ucwords; + +/** + * Gets the request headers + * + * @param array|null $serverParams + * + * @return array + * + * @link http://php.net/manual/en/reserved.variables.server.php + * @link https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.18 + */ +function server_request_headers(?array $serverParams = null): array +{ + $serverParams ??= $_SERVER; + + // https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.2 + if (!isset($serverParams['HTTP_CONTENT_LENGTH']) && isset($serverParams['CONTENT_LENGTH'])) { + $serverParams['HTTP_CONTENT_LENGTH'] = $serverParams['CONTENT_LENGTH']; + } + + // https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.3 + if (!isset($serverParams['HTTP_CONTENT_TYPE']) && isset($serverParams['CONTENT_TYPE'])) { + $serverParams['HTTP_CONTENT_TYPE'] = $serverParams['CONTENT_TYPE']; + } + + $result = []; + foreach ($serverParams as $key => $value) { + if (0 <> strncmp('HTTP_', $key, 5)) { + continue; + } + + $name = strtr(substr($key, 5), '_', '-'); + $name = ucwords(strtolower($name), '-'); + + $result[$name] = $value; + } + + return $result; +} diff --git a/functions/server_request_method.php b/functions/server_request_method.php new file mode 100644 index 0000000..8ebcb21 --- /dev/null +++ b/functions/server_request_method.php @@ -0,0 +1,29 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Gets the request method + * + * @param array|null $serverParams + * + * @return string + * + * @link http://php.net/manual/en/reserved.variables.server.php + * @link https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.12 + */ +function server_request_method(?array $serverParams = null): string +{ + $serverParams ??= $_SERVER; + + return $serverParams['REQUEST_METHOD'] ?? Request::METHOD_GET; +} diff --git a/functions/server_request_protocol_version.php b/functions/server_request_protocol_version.php new file mode 100644 index 0000000..2edc6ab --- /dev/null +++ b/functions/server_request_protocol_version.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import functions + */ +use function sprintf; +use function sscanf; + +/** + * Gets the request protocol version + * + * @param array|null $serverParams + * + * @return string + * + * @link http://php.net/manual/en/reserved.variables.server.php + * @link https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.16 + */ +function server_request_protocol_version(?array $serverParams = null): string +{ + $serverParams ??= $_SERVER; + + if (!isset($serverParams['SERVER_PROTOCOL'])) { + return '1.1'; + } + + // "HTTP" "/" 1*digit "." 1*digit + sscanf($serverParams['SERVER_PROTOCOL'], 'HTTP/%d.%d', $major, $minor); + + // e.g.: HTTP/1.1 + if (isset($minor)) { + return sprintf('%d.%d', $major, $minor); + } + + // e.g.: HTTP/2 + if (isset($major)) { + return sprintf('%d', $major); + } + + return '1.1'; +} diff --git a/functions/server_request_uri.php b/functions/server_request_uri.php new file mode 100644 index 0000000..dd3f9e4 --- /dev/null +++ b/functions/server_request_uri.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +/** + * Import classes + */ +use Psr\Http\Message\UriInterface; + +/** + * Import functions + */ +use function array_key_exists; + +/** + * Gets the request URI + * + * @param array|null $serverParams + * + * @return UriInterface + * + * @link http://php.net/manual/en/reserved.variables.server.php + */ +function server_request_uri(?array $serverParams = null): UriInterface +{ + $serverParams ??= $_SERVER; + + if (array_key_exists('HTTPS', $serverParams)) { + if (! ('off' === $serverParams['HTTPS'])) { + $scheme = 'https://'; + } + } + + if (array_key_exists('HTTP_HOST', $serverParams)) { + $host = $serverParams['HTTP_HOST']; + } elseif (array_key_exists('SERVER_NAME', $serverParams)) { + $host = $serverParams['SERVER_NAME']; + if (array_key_exists('SERVER_PORT', $serverParams)) { + $host .= ':' . $serverParams['SERVER_PORT']; + } + } + + if (array_key_exists('REQUEST_URI', $serverParams)) { + $target = $serverParams['REQUEST_URI']; + } elseif (array_key_exists('PHP_SELF', $serverParams)) { + $target = $serverParams['PHP_SELF']; + if (array_key_exists('QUERY_STRING', $serverParams)) { + $target .= '?' . $serverParams['QUERY_STRING']; + } + } + + return new Uri( + ($scheme ?? 'http://') . + ($host ?? 'localhost') . + ($target ?? '/') + ); +} From 3e25d61b9d6282feea1c152fd4f258bd54e5c73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:19:44 +0100 Subject: [PATCH 05/16] update .scrutinizer.yml --- .scrutinizer.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 1e6a96f..9194104 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,16 +1,36 @@ build: - environment: - php: - version: '8.0' + image: default-bionic nodes: analysis: + environment: + php: 8.2 tests: override: - php-scrutinizer-run coverage: + environment: + php: 8.2 tests: override: - command: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-clover coverage.xml coverage: file: coverage.xml format: clover + php80: + environment: + php: 8.1 + tests: + override: + - command: php vendor/bin/phpunit + php80: + environment: + php: 8.0 + tests: + override: + - command: php vendor/bin/phpunit + php74: + environment: + php: 7.4 + tests: + override: + - command: php vendor/bin/phpunit From d81d65b1feaeaff5a9f1669a5419edc5968e4699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:19:53 +0100 Subject: [PATCH 06/16] update README.md --- README.md | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index dfbe5e5..4f33291 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HTTP message wrapper for PHP 7.1+ based on RFC-7230, PSR-7 and PSR-17 -[![Build Status](https://circleci.com/gh/sunrise-php/http-message.svg?style=shield)](https://circleci.com/gh/sunrise-php/http-message) +[![Build Status](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/build.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/build-status/master) [![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) [![Total Downloads](https://poser.pugx.org/sunrise/http-message/downloads?format=flat)](https://packagist.org/packages/sunrise/http-message) @@ -12,41 +12,9 @@ ## Installation ```bash -composer require 'sunrise/http-message:^2.0' +composer require sunrise/http-message ``` -## How to use? - -#### Request message - -```php -use Sunrise\Http\Message\RequestFactory; - -$message = (new RequestFactory)->createRequest('GET', 'http://php.net/'); - -// just use PSR-7 methods... -``` - -#### Response message - -```php -use Sunrise\Http\Message\ResponseFactory; - -$message = (new ResponseFactory)->createResponse(200, 'OK'); - -// just use PSR-7 methods... -``` - -#### Related packages - -* https://github.com/sunrise-php/http-server-request -* https://github.com/sunrise-php/stream -* https://github.com/sunrise-php/uri - -#### Headers as objects - -* https://github.com/sunrise-php/http-header-kit - --- ## Test run From 98bc410cd9c0a166b15217d1d3b104842d729f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:20:02 +0100 Subject: [PATCH 07/16] update composer.json --- composer.json | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 0015c24..7a5cd1d 100644 --- a/composer.json +++ b/composer.json @@ -10,43 +10,53 @@ "message", "request", "response", + "header", + "stream", + "uri", + "upload", "rfc-7230", "psr-7", - "psr-17", - "php-7", - "php-8" + "psr-17" ], "authors": [ { - "name": "Anatoly Fenric", + "name": "Anatoly Nekhay", "email": "afenric@gmail.com", "homepage": "https://github.com/fenric" } ], "provide": { - "psr/http-message-implementation": "1.0" + "psr/http-message-implementation": "1.0", + "psr/http-factory-implementation": "1.0" }, "require": { - "php": "^7.1|^8.0", + "php": ">=7.4", "fig/http-message-util": "^1.1", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0", - "sunrise/http-header": "^2.0", - "sunrise/stream": "^1.2", - "sunrise/uri": "^1.2" + "psr/http-message": "^1.0" }, "require-dev": { - "phpunit/phpunit": "7.5.20|9.5.0", - "sunrise/coding-standard": "1.0.0" + "phpunit/phpunit": "~9.5.0", + "sunrise/coding-standard": "~1.0.0", + "php-http/psr7-integration-tests": "^1.1" }, "autoload": { "files": [ - "constants/REASON_PHRASES.php" + "functions/server_request_files.php", + "functions/server_request_headers.php", + "functions/server_request_method.php", + "functions/server_request_protocol_version.php", + "functions/server_request_uri.php" ], "psr-4": { "Sunrise\\Http\\Message\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Sunrise\\Http\\Message\\Tests\\": "tests/" + } + }, "scripts": { "test": [ "phpcs", From 3eaf7ddc2e9d602d6f6ce70edfa38a75f43c5b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:20:09 +0100 Subject: [PATCH 08/16] update phpcs.xml.dist --- phpcs.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index b1d5376..8b51380 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,7 +2,7 @@ - constants + functions src tests From e49305f5eaa887f8005f0ce72a62af6945f7e5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:20:18 +0100 Subject: [PATCH 09/16] update phpunit.xml.dist --- phpunit.xml.dist | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 20348be..07147dc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,6 +5,7 @@ > + ./functions ./src @@ -13,4 +14,12 @@ ./tests/ + + + + + + + + From fe50ac21f16d10251e632adf6153555b9d9b0501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:20:29 +0100 Subject: [PATCH 10/16] deleted --- constants/REASON_PHRASES.php | 93 ------------------------------------ 1 file changed, 93 deletions(-) delete mode 100644 constants/REASON_PHRASES.php diff --git a/constants/REASON_PHRASES.php b/constants/REASON_PHRASES.php deleted file mode 100644 index cb91cc1..0000000 --- a/constants/REASON_PHRASES.php +++ /dev/null @@ -1,93 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-message - */ - -namespace Sunrise\Http\Message; - -/** - * List of Reason Phrases - * - * @var array - * - * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - */ -const REASON_PHRASES = [ - - // 1xx - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - 103 => 'Early Hints', - - // 2xx - 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', - - // 3xx - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 307 => 'Temporary Redirect', - 308 => 'Permanent Redirect', - - // 4xx - 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 => 'Payload Too Large', - 414 => 'URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Range Not Satisfiable', - 417 => 'Expectation Failed', - 421 => 'Misdirected Request', - 422 => 'Unprocessable Entity', - 423 => 'Locked', - 424 => 'Failed Dependency', - 425 => 'Too Early', - 426 => 'Upgrade Required', - 428 => 'Precondition Required', - 429 => 'Too Many Requests', - 431 => 'Request Header Fields Too Large', - 451 => 'Unavailable For Legal Reasons', - - // 5xx - 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', - 511 => 'Network Authentication Required', -]; From 1535bf11f3d1d46c6eef93e4e35774fe383101da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Thu, 29 Dec 2022 04:23:21 +0100 Subject: [PATCH 11/16] update .scrutinizer.yml --- .scrutinizer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 9194104..ed0219b 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -16,7 +16,7 @@ build: coverage: file: coverage.xml format: clover - php80: + php81: environment: php: 8.1 tests: From b5f5883baf065563a88bf0aba01eecb691656a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Fri, 30 Dec 2022 17:34:42 +0100 Subject: [PATCH 12/16] update composer.json --- composer.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 7a5cd1d..3a6b2da 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "sunrise/http-message", "homepage": "https://github.com/sunrise-php/http-message", - "description": "HTTP message wrapper for PHP 7.1+ based on RFC-7230, PSR-7 and PSR-17", + "description": "HTTP message wrapper for PHP 7.4+ based on RFC-7230, PSR-7 and PSR-17", "license": "MIT", "keywords": [ "fenric", @@ -36,8 +36,8 @@ "psr/http-message": "^1.0" }, "require-dev": { - "phpunit/phpunit": "~9.5.0", "sunrise/coding-standard": "~1.0.0", + "phpunit/phpunit": "~9.5.0", "php-http/psr7-integration-tests": "^1.1" }, "autoload": { @@ -67,5 +67,13 @@ "phpdoc -d src/ -t phpdoc/", "XDEBUG_MODE=coverage phpunit --coverage-html coverage/" ] + }, + "conflict": { + "sunrise/http-factory": "*", + "sunrise/http-header": "*", + "sunrise/http-header-kit": "*", + "sunrise/http-server-request": "*", + "sunrise/stream": "*", + "sunrise/uri": "*" } } From 41cbfbb8dbf933dabf4b4271c82410e5e0152f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Fri, 30 Dec 2022 18:34:45 +0100 Subject: [PATCH 13/16] improve docs --- README.md | 46 +++- docs/headers.md | 618 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 663 insertions(+), 1 deletion(-) create mode 100644 docs/headers.md diff --git a/README.md b/README.md index 4f33291..8fd9227 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# HTTP message wrapper for PHP 7.1+ based on RFC-7230, PSR-7 and PSR-17 +# HTTP message wrapper for PHP 7.4+ based on RFC-7230, PSR-7 and PSR-17 [![Build Status](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/build.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/build-status/master) [![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/http-message/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sunrise-php/http-message/?branch=master) @@ -15,6 +15,50 @@ composer require sunrise/http-message ``` +## How to use + +⚠️ We highly recommend that you study [PSR-7](https://www.php-fig.org/psr/psr-7/) and [PSR-17](https://www.php-fig.org/psr/psr-17/), because only superficial examples will be presented below. + +### Headers as objects + +If you want to use headers as objects, then follow the example below: + +```php +use Sunrise\Http\Message\HeaderInterface; + +final class SomeHeader implements HeaderInterface +{ + // some code... +} + +$message->withHeader(...new SomeHeader()); +``` + +or you can extend your header from the base header which contains the necessary methods for validation and formatting: + +```php +use Sunrise\Http\Message\Header; + +final class SomeHeader extends Header +{ + // some code... +} + +$message->withHeader(...new SomeHeader()); +``` + +Below is an example of how you can set cookies using the already implemented [Set-Cookie](https://github.com/sunrise-php/http-message/blob/master/docs/headers.md#Set-Cookie) header: + +```php +use Sunrise\Http\Message\Header\SetCookieHeader; + +$cookie = new SetCookieHeader('sessionid', '38afes7a8'); + +$message->withAddedHeader(...$cookie); +``` + +You can see already implemented headers [here](https://github.com/sunrise-php/http-message/blob/master/docs/headers.md). + --- ## Test run diff --git a/docs/headers.md b/docs/headers.md new file mode 100644 index 0000000..44f2d71 --- /dev/null +++ b/docs/headers.md @@ -0,0 +1,618 @@ +### HTTP Headers + +#### Access-Control-Allow-Credentials + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + +```php +use Sunrise\Http\Message\Header\AccessControlAllowCredentialsHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AccessControlAllowCredentialsHeader(); +$response = $response->withHeader(...$header); +``` + +#### Access-Control-Allow-Headers + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + +```php +use Sunrise\Http\Message\Header\AccessControlAllowHeadersHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AccessControlAllowHeadersHeader('X-Custom-Header', 'Upgrade-Insecure-Requests'); +$response = $response->withHeader(...$header); +``` + +#### Access-Control-Allow-Methods + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + +```php +use Sunrise\Http\Message\Header\AccessControlAllowMethodsHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AccessControlAllowMethodsHeader('OPTIONS', 'HEAD', 'GET'); +$response = $response->withHeader(...$header); +``` + +#### Access-Control-Allow-Origin + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + +```php +use Sunrise\Http\Message\Header\AccessControlAllowOriginHeader; +use Sunrise\Http\Message\ResponseFactory; +use Sunrise\Uri\UriFactory; + +$response = (new ResponseFactory)->createResponse(); + +// A response that tells the browser to allow code from any origin to access +// a resource will include the following: +$header = new AccessControlAllowOriginHeader(null); +$response = $response->withHeader(...$header); + +// A response that tells the browser to allow requesting code from the origin +// https://developer.mozilla.org to access a resource will include the following: +$uri = (new UriFactory)->createUri('https://developer.mozilla.org'); +$header = new AccessControlAllowOriginHeader($uri); +$response = $response->withHeader(...$header); +``` + +#### Access-Control-Expose-Headers + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + +```php +use Sunrise\Http\Message\Header\AccessControlExposeHeadersHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AccessControlExposeHeadersHeader('Content-Length', 'X-Kuma-Revision'); +$response = $response->withHeader(...$header); +``` + +#### Access-Control-Max-Age + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + +```php +use Sunrise\Http\Message\Header\AccessControlMaxAgeHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AccessControlMaxAgeHeader(600); +$response = $response->withHeader(...$header); +``` + +#### Age + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age + +```php +use Sunrise\Http\Message\Header\AgeHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AgeHeader(24); +$response = $response->withHeader(...$header); +``` + +#### Allow + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow + +```php +use Sunrise\Http\Message\Header\AllowHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new AllowHeader('OPTIONS', 'HEAD', 'GET'); +$response = $response->withHeader(...$header); +``` + +#### Cache-Control + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + +```php +use Sunrise\Http\Message\Header\CacheControlHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// Preventing caching +$header = new CacheControlHeader(['no-cache' => '', 'no-store' => '', 'must-revalidate' => '']); +$response = $response->withHeader(...$header); + +// Caching static assets +$header = new CacheControlHeader(['public' => '', 'max-age' => '31536000']); +$response = $response->withHeader(...$header); +``` + +#### Clear-Site-Data + +> Usage link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data + +```php +use Sunrise\Http\Message\Header\ClearSiteDataHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// Single directive +$header = new ClearSiteDataHeader(['cache']); +$response = $response->withHeader(...$header); + +// Multiple directives (comma separated) +$header = new ClearSiteDataHeader(['cache', 'cookies']); +$response = $response->withHeader(...$header); + +// Wild card +$header = new ClearSiteDataHeader(['*']); +$response = $response->withHeader(...$header); +``` + +#### Connection + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection + +```php +use Sunrise\Http\Message\Header\ConnectionHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// close +$header = new ConnectionHeader(ConnectionHeader::CONNECTION_CLOSE); +$response = $response->withHeader(...$header); + +// keep-alive +$header = new ConnectionHeader(ConnectionHeader::CONNECTION_KEEP_ALIVE); +$response = $response->withHeader(...$header); +``` + +#### Content-Disposition + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition + +```php +use Sunrise\Http\Message\Header\ContentDispositionHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// As a response header for the main body +$header = new ContentDispositionHeader('attachment', ['filename' => 'filename.jpg']); +$response = $response->withHeader(...$header); + +// As a header for a multipart body +$header = new ContentDispositionHeader('form-data', ['name' => 'fieldName', 'filename' => 'filename.jpg']); +$response = $response->withHeader(...$header); +``` + +#### Content-Encoding + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + +```php +use Sunrise\Http\Message\Header\ContentEncodingHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ContentEncodingHeader('gzip'); +$response = $response->withHeader(...$header); +``` + +#### Content-Language + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language + +```php +use Sunrise\Http\Message\Header\ContentLanguageHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ContentLanguageHeader('de-DE', 'en-CA'); +$response = $response->withHeader(...$header); +``` + +#### Content-Length + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length + +```php +use Sunrise\Http\Message\Header\ContentLengthHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ContentLengthHeader(4096); +$response = $response->withHeader(...$header); +``` + +#### Content-Location + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Location + +```php +use Sunrise\Http\Message\Header\ContentLocationHeader; +use Sunrise\Http\Message\ResponseFactory; +use Sunrise\Uri\UriFactory; + +$response = (new ResponseFactory)->createResponse(); + +$uri = (new UriFactory)->createUri('https://example.com/documents/foo'); +$header = new ContentLocationHeader($uri); +$response = $response->withHeader(...$header); +``` + +#### Content-MD5 + +> Useful link: https://tools.ietf.org/html/rfc1864 + +```php +use Sunrise\Http\Message\Header\ContentMD5Header; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ContentMD5Header('MzAyMWU2OGRmOWE3MjAwMTM1NzI1YzYzMzEzNjlhMjI='); +$response = $response->withHeader(...$header); +``` + +#### Content-Range + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range + +```php +use Sunrise\Http\Message\Header\ContentRangeHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ContentRangeHeader( + 200, // An integer in the given unit indicating the beginning of the request range. + 1000, // An integer in the given unit indicating the end of the requested range. + 67589 // The total size of the document. +); +$response = $response->withHeader(...$header); +``` + +#### Content-Security-Policy + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +```php +use Sunrise\Http\Message\Header\ContentSecurityPolicyHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// Pre-existing site that uses too much inline code to fix but wants +// to ensure resources are loaded only over https and disable plugins: +$header = new ContentSecurityPolicyHeader(['default-src' => "https: 'unsafe-eval' 'unsafe-inline'", 'object-src' => "'none'"]); +$response = $response->withAddedHeader(...$header); + +// Don't implement the above policy yet; instead just report +// violations that would have occurred: +$header = new ContentSecurityPolicyHeader(['default-src' => 'https:', 'report-uri' => '/csp-violation-report-endpoint/']); +$response = $response->withAddedHeader(...$header); +``` + +#### Content-Security-Policy-Report-Only + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only + +```php +use Sunrise\Http\Message\Header\ContentSecurityPolicyReportOnlyHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// This header reports violations that would have occurred. +// You can use this to iteratively work on your content security policy. +// You observe how your site behaves, watching for violation reports, +// then choose the desired policy enforced by the Content-Security-Policy header. +$header = new ContentSecurityPolicyReportOnlyHeader(['default-src' => 'https:', 'report-uri' => '/csp-violation-report-endpoint/']); +$response = $response->withAddedHeader(...$header); + +// If you still want to receive reporting, but also want +// to enforce a policy, use the Content-Security-Policy header with the report-uri directive. +$header = new ContentSecurityPolicyReportOnlyHeader(['default-src' => 'https:', 'report-uri' => '/csp-violation-report-endpoint/']); +$response = $response->withAddedHeader(...$header); +``` + +#### Content-Type + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type + +```php +use Sunrise\Http\Message\Header\ContentTypeHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ContentTypeHeader('application/json', ['charset' => 'utf-8']); +$response = $response->withHeader(...$header); +``` + +#### Cookie + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie + +```php +use Sunrise\Http\Message\Header\CookieHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new CookieHeader(['name' => 'value', 'name2' => 'value2', 'name3' => 'value3']); +$response = $response->withHeader(...$header); +``` + +#### Date + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date + +```php +use DateTime; +use Sunrise\Http\Message\Header\DateHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new DateHeader(new DateTime('now')); +$response = $response->withHeader(...$header); +``` + +#### Etag + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag + +```php +use Sunrise\Http\Message\Header\EtagHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new EtagHeader('33a64df551425fcc55e4d42a148795d9f25f89d4'); +$response = $response->withHeader(...$header); +``` + +#### Expires + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires + +```php +use DateTime; +use Sunrise\Http\Message\Header\ExpiresHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new ExpiresHeader(new DateTime('1 day ago')); +$response = $response->withHeader(...$header); +``` + +#### Keep-Alive + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive + +```php +use Sunrise\Http\Message\Header\KeepAliveHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new KeepAliveHeader(['timeout' => '5', 'max' => '1000']); +$response = $response->withHeader(...$header); +``` + +#### Last-Modified + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified + +```php +use DateTime; +use Sunrise\Http\Message\Header\LastModifiedHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new LastModifiedHeader(new DateTime('1 year ago')); +$response = $response->withHeader(...$header); +``` + +#### Link + +> Useful link: https://www.w3.org/wiki/LinkHeader + +```php +use Sunrise\Http\Message\Header\LinkHeader; +use Sunrise\Http\Message\ResponseFactory; +use Sunrise\Uri\UriFactory; + +$response = (new ResponseFactory)->createResponse(); + +$uri = (new UriFactory)->createUri('meta.rdf'); +$header = new LinkHeader($uri, ['rel' => 'meta']); +$response = $response->withHeader(...$header); +``` + +#### Location + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location + +```php +use Sunrise\Http\Message\Header\LocationHeader; +use Sunrise\Http\Message\ResponseFactory; +use Sunrise\Uri\UriFactory; + +$response = (new ResponseFactory)->createResponse(); + +$uri = (new UriFactory)->createUri('/'); +$header = new LocationHeader($uri); +$response = $response->withHeader(...$header); +``` + +#### Refresh + +> Useful link: https://en.wikipedia.org/wiki/Meta_refresh + +```php +use Sunrise\Http\Message\Header\RefreshHeader; +use Sunrise\Http\Message\ResponseFactory; +use Sunrise\Uri\UriFactory; + +$response = (new ResponseFactory)->createResponse(); + +$uri = (new UriFactory)->createUri('/login'); +$header = new RefreshHeader(3, $uri); +$response = $response->withHeader(...$header); +``` + +#### Retry-After + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + +```php +use DateTime; +use Sunrise\Http\Message\Header\RetryAfterHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new RetryAfterHeader(new DateTime('+30 second')); +$response = $response->withHeader(...$header); +``` + +#### Set-Cookie + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + +```php +use DateTime; +use Sunrise\Http\Message\Header\SetCookieHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +// Session cookie +// Session cookies will get removed when the client is shut down. +// They don't specify the Expires or Max-Age directives. +// Note that web browser have often enabled session restoring. +$header = new SetCookieHeader('sessionid', '38afes7a8', null, ['path' => '/', 'httponly' => true]); +$response = $response->withAddedHeader(...$header); + +// Permanent cookie +// Instead of expiring when the client is closed, permanent cookies expire +// at a specific date (Expires) or after a specific length of time (Max-Age). +$header = new SetCookieHeader('id', 'a3fWa', new DateTime('+1 day'), ['secure' => true, 'httponly' => true]); +$response = $response->withAddedHeader(...$header); + +// Invalid domains +// A cookie belonging to a domain that does not include the origin server +// should be rejected by the user agent. The following cookie will be rejected +// if it was set by a server hosted on originalcompany.com. +$header = new SetCookieHeader('qwerty', '219ffwef9w0f', new DateTime('+1 day'), ['domain' => 'somecompany.co.uk', 'path' => '/']); +$response = $response->withAddedHeader(...$header); +``` + +#### Sunset + +> Useful link: https://tools.ietf.org/id/draft-wilde-sunset-header-03.html + +```php +use DateTime; +use Sunrise\Http\Message\Header\SunsetHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new SunsetHeader(new DateTime('2038-01-19 03:14:07')); +$response = $response->withHeader(...$header); +``` + +#### Trailer + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer + +```php +use Sunrise\Http\Message\Header\TrailerHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new TrailerHeader('Expires', 'X-Streaming-Error'); +$response = $response->withHeader(...$header); +``` + +#### Transfer-Encoding + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding + +```php +use Sunrise\Http\Message\Header\TransferEncodingHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new TransferEncodingHeader('gzip', 'chunked'); +$response = $response->withHeader(...$header); +``` + +#### Vary + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary + +```php +use Sunrise\Http\Message\Header\VaryHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new VaryHeader('User-Agent', 'Content-Language'); +$response = $response->withHeader(...$header); +``` + +#### WWW-Authenticate + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate + +```php +use Sunrise\Http\Message\Header\WWWAuthenticateHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new WWWAuthenticateHeader(WWWAuthenticateHeader::HTTP_AUTHENTICATE_SCHEME_BASIC, ['realm' => 'Access to the staging site', 'charset' => 'UTF-8']); +$response = $response->withHeader(...$header); +``` + +#### Warning + +> Useful link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning + +```php +use DateTime; +use Sunrise\Http\Message\Header\WarningHeader; +use Sunrise\Http\Message\ResponseFactory; + +$response = (new ResponseFactory)->createResponse(); + +$header = new WarningHeader(WarningHeader::HTTP_WARNING_CODE_RESPONSE_IS_STALE, 'anderson/1.3.37', 'Response is stale', new DateTime('now')); +$response = $response->withHeader(...$header); +``` From ce81b86a3f31ae3bdaf191712f2d2f7beb08b4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Sat, 31 Dec 2022 00:54:04 +0100 Subject: [PATCH 14/16] update README.md --- README.md | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8fd9227..f52db4e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ composer require sunrise/http-message ## How to use -⚠️ We highly recommend that you study [PSR-7](https://www.php-fig.org/psr/psr-7/) and [PSR-17](https://www.php-fig.org/psr/psr-17/), because only superficial examples will be presented below. +⚠️ We highly recommend that you study [PSR-7](https://www.php-fig.org/psr/psr-7/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) because only superficial examples will be presented below. ### Headers as objects @@ -34,7 +34,7 @@ final class SomeHeader implements HeaderInterface $message->withHeader(...new SomeHeader()); ``` -or you can extend your header from the base header which contains the necessary methods for validation and formatting: +... or you can extend your header from the base header which contains the necessary methods for validation and formatting: ```php use Sunrise\Http\Message\Header; @@ -59,6 +59,141 @@ $message->withAddedHeader(...$cookie); You can see already implemented headers [here](https://github.com/sunrise-php/http-message/blob/master/docs/headers.md). +### Server request from global environment + +```php +use Sunrise\Http\Message\ServerRequestFactory; + +$request = ServerRequestFactory::fromGlobals(); +``` + +### HTML and JSON responses + +#### HTML response + +```php +use Sunrise\Http\Message\Response\HtmlResponse; + +/** @var $html string|Stringable */ + +$response = new HtmlResponse(200, $html); +``` + +#### JSON response + +```php +use Sunrise\Http\Message\Response\JsonResponse; + +/** @var $data mixed */ + +$response = new JsonResponse(200, $data); +``` + +You can also specify [encoding flags](https://www.php.net/manual/en/json.constants.php#constant.json-hex-tag) and maximum nesting depth like bellow: + +```php +$response = new JsonResponse(200, $data, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE, 512); +``` + +### Streams + +#### File stream + +```php +use Sunrise\Http\Message\Stream\FileStream; + +$fileStream = new FileStream('/folder/file', 'r+b'); +``` + +#### PHP input stream + +More details about the stream at the [official page](https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input). + +```php +use Sunrise\Http\Message\Stream\PhpInputStream; + +$inputStream = new PhpInputStream(); +``` + +#### PHP memory stream + +More details about the stream at the [official page](https://www.php.net/manual/en/wrappers.php.php#wrappers.php.memory). + +```php +use Sunrise\Http\Message\Stream\PhpMemoryStream; + +$memoryStream = new PhpMemoryStream('r+b'); +``` + +#### PHP temporary stream + +More details about the stream at [the official page](https://www.php.net/manual/en/wrappers.php.php#wrappers.php.memory). + +```php +use Sunrise\Http\Message\Stream\PhpTempStream; + +$tempStream = new PhpTempStream('r+b'); +``` + +You can also specify the memory limit, when the limit is reached, PHP will start using the temporary file instead of memory. + +> Please note that the default memory limit is 2MB. + +```php +$maxMemory = 1e+6; // 1MB + +$tempStream = new PhpTempStream('r+b', $maxMemory); +``` + +#### Temporary file stream + +More details about the temporary file behaviour at [the official page](https://www.php.net/manual/en/function.tmpfile). + +The stream opens a unique temporary file in binary read/write (w+b) mode. The file will be automatically deleted when it is closed or the program terminates. + +```php +use Sunrise\Http\Message\Stream\TmpfileStream; + +$tmpfileStream = new TmpfileStream(); + +// Returns the file path... +$tmpfileStream->getMetadata('uri'); +``` + +### PSR-7 and PSR-17 + +The following classes implement PSR-7: + +- `Sunrise\Http\Message\Request` +- `Sunrise\Http\Message\Response` +- `Sunrise\Http\Message\ServerRequest` +- `Sunrise\Http\Message\Stream` +- `Sunrise\Http\Message\UploadedFile` +- `Sunrise\Http\Message\Uri` + +The following classes implement PSR-17: + +- `Sunrise\Http\Message\RequestFactory` +- `Sunrise\Http\Message\ResponseFactory` +- `Sunrise\Http\Message\ServerRequestFactory` +- `Sunrise\Http\Message\StreamFactory` +- `Sunrise\Http\Message\UploadedFileFactory` +- `Sunrise\Http\Message\UriFactory` + +### Exceptions + +Any exceptions of this package can be caught through the interface: + +```php +use Sunrise\Http\Message\Exception\ExceptionInterface; + +try { + // some code with the package... +} catch (ExceptionInterface $e) { + // the package error... +} +``` + --- ## Test run From b311508db233b7e8139bcb15fdfc98667769f4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Sat, 31 Dec 2022 01:02:28 +0100 Subject: [PATCH 15/16] update README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index f52db4e..cebe970 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,23 @@ composer require sunrise/http-message ``` +## Navigation + +- [Headers as objects](#headers-as-objects) +- - [Implemented headers](https://github.com/sunrise-php/http-message/blob/master/docs/headers.md) +- [Server request from global environment](#server-request-from-global-environment) +- [HTML and JSON responses](#html-and-json-responses) +- - [HTML response](#html-response) +- - [JSON response](#json-response) +- [Streams](#streams) +- - [File stream](#file-stream) +- - [PHP input stream](#php-input-stream) +- - [PHP memory stream](#php-memory-stream) +- - [PHP temporary stream](#php-temporary-stream) +- - [Temporary file stream](#temporary-file-stream) +- [PSR-7 and PSR-17](#psr-7-and-psr-17) +- [Exceptions](#exceptions) + ## How to use ⚠️ We highly recommend that you study [PSR-7](https://www.php-fig.org/psr/psr-7/) and [PSR-17](https://www.php-fig.org/psr/psr-17/) because only superficial examples will be presented below. From 85bb7c932b286500e90c34fc9db5f372f47bfe83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B0=D1=82=D0=BE=D0=BB=D0=B8=D0=B9=20=D0=9D?= =?UTF-8?q?=D0=B5=D1=85=D0=B0=D0=B9?= Date: Sat, 31 Dec 2022 01:04:54 +0100 Subject: [PATCH 16/16] update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cebe970..f9244b0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ composer require sunrise/http-message ``` -## Navigation +## Documentation navigation - [Headers as objects](#headers-as-objects) - - [Implemented headers](https://github.com/sunrise-php/http-message/blob/master/docs/headers.md) @@ -221,6 +221,6 @@ composer test ## Useful links -* https://tools.ietf.org/html/rfc7230 -* https://www.php-fig.org/psr/psr-7/ -* https://www.php-fig.org/psr/psr-17/ +- https://tools.ietf.org/html/rfc7230 +- https://www.php-fig.org/psr/psr-7/ +- https://www.php-fig.org/psr/psr-17/