Lines
90.00%
54 / 60
Methods
25.00%
1 / 4
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| resolve | 100.00% | 23 / 23 | 100.00% | 1 / 1 | 6 | ||
| mergePaths | 71.42% | 5 / 7 | 0.00% | 0 / 1 | 4.37 | ||
| removeDotSegments | 88.46% | 23 / 26 | 0.00% | 0 / 1 | 11.19 | ||
| removeLastSegment | 75.00% | 3 / 4 | 0.00% | 0 / 1 | 2.06 | ||
| 20 | final class UriResolver | |
| 21 | { | |
| 22 | /** | |
| 23 | * Resolve a URI reference against a base URI. | |
| 24 | * | |
| 25 | * The result is built by cloning `$rel` and applying `with*` mutators | |
| 26 | * conditionally โ no new URI is ever constructed from scratch. | |
| 27 | * | |
| 28 | * @param UriInterface $base The base URI (e.g. the configured `base_uri`) | |
| 29 | * @param UriInterface $rel The URI reference to resolve (e.g. the request URI) | |
| 30 | */ | |
| 31 | public static function resolve(UriInterface $base, UriInterface $rel): UriInterface | |
| 32 | { | |
| 33 | // ยง5.2.2: if the reference has a scheme, it is an absolute URI and | |
| 34 | // overrides the base entirely. | |
| 35 | if ($rel->getScheme() !== '') { | |
| 36 | return $rel->withPath(self::removeDotSegments($rel->getPath())); | |
| 37 | } | |
| 38 | ||
| 39 | $result = $rel; | |
| 40 | ||
| 41 | // Network-path reference ("//host/path") โ inherit the scheme from | |
| 42 | // the base, but keep the reference's own authority. | |
| 43 | if ($rel->getHost() !== '') { | |
| 44 | return $result | |
| 45 | ->withScheme($base->getScheme()) | |
| 46 | ->withPath(self::removeDotSegments($rel->getPath())) | |
| 47 | ->withQuery($rel->getQuery()); | |
| 48 | } | |
| 49 | ||
| 50 | // Relative reference โ inherit scheme and authority from the base. | |
| 51 | $result = $result | |
| 52 | ->withScheme($base->getScheme()) | |
| 53 | ->withUserInfo($base->getUserInfo()) | |
| 54 | ->withHost($base->getHost()) | |
| 55 | ->withPort($base->getPort()); | |
| 56 | ||
| 57 | // ยง5.2.2: empty path โ take the base path; query from the reference | |
| 58 | // if present, else from the base. Fragment always comes from $rel | |
| 59 | // (preserved by cloning). | |
| 60 | if ($rel->getPath() === '') { | |
| 61 | return $result | |
| 62 | ->withPath($base->getPath()) | |
| 63 | ->withQuery($rel->getQuery() !== '' ? $rel->getQuery() : $base->getQuery()); | |
| 64 | } | |
| 65 | ||
| 66 | // Absolute path ("/x") โ use as-is; rootless path ("x") โ merge | |
| 67 | // with the base's directory. | |
| 68 | if ($rel->getPath()[0] === '/') { | |
| 69 | $targetPath = self::removeDotSegments($rel->getPath()); | |
| 70 | } else { | |
| 71 | $targetPath = self::removeDotSegments(self::mergePaths($base, $rel->getPath())); | |
| 72 | } | |
| 73 | ||
| 74 | return $result | |
| 75 | ->withPath($targetPath) | |
| 76 | ->withQuery($rel->getQuery()); | |
| 77 | } | |
| 78 | ||
| 79 | /** | |
| 80 | * ยง5.2.3: merge a rootless reference path with the base path. | |
| 81 | * | |
| 82 | * If the base has an authority and an empty path, the result is "/" + R.path. | |
| 83 | * Otherwise, take the base path up to and including its rightmost "/" | |
| 84 | * (i.e. strip the last segment) and append R.path. | |
| 85 | */ | |
| 86 | private static function mergePaths(UriInterface $base, string $relPath): string | |
| 87 | { | |
| 88 | $basePath = $base->getPath(); | |
| 89 | ||
| 90 | if ($base->getHost() !== '' && $basePath === '') { | |
| 91 | return '/' . $relPath; | |
| 92 | } | |
| 93 | ||
| 94 | $lastSlash = strrpos($basePath, '/'); | |
| 95 | if ($lastSlash === false) { | |
| 96 | return $relPath; | |
| 97 | } | |
| 98 | ||
| 99 | return substr($basePath, 0, $lastSlash + 1) . $relPath; | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * ยง5.2.4: remove dot segments ("." and "..") from a path. | |
| 104 | * | |
| 105 | * Implements the RFC 3986 algorithm directly. | |
| 106 | */ | |
| 107 | private static function removeDotSegments(string $path): string | |
| 108 | { | |
| 109 | $output = ''; | |
| 110 | ||
| 111 | while ($path !== '') { | |
| 112 | if (str_starts_with($path, '../')) { | |
| 113 | $path = substr($path, 3); | |
| 114 | } elseif (str_starts_with($path, './')) { | |
| 115 | $path = substr($path, 2); | |
| 116 | } elseif (str_starts_with($path, '/./')) { | |
| 117 | $path = '/' . substr($path, 3); | |
| 118 | } elseif ($path === '/.') { | |
| 119 | $path = '/'; | |
| 120 | } elseif (str_starts_with($path, '/../')) { | |
| 121 | $path = '/' . substr($path, 4); | |
| 122 | $output = self::removeLastSegment($output); | |
| 123 | } elseif ($path === '/..') { | |
| 124 | $path = '/'; | |
| 125 | $output = self::removeLastSegment($output); | |
| 126 | } elseif ($path === '.' || $path === '..') { | |
| 127 | $path = ''; | |
| 128 | } else { | |
| 129 | // Move the first path segment (including its leading "/" | |
| 130 | // if any) to the output buffer. | |
| 131 | $next = strpos($path, '/', 1); | |
| 132 | if ($next === false) { | |
| 133 | $segment = $path; | |
| 134 | $path = ''; | |
| 135 | } else { | |
| 136 | $segment = substr($path, 0, $next); | |
| 137 | $path = substr($path, $next); | |
| 138 | } | |
| 139 | $output .= $segment; | |
| 140 | } | |
| 141 | } | |
| 142 | ||
| 143 | return $output; | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Remove the last path segment and its preceding "/" from the output buffer. | |
| 148 | */ | |
| 149 | private static function removeLastSegment(string $output): string | |
| 150 | { | |
| 151 | $pos = strrpos($output, '/'); | |
| 152 | if ($pos === false) { | |
| 153 | return ''; | |
| 154 | } | |
| 155 | ||
| 156 | return substr($output, 0, $pos); | |
| 157 | } | |
| 158 | } |