Completed
Pull Request — master (#154)
by
unknown
01:53
created

Uri::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 1
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 League\Uri\Contracts\UriInterface;
17
use League\Uri\Exceptions\FileinfoSupportMissing;
18
use League\Uri\Exceptions\IdnSupportMissing;
19
use League\Uri\Exceptions\SyntaxError;
20
use Psr\Http\Message\UriInterface as Psr7UriInterface;
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 defined;
27
use function explode;
28
use function file_get_contents;
29
use function filter_var;
30
use function function_exists;
31
use function idn_to_ascii;
32
use function implode;
33
use function in_array;
34
use function inet_pton;
35
use function is_scalar;
36
use function method_exists;
37
use function preg_match;
38
use function preg_replace;
39
use function preg_replace_callback;
40
use function rawurlencode;
41
use function sprintf;
42
use function str_replace;
43
use function strlen;
44
use function strpos;
45
use function strspn;
46
use function strtolower;
47
use function substr;
48
use const FILEINFO_MIME;
49
use const FILTER_FLAG_IPV4;
50
use const FILTER_FLAG_IPV6;
51
use const FILTER_NULL_ON_FAILURE;
52
use const FILTER_VALIDATE_BOOLEAN;
53
use const FILTER_VALIDATE_IP;
54
use const IDNA_CHECK_BIDI;
55
use const IDNA_CHECK_CONTEXTJ;
56
use const IDNA_ERROR_BIDI;
57
use const IDNA_ERROR_CONTEXTJ;
58
use const IDNA_ERROR_DISALLOWED;
59
use const IDNA_ERROR_DOMAIN_NAME_TOO_LONG;
60
use const IDNA_ERROR_EMPTY_LABEL;
61
use const IDNA_ERROR_HYPHEN_3_4;
62
use const IDNA_ERROR_INVALID_ACE_LABEL;
63
use const IDNA_ERROR_LABEL_HAS_DOT;
64
use const IDNA_ERROR_LABEL_TOO_LONG;
65
use const IDNA_ERROR_LEADING_COMBINING_MARK;
66
use const IDNA_ERROR_LEADING_HYPHEN;
67
use const IDNA_ERROR_PUNYCODE;
68
use const IDNA_ERROR_TRAILING_HYPHEN;
69
use const IDNA_NONTRANSITIONAL_TO_ASCII;
70
use const IDNA_NONTRANSITIONAL_TO_UNICODE;
71
use const INTL_IDNA_VARIANT_UTS46;
72
73
final class Uri implements UriInterface
74
{
75
    /**
76
     * RFC3986 invalid characters.
77
     *
78
     * @link https://tools.ietf.org/html/rfc3986#section-2.2
79
     *
80
     * @var string
81
     */
82
    private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
83
84
    /**
85
     * RFC3986 Sub delimiter characters regular expression pattern.
86
     *
87
     * @link https://tools.ietf.org/html/rfc3986#section-2.2
88
     *
89
     * @var string
90
     */
91
    private const REGEXP_CHARS_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
92
93
    /**
94
     * RFC3986 unreserved characters regular expression pattern.
95
     *
96
     * @link https://tools.ietf.org/html/rfc3986#section-2.3
97
     *
98
     * @var string
99
     */
100
    private const REGEXP_CHARS_UNRESERVED = 'A-Za-z0-9_\-\.~';
101
102
    /**
103
     * RFC3986 schema regular expression pattern.
104
     *
105
     * @link https://tools.ietf.org/html/rfc3986#section-3.1
106
     */
107
    private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i';
108
109
    /**
110
     * RFC3986 host identified by a registered name regular expression pattern.
111
     *
112
     * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
113
     */
114
    private const REGEXP_HOST_REGNAME = '/^(
115
        (?<unreserved>[a-z0-9_~\-\.])|
116
        (?<sub_delims>[!$&\'()*+,;=])|
117
        (?<encoded>%[A-F0-9]{2})
118
    )+$/x';
119
120
    /**
121
     * RFC3986 delimiters of the generic URI components regular expression pattern.
122
     *
123
     * @link https://tools.ietf.org/html/rfc3986#section-2.2
124
     */
125
    private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space.
126
127
    /**
128
     * RFC3986 IPvFuture regular expression pattern.
129
     *
130
     * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
131
     */
132
    private const REGEXP_HOST_IPFUTURE = '/^
133
        v(?<version>[A-F0-9])+\.
134
        (?:
135
            (?<unreserved>[a-z0-9_~\-\.])|
136
            (?<sub_delims>[!$&\'()*+,;=:])  # also include the : character
137
        )+
138
    $/ix';
139
140
    /**
141
     * Significant 10 bits of IP to detect Zone ID regular expression pattern.
142
     */
143
    private const HOST_ADDRESS_BLOCK = "\xfe\x80";
144
145
    /**
146
     * Regular expression pattern to for file URI.
147
     */
148
    private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<root>[a-zA-Z][:|\|])(?<rest>.*)?,';
149
150
    /**
151
     * Mimetype regular expression pattern.
152
     *
153
     * @link https://tools.ietf.org/html/rfc2397
154
     */
155
    private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
156
157
    /**
158
     * Base64 content regular expression pattern.
159
     *
160
     * @link https://tools.ietf.org/html/rfc2397
161
     */
162
    private const REGEXP_BINARY = ',(;|^)base64$,';
163
164
    /**
165
     * Windows file path string regular expression pattern.
166
     */
167
    private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),';
168
169
170
    /**
171
     * Supported schemes and corresponding default port.
172
     *
173
     * @var array
174
     */
175
    private const SCHEME_DEFAULT_PORT = [
176
        'data' => null,
177
        'file' => null,
178
        'ftp' => 21,
179
        'gopher' => 70,
180
        'http' => 80,
181
        'https' => 443,
182
        'ws' => 80,
183
        'wss' => 443,
184
    ];
185
186
    /**
187
     * URI validation methods per scheme.
188
     *
189
     * @var array
190
     */
191
    private const SCHEME_VALIDATION_METHOD = [
192
        'data' => 'isUriWithSchemeAndPathOnly',
193
        'file' => 'isUriWithSchemeHostAndPathOnly',
194
        'ftp' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
195
        'gopher' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
196
        'http' => 'isNonEmptyHostUri',
197
        'https' => 'isNonEmptyHostUri',
198
        'ws' => 'isNonEmptyHostUriWithoutFragment',
199
        'wss' => 'isNonEmptyHostUriWithoutFragment',
200
    ];
201
202
    /**
203
     * All ASCII letters sorted by typical frequency of occurrence.
204
     *
205
     * @var string
206
     */
207
    private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
208
209
    /**
210
     * URI scheme component.
211
     *
212
     * @var string|null
213
     */
214
    private $scheme;
215
216
    /**
217
     * URI user info part.
218
     *
219
     * @var string|null
220
     */
221
    private $user_info;
222
223
    /**
224
     * URI host component.
225
     *
226
     * @var string|null
227
     */
228
    private $host;
229
230
    /**
231
     * URI port component.
232
     *
233
     * @var int|null
234
     */
235
    private $port;
236
237
    /**
238
     * URI authority string representation.
239
     *
240
     * @var string|null
241
     */
242
    private $authority;
243
244
    /**
245
     * URI path component.
246
     *
247
     * @var string
248
     */
249
    private $path = '';
250
251
    /**
252
     * URI query component.
253
     *
254
     * @var string|null
255
     */
256
    private $query;
257
258
    /**
259
     * URI fragment component.
260
     *
261
     * @var string|null
262
     */
263
    private $fragment;
264
265
    /**
266
     * URI string representation.
267
     *
268
     * @var string|null
269
     */
270
    private $uri;
271
272
    /**
273
     * Create a new instance.
274
     *
275
     * @param ?string $scheme
276
     * @param ?string $user
277
     * @param ?string $pass
278
     * @param ?string $host
279
     * @param ?int    $port
280
     * @param ?string $query
281
     * @param ?string $fragment
282
     */
283 300
    private function __construct(
284
        ?string $scheme,
285
        ?string $user,
286
        ?string $pass,
287
        ?string $host,
288
        ?int $port,
289
        string $path,
290
        ?string $query,
291
        ?string $fragment
292
    ) {
293 300
        $this->scheme = $this->formatScheme($scheme);
294 300
        $this->user_info = $this->formatUserInfo($user, $pass);
295 300
        $this->host = $this->formatHost($host);
296 300
        $this->port = $this->formatPort($port);
297 300
        $this->authority = $this->setAuthority();
298 300
        $this->path = $this->formatPath($path);
299 300
        $this->query = $this->formatQueryAndFragment($query);
300 300
        $this->fragment = $this->formatQueryAndFragment($fragment);
301 300
        $this->assertValidState();
302 286
    }
303
304
    /**
305
     * Format the Scheme and Host component.
306
     *
307
     * @param ?string $scheme
308
     *
309
     * @throws SyntaxError if the scheme is invalid
310
     */
311 306
    private function formatScheme(?string $scheme): ?string
312
    {
313 306
        if ('' === $scheme || null === $scheme) {
314 234
            return $scheme;
315
        }
316
317 238
        $formatted_scheme = strtolower($scheme);
318 238
        if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) {
319 238
            return $formatted_scheme;
320
        }
321
322 2
        throw new SyntaxError(sprintf('The scheme `%s` is invalid', $scheme));
323
    }
324
325
    /**
326
     * Set the UserInfo component.
327
     *
328
     * @param ?string $user
329
     * @param ?string $password
330
     */
331 308
    private function formatUserInfo(?string $user, ?string $password): ?string
332
    {
333 308
        if (null === $user) {
334 282
            return $user;
335
        }
336
337 56
        static $user_pattern = '/(?:[^%'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
338 56
        $user = preg_replace_callback($user_pattern, [Uri::class, 'urlEncodeMatch'], $user);
339 56
        if (null === $password) {
340 6
            return $user;
341
        }
342
343 56
        static $password_pattern = '/(?:[^%:'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
344
345 56
        return $user.':'.preg_replace_callback($password_pattern, [Uri::class, 'urlEncodeMatch'], $password);
346
    }
347
348
    /**
349
     * Returns the RFC3986 encoded string matched.
350
     */
351 10
    private static function urlEncodeMatch(array $matches): string
352
    {
353 10
        return rawurlencode($matches[0]);
354
    }
355
356
    /**
357
     * Validate and Format the Host component.
358
     *
359
     * @param ?string $host
360
     */
361 332
    private function formatHost(?string $host): ?string
362
    {
363 332
        if (null === $host || '' === $host) {
364 234
            return $host;
365
        }
366
367 266
        if ('[' !== $host[0]) {
368 266
            return $this->formatRegisteredName($host);
369
        }
370
371 2
        return $this->formatIp($host);
372
    }
373
374
    /**
375
     * Validate and format a registered name.
376
     *
377
     * The host is converted to its ascii representation if needed
378
     *
379
     * @throws IdnSupportMissing if the submitted host required missing or misconfigured IDN support
380
     * @throws SyntaxError       if the submitted host is not a valid registered name
381
     */
382 266
    private function formatRegisteredName(string $host): string
383
    {
384
        // @codeCoverageIgnoreStart
385
        // added because it is not possible in travis to disabled the ext/intl extension
386
        // see travis issue https://github.com/travis-ci/travis-ci/issues/4701
387
        static $idn_support = null;
388
        $idn_support = $idn_support ?? function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46');
389
        // @codeCoverageIgnoreEnd
390
391 266
        $formatted_host = rawurldecode($host);
392 266
        if (1 === preg_match(self::REGEXP_HOST_REGNAME, $formatted_host)) {
393 258
            $formatted_host = strtolower($formatted_host);
394 258
            if (false === strpos($formatted_host, 'xn--')) {
395 254
                return $formatted_host;
396
            }
397
398
            // @codeCoverageIgnoreStart
399
            if (!$idn_support) {
400
                throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
401
            }
402
            // @codeCoverageIgnoreEnd
403
404 8
            $unicode = idn_to_utf8(
405 8
                $host,
406 8
                IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_UNICODE,
407 8
                INTL_IDNA_VARIANT_UTS46,
408 4
                $arr
409
            );
410
411 8
            if (0 !== $arr['errors']) {
412 2
                throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
413
            }
414
415
            // @codeCoverageIgnoreStart
416
            if (false === $unicode) {
417
                throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
418
            }
419
            // @codeCoverageIgnoreEnd
420
421 6
            return $formatted_host;
422
        }
423
424 16
        if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, $formatted_host)) {
425 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host));
426
        }
