Issues (6)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Uri.php (4 issues)

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

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

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