krz/vox-populi

A Tumblr web client.

clone: git clone https://gitbay.org/krz/vox-populi.git

main: vendor/guzzlehttp/psr7/src/UriResolver.php · raw

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