Completed
Pull Request — master (#178)
by ignace nyamagana
03:19
created

UriResolver::relativize()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 17
cts 17
cp 1
rs 8.8657
c 0
b 0
f 0
cc 6
nc 9
nop 2
crap 6
1
<?php
2
3
/**
4
 * League.Uri (https://uri.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Uri;
15
16
use League\Uri\Contracts\UriInterface;
17
use Psr\Http\Message\UriInterface as Psr7UriInterface;
18
use function array_pop;
19
use function array_reduce;
20
use function count;
21
use function end;
22
use function explode;
23
use function gettype;
24
use function implode;
25
use function in_array;
26
use function sprintf;
27
use function str_repeat;
28
use function strpos;
29
use function substr;
30
31
final 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 98
    public static function resolve($uri, $base_uri)
57
    {
58 98
        self::filterUri($uri);
59 96
        self::filterUri($base_uri);
60 96
        $null = $uri instanceof Psr7UriInterface ? '' : null;
61
62 96
        if ($null !== $uri->getScheme()) {
63
            return $uri
64 20
                ->withPath(self::removeDotSegments($uri->getPath()));
65
        }
66
67 76
        if ($null !== $uri->getAuthority()) {
68
            return $uri
69 4
                ->withScheme($base_uri->getScheme())
70 4
                ->withPath(self::removeDotSegments($uri->getPath()));
71
        }
72
73 72
        $user = $null;
74 72
        $pass = null;
75 72
        $userInfo = $base_uri->getUserInfo();
76 72
        if (null !== $userInfo) {
77 72
            [$user, $pass] = explode(':', $userInfo, 2) + [1 => null];
78
        }
79
80 72
        [$uri_path, $uri_query] = self::resolvePathAndQuery($uri, $base_uri);
0 ignored issues
show
Bug introduced by
The variable $uri_path does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $uri_query does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
81
82
        return $uri
83 72
            ->withPath(self::removeDotSegments($uri_path))
84 72
            ->withQuery($uri_query)
85 72
            ->withHost($base_uri->getHost())
86 72
            ->withPort($base_uri->getPort())
87 72
            ->withUserInfo((string) $user, $pass)
88 72
            ->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 200
    private static function filterUri($uri): void
100
    {
101 200
        if (!$uri instanceof UriInterface && !$uri instanceof Psr7UriInterface) {
102 4
            throw new \TypeError(sprintf('The uri must be a valid URI object received `%s`', gettype($uri)));
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with \sprintf('The uri must b... `%s`', \gettype($uri)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
103
        }
104 196
    }
105
106
    /**
107
     * Remove dot segments from the URI path.
108
     */
109 96
    private static function removeDotSegments(string $path): string
110
    {
111 96
        if (false === strpos($path, '.')) {
112 50
            return $path;
113
        }
114
115 48
        $old_segments = explode('/', $path);
116 48
        $new_path = implode('/', array_reduce($old_segments, [UriResolver::class, 'reducer'], []));
117 48
        if (isset(self::DOT_SEGMENTS[end($old_segments)])) {
118 8
            $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 42
        return $new_path;
129
    }
130
131
    /**
132
     * Remove dot segments.
133
     *
134
     * @return array<int, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
135
     */
136 48
    private static function reducer(array $carry, string $segment): array
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
137
    {
138 48
        if ('..' === $segment) {
139 26
            array_pop($carry);
140
141 26
            return $carry;
142
        }
143
144 48
        if (!isset(self::DOT_SEGMENTS[$segment])) {
145 48
            $carry[] = $segment;
146
        }
147
148 48
        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}
0 ignored issues
show
Documentation introduced by
The doc-type array{0:string, could not be parsed: Unknown type name "array{0:string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
158
     */
159 72
    private static function resolvePathAndQuery($uri, $base_uri): array
160
    {
161 72
        $target_path = $uri->getPath();
162 72
        $target_query = $uri->getQuery();
163 72
        $null = $uri instanceof Psr7UriInterface ? '' : null;
164 72
        $baseNull = $base_uri instanceof Psr7UriInterface ? '' : null;
165
166 72
        if (0 === strpos($target_path, '/')) {
167 6
            return [$target_path, $target_query];
168
        }
169
170 66
        if ('' === $target_path) {
171 6
            if ($null === $target_query) {
172 4
                $target_query = $base_uri->getQuery();
173
            }
174
175 6
            $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 6
            return [$target_path, $target_query];
184
        }
185
186 60
        $base_path = $base_uri->getPath();
187 60
        if ($baseNull !== $base_uri->getAuthority() && '' === $base_path) {
188 2
            $target_path = '/'.$target_path;
189
        }
190
191 60
        if ('' !== $base_path) {
192 58
            $segments = explode('/', $base_path);
193 58
            array_pop($segments);
194 58
            if ([] !== $segments) {
195 58
                $target_path = implode('/', $segments).'/'.$target_path;
196
            }
197
        }
198
199 60
        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 102
    public static function relativize($uri, $base_uri)
217
    {
218 102
        self::filterUri($uri);
219 100
        self::filterUri($base_uri);
220 100
        $uri = self::formatHost($uri);
221 100
        $base_uri = self::formatHost($base_uri);
222 100
        if (!self::isRelativizable($uri, $base_uri)) {
223 44
            return $uri;
224
        }
225
226 56
        $null = $uri instanceof Psr7UriInterface ? '' : null;
227 56
        $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null);
228 56
        $target_path = $uri->getPath();
229 56
        if ($target_path !== $base_uri->getPath()) {
230 40
            return $uri->withPath(self::relativizePath($target_path, $base_uri->getPath()));
231
        }
232
233 16
        if (self::componentEquals('getQuery', $uri, $base_uri)) {
234 8
            return $uri->withPath('')->withQuery($null);
235
        }
236
237 8
        if ($null === $uri->getQuery()) {
238 4
            return $uri->withPath(self::formatPathWithEmptyBaseQuery($target_path));
239
        }
240
241 4
        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 62
    private static function componentEquals(string $method, $uri, $base_uri): bool
251
    {
252 62
        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 62
    private static function getComponent(string $method, $uri): ?string
261
    {
262 62
        $component = $uri->$method();
263 62
        if ($uri instanceof Psr7UriInterface && '' === $component) {
264 8
            return null;
265
        }
266
267 62
        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 100
    private static function formatHost($uri)
280
    {
281 100
        if (!$uri instanceof Psr7UriInterface) {
282 100
            return $uri;
283
        }
284
285 100
        $host = $uri->getHost();
286 100
        if ('' === $host) {
287 34
            return $uri;
288
        }
289
290 66
        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 100
    private static function isRelativizable($uri, $base_uri): bool
300
    {
301 100
        return !UriInfo::isRelativePath($uri)
302 100
            && self::componentEquals('getScheme', $uri, $base_uri)
303 100
            &&  self::componentEquals('getAuthority', $uri, $base_uri);
304
    }
305
306
    /**
307
     * Relative the URI for a authority-less target URI.
308
     */
309 40
    private static function relativizePath(string $path, string $basepath): string
310
    {
311 40
        $base_segments = self::getSegments($basepath);
312 40
        $target_segments = self::getSegments($path);
313 40
        $target_basename = array_pop($target_segments);
314 40
        array_pop($base_segments);
315 40
        foreach ($base_segments as $offset => $segment) {
316 32
            if (!isset($target_segments[$offset]) || $segment !== $target_segments[$offset]) {
317 16
                break;
318
            }
319 22
            unset($base_segments[$offset], $target_segments[$offset]);
320
        }
321 40
        $target_segments[] = $target_basename;
322
323 40
        return self::formatPath(
324 40
            str_repeat('../', count($base_segments)).implode('/', $target_segments),
325 20
            $basepath
326
        );
327
    }
328
329
    /**
330
     * returns the path segments.
331
     *
332
     * @return string[]
333
     */
334 44
    private static function getSegments(string $path): array
335
    {
336 44
        if ('' !== $path && '/' === $path[0]) {
337 44
            $path = substr($path, 1);
338
        }
339
340 44
        return explode('/', $path);
341
    }
342
343
    /**
344
     * Formatting the path to keep a valid URI.
345
     */
346 40
    private static function formatPath(string $path, string $basepath): string
347
    {
348 40
        if ('' === $path) {
349 6
            return in_array($basepath, ['', '/'], true) ? $basepath : './';
350
        }
351
352 34
        if (false === ($colon_pos = strpos($path, ':'))) {
353 28
            return $path;
354
        }
355
356 6
        $slash_pos = strpos($path, '/');
357 6
        if (false === $slash_pos || $colon_pos < $slash_pos) {
358 4
            return "./$path";
359
        }
360
361 2
        return $path;
362
    }
363
364
    /**
365
     * Formatting the path to keep a resolvable URI.
366
     */
367 4
    private static function formatPathWithEmptyBaseQuery(string $path): string
368
    {
369 4
        $target_segments = self::getSegments($path);
370
        /** @var string $basename */
371 4
        $basename = end($target_segments);
372
373 4
        return '' === $basename ? './' : $basename;
374
    }
375
}
376