Completed
Pull Request — master (#132)
by Paweł
62:51
created

Uri::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 2
b 0
f 0
nc 1
nop 8
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 2
rs 9.9666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 finfo;
17
use League\Uri\Contracts\UriInterface;
18
use League\Uri\Exceptions\SyntaxError;
19
use Psr\Http\Message\UriInterface as Psr7UriInterface;
20
use TypeError;
21
use function array_filter;
22
use function array_map;
23
use function base64_decode;
24
use function base64_encode;
25
use function count;
26
use function explode;
27
use function file_get_contents;
28
use function filter_var;
29
use function implode;
30
use function in_array;
31
use function inet_pton;
32
use function is_scalar;
33
use function mb_detect_encoding;
34
use function method_exists;
35
use function preg_match;
36
use function preg_replace;
37
use function preg_replace_callback;
38
use function rawurlencode;
39
use function sprintf;
40
use function str_replace;
41
use function strlen;
42
use function strpos;
43
use function strtolower;
44
use function substr;
45
use const FILEINFO_MIME;
46
use const FILTER_FLAG_IPV4;
47
use const FILTER_FLAG_IPV6;
48
use const FILTER_NULL_ON_FAILURE;
49
use const FILTER_VALIDATE_BOOLEAN;
50
use const FILTER_VALIDATE_IP;
51
52
/**
53
 * Class Uri.
54
 *
55
 * @package League\Uri
56
 */
57
final class Uri implements UriInterface
58
{
59
    /**
60
     * RFC3986 Sub delimiter characters regular expression pattern.
61
     *
62
     * @see https://tools.ietf.org/html/rfc3986#section-2.2
63
     *
64
     * @var string
65
     */
66
    private const REGEXP_CHARS_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
67
68
    /**
69
     * RFC3986 unreserved characters regular expression pattern.
70
     *
71
     * @see https://tools.ietf.org/html/rfc3986#section-2.3
72
     *
73
     * @var string
74
     */
75
    private const REGEXP_CHARS_UNRESERVED = 'A-Za-z0-9_\-\.~';
76
77
    /**
78
     * RFC3986 schema regular expression pattern.
79
     *
80
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
81
     *
82
     * @var string
83
     */
84
    private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i';
85
86
    /**
87
     * Regular expression pattern to for file URI.
88
     *
89
     * @var string
90
     */
91
    private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<root>[a-zA-Z][:|\|])(?<rest>.*)?,';
92
93
    /**
94
     * Mimetype retular expression pattern.
95
     *
96
     * @see https://tools.ietf.org/html/rfc2397
97
     *
98
     * @var string
99
     */
100
    private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
101
102
    /**
103
     * Base64 content regular expression pattern.
104
     *
105
     * @see https://tools.ietf.org/html/rfc2397
106
     *
107
     * @var string
108
     */
109
    private const REGEXP_BINARY = ',(;|^)base64$,';
110
111
    /**
112
     * Windows path string regular expression pattern.
113
     */
114
    private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),';
115
116
    /**
117
     * Supported schemes and corresponding default port.
118
     *
119
     * @var array
120
     */
121
    private const SCHEME_DEFAULT_PORT = [
122
        'data' => null,
123
        'file' => null,
124
        'ftp' => 21,
125
        'gopher' => 70,
126
        'http' => 80,
127
        'https' => 443,
128
        'ws' => 80,
129
        'wss' => 443,
130
    ];
131
132
    /**
133
     * URI validation methods per scheme.
134
     *
135
     * @var array
136
     */
137
    private const SCHEME_VALIDATION_METHOD = [
138
        'data' => 'isUriWithSchemeAndPathOnly',
139
        'file' => 'isUriWithSchemeHostAndPathOnly',
140
        'ftp' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
141
        'gopher' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
142
        'http' => 'isNonEmptyHostUri',
143
        'https' => 'isNonEmptyHostUri',
144
        'ws' => 'isNonEmptyHostUriWithoutFragment',
145
        'wss' => 'isNonEmptyHostUriWithoutFragment',
146
    ];
147
148
    /**
149
     * URI scheme component.
150
     *
151
     * @var string|null
152
     */
153
    private $scheme;
154
155
    /**
156
     * URI user info part.
157
     *
158
     * @var string|null
159
     */
160
    private $user_info;
161
162
    /**
163
     * URI host component.
164
     *
165
     * @var string|null
166
     */
167
    private $host;
168
169
    /**
170
     * URI port component.
171
     *
172
     * @var int|null
173
     */
174
    private $port;
175
176
    /**
177
     * URI authority string representation.
178
     *
179
     * @var string|null
180
     */
181
    private $authority;
182
183
    /**
184
     * URI path component.
185
     *
186
     * @var string
187
     */
188
    private $path = '';
189
190
    /**
191
     * URI query component.
192
     *
193
     * @var string|null
194
     */
