PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / vendor_prefixed / guzzlehttp / psr7 / src / Utils.php

Utils.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at vendor_prefixed/guzzlehttp/psr7/src/Utils.php

598 lines 25.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace YoastSEO_Vendor\GuzzleHttp\Psr7;
5
6 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
7 use YoastSEO_Vendor\Psr\Http\Message\StreamInterface;
8 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
9 final class Utils
10 {
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 /**
65 * Remove the items given by the keys, case insensitively from the data.
66 *
67 * @param (string|int)[] $keys
68 */
69 public static function caselessRemove(array $keys, array $data) : array
70 {
71 $result = [];
72 foreach ($keys as &$key) {
73 $key = self::asciiToLower((string) $key);
74 }
75 foreach ($data as $k => $v) {
76 if (!\in_array(self::asciiToLower((string) $k), $keys)) {
77 $result[$k] = $v;
78 }
79 }
80 return $result;
81 }
82 /**
83 * Copy the contents of a stream into another stream until the given number
84 * of bytes have been read.
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 *
91 * @param StreamInterface $source Stream to read from
92 * @param StreamInterface $dest Stream to write to
93 * @param int $maxLen Maximum number of bytes to read. Pass -1
94 * to read the entire stream.
95 *
96 * @throws \RuntimeException on error.
97 */
98 public static function copyToStream(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, int $maxLen = -1) : void
99 {
100 $bufferSize = 8192;
101 if ($maxLen === -1) {
102 while (!$source->eof()) {
103 $buf = $source->read($bufferSize);
104 if ($buf === '') {
105 break;
106 }
107 if (!self::writeAll($dest, $buf)) {
108 break;
109 }
110 }
111 } else {
112 $remaining = $maxLen;
113 while ($remaining > 0 && !$source->eof()) {
114 $buf = $source->read(\min($bufferSize, $remaining));
115 $len = \strlen($buf);
116 if (!$len) {
117 break;
118 }
119 $remaining -= $len;
120 if (!self::writeAll($dest, $buf)) {
121 break;
122 }
123 }
124 }
125 }
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 /**
145 * Copy the contents of a stream into a string until the given number of
146 * bytes have been read.
147 *
148 * @param StreamInterface $stream Stream to read
149 * @param int $maxLen Maximum number of bytes to read. Pass -1
150 * to read the entire stream.
151 *
152 * @throws \RuntimeException on error.
153 */
154 public static function copyToString(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, int $maxLen = -1) : string
155 {
156 $buffer = '';
157 if ($maxLen === -1) {
158 while (!$stream->eof()) {
159 $buf = $stream->read(1048576);
160 if ($buf === '') {
161 break;
162 }
163 $buffer .= $buf;
164 }
165 return $buffer;
166 }
167 $len = 0;
168 while (!$stream->eof() && $len < $maxLen) {
169 $buf = $stream->read($maxLen - $len);
170 if ($buf === '') {
171 break;
172 }
173 $buffer .= $buf;
174 $len = \strlen($buffer);
175 }
176 return $buffer;
177 }
178 /**
179 * Calculate a hash of a stream.
180 *
181 * This method reads the entire stream to calculate a rolling hash, based
182 * on PHP's `hash_init` functions.
183 *
184 * @param StreamInterface $stream Stream to calculate the hash for
185 * @param string $algo Hash algorithm (e.g. md5, crc32, etc)
186 * @param bool $rawOutput Whether or not to use raw output
187 *
188 * @throws \RuntimeException on error.
189 */
190 public static function hash(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, string $algo, bool $rawOutput = \false) : string
191 {
192 $pos = $stream->tell();
193 if ($pos > 0) {
194 $stream->rewind();
195 }
196 $ctx = \hash_init($algo);
197 while (!$stream->eof()) {
198 \hash_update($ctx, $stream->read(1048576));
199 }
200 $out = \hash_final($ctx, $rawOutput);
201 $stream->seek($pos);
202 return $out;
203 }
204 /**
205 * Clone and modify a request with the given changes.
206 *
207 * This method is useful for reducing the number of clones needed to mutate
208 * a message.
209 *
210 * The changes can be one of:
211 * - method: (string) Changes the HTTP method.
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.
220 * - uri: (UriInterface) Set the URI.
221 * - query: (string) Set the query string value of the URI.
222 * - version: (string) Set the protocol version.
223 *
224 * @param RequestInterface $request Request to clone and modify.
225 * @param array $changes Changes to apply.
226 */
227 public static function modifyRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $changes) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface
228 {
229 if (!$changes) {
230 return $request;
231 }
232 self::warnOnInvalidModifyRequestChanges($changes);
233 $headers = $request->getHeaders();
234 if (!isset($changes['uri'])) {
235 $uri = $request->getUri();
236 } else {
237 // Remove the host header if one is on the URI
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 }
247 $changes['set_headers']['Host'] = $host;
248 if ($port = $changes['uri']->getPort()) {
249 $standardPorts = ['http' => 80, 'https' => 443];
250 $scheme = $changes['uri']->getScheme();
251 if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
252 $changes['set_headers']['Host'] .= ':' . $port;
253 }
254 }
255 }
256 $uri = $changes['uri'];
257 }
258 if (!empty($changes['remove_headers'])) {
259 $headers = self::caselessRemove($changes['remove_headers'], $headers);
260 }
261 if (!empty($changes['set_headers'])) {
262 $headers = self::caselessRemove(\array_keys($changes['set_headers']), $headers);
263 $headers = $changes['set_headers'] + $headers;
264 }
265 if (isset($changes['query'])) {
266 $uri = $uri->withQuery($changes['query']);
267 }
268 $hasHost = \false;
269 foreach (\array_keys($headers) as $header) {
270 if (self::asciiToLower((string) $header) === 'host') {
271 $hasHost = \true;
272 break;
273 }
274 }
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;
318 }
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 /**
381 * Read a line from the stream up to the maximum allowed buffer length.
382 *
383 * @param StreamInterface $stream Stream to read from
384 * @param int|null $maxLength Maximum buffer length
385 */
386 public static function readLine(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?int $maxLength = null) : string
387 {
388 $buffer = '';
389 $size = 0;
390 while (!$stream->eof()) {
391 if ('' === ($byte = $stream->read(1))) {
392 return $buffer;
393 }
394 $buffer .= $byte;
395 // Break when a new line is found or the max length - 1 is reached
396 if ($byte === "\n" || ++$size === $maxLength - 1) {
397 break;
398 }
399 }
400 return $buffer;
401 }
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 /**
414 * Create a new stream based on the input type.
415 *
416 * Options is an associative array that can contain the following keys:
417 * - metadata: Array of custom metadata.
418 * - size: Size of the stream.
419 *
420 * This method accepts the following `$resource` types:
421 * - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
422 * - `string`: Creates a stream object that uses the given string as the contents.
423 * - `resource`: Creates a stream object that wraps the given PHP stream resource.
424 * - `Iterator`: If the provided value implements `Iterator`, then a read-only
425 * stream object will be created that wraps the given iterable. Each time the
426 * stream is read from, data from the iterator will fill a buffer and will be
427 * continuously called until the buffer is equal to the requested read size.
428 * Subsequent read calls will first read from the buffer and then call `next`
429 * on the underlying iterator until it is exhausted.
430 * - `object` with `__toString()`: If the object has the `__toString()` method,
431 * the object will be cast to a string and then a stream will be returned that
432 * uses the string value.
433 * - `NULL`: When `null` is passed, an empty stream object is returned.
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.
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 *
446 * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
447 * @param array{size?: int, metadata?: array} $options Additional options
448 *
449 * @throws \InvalidArgumentException if the $resource arg is not valid.
450 */
451 public static function streamFor($resource = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface
452 {
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 }
463 $stream = self::tryFopen('php://temp', 'r+');
464 if ($resource !== '') {
465 \fwrite($stream, (string) $resource);
466 \fseek($stream, 0);
467 }
468 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($stream, $options);
469 }
470 switch (\gettype($resource)) {
471 case 'resource':
472 /*
473 * The 'php://input' is a special stream with quirks and inconsistencies.
474 * We avoid using that stream by reading it into php://temp
475 */
476 /** @var resource $resource */
477 if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') {
478 $stream = self::tryFopen('php://temp', 'w+');
479 \stream_copy_to_stream($resource, $stream);
480 \fseek($stream, 0);
481 $resource = $stream;
482 }
483 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($resource, $options);
484 case 'object':
485 /** @var object $resource */
486 if ($resource instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) {
487 return $resource;
488 } elseif ($resource instanceof \Iterator) {
489 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream(function () use($resource) {
490 if (!$resource->valid()) {
491 return \false;
492 }
493 $result = $resource->current();
494 $resource->next();
495 return $result;
496 }, $options);
497 } elseif (\method_exists($resource, '__toString')) {
498 return self::streamFor((string) $resource, $options);
499 }
500 break;
501 case 'NULL':
502 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream(self::tryFopen('php://temp', 'r+'), $options);
503 }
504 if (\is_callable($resource)) {
505 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream($resource, $options);
506 }
507 throw new \InvalidArgumentException('Invalid resource type: ' . \gettype($resource));
508 }
509 /**
510 * Safely opens a PHP stream resource using a filename.
511 *
512 * When fopen fails, PHP normally raises a warning. This function adds an
513 * error handler that checks for errors and throws an exception instead.
514 *
515 * @param string $filename File to open
516 * @param string $mode Mode used to open the file
517 *
518 * @return resource
519 *
520 * @throws \RuntimeException if the file cannot be opened
521 */
522 public static function tryFopen(string $filename, string $mode)
523 {
524 $ex = null;
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));
527 return \true;
528 });
529 try {
530 /** @var resource $handle */
531 $handle = \fopen($filename, $mode);
532 } catch (\Throwable $e) {
533 $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage()), 0, $e);
534 }
535 \restore_error_handler();
536 if ($ex) {
537 /** @var \RuntimeException $ex */
538 throw $ex;
539 }
540 return $handle;
541 }
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 /**
577 * Returns a UriInterface for the given value.
578 *
579 * This function accepts a string or UriInterface and returns a
580 * UriInterface for the given value. If the value is already a
581 * UriInterface, it is returned as-is.
582 *
583 * @param string|UriInterface $uri
584 *
585 * @throws \InvalidArgumentException
586 */
587 public static function uriFor($uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
588 {
589 if ($uri instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) {
590 return $uri;
591 }
592 if (\is_string($uri)) {
593 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($uri);
594 }
595 throw new \InvalidArgumentException('URI must be a string or UriInterface');
596 }
597 }
598