427
428
        // @codeCoverageIgnoreStart
429
        if (!$idn_support) {
430
            throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host));
431
        }
432
        // @codeCoverageIgnoreEnd
433
434 14
        $formatted_host = idn_to_ascii(
435 14
            $formatted_host,
436 14
            IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_ASCII,
437 14
            INTL_IDNA_VARIANT_UTS46,
438 14
            $arr
439
        );
440
441 14
        if ($arr === []) {
442 2
            throw new SyntaxError(sprintf('Host `%s` is invalid', $host));
443
        }
444
445 12
        if (0 !== $arr['errors']) {
446 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
447
        }
448
449
        // @codeCoverageIgnoreStart
450
        if (false === $formatted_host) {
451
            throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
452
        }
453
        // @codeCoverageIgnoreEnd
454
455 10
        return $arr['result'];
456
    }
457
458
    /**
459
     * Retrieves and format IDNA conversion error message.
460
     *
461
     * @link http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html
462
     */
463 4
    private function getIDNAErrors(int $error_byte): string
464
    {
465
        /**
466
         * IDNA errors.
467
         */
468 4
        static $idnErrors = [
469
            IDNA_ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
470
            IDNA_ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
471
            IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
472
            IDNA_ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
473
            IDNA_ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
474
            IDNA_ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
475
            IDNA_ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
476
            IDNA_ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
477
            IDNA_ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
478
            IDNA_ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
479
            IDNA_ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
480
            IDNA_ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
481
            IDNA_ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
482
        ];
483
484 4
        $res = [];
485 4
        foreach ($idnErrors as $error => $reason) {
486 4
            if ($error === ($error_byte & $error)) {
487 4
                $res[] = $reason;
488
            }
489
        }
490
491 4
        return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.';
492
    }
