1<?php
2
3/**
4 * League.Uri (https://uri.thephpleague.com)
5 *
6 * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12declare(strict_types=1);
13
14namespace League\Uri;
15
16use League\Uri\Contracts\UriInterface;
17use Psr\Http\Message\UriInterface as Psr7UriInterface;
18use function array_pop;
19use function array_reduce;
20use function count;
21use function end;
22use function explode;
23use function gettype;
24use function implode;
25use function in_array;
26use function sprintf;
27use function str_repeat;
28use function strpos;
29use function substr;
30
31final class UriResolver
32{
33    /**
34     * @var array<string,int>
35     */
36    const DOT_SEGMENTS = ['.' => 1, '..' => 1];
37
38    /**
39     * @codeCoverageIgnore
40     */
41    private function __construct()
42    {
43    }
44
45    /**
46     * Resolve an URI against a base URI using RFC3986 rules.
47     *
48     * If the first argument is a UriInterface the method returns a UriInterface object
49     * If the first argument is a Psr7UriInterface the method returns a Psr7UriInterface object
50     *
51     * @param Psr7UriInterface|UriInterface $uri
52     * @param Psr7UriInterface|UriInterface $base_uri
53     *
54     * @return Psr7UriInterface|UriInterface
55     */
56    public static function resolve($uri, $base_uri)
57    {
58        self::filterUri($uri);
59        self::filterUri($base_uri);
60        $null = $uri instanceof Psr7UriInterface ? '' : null;
61
62        if ($null !== $uri->getScheme()) {
63            return $uri
64                ->withPath(self::removeDotSegments($uri->getPath()));
65        }
66
67        if ($null !== $uri->getAuthority()) {
68            return $uri
69                ->withScheme($base_uri->getScheme())
70                ->withPath(self::removeDotSegments($uri->getPath()));
71        }
72
73        $user = $null;
74        $pass = null;
75        $userInfo = $base_uri->getUserInfo();
76        if (null !== $userInfo) {
77            [$user, $pass] = explode(':', $userInfo, 2) + [1 => null];
78        }
79
80        [$uri_path, $uri_query] = self::resolvePathAndQuery($uri, $base_uri);
81
82        return $uri
83            ->withPath(self::removeDotSegments($uri_path))
84            ->withQuery($uri_query)
85            ->withHost($base_uri->getHost())
86            ->withPort($base_uri->getPort())
87            ->withUserInfo((string) $user, $pass)
88            ->withScheme($base_uri->getScheme())
89        ;
90    }
91
92    /**
93     * Filter the URI object.
94     *
95     * @param mixed $uri an URI object
96     *
97     * @throws \TypeError if the URI object does not implements the supported interfaces.
98     */
99    private static function filterUri($uri): void
100    {
101        if (!$uri instanceof UriInterface && !$uri instanceof Psr7UriInterface) {
102            throw new \TypeError(sprintf('The uri must be a valid URI object received `%s`', gettype($uri)));
103        }
104    }
105
106    /**
107     * Remove dot segments from the URI path.
108     */
109    private static function removeDotSegments(string $path): string
110    {
111        if (false === strpos($path, '.')) {
112            return $path;
113        }
114
115        $old_segments = explode('/', $path);
116        $new_path = implode('/', array_reduce($old_segments, [UriResolver::class, 'reducer'], []));
117        if (isset(self::DOT_SEGMENTS[end($old_segments)])) {
118            $new_path .= '/';
119        }
120
121        // @codeCoverageIgnoreStart
122        // added because some PSR-7 implementations do not respect RFC3986
123        if (0 === strpos($path, '/') && 0 !== strpos($new_path, '/')) {
124            return '/'.$new_path;
125        }
126        // @codeCoverageIgnoreEnd
127
128        return $new_path;
129    }
130
131    /**
132     * Remove dot segments.
133     *
134     * @return array<int, string>
135     */
136    private static function reducer(array $carry, string $segment): array
137    {
138        if ('..' === $segment) {
139            array_pop($carry);
140
141            return $carry;
142        }
143
144        if (!isset(self::DOT_SEGMENTS[$segment])) {
145            $carry[] = $segment;
146        }
147
148        return $carry;
149    }
150
151    /**
152     * Resolve an URI path and query component.
153     *
154     * @param Psr7UriInterface|UriInterface $uri
155     * @param Psr7UriInterface|UriInterface $base_uri
156     *
157     * @return array{0:string, 1:string|null}
158     */
159    private static function resolvePathAndQuery($uri, $base_uri): array
160    {
161        $target_path = $uri->getPath();
162        $target_query = $uri->getQuery();
163        $null = $uri instanceof Psr7UriInterface ? '' : null;
164        $baseNull = $base_uri instanceof Psr7UriInterface ? '' : null;
165
166        if (0 === strpos($target_path, '/')) {
167            return [$target_path, $target_query];
168        }
169
170        if ('' === $target_path) {
171            if ($null === $target_query) {
172                $target_query = $base_uri->getQuery();
173            }
174
175            $target_path = $base_uri->getPath();
176            //@codeCoverageIgnoreStart
177            //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction
178            if ($baseNull !== $base_uri->getAuthority() && 0 !== strpos($target_path, '/')) {
179                $target_path = '/'.$target_path;
180            }
181            //@codeCoverageIgnoreEnd
182
183            return [$target_path, $target_query];
184        }
185
186        $base_path = $base_uri->getPath();
187        if ($baseNull !== $base_uri->getAuthority() && '' === $base_path) {
188            $target_path = '/'.$target_path;
189        }
190
191        if ('' !== $base_path) {
192            $segments = explode('/', $base_path);
193            array_pop($segments);
194            if ([] !== $segments) {
195                $target_path = implode('/', $segments).'/'.$target_path;
196            }
197        }
198
199        return [$target_path, $target_query];
200    }
201
202    /**
203     * Relativize an URI according to a base URI.
204     *
205     * This method MUST retain the state of the submitted URI instance, and return
206     * an URI instance of the same type that contains the applied modifications.
207     *
208     * This method MUST be transparent when dealing with error and exceptions.
209     * It MUST not alter of silence them apart from validating its own parameters.
210     *
211     * @param Psr7UriInterface|UriInterface $uri
212     * @param Psr7UriInterface|UriInterface $base_uri
213     *
214     * @return Psr7UriInterface|UriInterface
215     */
216    public static function relativize($uri, $base_uri)
217    {
218        self::filterUri($uri);
219        self::filterUri($base_uri);
220        $uri = self::formatHost($uri);
221        $base_uri = self::formatHost($base_uri);
222        if (!self::isRelativizable($uri, $base_uri)) {
223            return $uri;
224        }
225
226        $null = $uri instanceof Psr7UriInterface ? '' : null;
227        $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null);
228        $target_path = $uri->getPath();
229        if ($target_path !== $base_uri->getPath()) {
230            return $uri->withPath(self::relativizePath($target_path, $base_uri->getPath()));
231        }
232
233        if (self::componentEquals('getQuery', $uri, $base_uri)) {
234            return $uri->withPath('')->withQuery($null);
235        }
236
237        if ($null === $uri->getQuery()) {
238            return $uri->withPath(self::formatPathWithEmptyBaseQuery($target_path));
239        }
240
241        return $uri->withPath('');
242    }
243
244    /**
245     * Tells whether the component value from both URI object equals.
246     *
247     * @param Psr7UriInterface|UriInterface $uri
248     * @param Psr7UriInterface|UriInterface $base_uri
249     */
250    private static function componentEquals(string $method, $uri, $base_uri): bool
251    {
252        return self::getComponent($method, $uri) === self::getComponent($method, $base_uri);
253    }
254
255    /**
256     * Returns the component value from the submitted URI object.
257     *
258     * @param Psr7UriInterface|UriInterface $uri
259     */
260    private static function getComponent(string $method, $uri): ?string
261    {
262        $component = $uri->$method();
263        if ($uri instanceof Psr7UriInterface && '' === $component) {
264            return null;
265        }
266
267        return $component;
268    }
269
270    /**
271     * Filter the URI object.
272     *
273     * @param null|mixed $uri
274     *
275     * @throws \TypeError if the URI object does not implements the supported interfaces.
276     *
277     * @return Psr7UriInterface|UriInterface
278     */
279    private static function formatHost($uri)
280    {
281        if (!$uri instanceof Psr7UriInterface) {
282            return $uri;
283        }
284
285        $host = $uri->getHost();
286        if ('' === $host) {
287            return $uri;
288        }
289
290        return $uri->withHost((string) Uri::createFromComponents(['host' => $host])->getHost());
291    }
292
293    /**
294     * Tell whether the submitted URI object can be relativize.
295     *
296     * @param Psr7UriInterface|UriInterface $uri
297     * @param Psr7UriInterface|UriInterface $base_uri
298     */
299    private static function isRelativizable($uri, $base_uri): bool
300    {
301        return !UriInfo::isRelativePath($uri)
302            && self::componentEquals('getScheme', $uri, $base_uri)
303            &&  self::componentEquals('getAuthority', $uri, $base_uri);
304    }
305
306    /**
307     * Relative the URI for a authority-less target URI.
308     */
309    private static function relativizePath(string $path, string $basepath): string
310    {
311        $base_segments = self::getSegments($basepath);
312        $target_segments = self::getSegments($path);
313        $target_basename = array_pop($target_segments);
314        array_pop($base_segments);
315        foreach ($base_segments as $offset => $segment) {
316            if (!isset($target_segments[$offset]) || $segment !== $target_segments[$offset]) {
317                break;
318            }
319            unset($base_segments[$offset], $target_segments[$offset]);
320        }
321        $target_segments[] = $target_basename;
322
323        return self::formatPath(
324            str_repeat('../', count($base_segments)).implode('/', $target_segments),
325            $basepath
326        );
327    }
328
329    /**
330     * returns the path segments.
331     *
332     * @return string[]
333     */
334    private static function getSegments(string $path): array
335    {
336        if ('' !== $path && '/' === $path[0]) {
337            $path = substr($path, 1);
338        }
339
340        return explode('/', $path);
341    }
342
343    /**
344     * Formatting the path to keep a valid URI.
345     */
346    private static function formatPath(string $path, string $basepath): string
347    {
348        if ('' === $path) {
349            return in_array($basepath, ['', '/'], true) ? $basepath : './';
350        }
351
352        if (false === ($colon_pos = strpos($path, ':'))) {
353            return $path;
354        }
355
356        $slash_pos = strpos($path, '/');
357        if (false === $slash_pos || $colon_pos < $slash_pos) {
358            return "./$path";
359        }
360
361        return $path;
362    }
363
364    /**
365     * Formatting the path to keep a resolvable URI.
366     */
367    private static function formatPathWithEmptyBaseQuery(string $path): string
368    {
369        $target_segments = self::getSegments($path);
370        /** @var string $basename */
371        $basename = end($target_segments);
372
373        return '' === $basename ? './' : $basename;
374    }
375}
376