PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.6 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 All 130 releases
← All changes | vendor_prefixed/guzzlehttp/psr7/src/Utils.php +304 -62 18.6 → trunk View file →
@@ -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 }