| @@ -3,14 +3,66 @@ | ||
| 3 | 3 | declare (strict_types=1); |
| 4 | 4 | namespace YoastSEO_Vendor\GuzzleHttp\Psr7; |
| 5 | 5 | |
| 6 | 6 | use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; |
| 7 | -use YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; | |
| 8 | 7 | use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; |
| 9 | 8 | use YoastSEO_Vendor\Psr\Http\Message\UriInterface; |
| 10 | 9 | final class Utils |
| 11 | 10 | { |
| 12 | 11 | /** |
| 12 | + * Converts ASCII uppercase letters in a string to lowercase. | |
| 13 | + * | |
| 14 | + * Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the | |
| 15 | + * conversion is locale-independent and leaves every non-ASCII byte | |
| 16 | + * unchanged, as HTTP protocol elements require. | |
| 17 | + */ | |
| 18 | + public static function asciiToLower(string $string) : string | |
| 19 | + { | |
| 20 | + return \strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); | |
| 21 | + } | |
| 22 | + /** | |
| 23 | + * Converts ASCII lowercase letters in a string to uppercase. | |
| 24 | + * | |
| 25 | + * Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the | |
| 26 | + * conversion is locale-independent and leaves every non-ASCII byte | |
| 27 | + * unchanged, as HTTP protocol elements require. | |
| 28 | + */ | |
| 29 | + public static function asciiToUpper(string $string) : string | |
| 30 | + { | |
| 31 | + return \strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); | |
| 32 | + } | |
| 33 | + /** | |
| 34 | + * Converts the first character of a string to uppercase when it is an | |
| 35 | + * ASCII lowercase letter. | |
| 36 | + * | |
| 37 | + * Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion | |
| 38 | + * is locale-independent and leaves every non-ASCII byte unchanged, as | |
| 39 | + * HTTP protocol elements require. | |
| 40 | + */ | |
| 41 | + public static function asciiUcFirst(string $string) : string | |
| 42 | + { | |
| 43 | + if ($string === '') { | |
| 44 | + return ''; | |
| 45 | + } | |
| 46 | + return self::asciiToUpper($string[0]) . \substr($string, 1); | |
| 47 | + } | |
| 48 | + /** | |
| 49 | + * Checks whether the haystack contains the needle, comparing ASCII | |
| 50 | + * letters case-insensitively and without locale sensitivity. | |
| 51 | + */ | |
| 52 | + public static function caselessContains(string $haystack, string $needle) : bool | |
| 53 | + { | |
| 54 | + return \str_contains(self::asciiToLower($haystack), self::asciiToLower($needle)); | |
| 55 | + } | |
| 56 | + /** | |
| 57 | + * Checks whether two strings are equal, comparing ASCII letters | |
| 58 | + * case-insensitively and without locale sensitivity. | |
| 59 | + */ | |
| 60 | + public static function caselessEquals(string $left, string $right) : bool | |
| 61 | + { | |
| 62 | + return self::asciiToLower($left) === self::asciiToLower($right); | |
| 63 | + } | |
| 64 | + /** | |
| 13 | 65 | * Remove the items given by the keys, case insensitively from the data. |
| 14 | 66 | * |
| 15 | 67 | * @param (string|int)[] $keys |
| 16 | 68 | */ |
| @@ -17,12 +69,12 @@ | ||
| 17 | 69 | public static function caselessRemove(array $keys, array $data) : array |
| 18 | 70 | { |
| 19 | 71 | $result = []; |
| 20 | 72 | foreach ($keys as &$key) { |
| 21 | - $key = \strtolower((string) $key); | |
| 73 | + $key = self::asciiToLower((string) $key); | |
| 22 | 74 | } |
| 23 | 75 | foreach ($data as $k => $v) { |
| 24 | - if (!\in_array(\strtolower((string) $k), $keys)) { | |
| 76 | + if (!\in_array(self::asciiToLower((string) $k), $keys)) { | |
| 25 | 77 | $result[$k] = $v; |
| 26 | 78 | } |
| 27 | 79 | } |
| 28 | 80 | return $result; |
| @@ -30,8 +82,13 @@ | ||
| 30 | 82 | /** |
| 31 | 83 | * Copy the contents of a stream into another stream until the given number |
| 32 | 84 | * of bytes have been read. |
| 33 | 85 | * |
| 86 | + * The copy stops if the destination write returns 0, for example a | |
| 87 | + * BufferStream at its high water mark or a full DroppingStream. For a | |
| 88 | + * guaranteed full copy use a normal writable stream such as a file or | |
| 89 | + * php://temp stream. | |
| 90 | + * | |
| 34 | 91 | * @param StreamInterface $source Stream to read from |
| 35 | 92 | * @param StreamInterface $dest Stream to write to |
| 36 | 93 | * @param int $maxLen Maximum number of bytes to read. Pass -1 |
| 37 | 94 | * to read the entire stream. |
| @@ -42,11 +99,15 @@ | ||
| 42 | 99 | { |
| 43 | 100 | $bufferSize = 8192; |
| 44 | 101 | if ($maxLen === -1) { |
| 45 | 102 | while (!$source->eof()) { |
| 46 | - if (!$dest->write($source->read($bufferSize))) { | |
| 103 | + $buf = $source->read($bufferSize); | |
| 104 | + if ($buf === '') { | |
| 47 | 105 | break; |
| 48 | 106 | } |
| 107 | + if (!self::writeAll($dest, $buf)) { | |
| 108 | + break; | |
| 109 | + } | |
| 49 | 110 | } |
| 50 | 111 | } else { |
| 51 | 112 | $remaining = $maxLen; |
| 52 | 113 | while ($remaining > 0 && !$source->eof()) { |
| @@ -55,13 +116,33 @@ | ||
| 55 | 116 | if (!$len) { |
| 56 | 117 | break; |
| 57 | 118 | } |
| 58 | 119 | $remaining -= $len; |
| 59 | - $dest->write($buf); | |
| 120 | + if (!self::writeAll($dest, $buf)) { | |
| 121 | + break; | |
| 122 | + } | |
| 60 | 123 | } |
| 61 | 124 | } |
| 62 | 125 | } |
| 63 | 126 | /** |
| 127 | + * Writes the full buffer to the destination, retrying short writes. | |
| 128 | + * | |
| 129 | + * Returns false when the destination write returns 0 or less. | |
| 130 | + */ | |
| 131 | + private static function writeAll(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, string $buf) : bool | |
| 132 | + { | |
| 133 | + $written = 0; | |
| 134 | + $len = \strlen($buf); | |
| 135 | + while ($written < $len) { | |
| 136 | + $result = $dest->write(\substr($buf, $written)); | |
| 137 | + if ($result <= 0) { | |
| 138 | + return \false; | |
| 139 | + } | |
| 140 | + $written += $result; | |
| 141 | + } | |
| 142 | + return \true; | |
| 143 | + } | |
| 144 | + /** | |
| 64 | 145 | * Copy the contents of a stream into a string until the given number of |
| 65 | 146 | * bytes have been read. |
| 66 | 147 | * |
| 67 | 148 | * @param StreamInterface $stream Stream to read |
| @@ -127,11 +208,16 @@ | ||
| 127 | 208 | * a message. |
| 128 | 209 | * |
| 129 | 210 | * The changes can be one of: |
| 130 | 211 | * - method: (string) Changes the HTTP method. |
| 131 | - * - set_headers: (array) Sets the given headers. | |
| 132 | - * - remove_headers: (array) Remove the given headers. | |
| 133 | - * - body: (mixed) Sets the given body. | |
| 212 | + * - set_headers: (array) Sets the given headers. Values must be strings | |
| 213 | + * or non-empty arrays of strings. | |
| 214 | + * - remove_headers: (array) Remove the given headers. Values may be | |
| 215 | + * strings or integers. | |
| 216 | + * - body: (mixed) Sets the given body. Present non-null values are converted | |
| 217 | + * with self::streamFor(), including scalar values, resources, streams, | |
| 218 | + * iterators, callable arrays, closures, invokable objects, and objects | |
| 219 | + * with __toString(). String inputs remain literal bodies. | |
| 134 | 220 | * - uri: (UriInterface) Set the URI. |
| 135 | 221 | * - query: (string) Set the query string value of the URI. |
| 136 | 222 | * - version: (string) Set the protocol version. |
| 137 | 223 | * |
| @@ -142,14 +228,23 @@ | ||
| 142 | 228 | { |
| 143 | 229 | if (!$changes) { |
| 144 | 230 | return $request; |
| 145 | 231 | } |
| 232 | + self::warnOnInvalidModifyRequestChanges($changes); | |
| 146 | 233 | $headers = $request->getHeaders(); |
| 147 | 234 | if (!isset($changes['uri'])) { |
| 148 | 235 | $uri = $request->getUri(); |
| 149 | 236 | } else { |
| 150 | 237 | // Remove the host header if one is on the URI |
| 151 | - if ($host = $changes['uri']->getHost()) { | |
| 238 | + $host = $changes['uri']->getHost(); | |
| 239 | + if ($host !== '') { | |
| 240 | + if (isset($changes['set_headers']) && \is_array($changes['set_headers'])) { | |
| 241 | + foreach (\array_keys($changes['set_headers']) as $header) { | |
| 242 | + if (self::asciiToLower((string) $header) === 'host') { | |
| 243 | + throw new \InvalidArgumentException('Cannot modify request with both a URI containing a host and an explicit Host header.'); | |
| 244 | + } | |
| 245 | + } | |
| 246 | + } | |
| 152 | 247 | $changes['set_headers']['Host'] = $host; |
| 153 | 248 | if ($port = $changes['uri']->getPort()) { |
| 154 | 249 | $standardPorts = ['http' => 80, 'https' => 443]; |
| 155 | 250 | $scheme = $changes['uri']->getScheme(); |
| @@ -169,18 +264,121 @@ | ||
| 169 | 264 | } |
| 170 | 265 | if (isset($changes['query'])) { |
| 171 | 266 | $uri = $uri->withQuery($changes['query']); |
| 172 | 267 | } |
| 173 | - if ($request instanceof \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface) { | |
| 174 | - $new = (new \YoastSEO_Vendor\GuzzleHttp\Psr7\ServerRequest($changes['method'] ?? $request->getMethod(), $uri, $headers, $changes['body'] ?? $request->getBody(), $changes['version'] ?? $request->getProtocolVersion(), $request->getServerParams()))->withParsedBody($request->getParsedBody())->withQueryParams($request->getQueryParams())->withCookieParams($request->getCookieParams())->withUploadedFiles($request->getUploadedFiles()); | |
| 175 | - foreach ($request->getAttributes() as $key => $value) { | |
| 176 | - $new = $new->withAttribute($key, $value); | |
| 268 | + $hasHost = \false; | |
| 269 | + foreach (\array_keys($headers) as $header) { | |
| 270 | + if (self::asciiToLower((string) $header) === 'host') { | |
| 271 | + $hasHost = \true; | |
| 272 | + break; | |
| 177 | 273 | } |
| 178 | - return $new; | |
| 179 | 274 | } |
| 180 | - return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request($changes['method'] ?? $request->getMethod(), $uri, $headers, $changes['body'] ?? $request->getBody(), $changes['version'] ?? $request->getProtocolVersion()); | |
| 275 | + // Match Request::__construct() by adding a Host header when one is not provided. | |
| 276 | + if (!$hasHost && $uri->getHost() !== '') { | |
| 277 | + $host = $uri->getHost(); | |
| 278 | + if (($port = $uri->getPort()) !== null) { | |
| 279 | + $host .= ':' . $port; | |
| 280 | + } | |
| 281 | + $headers = ['Host' => [$host]] + $headers; | |
| 282 | + } | |
| 283 | + $new = $request; | |
| 284 | + if (isset($changes['method'])) { | |
| 285 | + $new = $new->withMethod($changes['method']); | |
| 286 | + } | |
| 287 | + if (isset($changes['uri']) || isset($changes['query'])) { | |
| 288 | + $new = $new->withUri($uri, \true); | |
| 289 | + } | |
| 290 | + if ($headers !== $new->getHeaders()) { | |
| 291 | + foreach (\array_keys($new->getHeaders()) as $header) { | |
| 292 | + /** @var RequestInterface */ | |
| 293 | + $new = $new->withoutHeader((string) $header); | |
| 294 | + } | |
| 295 | + $addedHeaders = []; | |
| 296 | + foreach ($headers as $header => $value) { | |
| 297 | + $header = (string) $header; | |
| 298 | + $normalized = self::asciiToLower($header); | |
| 299 | + if (isset($addedHeaders[$normalized])) { | |
| 300 | + /** @var RequestInterface */ | |
| 301 | + $new = $new->withAddedHeader($addedHeaders[$normalized], $value); | |
| 302 | + } else { | |
| 303 | + /** @var RequestInterface */ | |
| 304 | + $new = $new->withHeader($header, $value); | |
| 305 | + $addedHeaders[$normalized] = $header; | |
| 306 | + } | |
| 307 | + } | |
| 308 | + } | |
| 309 | + if (isset($changes['body'])) { | |
| 310 | + /** @var RequestInterface */ | |
| 311 | + $new = $new->withBody(self::streamFor($changes['body'])); | |
| 312 | + } | |
| 313 | + if (isset($changes['version'])) { | |
| 314 | + /** @var RequestInterface */ | |
| 315 | + $new = $new->withProtocolVersion($changes['version']); | |
| 316 | + } | |
| 317 | + return $new; | |
| 181 | 318 | } |
| 182 | 319 | /** |
| 320 | + * @param array<array-key, mixed> $changes | |
| 321 | + */ | |
| 322 | + private static function warnOnInvalidModifyRequestChanges(array $changes) : void | |
| 323 | + { | |
| 324 | + foreach (['method', 'query', 'version'] as $key) { | |
| 325 | + if (\array_key_exists($key, $changes) && !\is_string($changes[$key])) { | |
| 326 | + self::warnOnInvalidModifyRequestChange($key, 'string', $changes[$key]); | |
| 327 | + } | |
| 328 | + } | |
| 329 | + if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) { | |
| 330 | + self::warnOnInvalidModifyRequestChange('uri', 'UriInterface', $changes['uri']); | |
| 331 | + } | |
| 332 | + if (\array_key_exists('body', $changes) && $changes['body'] === null) { | |
| 333 | + self::warnOnInvalidModifyRequestChange('body', 'resource|string|int|float|bool|StreamInterface|callable|\\Iterator|\\Stringable', $changes['body']); | |
| 334 | + } | |
| 335 | + if (\array_key_exists('set_headers', $changes)) { | |
| 336 | + if (!\is_array($changes['set_headers'])) { | |
| 337 | + self::warnOnInvalidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']); | |
| 338 | + } else { | |
| 339 | + foreach ($changes['set_headers'] as $header => $value) { | |
| 340 | + $headerPath = \sprintf('set_headers.%s', (string) $header); | |
| 341 | + if (\is_array($value)) { | |
| 342 | + if ($value === []) { | |
| 343 | + self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value); | |
| 344 | + break; | |
| 345 | + } | |
| 346 | + foreach ($value as $index => $item) { | |
| 347 | + if (!\is_string($item)) { | |
| 348 | + self::warnOnInvalidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item); | |
| 349 | + break 2; | |
| 350 | + } | |
| 351 | + } | |
| 352 | + } elseif (!\is_string($value)) { | |
| 353 | + self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value); | |
| 354 | + break; | |
| 355 | + } | |
| 356 | + } | |
| 357 | + } | |
| 358 | + } | |
| 359 | + if (!\array_key_exists('remove_headers', $changes)) { | |
| 360 | + return; | |
| 361 | + } | |
| 362 | + if (!\is_array($changes['remove_headers'])) { | |
| 363 | + self::warnOnInvalidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']); | |
| 364 | + return; | |
| 365 | + } | |
| 366 | + foreach ($changes['remove_headers'] as $index => $header) { | |
| 367 | + if (!\is_string($header) && !\is_int($header)) { | |
| 368 | + self::warnOnInvalidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header); | |
| 369 | + return; | |
| 370 | + } | |
| 371 | + } | |
| 372 | + } | |
| 373 | + /** | |
| 374 | + * @param mixed $value | |
| 375 | + */ | |
| 376 | + private static function warnOnInvalidModifyRequestChange(string $key, string $expected, $value) : void | |
| 377 | + { | |
| 378 | + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/psr7', '2.11', 'Passing %s to Utils::modifyRequest() change "%s" is deprecated; guzzlehttp/psr7 3.0 requires %s.', \get_debug_type($value), $key, $expected); | |
| 379 | + } | |
| 380 | + /** | |
| 183 | 381 | * Read a line from the stream up to the maximum allowed buffer length. |
| 184 | 382 | * |
| 185 | 383 | * @param StreamInterface $stream Stream to read from |
| 186 | 384 | * @param int|null $maxLength Maximum buffer length |
| @@ -232,16 +430,20 @@ | ||
| 232 | 430 | * - `object` with `__toString()`: If the object has the `__toString()` method, |
| 233 | 431 | * the object will be cast to a string and then a stream will be returned that |
| 234 | 432 | * uses the string value. |
| 235 | 433 | * - `NULL`: When `null` is passed, an empty stream object is returned. |
| 236 | - * - `callable` When a callable is passed, a read-only stream object will be | |
| 237 | - * created that invokes the given callable. The callable is invoked with the | |
| 238 | - * number of suggested bytes to read. The callable can return any number of | |
| 239 | - * bytes, but MUST return `false` when there is no more data to return. The | |
| 240 | - * stream object that wraps the callable will invoke the callable until the | |
| 241 | - * number of requested bytes are available. Any additional bytes will be | |
| 242 | - * buffered and used in subsequent reads. | |
| 434 | + * - `callable`: When a callable array, closure, or invokable object is passed | |
| 435 | + * and no earlier resource or object rule applies, a read-only stream object | |
| 436 | + * will be created that invokes the given callable. The callable is invoked | |
| 437 | + * with the suggested number of bytes to read. The callable can return fewer | |
| 438 | + * or more bytes than requested, but MUST return `false` or `null` when there | |
| 439 | + * is no more data to return. Any additional bytes will be buffered and used | |
| 440 | + * in subsequent reads. String inputs are always treated as string bodies, | |
| 441 | + * even when they name callable functions. | |
| 243 | 442 | * |
| 443 | + * Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast | |
| 444 | + * it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars. | |
| 445 | + * | |
| 244 | 446 | * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data |
| 245 | 447 | * @param array{size?: int, metadata?: array} $options Additional options |
| 246 | 448 | * |
| 247 | 449 | * @throws \InvalidArgumentException if the $resource arg is not valid. |
| @@ -248,8 +450,17 @@ | ||
| 248 | 450 | */ |
| 249 | 451 | public static function streamFor($resource = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface |
| 250 | 452 | { |
| 251 | 453 | if (\is_scalar($resource)) { |
| 454 | + if (!\is_string($resource)) { | |
| 455 | + \YoastSEO_Vendor\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)); | |
| 456 | + if (\is_float($resource) && !\is_finite($resource)) { | |
| 457 | + // Normalized only to avoid PHP 8.5's (string) NAN warning | |
| 458 | + // while deprecated; 3.0 rejects non-finite floats with every | |
| 459 | + // other non-string scalar. | |
| 460 | + $resource = \is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF'); | |
| 461 | + } | |
| 462 | + } | |
| 252 | 463 | $stream = self::tryFopen('php://temp', 'r+'); |
| 253 | 464 | if ($resource !== '') { |
| 254 | 465 | \fwrite($stream, (string) $resource); |
| 255 | 466 | \fseek($stream, 0); |