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/Uri.php +138 -40 27.7 → trunk View file →
@@ -21,21 +21,9 @@
21 21 * valid URI.
22 22 */
23 23 private const HTTP_DEFAULT_HOST = 'localhost';
24 24 private const DEFAULT_PORTS = ['http' => 80, 'https' => 443, 'ftp' => 21, 'gopher' => 70, 'nntp' => 119, 'news' => 119, 'telnet' => 23, 'tn3270' => 23, 'imap' => 143, 'pop' => 110, 'ldap' => 389];
25 - /**
26 - * Unreserved characters for use in a regex.
27 - *
28 - * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
29 - */
30 - private const CHAR_UNRESERVED = 'a-zA-Z0-9_\\-\\.~';
31 - /**
32 - * Sub-delims for use in a regex.
33 - *
34 - * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
35 - */
36 - private const CHAR_SUB_DELIMS = '!\\$&\'\\(\\)\\*\\+,;=';
37 - private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26'];
25 + private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
38 26 /** @var string Uri scheme. */
39 27 private $scheme = '';
40 28 /** @var string Uri user info. */
41 29 private $userInfo = '';
@@ -48,10 +36,8 @@
48 36 /** @var string Uri query string. */
49 37 private $query = '';
50 38 /** @var string Uri fragment. */
51 39 private $fragment = '';
52 - /** @var string|null String representation */
53 - private $composedComponents;
54 40 public function __construct(string $uri = '')
55 41 {
56 42 if ($uri !== '') {
57 43 $parts = self::parse($uri);
@@ -57,9 +43,15 @@
57 43 $parts = self::parse($uri);
58 44 if ($parts === \false) {
59 45 throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException("Unable to parse URI: {$uri}");
60 46 }
61 - $this->applyParts($parts);
47 + try {
48 + $this->applyParts($parts);
49 + } catch (\YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException $e) {
50 + throw $e;
51 + } catch (\InvalidArgumentException $e) {
52 + throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException($e->getMessage(), 0, $e);
53 + }
62 54 }
63 55 }
64 56 /**
65 57 * UTF-8 aware \parse_url() replacement.
@@ -77,19 +69,39 @@
77 69 * @return array|false
78 70 */
79 71 private static function parse(string $url)
80 72 {
81 - // If IPv6
73 + if (self::isPathNoSchemeReference($url)) {
74 + return self::parsePathNoSchemeReference($url);
75 + }
76 + // Preserve bracketed IPv6 literals before encoding, including dotted IPv4
77 + // tails. DEL (\x7F) is excluded so a raw-DEL host falls through to the
78 + // general path and is rejected rather than silently mutated by parse_url().
82 79 $prefix = '';
83 - if (\preg_match('%^(.*://\\[[0-9:a-fA-F]+\\])(.*?)$%', $url, $matches)) {
80 + $ipv6Prefix = \preg_match('%\\A([0-9A-Za-z+.-]+://\\[[^\\]\\x00-\\x20\\x7F/?#@]+\\])(.*)\\z%s', $url, $matches);
81 + if ($ipv6Prefix === \false) {
82 + return \false;
83 + }
84 + if ($ipv6Prefix === 1) {
84 85 /** @var array{0:string, 1:string, 2:string} $matches */
86 + $suffix = $matches[2];
87 + // After the bracketed host only an optional numeric port and/or a
88 + // path, query, or fragment may follow. Anything else (for example
89 + // `:80@evil` or `:80x`) would let parse_url() reinterpret a
90 + // different host.
91 + if (\preg_match('%\\A(?::[0-9]*)?(?:[/?#].*)?\\z%s', $suffix) !== 1) {
92 + return \false;
93 + }
85 94 $prefix = $matches[1];
86 - $url = $matches[2];
95 + $url = $suffix;
87 96 }
88 - /** @var string */
97 + /** @var string|null */
89 98 $encodedUrl = \preg_replace_callback('%[^:/@?&=#]+%usD', static function ($matches) {
90 99 return \urlencode($matches[0]);
91 100 }, $url);
101 + if ($encodedUrl === null) {
102 + return \false;
103 + }
92 104 $result = \parse_url($prefix . $encodedUrl);
93 105 if ($result === \false) {
94 106 return \false;
95 107 }
@@ -94,14 +106,36 @@
94 106 return \false;
95 107 }
96 108 return \array_map('urldecode', $result);
97 109 }
110 + private static function isPathNoSchemeReference(string $url) : bool
111 + {
112 + if ($url === '' || $url[0] === '/' || $url[0] === '?' || $url[0] === '#') {
113 + return \false;
114 + }
115 + $firstSegment = \substr($url, 0, \strcspn($url, '/?#'));
116 + return \strpos($firstSegment, ':') === \false;
117 + }
118 + /**
119 + * @return array{path: string, query?: string, fragment?: string}
120 + */
121 + private static function parsePathNoSchemeReference(string $url) : array
122 + {
123 + $parts = [];
124 + if (\false !== ($fragmentPosition = \strpos($url, '#'))) {
125 + $parts['fragment'] = \substr($url, $fragmentPosition + 1);
126 + $url = \substr($url, 0, $fragmentPosition);
127 + }
128 + if (\false !== ($queryPosition = \strpos($url, '?'))) {
129 + $parts['query'] = \substr($url, $queryPosition + 1);
130 + $url = \substr($url, 0, $queryPosition);
131 + }
132 + $parts['path'] = $url;
133 + return $parts;
134 + }
98 135 public function __toString() : string
99 136 {
100 - if ($this->composedComponents === null) {
101 - $this->composedComponents = self::composeComponents($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
102 - }
103 - return $this->composedComponents;
137 + return self::composeComponents($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
104 138 }
105 139 /**
106 140 * Composes a URI reference string from its various components.
107 141 *
@@ -268,13 +302,31 @@
268 302 public static function withQueryValues(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $keyValueArray) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
269 303 {
270 304 $result = self::getFilteredQueryString($uri, \array_keys($keyValueArray));
271 305 foreach ($keyValueArray as $key => $value) {
272 - $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
306 + $result[] = self::generateQueryString((string) $key, $value !== null ? self::stringifyQueryValue($value) : null);
273 307 }
274 308 return $uri->withQuery(\implode('&', $result));
275 309 }
276 310 /**
311 + * Stringifies a non-null query value, deprecating non-string values that
312 + * guzzlehttp/psr7 3.0 will reject. Non-finite floats are normalized to the
313 + * strings PHP coerces them to, as implicit coercion of NAN emits a warning
314 + * on PHP 8.5.
315 + *
316 + * @param mixed $value
317 + */
318 + private static function stringifyQueryValue($value) : string
319 + {
320 + if (!\is_string($value)) {
321 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/psr7', '2.12', 'Passing %s to Uri::withQueryValues() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string or null query values.', \gettype($value));
322 + if (\is_float($value) && !\is_finite($value)) {
323 + return \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
324 + }
325 + }
326 + return (string) $value;
327 + }
328 + /**
277 329 * Creates a URI from a hash of `parse_url` components.
278 330 *
279 331 * @see https://www.php.net/manual/en/function.parse-url.php
280 332 *
@@ -282,12 +334,47 @@
282 334 */
283 335 public static function fromParts(array $parts) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
284 336 {
285 337 $uri = new self();
286 - $uri->applyParts($parts);
287 - $uri->validateState();
338 + try {
339 + $uri->applyParts($parts);
340 + $uri->validateState();
341 + } catch (\YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException $e) {
342 + throw $e;
343 + } catch (\InvalidArgumentException $e) {
344 + throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException($e->getMessage(), 0, $e);
345 + }
288 346 return $uri;
289 347 }
348 + /**
349 + * @throws \InvalidArgumentException If the host is invalid.
350 + *
351 + * @internal
352 + */
353 + public static function assertValidHost(string $host) : void
354 + {
355 + if ($host === '') {
356 + return;
357 + }
358 + // Reject control characters and URI authority delimiters so getHost()
359 + // cannot disagree with the on-wire authority.
360 + $invalidHost = \preg_match('/[\\x00-\\x20\\x7F\\/\\?#@\\\\]/', $host);
361 + if ($invalidHost === \false) {
362 + throw new \RuntimeException('Unable to validate URI host: ' . \preg_last_error_msg());
363 + }
364 + if ($invalidHost === 1) {
365 + throw new \InvalidArgumentException(\sprintf('Invalid host: "%s"', $host));
366 + }
367 + if (\strpos($host, '[') !== \false || \strpos($host, ']') !== \false) {
368 + if ($host[0] !== '[' || \substr($host, -1) !== ']') {
369 + throw new \InvalidArgumentException(\sprintf('Invalid host: "%s"', $host));
370 + }
371 + return;
372 + }
373 + if (\strpos($host, ':') !== \false) {
374 + throw new \InvalidArgumentException(\sprintf('Invalid host: "%s"', $host));
375 + }
376 + }
290 377 public function getScheme() : string
291 378 {
292 379 return $this->scheme;
293 380 }
@@ -333,9 +420,8 @@
333 420 return $this;
334 421 }
335 422 $new = clone $this;
336 423 $new->scheme = $scheme;
337 - $new->composedComponents = null;
338 424 $new->removeDefaultPort();
339 425 $new->validateState();
340 426 return $new;
341 427 }
@@ -349,9 +435,8 @@
349 435 return $this;
350 436 }
351 437 $new = clone $this;
352 438 $new->userInfo = $info;
353 - $new->composedComponents = null;
354 439 $new->validateState();
355 440 return $new;
356 441 }
357 442 public function withHost($host) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
@@ -361,14 +446,16 @@
361 446 return $this;
362 447 }
363 448 $new = clone $this;
364 449 $new->host = $host;
365 - $new->composedComponents = null;
366 450 $new->validateState();
367 451 return $new;
368 452 }
369 453 public function withPort($port) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
370 454 {
455 + if ($port !== null && !\is_int($port)) {
456 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/psr7', '2.11', 'Passing %s to UriInterface::withPort() is deprecated; guzzlehttp/psr7 3.0 requires int|null.', \get_debug_type($port));
457 + }
371 458 $port = $this->filterPort($port);
372 459 if ($this->port === $port) {
373 460 return $this;
374 461 }
@@ -373,9 +460,8 @@
373 460 return $this;
374 461 }
375 462 $new = clone $this;
376 463 $new->port = $port;
377 - $new->composedComponents = null;
378 464 $new->removeDefaultPort();
379 465 $new->validateState();
380 466 return $new;
381 467 }
@@ -386,9 +472,8 @@
386 472 return $this;
387 473 }
388 474 $new = clone $this;
389 475 $new->path = $path;
390 - $new->composedComponents = null;
391 476 $new->validateState();
392 477 return $new;
393 478 }
394 479 public function withQuery($query) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
@@ -398,9 +483,8 @@
398 483 return $this;
399 484 }
400 485 $new = clone $this;
401 486 $new->query = $query;
402 - $new->composedComponents = null;
403 487 return $new;
404 488 }
405 489 public function withFragment($fragment) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
406 490 {
@@ -409,9 +493,8 @@
409 493 return $this;
410 494 }
411 495 $new = clone $this;
412 496 $new->fragment = $fragment;
413 - $new->composedComponents = null;
414 497 return $new;
415 498 }
416 499 public function jsonSerialize() : string
417 500 {
@@ -445,9 +528,13 @@
445 528 {
446 529 if (!\is_string($scheme)) {
447 530 throw new \InvalidArgumentException('Scheme must be a string');
448 531 }
449 - return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
532 + $scheme = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($scheme);
533 + if ($scheme !== '' && !\preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
534 + \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/psr7', '2.11', 'Passing "%s" as a URI scheme is deprecated; guzzlehttp/psr7 3.0 requires URI schemes to match RFC 3986 syntax and begin with a letter.', $scheme);
535 + }
536 + return $scheme;
450 537 }
451 538 /**
452 539 * @param mixed $component
453 540 *
@@ -457,9 +544,9 @@
457 544 {
458 545 if (!\is_string($component)) {
459 546 throw new \InvalidArgumentException('User info must be a string');
460 547 }
461 - return \preg_replace_callback('/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $component);
548 + return $this->filterComponent('/(?:[^%' . \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc3986::CHAR_UNRESERVED . \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc3986::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/', $component, 'Unable to filter URI user info');
462 549 }
463 550 /**
464 551 * @param mixed $host
465 552 *
@@ -469,9 +556,11 @@
469 556 {
470 557 if (!\is_string($host)) {
471 558 throw new \InvalidArgumentException('Host must be a string');
472 559 }
473 - return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
560 + $host = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($host);
561 + self::assertValidHost($host);
562 + return $host;
474 563 }
475 564 /**
476 565 * @param mixed $port
477 566 *
@@ -507,9 +596,10 @@
507 596 });
508 597 }
509 598 private static function generateQueryString(string $key, ?string $value) : string
510 599 {
511 - // Query string separators ("=", "&") within the key or value need to be encoded
600 + // Query string separators ("=", "&") and literal plus signs ("+") within the
601 + // key or value need to be encoded
512 602 // (while preventing double-encoding) before setting the query string. All other
513 603 // chars that need percent-encoding will be encoded by withQuery().
514 604 $queryString = \strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
515 605 if ($value !== null) {
@@ -534,9 +624,9 @@
534 624 {
535 625 if (!\is_string($path)) {
536 626 throw new \InvalidArgumentException('Path must be a string');
537 627 }
538 - return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path);
628 + return $this->filterComponent('/(?:[^' . \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc3986::CHAR_UNRESERVED . \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc3986::CHAR_SUB_DELIMS . '%:@\\/]++|%(?![A-Fa-f0-9]{2}))/', $path, 'Unable to filter URI path');
539 629 }
540 630 /**
541 631 * Filters the query string or fragment of a URI.
542 632 *
@@ -548,9 +638,17 @@
548 638 {
549 639 if (!\is_string($str)) {
550 640 throw new \InvalidArgumentException('Query and fragment must be a string');
551 641 }
552 - return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\\/\\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str);
642 + return $this->filterComponent('/(?:[^' . \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc3986::CHAR_UNRESERVED . \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc3986::CHAR_SUB_DELIMS . '%:@\\/\\?]++|%(?![A-Fa-f0-9]{2}))/', $str, 'Unable to filter URI query or fragment');
643 + }
644 + private function filterComponent(string $pattern, string $component, string $context) : string
645 + {
646 + $filtered = \preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
647 + if ($filtered === null) {
648 + throw new \RuntimeException($context . ': ' . \preg_last_error_msg());
649 + }
650 + return $filtered;
553 651 }
554 652 private function rawurlencodeMatchZero(array $match) : string
555 653 {
556 654 return \rawurlencode($match[0]);