493
494
    /**
495
     * Validate and Format the IPv6/IPvfuture host.
496
     *
497
     * @throws SyntaxError if the submitted host is not a valid IP host
498
     */
499 16
    private function formatIp(string $host): string
500
    {
501 16
        $ip = substr($host, 1, -1);
502 16
        if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
503 2
            return $host;
504
        }
505
506 14
        if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
507 2
            return $host;
508
        }
509
510 12
        $pos = strpos($ip, '%');
511 12
        if (false === $pos) {
512 4
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
513
        }
514
515 8
        if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) {
516 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
517
        }
518
519 6
        $ip = substr($ip, 0, $pos);
520 6
        if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
521 2
            throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
522
        }
523
524
        //Only the address block fe80::/10 can have a Zone ID attach to
525
        //let's detect the link local significant 10 bits
526 4
        if (0 === strpos((string) inet_pton($ip), self::HOST_ADDRESS_BLOCK)) {
527 2
            return $host;
528
        }
529
530 2
        throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
531
    }
532
533
    /**
534
     * Format the Port component.
535
     *
536
     * @param null|mixed $port
537
     *
538
     * @throws SyntaxError
539
     */
540 328
    private function formatPort($port = null): ?int
541
    {
542 328
        if (null === $port || '' === $port) {
543 278
            return null;
544
        }
545
546 92
        if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) {
547 2
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
548
        }
549
550 92
        $port = (int) $port;
551 92
        if (0 > $port) {
552 2
            throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
553
        }
554
555 92
        $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null;
556 92
        if ($defaultPort === $port) {
557 14
            return null;
558
        }
559
560 84
        return $port;
561
    }
562
563
    /**
564
     * {@inheritDoc}
565
     */
566 18
    public static function __set_state(array $components): self
567
    {
568 18
        $components['user'] = null;
569 18
        $components['pass'] = null;
570 18
        if (null !== $components['user_info']) {
571 14
            [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null];
572
        }
573
574 18
        return new self(
575 18
            $components['scheme'],
576 18
            $components['user'],
577 18
            $components['pass'],
578 18
            $components['host'],
579 18
            $components['port'],
580 18
            $components['path'],
581 18
            $components['query'],
582 18
            $components['fragment']
583
        );
584
    }
585
586
    /**
587
     * Create a new instance from a URI and a Base URI.
588
     *
589
     * The returned URI must be absolute.
590
     *
591
     * @param mixed      $uri      the input URI to create
592
     * @param null|mixed $base_uri the base URI used for reference
593
     */
594 86
    public static function createFromBaseUri($uri, $base_uri = null): UriInterface