195
    private $query;
196
197
    /**
198
     * URI fragment component.
199
     *
200
     * @var string|null
201
     */
202
    private $fragment;
203
204
    /**
205
     * URI string representation.
206
     *
207
     * @var string|null
208
     */
209
    private $uri;
210
211
    /**
212
     * Create a new instance.
213
     *
214
     * @param ?string $scheme
215
     * @param ?string $user
216
     * @param ?string $pass
217
     * @param ?string $host
218
     * @param ?int    $port
219
     * @param ?string $query
220
     * @param ?string $fragment
221
     */
222
    private function __construct(?string $scheme, ?string $user, ?string $pass, ?string $host, ?int $port, string $path, ?string $query, ?string $fragment)
223
    {
224
        $this->scheme = $this->formatScheme($scheme);
225
        $this->user_info = $this->formatUserInfo($user, $pass);
226
        $this->host = $this->formatHost($host);
227
        $this->port = $this->formatPort($port);
228
        $this->authority = $this->setAuthority();
229
        $this->path = $this->formatPath($path);
230
        $this->query = $this->formatQueryAndFragment($query);
231
        $this->fragment = $this->formatQueryAndFragment($fragment);
232
        $this->assertValidState();
233
    }
234
235
    /**
236
     * Format the Scheme and Host component.
237
     *
238
     * @param ?string $scheme
239
     *
240
     * @throws SyntaxError if the scheme is invalid
241
     */
242
    private function formatScheme(?string $scheme): ?string
243
    {
244
        if ('' === $scheme || null === $scheme) {
245
            return $scheme;
246
        }
247
248
        $formatted_scheme = strtolower($scheme);
249
250
        if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) {
251
            return $formatted_scheme;
252
        }
253
254
        throw new SyntaxError(sprintf('The scheme `%s` is invalid', $scheme));
255
    }
256
257 302
    /**
258
     * Set the UserInfo component.
259
     *
260
     * @param ?string $user
261
     * @param ?string $password
262
     */
263
    private function formatUserInfo(?string $user, ?string $password): ?string
264
    {
265
        if (null === $user) {
266
            return $user;
267 302
        }
268 302
269 302
        static $user_pattern = '/(?:[^%'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
270 302
        $user = preg_replace_callback($user_pattern, [Uri::class, 'urlEncodeMatch'], $user);
271 302
272 302
        if (null === $password) {
273 302
            return $user;
274 302
        }
275 302
276 288
        static $password_pattern = '/(?:[^%:'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
277
278
        return $user.':'.preg_replace_callback($password_pattern, [Uri::class, 'urlEncodeMatch'], $password);
279
    }
280
281
    /**
282
     * Returns the RFC3986 encoded string matched.
283
     */
284
    private static function urlEncodeMatch(array $matches): string
285 308
    {
286
        return rawurlencode($matches[0]);
287 308
    }
288 234
289
    /**
290
     * Validate and Format the Host component.
291 240
     *
292 240
     * @param ?string $host
293 240
     */
294
    private function formatHost(?string $host): ?string
295
    {
296 2
        if (null === $host || '' === $host) {
297
            return $host;
298
        }
299
300
        if ('[' !== $host[0]) {
301
            return Common::filterRegisteredName($host);
302
        }
303
304
        return $this->formatIp($host);
305 308
    }
306
307 308
    /**
308 282
     * Validate and Format the IPv6/IPvfuture host.
309
     *
310
     * @throws SyntaxError if the submitted host is not a valid IP host
311 56
     */
312 56
    private function formatIp(string $host): string
313 56
    {
314 6
        $ip = substr($host, 1, -1);
315
316
        if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
317 56
            return $host;
318
        }
319 56
320
        if (1 === preg_match(Common::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
321
            return $host;
322
        }
323
324
        $pos = strpos($ip, '%');
325 10
326
        if (false === $pos) {
327 10
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
328
        }
329
330
        if (1 === preg_match(Common::REGEXP_INVALID_HOST_CHARS, rawurldecode(substr($ip, $pos)))) {
331
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
332
        }
333
334
        $ip = substr($ip, 0, $pos);
335 334
336
        if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
337 334
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
338 234
        }
339
340
        //Only the address block fe80::/10 can have a Zone ID attach to
341 268
        //let's detect the link local significant 10 bits
342 268
        if (0 === strpos((string) inet_pton($ip), Common::ZONE_ID_ADDRESS_BLOCK)) {
343
            return $host;
344
        }
345 2
346
        throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
347
    }
348
349
    /**
350
     * Format the Port component.
351
     *
352
     * @param null|mixed $port
353
     *
354
     * @throws SyntaxError
355
     */
356 266
    private function formatPort($port = null): ?int
357
    {
358
        if (null === $port || '' === $port) {
359
            return null;
360
        }
361
362
        if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) {
363
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
364
        }
