| 1 | efrain | 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 |  *
 | 
        
           | 1441 | ariadna | 14 |  * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5
 | 
        
           | 1 | efrain | 15 |  */
 | 
        
           |  |  | 16 | final class UriResolver
 | 
        
           |  |  | 17 | {
 | 
        
           |  |  | 18 |     /**
 | 
        
           |  |  | 19 |      * Removes dot segments from a path and returns the new path.
 | 
        
           |  |  | 20 |      *
 | 
        
           | 1441 | ariadna | 21 |      * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
 | 
        
           | 1 | efrain | 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 "/.."
 | 
        
           | 1441 | ariadna | 43 |             $newPath = '/'.$newPath;
 | 
        
           | 1 | efrain | 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 |      *
 | 
        
           | 1441 | ariadna | 56 |      * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
 | 
        
           | 1 | efrain | 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() === '') {
 | 
        
           | 1441 | ariadna | 83 |                         $targetPath = '/'.$rel->getPath();
 | 
        
           | 1 | efrain | 84 |                     } else {
 | 
        
           |  |  | 85 |                         $lastSlashPos = strrpos($base->getPath(), '/');
 | 
        
           |  |  | 86 |                         if ($lastSlashPos === false) {
 | 
        
           |  |  | 87 |                             $targetPath = $rel->getPath();
 | 
        
           |  |  | 88 |                         } else {
 | 
        
           | 1441 | ariadna | 89 |                             $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
 | 
        
           | 1 | efrain | 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 |     {
 | 
        
           | 1441 | ariadna | 130 |         if ($target->getScheme() !== ''
 | 
        
           |  |  | 131 |             && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '')
 | 
        
           | 1 | efrain | 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;
 | 
        
           | 1441 | ariadna | 188 |         $relativePath = str_repeat('../', count($sourceSegments)).implode('/', $targetSegments);
 | 
        
           | 1 | efrain | 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 | }
 |