595
    {
596 86
        if (!$uri instanceof UriInterface) {
597 86
            $uri = self::createFromString($uri);
598
        }
599
600 86
        if (null === $base_uri) {
601 6
            if (null === $uri->getScheme()) {
602 2
                throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri));
603
            }
604
605 4
            if (null === $uri->getAuthority()) {
606 2
                return $uri;
607
            }
608
609
            /** @var UriInterface $uri */
610 2
            $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath(''));
611
612 2
            return $uri;
613
        }
614
615 80
        if (!$base_uri instanceof UriInterface) {
616 80
            $base_uri = self::createFromString($base_uri);
617
        }
618
619 80
        if (null === $base_uri->getScheme()) {
620 2
            throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri));
621
        }
622
623
        /** @var UriInterface $uri */
624 78
        $uri = UriResolver::resolve($uri, $base_uri);
625
626 78
        return $uri;
627
    }
628
629
    /**
630
     * Create a new instance from a string.
631
     *
632
     * @param string|mixed $uri
633
     */
634 278
    public static function createFromString($uri = ''): self
635
    {
636 278
        $components = UriString::parse($uri);
637
638 278
        return new self(
639 278
            $components['scheme'],
640 278
            $components['user'],
641 278
            $components['pass'],
642 278
            $components['host'],
643 278
            $components['port'],
644 278
            $components['path'],
645 278
            $components['query'],
646 278
            $components['fragment']
647
        );
648
    }
649
650
    /**
651
     * Create a new instance from a hash of parse_url parts.
652
     *
653
     * Create an new instance from a hash representation of the URI similar
654
     * to PHP parse_url function result
655
     *
656
     * @param array<string, mixed> $components
657
     */
658 90
    public static function createFromComponents(array $components = []): self
659
    {
660
        $components += [
661 90
            'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
662
            'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
663
        ];
664
665 90
        return new self(
666 90
            $components['scheme'],
667 90
            $components['user'],
668 90
            $components['pass'],
669 90
            $components['host'],
670 90
            $components['port'],
671 90
            $components['path'],
672 90
            $components['query'],
673 90
            $components['fragment']
674
        );
675
    }
676
677
    /**
678
     * Create a new instance from a data file path.
679
     *
680
     * @param resource|null $context
681
     *
682
     * @throws FileinfoSupportMissing If ext/fileinfo is not installed
683
     * @throws SyntaxError            If the file does not exist or is not readable
684
     */
685 6
    public static function createFromDataPath(string $path, $context = null): self
686
    {
687 6
        static $finfo_support = null;
688 6
        $finfo_support = $finfo_support ?? class_exists(\finfo::class);
689
690 6
        if (!$finfo_support) {
691
            throw new FileinfoSupportMissing(sprintf('Please install ext/fileinfo to use the %s() method.', __METHOD__));
692
        }
693
694 6
        $file_args = [$path, false];
695 6
        $mime_args = [$path, FILEINFO_MIME];
696 6
        if (null !== $context) {
697 4
            $file_args[] = $context;
698 4
            $mime_args[] = $context;
699
        }
700
701 6
        $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

701
        $raw = @file_get_contents(/** @scrutinizer ignore-type */ ...$file_args);
Loading history...
702 6
        if (false === $raw) {
703 2
            throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
704
        }
705
706 4
        $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

706
        $mimetype = (string) (new \finfo(FILEINFO_MIME))->file(/** @scrutinizer ignore-type */ ...$mime_args);
Loading history...
707
708 4
        return Uri::createFromComponents([
709 4
            'scheme' => 'data',
710 4
            'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)),
711
        ]);
712
    }
713
714
    /**
715
     * Create a new instance from a Unix path string.
716
     */
717 10
    public static function createFromUnixPath(string $uri = ''): self
718
    {
719 10
        $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
720 10
        if ('/' !== ($uri[0] ?? '')) {
721 4
            return Uri::createFromComponents(['path' => $uri]);
722
        }
723
724 6
        return Uri::createFromComponents(['path' => $uri, 'scheme' => 'file', 'host' => '']);
725
    }
726
727
    /**
728
     * Create a new instance from a local Windows path string.
729
     */
730 16
    public static function createFromWindowsPath(string $uri = ''): self
731
    {
732 16
        $root = '';
733 16
        if (1 === preg_match(self::REGEXP_WINDOW_PATH, $uri, $matches)) {
734 8
            $root = substr($matches['root'], 0, -1).':';
735 8
            $uri = substr($uri, strlen($root));
736
        }
737 16
        $uri = str_replace('\\', '/', $uri);
738 16
        $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
739
740
        //Local Windows absolute path
741 16
        if ('' !== $root) {
742 8
            return Uri::createFromComponents(['path' => '/'.$root.$uri, 'scheme' => 'file', 'host' => '']);
743
        }
744
745
        //UNC Windows Path
746 8
        if ('//' !== substr($uri, 0, 2)) {
747 6
            return Uri::createFromComponents(['path' => $uri]);
748
        }
749
750 2
        $parts = explode('/', substr($uri, 2), 2) + [1 => null];
751
752 2
        return Uri::createFromComponents(['host' => $parts[0], 'path' => '/'.$parts[1], 'scheme' => 'file']);
753
    }
754
755
    /**
756
     * Create a new instance from a URI object.
757
     *
758
     * @param Psr7UriInterface|UriInterface $uri the input URI to create
759
     */
760 4
    public static function createFromUri($uri): self