365 266
366 266
        $port = (int) $port;
367 258
368 258
        if (0 > $port) {
369 254
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
370
        }
371
372
        $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null;
373
374
        if ($defaultPort === $port) {
375
            return null;
376
        }
377
378 8
        return $port;
379 8
    }
380 8
381 8
    /**
382 4
     * {@inheritdoc}
383
     */
384
    public static function __set_state(array $components): self
385 8
    {
386 2
        $components['user'] = null;
387
        $components['pass'] = null;
388
389
        if (null !== $components['user_info']) {
390
            [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null];
391
        }
392
393
        return new self(
394
            $components['scheme'],
395 6
            $components['user'],
396
            $components['pass'],
397
            $components['host'],
398 14
            $components['port'],
399 2
            $components['path'],
400
            $components['query'],
401
            $components['fragment']
402
        );
403
    }
404
405
    /**
406
     * Create a new instance from a URI and a Base URI.
407
     *
408 12
     * The returned URI must be absolute.
409 12
     *
410 12
     * @param null|mixed $base_uri
411 12
     */
412 12
    public static function createFromBaseUri($uri, $base_uri = null): UriInterface
413
    {
414 12
        if (!$uri instanceof UriInterface) {
415 2
            $uri = self::createFromString($uri);
416
        }
417
418
        if (null === $base_uri) {
419
            if (null === $uri->getScheme()) {
420
                throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri));
421
            }
422
423
            if (null === $uri->getAuthority()) {
424 10
                return $uri;
425
            }
426
427
            /** @var UriInterface $uri */
428
            $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath(''));
429
430
            return $uri;
431
        }
432 4
433
        if (!$base_uri instanceof UriInterface) {
434 4
            $base_uri = self::createFromString($base_uri);
435 4
        }
436 4
437 4
        if (null === $base_uri->getScheme()) {
438
            throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri));
439
        }
440
441 4
        /** @var UriInterface $uri */
442
        $uri = UriResolver::resolve($uri, $base_uri);
443
444
        return $uri;
445
    }
446
447
    /**
448
     * Create a new instance from a string.
449 16
     *
450
     * @param string|mixed $uri
451 16
     */
452 16
    public static function createFromString($uri = ''): self
453 2
    {
454
        $components = UriString::parse($uri);
455
456 14
        return new self(
457 2
            $components['scheme'],
458
            $components['user'],
459
            $components['pass'],
460 12
            $components['host'],
461 12
            $components['port'],
462 4
            $components['path'],
463
            $components['query'],
464
            $components['fragment']
465 8
        );
466 2
    }
467
468
    /**
469 6
     * Create a new instance from a hash of parse_url parts.
470 6
     *
471 2
     * Create an new instance from a hash representation of the URI similar
472
     * to PHP parse_url function result
473
     */
474
    public static function createFromComponents(array $components = []): self
475
    {
476 4
        $components += [
477 2
            'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
478
            'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
479
        ];
480 2
481
        return new self(
482
            $components['scheme'],
483
            $components['user'],
484
            $components['pass'],
485
            $components['host'],
486
            $components['port'],
487
            $components['path'],
488 330
            $components['query'],
489
            $components['fragment']
490 330
        );
491 278
    }
492
493
    /**
494 94
     * Create a new instance from a data file path.
495 2
     *
496
     * @param null|mixed $context
497
     *
498 94
     * @throws SyntaxError If the file does not exist or is not readable
499 94
     */
500 2
    public static function createFromDataPath(string $path, $context = null): self
501
    {
502
        $file_args = [$path, false];
503 94
        $mime_args = [$path, FILEINFO_MIME];
504 94
505 14
        if (null !== $context) {
506
            $file_args[] = $context;
507
            $mime_args[] = $context;
508 86
        }
509
510
        $raw = @file_get_contents(...$file_args);
0 ignored issues
show
Bug introduced by
$file_args is expanded, but the parameter $filename of file_get_contents() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

510
        $raw = @file_get_contents(/** @scrutinizer ignore-type */ ...$file_args);
Loading history...
511
512
        if (false === $raw) {
513
            throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
514 18
        }
515
516 18
        $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mime_args);
0 ignored issues
show
Bug introduced by
$mime_args is expanded, but the parameter $file_name of finfo::file() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

516
        $mimetype = (string) (new finfo(FILEINFO_MIME))->file(/** @scrutinizer ignore-type */ ...$mime_args);
Loading history...
517 18
518 18
        return Uri::createFromComponents([
519 14
            'scheme' => 'data',
520
            'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)),
521
        ]);
522 18
    }
523 18
524 18
    /**
525 18
     * Create a new instance from a Unix path string.
526 18
     */
527 18
    public static function createFromUnixPath(string $uri = ''): self
