Completed
Pull Request — master (#127)
by ignace nyamagana
02:03
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 0
Metric Value
cc 1
eloc 9
nc 1
nop 8
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 1
rs 9.9666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

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