Completed
Push — master ( 25df9a...a7d926 )
by ignace nyamagana
26s queued 10s
created

Uri::formatRegisteredName()   C

Complexity

Conditions 12
Paths 22

Size

Total Lines 74
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 35
c 1
b 0
f 0
nc 22
nop 1
dl 0
loc 74
ccs 26
cts 26
cp 1
crap 12
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

687
        $raw = @file_get_contents(/** @scrutinizer ignore-type */ ...$file_args);
Loading history...
688 6
        if (false === $raw) {
689 2
            throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
690
        }
691
692 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

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