PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.0
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.0
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.0, at vendor_prefixed/guzzlehttp/psr7/src/Utils.php

545 lines 22.9 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 * Remove the items given by the keys, case insensitively from the data.
13 *
14 * @param (string|int)[] $keys
15 */
16 public static function caselessRemove(array $keys, array $data) : array
17 {
18 $result = [];
19 foreach ($keys as &$key) {
20 $key = \strtolower((string) $key);
21 }
22 foreach ($data as $k => $v) {
23 if (!\in_array(\strtolower((string) $k), $keys)) {
24 $result[$k] = $v;
25 }
26 }
27 return $result;
28 }
29 /**
30 * Copy the contents of a stream into another stream until the given number
31 * of bytes have been read.
32 *
33 * The copy stops if the destination write returns 0, for example a
34 * BufferStream at its high water mark or a full DroppingStream. For a
35 * guaranteed full copy use a normal writable stream such as a file or
36 * php://temp stream.
37 *
38 * @param StreamInterface $source Stream to read from
39 * @param StreamInterface $dest Stream to write to
40 * @param int $maxLen Maximum number of bytes to read. Pass -1
41 * to read the entire stream.
42 *
43 * @throws \RuntimeException on error.
44 */
45 public static function copyToStream(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, int $maxLen = -1) : void
46 {
47 $bufferSize = 8192;
48 if ($maxLen === -1) {
49 while (!$source->eof()) {
50 $buf = $source->read($bufferSize);
51 if ($buf === '') {
52 break;
53 }
54 if (!self::writeAll($dest, $buf)) {
55 break;
56 }
57 }
58 } else {
59 $remaining = $maxLen;
60 while ($remaining > 0 && !$source->eof()) {
61 $buf = $source->read(\min($bufferSize, $remaining));
62 $len = \strlen($buf);
63 if (!$len) {
64 break;
65 }
66 $remaining -= $len;
67 if (!self::writeAll($dest, $buf)) {
68 break;
69 }
70 }
71 }
72 }
73 /**
74 * Writes the full buffer to the destination, retrying short writes.
75 *
76 * Returns false when the destination write returns 0 or less.
77 */
78 private static function writeAll(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $dest, string $buf) : bool
79 {
80 $written = 0;
81 $len = \strlen($buf);
82 while ($written < $len) {
83 $result = $dest->write(\substr($buf, $written));
84 if ($result <= 0) {
85 return \false;
86 }
87 $written += $result;
88 }
89 return \true;
90 }
91 /**
92 * Copy the contents of a stream into a string until the given number of
93 * bytes have been read.
94 *
95 * @param StreamInterface $stream Stream to read
96 * @param int $maxLen Maximum number of bytes to read. Pass -1
97 * to read the entire stream.
98 *
99 * @throws \RuntimeException on error.
100 */
101 public static function copyToString(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, int $maxLen = -1) : string
102 {
103 $buffer = '';
104 if ($maxLen === -1) {
105 while (!$stream->eof()) {
106 $buf = $stream->read(1048576);
107 if ($buf === '') {
108 break;
109 }
110 $buffer .= $buf;
111 }
112 return $buffer;
113 }
114 $len = 0;
115 while (!$stream->eof() && $len < $maxLen) {
116 $buf = $stream->read($maxLen - $len);
117 if ($buf === '') {
118 break;
119 }
120 $buffer .= $buf;
121 $len = \strlen($buffer);
122 }
123 return $buffer;
124 }
125 /**
126 * Calculate a hash of a stream.
127 *
128 * This method reads the entire stream to calculate a rolling hash, based
129 * on PHP's `hash_init` functions.
130 *
131 * @param StreamInterface $stream Stream to calculate the hash for
132 * @param string $algo Hash algorithm (e.g. md5, crc32, etc)
133 * @param bool $rawOutput Whether or not to use raw output
134 *
135 * @throws \RuntimeException on error.
136 */
137 public static function hash(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, string $algo, bool $rawOutput = \false) : string
138 {
139 $pos = $stream->tell();
140 if ($pos > 0) {
141 $stream->rewind();
142 }
143 $ctx = \hash_init($algo);
144 while (!$stream->eof()) {
145 \hash_update($ctx, $stream->read(1048576));
146 }
147 $out = \hash_final($ctx, $rawOutput);
148 $stream->seek($pos);
149 return $out;
150 }
151 /**
152 * Clone and modify a request with the given changes.
153 *
154 * This method is useful for reducing the number of clones needed to mutate
155 * a message.
156 *
157 * The changes can be one of:
158 * - method: (string) Changes the HTTP method.
159 * - set_headers: (array) Sets the given headers. Values must be strings
160 * or non-empty arrays of strings.
161 * - remove_headers: (array) Remove the given headers. Values may be
162 * strings or integers.
163 * - body: (mixed) Sets the given body. Present non-null values are converted
164 * with self::streamFor(), including scalar values, resources, streams,
165 * iterators, callable arrays, closures, invokable objects, and objects
166 * with __toString(). String inputs remain literal bodies.
167 * - uri: (UriInterface) Set the URI.
168 * - query: (string) Set the query string value of the URI.
169 * - version: (string) Set the protocol version.
170 *
171 * @param RequestInterface $request Request to clone and modify.
172 * @param array $changes Changes to apply.
173 */
174 public static function modifyRequest(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $changes) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface
175 {
176 if (!$changes) {
177 return $request;
178 }
179 self::warnOnInvalidModifyRequestChanges($changes);
180 $headers = $request->getHeaders();
181 if (!isset($changes['uri'])) {
182 $uri = $request->getUri();
183 } else {
184 // Remove the host header if one is on the URI
185 $host = $changes['uri']->getHost();
186 if ($host !== '') {
187 if (isset($changes['set_headers']) && \is_array($changes['set_headers'])) {
188 foreach (\array_keys($changes['set_headers']) as $header) {
189 if (\strtolower((string) $header) === 'host') {
190 throw new \InvalidArgumentException('Cannot modify request with both a URI containing a host and an explicit Host header.');
191 }
192 }
193 }
194 $changes['set_headers']['Host'] = $host;
195 if ($port = $changes['uri']->getPort()) {
196 $standardPorts = ['http' => 80, 'https' => 443];
197 $scheme = $changes['uri']->getScheme();
198 if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
199 $changes['set_headers']['Host'] .= ':' . $port;
200 }
201 }
202 }
203 $uri = $changes['uri'];
204 }
205 if (!empty($changes['remove_headers'])) {
206 $headers = self::caselessRemove($changes['remove_headers'], $headers);
207 }
208 if (!empty($changes['set_headers'])) {
209 $headers = self::caselessRemove(\array_keys($changes['set_headers']), $headers);
210 $headers = $changes['set_headers'] + $headers;
211 }
212 if (isset($changes['query'])) {
213 $uri = $uri->withQuery($changes['query']);
214 }
215 $hasHost = \false;
216 foreach (\array_keys($headers) as $header) {
217 if (\strtolower((string) $header) === 'host') {
218 $hasHost = \true;
219 break;
220 }
221 }
222 // Match Request::__construct() by adding a Host header when one is not provided.
223 if (!$hasHost && $uri->getHost() !== '') {
224 $host = $uri->getHost();
225 if (($port = $uri->getPort()) !== null) {
226 $host .= ':' . $port;
227 }
228 $headers = ['Host' => [$host]] + $headers;
229 }
230 $new = $request;
231 if (isset($changes['method'])) {
232 $new = $new->withMethod($changes['method']);
233 }
234 if (isset($changes['uri']) || isset($changes['query'])) {
235 $new = $new->withUri($uri, \true);
236 }
237 if ($headers !== $new->getHeaders()) {
238 foreach (\array_keys($new->getHeaders()) as $header) {
239 /** @var RequestInterface */
240 $new = $new->withoutHeader((string) $header);
241 }
242 $addedHeaders = [];
243 foreach ($headers as $header => $value) {
244 $header = (string) $header;
245 $normalized = \strtolower($header);
246 if (isset($addedHeaders[$normalized])) {
247 /** @var RequestInterface */
248 $new = $new->withAddedHeader($addedHeaders[$normalized], $value);
249 } else {
250 /** @var RequestInterface */
251 $new = $new->withHeader($header, $value);
252 $addedHeaders[$normalized] = $header;
253 }
254 }
255 }
256 if (isset($changes['body'])) {
257 /** @var RequestInterface */
258 $new = $new->withBody(self::streamFor($changes['body']));
259 }
260 if (isset($changes['version'])) {
261 /** @var RequestInterface */
262 $new = $new->withProtocolVersion($changes['version']);
263 }
264 return $new;
265 }
266 /**
267 * @param array<array-key, mixed> $changes
268 */
269 private static function warnOnInvalidModifyRequestChanges(array $changes) : void
270 {
271 foreach (['method', 'query', 'version'] as $key) {
272 if (\array_key_exists($key, $changes) && !\is_string($changes[$key])) {
273 self::warnOnInvalidModifyRequestChange($key, 'string', $changes[$key]);
274 }
275 }
276 if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) {
277 self::warnOnInvalidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
278 }
279 if (\array_key_exists('body', $changes) && $changes['body'] === null) {
280 self::warnOnInvalidModifyRequestChange('body', 'resource|string|int|float|bool|StreamInterface|callable|\\Iterator|\\Stringable', $changes['body']);
281 }
282 if (\array_key_exists('set_headers', $changes)) {
283 if (!\is_array($changes['set_headers'])) {
284 self::warnOnInvalidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
285 } else {
286 foreach ($changes['set_headers'] as $header => $value) {
287 $headerPath = \sprintf('set_headers.%s', (string) $header);
288 if (\is_array($value)) {
289 if ($value === []) {
290 self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
291 break;
292 }
293 foreach ($value as $index => $item) {
294 if (!\is_string($item)) {
295 self::warnOnInvalidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
296 break 2;
297 }
298 }
299 } elseif (!\is_string($value)) {
300 self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
301 break;
302 }
303 }
304 }
305 }
306 if (!\array_key_exists('remove_headers', $changes)) {
307 return;
308 }
309 if (!\is_array($changes['remove_headers'])) {
310 self::warnOnInvalidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
311 return;
312 }
313 foreach ($changes['remove_headers'] as $index => $header) {
314 if (!\is_string($header) && !\is_int($header)) {
315 self::warnOnInvalidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
316 return;
317 }
318 }
319 }
320 /**
321 * @param mixed $value
322 */
323 private static function warnOnInvalidModifyRequestChange(string $key, string $expected, $value) : void
324 {
325 \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);
326 }
327 /**
328 * Read a line from the stream up to the maximum allowed buffer length.
329 *
330 * @param StreamInterface $stream Stream to read from
331 * @param int|null $maxLength Maximum buffer length
332 */
333 public static function readLine(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, ?int $maxLength = null) : string
334 {
335 $buffer = '';
336 $size = 0;
337 while (!$stream->eof()) {
338 if ('' === ($byte = $stream->read(1))) {
339 return $buffer;
340 }
341 $buffer .= $byte;
342 // Break when a new line is found or the max length - 1 is reached
343 if ($byte === "\n" || ++$size === $maxLength - 1) {
344 break;
345 }
346 }
347 return $buffer;
348 }
349 /**
350 * Redact the password in the user info part of a URI.
351 */
352 public static function redactUserInfo(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
353 {
354 $userInfo = $uri->getUserInfo();
355 if (\false !== ($pos = \strpos($userInfo, ':'))) {
356 return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***');
357 }
358 return $uri;
359 }
360 /**
361 * Create a new stream based on the input type.
362 *
363 * Options is an associative array that can contain the following keys:
364 * - metadata: Array of custom metadata.
365 * - size: Size of the stream.
366 *
367 * This method accepts the following `$resource` types:
368 * - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
369 * - `string`: Creates a stream object that uses the given string as the contents.
370 * - `resource`: Creates a stream object that wraps the given PHP stream resource.
371 * - `Iterator`: If the provided value implements `Iterator`, then a read-only
372 * stream object will be created that wraps the given iterable. Each time the
373 * stream is read from, data from the iterator will fill a buffer and will be
374 * continuously called until the buffer is equal to the requested read size.
375 * Subsequent read calls will first read from the buffer and then call `next`
376 * on the underlying iterator until it is exhausted.
377 * - `object` with `__toString()`: If the object has the `__toString()` method,
378 * the object will be cast to a string and then a stream will be returned that
379 * uses the string value.
380 * - `NULL`: When `null` is passed, an empty stream object is returned.
381 * - `callable`: When a callable array, closure, or invokable object is passed
382 * and no earlier resource or object rule applies, a read-only stream object
383 * will be created that invokes the given callable. The callable is invoked
384 * with the suggested number of bytes to read. The callable can return fewer
385 * or more bytes than requested, but MUST return `false` or `null` when there
386 * is no more data to return. Any additional bytes will be buffered and used
387 * in subsequent reads. String inputs are always treated as string bodies,
388 * even when they name callable functions.
389 *
390 * Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast
391 * it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars.
392 *
393 * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
394 * @param array{size?: int, metadata?: array} $options Additional options
395 *
396 * @throws \InvalidArgumentException if the $resource arg is not valid.
397 */
398 public static function streamFor($resource = '', array $options = []) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface
399 {
400 if (\is_scalar($resource)) {
401 if (!\is_string($resource)) {
402 \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));
403 if (\is_float($resource) && !\is_finite($resource)) {
404 // Normalized only to avoid PHP 8.5's (string) NAN warning
405 // while deprecated; 3.0 rejects non-finite floats with every
406 // other non-string scalar.
407 $resource = \is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
408 }
409 }
410 $stream = self::tryFopen('php://temp', 'r+');
411 if ($resource !== '') {
412 \fwrite($stream, (string) $resource);
413 \fseek($stream, 0);
414 }
415 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($stream, $options);
416 }
417 switch (\gettype($resource)) {
418 case 'resource':
419 /*
420 * The 'php://input' is a special stream with quirks and inconsistencies.
421 * We avoid using that stream by reading it into php://temp
422 */
423 /** @var resource $resource */
424 if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') {
425 $stream = self::tryFopen('php://temp', 'w+');
426 \stream_copy_to_stream($resource, $stream);
427 \fseek($stream, 0);
428 $resource = $stream;
429 }
430 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream($resource, $options);
431 case 'object':
432 /** @var object $resource */
433 if ($resource instanceof \YoastSEO_Vendor\Psr\Http\Message\StreamInterface) {
434 return $resource;
435 } elseif ($resource instanceof \Iterator) {
436 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream(function () use($resource) {
437 if (!$resource->valid()) {
438 return \false;
439 }
440 $result = $resource->current();
441 $resource->next();
442 return $result;
443 }, $options);
444 } elseif (\method_exists($resource, '__toString')) {
445 return self::streamFor((string) $resource, $options);
446 }
447 break;
448 case 'NULL':
449 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Stream(self::tryFopen('php://temp', 'r+'), $options);
450 }
451 if (\is_callable($resource)) {
452 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\PumpStream($resource, $options);
453 }
454 throw new \InvalidArgumentException('Invalid resource type: ' . \gettype($resource));
455 }
456 /**
457 * Safely opens a PHP stream resource using a filename.
458 *
459 * When fopen fails, PHP normally raises a warning. This function adds an
460 * error handler that checks for errors and throws an exception instead.
461 *
462 * @param string $filename File to open
463 * @param string $mode Mode used to open the file
464 *
465 * @return resource
466 *
467 * @throws \RuntimeException if the file cannot be opened
468 */
469 public static function tryFopen(string $filename, string $mode)
470 {
471 $ex = null;
472 \set_error_handler(static function (int $errno, string $errstr) use($filename, $mode, &$ex) : bool {
473 $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $errstr));
474 return \true;
475 });
476 try {
477 /** @var resource $handle */
478 $handle = \fopen($filename, $mode);
479 } catch (\Throwable $e) {
480 $ex = new \RuntimeException(\sprintf('Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage()), 0, $e);
481 }
482 \restore_error_handler();
483 if ($ex) {
484 /** @var \RuntimeException $ex */
485 throw $ex;
486 }
487 return $handle;
488 }
489 /**
490 * Safely gets the contents of a given stream.
491 *
492 * When stream_get_contents fails, PHP normally raises a warning. This
493 * function adds an error handler that checks for errors and throws an
494 * exception instead.
495 *
496 * @param resource $stream
497 *
498 * @throws \RuntimeException if the stream cannot be read
499 */
500 public static function tryGetContents($stream) : string
501 {
502 $ex = null;
503 \set_error_handler(static function (int $errno, string $errstr) use(&$ex) : bool {
504 $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', $errstr));
505 return \true;
506 });
507 try {
508 /** @var string|false $contents */
509 $contents = \stream_get_contents($stream);
510 if ($contents === \false) {
511 $ex = new \RuntimeException('Unable to read stream contents');
512 }
513 } catch (\Throwable $e) {
514 $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', $e->getMessage()), 0, $e);
515 }
516 \restore_error_handler();
517 if ($ex) {
518 /** @var \RuntimeException $ex */
519 throw $ex;
520 }
521 return $contents;
522 }
523 /**
524 * Returns a UriInterface for the given value.
525 *
526 * This function accepts a string or UriInterface and returns a
527 * UriInterface for the given value. If the value is already a
528 * UriInterface, it is returned as-is.
529 *
530 * @param string|UriInterface $uri
531 *
532 * @throws \InvalidArgumentException
533 */
534 public static function uriFor($uri) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
535 {
536 if ($uri instanceof \YoastSEO_Vendor\Psr\Http\Message\UriInterface) {
537 return $uri;
538 }
539 if (\is_string($uri)) {
540 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Uri($uri);
541 }
542 throw new \InvalidArgumentException('URI must be a string or UriInterface');
543 }
544 }
545