PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.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 / Uri.php

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

573 lines 21.7 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\GuzzleHttp\Psr7\Exception\MalformedUriException;
7 use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
8 /**
9 * PSR-7 URI implementation.
10 *
11 * @author Michael Dowling
12 * @author Tobias Schultze
13 * @author Matthew Weier O'Phinney
14 */
15 class Uri implements \YoastSEO_Vendor\Psr\Http\Message\UriInterface, \JsonSerializable
16 {
17 /**
18 * Absolute http and https URIs require a host per RFC 7230 Section 2.7
19 * but in generic URIs the host can be empty. So for http(s) URIs
20 * we apply this default host when no host is given yet to form a
21 * valid URI.
22 */
23 private const HTTP_DEFAULT_HOST = 'localhost';
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'];
38 /** @var string Uri scheme. */
39 private $scheme = '';
40 /** @var string Uri user info. */
41 private $userInfo = '';
42 /** @var string Uri host. */
43 private $host = '';
44 /** @var int|null Uri port. */
45 private $port;
46 /** @var string Uri path. */
47 private $path = '';
48 /** @var string Uri query string. */
49 private $query = '';
50 /** @var string Uri fragment. */
51 private $fragment = '';
52 /** @var string|null String representation */
53 private $composedComponents;
54 public function __construct(string $uri = '')
55 {
56 if ($uri !== '') {
57 $parts = self::parse($uri);
58 if ($parts === \false) {
59 throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException("Unable to parse URI: {$uri}");
60 }
61 $this->applyParts($parts);
62 }
63 }
64 /**
65 * UTF-8 aware \parse_url() replacement.
66 *
67 * The internal function produces broken output for non ASCII domain names
68 * (IDN) when used with locales other than "C".
69 *
70 * On the other hand, cURL understands IDN correctly only when UTF-8 locale
71 * is configured ("C.UTF-8", "en_US.UTF-8", etc.).
72 *
73 * @see https://bugs.php.net/bug.php?id=52923
74 * @see https://www.php.net/manual/en/function.parse-url.php#114817
75 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING
76 *
77 * @return array|false
78 */
79 private static function parse(string $url)
80 {
81 // If IPv6
82 $prefix = '';
83 if (\preg_match('%^(.*://\\[[0-9:a-fA-F]+\\])(.*?)$%', $url, $matches)) {
84 /** @var array{0:string, 1:string, 2:string} $matches */
85 $prefix = $matches[1];
86 $url = $matches[2];
87 }
88 /** @var string */
89 $encodedUrl = \preg_replace_callback('%[^:/@?&=#]+%usD', static function ($matches) {
90 return \urlencode($matches[0]);
91 }, $url);
92 $result = \parse_url($prefix . $encodedUrl);
93 if ($result === \false) {
94 return \false;
95 }
96 return \array_map('urldecode', $result);
97 }
98 public function __toString() : string
99 {
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;
104 }
105 /**
106 * Composes a URI reference string from its various components.
107 *
108 * Usually this method does not need to be called manually but instead is used indirectly via
109 * `Psr\Http\Message\UriInterface::__toString`.
110 *
111 * PSR-7 UriInterface treats an empty component the same as a missing component as
112 * getQuery(), getFragment() etc. always return a string. This explains the slight
113 * difference to RFC 3986 Section 5.3.
114 *
115 * Another adjustment is that the authority separator is added even when the authority is missing/empty
116 * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with
117 * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But
118 * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to
119 * that format).
120 *
121 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3
122 */
123 public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment) : string
124 {
125 $uri = '';
126 // weak type checks to also accept null until we can add scalar type hints
127 if ($scheme != '') {
128 $uri .= $scheme . ':';
129 }
130 if ($authority != '' || $scheme === 'file') {
131 $uri .= '//' . $authority;
132 }
133 if ($authority != '' && $path != '' && $path[0] != '/') {
134 $path = '/' . $path;
135 }
136 $uri .= $path;
137 if ($query != '') {
138 $uri .= '?' . $query;
139 }
140 if ($fragment != '') {
141 $uri .= '#' . $fragment;
142 }
143 return $uri;
144 }
145 /**
146 * Whether the URI has the default port of the current scheme.
147 *
148 * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
149 * independently of the implementation.
150 */
151 public static function isDefaultPort(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool
152 {
153 return $uri->getPort() === null || isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()];
154 }
155 /**
156 * Whether the URI is absolute, i.e. it has a scheme.
157 *
158 * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
159 * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
160 * to another URI, the base URI. Relative references can be divided into several forms:
161 * - network-path references, e.g. '//example.com/path'
162 * - absolute-path references, e.g. '/path'
163 * - relative-path references, e.g. 'subpath'
164 *
165 * @see Uri::isNetworkPathReference
166 * @see Uri::isAbsolutePathReference
167 * @see Uri::isRelativePathReference
168 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4
169 */
170 public static function isAbsolute(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool
171 {
172 return $uri->getScheme() !== '';
173 }
174 /**
175 * Whether the URI is a network-path reference.
176 *
177 * A relative reference that begins with two slash characters is termed an network-path reference.
178 *
179 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
180 */
181 public static function isNetworkPathReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool
182 {
183 return $uri->getScheme() === '' && $uri->getAuthority() !== '';
184 }
185 /**
186 * Whether the URI is a absolute-path reference.
187 *
188 * A relative reference that begins with a single slash character is termed an absolute-path reference.
189 *
190 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
191 */
192 public static function isAbsolutePathReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool
193 {
194 return $uri->getScheme() === '' && $uri->getAuthority() === '' && isset($uri->getPath()[0]) && $uri->getPath()[0] === '/';
195 }
196 /**
197 * Whether the URI is a relative-path reference.
198 *
199 * A relative reference that does not begin with a slash character is termed a relative-path reference.
200 *
201 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
202 */
203 public static function isRelativePathReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri) : bool
204 {
205 return $uri->getScheme() === '' && $uri->getAuthority() === '' && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');
206 }
207 /**
208 * Whether the URI is a same-document reference.
209 *
210 * A same-document reference refers to a URI that is, aside from its fragment
211 * component, identical to the base URI. When no base URI is given, only an empty
212 * URI reference (apart from its fragment) is considered a same-document reference.
213 *
214 * @param UriInterface $uri The URI to check
215 * @param UriInterface|null $base An optional base URI to compare against
216 *
217 * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.4
218 */
219 public static function isSameDocumentReference(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, ?\YoastSEO_Vendor\Psr\Http\Message\UriInterface $base = null) : bool
220 {
221 if ($base !== null) {
222 $uri = \YoastSEO_Vendor\GuzzleHttp\Psr7\UriResolver::resolve($base, $uri);
223 return $uri->getScheme() === $base->getScheme() && $uri->getAuthority() === $base->getAuthority() && $uri->getPath() === $base->getPath() && $uri->getQuery() === $base->getQuery();
224 }
225 return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === '';
226 }
227 /**
228 * Creates a new URI with a specific query string value removed.
229 *
230 * Any existing query string values that exactly match the provided key are
231 * removed.
232 *
233 * @param UriInterface $uri URI to use as a base.
234 * @param string $key Query string key to remove.
235 */
236 public static function withoutQueryValue(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, string $key) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
237 {
238 $result = self::getFilteredQueryString($uri, [$key]);
239 return $uri->withQuery(\implode('&', $result));
240 }
241 /**
242 * Creates a new URI with a specific query string value.
243 *
244 * Any existing query string values that exactly match the provided key are
245 * removed and replaced with the given key value pair.
246 *
247 * A value of null will set the query string key without a value, e.g. "key"
248 * instead of "key=value".
249 *
250 * @param UriInterface $uri URI to use as a base.
251 * @param string $key Key to set.
252 * @param string|null $value Value to set
253 */
254 public static function withQueryValue(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, string $key, ?string $value) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
255 {
256 $result = self::getFilteredQueryString($uri, [$key]);
257 $result[] = self::generateQueryString($key, $value);
258 return $uri->withQuery(\implode('&', $result));
259 }
260 /**
261 * Creates a new URI with multiple specific query string values.
262 *
263 * It has the same behavior as withQueryValue() but for an associative array of key => value.
264 *
265 * @param UriInterface $uri URI to use as a base.
266 * @param (string|null)[] $keyValueArray Associative array of key and values
267 */
268 public static function withQueryValues(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $keyValueArray) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
269 {
270 $result = self::getFilteredQueryString($uri, \array_keys($keyValueArray));
271 foreach ($keyValueArray as $key => $value) {
272 $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
273 }
274 return $uri->withQuery(\implode('&', $result));
275 }
276 /**
277 * Creates a URI from a hash of `parse_url` components.
278 *
279 * @see https://www.php.net/manual/en/function.parse-url.php
280 *
281 * @throws MalformedUriException If the components do not form a valid URI.
282 */
283 public static function fromParts(array $parts) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
284 {
285 $uri = new self();
286 $uri->applyParts($parts);
287 $uri->validateState();
288 return $uri;
289 }
290 public function getScheme() : string
291 {
292 return $this->scheme;
293 }
294 public function getAuthority() : string
295 {
296 $authority = $this->host;
297 if ($this->userInfo !== '') {
298 $authority = $this->userInfo . '@' . $authority;
299 }
300 if ($this->port !== null) {
301 $authority .= ':' . $this->port;
302 }
303 return $authority;
304 }
305 public function getUserInfo() : string
306 {
307 return $this->userInfo;
308 }
309 public function getHost() : string
310 {
311 return $this->host;
312 }
313 public function getPort() : ?int
314 {
315 return $this->port;
316 }
317 public function getPath() : string
318 {
319 return $this->path;
320 }
321 public function getQuery() : string
322 {
323 return $this->query;
324 }
325 public function getFragment() : string
326 {
327 return $this->fragment;
328 }
329 public function withScheme($scheme) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
330 {
331 $scheme = $this->filterScheme($scheme);
332 if ($this->scheme === $scheme) {
333 return $this;
334 }
335 $new = clone $this;
336 $new->scheme = $scheme;
337 $new->composedComponents = null;
338 $new->removeDefaultPort();
339 $new->validateState();
340 return $new;
341 }
342 public function withUserInfo($user, $password = null) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
343 {
344 $info = $this->filterUserInfoComponent($user);
345 if ($password !== null) {
346 $info .= ':' . $this->filterUserInfoComponent($password);
347 }
348 if ($this->userInfo === $info) {
349 return $this;
350 }
351 $new = clone $this;
352 $new->userInfo = $info;
353 $new->composedComponents = null;
354 $new->validateState();
355 return $new;
356 }
357 public function withHost($host) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
358 {
359 $host = $this->filterHost($host);
360 if ($this->host === $host) {
361 return $this;
362 }
363 $new = clone $this;
364 $new->host = $host;
365 $new->composedComponents = null;
366 $new->validateState();
367 return $new;
368 }
369 public function withPort($port) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
370 {
371 $port = $this->filterPort($port);
372 if ($this->port === $port) {
373 return $this;
374 }
375 $new = clone $this;
376 $new->port = $port;
377 $new->composedComponents = null;
378 $new->removeDefaultPort();
379 $new->validateState();
380 return $new;
381 }
382 public function withPath($path) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
383 {
384 $path = $this->filterPath($path);
385 if ($this->path === $path) {
386 return $this;
387 }
388 $new = clone $this;
389 $new->path = $path;
390 $new->composedComponents = null;
391 $new->validateState();
392 return $new;
393 }
394 public function withQuery($query) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
395 {
396 $query = $this->filterQueryAndFragment($query);
397 if ($this->query === $query) {
398 return $this;
399 }
400 $new = clone $this;
401 $new->query = $query;
402 $new->composedComponents = null;
403 return $new;
404 }
405 public function withFragment($fragment) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
406 {
407 $fragment = $this->filterQueryAndFragment($fragment);
408 if ($this->fragment === $fragment) {
409 return $this;
410 }
411 $new = clone $this;
412 $new->fragment = $fragment;
413 $new->composedComponents = null;
414 return $new;
415 }
416 public function jsonSerialize() : string
417 {
418 return $this->__toString();
419 }
420 /**
421 * Apply parse_url parts to a URI.
422 *
423 * @param array $parts Array of parse_url parts to apply.
424 */
425 private function applyParts(array $parts) : void
426 {
427 $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
428 $this->userInfo = isset($parts['user']) ? $this->filterUserInfoComponent($parts['user']) : '';
429 $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : '';
430 $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
431 $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
432 $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
433 $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
434 if (isset($parts['pass'])) {
435 $this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']);
436 }
437 $this->removeDefaultPort();
438 }
439 /**
440 * @param mixed $scheme
441 *
442 * @throws \InvalidArgumentException If the scheme is invalid.
443 */
444 private function filterScheme($scheme) : string
445 {
446 if (!\is_string($scheme)) {
447 throw new \InvalidArgumentException('Scheme must be a string');
448 }
449 return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
450 }
451 /**
452 * @param mixed $component
453 *
454 * @throws \InvalidArgumentException If the user info is invalid.
455 */
456 private function filterUserInfoComponent($component) : string
457 {
458 if (!\is_string($component)) {
459 throw new \InvalidArgumentException('User info must be a string');
460 }
461 return \preg_replace_callback('/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $component);
462 }
463 /**
464 * @param mixed $host
465 *
466 * @throws \InvalidArgumentException If the host is invalid.
467 */
468 private function filterHost($host) : string
469 {
470 if (!\is_string($host)) {
471 throw new \InvalidArgumentException('Host must be a string');
472 }
473 return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
474 }
475 /**
476 * @param mixed $port
477 *
478 * @throws \InvalidArgumentException If the port is invalid.
479 */
480 private function filterPort($port) : ?int
481 {
482 if ($port === null) {
483 return null;
484 }
485 $port = (int) $port;
486 if (0 > $port || 0xffff < $port) {
487 throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
488 }
489 return $port;
490 }
491 /**
492 * @param (string|int)[] $keys
493 *
494 * @return string[]
495 */
496 private static function getFilteredQueryString(\YoastSEO_Vendor\Psr\Http\Message\UriInterface $uri, array $keys) : array
497 {
498 $current = $uri->getQuery();
499 if ($current === '') {
500 return [];
501 }
502 $decodedKeys = \array_map(function ($k) : string {
503 return \rawurldecode((string) $k);
504 }, $keys);
505 return \array_filter(\explode('&', $current), function ($part) use($decodedKeys) {
506 return !\in_array(\rawurldecode(\explode('=', $part)[0]), $decodedKeys, \true);
507 });
508 }
509 private static function generateQueryString(string $key, ?string $value) : string
510 {
511 // Query string separators ("=", "&") within the key or value need to be encoded
512 // (while preventing double-encoding) before setting the query string. All other
513 // chars that need percent-encoding will be encoded by withQuery().
514 $queryString = \strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
515 if ($value !== null) {
516 $queryString .= '=' . \strtr($value, self::QUERY_SEPARATORS_REPLACEMENT);
517 }
518 return $queryString;
519 }
520 private function removeDefaultPort() : void
521 {
522 if ($this->port !== null && self::isDefaultPort($this)) {
523 $this->port = null;
524 }
525 }
526 /**
527 * Filters the path of a URI
528 *
529 * @param mixed $path
530 *
531 * @throws \InvalidArgumentException If the path is invalid.
532 */
533 private function filterPath($path) : string
534 {
535 if (!\is_string($path)) {
536 throw new \InvalidArgumentException('Path must be a string');
537 }
538 return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path);
539 }
540 /**
541 * Filters the query string or fragment of a URI.
542 *
543 * @param mixed $str
544 *
545 * @throws \InvalidArgumentException If the query or fragment is invalid.
546 */
547 private function filterQueryAndFragment($str) : string
548 {
549 if (!\is_string($str)) {
550 throw new \InvalidArgumentException('Query and fragment must be a string');
551 }
552 return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\\/\\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str);
553 }
554 private function rawurlencodeMatchZero(array $match) : string
555 {
556 return \rawurlencode($match[0]);
557 }
558 private function validateState() : void
559 {
560 if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
561 $this->host = self::HTTP_DEFAULT_HOST;
562 }
563 if ($this->getAuthority() === '') {
564 if (0 === \strpos($this->path, '//')) {
565 throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException('The path of a URI without an authority must not start with two slashes "//"');
566 }
567 if ($this->scheme === '' && \false !== \strpos(\explode('/', $this->path, 2)[0], ':')) {
568 throw new \YoastSEO_Vendor\GuzzleHttp\Psr7\Exception\MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon');
569 }
570 }
571 }
572 }
573