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}
220