Completed
Push — master ( 567b73...e8ae03 )
by ignace nyamagana
63:14 queued 61:32
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
        // @codeCoverageIgnoreStart
691
        if (!$finfo_support) {
692
            throw new FileinfoSupportMissing(sprintf('Please install ext/fileinfo to use the %s() method.', __METHOD__));
693
        }
694
        // @codeCoverageIgnoreEnd
695
696 6
        $file_args = [$path, false];
697 6
        $mime_args = [$path, FILEINFO_MIME];
698 6
        if (null !== $context) {
699 4
            $file_args[] = $context;
700 4
            $mime_args[] = $context;
701
        }
702
703 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

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

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