528 18
    {
529 18
        $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
530 18
        if ('/' !== ($uri[0] ?? '')) {
531
            return Uri::createFromComponents(['path' => $uri]);
532
        }
533
534
        return Uri::createFromComponents(['path' => $uri, 'scheme' => 'file', 'host' => '']);
535
    }
536
537
    /**
538
     * Create a new instance from a local Windows path string.
539
     */
540
    public static function createFromWindowsPath(string $uri = ''): self
541
    {
542 86
        $root = '';
543
544 86
        if (1 === preg_match(self::REGEXP_WINDOW_PATH, $uri, $matches)) {
545 86
            $root = substr($matches['root'], 0, -1).':';
546
            $uri = substr($uri, strlen($root));
547
        }
548 86
        $uri = str_replace('\\', '/', $uri);
549 6
        $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
550 2
551
        //Local Windows absolute path
552
        if ('' !== $root) {
553 4
            return Uri::createFromComponents(['path' => '/'.$root.$uri, 'scheme' => 'file', 'host' => '']);
554 2
        }
555
556
        //UNC Windows Path
557
        if ('//' !== substr($uri, 0, 2)) {
558 2
            return Uri::createFromComponents(['path' => $uri]);
559
        }
560 2
561
        $parts = explode('/', substr($uri, 2), 2) + [1 => null];
562
563 80
        return Uri::createFromComponents(['host' => $parts[0], 'path' => '/'.$parts[1], 'scheme' => 'file']);
564 80
    }
565
566
    /**
567 80
     * Create a new instance from a URI object.
568 2
     *
569
     * @param Psr7UriInterface|UriInterface $uri the input URI to create
570
     */
571
    public static function createFromUri($uri): self
572 78
    {
573
        if ($uri instanceof UriInterface) {
574 78
            $user_info = $uri->getUserInfo();
575
            $user = null;
576
            $pass = null;
577
578
            if (null !== $user_info) {
579
                [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
580
            }
581
582 280
            return new self(
583
                $uri->getScheme(),
584 280
                $user,
585
                $pass,
586 280
                $uri->getHost(),
587 280
                $uri->getPort(),
588 280
                $uri->getPath(),
589 280
                $uri->getQuery(),
590 280
                $uri->getFragment()
591 280
            );
592 280
        }
593 280
594 280
        if (!$uri instanceof Psr7UriInterface) {
0 ignored issues
show
introduced by
$uri is always a sub-type of Psr\Http\Message\UriInterface.
Loading history...
595
            throw new TypeError(sprintf('The object must implement the `%s` or the `%s`', Psr7UriInterface::class, UriInterface::class));
596
        }
597
598
        $scheme = $uri->getScheme();
599
600
        if ('' === $scheme) {
601
            $scheme = null;
602
        }
603
604 90
        $fragment = $uri->getFragment();
605
606
        if ('' === $fragment) {
607 90
            $fragment = null;
608
        }
609
610
        $query = $uri->getQuery();
611 90
612 90
        if ('' === $query) {
613 90
            $query = null;
614 90
        }
615 90
616 90
        $host = $uri->getHost();
617 90
618 90
        if ('' === $host) {
619 90
            $host = null;
620
        }
621
622
        $user_info = $uri->getUserInfo();
623
        $user = null;
624
        $pass = null;
625
626
        if ('' !== $user_info) {
627
            [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
628
        }
629
630 6
        return new self(
631
            $scheme,
632 6
            $user,
633 6
            $pass,
634 6
            $host,
635 4
            $uri->getPort(),
636 4
            $uri->getPath(),
637
            $query,
638
            $fragment
639 6
        );
640 6
    }
641 2
642
    /**
643
     * Create a new instance from the environment.
644 4
     */
645
    public static function createFromServer(array $server): self
646 4
    {
647 4
        [$user, $pass] = self::fetchUserInfo($server);
648 4
        [$host, $port] = self::fetchHostname($server);
649
        [$path, $query] = self::fetchRequestUri($server);
650
651
        return Uri::createFromComponents([
652
            'scheme' => self::fetchScheme($server),
653
            'user' => $user,
654
            'pass' => $pass,
655 10
            'host' => $host,
656
            'port' => $port,
657 10
            'path' => $path,
658 10
            'query' => $query,
659 4
        ]);
660
    }
661
662 6
    /**
663
     * Returns the environment scheme.
664
     */
665
    private static function fetchScheme(array $server): string
666
    {
667
        $server += ['HTTPS' => ''];
668 16
        $res = filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
669
670 16
        return $res !== false ? 'https' : 'http';
671 16
    }
672 8
673 8
    /**
674
     * Returns the environment user info.
675 16
     */
676 16
    private static function fetchUserInfo(array $server): array
677
    {
678
        $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => ''];
679 16
        $user = $server['PHP_AUTH_USER'];
680 8
        $pass = $server['PHP_AUTH_PW'];
681
682
        if (0 === strpos(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) {
683
            $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true);
684 8
685 6
            if (false === $userinfo) {
686
                throw new SyntaxError('The user info could not be detected');
687
            }
688 2
            [$user, $pass] = explode(':', $userinfo, 2) + [1 => null];
689
        }
690 2
691
        if (null !== $user) {
692
            $user = rawurlencode($user);
693
        }
694
695
        if (null !== $pass) {
696
            $pass = rawurlencode($pass);
697
        }
698 4
699
        return [$user, $pass];
700 4
    }
701 2
702 2
    /**
703 2
     * Returns the environment host.
704 2
     *
705 2
     * @throws SyntaxError If the host can not be detected
706
     */
707
    private static function fetchHostname(array $server): array
708 2
    {
709 2
        $server += ['SERVER_PORT' => null];
710 1
711 1
        if (null !== $server['SERVER_PORT']) {
712 2
            $server['SERVER_PORT'] = (int) $server['SERVER_PORT'];
713 2
        }
714 2
715 2
        if (isset($server['HTTP_HOST'])) {
716 2
            preg_match(',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x', $server['HTTP_HOST'], $matches);
717
718
            return [
719
                $matches['host'],
720 4
                isset($matches['port']) ? (int) $matches['port'] : $server['SERVER_PORT'],
721 2
            ];
722
        }
723
724 2
        if (!isset($server['SERVER_ADDR'])) {
725 2
            throw new SyntaxError('The host could not be detected');
726 2
        }
727
728
        if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
729 2
            $server['SERVER_ADDR'] = '['.$server['SERVER_ADDR'].']';
730 2
        }
731 2
732
        return [$server['SERVER_ADDR'], $server['SERVER_PORT']];
733
    }
