File indexing completed on 2024-12-29 05:27:24

0001 <?php
0002 namespace GuzzleHttp\Psr7;
0003 
0004 use Psr\Http\Message\UriInterface;
0005 
0006 /**
0007  * Resolves a URI reference in the context of a base URI and the opposite way.
0008  *
0009  * @author Tobias Schultze
0010  *
0011  * @link https://tools.ietf.org/html/rfc3986#section-5
0012  */
0013 final class UriResolver
0014 {
0015     /**
0016      * Removes dot segments from a path and returns the new path.
0017      *
0018      * @param string $path
0019      *
0020      * @return string
0021      * @link http://tools.ietf.org/html/rfc3986#section-5.2.4
0022      */
0023     public static function removeDotSegments($path)
0024     {
0025         if ($path === '' || $path === '/') {
0026             return $path;
0027         }
0028 
0029         $results = [];
0030         $segments = explode('/', $path);
0031         foreach ($segments as $segment) {
0032             if ($segment === '..') {
0033                 array_pop($results);
0034             } elseif ($segment !== '.') {
0035                 $results[] = $segment;
0036             }
0037         }
0038 
0039         $newPath = implode('/', $results);
0040 
0041         if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
0042             // Re-add the leading slash if necessary for cases like "/.."
0043             $newPath = '/' . $newPath;
0044         } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
0045             // Add the trailing slash if necessary
0046             // If newPath is not empty, then $segment must be set and is the last segment from the foreach
0047             $newPath .= '/';
0048         }
0049 
0050         return $newPath;
0051     }
0052 
0053     /**
0054      * Converts the relative URI into a new URI that is resolved against the base URI.
0055      *
0056      * @param UriInterface $base Base URI
0057      * @param UriInterface $rel  Relative URI
0058      *
0059      * @return UriInterface
0060      * @link http://tools.ietf.org/html/rfc3986#section-5.2
0061      */
0062     public static function resolve(UriInterface $base, UriInterface $rel)
0063     {
0064         if ((string) $rel === '') {
0065             // we can simply return the same base URI instance for this same-document reference
0066             return $base;
0067         }
0068 
0069         if ($rel->getScheme() != '') {
0070             return $rel->withPath(self::removeDotSegments($rel->getPath()));
0071         }
0072 
0073         if ($rel->getAuthority() != '') {
0074             $targetAuthority = $rel->getAuthority();
0075             $targetPath = self::removeDotSegments($rel->getPath());
0076             $targetQuery = $rel->getQuery();
0077         } else {
0078             $targetAuthority = $base->getAuthority();
0079             if ($rel->getPath() === '') {
0080                 $targetPath = $base->getPath();
0081                 $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
0082             } else {
0083                 if ($rel->getPath()[0] === '/') {
0084                     $targetPath = $rel->getPath();
0085                 } else {
0086                     if ($targetAuthority != '' && $base->getPath() === '') {
0087                         $targetPath = '/' . $rel->getPath();
0088                     } else {
0089                         $lastSlashPos = strrpos($base->getPath(), '/');
0090                         if ($lastSlashPos === false) {
0091                             $targetPath = $rel->getPath();
0092                         } else {
0093                             $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath();
0094                         }
0095                     }
0096                 }
0097                 $targetPath = self::removeDotSegments($targetPath);
0098                 $targetQuery = $rel->getQuery();
0099             }
0100         }
0101 
0102         return new Uri(Uri::composeComponents(
0103             $base->getScheme(),
0104             $targetAuthority,
0105             $targetPath,
0106             $targetQuery,
0107             $rel->getFragment()
0108         ));
0109     }
0110 
0111     /**
0112      * Returns the target URI as a relative reference from the base URI.
0113      *
0114      * This method is the counterpart to resolve():
0115      *
0116      *    (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
0117      *
0118      * One use-case is to use the current request URI as base URI and then generate relative links in your documents
0119      * to reduce the document size or offer self-contained downloadable document archives.
0120      *
0121      *    $base = new Uri('http://example.com/a/b/');
0122      *    echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c'));  // prints 'c'.
0123      *    echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y'));  // prints '../x/y'.
0124      *    echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
0125      *    echo UriResolver::relativize($base, new Uri('http://example.org/a/b/'));   // prints '//example.org/a/b/'.
0126      *
0127      * This method also accepts a target that is already relative and will try to relativize it further. Only a
0128      * relative-path reference will be returned as-is.
0129      *
0130      *    echo UriResolver::relativize($base, new Uri('/a/b/c'));  // prints 'c' as well
0131      *
0132      * @param UriInterface $base   Base URI
0133      * @param UriInterface $target Target URI
0134      *
0135      * @return UriInterface The relative URI reference
0136      */
0137     public static function relativize(UriInterface $base, UriInterface $target)
0138     {
0139         if ($target->getScheme() !== '' &&
0140             ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '')
0141         ) {
0142             return $target;
0143         }
0144 
0145         if (Uri::isRelativePathReference($target)) {
0146             // As the target is already highly relative we return it as-is. It would be possible to resolve
0147             // the target with `$target = self::resolve($base, $target);` and then try make it more relative
0148             // by removing a duplicate query. But let's not do that automatically.
0149             return $target;
0150         }
0151 
0152         if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
0153             return $target->withScheme('');
0154         }
0155 
0156         // We must remove the path before removing the authority because if the path starts with two slashes, the URI
0157         // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also
0158         // invalid.
0159         $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
0160 
0161         if ($base->getPath() !== $target->getPath()) {
0162             return $emptyPathUri->withPath(self::getRelativePath($base, $target));
0163         }
0164 
0165         if ($base->getQuery() === $target->getQuery()) {
0166             // Only the target fragment is left. And it must be returned even if base and target fragment are the same.
0167             return $emptyPathUri->withQuery('');
0168         }
0169 
0170         // If the base URI has a query but the target has none, we cannot return an empty path reference as it would
0171         // inherit the base query component when resolving.
0172         if ($target->getQuery() === '') {
0173             $segments = explode('/', $target->getPath());
0174             $lastSegment = end($segments);
0175 
0176             return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment);
0177         }
0178 
0179         return $emptyPathUri;
0180     }
0181 
0182     private static function getRelativePath(UriInterface $base, UriInterface $target)
0183     {
0184         $sourceSegments = explode('/', $base->getPath());
0185         $targetSegments = explode('/', $target->getPath());
0186         array_pop($sourceSegments);
0187         $targetLastSegment = array_pop($targetSegments);
0188         foreach ($sourceSegments as $i => $segment) {
0189             if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
0190                 unset($sourceSegments[$i], $targetSegments[$i]);
0191             } else {
0192                 break;
0193             }
0194         }
0195         $targetSegments[] = $targetLastSegment;
0196         $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments);
0197 
0198         // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./".
0199         // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
0200         // as the first segment of a relative-path reference, as it would be mistaken for a scheme name.
0201         if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
0202             $relativePath = "./$relativePath";
0203         } elseif ('/' === $relativePath[0]) {
0204             if ($base->getAuthority() != '' && $base->getPath() === '') {
0205                 // In this case an extra slash is added by resolve() automatically. So we must not add one here.
0206                 $relativePath = ".$relativePath";
0207             } else {
0208                 $relativePath = "./$relativePath";
0209             }
0210         }
0211 
0212         return $relativePath;
0213     }
0214 
0215     private function __construct()
0216     {
0217         // cannot be instantiated
0218     }
0219 }