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