734 2
735 2
    /**
736 2
     * Returns the environment path.
737
     */
738
    private static function fetchRequestUri(array $server): array
739 2
    {
740 2
        $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null];
741 2
742
        if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) {
743
            return explode('?', $server['UNENCODED_URL'], 2) + [1 => null];
744 2
        }
745 2
746 2
        if (isset($server['REQUEST_URI'])) {
747 2
            [$path, ] = explode('?', $server['REQUEST_URI'], 2);
748 2
            $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null;
749
750
            return [$path, $query];
751 2
        }
752 2
753 1
        return [$server['PHP_SELF'], $server['QUERY_STRING']];
754 1
    }
755 1
756 2
    /**
757 2
     * Generate the URI authority part.
758 1
     */
759 1
    private function setAuthority(): ?string
760
    {
761
        $authority = null;
762
763
        if (null !== $this->user_info) {
764
            $authority = $this->user_info.'@';
765
        }
766 26
767
        if (null !== $this->host) {
768 26
            $authority .= $this->host;
769 26
        }
770 26
771
        if (null !== $this->port) {
772 26
            $authority .= ':'.$this->port;
773 26
        }
774 26
775 26
        return $authority;
776 26
    }
777 26
778 26
    /**
779 26
     * Format the Path component.
780
     */
781
    private function formatPath(string $path): string
782
    {
783
        $path = $this->formatDataPath($path);
784
785
        static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/}{]++\|%(?![A-Fa-f0-9]{2}))/';
786 26
787
        $path = (string) preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $path);
788 26
789 26
        return $this->formatFilePath($path);
790
    }
791 26
792
    /**
793
     * Filter the Path component.
794
     *
795
     * @see https://tools.ietf.org/html/rfc2397
796
     *
797 28
     * @throws SyntaxError If the path is not compliant with RFC2397
798
     */
799 28
    private function formatDataPath(string $path): string
800 28
    {
801 28
        if ('data' !== $this->scheme) {
802 28
            return $path;
803 4
        }
804 4
805 2
        if ('' == $path) {
806
            return 'text/plain;charset=us-ascii,';
807 2
        }
808
809
        if (false === mb_detect_encoding($path, 'US-ASCII', true) || false === strpos($path, ',')) {
810 26
            throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937', $path));
811 4
        }
812
813
        $parts = explode(',', $path, 2) + [1 => null];
814 26
        $mediatype = explode(';', (string) $parts[0], 2) + [1 => null];
815 4
        $data = (string) $parts[1];
816
        $mimetype = $mediatype[0];
817
818 26
        if (null === $mimetype || '' === $mimetype) {
819
            $mimetype = 'text/plain';
820
        }
821
822
        $parameters = $mediatype[1];
823
824
        if (null === $parameters || '' === $parameters) {
825
            $parameters = 'charset=us-ascii';
826 28
        }
827
828 28
        $this->assertValidPath($mimetype, $parameters, $data);
829 28
830 26
        return $mimetype.';'.$parameters.','.$data;
831
    }
832
833 28
    /**
834 18
     * Assert the path is a compliant with RFC2397.
835
     *
836
     * @see https://tools.ietf.org/html/rfc2397
837 18
     *
838 18
     * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
839
     */
