| 1 |
<?php |
| 2 |
|
| 3 |
declare (strict_types=1); |
| 4 |
namespace WCPOS\Vendor\GuzzleHttp\Psr7; |
| 5 |
|
| 6 |
use WCPOS\Vendor\Psr\Http\Message\UriInterface; |
| 7 |
/** |
| 8 |
* Provides methods to normalize and compare URIs. |
| 9 |
* |
| 10 |
* @author Tobias Schultze |
| 11 |
* |
| 12 |
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-6 |
| 13 |
*/ |
| 14 |
final class UriNormalizer |
| 15 |
{ |
| 16 |
/** |
| 17 |
* Default normalizations which only include the ones that preserve |
| 18 |
* semantics. |
| 19 |
*/ |
| 20 |
public const PRESERVING_NORMALIZATIONS = self::CAPITALIZE_PERCENT_ENCODING | self::DECODE_UNRESERVED_CHARACTERS | self::CONVERT_EMPTY_PATH | self::REMOVE_DEFAULT_HOST | self::REMOVE_DEFAULT_PORT | self::REMOVE_DOT_SEGMENTS | self::CANONICALIZE_IPV6_HOST; |
| 21 |
/** |
| 22 |
* All letters within a percent-encoding triplet (e.g., "%3A") are |
| 23 |
* case-insensitive, and should be capitalized. This applies to the |
| 24 |
* userinfo, host, path, query, and fragment components. Bracketed |
| 25 |
* IP-literal hosts are skipped as a legacy tolerance for nonstandard values |
| 26 |
* other implementations may carry; zone-identifier text was briefly valid |
| 27 |
* URI syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The |
| 28 |
* userinfo and host are only rewritten when the value returned by the |
| 29 |
* implementation matches the normalized form, and a userinfo with an empty |
| 30 |
* user segment is never rewritten. No percent-encoding normalization is |
| 31 |
* applied to a component that contains malformed percent syntax, such as a |
| 32 |
* `%` not followed by two hexadecimal digits. |
| 33 |
* |
| 34 |
* Example: http://example.org/a%c2%b1b → http://example.org/a%C2%B1b |
| 35 |
*/ |
| 36 |
public const CAPITALIZE_PERCENT_ENCODING = 1; |
| 37 |
/** |
| 38 |
* Decodes percent-encoded octets of unreserved characters. |
| 39 |
* |
| 40 |
* For consistency, percent-encoded octets in the ranges of ALPHA (%41–%5A |
| 41 |
* and %61–%7A), DIGIT (%30–%39), hyphen (%2D), period (%2E), underscore |
| 42 |
* (%5F), or tilde (%7E) should not be created by URI producers and, when |
| 43 |
* found in a URI, should be decoded to their corresponding unreserved |
| 44 |
* characters by URI normalizers. This applies to the userinfo, host, path, |
| 45 |
* query, and fragment components. Since the host is case-insensitive and |
| 46 |
* PSR-7 requires it to be lowercase, octets decoded in the host are |
| 47 |
* lowercased (e.g., "%41" becomes "a"). Bracketed IP-literal hosts are |
| 48 |
* skipped as a legacy tolerance for nonstandard values other |
| 49 |
* implementations may carry; zone-identifier text was briefly valid URI |
| 50 |
* syntax under RFC 6874, which RFC 9844 obsoleted and reverted. The |
| 51 |
* userinfo and host are only rewritten when the value returned by the |
| 52 |
* implementation matches the normalized form, and a userinfo with an empty |
| 53 |
* user segment is never rewritten. No percent-encoding normalization is |
| 54 |
* applied to a component that contains malformed percent syntax, such as a |
| 55 |
* `%` not followed by two hexadecimal digits. |
| 56 |
* |
| 57 |
* Example: http://example.org/%7Eusern%61me/ → http://example.org/~username/ |
| 58 |
*/ |
| 59 |
public const DECODE_UNRESERVED_CHARACTERS = 2; |
| 60 |
/** |
| 61 |
* Converts the empty path to "/" for http and https URIs. |
| 62 |
* |
| 63 |
* Example: http://example.org → http://example.org/ |
| 64 |
*/ |
| 65 |
public const CONVERT_EMPTY_PATH = 4; |
| 66 |
/** |
| 67 |
* Removes the default host of the given URI scheme from the URI. |
| 68 |
* |
| 69 |
* Only the "file" scheme defines the default host "localhost". All of |
| 70 |
* `file:/myfile`, `file:///myfile`, and `file://localhost/myfile` are |
| 71 |
* equivalent according to RFC 3986. The first format is not accepted by |
| 72 |
* PHPs stream functions and thus already normalized implicitly to the |
| 73 |
* second format in the Uri class. See |
| 74 |
* `GuzzleHttp\Psr7\Uri::composeComponents`. |
| 75 |
* |
| 76 |
* When removing the host leaves a URI without an authority whose path |
| 77 |
* begins with `//`, the path is serialized with a `/.` prefix. |
| 78 |
* |
| 79 |
* Example: file://localhost/myfile → file:///myfile |
| 80 |
* Example: file://localhost//x → file:///.//x |
| 81 |
*/ |
| 82 |
public const REMOVE_DEFAULT_HOST = 8; |
| 83 |
/** |
| 84 |
* Removes the default port of the given URI scheme from the URI. |
| 85 |
* |
| 86 |
* Example: http://example.org:80/ → http://example.org/ |
| 87 |
*/ |
| 88 |
public const REMOVE_DEFAULT_PORT = 16; |
| 89 |
/** |
| 90 |
* Removes unnecessary dot-segments. |
| 91 |
* |
| 92 |
* Dot-segments in relative-path references are not removed as it would |
| 93 |
* change the semantics of the URI reference. |
| 94 |
* |
| 95 |
* Example: http://example.org/../a/b/../c/./d.html → http://example.org/a/c/d.html |
| 96 |
*/ |
| 97 |
public const REMOVE_DOT_SEGMENTS = 32; |
| 98 |
/** |
| 99 |
* Paths which include two or more adjacent slashes are converted to one. |
| 100 |
* |
| 101 |
* Webservers usually ignore duplicate slashes and treat those URIs |
| 102 |
* equivalent. But in theory those URIs do not need to be equivalent. So |
| 103 |
* this normalization may change the semantics. Encoded slashes (%2F) are |
| 104 |
* not removed. |
| 105 |
* |
| 106 |
* Example: http://example.org//foo///bar.html → http://example.org/foo/bar.html |
| 107 |
*/ |
| 108 |
public const REMOVE_DUPLICATE_SLASHES = 64; |
| 109 |
/** |
| 110 |
* Sort query parameters with their values in alphabetical order. |
| 111 |
* |
| 112 |
* However, the order of parameters in a URI may be significant (this is not |
| 113 |
* defined by the standard). So this normalization is not safe and may |
| 114 |
* change the semantics of the URI. |
| 115 |
* |
| 116 |
* Example: ?lang=en&article=fred → ?article=fred&lang=en |
| 117 |
* |
| 118 |
* Note: The sorting is neither locale nor Unicode aware (the URI query does |
| 119 |
* not get decoded at all) as the purpose is to be able to compare URIs in a |
| 120 |
* reproducible way, not to have the params sorted perfectly. |
| 121 |
*/ |
| 122 |
public const SORT_QUERY_PARAMETERS = 128; |
| 123 |
/** |
| 124 |
* Canonicalizes IPv6 hosts to their RFC 5952 form. |
| 125 |
* |
| 126 |
* IPv6 addresses allow leading zeros and multiple placements of the `::` |
| 127 |
* elision, so the same address has many textual spellings. The canonical |
| 128 |
* form is required for IPv6 literals in URIs by RFC 5952 Section 6 and |
| 129 |
* never changes what the URI refers to. Native `Uri` instances already |
| 130 |
* guarantee canonical output; for other implementations, the canonical |
| 131 |
* host is requested through `withHost()` and the result is kept only when |
| 132 |
* the returned `getHost()` exactly matches the requested spelling, |
| 133 |
* otherwise this step leaves the URI unchanged while other selected |
| 134 |
* normalizations still apply, and setter exceptions propagate. |
| 135 |
* |
| 136 |
* Example: http://[::0:0a]/ → http://[::a]/ |
| 137 |
*/ |
| 138 |
public const CANONICALIZE_IPV6_HOST = 256; |
| 139 |
/** |
| 140 |
* Returns a normalized URI. |
| 141 |
* |
| 142 |
* The scheme and host component are already normalized to lowercase per |
| 143 |
* PSR-7 UriInterface. This method adds additional normalizations that can |
| 144 |
* be configured with the `$flags` parameter, which is a bitmask of |
| 145 |
* normalizations to apply. |
| 146 |
* |
| 147 |
* PSR-7 UriInterface cannot distinguish between an empty component and a |
| 148 |
* missing component as `getQuery()`, `getFragment()` etc. always return a |
| 149 |
* string. This means the URIs `/?#` and `/` are treated equivalent which is |
| 150 |
* not necessarily true according to RFC 3986. But that difference is highly |
| 151 |
* uncommon in reality. So this potential normalization is implied in PSR-7 |
| 152 |
* as well. |
| 153 |
* |
| 154 |
* A path the URI cannot hold, such as a `//`-leading path without an |
| 155 |
* authority or a relative-path reference whose first segment contains a |
| 156 |
* colon, is prefixed with `/.` or `./` respectively instead of throwing, as |
| 157 |
* `UriResolver::resolve()` does. The percent-encoding normalizations only |
| 158 |
* do so where they rewrote the path. For example, decoding `a%41:` yields |
| 159 |
* `./aA:`, since `aA:` would be an absolute URI with the scheme `aa`. |
| 160 |
* |
| 161 |
* @param UriInterface $uri The URI to normalize |
| 162 |
* @param int $flags A bitmask of normalizations to apply, see constants |
| 163 |
* |
| 164 |
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 |
| 165 |
*/ |
| 166 |
public static function normalize(UriInterface $uri, int $flags = self::PRESERVING_NORMALIZATIONS) : UriInterface |
| 167 |
{ |
| 168 |
if ($flags & self::CAPITALIZE_PERCENT_ENCODING) { |
| 169 |
$uri = self::capitalizePercentEncoding($uri); |
| 170 |
} |
| 171 |
if ($flags & self::DECODE_UNRESERVED_CHARACTERS) { |
| 172 |
$uri = self::decodeUnreservedCharacters($uri); |
| 173 |
} |
| 174 |
if ($flags & self::CONVERT_EMPTY_PATH && $uri->getPath() === '' && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https')) { |
| 175 |
$uri = $uri->withPath('/'); |
| 176 |
} |
| 177 |
if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { |
| 178 |
if ($uri->getUserInfo() === '' && $uri->getPort() === null) { |
| 179 |
$path = Uri::rawPath($uri); |
| 180 |
if (\str_starts_with($path, '//')) { |
| 181 |
// "/." keeps a "//" path unambiguous once the authority is gone |
| 182 |
$uri = $uri->withPath('/.' . $path); |
| 183 |
} |
| 184 |
} |
| 185 |
$uri = $uri->withHost(''); |
| 186 |
} |
| 187 |
if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { |
| 188 |
$uri = $uri->withPort(null); |
| 189 |
} |
| 190 |
$removeDotSegments = $flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri); |
| 191 |
if ($removeDotSegments || $flags & self::REMOVE_DUPLICATE_SLASHES) { |
| 192 |
$path = Uri::rawPath($uri); |
| 193 |
if ($removeDotSegments) { |
| 194 |
$path = UriResolver::removeDotSegments($path); |
| 195 |
} |
| 196 |
if ($flags & self::REMOVE_DUPLICATE_SLASHES) { |
| 197 |
$path = \preg_replace('#//++#', '/', $path); |
| 198 |
if ($path === null) { |
| 199 |
throw new \RuntimeException('Unable to remove duplicate slashes from URI path: ' . \preg_last_error_msg()); |
| 200 |
} |
| 201 |
} |
| 202 |
$uri = $uri->withPath(UriResolver::guardedPath($uri, $path)); |
| 203 |
} |
| 204 |
if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { |
| 205 |
$queryKeyValues = \explode('&', $uri->getQuery()); |
| 206 |
\sort($queryKeyValues); |
| 207 |
$uri = $uri->withQuery(\implode('&', $queryKeyValues)); |
| 208 |
} |
| 209 |
if ($flags & self::CANONICALIZE_IPV6_HOST) { |
| 210 |
$uri = self::canonicalizeIpv6Host($uri); |
| 211 |
} |
| 212 |
return $uri; |
| 213 |
} |
| 214 |
/** |
| 215 |
* Whether two URIs can be considered equivalent. |
| 216 |
* |
| 217 |
* Both URIs are normalized automatically before comparison with the given |
| 218 |
* `$normalizations` bitmask. The method also accepts relative URI |
| 219 |
* references and returns true when they are equivalent. This of course |
| 220 |
* assumes they will be resolved against the same base URI. If this is not |
| 221 |
* the case, determination of equivalence or difference of relative |
| 222 |
* references does not mean anything. |
| 223 |
* |
| 224 |
* @param UriInterface $uri1 An URI to compare |
| 225 |
* @param UriInterface $uri2 An URI to compare |
| 226 |
* @param int $normalizations A bitmask of normalizations to apply, see constants |
| 227 |
* |
| 228 |
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.1 |
| 229 |
*/ |
| 230 |
public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, int $normalizations = self::PRESERVING_NORMALIZATIONS) : bool |
| 231 |
{ |
| 232 |
return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); |
| 233 |
} |
| 234 |
private static function capitalizePercentEncoding(UriInterface $uri) : UriInterface |
| 235 |
{ |
| 236 |
$regex = '/(?:%' . Rfc3986::HEX_OCTET . ')++/'; |
| 237 |
$callback = function (array $match) : string { |
| 238 |
return Utils::asciiToUpper($match[0]); |
| 239 |
}; |
| 240 |
$uri = self::withNormalizedUserInfo($uri, $regex, $callback); |
| 241 |
$uri = self::withNormalizedHost($uri, $regex, $callback); |
| 242 |
return self::withGuardedPath($uri, self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback)); |
| 243 |
} |
| 244 |
private static function decodeUnreservedCharacters(UriInterface $uri) : UriInterface |
| 245 |
{ |
| 246 |
$regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; |
| 247 |
$callback = function (array $match) : string { |
| 248 |
return \rawurldecode($match[0]); |
| 249 |
}; |
| 250 |
// The host is case-insensitive and PSR-7 requires it to be lowercase, |
| 251 |
// so decoded ALPHA octets (e.g. "%41") must land lowercase even for |
| 252 |
// implementations whose withHost() does not normalize the case. |
| 253 |
$hostCallback = function (array $match) : string { |
| 254 |
return Utils::asciiToLower(\rawurldecode($match[0])); |
| 255 |
}; |
| 256 |
$uri = self::withNormalizedUserInfo($uri, $regex, $callback); |
| 257 |
$uri = self::withNormalizedHost($uri, $regex, $hostCallback); |
| 258 |
return self::withGuardedPath($uri, self::normalizePercentEncodingInComponent(Uri::rawPath($uri), $regex, $callback))->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback)); |
| 259 |
} |
| 260 |
/** |
| 261 |
* Writes the given path only when it differs from the current one, guarded |
| 262 |
* so the write cannot throw. |
| 263 |
*/ |
| 264 |
private static function withGuardedPath(UriInterface $uri, string $path) : UriInterface |
| 265 |
{ |
| 266 |
if ($path === Uri::rawPath($uri)) { |
| 267 |
return $uri; |
| 268 |
} |
| 269 |
return $uri->withPath(UriResolver::guardedPath($uri, $path)); |
| 270 |
} |
| 271 |
/** |
| 272 |
* @param callable(array): string $callback |
| 273 |
*/ |
| 274 |
private static function withNormalizedUserInfo(UriInterface $uri, string $regex, callable $callback) : UriInterface |
| 275 |
{ |
| 276 |
$userInfo = $uri->getUserInfo(); |
| 277 |
if (!\str_contains($userInfo, '%')) { |
| 278 |
return $uri; |
| 279 |
} |
| 280 |
$normalized = self::normalizePercentEncodingInComponent($userInfo, $regex, $callback); |
| 281 |
if ($normalized === $userInfo) { |
| 282 |
return $uri; |
| 283 |
} |
| 284 |
// Normalization cannot create a colon: decoding is confined to |
| 285 |
// unreserved characters and capitalization keeps octets encoded. So |
| 286 |
// splitting on the first colon preserves the user/password boundary. |
| 287 |
$parts = \explode(':', $normalized, 2); |
| 288 |
// PSR-7 defines withUserInfo('') as removing the userinfo, so a |
| 289 |
// userinfo with an empty user segment (e.g. ":pass") cannot be |
| 290 |
// expressed through the setter and is preserved as-is instead. |
| 291 |
if ($parts[0] === '') { |
| 292 |
return $uri; |
| 293 |
} |
| 294 |
$candidate = $uri->withUserInfo($parts[0], $parts[1] ?? null); |
| 295 |
// Normalization must never lose or corrupt information, so verify the |
| 296 |
// representation the setter returned and leave the component untouched |
| 297 |
// when the implementation cannot represent the normalized form. |
| 298 |
if ($candidate->getUserInfo() !== $normalized) { |
| 299 |
return $uri; |
| 300 |
} |
| 301 |
return $candidate; |
| 302 |
} |
| 303 |
/** |
| 304 |
* @param callable(array): string $callback |
| 305 |
*/ |
| 306 |
private static function withNormalizedHost(UriInterface $uri, string $regex, callable $callback) : UriInterface |
| 307 |
{ |
| 308 |
$host = $uri->getHost(); |
| 309 |
// Bracketed IP-literal hosts are skipped as a legacy tolerance for |
| 310 |
// nonstandard values other implementations may carry, such as a zone |
| 311 |
// identifier in "[fe80::1%25eth0]"; that text was briefly valid URI |
| 312 |
// syntax under RFC 6874, which RFC 9844 obsoleted and reverted. |
| 313 |
if (\str_starts_with($host, '[') || !\str_contains($host, '%')) { |
| 314 |
return $uri; |
| 315 |
} |
| 316 |
$normalized = self::normalizePercentEncodingInComponent($host, $regex, $callback); |
| 317 |
if ($normalized === $host) { |
| 318 |
return $uri; |
| 319 |
} |
| 320 |
$candidate = $uri->withHost($normalized); |
| 321 |
// Normalization must never lose or corrupt information, so verify the |
| 322 |
// representation the setter returned and leave the component untouched |
| 323 |
// when the implementation cannot represent the normalized form. |
| 324 |
if ($candidate->getHost() !== $normalized) { |
| 325 |
return $uri; |
| 326 |
} |
| 327 |
return $candidate; |
| 328 |
} |
| 329 |
/** |
| 330 |
* @param callable(array): string $callback |
| 331 |
*/ |
| 332 |
private static function normalizePercentEncodingInComponent(string $component, string $regex, callable $callback) : string |
| 333 |
{ |
| 334 |
// Decoding a valid triplet that follows a dangling "%" would complete |
| 335 |
// the malformed sequence into a new valid triplet ("example%6%31com" |
| 336 |
// becomes "example%61com"), turning malformed text valid and breaking |
| 337 |
// idempotence, so a component containing malformed percent syntax is |
| 338 |
// returned unchanged. |
| 339 |
$malformed = \preg_match('/%(?!' . Rfc3986::HEX_OCTET . ')/', $component); |
| 340 |
if ($malformed === \false) { |
| 341 |
throw new \RuntimeException('Unable to scan URI component percent-encoding: ' . \preg_last_error_msg()); |
| 342 |
} |
| 343 |
if ($malformed === 1) { |
| 344 |
return $component; |
| 345 |
} |
| 346 |
$normalized = \preg_replace_callback($regex, $callback, $component); |
| 347 |
if ($normalized === null) { |
| 348 |
throw new \RuntimeException('Unable to normalize URI component percent-encoding: ' . \preg_last_error_msg()); |
| 349 |
} |
| 350 |
return $normalized; |
| 351 |
} |
| 352 |
private static function canonicalizeIpv6Host(UriInterface $uri) : UriInterface |
| 353 |
{ |
| 354 |
$host = $uri->getHost(); |
| 355 |
if (!\str_starts_with($host, '[') || !\str_ends_with($host, ']')) { |
| 356 |
return $uri; |
| 357 |
} |
| 358 |
// Foreign UriInterface implementations may carry IPvFuture literals, |
| 359 |
// IPv6 zone identifiers, uppercase text, or invalid spellings; |
| 360 |
// tryCanonicalizeIpv6() canonicalizes only what is unambiguously an |
| 361 |
// IPv6 address and leaves everything else untouched. |
| 362 |
$canonical = Rfc3986::tryCanonicalizeIpv6(\substr($host, 1, -1)); |
| 363 |
if ($canonical === null || '[' . $canonical . ']' === $host) { |
| 364 |
return $uri; |
| 365 |
} |
| 366 |
$candidate = $uri->withHost('[' . $canonical . ']'); |
| 367 |
// Normalization must never corrupt a component, so keep the original |
| 368 |
// host when the implementation does not retain the canonical form. |
| 369 |
if ($candidate->getHost() !== '[' . $canonical . ']') { |
| 370 |
return $uri; |
| 371 |
} |
| 372 |
return $candidate; |
| 373 |
} |
| 374 |
private function __construct() |
| 375 |
{ |
| 376 |
// cannot be instantiated |
| 377 |
} |
| 378 |
} |
| 379 |
|