761
    {
762 4
        if ($uri instanceof UriInterface) {
763 2
            $user_info = $uri->getUserInfo();
764 2
            $user = null;
765 2
            $pass = null;
766 2
            if (null !== $user_info) {
767 2
                [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
768
            }
769
770 2
            return new self(
771 2
                $uri->getScheme(),
772 1
                $user,
773 1
                $pass,
774 2
                $uri->getHost(),
775 2
                $uri->getPort(),
776 2
                $uri->getPath(),
777 2
                $uri->getQuery(),
778 2
                $uri->getFragment()
779
            );
780
        }
781
782 4
        if (!$uri instanceof Psr7UriInterface) {
0 ignored issues
show
introduced by
$uri is always a sub-type of Psr\Http\Message\UriInterface.
Loading history...
783 2
            throw new \TypeError(sprintf('The object must implement the `%s` or the `%s`', Psr7UriInterface::class, UriInterface::class));
784
        }
785
786 2
        $scheme = $uri->getScheme();
787 2
        if ('' === $scheme) {
788 2
            $scheme = null;
789
        }
790
791 2
        $fragment = $uri->getFragment();
792 2
        if ('' === $fragment) {
793 2
            $fragment = null;
794
        }
795
796 2
        $query = $uri->getQuery();
797 2
        if ('' === $query) {
798 2
            $query = null;
799
        }
800
801 2
        $host = $uri->getHost();
802 2
        if ('' === $host) {
803 2
            $host = null;
804
        }
805
806 2
        $user_info = $uri->getUserInfo();
807 2
        $user = null;
808 2
        $pass = null;
809 2
        if ('' !== $user_info) {
810 2
            [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
811
        }
812
813 2
        return new self(
814 2
            $scheme,
815 1
            $user,
816 1
            $pass,
817 1
            $host,
818 2
            $uri->getPort(),
819 2
            $uri->getPath(),
820 1
            $query,
821 1
            $fragment
822
        );
823
    }
824
825
    /**
826
     * Create a new instance from the environment.
827
     */
828 26
    public static function createFromServer(array $server): self
829
    {
830 26
        [$user, $pass] = self::fetchUserInfo($server);
831 26
        [$host, $port] = self::fetchHostname($server);
832 26
        [$path, $query] = self::fetchRequestUri($server);
833
834 26
        return Uri::createFromComponents([
835 26
            'scheme' => self::fetchScheme($server),
836 26
            'user' => $user,
837 26
            'pass' => $pass,
838 26
            'host' => $host,
839 26
            'port' => $port,
840 26
            'path' => $path,
841 26
            'query' => $query,
842
        ]);
843
    }
844
845
    /**
846
     * Returns the environment scheme.
847
     */
848 26
    private static function fetchScheme(array $server): string
849
    {
850 26
        $server += ['HTTPS' => ''];
851 26
        $res = filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
852
853 26
        return false !== $res ? 'https' : 'http';
854
    }
855
856
    /**
857
     * Returns the environment user info.
858
     *
859
     * @return array{0:?string, 1:?string}
860
     */
861 28
    private static function fetchUserInfo(array $server): array
862
    {
863 28
        $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => ''];
864 28
        $user = $server['PHP_AUTH_USER'];
865 28
        $pass = $server['PHP_AUTH_PW'];
866 28
        if (0 === strpos(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) {
867 4
            $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true);
868 4
            if (false === $userinfo) {
869 2
                throw new SyntaxError('The user info could not be detected');
870
            }
871 2
            [$user, $pass] = explode(':', $userinfo, 2) + [1 => null];
872
        }
873
874 26
        if (null !== $user) {
875 4
            $user = rawurlencode($user);
876
        }
877
878 26
        if (null !== $pass) {
879 4
            $pass = rawurlencode($pass);
880
        }
881
882 26
        return [$user, $pass];
883
    }
884
885
    /**
886
     * Returns the environment host.
887
     *
888
     * @throws SyntaxError If the host can not be detected
889
     *
890
     * @return array{0:?string, 1:?string}
891
     */
892 28
    private static function fetchHostname(array $server): array
893
    {
894 28
        $server += ['SERVER_PORT' => null];
895 28
        if (null !== $server['SERVER_PORT']) {
896 26
            $server['SERVER_PORT'] = (int) $server['SERVER_PORT'];
897
        }
898
899 28
        if (isset($server['HTTP_HOST'])) {
900 18
            preg_match(',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x', $server['HTTP_HOST'], $matches);
901
902
            return [
903 18
                $matches['host'],
904 18
                isset($matches['port']) ? (int) $matches['port'] : $server['SERVER_PORT'],
905
            ];
906
        }
907
908 10
        if (!isset($server['SERVER_ADDR'])) {
909 2
            throw new SyntaxError('The host could not be detected');
910
        }
911
912 8
        if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
913 2
            $server['SERVER_ADDR'] = '['.$server['SERVER_ADDR'].']';
914
        }
915
916 8
        return [$server['SERVER_ADDR'], $server['SERVER_PORT']];
917
    }
918
919
    /**
920
     * Returns the environment path.
921
     *
922
     * @return array{0:?string, 1:?string}
923
     */
924 26
    private static function fetchRequestUri(array $server): array
925
    {
926 26
        $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null];
927 26
        if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) {
928
            /** @var array{0:?string, 1:?string} $retval */
929 2
            $retval = explode('?', $server['UNENCODED_URL'], 2) + [1 => null];
930
931 2
            return $retval;
932
        }
933
934 24
        if (isset($server['REQUEST_URI'])) {
935 20
            [$path, ] = explode('?', $server['REQUEST_URI'], 2);
936 20
            $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null;
937
938 20
            return [$path, $query];
939
        }
940
941 4
        return [$server['PHP_SELF'], $server['QUERY_STRING']];
942
    }
943
944
    /**
945
     * Generate the URI authority part.
946
     */
947 300
    private function setAuthority(): ?string