840
    private function assertValidPath(string $mimetype, string $parameters, string $data): void
841
    {
842 10
        if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
843 2
            throw new SyntaxError(sprintf('The path mimetype `%s` is invalid', $mimetype));
844
        }
845
846 8
        $is_binary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches);
847 2
848
        if ($is_binary) {
849
            $parameters = substr($parameters, 0, - strlen($matches[0]));
850 8
        }
851
852
        $res = array_filter(array_filter(explode(';', $parameters), [$this, 'validateParameter']));
853
854
        if ([] !== $res) {
855
            throw new SyntaxError(sprintf('The path paremeters `%s` is invalid', $parameters));
856 26
        }
857
858 26
        if (!$is_binary) {
859 26
            return;
860 2
        }
861
862
        $res = base64_decode($data, true);
863 24
864 20
        if (false === $res || $data !== base64_encode($res)) {
865 20
            throw new SyntaxError(sprintf('The path data `%s` is invalid', $data));
866
        }
867 20
    }
868
869
    /**
870 4
     * Validate mediatype parameter.
871
     */
872
    private function validateParameter(string $parameter): bool
873
    {
874
        $properties = explode('=', $parameter);
875
876
        return 2 != count($properties) || strtolower($properties[0]) === 'base64';
877 302
    }
878
879 302
    /**
880 302
     * Format path component for file scheme.
881 50
     */
882
    private function formatFilePath(string $path): string
883
    {
884 302
        if ('file' !== $this->scheme) {
885 254
            return $path;
886
        }
887
888 302
        $replace = static function (array $matches): string {
889 58
            return $matches['delim'].str_replace('|', ':', $matches['root']).$matches['rest'];
890
        };
891
892 302
        return (string) preg_replace_callback(self::REGEXP_FILE_PATH, $replace, $path);
893
    }
894
895
    /**
896
     * Format the Query or the Fragment component.
897
     *
898 314
     * Returns a array containing:
899
     * <ul>
900 314
     * <li> the formatted component (a string or null)</li>
901
     * <li> a boolean flag telling wether the delimiter is to be added to the component
902 314
     * when building the URI string representation</li>
903
     * </ul>
904 314
     *
905
     * @param ?string $component
906 314
     */
907
    private function formatQueryAndFragment(?string $component): ?string
908
    {
909
        if (null === $component || '' === $component) {
910
            return $component;
911
        }
912
913
        static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/';
914
        return preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $component);
915
    }
916 328
917
    /**
918 328
     * assert the URI internal state is valid.
919 300
     *
920
     * @see https://tools.ietf.org/html/rfc3986#section-3
921
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
922 28
     *
923 2
     * @throws SyntaxError if the URI is in an invalid state according to RFC3986
924
     * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules
925
     */
926 26
    private function assertValidState(): void
927 4
    {
928
        if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) {
929
            throw new SyntaxError('If an authority is present the path must be empty or start with a `/`');
930 22
        }
931 22
932 22
        if (null === $this->authority && 0 === strpos($this->path, '//')) {
933 22
            throw new SyntaxError(sprintf('If there is no authority the path `%s` can not start with a `//`', $this->path));
934 22
        }
935 4
936
        $pos = strpos($this->path, ':');
937
938 22
        if (null === $this->authority
939 22
            && null === $this->scheme
940 6
            && false !== $pos
941
            && false === strpos(substr($this->path, 0, $pos), '/')
942
        ) {
943 22
            throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
944
        }
945 14
946
        $validationMethod = self::SCHEME_VALIDATION_METHOD[$this->scheme] ?? null;
947
948
        if (null === $validationMethod || true === $this->$validationMethod()) {
949
            $this->uri = null;
950
951
            return;
952
        }
953
954
        throw new SyntaxError(sprintf('The uri `%s` is invalid for the data scheme', (string) $this));
955 22
    }
956
957 22
    /**
958 2
     * URI validation for URI schemes which allows only scheme and path components.
959
     */
960
    private function isUriWithSchemeAndPathOnly()
961 20
    {
962 20
        return null === $this->authority
963 8
            && null === $this->query
964
            && null === $this->fragment;
965
    }
966 20
967 20
    /**
968 4
     * URI validation for URI schemes which allows only scheme, host and path components.
969
     */
970
    private function isUriWithSchemeHostAndPathOnly()
971 16
    {
972 12
        return null === $this->user_info
973
            && null === $this->port
974
            && null === $this->query
975 4
            && null === $this->fragment
976 4
            && !('' != $this->scheme && null === $this->host);
977 2
    }
978
979 2
    /**
980
     * URI validation for URI schemes which disallow the empty '' host.
981
     */
982
    private function isNonEmptyHostUri()
983
    {
984 4
        return '' !== $this->host
985
            && !(null !== $this->scheme && null === $this->host);
986 4
    }
