forked from admin/services_core
big update base struktur tahap 1
This commit is contained in:
Vendored
+60
@@ -5,6 +5,66 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.13.0 - 2026-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Utils::` `asciiToLower`, `asciiToUpper`, `asciiUcFirst`, `caselessEquals`, `caselessContains`
|
||||
|
||||
### Changed
|
||||
|
||||
- Use locale-independent ASCII case folding everywhere case is normalized
|
||||
- Trigger a runtime deprecation for previously deprecated functionality in 2.3.0
|
||||
|
||||
## 2.12.5 - 2026-07-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Compare header names and hosts with locale-independent ASCII lowercasing
|
||||
- Compare hosts without locale sensitivity when detecting cross-origin redirects
|
||||
|
||||
## 2.12.4 - 2026-07-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Pass explicit trim characters ahead of the PHP 8.6 trim default change
|
||||
|
||||
### Fixed
|
||||
|
||||
- Anchor server port and response start-line patterns to the true end of input
|
||||
- Treat host-less origin-form request targets starting with `//` as paths in `Message::parseRequest()`
|
||||
- Reject raw DEL bytes in bracketed IP-literal hosts instead of parsing a mutated host
|
||||
- Reject invalid bytes after a bracketed IP-literal host instead of reparsing a different host
|
||||
|
||||
## 2.12.3 - 2026-06-23
|
||||
|
||||
### Security
|
||||
|
||||
- Validate the URI host so `getHost()` matches the URI authority (GHSA-c2w2-prh8-qm98)
|
||||
|
||||
## 2.12.2 - 2026-06-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Report URI parsing, filtering, and normalization PCRE failures explicitly
|
||||
- Report HTTP message parser PCRE failures explicitly
|
||||
- Fail closed when PCRE validation fails for request targets and hosts
|
||||
|
||||
## 2.12.1 - 2026-06-18
|
||||
|
||||
### Security
|
||||
|
||||
- Reject CR/LF in HTTP method, protocol version, and reason phrase (GHSA-vm85-hxw5-5432)
|
||||
|
||||
## 2.12.0 - 2026-06-16
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated non-finite float values in `Query::build()` that guzzlehttp/psr7 3.0 rejects
|
||||
- Deprecated non-finite float multipart contents that guzzlehttp/psr7 3.0 rejects
|
||||
- Deprecated non-string scalar bodies in `Utils::streamFor()`; cast them to a string for 3.0
|
||||
- Deprecated non-string `Uri::withQueryValues()` values; cast them to a string for 3.0
|
||||
|
||||
## 2.11.1 - 2026-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
Vendored
+50
@@ -455,6 +455,56 @@ string. This function does not modify the provided keys when an array is
|
||||
encountered (like `http_build_query()` would).
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::asciiToLower`
|
||||
|
||||
`public static function asciiToLower(string $string): string`
|
||||
|
||||
Converts ASCII uppercase letters in a string to lowercase.
|
||||
|
||||
Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the conversion is
|
||||
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
|
||||
elements require.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::asciiToUpper`
|
||||
|
||||
`public static function asciiToUpper(string $string): string`
|
||||
|
||||
Converts ASCII lowercase letters in a string to uppercase.
|
||||
|
||||
Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the conversion is
|
||||
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
|
||||
elements require.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::asciiUcFirst`
|
||||
|
||||
`public static function asciiUcFirst(string $string): string`
|
||||
|
||||
Converts the first character of a string to uppercase when it is an ASCII
|
||||
lowercase letter.
|
||||
|
||||
Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion is
|
||||
locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
|
||||
elements require.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessContains`
|
||||
|
||||
`public static function caselessContains(string $haystack, string $needle): bool`
|
||||
|
||||
Checks whether the haystack contains the needle, comparing ASCII letters
|
||||
case-insensitively and without locale sensitivity.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessEquals`
|
||||
|
||||
`public static function caselessEquals(string $left, string $right): bool`
|
||||
|
||||
Checks whether two strings are equal, comparing ASCII letters
|
||||
case-insensitively and without locale sensitivity.
|
||||
|
||||
|
||||
## `GuzzleHttp\Psr7\Utils::caselessRemove`
|
||||
|
||||
`public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`
|
||||
|
||||
Vendored
+1
-1
@@ -55,7 +55,7 @@
|
||||
"psr/http-message": "^1.1 || ^2.0",
|
||||
"ralouphie/getallheaders": "^3.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.24"
|
||||
"symfony/polyfill-php80": "^1.25"
|
||||
},
|
||||
"require-dev": {
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
|
||||
+4
-2
@@ -95,6 +95,8 @@ final class Header
|
||||
*/
|
||||
public static function normalize($header): array
|
||||
{
|
||||
\trigger_deprecation('guzzlehttp/psr7', '2.3', 'Header::normalize() is deprecated and will be removed in guzzlehttp/psr7 3.0. Use Header::splitList() instead.');
|
||||
|
||||
$result = [];
|
||||
foreach ((array) $header as $value) {
|
||||
foreach (self::splitList($value) as $parsed) {
|
||||
@@ -142,7 +144,7 @@ final class Header
|
||||
}
|
||||
|
||||
if (!$isQuoted && $value[$i] === ',') {
|
||||
$v = \trim($v);
|
||||
$v = \trim($v, " \n\r\t\0\x0B");
|
||||
if ($v !== '') {
|
||||
$result[] = $v;
|
||||
}
|
||||
@@ -167,7 +169,7 @@ final class Header
|
||||
$v .= $value[$i];
|
||||
}
|
||||
|
||||
$v = \trim($v);
|
||||
$v = \trim($v, " \n\r\t\0\x0B");
|
||||
if ($v !== '') {
|
||||
$result[] = $v;
|
||||
}
|
||||
|
||||
+69
-10
@@ -19,7 +19,7 @@ final class Message
|
||||
{
|
||||
if ($message instanceof RequestInterface) {
|
||||
$msg = trim($message->getMethod().' '
|
||||
.$message->getRequestTarget())
|
||||
.$message->getRequestTarget(), " \n\r\t\0\x0B")
|
||||
.' HTTP/'.$message->getProtocolVersion();
|
||||
if (!$message->hasHeader('host')) {
|
||||
$msg .= "\r\nHost: ".$message->getUri()->getHost();
|
||||
@@ -33,7 +33,7 @@ final class Message
|
||||
}
|
||||
|
||||
foreach ($message->getHeaders() as $name => $values) {
|
||||
if (is_string($name) && strtolower($name) === 'set-cookie') {
|
||||
if (is_string($name) && Utils::asciiToLower($name) === 'set-cookie') {
|
||||
foreach ($values as $value) {
|
||||
$msg .= "\r\n{$name}: ".$value;
|
||||
}
|
||||
@@ -181,7 +181,11 @@ final class Message
|
||||
|
||||
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
|
||||
|
||||
if ($messageParts === false || count($messageParts) !== 2) {
|
||||
if ($messageParts === false) {
|
||||
throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if (count($messageParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
|
||||
}
|
||||
|
||||
@@ -189,24 +193,48 @@ final class Message
|
||||
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
|
||||
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
|
||||
|
||||
if ($headerParts === false || count($headerParts) !== 2) {
|
||||
if ($headerParts === false) {
|
||||
throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if (count($headerParts) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid message: Missing status line');
|
||||
}
|
||||
|
||||
[$startLine, $rawHeaders] = $headerParts;
|
||||
|
||||
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
|
||||
$versionMatch = preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches);
|
||||
|
||||
if ($versionMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($versionMatch === 1 && $matches[1] === '1.0') {
|
||||
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
|
||||
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
|
||||
|
||||
if ($rawHeaders === null) {
|
||||
throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array[] $headerLines */
|
||||
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
|
||||
|
||||
if ($count === false) {
|
||||
throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
// If these aren't the same, then one line didn't match and there's an invalid header.
|
||||
if ($count !== substr_count($rawHeaders, "\n")) {
|
||||
// Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
|
||||
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
|
||||
$hasFoldedHeader = preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders);
|
||||
|
||||
if ($hasFoldedHeader === false) {
|
||||
throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($hasFoldedHeader === 1) {
|
||||
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
|
||||
}
|
||||
|
||||
@@ -237,8 +265,10 @@ final class Message
|
||||
$host = self::getHostFromHeaders($headers);
|
||||
|
||||
// If no host is found, then a full URI cannot be constructed.
|
||||
// Collapse leading slashes so an origin-form target cannot be
|
||||
// parsed as a network-path reference with its own authority.
|
||||
if ($host === null) {
|
||||
return $path;
|
||||
return self::normalizePathForOriginForm($path);
|
||||
}
|
||||
|
||||
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
|
||||
@@ -246,6 +276,15 @@ final class Message
|
||||
return $scheme.'://'.$host.'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
private static function normalizePathForOriginForm(string $path): string
|
||||
{
|
||||
if (0 === strpos($path, '//')) {
|
||||
return '/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $headers Array of headers (each value an array).
|
||||
*/
|
||||
@@ -255,7 +294,7 @@ final class Message
|
||||
// Numeric array keys are converted to int by PHP.
|
||||
$k = (string) $k;
|
||||
|
||||
return strtolower($k) === 'host';
|
||||
return Utils::asciiToLower($k) === 'host';
|
||||
});
|
||||
|
||||
if (!$hostKey) {
|
||||
@@ -278,8 +317,18 @@ final class Message
|
||||
public static function parseRequest(string $message): RequestInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
if (strpbrk($data['start-line'], "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
|
||||
$requestStartLineMatch = preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches);
|
||||
|
||||
if ($requestStartLineMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($requestStartLineMatch === 0) {
|
||||
throw new \InvalidArgumentException('Invalid request string');
|
||||
}
|
||||
$parts = explode(' ', $data['start-line'], 3);
|
||||
@@ -304,10 +353,20 @@ final class Message
|
||||
public static function parseResponse(string $message): ResponseInterface
|
||||
{
|
||||
$data = self::parseMessage($message);
|
||||
if (strpbrk($data['start-line'], "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException('Invalid response string');
|
||||
}
|
||||
|
||||
// According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
|
||||
// the space between status-code and reason-phrase is required. But
|
||||
// browsers accept responses without space and reason as well.
|
||||
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
|
||||
$responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/D', $data['start-line']);
|
||||
|
||||
if ($responseStartLineMatch === false) {
|
||||
throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($responseStartLineMatch === 0) {
|
||||
throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
|
||||
}
|
||||
$parts = explode(' ', $data['start-line'], 3);
|
||||
|
||||
+25
-6
@@ -43,6 +43,8 @@ trait MessageTrait
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
if ($this->protocol === $version) {
|
||||
return $this;
|
||||
}
|
||||
@@ -60,12 +62,12 @@ trait MessageTrait
|
||||
|
||||
public function hasHeader($header): bool
|
||||
{
|
||||
return isset($this->headerNames[strtolower($header)]);
|
||||
return isset($this->headerNames[Utils::asciiToLower($header)]);
|
||||
}
|
||||
|
||||
public function getHeader($header): array
|
||||
{
|
||||
$header = strtolower($header);
|
||||
$header = Utils::asciiToLower($header);
|
||||
|
||||
if (!isset($this->headerNames[$header])) {
|
||||
return [];
|
||||
@@ -101,7 +103,7 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
@@ -133,7 +135,7 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
|
||||
$new = clone $this;
|
||||
if (isset($new->headerNames[$normalized])) {
|
||||
@@ -152,7 +154,7 @@ trait MessageTrait
|
||||
*/
|
||||
public function withoutHeader($header): MessageInterface
|
||||
{
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
|
||||
if (!isset($this->headerNames[$normalized])) {
|
||||
return $this;
|
||||
@@ -216,7 +218,7 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
$value = $this->normalizeHeaderValue($value);
|
||||
$normalized = strtolower($header);
|
||||
$normalized = Utils::asciiToLower($header);
|
||||
if (isset($this->headerNames[$normalized])) {
|
||||
$header = $this->headerNames[$normalized];
|
||||
$this->headers[$header] = array_merge($this->headers[$header], $value);
|
||||
@@ -307,6 +309,23 @@ trait MessageTrait
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $version
|
||||
*/
|
||||
private function assertProtocolVersion($version): void
|
||||
{
|
||||
if (is_string($version)) {
|
||||
$this->assertNoLineSeparators($version, 'Protocol version');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertNoLineSeparators(string $value, string $field): void
|
||||
{
|
||||
if (strpbrk($value, "\r\n") !== false) {
|
||||
throw new \InvalidArgumentException($field.' must not contain CR or LF characters.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
|
||||
*
|
||||
|
||||
+1
-1
@@ -1300,6 +1300,6 @@ final class MimeType
|
||||
*/
|
||||
public static function fromExtension(string $extension): ?string
|
||||
{
|
||||
return self::MIME_TYPES[strtolower($extension)] ?? null;
|
||||
return self::MIME_TYPES[Utils::asciiToLower($extension)] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -26,8 +26,9 @@ final class MultipartStream implements StreamInterface
|
||||
* @param array $elements Array of associative arrays, each containing a
|
||||
* required "name" key mapping to the form field,
|
||||
* name, a required "contents" key mapping to any
|
||||
* non-array value accepted by Utils::streamFor(),
|
||||
* or an array for nested expansion.
|
||||
* non-array value accepted by Utils::streamFor()
|
||||
* (non-string scalar field values are cast to
|
||||
* string), or an array for nested expansion.
|
||||
* Optional keys include "headers" (associative
|
||||
* array of custom headers) and "filename" (string
|
||||
* to send as the filename in the part).
|
||||
@@ -77,7 +78,7 @@ final class MultipartStream implements StreamInterface
|
||||
$str .= "{$key}: {$value}\r\n";
|
||||
}
|
||||
|
||||
return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n";
|
||||
return "--{$this->boundary}\r\n".trim($str, " \n\r\t\0\x0B")."\r\n\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +125,27 @@ final class MultipartStream implements StreamInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$element['contents'] = Utils::streamFor($element['contents']);
|
||||
$contents = $element['contents'];
|
||||
if (is_scalar($contents) && !is_string($contents)) {
|
||||
// Multipart field values are byte strings on the wire, so finite
|
||||
// numeric and boolean field values are cast to string here rather
|
||||
// than tripping streamFor()'s non-string-scalar deprecation. Non-finite
|
||||
// floats are deprecated and normalized here too, so the deprecation is
|
||||
// reported against MultipartStream instead of transitively through
|
||||
// streamFor().
|
||||
if (is_float($contents) && !is_finite($contents)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing a non-finite float as multipart contents is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
|
||||
);
|
||||
|
||||
$contents = is_nan($contents) ? 'NAN' : ($contents > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
|
||||
$contents = (string) $contents;
|
||||
}
|
||||
$element['contents'] = Utils::streamFor($contents);
|
||||
|
||||
if (empty($element['filename'])) {
|
||||
$uri = $element['contents']->getMetadata('uri');
|
||||
@@ -206,9 +227,9 @@ final class MultipartStream implements StreamInterface
|
||||
*/
|
||||
private static function getHeader(array $headers, string $key): ?string
|
||||
{
|
||||
$lowercaseHeader = strtolower($key);
|
||||
$lowercaseHeader = Utils::asciiToLower($key);
|
||||
foreach ($headers as $k => $v) {
|
||||
if (strtolower((string) $k) === $lowercaseHeader) {
|
||||
if (Utils::asciiToLower((string) $k) === $lowercaseHeader) {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -127,6 +127,12 @@ final class Query
|
||||
private static function normalizeNonFiniteFloat($value)
|
||||
{
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing a non-finite float to Query::build() is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
|
||||
);
|
||||
|
||||
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -40,12 +40,14 @@ class Request implements RequestInterface
|
||||
string $version = '1.1'
|
||||
) {
|
||||
$this->assertMethod($method);
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
if (!$uri instanceof UriInterface) {
|
||||
$uri = new Uri($uri);
|
||||
}
|
||||
|
||||
self::warnOnMethodCasingChange($method);
|
||||
$this->method = strtoupper($method);
|
||||
$this->method = Utils::asciiToUpper($method);
|
||||
$this->uri = $uri;
|
||||
$this->setHeaders($headers);
|
||||
$this->protocol = $version;
|
||||
@@ -78,7 +80,13 @@ class Request implements RequestInterface
|
||||
|
||||
public function withRequestTarget($requestTarget): RequestInterface
|
||||
{
|
||||
if (preg_match('#\s#', $requestTarget)) {
|
||||
$hasWhitespace = preg_match('#\s#', $requestTarget);
|
||||
|
||||
if ($hasWhitespace === false) {
|
||||
throw new \RuntimeException('Unable to validate request target: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($hasWhitespace === 1) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid request target provided; cannot contain whitespace'
|
||||
);
|
||||
@@ -100,7 +108,7 @@ class Request implements RequestInterface
|
||||
$this->assertMethod($method);
|
||||
self::warnOnMethodCasingChange($method);
|
||||
$new = clone $this;
|
||||
$new->method = strtoupper($method);
|
||||
$new->method = Utils::asciiToUpper($method);
|
||||
|
||||
return $new;
|
||||
}
|
||||
@@ -170,11 +178,13 @@ class Request implements RequestInterface
|
||||
if (!is_string($method) || $method === '') {
|
||||
throw new InvalidArgumentException('Method must be a non-empty string.');
|
||||
}
|
||||
|
||||
$this->assertNoLineSeparators($method, 'Method');
|
||||
}
|
||||
|
||||
private static function warnOnMethodCasingChange(string $method): void
|
||||
{
|
||||
if ($method !== strtoupper($method)) {
|
||||
if ($method !== Utils::asciiToUpper($method)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.11',
|
||||
|
||||
+9
-3
@@ -99,6 +99,7 @@ class Response implements ResponseInterface
|
||||
?string $reason = null
|
||||
) {
|
||||
$this->assertStatusCodeRange($status);
|
||||
$this->assertProtocolVersion($version);
|
||||
|
||||
$this->statusCode = $status;
|
||||
|
||||
@@ -108,11 +109,14 @@ class Response implements ResponseInterface
|
||||
|
||||
$this->setHeaders($headers);
|
||||
if ($reason == '' && isset(self::PHRASES[$this->statusCode])) {
|
||||
$this->reasonPhrase = self::PHRASES[$this->statusCode];
|
||||
$reasonPhrase = self::PHRASES[$this->statusCode];
|
||||
} else {
|
||||
$this->reasonPhrase = (string) $reason;
|
||||
$reasonPhrase = (string) $reason;
|
||||
}
|
||||
|
||||
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
|
||||
$this->reasonPhrase = $reasonPhrase;
|
||||
|
||||
$this->protocol = $version;
|
||||
}
|
||||
|
||||
@@ -155,7 +159,9 @@ class Response implements ResponseInterface
|
||||
if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
|
||||
$reasonPhrase = self::PHRASES[$new->statusCode];
|
||||
}
|
||||
$new->reasonPhrase = (string) $reasonPhrase;
|
||||
$reasonPhrase = (string) $reasonPhrase;
|
||||
$this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
|
||||
$new->reasonPhrase = $reasonPhrase;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
+7
-1
@@ -68,7 +68,13 @@ final class Rfc7230
|
||||
|
||||
private static function isValidHostHeaderHost(string $host): bool
|
||||
{
|
||||
if (preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host)) {
|
||||
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
|
||||
|
||||
if ($invalidHost === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($invalidHost === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -165,7 +165,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||
*/
|
||||
public static function fromGlobals(): ServerRequestInterface
|
||||
{
|
||||
$method = strtoupper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
|
||||
$method = Utils::asciiToUpper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
|
||||
$headers = self::removeInvalidHostHeader(self::getAllHeaders());
|
||||
$uri = self::getUriFromGlobals();
|
||||
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
|
||||
@@ -220,7 +220,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||
private static function removeInvalidHostHeader(array $headers): array
|
||||
{
|
||||
foreach ($headers as $name => $value) {
|
||||
if (strtolower((string) $name) !== 'host') {
|
||||
if (Utils::asciiToLower((string) $name) !== 'host') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ class ServerRequest extends Request implements ServerRequestInterface
|
||||
}
|
||||
|
||||
$serverPort = self::getServerParam('SERVER_PORT');
|
||||
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/', $serverPort) === 1) {
|
||||
if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/D', $serverPort) === 1) {
|
||||
$uri = $uri->withPort((int) $serverPort);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+79
-19
@@ -99,12 +99,30 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
return self::parsePathNoSchemeReference($url);
|
||||
}
|
||||
|
||||
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4 tails.
|
||||
// Preserve bracketed IPv6 literals before encoding, including dotted IPv4
|
||||
// tails. DEL (\x7F) is excluded so a raw-DEL host falls through to the
|
||||
// general path and is rejected rather than silently mutated by parse_url().
|
||||
$prefix = '';
|
||||
if (preg_match('%^([0-9A-Za-z+.-]+://\[[0-9:.a-fA-F]+\])(.*?)$%', $url, $matches)) {
|
||||
$ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
|
||||
|
||||
if ($ipv6Prefix === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ipv6Prefix === 1) {
|
||||
/** @var array{0:string, 1:string, 2:string} $matches */
|
||||
$suffix = $matches[2];
|
||||
|
||||
// After the bracketed host only an optional numeric port and/or a
|
||||
// path, query, or fragment may follow. Anything else (for example
|
||||
// `:80@evil` or `:80x`) would let parse_url() reinterpret a
|
||||
// different host.
|
||||
if (preg_match('%\A(?::[0-9]*)?(?:[/?#].*)?\z%s', $suffix) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prefix = $matches[1];
|
||||
$url = $matches[2];
|
||||
$url = $suffix;
|
||||
}
|
||||
|
||||
/** @var string|null */
|
||||
@@ -378,15 +396,26 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts non-finite floats to the strings PHP coerces them to, as
|
||||
* implicit coercion of NAN emits a warning on PHP 8.5.
|
||||
* Stringifies a non-null query value, deprecating non-string values that
|
||||
* guzzlehttp/psr7 3.0 will reject. Non-finite floats are normalized to the
|
||||
* strings PHP coerces them to, as implicit coercion of NAN emits a warning
|
||||
* on PHP 8.5.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function stringifyQueryValue($value): string
|
||||
{
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
if (!is_string($value)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing %s to Uri::withQueryValues() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string or null query values.',
|
||||
\gettype($value)
|
||||
);
|
||||
|
||||
if (is_float($value) && !is_finite($value)) {
|
||||
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
@@ -425,7 +454,27 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
return;
|
||||
}
|
||||
|
||||
if (preg_match('/[\x00-\x20\x7F]/', $host)) {
|
||||
// Reject control characters and URI authority delimiters so getHost()
|
||||
// cannot disagree with the on-wire authority.
|
||||
$invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
|
||||
|
||||
if ($invalidHost === false) {
|
||||
throw new \RuntimeException('Unable to validate URI host: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
if ($invalidHost === 1) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
}
|
||||
|
||||
if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
|
||||
if ($host[0] !== '[' || substr($host, -1) !== ']') {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strpos($host, ':') !== false) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
|
||||
}
|
||||
}
|
||||
@@ -647,7 +696,7 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Scheme must be a string');
|
||||
}
|
||||
|
||||
$scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
$scheme = Utils::asciiToLower($scheme);
|
||||
|
||||
if ($scheme !== '' && !preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
|
||||
\trigger_deprecation(
|
||||
@@ -672,10 +721,10 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('User info must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
return $this->filterComponent(
|
||||
'/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$component
|
||||
$component,
|
||||
'Unable to filter URI user info'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -690,7 +739,7 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Host must be a string');
|
||||
}
|
||||
|
||||
$host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
$host = Utils::asciiToLower($host);
|
||||
self::assertValidHost($host);
|
||||
|
||||
return $host;
|
||||
@@ -774,10 +823,10 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Path must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
return $this->filterComponent(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$path
|
||||
$path,
|
||||
'Unable to filter URI path'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -794,13 +843,24 @@ class Uri implements UriInterface, \JsonSerializable
|
||||
throw new \InvalidArgumentException('Query and fragment must be a string');
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
return $this->filterComponent(
|
||||
'/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
|
||||
[$this, 'rawurlencodeMatchZero'],
|
||||
$str
|
||||
$str,
|
||||
'Unable to filter URI query or fragment'
|
||||
);
|
||||
}
|
||||
|
||||
private function filterComponent(string $pattern, string $component, string $context): string
|
||||
{
|
||||
$filtered = preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
|
||||
|
||||
if ($filtered === null) {
|
||||
throw new \RuntimeException($context.': '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
private function rawurlencodeMatchZero(array $match): string
|
||||
{
|
||||
return rawurlencode($match[0]);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ final class UriComparator
|
||||
*/
|
||||
public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
|
||||
{
|
||||
if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
|
||||
if (!Utils::caselessEquals($original->getHost(), $modified->getHost())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+9
-3
@@ -150,7 +150,13 @@ final class UriNormalizer
|
||||
}
|
||||
|
||||
if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
|
||||
$uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath()));
|
||||
$path = preg_replace('#//++#', '/', $uri->getPath());
|
||||
|
||||
if ($path === null) {
|
||||
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
$uri = $uri->withPath($path);
|
||||
}
|
||||
|
||||
if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
|
||||
@@ -186,7 +192,7 @@ final class UriNormalizer
|
||||
$regex = '/(?:%[A-Fa-f0-9]{2})++/';
|
||||
|
||||
$callback = function (array $match): string {
|
||||
return strtoupper($match[0]);
|
||||
return Utils::asciiToUpper($match[0]);
|
||||
};
|
||||
|
||||
return $uri
|
||||
@@ -217,7 +223,7 @@ final class UriNormalizer
|
||||
$normalized = preg_replace_callback($regex, $callback, $component);
|
||||
|
||||
if ($normalized === null) {
|
||||
throw new \RuntimeException('Unable to normalize URI component percent-encoding');
|
||||
throw new \RuntimeException('Unable to normalize URI component percent-encoding: '.preg_last_error_msg());
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
|
||||
Vendored
+81
-9
@@ -10,6 +10,65 @@ use Psr\Http\Message\UriInterface;
|
||||
|
||||
final class Utils
|
||||
{
|
||||
/**
|
||||
* Converts ASCII uppercase letters in a string to lowercase.
|
||||
*
|
||||
* Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the
|
||||
* conversion is locale-independent and leaves every non-ASCII byte
|
||||
* unchanged, as HTTP protocol elements require.
|
||||
*/
|
||||
public static function asciiToLower(string $string): string
|
||||
{
|
||||
return strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts ASCII lowercase letters in a string to uppercase.
|
||||
*
|
||||
* Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the
|
||||
* conversion is locale-independent and leaves every non-ASCII byte
|
||||
* unchanged, as HTTP protocol elements require.
|
||||
*/
|
||||
public static function asciiToUpper(string $string): string
|
||||
{
|
||||
return strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the first character of a string to uppercase when it is an
|
||||
* ASCII lowercase letter.
|
||||
*
|
||||
* Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion
|
||||
* is locale-independent and leaves every non-ASCII byte unchanged, as
|
||||
* HTTP protocol elements require.
|
||||
*/
|
||||
public static function asciiUcFirst(string $string): string
|
||||
{
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::asciiToUpper($string[0]).substr($string, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the haystack contains the needle, comparing ASCII
|
||||
* letters case-insensitively and without locale sensitivity.
|
||||
*/
|
||||
public static function caselessContains(string $haystack, string $needle): bool
|
||||
{
|
||||
return str_contains(self::asciiToLower($haystack), self::asciiToLower($needle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two strings are equal, comparing ASCII letters
|
||||
* case-insensitively and without locale sensitivity.
|
||||
*/
|
||||
public static function caselessEquals(string $left, string $right): bool
|
||||
{
|
||||
return self::asciiToLower($left) === self::asciiToLower($right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the items given by the keys, case insensitively from the data.
|
||||
*
|
||||
@@ -20,11 +79,11 @@ final class Utils
|
||||
$result = [];
|
||||
|
||||
foreach ($keys as &$key) {
|
||||
$key = strtolower((string) $key);
|
||||
$key = self::asciiToLower((string) $key);
|
||||
}
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
if (!in_array(strtolower((string) $k), $keys)) {
|
||||
if (!in_array(self::asciiToLower((string) $k), $keys)) {
|
||||
$result[$k] = $v;
|
||||
}
|
||||
}
|
||||
@@ -212,7 +271,7 @@ final class Utils
|
||||
if ($host !== '') {
|
||||
if (isset($changes['set_headers']) && is_array($changes['set_headers'])) {
|
||||
foreach (array_keys($changes['set_headers']) as $header) {
|
||||
if (strtolower((string) $header) === 'host') {
|
||||
if (self::asciiToLower((string) $header) === 'host') {
|
||||
throw new \InvalidArgumentException(
|
||||
'Cannot modify request with both a URI containing a host and an explicit Host header.'
|
||||
);
|
||||
@@ -248,7 +307,7 @@ final class Utils
|
||||
|
||||
$hasHost = false;
|
||||
foreach (array_keys($headers) as $header) {
|
||||
if (strtolower((string) $header) === 'host') {
|
||||
if (self::asciiToLower((string) $header) === 'host') {
|
||||
$hasHost = true;
|
||||
break;
|
||||
}
|
||||
@@ -284,7 +343,7 @@ final class Utils
|
||||
$addedHeaders = [];
|
||||
foreach ($headers as $header => $value) {
|
||||
$header = (string) $header;
|
||||
$normalized = strtolower($header);
|
||||
$normalized = self::asciiToLower($header);
|
||||
|
||||
if (isset($addedHeaders[$normalized])) {
|
||||
/** @var RequestInterface */
|
||||
@@ -462,6 +521,9 @@ final class Utils
|
||||
* in subsequent reads. String inputs are always treated as string bodies,
|
||||
* even when they name callable functions.
|
||||
*
|
||||
* Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast
|
||||
* it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars.
|
||||
*
|
||||
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
|
||||
* @param array{size?: int, metadata?: array} $options Additional options
|
||||
*
|
||||
@@ -470,10 +532,20 @@ final class Utils
|
||||
public static function streamFor($resource = '', array $options = []): StreamInterface
|
||||
{
|
||||
if (is_scalar($resource)) {
|
||||
// Convert non-finite floats explicitly, as implicit coercion of
|
||||
// NAN emits a warning on PHP 8.5.
|
||||
if (is_float($resource) && !is_finite($resource)) {
|
||||
$resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
|
||||
if (!is_string($resource)) {
|
||||
\trigger_deprecation(
|
||||
'guzzlehttp/psr7',
|
||||
'2.12',
|
||||
'Passing %s to Utils::streamFor() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
|
||||
\gettype($resource)
|
||||
);
|
||||
|
||||
if (is_float($resource) && !is_finite($resource)) {
|
||||
// Normalized only to avoid PHP 8.5's (string) NAN warning
|
||||
// while deprecated; 3.0 rejects non-finite floats with every
|
||||
// other non-string scalar.
|
||||
$resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
|
||||
}
|
||||
}
|
||||
|
||||
$stream = self::tryFopen('php://temp', 'r+');
|
||||
|
||||
Reference in New Issue
Block a user