948
    {
949 300
        $authority = null;
950 300
        if (null !== $this->user_info) {
951 48
            $authority = $this->user_info.'@';
952
        }
953
954 300
        if (null !== $this->host) {
955 252
            $authority .= $this->host;
956
        }
957
958 300
        if (null !== $this->port) {
959 56
            $authority .= ':'.$this->port;
960
        }
961
962 300
        return $authority;
963
    }
964
965
    /**
966
     * Format the Path component.
967
     */
968 312
    private function formatPath(string $path): string
969
    {
970 312
        $path = $this->formatDataPath($path);
971
972 312
        static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/}{]++\|%(?![A-Fa-f0-9]{2}))/';
973
974 312
        $path = (string) preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $path);
975
976 312
        return $this->formatFilePath($path);
977
    }
978
979
    /**
980
     * Filter the Path component.
981
     *
982
     * @link https://tools.ietf.org/html/rfc2397
983
     *
984
     * @throws SyntaxError If the path is not compliant with RFC2397
985
     */
986 326
    private function formatDataPath(string $path): string
987
    {
988 326
        if ('data' !== $this->scheme) {
989 298
            return $path;
990
        }
991
992 28
        if ('' == $path) {
993 2
            return 'text/plain;charset=us-ascii,';
994
        }
995
996 26
        if (strlen($path) !== strspn($path, self::ASCII) || false === strpos($path, ',')) {
997 4
            throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937', $path));
998
        }
999
1000 22
        $parts = explode(',', $path, 2) + [1 => null];
1001 22
        $mediatype = explode(';', (string) $parts[0], 2) + [1 => null];
1002 22
        $data = (string) $parts[1];
1003 22
        $mimetype = $mediatype[0];
1004 22
        if (null === $mimetype || '' === $mimetype) {
1005 4
            $mimetype = 'text/plain';
1006
        }
1007
1008 22
        $parameters = $mediatype[1];
1009 22
        if (null === $parameters || '' === $parameters) {
1010 6
            $parameters = 'charset=us-ascii';
1011
        }
1012
1013 22
        $this->assertValidPath($mimetype, $parameters, $data);
1014
1015 14
        return $mimetype.';'.$parameters.','.$data;
1016
    }
1017
1018
    /**
1019
     * Assert the path is a compliant with RFC2397.
1020
     *
1021
     * @link https://tools.ietf.org/html/rfc2397
1022
     *
1023
     * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
1024
     */
1025 22
    private function assertValidPath(string $mimetype, string $parameters, string $data): void
1026
    {
1027 22
        if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
1028 2
            throw new SyntaxError(sprintf('The path mimetype `%s` is invalid', $mimetype));
1029
        }
1030
1031 20
        $is_binary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches);
1032 20
        if ($is_binary) {
1033 8
            $parameters = substr($parameters, 0, - strlen($matches[0]));
1034
        }
1035
1036 20
        $res = array_filter(array_filter(explode(';', $parameters), [$this, 'validateParameter']));
1037 20
        if ([] !== $res) {
1038 4
            throw new SyntaxError(sprintf('The path paremeters `%s` is invalid', $parameters));
1039
        }
1040
1041 16
        if (!$is_binary) {
1042 12
            return;
1043
        }
1044
1045 4
        $res = base64_decode($data, true);
1046 4
        if (false === $res || $data !== base64_encode($res)) {
1047 2
            throw new SyntaxError(sprintf('The path data `%s` is invalid', $data));
1048
        }
1049 2
    }
1050
1051
    /**
1052
     * Validate mediatype parameter.
1053
     */
1054 4
    private function validateParameter(string $parameter): bool
1055
    {
1056 4
        $properties = explode('=', $parameter);
1057
1058 4
        return 2 != count($properties) || 'base64' === strtolower($properties[0]);
1059
    }
1060
1061
    /**
1062
     * Format path component for file scheme.
1063
     */
1064 320
    private function formatFilePath(string $path): string
1065
    {
1066 320
        if ('file' !== $this->scheme) {
1067 312
            return $path;
1068
        }
1069
1070
        $replace = static function (array $matches): string {
1071 2
            return $matches['delim'].str_replace('|', ':', $matches['root']).$matches['rest'];
1072 8
        };
1073
1074 8
        return (string) preg_replace_callback(self::REGEXP_FILE_PATH, $replace, $path);
1075
    }
1076
1077
    /**
1078
     * Format the Query or the Fragment component.
1079
     *
1080
     * Returns a array containing:
1081
     * <ul>
1082
     * <li> the formatted component (a string or null)</li>
1083
     * <li> a boolean flag telling wether the delimiter is to be added to the component
1084
     * when building the URI string representation</li>
1085
     * </ul>
1086
     *
1087
     * @param ?string $component
1088
     */
1089 306
    private function formatQueryAndFragment(?string $component): ?string
1090
    {
1091 306
        if (null === $component || '' === $component) {
1092 282
            return $component;
1093
        }
1094
1095 206
        static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/';
1096 206
        return preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $component);
1097
    }
1098
1099
    /**
1100
     * assert the URI internal state is valid.
1101
     *
1102
     * @link https://tools.ietf.org/html/rfc3986#section-3
1103
     * @link https://tools.ietf.org/html/rfc3986#section-3.3
1104
     *
1105
     * @throws SyntaxError if the URI is in an invalid state according to RFC3986
1106
     * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules
1107
     */
1108 354
    private function assertValidState(): void
1109
    {
1110 354
        if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) {
1111 4
            throw new SyntaxError('If an authority is present the path must be empty or start with a `/`');
1112
        }
1113
1114 354
        if (null === $this->authority && 0 === strpos($this->path, '//')) {
1115 10
            throw new SyntaxError(sprintf('If there is no authority the path `%s` can not start with a `//`', $this->path));
1116
        }
1117
1118 354
        $pos = strpos($this->path, ':');