987
988 4
    /**
989
     * URI validation for URIs schemes which disallow the empty '' host
990
     * and forbids the fragment component.
991 322
     */
992
    private function isNonEmptyHostUriWithoutFragment()
993 322
    {
994 314
        return $this->isNonEmptyHostUri() && null === $this->fragment;
995
    }
996
997
    /**
998 2
     * URI validation for URIs schemes which disallow the empty '' host
999 8
     * and forbids fragment and query components.
1000
     */
1001 8
    private function isNonEmptyHostUriWithoutFragmentAndQuery()
1002
    {
1003
        return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
1004
    }
1005
1006
    /**
1007
     * Generate the URI string representation from its components.
1008
     *
1009
     * @see https://tools.ietf.org/html/rfc3986#section-5.3
1010
     *
1011
     * @param ?string $scheme
1012
     * @param ?string $authority
1013
     * @param ?string $query
1014
     * @param ?string $fragment
1015
     */
1016 308
    private function getUriString(
1017
        ?string $scheme,
1018 308
        ?string $authority,
1019 282
        string $path,
1020
        ?string $query,
1021
        ?string $fragment
1022 208
    ): string {
1023 208
        if (null !== $scheme) {
1024
            $scheme = $scheme.':';
1025
        }
1026
1027
        if (null !== $authority) {
1028
            $authority = '//'.$authority;
1029
        }
1030
1031
        if (null !== $query) {
1032
            $query = '?'.$query;
1033
        }
1034
1035 356
        if (null !== $fragment) {
1036
            $fragment = '#'.$fragment;
1037 356
        }
1038 4
1039
        return $scheme.$authority.$path.$query.$fragment;
1040
    }
1041 356
1042 10
    /**
1043
     * {@inheritDoc}
1044
     */
1045 356
    public function __toString(): string
1046 356
    {
1047 356
        $this->uri = $this->uri ?? $this->getUriString(
1048 356
            $this->scheme,
1049 356
            $this->authority,
1050
            $this->path,
1051 6
            $this->query,
1052
            $this->fragment
1053
        );
1054 356
1055 356
        return $this->uri;
1056 328
    }
1057
1058 328
    /**
1059
     * {@inheritdoc}
1060
     */
1061 38
    public function jsonSerialize(): string
1062
    {
1063
        return $this->__toString();
1064
    }
1065
1066
    /**
1067 2
     * {@inheritdoc}
1068
     */
1069 2
    public function __debugInfo(): array
1070 2
    {
1071 2
        return [
1072
            'scheme' => $this->scheme,
1073
            'user_info' => isset($this->user_info) ? preg_replace(',:(.*).?$,', ':***', $this->user_info) : null,
1074
            'host' => $this->host,
1075
            'port' => $this->port,
1076
            'path' => $this->path,
1077 20
            'query' => $this->query,
1078
            'fragment' => $this->fragment,
1079 20
        ];
1080 20
    }
1081 20
1082 20
    /**
1083 20
     * {@inheritDoc}
1084
     */
1085
    public function getScheme(): ?string
1086
    {
1087
        return $this->scheme;
1088
    }
1089 248
1090
    /**
1091 248
     * {@inheritDoc}
1092 248
     */
1093
    public function getAuthority(): ?string
1094
    {
1095
        return $this->authority;
1096
    }
1097
1098
    /**
1099 18
     * {@inheritDoc}
1100
     */
1101 18
    public function getUserInfo(): ?string
1102
    {
1103
        return $this->user_info;
1104
    }
1105
1106
    /**
1107
     * {@inheritDoc}
1108 22
     */
1109
    public function getHost(): ?string
1110 22
    {
1111
        return $this->host;
1112
    }
1113
1114
    /**
1115
     * {@inheritDoc}
1116
     */
1117
    public function getPort(): ?int
1118
    {
1119
        return $this->port;
1120
    }
1121
1122 252
    /**
1123
     * {@inheritDoc}
1124
     */
1125
    public function getPath(): string
1126
    {
1127
        return $this->path;
1128
    }
1129 252
1130 134
    /**
1131
     * {@inheritDoc}
1132
     */
1133 252
    public function getQuery(): ?string
1134 124
    {
1135
        return $this->query;
1136
    }
1137 252
1138 42
    /**
1139
     * {@inheritDoc}
1140
     */
1141 252
    public function getFragment(): ?string
1142 36
    {
1143
        return $this->fragment;
1144
    }
1145 252
1146
    /**
1147
     * {@inheritDoc}
1148
     */
1149
    public function withScheme($scheme): UriInterface
1150
    {
1151 262
        $scheme = $this->formatScheme($this->filterString($scheme));
1152
1153 262
        if ($scheme === $this->scheme) {
1154 262
            return $this;
1155 262
        }
1156 262
1157 262
        $clone = clone $this;
1158 262
        $clone->scheme = $scheme;
1159
        $clone->port = $clone->formatPort($clone->port);
1160
        $clone->authority = $clone->setAuthority();
1161 262
        $clone->assertValidState();
1162
1163
        return $clone;
1164
    }
