PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.0
Defender Security – Malware Scanner, Login Security & Firewall v5.7.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / vendor / league / uri / src / Uri.php

Uri.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.0, at vendor/league/uri/src/Uri.php

1,338 lines 38.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * League.Uri (https://uri.thephpleague.com)
5 *
6 * (c) Ignace Nyamagana Butera <[email protected]>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 declare(strict_types=1);
13
14 namespace League\Uri;
15
16 use finfo;
17 use League\Uri\Contracts\UriInterface;
18 use League\Uri\Exceptions\FileinfoSupportMissing;
19 use League\Uri\Exceptions\IdnaConversionFailed;
20 use League\Uri\Exceptions\IdnSupportMissing;
21 use League\Uri\Exceptions\SyntaxError;
22 use League\Uri\Idna\Idna;
23 use Psr\Http\Message\UriInterface as Psr7UriInterface;
24 use TypeError;
25 use function array_filter;
26 use function array_map;
27 use function base64_decode;
28 use function base64_encode;
29 use function count;
30 use function explode;
31 use function file_get_contents;
32 use function filter_var;
33 use function implode;
34 use function in_array;
35 use function inet_pton;
36 use function is_object;
37 use function is_scalar;
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 strspn;
48 use function strtolower;
49 use function substr;
50 use const FILEINFO_MIME;
51 use const FILTER_FLAG_IPV4;
52 use const FILTER_FLAG_IPV6;
53 use const FILTER_NULL_ON_FAILURE;
54 use const FILTER_VALIDATE_BOOLEAN;
55 use const FILTER_VALIDATE_IP;
56
57 final class Uri implements UriInterface
58 {
59 /**
60 * RFC3986 invalid characters.
61 *
62 * @link https://tools.ietf.org/html/rfc3986#section-2.2
63 *
64 * @var string
65 */
66 private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
67
68 /**
69 * RFC3986 Sub delimiter characters regular expression pattern.
70 *
71 * @link https://tools.ietf.org/html/rfc3986#section-2.2
72 *
73 * @var string
74 */
75 private const REGEXP_CHARS_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
76
77 /**
78 * RFC3986 unreserved characters regular expression pattern.
79 *
80 * @link https://tools.ietf.org/html/rfc3986#section-2.3
81 *
82 * @var string
83 */
84 private const REGEXP_CHARS_UNRESERVED = 'A-Za-z0-9_\-\.~';
85
86 /**
87 * RFC3986 schema regular expression pattern.
88 *
89 * @link https://tools.ietf.org/html/rfc3986#section-3.1
90 */
91 private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i';
92
93 /**
94 * RFC3986 host identified by a registered name regular expression pattern.
95 *
96 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
97 */
98 private const REGEXP_HOST_REGNAME = '/^(
99 (?<unreserved>[a-z0-9_~\-\.])|
100 (?<sub_delims>[!$&\'()*+,;=])|
101 (?<encoded>%[A-F0-9]{2})
102 )+$/x';
103
104 /**
105 * RFC3986 delimiters of the generic URI components regular expression pattern.
106 *
107 * @link https://tools.ietf.org/html/rfc3986#section-2.2
108 */
109 private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space.
110
111 /**
112 * RFC3986 IPvFuture regular expression pattern.
113 *
114 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
115 */
116 private const REGEXP_HOST_IPFUTURE = '/^
117 v(?<version>[A-F0-9])+\.
118 (?:
119 (?<unreserved>[a-z0-9_~\-\.])|
120 (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
121 )+
122 $/ix';
123
124 /**
125 * RFC3986 IPvFuture host and port component.
126 */
127 private const REGEXP_HOST_PORT = ',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x';
128
129 /**
130 * Significant 10 bits of IP to detect Zone ID regular expression pattern.
131 */
132 private const HOST_ADDRESS_BLOCK = "\xfe\x80";
133
134 /**
135 * Regular expression pattern to for file URI.
136 * <volume> contains the volume but not the volume separator.
137 * The volume separator may be URL-encoded (`|` as `%7C`) by ::formatPath(),
138 * so we account for that here.
139 */
140 private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<volume>[a-zA-Z])(?:[:|\|]|%7C)(?<rest>.*)?,';
141
142 /**
143 * Mimetype regular expression pattern.
144 *
145 * @link https://tools.ietf.org/html/rfc2397
146 */
147 private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
148
149 /**
150 * Base64 content regular expression pattern.
151 *
152 * @link https://tools.ietf.org/html/rfc2397
153 */
154 private const REGEXP_BINARY = ',(;|^)base64$,';
155
156 /**
157 * Windows file path string regular expression pattern.
158 * <root> contains both the volume and volume separator.
159 */
160 private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),';
161
162 /**
163 * Supported schemes and corresponding default port.
164 *
165 * @var array<string, int|null>
166 */
167 private const SCHEME_DEFAULT_PORT = [
168 'data' => null,
169 'file' => null,
170 'ftp' => 21,
171 'gopher' => 70,
172 'http' => 80,
173 'https' => 443,
174 'ws' => 80,
175 'wss' => 443,
176 ];
177
178 /**
179 * URI validation methods per scheme.
180 *
181 * @var array<string>
182 */
183 private const SCHEME_VALIDATION_METHOD = [
184 'data' => 'isUriWithSchemeAndPathOnly',
185 'file' => 'isUriWithSchemeHostAndPathOnly',
186 'ftp' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
187 'gopher' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
188 'http' => 'isNonEmptyHostUri',
189 'https' => 'isNonEmptyHostUri',
190 'ws' => 'isNonEmptyHostUriWithoutFragment',
191 'wss' => 'isNonEmptyHostUriWithoutFragment',
192 ];
193
194 /**
195 * All ASCII letters sorted by typical frequency of occurrence.
196 *
197 * @var string
198 */
199 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";
200
201 private ?string $scheme;
202 private ?string $user_info;
203 private ?string $host;
204 private ?int $port;
205 private ?string $authority;
206 private string $path = '';
207 private ?string $query;
208 private ?string $fragment;
209 private ?string $uri;
210
211 private function __construct(
212 ?string $scheme,
213 ?string $user,
214 ?string $pass,
215 ?string $host,
216 ?int $port,
217 string $path,
218 ?string $query,
219 ?string $fragment
220 ) {
221 $this->scheme = $this->formatScheme($scheme);
222 $this->user_info = $this->formatUserInfo($user, $pass);
223 $this->host = $this->formatHost($host);
224 $this->port = $this->formatPort($port);
225 $this->authority = $this->setAuthority();
226 $this->path = $this->formatPath($path);
227 $this->query = $this->formatQueryAndFragment($query);
228 $this->fragment = $this->formatQueryAndFragment($fragment);
229 $this->assertValidState();
230 }
231
232 /**
233 * Format the Scheme and Host component.
234 *
235 * @param ?string $scheme
236 * @throws SyntaxError if the scheme is invalid
237 */
238 private function formatScheme(?string $scheme): ?string
239 {
240 if (null === $scheme) {
241 return $scheme;
242 }
243
244 $formatted_scheme = strtolower($scheme);
245 if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) {
246 return $formatted_scheme;
247 }
248
249 throw new SyntaxError(sprintf('The scheme `%s` is invalid.', $scheme));
250 }
251
252 /**
253 * Set the UserInfo component.
254 * @param ?string $user
255 * @param ?string $password
256 */
257 private function formatUserInfo(?string $user, ?string $password): ?string
258 {
259 if (null === $user) {
260 return $user;
261 }
262
263 static $user_pattern = '/(?:[^%'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
264 $user = preg_replace_callback($user_pattern, [Uri::class, 'urlEncodeMatch'], $user);
265 if (null === $password) {
266 return $user;
267 }
268
269 static $password_pattern = '/(?:[^%:'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
270
271 return $user.':'.preg_replace_callback($password_pattern, [Uri::class, 'urlEncodeMatch'], $password);
272 }
273
274 /**
275 * Returns the RFC3986 encoded string matched.
276 */
277 private static function urlEncodeMatch(array $matches): string
278 {
279 return rawurlencode($matches[0]);
280 }
281
282 /**
283 * Validate and Format the Host component.
284 * @param ?string $host
285 */
286 private function formatHost(?string $host): ?string
287 {
288 if (null === $host || '' === $host) {
289 return $host;
290 }
291
292 if ('[' !== $host[0]) {
293 return $this->formatRegisteredName($host);
294 }
295
296 return $this->formatIp($host);
297 }
298
299 /**
300 * Validate and format a registered name.
301 *
302 * The host is converted to its ascii representation if needed
303 *
304 * @throws IdnSupportMissing if the submitted host required missing or misconfigured IDN support
305 * @throws SyntaxError if the submitted host is not a valid registered name
306 */
307 private function formatRegisteredName(string $host): string
308 {
309 $formatted_host = rawurldecode($host);
310 if (1 === preg_match(self::REGEXP_HOST_REGNAME, $formatted_host)) {
311 return $formatted_host;
312 }
313
314 if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, $formatted_host)) {
315 throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host));
316 }
317
318 $info = Idna::toAscii($host, Idna::IDNA2008_ASCII);
319 if (0 !== $info->errors()) {
320 throw IdnaConversionFailed::dueToIDNAError($host, $info);
321 }
322
323 return $info->result();
324 }
325
326 /**
327 * Validate and Format the IPv6/IPvfuture host.
328 *
329 * @throws SyntaxError if the submitted host is not a valid IP host
330 */
331 private function formatIp(string $host): string
332 {
333 $ip = substr($host, 1, -1);
334 if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
335 return $host;
336 }
337
338 if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
339 return $host;
340 }
341
342 $pos = strpos($ip, '%');
343 if (false === $pos) {
344 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
345 }
346
347 if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) {
348 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
349 }
350
351 $ip = substr($ip, 0, $pos);
352 if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
353 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
354 }
355
356 //Only the address block fe80::/10 can have a Zone ID attach to
357 //let's detect the link local significant 10 bits
358 if (0 === strpos((string) inet_pton($ip), self::HOST_ADDRESS_BLOCK)) {
359 return $host;
360 }
361
362 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
363 }
364
365 /**
366 * Format the Port component.
367 *
368 * @param object|null|int|string $port
369 *
370 * @throws SyntaxError
371 */
372 private function formatPort($port = null): ?int
373 {
374 if (null === $port || '' === $port) {
375 return null;
376 }
377
378 if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) {
379 throw new SyntaxError('The port is expected to be an integer or a string representing an integer; '.gettype($port).' given.');
380 }
381
382 $port = (int) $port;
383 if (0 > $port) {
384 throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
385 }
386
387 $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null;
388 if ($defaultPort === $port) {
389 return null;
390 }
391
392 return $port;
393 }
394
395 /**
396 * {@inheritDoc}
397 */
398 public static function __set_state(array $components): self
399 {
400 $components['user'] = null;
401 $components['pass'] = null;
402 if (null !== $components['user_info']) {
403 [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null];
404 }
405
406 return new self(
407 $components['scheme'],
408 $components['user'],
409 $components['pass'],
410 $components['host'],
411 $components['port'],
412 $components['path'],
413 $components['query'],
414 $components['fragment']
415 );
416 }
417
418 /**
419 * Create a new instance from a URI and a Base URI.
420 *
421 * The returned URI must be absolute.
422 *
423 * @param mixed $uri the input URI to create
424 * @param null|mixed $base_uri the base URI used for reference
425 */
426 public static function createFromBaseUri($uri, $base_uri = null): UriInterface
427 {
428 if (!$uri instanceof UriInterface) {
429 $uri = self::createFromString($uri);
430 }
431
432 if (null === $base_uri) {
433 if (null === $uri->getScheme()) {
434 throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri));
435 }
436
437 if (null === $uri->getAuthority()) {
438 return $uri;
439 }
440
441 /** @var UriInterface $uri */
442 $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath(''));
443
444 return $uri;
445 }
446
447 if (!$base_uri instanceof UriInterface) {
448 $base_uri = self::createFromString($base_uri);
449 }
450
451 if (null === $base_uri->getScheme()) {
452 throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri));
453 }
454
455 /** @var UriInterface $uri */
456 $uri = UriResolver::resolve($uri, $base_uri);
457
458 return $uri;
459 }
460
461 /**
462 * Create a new instance from a string.
463 *
464 * @param string|mixed $uri
465 */
466 public static function createFromString($uri = ''): self
467 {
468 $components = UriString::parse($uri);
469
470 return new self(
471 $components['scheme'],
472 $components['user'],
473 $components['pass'],
474 $components['host'],
475 $components['port'],
476 $components['path'],
477 $components['query'],
478 $components['fragment']
479 );
480 }
481
482 /**
483 * Create a new instance from a hash representation of the URI similar
484 * to PHP parse_url function result.
485 */
486 public static function createFromComponents(array $components = []): self
487 {
488 $components += [
489 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
490 'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
491 ];
492
493 return new self(
494 $components['scheme'],
495 $components['user'],
496 $components['pass'],
497 $components['host'],
498 $components['port'],
499 $components['path'],
500 $components['query'],
501 $components['fragment']
502 );
503 }
504
505 /**
506 * Create a new instance from a data file path.
507 *
508 * @param resource|null $context
509 *
510 * @throws FileinfoSupportMissing If ext/fileinfo is not installed
511 * @throws SyntaxError If the file does not exist or is not readable
512 */
513 public static function createFromDataPath(string $path, $context = null): self
514 {
515 static $finfo_support = null;
516 $finfo_support = $finfo_support ?? class_exists(finfo::class);
517
518 // @codeCoverageIgnoreStart
519 if (!$finfo_support) {
520 throw new FileinfoSupportMissing(sprintf('Please install ext/fileinfo to use the %s() method.', __METHOD__));
521 }
522 // @codeCoverageIgnoreEnd
523
524 $file_args = [$path, false];
525 $mime_args = [$path, FILEINFO_MIME];
526 if (null !== $context) {
527 $file_args[] = $context;
528 $mime_args[] = $context;
529 }
530
531 $raw = @file_get_contents(...$file_args);
532 if (false === $raw) {
533 throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
534 }
535
536 $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mime_args);
537
538 return Uri::createFromComponents([
539 'scheme' => 'data',
540 'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)),
541 ]);
542 }
543
544 /**
545 * Create a new instance from a Unix path string.
546 */
547 public static function createFromUnixPath(string $uri = ''): self
548 {
549 $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
550 if ('/' !== ($uri[0] ?? '')) {
551 return Uri::createFromComponents(['path' => $uri]);
552 }
553
554 return Uri::createFromComponents(['path' => $uri, 'scheme' => 'file', 'host' => '']);
555 }
556
557 /**
558 * Create a new instance from a local Windows path string.
559 */
560 public static function createFromWindowsPath(string $uri = ''): self
561 {
562 $root = '';
563 if (1 === preg_match(self::REGEXP_WINDOW_PATH, $uri, $matches)) {
564 $root = substr($matches['root'], 0, -1).':';
565 $uri = substr($uri, strlen($root));
566 }
567 $uri = str_replace('\\', '/', $uri);
568 $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
569
570 //Local Windows absolute path
571 if ('' !== $root) {
572 return Uri::createFromComponents(['path' => '/'.$root.$uri, 'scheme' => 'file', 'host' => '']);
573 }
574
575 //UNC Windows Path
576 if ('//' !== substr($uri, 0, 2)) {
577 return Uri::createFromComponents(['path' => $uri]);
578 }
579
580 $parts = explode('/', substr($uri, 2), 2) + [1 => null];
581
582 return Uri::createFromComponents(['host' => $parts[0], 'path' => '/'.$parts[1], 'scheme' => 'file']);
583 }
584
585 /**
586 * Create a new instance from a URI object.
587 *
588 * @param Psr7UriInterface|UriInterface $uri the input URI to create
589 */
590 public static function createFromUri($uri): self
591 {
592 if ($uri instanceof UriInterface) {
593 $user_info = $uri->getUserInfo();
594 $user = null;
595 $pass = null;
596 if (null !== $user_info) {
597 [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
598 }
599
600 return new self(
601 $uri->getScheme(),
602 $user,
603 $pass,
604 $uri->getHost(),
605 $uri->getPort(),
606 $uri->getPath(),
607 $uri->getQuery(),
608 $uri->getFragment()
609 );
610 }
611
612 if (!$uri instanceof Psr7UriInterface) {
613 throw new TypeError(sprintf('The object must implement the `%s` or the `%s`', Psr7UriInterface::class, UriInterface::class));
614 }
615
616 $scheme = $uri->getScheme();
617 if ('' === $scheme) {
618 $scheme = null;
619 }
620
621 $fragment = $uri->getFragment();
622 if ('' === $fragment) {
623 $fragment = null;
624 }
625
626 $query = $uri->getQuery();
627 if ('' === $query) {
628 $query = null;
629 }
630
631 $host = $uri->getHost();
632 if ('' === $host) {
633 $host = null;
634 }
635
636 $user_info = $uri->getUserInfo();
637 $user = null;
638 $pass = null;
639 if ('' !== $user_info) {
640 [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
641 }
642
643 return new self(
644 $scheme,
645 $user,
646 $pass,
647 $host,
648 $uri->getPort(),
649 $uri->getPath(),
650 $query,
651 $fragment
652 );
653 }
654
655 /**
656 * Create a new instance from the environment.
657 */
658 public static function createFromServer(array $server): self
659 {
660 [$user, $pass] = self::fetchUserInfo($server);
661 [$host, $port] = self::fetchHostname($server);
662 [$path, $query] = self::fetchRequestUri($server);
663
664 return Uri::createFromComponents([
665 'scheme' => self::fetchScheme($server),
666 'user' => $user,
667 'pass' => $pass,
668 'host' => $host,
669 'port' => $port,
670 'path' => $path,
671 'query' => $query,
672 ]);
673 }
674
675 /**
676 * Returns the environment scheme.
677 */
678 private static function fetchScheme(array $server): string
679 {
680 $server += ['HTTPS' => ''];
681 $res = filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
682
683 return false !== $res ? 'https' : 'http';
684 }
685
686 /**
687 * Returns the environment user info.
688 *
689 * @return array{0:?string, 1:?string}
690 */
691 private static function fetchUserInfo(array $server): array
692 {
693 $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => ''];
694 $user = $server['PHP_AUTH_USER'];
695 $pass = $server['PHP_AUTH_PW'];
696 if (0 === strpos(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) {
697 $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true);
698 if (false === $userinfo) {
699 throw new SyntaxError('The user info could not be detected');
700 }
701 [$user, $pass] = explode(':', $userinfo, 2) + [1 => null];
702 }
703
704 if (null !== $user) {
705 $user = rawurlencode($user);
706 }
707
708 if (null !== $pass) {
709 $pass = rawurlencode($pass);
710 }
711
712 return [$user, $pass];
713 }
714
715 /**
716 * Returns the environment host.
717 *
718 * @throws SyntaxError If the host can not be detected
719 *
720 * @return array{0:string|null, 1:int|null}
721 */
722 private static function fetchHostname(array $server): array
723 {
724 $server += ['SERVER_PORT' => null];
725 if (null !== $server['SERVER_PORT']) {
726 $server['SERVER_PORT'] = (int) $server['SERVER_PORT'];
727 }
728
729 if (isset($server['HTTP_HOST']) && 1 === preg_match(self::REGEXP_HOST_PORT, $server['HTTP_HOST'], $matches)) {
730 if (isset($matches['port'])) {
731 $matches['port'] = (int) $matches['port'];
732 }
733
734 return [
735 $matches['host'],
736 $matches['port'] ?? $server['SERVER_PORT'],
737 ];
738 }
739
740 if (!isset($server['SERVER_ADDR'])) {
741 throw new SyntaxError('The host could not be detected');
742 }
743
744 if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
745 $server['SERVER_ADDR'] = '['.$server['SERVER_ADDR'].']';
746 }
747
748 return [$server['SERVER_ADDR'], $server['SERVER_PORT']];
749 }
750
751 /**
752 * Returns the environment path.
753 *
754 * @return array{0:?string, 1:?string}
755 */
756 private static function fetchRequestUri(array $server): array
757 {
758 $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null];
759 if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) {
760 /** @var array{0:?string, 1:?string} $retval */
761 $retval = explode('?', $server['UNENCODED_URL'], 2) + [1 => null];
762
763 return $retval;
764 }
765
766 if (isset($server['REQUEST_URI'])) {
767 [$path, ] = explode('?', $server['REQUEST_URI'], 2);
768 $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null;
769
770 return [$path, $query];
771 }
772
773 return [$server['PHP_SELF'], $server['QUERY_STRING']];
774 }
775
776 /**
777 * Generate the URI authority part.
778 */
779 private function setAuthority(): ?string
780 {
781 $authority = null;
782 if (null !== $this->user_info) {
783 $authority = $this->user_info.'@';
784 }
785
786 if (null !== $this->host) {
787 $authority .= $this->host;
788 }
789
790 if (null !== $this->port) {
791 $authority .= ':'.$this->port;
792 }
793
794 return $authority;
795 }
796
797 /**
798 * Format the Path component.
799 */
800 private function formatPath(string $path): string
801 {
802 $path = $this->formatDataPath($path);
803
804 static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/}{]++|%(?![A-Fa-f0-9]{2}))/';
805
806 $path = (string) preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $path);
807
808 return $this->formatFilePath($path);
809 }
810
811 /**
812 * Filter the Path component.
813 *
814 * @link https://tools.ietf.org/html/rfc2397
815 *
816 * @throws SyntaxError If the path is not compliant with RFC2397
817 */
818 private function formatDataPath(string $path): string
819 {
820 if ('data' !== $this->scheme) {
821 return $path;
822 }
823
824 if ('' == $path) {
825 return 'text/plain;charset=us-ascii,';
826 }
827
828 if (strlen($path) !== strspn($path, self::ASCII) || false === strpos($path, ',')) {
829 throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937', $path));
830 }
831
832 $parts = explode(',', $path, 2) + [1 => null];
833 $mediatype = explode(';', (string) $parts[0], 2) + [1 => null];
834 $data = (string) $parts[1];
835 $mimetype = $mediatype[0];
836 if (null === $mimetype || '' === $mimetype) {
837 $mimetype = 'text/plain';
838 }
839
840 $parameters = $mediatype[1];
841 if (null === $parameters || '' === $parameters) {
842 $parameters = 'charset=us-ascii';
843 }
844
845 $this->assertValidPath($mimetype, $parameters, $data);
846
847 return $mimetype.';'.$parameters.','.$data;
848 }
849
850 /**
851 * Assert the path is a compliant with RFC2397.
852 *
853 * @link https://tools.ietf.org/html/rfc2397
854 *
855 * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
856 */
857 private function assertValidPath(string $mimetype, string $parameters, string $data): void
858 {
859 if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
860 throw new SyntaxError(sprintf('The path mimetype `%s` is invalid', $mimetype));
861 }
862
863 $is_binary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches);
864 if ($is_binary) {
865 $parameters = substr($parameters, 0, - strlen($matches[0]));
866 }
867
868 $res = array_filter(array_filter(explode(';', $parameters), [$this, 'validateParameter']));
869 if ([] !== $res) {
870 throw new SyntaxError(sprintf('The path paremeters `%s` is invalid', $parameters));
871 }
872
873 if (!$is_binary) {
874 return;
875 }
876
877 $res = base64_decode($data, true);
878 if (false === $res || $data !== base64_encode($res)) {
879 throw new SyntaxError(sprintf('The path data `%s` is invalid', $data));
880 }
881 }
882
883 /**
884 * Validate mediatype parameter.
885 */
886 private function validateParameter(string $parameter): bool
887 {
888 $properties = explode('=', $parameter);
889
890 return 2 != count($properties) || 'base64' === strtolower($properties[0]);
891 }
892
893 /**
894 * Format path component for file scheme.
895 */
896 private function formatFilePath(string $path): string
897 {
898 if ('file' !== $this->scheme) {
899 return $path;
900 }
901
902 $replace = static function (array $matches): string {
903 return $matches['delim'].$matches['volume'].':'.$matches['rest'];
904 };
905
906 return (string) preg_replace_callback(self::REGEXP_FILE_PATH, $replace, $path);
907 }
908
909 /**
910 * Format the Query or the Fragment component.
911 *
912 * Returns a array containing:
913 * <ul>
914 * <li> the formatted component (a string or null)</li>
915 * <li> a boolean flag telling wether the delimiter is to be added to the component
916 * when building the URI string representation</li>
917 * </ul>
918 *
919 * @param ?string $component
920 */
921 private function formatQueryAndFragment(?string $component): ?string
922 {
923 if (null === $component || '' === $component) {
924 return $component;
925 }
926
927 static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/';
928 return preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $component);
929 }
930
931 /**
932 * assert the URI internal state is valid.
933 *
934 * @link https://tools.ietf.org/html/rfc3986#section-3
935 * @link https://tools.ietf.org/html/rfc3986#section-3.3
936 *
937 * @throws SyntaxError if the URI is in an invalid state according to RFC3986
938 * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules
939 */
940 private function assertValidState(): void
941 {
942 if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) {
943 throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.');
944 }
945
946 if (null === $this->authority && 0 === strpos($this->path, '//')) {
947 throw new SyntaxError(sprintf('If there is no authority the path `%s` can not start with a `//`.', $this->path));
948 }
949
950 $pos = strpos($this->path, ':');
951 if (null === $this->authority
952 && null === $this->scheme
953 && false !== $pos
954 && false === strpos(substr($this->path, 0, $pos), '/')
955 ) {
956 throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
957 }
958
959 $validationMethod = self::SCHEME_VALIDATION_METHOD[$this->scheme] ?? null;
960 if (null === $validationMethod || true === $this->$validationMethod()) {
961 $this->uri = null;
962
963 return;
964 }
965
966 throw new SyntaxError(sprintf('The uri `%s` is invalid for the `%s` scheme.', (string) $this, $this->scheme));
967 }
968
969 /**
970 * URI validation for URI schemes which allows only scheme and path components.
971 */
972 private function isUriWithSchemeAndPathOnly(): bool
973 {
974 return null === $this->authority
975 && null === $this->query
976 && null === $this->fragment;
977 }
978
979 /**
980 * URI validation for URI schemes which allows only scheme, host and path components.
981 */
982 private function isUriWithSchemeHostAndPathOnly(): bool
983 {
984 return null === $this->user_info
985 && null === $this->port
986 && null === $this->query
987 && null === $this->fragment
988 && !('' != $this->scheme && null === $this->host);
989 }
990
991 /**
992 * URI validation for URI schemes which disallow the empty '' host.
993 */
994 private function isNonEmptyHostUri(): bool
995 {
996 return '' !== $this->host
997 && !(null !== $this->scheme && null === $this->host);
998 }
999
1000 /**
1001 * URI validation for URIs schemes which disallow the empty '' host
1002 * and forbids the fragment component.
1003 */
1004 private function isNonEmptyHostUriWithoutFragment(): bool
1005 {
1006 return $this->isNonEmptyHostUri() && null === $this->fragment;
1007 }
1008
1009 /**
1010 * URI validation for URIs schemes which disallow the empty '' host
1011 * and forbids fragment and query components.
1012 */
1013 private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool
1014 {
1015 return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
1016 }
1017
1018 /**
1019 * Generate the URI string representation from its components.
1020 *
1021 * @link https://tools.ietf.org/html/rfc3986#section-5.3
1022 *
1023 * @param ?string $scheme
1024 * @param ?string $authority
1025 * @param ?string $query
1026 * @param ?string $fragment
1027 */
1028 private function getUriString(
1029 ?string $scheme,
1030 ?string $authority,
1031 string $path,
1032 ?string $query,
1033 ?string $fragment
1034 ): string {
1035 if (null !== $scheme) {
1036 $scheme = $scheme.':';
1037 }
1038
1039 if (null !== $authority) {
1040 $authority = '//'.$authority;
1041 }
1042
1043 if (null !== $query) {
1044 $query = '?'.$query;
1045 }
1046
1047 if (null !== $fragment) {
1048 $fragment = '#'.$fragment;
1049 }
1050
1051 return $scheme.$authority.$path.$query.$fragment;
1052 }
1053
1054 public function toString(): string
1055 {
1056 $this->uri = $this->uri ?? $this->getUriString(
1057 $this->scheme,
1058 $this->authority,
1059 $this->path,
1060 $this->query,
1061 $this->fragment
1062 );
1063
1064 return $this->uri;
1065 }
1066
1067 /**
1068 * {@inheritDoc}
1069 */
1070 public function __toString(): string
1071 {
1072 return $this->toString();
1073 }
1074
1075 /**
1076 * {@inheritDoc}
1077 */
1078 public function jsonSerialize(): string
1079 {
1080 return $this->toString();
1081 }
1082
1083 /**
1084 * {@inheritDoc}
1085 *
1086 * @return array{scheme:?string, user_info:?string, host:?string, port:?int, path:string, query:?string, fragment:?string}
1087 */
1088 public function __debugInfo(): array
1089 {
1090 return [
1091 'scheme' => $this->scheme,
1092 'user_info' => isset($this->user_info) ? preg_replace(',:(.*).?$,', ':***', $this->user_info) : null,
1093 'host' => $this->host,
1094 'port' => $this->port,
1095 'path' => $this->path,
1096 'query' => $this->query,
1097 'fragment' => $this->fragment,
1098 ];
1099 }
1100
1101 /**
1102 * {@inheritDoc}
1103 */
1104 public function getScheme(): ?string
1105 {
1106 return $this->scheme;
1107 }
1108
1109 /**
1110 * {@inheritDoc}
1111 */
1112 public function getAuthority(): ?string
1113 {
1114 return $this->authority;
1115 }
1116
1117 /**
1118 * {@inheritDoc}
1119 */
1120 public function getUserInfo(): ?string
1121 {
1122 return $this->user_info;
1123 }
1124
1125 /**
1126 * {@inheritDoc}
1127 */
1128 public function getHost(): ?string
1129 {
1130 return $this->host;
1131 }
1132
1133 /**
1134 * {@inheritDoc}
1135 */
1136 public function getPort(): ?int
1137 {
1138 return $this->port;
1139 }
1140
1141 /**
1142 * {@inheritDoc}
1143 */
1144 public function getPath(): string
1145 {
1146 if (0 === strpos($this->path, '//')) {
1147 return '/'.ltrim($this->path, '/');
1148 }
1149
1150 return $this->path;
1151 }
1152
1153 /**
1154 * {@inheritDoc}
1155 */
1156 public function getQuery(): ?string
1157 {
1158 return $this->query;
1159 }
1160
1161 /**
1162 * {@inheritDoc}
1163 */
1164 public function getFragment(): ?string
1165 {
1166 return $this->fragment;
1167 }
1168
1169 /**
1170 * {@inheritDoc}
1171 */
1172 public function withScheme($scheme): UriInterface
1173 {
1174 $scheme = $this->formatScheme($this->filterString($scheme));
1175 if ($scheme === $this->scheme) {
1176 return $this;
1177 }
1178
1179 $clone = clone $this;
1180 $clone->scheme = $scheme;
1181 $clone->port = $clone->formatPort($clone->port);
1182 $clone->authority = $clone->setAuthority();
1183 $clone->assertValidState();
1184
1185 return $clone;
1186 }
1187
1188 /**
1189 * Filter a string.
1190 *
1191 * @param mixed $str the value to evaluate as a string
1192 *
1193 * @throws SyntaxError if the submitted data can not be converted to string
1194 */
1195 private function filterString($str): ?string
1196 {
1197 if (null === $str) {
1198 return $str;
1199 }
1200
1201 if (is_object($str) && method_exists($str, '__toString')) {
1202 $str = (string) $str;
1203 }
1204
1205 if (!is_scalar($str)) {
1206 throw new SyntaxError(sprintf('The component must be a string, a scalar or a stringable object; `%s` given.', gettype($str)));
1207 }
1208
1209 $str = (string) $str;
1210 if (1 !== preg_match(self::REGEXP_INVALID_CHARS, $str)) {
1211 return $str;
1212 }
1213
1214 throw new SyntaxError(sprintf('The component `%s` contains invalid characters.', $str));
1215 }
1216
1217 /**
1218 * {@inheritDoc}
1219 */
1220 public function withUserInfo($user, $password = null): UriInterface
1221 {
1222 $user_info = null;
1223 $user = $this->filterString($user);
1224 if (null !== $password) {
1225 $password = $this->filterString($password);
1226 }
1227
1228 if ('' !== $user) {
1229 $user_info = $this->formatUserInfo($user, $password);
1230 }
1231
1232 if ($user_info === $this->user_info) {
1233 return $this;
1234 }
1235
1236 $clone = clone $this;
1237 $clone->user_info = $user_info;
1238 $clone->authority = $clone->setAuthority();
1239 $clone->assertValidState();
1240
1241 return $clone;
1242 }
1243
1244 /**
1245 * {@inheritDoc}
1246 */
1247 public function withHost($host): UriInterface
1248 {
1249 $host = $this->formatHost($this->filterString($host));
1250 if ($host === $this->host) {
1251 return $this;
1252 }
1253
1254 $clone = clone $this;
1255 $clone->host = $host;
1256 $clone->authority = $clone->setAuthority();
1257 $clone->assertValidState();
1258
1259 return $clone;
1260 }
1261
1262 /**
1263 * {@inheritDoc}
1264 */
1265 public function withPort($port): UriInterface
1266 {
1267 $port = $this->formatPort($port);
1268 if ($port === $this->port) {
1269 return $this;
1270 }
1271
1272 $clone = clone $this;
1273 $clone->port = $port;
1274 $clone->authority = $clone->setAuthority();
1275 $clone->assertValidState();
1276
1277 return $clone;
1278 }
1279
1280 /**
1281 * {@inheritDoc}
1282 *
1283 * @param string|object $path
1284 */
1285 public function withPath($path): UriInterface
1286 {
1287 $path = $this->filterString($path);
1288 if (null === $path) {
1289 throw new TypeError('A path must be a string NULL given.');
1290 }
1291
1292 $path = $this->formatPath($path);
1293 if ($path === $this->path) {
1294 return $this;
1295 }
1296
1297 $clone = clone $this;
1298 $clone->path = $path;
1299 $clone->assertValidState();
1300
1301 return $clone;
1302 }
1303
1304 /**
1305 * {@inheritDoc}
1306 */
1307 public function withQuery($query): UriInterface
1308 {
1309 $query = $this->formatQueryAndFragment($this->filterString($query));
1310 if ($query === $this->query) {
1311 return $this;
1312 }
1313
1314 $clone = clone $this;
1315 $clone->query = $query;
1316 $clone->assertValidState();
1317
1318 return $clone;
1319 }
1320
1321 /**
1322 * {@inheritDoc}
1323 */
1324 public function withFragment($fragment): UriInterface
1325 {
1326 $fragment = $this->formatQueryAndFragment($this->filterString($fragment));
1327 if ($fragment === $this->fragment) {
1328 return $this;
1329 }
1330
1331 $clone = clone $this;
1332 $clone->fragment = $fragment;
1333 $clone->assertValidState();
1334
1335 return $clone;
1336 }
1337 }
1338