1119 354
        if (null === $this->authority
1120 354
            && null === $this->scheme
1121 354
            && false !== $pos
1122 354
            && false === strpos(substr($this->path, 0, $pos), '/')
1123
        ) {
1124 6
            throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
1125
        }
1126
1127 354
        $validationMethod = self::SCHEME_VALIDATION_METHOD[$this->scheme] ?? null;
1128 354
        if (null === $validationMethod || true === $this->$validationMethod()) {
1129 326
            $this->uri = null;
1130
1131 326
            return;
1132
        }
1133
1134 38
        throw new SyntaxError(sprintf('The uri `%s` is invalid for the data scheme', (string) $this));
1135
    }
1136
1137
    /**
1138
     * URI validation for URI schemes which allows only scheme and path components.
1139
     */
1140 2
    private function isUriWithSchemeAndPathOnly(): bool
1141
    {
1142 2
        return null === $this->authority
1143 2
            && null === $this->query
1144 2
            && null === $this->fragment;
1145
    }
1146
1147
    /**
1148
     * URI validation for URI schemes which allows only scheme, host and path components.
1149
     */
1150 20
    private function isUriWithSchemeHostAndPathOnly(): bool
1151
    {
1152 20
        return null === $this->user_info
1153 20
            && null === $this->port
1154 20
            && null === $this->query
1155 20
            && null === $this->fragment
1156 20
            && !('' != $this->scheme && null === $this->host);
1157
    }
1158
1159
    /**
1160
     * URI validation for URI schemes which disallow the empty '' host.
1161
     */
1162 246
    private function isNonEmptyHostUri(): bool
1163
    {
1164 246
        return '' !== $this->host
1165 246
            && !(null !== $this->scheme && null === $this->host);
1166
    }
1167
1168
    /**
1169
     * URI validation for URIs schemes which disallow the empty '' host
1170
     * and forbids the fragment component.
1171
     */
1172 18
    private function isNonEmptyHostUriWithoutFragment(): bool
1173
    {
1174 18
        return $this->isNonEmptyHostUri() && null === $this->fragment;
1175
    }
1176
1177
    /**
1178
     * URI validation for URIs schemes which disallow the empty '' host
1179
     * and forbids fragment and query components.
1180
     */
1181 22
    private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool
1182
    {
1183 22
        return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
1184
    }
1185
1186
    /**
1187
     * Generate the URI string representation from its components.
1188
     *
1189
     * @link https://tools.ietf.org/html/rfc3986#section-5.3
1190
     *
1191
     * @param ?string $scheme
1192
     * @param ?string $authority
1193
     * @param ?string $query
1194
     * @param ?string $fragment
1195
     */
1196 250
    private function getUriString(
1197
        ?string $scheme,
1198
        ?string $authority,
1199
        string $path,
1200
        ?string $query,
1201
        ?string $fragment
1202
    ): string {
1203 250
        if (null !== $scheme) {
1204 132
            $scheme = $scheme.':';
1205
        }
1206
1207 250
        if (null !== $authority) {
1208 122
            $authority = '//'.$authority;
1209
        }
1210
1211 250
        if (null !== $query) {
1212 40
            $query = '?'.$query;
1213
        }
1214
1215 250
        if (null !== $fragment) {
1216 34
            $fragment = '#'.$fragment;
1217
        }
1218
1219 250
        return $scheme.$authority.$path.$query.$fragment;
1220
    }
1221
1222
    /**
1223
     * {@inheritDoc}
1224
     */
1225 260
    public function __toString(): string
1226
    {
1227 260
        $this->uri = $this->uri ?? $this->getUriString(
1228 260
            $this->scheme,
1229 260
            $this->authority,
1230 260
            $this->path,
1231 260
            $this->query,
1232 260
            $this->fragment
1233
        );
1234
1235 260
        return $this->uri;
1236
    }
1237
1238
    /**
1239
     * {@inheritDoc}
1240
     */
1241 2
    public function jsonSerialize(): string
1242
    {
1243 2
        return $this->__toString();
1244
    }
1245
1246
    /**
1247
     * {@inheritDoc}
1248
     *
1249
     * @return array{scheme:?string, user_info:?string, host:?string, port:?int, path:string, query:?string, fragment:?string}
1250
     */
1251 2
    public function __debugInfo(): array
1252
    {
1253
        return [
1254 2
            'scheme' => $this->scheme,
1255 2
            'user_info' => isset($this->user_info) ? preg_replace(',:(.*).?$,', ':***', $this->user_info) : null,
1256 2
            'host' => $this->host,
1257 2
            'port' => $this->port,
1258 2
            'path' => $this->path,
1259 2
            'query' => $this->query,
1260 2
            'fragment' => $this->fragment,
1261
        ];
1262
    }
1263
1264
    /**
1265
     * {@inheritDoc}
1266
     */
1267 230
    public function getScheme(): ?string
1268
    {
1269 230
        return $this->scheme;
1270
    }
1271
1272
    /**
1273
     * {@inheritDoc}
1274
     */
1275 198
    public function getAuthority(): ?string
1276
    {
1277 198
        return $this->authority;
1278
    }
1279
1280
    /**
1281
     * {@inheritDoc}
1282
     */
1283 96
    public function getUserInfo(): ?string
1284
    {
1285 96
        return $this->user_info;
1286
    }
1287
1288
    /**
1289
     * {@inheritDoc}
1290
     */
1291 208
    public function getHost(): ?string
1292
    {
1293 208
        return $this->host;
1294
    }
1295
1296
    /**
1297
     * {@inheritDoc}
1298
     */
1299 238
    public function getPort(): ?int
1300
    {
1301 238
        return $this->port;
1302
    }
1303
1304
    /**
1305
     * {@inheritDoc}
1306
     */
