| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,211 @@ |
| 1 |
+<?php |
|
| 2 |
+ |
|
| 3 |
+declare(strict_types=1); |
|
| 4 |
+ |
|
| 5 |
+namespace GuzzleHttp\Psr7; |
|
| 6 |
+ |
|
| 7 |
+use Psr\Http\Message\UriInterface; |
|
| 8 |
+ |
|
| 9 |
+/** |
|
| 10 |
+ * Resolves a URI reference in the context of a base URI and the opposite way. |
|
| 11 |
+ * |
|
| 12 |
+ * @author Tobias Schultze |
|
| 13 |
+ * |
|
| 14 |
+ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5 |
|
| 15 |
+ */ |
|
| 16 |
+final class UriResolver |
|
| 17 |
+{
|
|
| 18 |
+ /** |
|
| 19 |
+ * Removes dot segments from a path and returns the new path. |
|
| 20 |
+ * |
|
| 21 |
+ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 |
|
| 22 |
+ */ |
|
| 23 |
+ public static function removeDotSegments(string $path): string |
|
| 24 |
+ {
|
|
| 25 |
+ if ($path === '' || $path === '/') {
|
|
| 26 |
+ return $path; |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ $results = []; |
|
| 30 |
+ $segments = explode('/', $path);
|
|
| 31 |
+ foreach ($segments as $segment) {
|
|
| 32 |
+ if ($segment === '..') {
|
|
| 33 |
+ array_pop($results); |
|
| 34 |
+ } elseif ($segment !== '.') {
|
|
| 35 |
+ $results[] = $segment; |
|
| 36 |
+ } |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ $newPath = implode('/', $results);
|
|
| 40 |
+ |
|
| 41 |
+ if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
|
|
| 42 |
+ // Re-add the leading slash if necessary for cases like "/.." |
|
| 43 |
+ $newPath = '/'.$newPath; |
|
| 44 |
+ } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
|
|
| 45 |
+ // Add the trailing slash if necessary |
|
| 46 |
+ // If newPath is not empty, then $segment must be set and is the last segment from the foreach |
|
| 47 |
+ $newPath .= '/'; |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ return $newPath; |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ /** |
|
| 54 |
+ * Converts the relative URI into a new URI that is resolved against the base URI. |
|
| 55 |
+ * |
|
| 56 |
+ * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2 |
|
| 57 |
+ */ |
|
| 58 |
+ public static function resolve(UriInterface $base, UriInterface $rel): UriInterface |
|
| 59 |
+ {
|
|
| 60 |
+ if ((string) $rel === '') {
|
|
| 61 |
+ // we can simply return the same base URI instance for this same-document reference |
|
| 62 |
+ return $base; |
|
| 63 |
+ } |
|
| 64 |
+ |
|
| 65 |
+ if ($rel->getScheme() != '') {
|
|
| 66 |
+ return $rel->withPath(self::removeDotSegments($rel->getPath())); |
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ if ($rel->getAuthority() != '') {
|
|
| 70 |
+ $targetAuthority = $rel->getAuthority(); |
|
| 71 |
+ $targetPath = self::removeDotSegments($rel->getPath()); |
|
| 72 |
+ $targetQuery = $rel->getQuery(); |
|
| 73 |
+ } else {
|
|
| 74 |
+ $targetAuthority = $base->getAuthority(); |
|
| 75 |
+ if ($rel->getPath() === '') {
|
|
| 76 |
+ $targetPath = $base->getPath(); |
|
| 77 |
+ $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); |
|
| 78 |
+ } else {
|
|
| 79 |
+ if ($rel->getPath()[0] === '/') {
|
|
| 80 |
+ $targetPath = $rel->getPath(); |
|
| 81 |
+ } else {
|
|
| 82 |
+ if ($targetAuthority != '' && $base->getPath() === '') {
|
|
| 83 |
+ $targetPath = '/'.$rel->getPath(); |
|
| 84 |
+ } else {
|
|
| 85 |
+ $lastSlashPos = strrpos($base->getPath(), '/'); |
|
| 86 |
+ if ($lastSlashPos === false) {
|
|
| 87 |
+ $targetPath = $rel->getPath(); |
|
| 88 |
+ } else {
|
|
| 89 |
+ $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath(); |
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 92 |
+ } |
|
| 93 |
+ $targetPath = self::removeDotSegments($targetPath); |
|
| 94 |
+ $targetQuery = $rel->getQuery(); |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ return new Uri(Uri::composeComponents( |
|
| 99 |
+ $base->getScheme(), |
|
| 100 |
+ $targetAuthority, |
|
| 101 |
+ $targetPath, |
|
| 102 |
+ $targetQuery, |
|
| 103 |
+ $rel->getFragment() |
|
| 104 |
+ )); |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ /** |
|
| 108 |
+ * Returns the target URI as a relative reference from the base URI. |
|
| 109 |
+ * |
|
| 110 |
+ * This method is the counterpart to resolve(): |
|
| 111 |
+ * |
|
| 112 |
+ * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) |
|
| 113 |
+ * |
|
| 114 |
+ * One use-case is to use the current request URI as base URI and then generate relative links in your documents |
|
| 115 |
+ * to reduce the document size or offer self-contained downloadable document archives. |
|
| 116 |
+ * |
|
| 117 |
+ * $base = new Uri('http://example.com/a/b/');
|
|
| 118 |
+ * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
|
|
| 119 |
+ * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
|
|
| 120 |
+ * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
|
|
| 121 |
+ * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
|
|
| 122 |
+ * |
|
| 123 |
+ * This method also accepts a target that is already relative and will try to relativize it further. Only a |
|
| 124 |
+ * relative-path reference will be returned as-is. |
|
| 125 |
+ * |
|
| 126 |
+ * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
|
|
| 127 |
+ */ |
|
| 128 |
+ public static function relativize(UriInterface $base, UriInterface $target): UriInterface |
|
| 129 |
+ {
|
|
| 130 |
+ if ($target->getScheme() !== '' |
|
| 131 |
+ && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') |
|
| 132 |
+ ) {
|
|
| 133 |
+ return $target; |
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ if (Uri::isRelativePathReference($target)) {
|
|
| 137 |
+ // As the target is already highly relative we return it as-is. It would be possible to resolve |
|
| 138 |
+ // the target with `$target = self::resolve($base, $target);` and then try make it more relative |
|
| 139 |
+ // by removing a duplicate query. But let's not do that automatically. |
|
| 140 |
+ return $target; |
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+ if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
|
|
| 144 |
+ return $target->withScheme('');
|
|
| 145 |
+ } |
|
| 146 |
+ |
|
| 147 |
+ // We must remove the path before removing the authority because if the path starts with two slashes, the URI |
|
| 148 |
+ // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also |
|
| 149 |
+ // invalid. |
|
| 150 |
+ $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
|
|
| 151 |
+ |
|
| 152 |
+ if ($base->getPath() !== $target->getPath()) {
|
|
| 153 |
+ return $emptyPathUri->withPath(self::getRelativePath($base, $target)); |
|
| 154 |
+ } |
|
| 155 |
+ |
|
| 156 |
+ if ($base->getQuery() === $target->getQuery()) {
|
|
| 157 |
+ // Only the target fragment is left. And it must be returned even if base and target fragment are the same. |
|
| 158 |
+ return $emptyPathUri->withQuery('');
|
|
| 159 |
+ } |
|
| 160 |
+ |
|
| 161 |
+ // If the base URI has a query but the target has none, we cannot return an empty path reference as it would |
|
| 162 |
+ // inherit the base query component when resolving. |
|
| 163 |
+ if ($target->getQuery() === '') {
|
|
| 164 |
+ $segments = explode('/', $target->getPath());
|
|
| 165 |
+ /** @var string $lastSegment */ |
|
| 166 |
+ $lastSegment = end($segments); |
|
| 167 |
+ |
|
| 168 |
+ return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); |
|
| 169 |
+ } |
|
| 170 |
+ |
|
| 171 |
+ return $emptyPathUri; |
|
| 172 |
+ } |
|
| 173 |
+ |
|
| 174 |
+ private static function getRelativePath(UriInterface $base, UriInterface $target): string |
|
| 175 |
+ {
|
|
| 176 |
+ $sourceSegments = explode('/', $base->getPath());
|
|
| 177 |
+ $targetSegments = explode('/', $target->getPath());
|
|
| 178 |
+ array_pop($sourceSegments); |
|
| 179 |
+ $targetLastSegment = array_pop($targetSegments); |
|
| 180 |
+ foreach ($sourceSegments as $i => $segment) {
|
|
| 181 |
+ if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
|
|
| 182 |
+ unset($sourceSegments[$i], $targetSegments[$i]); |
|
| 183 |
+ } else {
|
|
| 184 |
+ break; |
|
| 185 |
+ } |
|
| 186 |
+ } |
|
| 187 |
+ $targetSegments[] = $targetLastSegment; |
|
| 188 |
+ $relativePath = str_repeat('../', count($sourceSegments)).implode('/', $targetSegments);
|
|
| 189 |
+ |
|
| 190 |
+ // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". |
|
| 191 |
+ // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used |
|
| 192 |
+ // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. |
|
| 193 |
+ if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
|
|
| 194 |
+ $relativePath = "./$relativePath"; |
|
| 195 |
+ } elseif ('/' === $relativePath[0]) {
|
|
| 196 |
+ if ($base->getAuthority() != '' && $base->getPath() === '') {
|
|
| 197 |
+ // In this case an extra slash is added by resolve() automatically. So we must not add one here. |
|
| 198 |
+ $relativePath = ".$relativePath"; |
|
| 199 |
+ } else {
|
|
| 200 |
+ $relativePath = "./$relativePath"; |
|
| 201 |
+ } |
|
| 202 |
+ } |
|
| 203 |
+ |
|
| 204 |
+ return $relativePath; |
|
| 205 |
+ } |
|
| 206 |
+ |
|
| 207 |
+ private function __construct() |
|
| 208 |
+ {
|
|
| 209 |
+ // cannot be instantiated |
|
| 210 |
+ } |
|
| 211 |
+} |
| 1 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,220 +0,0 @@ |
| 1 |
-<?php |
|
| 2 |
- |
|
| 3 |
-namespace GuzzleHttp\Psr7; |
|
| 4 |
- |
|
| 5 |
-use Psr\Http\Message\UriInterface; |
|
| 6 |
- |
|
| 7 |
-/** |
|
| 8 |
- * Resolves a URI reference in the context of a base URI and the opposite way. |
|
| 9 |
- * |
|
| 10 |
- * @author Tobias Schultze |
|
| 11 |
- * |
|
| 12 |
- * @link https://tools.ietf.org/html/rfc3986#section-5 |
|
| 13 |
- */ |
|
| 14 |
-final class UriResolver |
|
| 15 |
-{
|
|
| 16 |
- /** |
|
| 17 |
- * Removes dot segments from a path and returns the new path. |
|
| 18 |
- * |
|
| 19 |
- * @param string $path |
|
| 20 |
- * |
|
| 21 |
- * @return string |
|
| 22 |
- * @link http://tools.ietf.org/html/rfc3986#section-5.2.4 |
|
| 23 |
- */ |
|
| 24 |
- public static function removeDotSegments($path) |
|
| 25 |
- {
|
|
| 26 |
- if ($path === '' || $path === '/') {
|
|
| 27 |
- return $path; |
|
| 28 |
- } |
|
| 29 |
- |
|
| 30 |
- $results = []; |
|
| 31 |
- $segments = explode('/', $path);
|
|
| 32 |
- foreach ($segments as $segment) {
|
|
| 33 |
- if ($segment === '..') {
|
|
| 34 |
- array_pop($results); |
|
| 35 |
- } elseif ($segment !== '.') {
|
|
| 36 |
- $results[] = $segment; |
|
| 37 |
- } |
|
| 38 |
- } |
|
| 39 |
- |
|
| 40 |
- $newPath = implode('/', $results);
|
|
| 41 |
- |
|
| 42 |
- if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
|
|
| 43 |
- // Re-add the leading slash if necessary for cases like "/.." |
|
| 44 |
- $newPath = '/' . $newPath; |
|
| 45 |
- } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
|
|
| 46 |
- // Add the trailing slash if necessary |
|
| 47 |
- // If newPath is not empty, then $segment must be set and is the last segment from the foreach |
|
| 48 |
- $newPath .= '/'; |
|
| 49 |
- } |
|
| 50 |
- |
|
| 51 |
- return $newPath; |
|
| 52 |
- } |
|
| 53 |
- |
|
| 54 |
- /** |
|
| 55 |
- * Converts the relative URI into a new URI that is resolved against the base URI. |
|
| 56 |
- * |
|
| 57 |
- * @param UriInterface $base Base URI |
|
| 58 |
- * @param UriInterface $rel Relative URI |
|
| 59 |
- * |
|
| 60 |
- * @return UriInterface |
|
| 61 |
- * @link http://tools.ietf.org/html/rfc3986#section-5.2 |
|
| 62 |
- */ |
|
| 63 |
- public static function resolve(UriInterface $base, UriInterface $rel) |
|
| 64 |
- {
|
|
| 65 |
- if ((string) $rel === '') {
|
|
| 66 |
- // we can simply return the same base URI instance for this same-document reference |
|
| 67 |
- return $base; |
|
| 68 |
- } |
|
| 69 |
- |
|
| 70 |
- if ($rel->getScheme() != '') {
|
|
| 71 |
- return $rel->withPath(self::removeDotSegments($rel->getPath())); |
|
| 72 |
- } |
|
| 73 |
- |
|
| 74 |
- if ($rel->getAuthority() != '') {
|
|
| 75 |
- $targetAuthority = $rel->getAuthority(); |
|
| 76 |
- $targetPath = self::removeDotSegments($rel->getPath()); |
|
| 77 |
- $targetQuery = $rel->getQuery(); |
|
| 78 |
- } else {
|
|
| 79 |
- $targetAuthority = $base->getAuthority(); |
|
| 80 |
- if ($rel->getPath() === '') {
|
|
| 81 |
- $targetPath = $base->getPath(); |
|
| 82 |
- $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); |
|
| 83 |
- } else {
|
|
| 84 |
- if ($rel->getPath()[0] === '/') {
|
|
| 85 |
- $targetPath = $rel->getPath(); |
|
| 86 |
- } else {
|
|
| 87 |
- if ($targetAuthority != '' && $base->getPath() === '') {
|
|
| 88 |
- $targetPath = '/' . $rel->getPath(); |
|
| 89 |
- } else {
|
|
| 90 |
- $lastSlashPos = strrpos($base->getPath(), '/'); |
|
| 91 |
- if ($lastSlashPos === false) {
|
|
| 92 |
- $targetPath = $rel->getPath(); |
|
| 93 |
- } else {
|
|
| 94 |
- $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); |
|
| 95 |
- } |
|
| 96 |
- } |
|
| 97 |
- } |
|
| 98 |
- $targetPath = self::removeDotSegments($targetPath); |
|
| 99 |
- $targetQuery = $rel->getQuery(); |
|
| 100 |
- } |
|
| 101 |
- } |
|
| 102 |
- |
|
| 103 |
- return new Uri(Uri::composeComponents( |
|
| 104 |
- $base->getScheme(), |
|
| 105 |
- $targetAuthority, |
|
| 106 |
- $targetPath, |
|
| 107 |
- $targetQuery, |
|
| 108 |
- $rel->getFragment() |
|
| 109 |
- )); |
|
| 110 |
- } |
|
| 111 |
- |
|
| 112 |
- /** |
|
| 113 |
- * Returns the target URI as a relative reference from the base URI. |
|
| 114 |
- * |
|
| 115 |
- * This method is the counterpart to resolve(): |
|
| 116 |
- * |
|
| 117 |
- * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) |
|
| 118 |
- * |
|
| 119 |
- * One use-case is to use the current request URI as base URI and then generate relative links in your documents |
|
| 120 |
- * to reduce the document size or offer self-contained downloadable document archives. |
|
| 121 |
- * |
|
| 122 |
- * $base = new Uri('http://example.com/a/b/');
|
|
| 123 |
- * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
|
|
| 124 |
- * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
|
|
| 125 |
- * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
|
|
| 126 |
- * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
|
|
| 127 |
- * |
|
| 128 |
- * This method also accepts a target that is already relative and will try to relativize it further. Only a |
|
| 129 |
- * relative-path reference will be returned as-is. |
|
| 130 |
- * |
|
| 131 |
- * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
|
|
| 132 |
- * |
|
| 133 |
- * @param UriInterface $base Base URI |
|
| 134 |
- * @param UriInterface $target Target URI |
|
| 135 |
- * |
|
| 136 |
- * @return UriInterface The relative URI reference |
|
| 137 |
- */ |
|
| 138 |
- public static function relativize(UriInterface $base, UriInterface $target) |
|
| 139 |
- {
|
|
| 140 |
- if ($target->getScheme() !== '' && |
|
| 141 |
- ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') |
|
| 142 |
- ) {
|
|
| 143 |
- return $target; |
|
| 144 |
- } |
|
| 145 |
- |
|
| 146 |
- if (Uri::isRelativePathReference($target)) {
|
|
| 147 |
- // As the target is already highly relative we return it as-is. It would be possible to resolve |
|
| 148 |
- // the target with `$target = self::resolve($base, $target);` and then try make it more relative |
|
| 149 |
- // by removing a duplicate query. But let's not do that automatically. |
|
| 150 |
- return $target; |
|
| 151 |
- } |
|
| 152 |
- |
|
| 153 |
- if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
|
|
| 154 |
- return $target->withScheme('');
|
|
| 155 |
- } |
|
| 156 |
- |
|
| 157 |
- // We must remove the path before removing the authority because if the path starts with two slashes, the URI |
|
| 158 |
- // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also |
|
| 159 |
- // invalid. |
|
| 160 |
- $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
|
|
| 161 |
- |
|
| 162 |
- if ($base->getPath() !== $target->getPath()) {
|
|
| 163 |
- return $emptyPathUri->withPath(self::getRelativePath($base, $target)); |
|
| 164 |
- } |
|
| 165 |
- |
|
| 166 |
- if ($base->getQuery() === $target->getQuery()) {
|
|
| 167 |
- // Only the target fragment is left. And it must be returned even if base and target fragment are the same. |
|
| 168 |
- return $emptyPathUri->withQuery('');
|
|
| 169 |
- } |
|
| 170 |
- |
|
| 171 |
- // If the base URI has a query but the target has none, we cannot return an empty path reference as it would |
|
| 172 |
- // inherit the base query component when resolving. |
|
| 173 |
- if ($target->getQuery() === '') {
|
|
| 174 |
- $segments = explode('/', $target->getPath());
|
|
| 175 |
- $lastSegment = end($segments); |
|
| 176 |
- |
|
| 177 |
- return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); |
|
| 178 |
- } |
|
| 179 |
- |
|
| 180 |
- return $emptyPathUri; |
|
| 181 |
- } |
|
| 182 |
- |
|
| 183 |
- private static function getRelativePath(UriInterface $base, UriInterface $target) |
|
| 184 |
- {
|
|
| 185 |
- $sourceSegments = explode('/', $base->getPath());
|
|
| 186 |
- $targetSegments = explode('/', $target->getPath());
|
|
| 187 |
- array_pop($sourceSegments); |
|
| 188 |
- $targetLastSegment = array_pop($targetSegments); |
|
| 189 |
- foreach ($sourceSegments as $i => $segment) {
|
|
| 190 |
- if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
|
|
| 191 |
- unset($sourceSegments[$i], $targetSegments[$i]); |
|
| 192 |
- } else {
|
|
| 193 |
- break; |
|
| 194 |
- } |
|
| 195 |
- } |
|
| 196 |
- $targetSegments[] = $targetLastSegment; |
|
| 197 |
- $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments);
|
|
| 198 |
- |
|
| 199 |
- // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". |
|
| 200 |
- // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used |
|
| 201 |
- // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. |
|
| 202 |
- if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
|
|
| 203 |
- $relativePath = "./$relativePath"; |
|
| 204 |
- } elseif ('/' === $relativePath[0]) {
|
|
| 205 |
- if ($base->getAuthority() != '' && $base->getPath() === '') {
|
|
| 206 |
- // In this case an extra slash is added by resolve() automatically. So we must not add one here. |
|
| 207 |
- $relativePath = ".$relativePath"; |
|
| 208 |
- } else {
|
|
| 209 |
- $relativePath = "./$relativePath"; |
|
| 210 |
- } |
|
| 211 |
- } |
|
| 212 |
- |
|
| 213 |
- return $relativePath; |
|
| 214 |
- } |
|
| 215 |
- |
|
| 216 |
- private function __construct() |
|
| 217 |
- {
|
|
| 218 |
- // cannot be instantiated |
|
| 219 |
- } |
|
| 220 |
-} |
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,220 @@ |
| 1 |
+<?php |
|
| 2 |
+ |
|
| 3 |
+namespace GuzzleHttp\Psr7; |
|
| 4 |
+ |
|
| 5 |
+use Psr\Http\Message\UriInterface; |
|
| 6 |
+ |
|
| 7 |
+/** |
|
| 8 |
+ * Resolves a URI reference in the context of a base URI and the opposite way. |
|
| 9 |
+ * |
|
| 10 |
+ * @author Tobias Schultze |
|
| 11 |
+ * |
|
| 12 |
+ * @link https://tools.ietf.org/html/rfc3986#section-5 |
|
| 13 |
+ */ |
|
| 14 |
+final class UriResolver |
|
| 15 |
+{
|
|
| 16 |
+ /** |
|
| 17 |
+ * Removes dot segments from a path and returns the new path. |
|
| 18 |
+ * |
|
| 19 |
+ * @param string $path |
|
| 20 |
+ * |
|
| 21 |
+ * @return string |
|
| 22 |
+ * @link http://tools.ietf.org/html/rfc3986#section-5.2.4 |
|
| 23 |
+ */ |
|
| 24 |
+ public static function removeDotSegments($path) |
|
| 25 |
+ {
|
|
| 26 |
+ if ($path === '' || $path === '/') {
|
|
| 27 |
+ return $path; |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ $results = []; |
|
| 31 |
+ $segments = explode('/', $path);
|
|
| 32 |
+ foreach ($segments as $segment) {
|
|
| 33 |
+ if ($segment === '..') {
|
|
| 34 |
+ array_pop($results); |
|
| 35 |
+ } elseif ($segment !== '.') {
|
|
| 36 |
+ $results[] = $segment; |
|
| 37 |
+ } |
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ $newPath = implode('/', $results);
|
|
| 41 |
+ |
|
| 42 |
+ if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
|
|
| 43 |
+ // Re-add the leading slash if necessary for cases like "/.." |
|
| 44 |
+ $newPath = '/' . $newPath; |
|
| 45 |
+ } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
|
|
| 46 |
+ // Add the trailing slash if necessary |
|
| 47 |
+ // If newPath is not empty, then $segment must be set and is the last segment from the foreach |
|
| 48 |
+ $newPath .= '/'; |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ return $newPath; |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ /** |
|
| 55 |
+ * Converts the relative URI into a new URI that is resolved against the base URI. |
|
| 56 |
+ * |
|
| 57 |
+ * @param UriInterface $base Base URI |
|
| 58 |
+ * @param UriInterface $rel Relative URI |
|
| 59 |
+ * |
|
| 60 |
+ * @return UriInterface |
|
| 61 |
+ * @link http://tools.ietf.org/html/rfc3986#section-5.2 |
|
| 62 |
+ */ |
|
| 63 |
+ public static function resolve(UriInterface $base, UriInterface $rel) |
|
| 64 |
+ {
|
|
| 65 |
+ if ((string) $rel === '') {
|
|
| 66 |
+ // we can simply return the same base URI instance for this same-document reference |
|
| 67 |
+ return $base; |
|
| 68 |
+ } |
|
| 69 |
+ |
|
| 70 |
+ if ($rel->getScheme() != '') {
|
|
| 71 |
+ return $rel->withPath(self::removeDotSegments($rel->getPath())); |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ if ($rel->getAuthority() != '') {
|
|
| 75 |
+ $targetAuthority = $rel->getAuthority(); |
|
| 76 |
+ $targetPath = self::removeDotSegments($rel->getPath()); |
|
| 77 |
+ $targetQuery = $rel->getQuery(); |
|
| 78 |
+ } else {
|
|
| 79 |
+ $targetAuthority = $base->getAuthority(); |
|
| 80 |
+ if ($rel->getPath() === '') {
|
|
| 81 |
+ $targetPath = $base->getPath(); |
|
| 82 |
+ $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); |
|
| 83 |
+ } else {
|
|
| 84 |
+ if ($rel->getPath()[0] === '/') {
|
|
| 85 |
+ $targetPath = $rel->getPath(); |
|
| 86 |
+ } else {
|
|
| 87 |
+ if ($targetAuthority != '' && $base->getPath() === '') {
|
|
| 88 |
+ $targetPath = '/' . $rel->getPath(); |
|
| 89 |
+ } else {
|
|
| 90 |
+ $lastSlashPos = strrpos($base->getPath(), '/'); |
|
| 91 |
+ if ($lastSlashPos === false) {
|
|
| 92 |
+ $targetPath = $rel->getPath(); |
|
| 93 |
+ } else {
|
|
| 94 |
+ $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath(); |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ } |
|
| 98 |
+ $targetPath = self::removeDotSegments($targetPath); |
|
| 99 |
+ $targetQuery = $rel->getQuery(); |
|
| 100 |
+ } |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ return new Uri(Uri::composeComponents( |
|
| 104 |
+ $base->getScheme(), |
|
| 105 |
+ $targetAuthority, |
|
| 106 |
+ $targetPath, |
|
| 107 |
+ $targetQuery, |
|
| 108 |
+ $rel->getFragment() |
|
| 109 |
+ )); |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ /** |
|
| 113 |
+ * Returns the target URI as a relative reference from the base URI. |
|
| 114 |
+ * |
|
| 115 |
+ * This method is the counterpart to resolve(): |
|
| 116 |
+ * |
|
| 117 |
+ * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) |
|
| 118 |
+ * |
|
| 119 |
+ * One use-case is to use the current request URI as base URI and then generate relative links in your documents |
|
| 120 |
+ * to reduce the document size or offer self-contained downloadable document archives. |
|
| 121 |
+ * |
|
| 122 |
+ * $base = new Uri('http://example.com/a/b/');
|
|
| 123 |
+ * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
|
|
| 124 |
+ * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
|
|
| 125 |
+ * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
|
|
| 126 |
+ * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
|
|
| 127 |
+ * |
|
| 128 |
+ * This method also accepts a target that is already relative and will try to relativize it further. Only a |
|
| 129 |
+ * relative-path reference will be returned as-is. |
|
| 130 |
+ * |
|
| 131 |
+ * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
|
|
| 132 |
+ * |
|
| 133 |
+ * @param UriInterface $base Base URI |
|
| 134 |
+ * @param UriInterface $target Target URI |
|
| 135 |
+ * |
|
| 136 |
+ * @return UriInterface The relative URI reference |
|
| 137 |
+ */ |
|
| 138 |
+ public static function relativize(UriInterface $base, UriInterface $target) |
|
| 139 |
+ {
|
|
| 140 |
+ if ($target->getScheme() !== '' && |
|
| 141 |
+ ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') |
|
| 142 |
+ ) {
|
|
| 143 |
+ return $target; |
|
| 144 |
+ } |
|
| 145 |
+ |
|
| 146 |
+ if (Uri::isRelativePathReference($target)) {
|
|
| 147 |
+ // As the target is already highly relative we return it as-is. It would be possible to resolve |
|
| 148 |
+ // the target with `$target = self::resolve($base, $target);` and then try make it more relative |
|
| 149 |
+ // by removing a duplicate query. But let's not do that automatically. |
|
| 150 |
+ return $target; |
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 153 |
+ if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
|
|
| 154 |
+ return $target->withScheme('');
|
|
| 155 |
+ } |
|
| 156 |
+ |
|
| 157 |
+ // We must remove the path before removing the authority because if the path starts with two slashes, the URI |
|
| 158 |
+ // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also |
|
| 159 |
+ // invalid. |
|
| 160 |
+ $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
|
|
| 161 |
+ |
|
| 162 |
+ if ($base->getPath() !== $target->getPath()) {
|
|
| 163 |
+ return $emptyPathUri->withPath(self::getRelativePath($base, $target)); |
|
| 164 |
+ } |
|
| 165 |
+ |
|
| 166 |
+ if ($base->getQuery() === $target->getQuery()) {
|
|
| 167 |
+ // Only the target fragment is left. And it must be returned even if base and target fragment are the same. |
|
| 168 |
+ return $emptyPathUri->withQuery('');
|
|
| 169 |
+ } |
|
| 170 |
+ |
|
| 171 |
+ // If the base URI has a query but the target has none, we cannot return an empty path reference as it would |
|
| 172 |
+ // inherit the base query component when resolving. |
|
| 173 |
+ if ($target->getQuery() === '') {
|
|
| 174 |
+ $segments = explode('/', $target->getPath());
|
|
| 175 |
+ $lastSegment = end($segments); |
|
| 176 |
+ |
|
| 177 |
+ return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ return $emptyPathUri; |
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ private static function getRelativePath(UriInterface $base, UriInterface $target) |
|
| 184 |
+ {
|
|
| 185 |
+ $sourceSegments = explode('/', $base->getPath());
|
|
| 186 |
+ $targetSegments = explode('/', $target->getPath());
|
|
| 187 |
+ array_pop($sourceSegments); |
|
| 188 |
+ $targetLastSegment = array_pop($targetSegments); |
|
| 189 |
+ foreach ($sourceSegments as $i => $segment) {
|
|
| 190 |
+ if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
|
|
| 191 |
+ unset($sourceSegments[$i], $targetSegments[$i]); |
|
| 192 |
+ } else {
|
|
| 193 |
+ break; |
|
| 194 |
+ } |
|
| 195 |
+ } |
|
| 196 |
+ $targetSegments[] = $targetLastSegment; |
|
| 197 |
+ $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments);
|
|
| 198 |
+ |
|
| 199 |
+ // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". |
|
| 200 |
+ // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used |
|
| 201 |
+ // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. |
|
| 202 |
+ if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
|
|
| 203 |
+ $relativePath = "./$relativePath"; |
|
| 204 |
+ } elseif ('/' === $relativePath[0]) {
|
|
| 205 |
+ if ($base->getAuthority() != '' && $base->getPath() === '') {
|
|
| 206 |
+ // In this case an extra slash is added by resolve() automatically. So we must not add one here. |
|
| 207 |
+ $relativePath = ".$relativePath"; |
|
| 208 |
+ } else {
|
|
| 209 |
+ $relativePath = "./$relativePath"; |
|
| 210 |
+ } |
|
| 211 |
+ } |
|
| 212 |
+ |
|
| 213 |
+ return $relativePath; |
|
| 214 |
+ } |
|
| 215 |
+ |
|
| 216 |
+ private function __construct() |
|
| 217 |
+ {
|
|
| 218 |
+ // cannot be instantiated |
|
| 219 |
+ } |
|
| 220 |
+} |