| @@ -1,29 +1,80 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | +declare (strict_types=1); | |
| 3 | 4 | namespace YoastSEO_Vendor\GuzzleHttp\Psr7; |
| 4 | 5 | |
| 5 | 6 | use YoastSEO_Vendor\Psr\Http\Message\RequestInterface; |
| 6 | -use YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface; | |
| 7 | 7 | use YoastSEO_Vendor\Psr\Http\Message\StreamInterface; |
| 8 | 8 | use YoastSEO_Vendor\Psr\Http\Message\UriInterface; |
| 9 | 9 | final class Utils |
| 10 | 10 | { |
| 11 | 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 | + /** | |
| 12 | 65 | * Remove the items given by the keys, case insensitively from the data. |
| 13 | 66 | * |
| 14 | - * @param iterable<string> $keys | |
| 15 | - * | |
| 16 | - * @return array | |
| 67 | + * @param (string|int)[] $keys | |
| 17 | 68 | */ |
| 18 | - public static function caselessRemove($keys, array $data) | |
| 69 | + public static function caselessRemove(array $keys, array $data) : array | |
| 19 | 70 | { |
| 20 | 71 | $result = []; |
| 21 | 72 | foreach ($keys as &$key) { |
| 22 | - $key = \strtolower($key); | |
| 73 | + $key = self::asciiToLower((string) $key); | |
| 23 | 74 | } |
| 24 | 75 | foreach ($data as $k => $v) { |
| 25 | - if (!\in_array(\strtolower($k), $keys)) { | |
| 76 | + if (!\in_array(self::asciiToLower((string) $k), $keys)) { | |
| 26 | 77 | $result[$k] = $v; |
| 27 | 78 | } |
| 28 | 79 | } |
| 29 | 80 | return $result; |
| @@ -31,8 +82,13 @@ | ||
| 31 | 82 | /** |
| 32 | 83 | * Copy the contents of a stream into another stream until the given number |
| 33 | 84 | * of bytes have been read. |
| 34 | 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 | + * | |
| 35 | 91 | * @param StreamInterface $source Stream to read from |
| 36 | 92 | * @param StreamInterface $dest Stream to write to |
| 37 | 93 | * @param int $maxLen Maximum number of bytes to read. Pass -1 |
| 38 | 94 | * to read the entire stream. |
| @@ -38,16 +94,20 @@ | ||
| 38 | 94 | * to read the entire stream. |
| 39 | 95 | * |
| 40 | 96 | * @throws \RuntimeException on error. |
| 41 | 97 | */ |
| 42 | - public static function copyToStream(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, $maxLen = -1) | |
| 98 | + public static function copyToStream(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, int $maxLen = -1) : void | |
| 43 | 99 | { |
| 44 | 100 | $bufferSize = 8192; |
| 45 | 101 | if ($maxLen === -1) { |
| 46 | 102 | while (!$source->eof()) { |
| 47 | - if (!$dest->write($source->read($bufferSize))) { | |
| 103 | + $buf = $source->read($bufferSize); | |
| 104 | + if ($buf === '') { | |
| 48 | 105 | break; |
| 49 | 106 | } |
| 107 | + if (!self::writeAll($dest, $buf)) { | |
| 108 | + break; | |
| 109 | + } | |
| 50 | 110 | } |
| 51 | 111 | } else { |
| 52 | 112 | $remaining = $maxLen; |
| 53 | 113 | while ($remaining > 0 && !$source->eof()) { |
| @@ -56,13 +116,33 @@ | ||
| 56 | 116 | if (!$len) { |
| 57 | 117 | break; |
| 58 | 118 | } |
| 59 | 119 | $remaining -= $len; |
| 60 | - $dest->write($buf); | |
| 120 | + if (!self::writeAll($dest, $buf)) { | |
| 121 | + break; | |
| 122 | + } | |
| 61 | 123 | } |
| 62 | 124 | } |
| 63 | 125 | } |
| 64 | 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 | + /** | |
| 65 | 145 | * Copy the contents of a stream into a string until the given number of |
| 66 | 146 | * bytes have been read. |
| 67 | 147 | * |
| 68 | 148 | * @param StreamInterface $stream Stream to read |
| @@ -68,20 +148,17 @@ | ||
| 68 | 148 | * @param StreamInterface $stream Stream to read |
| 69 | 149 | * @param int $maxLen Maximum number of bytes to read. Pass -1 |
| 70 | 150 | * to read the entire stream. |
| 71 | 151 | * |
| 72 | - * @return string | |
| 73 | - * | |
| 74 | 152 | * @throws \RuntimeException on error. |
| 75 | 153 | */ |
| 76 | - public static function copyToString(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, $maxLen = -1) | |
| 154 | + public static function copyToString(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, int $maxLen = -1) : string | |
| 77 | 155 | { |
| 78 | 156 | $buffer = ''; |
| 79 | 157 | if ($maxLen === -1) { |
| 80 | 158 | while (!$stream->eof()) { |
| 81 | 159 | $buf = $stream->read(1048576); |
| 82 | - // Using a loose equality here to match on '' and false. | |
| 83 | - if ($buf == null) { | |
| 160 | + if ($buf === '') { | |
| 84 | 161 | break; |
| 85 | 162 | } |
| 86 | 163 | $buffer .= $buf; |
| 87 | 164 | } |
| @@ -89,10 +166,9 @@ | ||
| 89 | 166 | } |
| 90 | 167 | $len = 0; |
| 91 | 168 | while (!$stream->eof() && $len < $maxLen) { |
| 92 | 169 | $buf = $stream->read($maxLen - $len); |
| 93 | - // Using a loose equality here to match on '' and false. | |
| 94 | - if ($buf == null) { | |
| 170 | + if ($buf === '') { | |
| 95 | 171 | break; |
| 96 | 172 | } |
| 97 | 173 | $buffer .= $buf; |
| 98 | 174 | $len = \strlen($buffer); |
| @@ -108,13 +184,11 @@ | ||
| 108 | 184 | * @param StreamInterface $stream Stream to calculate the hash for |
| 109 | 185 | * @param string $algo Hash algorithm (e.g. md5, crc32, etc) |
| 110 | 186 | * @param bool $rawOutput Whether or not to use raw output |
| 111 | 187 | * |
| 112 | - * @return string Returns the hash of the stream | |
| 113 | - * | |
| 114 | 188 | * @throws \RuntimeException on error. |
| 115 | 189 | */ |
| 116 | - public static function hash(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, $algo, $rawOutput = \false) | |
| 190 | + public static function hash(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, string $algo, bool $rawOutput = \false) : string | |
| 117 | 191 | { |
| 118 | 192 | $pos = $stream->tell(); |
| 119 | 193 | if ($pos > 0) { |
| 120 | 194 | $stream->rewind(); |
| @@ -122,9 +196,9 @@ | ||
| 122 | 196 | $ctx = \hash_init($algo); |
| 123 | 197 | while (!$stream->eof()) { |
| 124 | 198 | \hash_update($ctx, $stream->read(1048576)); |
| 125 | 199 | } |
| 126 | - $out = \hash_final($ctx, (bool) $rawOutput); | |
| 200 | + $out = \hash_final($ctx, $rawOutput); | |
| 127 | 201 | $stream->seek($pos); |
| 128 | 202 | return $out; |
| 129 | 203 | } |
| 130 | 204 | /** |
| @@ -134,11 +208,16 @@ | ||
| 134 | 208 | * a message. |
| 135 | 209 | * |
| 136 | 210 | * The changes can be one of: |
| 137 | 211 | * - method: (string) Changes the HTTP method. |
| 138 | - * - set_headers: (array) Sets the given headers. | |
| 139 | - * - remove_headers: (array) Remove the given headers. | |
| 140 | - * - 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. | |
| 141 | 220 | * - uri: (UriInterface) Set the URI. |
| 142 | 221 | * - query: (string) Set the query string value of the URI. |
| 143 | 222 | * - version: (string) Set the protocol version. |
| 144 | 223 | * |
| @@ -143,22 +222,29 @@ | ||
| 143 | 222 | * - version: (string) Set the protocol version. |
| 144 | 223 | * |
| 145 | 224 | * @param RequestInterface $request Request to clone and modify. |
| 146 | 225 | * @param array $changes Changes to apply. |
| 147 | - * | |
| 148 | - * @return RequestInterface | |
| 149 | 226 | */ |
| 150 | - public static function modifyRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $changes) | |
| 227 | + public static function modifyRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $changes) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface | |
| 151 | 228 | { |
| 152 | 229 | if (!$changes) { |
| 153 | 230 | return $request; |
| 154 | 231 | } |
| 232 | + self::warnOnInvalidModifyRequestChanges($changes); | |
| 155 | 233 | $headers = $request->getHeaders(); |
| 156 | 234 | if (!isset($changes['uri'])) { |
| 157 | 235 | $uri = $request->getUri(); |
| 158 | 236 | } else { |
| 159 | 237 | // Remove the host header if one is on the URI |
| 160 | - 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 | + } | |
| 161 | 247 | $changes['set_headers']['Host'] = $host; |
| 162 | 248 | if ($port = $changes['uri']->getPort()) { |
| 163 | 249 | $standardPorts = ['http' => 80, 'https' => 443]; |
| 164 | 250 | $scheme = $changes['uri']->getScheme(); |
| @@ -178,32 +264,132 @@ | ||
| 178 | 264 | } |
| 179 | 265 | if (isset($changes['query'])) { |
| 180 | 266 | $uri = $uri->withQuery($changes['query']); |
| 181 | 267 | } |
| 182 | - if ($request instanceof \YoastSEO_Vendor\Psr\Http\Message\ServerRequestInterface) { | |
| 183 | - $new = (new \YoastSEO_Vendor\GuzzleHttp\Psr7\ServerRequest(isset($changes['method']) ? $changes['method'] : $request->getMethod(), $uri, $headers, isset($changes['body']) ? $changes['body'] : $request->getBody(), isset($changes['version']) ? $changes['version'] : $request->getProtocolVersion(), $request->getServerParams()))->withParsedBody($request->getParsedBody())->withQueryParams($request->getQueryParams())->withCookieParams($request->getCookieParams())->withUploadedFiles($request->getUploadedFiles()); | |
| 184 | - foreach ($request->getAttributes() as $key => $value) { | |
| 185 | - $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; | |
| 186 | 273 | } |
| 187 | - return $new; | |
| 188 | 274 | } |
| 189 | - return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Request(isset($changes['method']) ? $changes['method'] : $request->getMethod(), $uri, $headers, isset($changes['body']) ? $changes['body'] : $request->getBody(), isset($changes['version']) ? $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; | |
| 190 | 318 | } |
| 191 | 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 | + /** | |
| 192 | 381 | * Read a line from the stream up to the maximum allowed buffer length. |
| 193 | 382 | * |
| 194 | 383 | * @param StreamInterface $stream Stream to read from |
| 195 | 384 | * @param int|null $maxLength Maximum buffer length |
| 196 | - * | |
| 197 | - * @return string | |
| 198 | 385 | */ |
| 199 | - public static function readLine(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, $maxLength = null) | |
| 386 | + public static function readLine(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?int $maxLength = null) : string | |
| 200 | 387 | { |
| 201 | 388 | $buffer = ''; |
| 202 | 389 | $size = 0; |
| 203 | 390 | while (!$stream->eof()) { |
| 204 | - // Using a loose equality here to match on '' and false. | |
| 205 | - if (null == ($byte = $stream->read(1))) { | |
| 391 | + if ('' === ($byte = $stream->read(1))) { | |
| 206 | 392 | return $buffer; |
| 207 | 393 | } |
| 208 | 394 | $buffer .= $byte; |
| 209 | 395 | // Break when a new line is found or the max length - 1 is reached |
| @@ -213,8 +399,19 @@ | ||
| 213 | 399 | } |
| 214 | 400 | return $buffer; |
| 215 | 401 | } |
| 216 | 402 | /** |
| 403 | + * Redact the password in the user info part of a URI. | |
| 404 | + */ | |
| 405 | + public static function redactUserInfo(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface | |
| 406 | + { | |
| 407 | + $userInfo = $uri->getUserInfo(); | |
| 408 | + if (\false !== ($pos = \strpos($userInfo, ':'))) { | |
| 409 | + return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***'); | |
| 410 | + } | |
| 411 | + return $uri; | |
| 412 | + } | |
| 413 | + /** | |
| 217 | 414 | * Create a new stream based on the input type. |
| 218 | 415 | * |
| 219 | 416 | * Options is an associative array that can contain the following keys: |
| 220 | 417 | * - metadata: Array of custom metadata. |
| @@ -233,29 +430,40 @@ | ||
| 233 | 430 | * - `object` with `__toString()`: If the object has the `__toString()` method, |
| 234 | 431 | * the object will be cast to a string and then a stream will be returned that |
| 235 | 432 | * uses the string value. |
| 236 | 433 | * - `NULL`: When `null` is passed, an empty stream object is returned. |
| 237 | - * - `callable` When a callable is passed, a read-only stream object will be | |
| 238 | - * created that invokes the given callable. The callable is invoked with the | |
| 239 | - * number of suggested bytes to read. The callable can return any number of | |
| 240 | - * bytes, but MUST return `false` when there is no more data to return. The | |
| 241 | - * stream object that wraps the callable will invoke the callable until the | |
| 242 | - * number of requested bytes are available. Any additional bytes will be | |
| 243 | - * 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. | |
| 244 | 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 | + * | |
| 245 | 446 | * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data |
| 246 | - * @param array $options Additional options | |
| 447 | + * @param array{size?: int, metadata?: array} $options Additional options | |
| 247 | 448 | * |
| 248 | - * @return StreamInterface | |
| 249 | - * | |
| 250 | 449 | * @throws \InvalidArgumentException if the $resource arg is not valid. |
| 251 | 450 | */ |
| 252 | - public static function streamFor($resource = '', array $options = []) | |
| 451 | + public static function streamFor($resource = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface | |
| 253 | 452 | { |
| 254 | 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 | + } | |
| 255 | 463 | $stream = self::tryFopen('php://temp', 'r+'); |
| 256 | 464 | if ($resource !== '') { |
| 257 | - \fwrite($stream, $resource); | |
| 465 | + \fwrite($stream, (string) $resource); | |
| 258 | 466 | \fseek($stream, 0); |
| 259 | 467 | } |
| 260 | 468 | return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($stream, $options); |
| 261 | 469 | } |
| @@ -264,17 +472,18 @@ | ||
| 264 | 472 | /* |
| 265 | 473 | * The 'php://input' is a special stream with quirks and inconsistencies. |
| 266 | 474 | * We avoid using that stream by reading it into php://temp |
| 267 | 475 | */ |
| 268 | - $metaData = \stream_get_meta_data($resource); | |
| 269 | - if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') { | |
| 476 | + /** @var resource $resource */ | |
| 477 | + if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') { | |
| 270 | 478 | $stream = self::tryFopen('php://temp', 'w+'); |
| 271 | - \fwrite($stream, \stream_get_contents($resource)); | |
| 479 | + \stream_copy_to_stream($resource, $stream); | |
| 272 | 480 | \fseek($stream, 0); |
| 273 | 481 | $resource = $stream; |
| 274 | 482 | } |
| 275 | 483 | return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($resource, $options); |
| 276 | 484 | case 'object': |
| 485 | + /** @var object $resource */ | |
| 277 | 486 | if ($resource instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) { |
| 278 | 487 | return $resource; |
| 279 | 488 | } elseif ($resource instanceof \Iterator) { |
| 280 | 489 | return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream(function () use($resource) { |
| @@ -285,9 +494,9 @@ | ||
| 285 | 494 | $resource->next(); |
| 286 | 495 | return $result; |
| 287 | 496 | }, $options); |
| 288 | 497 | } elseif (\method_exists($resource, '__toString')) { |
| 289 | - return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor((string) $resource, $options); | |
| 498 | + return self::streamFor((string) $resource, $options); | |
| 290 | 499 | } |
| 291 | 500 | break; |
| 292 | 501 | case 'NULL': |
| 293 | 502 | return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream(self::tryFopen('php://temp', 'r+'), $options); |
| @@ -309,16 +518,17 @@ | ||
| 309 | 518 | * @return resource |
| 310 | 519 | * |
| 311 | 520 | * @throws \RuntimeException if the file cannot be opened |
| 312 | 521 | */ |
| 313 | - public static function tryFopen($filename, $mode) | |
| 522 | + public static function tryFopen(string $filename, string $mode) | |
| 314 | 523 | { |
| 315 | 524 | $ex = null; |
| 316 | - \set_error_handler(function () use($filename, $mode, &$ex) { | |
| 317 | - $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, \func_get_args()[1])); | |
| 525 | + \set_error_handler(static function (int $errno, string $errstr) use($filename, $mode, &$ex) : bool { | |
| 526 | + $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $errstr)); | |
| 318 | 527 | return \true; |
| 319 | 528 | }); |
| 320 | 529 | try { |
| 530 | + /** @var resource $handle */ | |
| 321 | 531 | $handle = \fopen($filename, $mode); |
| 322 | 532 | } catch (\Throwable $e) { |
| 323 | 533 | $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage()), 0, $e); |
| 324 | 534 | } |
| @@ -323,14 +533,48 @@ | ||
| 323 | 533 | $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage()), 0, $e); |
| 324 | 534 | } |
| 325 | 535 | \restore_error_handler(); |
| 326 | 536 | if ($ex) { |
| 327 | - /** @var $ex \RuntimeException */ | |
| 537 | + /** @var \RuntimeException $ex */ | |
| 328 | 538 | throw $ex; |
| 329 | 539 | } |
| 330 | 540 | return $handle; |
| 331 | 541 | } |
| 332 | 542 | /** |
| 543 | + * Safely gets the contents of a given stream. | |
| 544 | + * | |
| 545 | + * When stream_get_contents fails, PHP normally raises a warning. This | |
| 546 | + * function adds an error handler that checks for errors and throws an | |
| 547 | + * exception instead. | |
| 548 | + * | |
| 549 | + * @param resource $stream | |
| 550 | + * | |
| 551 | + * @throws \RuntimeException if the stream cannot be read | |
| 552 | + */ | |
| 553 | + public static function tryGetContents($stream) : string | |
| 554 | + { | |
| 555 | + $ex = null; | |
| 556 | + \set_error_handler(static function (int $errno, string $errstr) use(&$ex) : bool { | |
| 557 | + $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', $errstr)); | |
| 558 | + return \true; | |
| 559 | + }); | |
| 560 | + try { | |
| 561 | + /** @var string|false $contents */ | |
| 562 | + $contents = \stream_get_contents($stream); | |
| 563 | + if ($contents === \false) { | |
| 564 | + $ex = new \RuntimeException('Unable to read stream contents'); | |
| 565 | + } | |
| 566 | + } catch (\Throwable $e) { | |
| 567 | + $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', $e->getMessage()), 0, $e); | |
| 568 | + } | |
| 569 | + \restore_error_handler(); | |
| 570 | + if ($ex) { | |
| 571 | + /** @var \RuntimeException $ex */ | |
| 572 | + throw $ex; | |
| 573 | + } | |
| 574 | + return $contents; | |
| 575 | + } | |
| 576 | + /** | |
| 333 | 577 | * Returns a UriInterface for the given value. |
| 334 | 578 | * |
| 335 | 579 | * This function accepts a string or UriInterface and returns a |
| 336 | 580 | * UriInterface for the given value. If the value is already a |
| @@ -337,13 +581,11 @@ | ||
| 337 | 581 | * UriInterface, it is returned as-is. |
| 338 | 582 | * |
| 339 | 583 | * @param string|UriInterface $uri |
| 340 | 584 | * |
| 341 | - * @return UriInterface | |
| 342 | - * | |
| 343 | 585 | * @throws \InvalidArgumentException |
| 344 | 586 | */ |
| 345 | - public static function uriFor($uri) | |
| 587 | + public static function uriFor($uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface | |
| 346 | 588 | { |
| 347 | 589 | if ($uri instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) { |
| 348 | 590 | return $uri; |
| 349 | 591 | } |