1307 204
    public function getPath(): string
1308
    {
1309 204
        return $this->path;
1310
    }
1311
1312
    /**
1313
     * {@inheritDoc}
1314
     */
1315 114
    public function getQuery(): ?string
1316
    {
1317 114
        return $this->query;
1318
    }
1319
1320
    /**
1321
     * {@inheritDoc}
1322
     */
1323 26
    public function getFragment(): ?string
1324
    {
1325 26
        return $this->fragment;
1326
    }
1327
1328
    /**
1329
     * {@inheritDoc}
1330
     */
1331 146
    public function withScheme($scheme): UriInterface
1332
    {
1333 146
        $scheme = $this->formatScheme($this->filterString($scheme));
1334 144
        if ($scheme === $this->scheme) {
1335 10
            return $this;
1336
        }
1337
1338 136
        $clone = clone $this;
1339 136
        $clone->scheme = $scheme;
1340 136
        $clone->port = $clone->formatPort($clone->port);
1341 136
        $clone->authority = $clone->setAuthority();
1342 136
        $clone->assertValidState();
1343
1344 136
        return $clone;
1345
    }
1346
1347
    /**
1348
     * Filter a string.
1349
     *
1350
     * @param mixed $str the value to evaluate as a string
1351
     *
1352
     * @throws SyntaxError if the submitted data can not be converted to string
1353
     */
1354 206
    private function filterString($str): ?string
1355
    {
1356 206
        if (null === $str) {
1357 146
            return $str;
1358
        }
1359
1360 204
        if (!is_scalar($str) && !method_exists($str, '__toString')) {
1361 2
            throw new \TypeError(sprintf('The component must be a string, a scalar or a stringable object %s given', gettype($str)));
1362
        }
1363
1364 202
        $str = (string) $str;
1365 202
        if (1 !== preg_match(self::REGEXP_INVALID_CHARS, $str)) {
1366 200
            return $str;
1367
        }
1368
1369 2
        throw new SyntaxError(sprintf('The component `%s` contains invalid characters', $str));
1370
    }
1371
1372
    /**
1373
     * {@inheritDoc}
1374
     */
1375 146
    public function withUserInfo($user, $password = null): UriInterface
1376
    {
1377 146
        $user_info = null;
1378 146
        $user = $this->filterString($user);
1379 146
        if (null !== $password) {
1380 16
            $password = $this->filterString($password);
1381
        }
1382
1383 146
        if ('' !== $user) {
1384 76
            $user_info = $this->formatUserInfo($user, $password);
1385
        }
1386
1387 146
        if ($user_info === $this->user_info) {
1388 128
            return $this;
1389
        }
1390
1391 20
        $clone = clone $this;
1392 20
        $clone->user_info = $user_info;
1393 20
        $clone->authority = $clone->setAuthority();
1394 20
        $clone->assertValidState();
1395
1396 20
        return $clone;
1397
    }
1398
1399
    /**
1400
     * {@inheritDoc}
1401
     */
1402 178
    public function withHost($host): UriInterface
1403
    {
1404 178
        $host = $this->formatHost($this->filterString($host));
1405 176
        if ($host === $this->host) {
1406 96
            return $this;
1407
        }
1408
1409 132
        $clone = clone $this;
1410 132
        $clone->host = $host;
1411 132
        $clone->authority = $clone->setAuthority();
1412 132
        $clone->assertValidState();
1413
1414 132
        return $clone;
1415
    }
1416
1417
    /**
1418
     * {@inheritDoc}
1419
     */
1420 136
    public function withPort($port): UriInterface
1421
    {
1422 136
        $port = $this->formatPort($port);
1423 132
        if ($port === $this->port) {
1424 130
            return $this;
1425
        }
1426
1427 4
        $clone = clone $this;
1428 4
        $clone->port = $port;
1429 4
        $clone->authority = $clone->setAuthority();
1430 4
        $clone->assertValidState();
1431
1432 4
        return $clone;
1433
    }
1434
1435
    /**
1436
     * {@inheritDoc}
1437
     */
1438 172
    public function withPath($path): UriInterface
1439
    {
1440 172
        $path = $this->filterString($path);
1441 172
        if (null === $path) {
0 ignored issues
show
introduced by
The condition null === $path is always false.
Loading history...
1442 2
            throw new \TypeError('A path must be a string NULL given');
1443
        }
1444
1445 170
        $path = $this->formatPath($path);
1446 170
        if ($path === $this->path) {
1447 34
            return $this;
1448
        }
1449
1450 158
        $clone = clone $this;
1451 158
        $clone->path = $path;
1452 158
        $clone->assertValidState();
1453
1454 146
        return $clone;
1455
    }
1456
1457
    /**
1458
     * {@inheritDoc}
1459
     */
1460 104
    public function withQuery($query): UriInterface
1461
    {
1462 104
        $query = $this->formatQueryAndFragment($this->filterString($query));
1463 104
        if ($query === $this->query) {
1464 96
            return $this;
1465
        }
1466
1467 14
        $clone = clone $this;
1468 14
        $clone->query = $query;
1469 14
        $clone->assertValidState();
1470
1471 14
        return $clone;
1472
    }
1473
1474
    /**
1475
     * {@inheritDoc}
1476
     */
1477 24
    public function withFragment($fragment): UriInterface
1478
    {
1479 24
        $fragment = $this->formatQueryAndFragment($this->filterString($fragment));
1480 24
        if ($fragment === $this->fragment) {
1481 24
            return $this;
1482
        }
1483
1484 4
        $clone = clone $this;
1485 4
        $clone->fragment = $fragment;
1486 4
        $clone->assertValidState();
1487
1488 4
        return $clone;
1489
    }
1490
}
1491