Completed
Pull Request — master (#154)
by
unknown
64:29 queued 62:58
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
        if (!class_exists(\finfo::class)) {
688
            throw new FileinfoSupportMissing(sprintf('Please install ext/fileinfo to use the %s() method.', __METHOD__));
689
        }
690
691 6
        $file_args = [$path, false];
692 6
        $mime_args = [$path, FILEINFO_MIME];
693 6
        if (null !== $context) {
694 4
            $file_args[] = $context;
695 4
            $mime_args[] = $context;
696
        }
697
698 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

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

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