Completed
Pull Request — master (#127)
by ignace nyamagana
02:03
created

UriResolver::formatHost()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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