1165
1166
    /**
1167 2
     * Filter a string.
1168
     *
1169 2
     * @param mixed $str the value to evaluate as a string
1170
     *
1171
     * @throws SyntaxError if the submitted data can not be converted to string
1172
     */
1173
    private function filterString($str): ?string
1174
    {
1175 2
        if (null === $str) {
1176
            return $str;
1177
        }
1178 2
1179 2
        if (!is_scalar($str) && !method_exists($str, '__toString')) {
1180 2
            throw new TypeError(sprintf('The component must be a string, a scalar or a stringable object %s given', gettype($str)));
1181 2
        }
1182 2
1183 2
        $str = (string) $str;
1184 2
1185
        if (1 !== preg_match(Common::REGEXP_INVALID_URI_CHARS, $str)) {
1186
            return $str;
1187
        }
1188
1189
        throw new SyntaxError(sprintf('The component `%s` contains invalid characters', $str));
1190
    }
1191 230
1192
    /**
1193 230
     * {@inheritDoc}
1194
     */
1195
    public function withUserInfo($user, $password = null): UriInterface
1196
    {
1197
        $user_info = null;
1198
        $user = $this->filterString($user);
1199 198
1200
        if (null !== $password) {
1201 198
            $password = $this->filterString($password);
1202
        }
1203
1204
        if ('' !== $user) {
1205
            $user_info = $this->formatUserInfo($user, $password);
1206
        }
1207 96
1208
        if ($user_info === $this->user_info) {
1209 96
            return $this;
1210
        }
1211
1212
        $clone = clone $this;
1213
        $clone->user_info = $user_info;
1214
        $clone->authority = $clone->setAuthority();
1215 208
        $clone->assertValidState();
1216
1217 208
        return $clone;
1218
    }
1219
1220
    /**
1221
     * {@inheritDoc}
1222
     */
1223 238
    public function withHost($host): UriInterface
1224
    {
1225 238
        $host = $this->formatHost($this->filterString($host));
1226
1227
        if ($host === $this->host) {
1228
            return $this;
1229
        }
1230
1231 204
        $clone = clone $this;
1232
        $clone->host = $host;
1233 204
        $clone->authority = $clone->setAuthority();
1234
        $clone->assertValidState();
1235
1236
        return $clone;
1237
    }
1238
1239 114
    /**
1240
     * {@inheritDoc}
1241 114
     */
1242
    public function withPort($port): UriInterface
1243
    {
1244
        $port = $this->formatPort($port);
1245
1246
        if ($port === $this->port) {
1247 26
            return $this;
1248
        }
1249 26
1250
        $clone = clone $this;
1251
        $clone->port = $port;
1252
        $clone->authority = $clone->setAuthority();
1253
        $clone->assertValidState();
1254
1255 146
        return $clone;
1256
    }
1257 146
1258 144
    /**
1259 10
     * {@inheritDoc}
1260
     */
1261
    public function withPath($path): UriInterface
1262 136
    {
1263 136
        $path = $this->filterString($path);
1264 136
1265 136
        if (null === $path) {
0 ignored issues
show
introduced by
The condition null === $path is always false.
Loading history...
1266 136
            throw new TypeError('A path must be a string NULL given');
1267
        }
1268 136
1269
        $path = $this->formatPath($path);
1270
1271
        if ($path === $this->path) {
1272
            return $this;
1273
        }
1274
1275
        $clone = clone $this;
1276
        $clone->path = $path;
1277
        $clone->assertValidState();
1278 208
1279
        return $clone;
1280 208
    }
1281 148
1282
    /**
1283
     * {@inheritDoc}
1284 206
     */
1285 2
    public function withQuery($query): UriInterface
1286
    {
1287
        $query = $this->formatQueryAndFragment($this->filterString($query));
1288 204
1289 204
        if ($query === $this->query) {
1290 202
            return $this;
1291
        }
1292
1293 2
        $clone = clone $this;
1294
        $clone->query = $query;
1295
        $clone->assertValidState();
1296
1297
        return $clone;
1298
    }
1299
1300 146
    /**
1301
     * {@inheritDoc}
1302 146
     */
1303 146
    public function withFragment($fragment): UriInterface
1304 146
    {
1305 16
        $fragment = $this->formatQueryAndFragment($this->filterString($fragment));
1306
1307
        if ($fragment === $this->fragment) {
1308 146
            return $this;
1309 76
        }
1310
1311
        $clone = clone $this;
1312 146
        $clone->fragment = $fragment;
1313 128
        $clone->assertValidState();
1314
1315
        return $clone;
1316 20
    }
1317
}
1318