at path:ROOT / clients / vendor / league / uri / src / Uri.php
run:R W Run
DIR
2026-04-08 19:44:12
R W Run
DIR
2026-04-08 19:44:14
R W Run
7.53 KB
2026-04-08 19:36:18
R W Run
552 By
2026-04-08 19:36:18
R W Run
38.07 KB
2026-04-08 19:36:19
R W Run
6.58 KB
2026-04-08 19:36:19
R W Run
10.87 KB
2026-04-08 19:36:19
R W Run
15.02 KB
2026-04-08 19:36:18
R W Run
3.98 KB
2026-04-08 19:36:18
R W Run
error_log
📄Uri.php
1<?php
2
3/**
4 * League.Uri (https://uri.thephpleague.com)
5 *
6 * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12declare(strict_types=1);
13
14namespace League\Uri;
15
16use finfo;
17use League\Uri\Contracts\UriInterface;
18use League\Uri\Exceptions\FileinfoSupportMissing;
19use League\Uri\Exceptions\IdnaConversionFailed;
20use League\Uri\Exceptions\IdnSupportMissing;
21use League\Uri\Exceptions\SyntaxError;
22use League\Uri\Idna\Idna;
23use Psr\Http\Message\UriInterface as Psr7UriInterface;
24use TypeError;
25use function array_filter;
26use function array_map;
27use function base64_decode;
28use function base64_encode;
29use function count;
30use function explode;
31use function file_get_contents;
32use function filter_var;
33use function implode;
34use function in_array;
35use function inet_pton;
36use function is_object;
37use function is_scalar;
38use function method_exists;
39use function preg_match;
40use function preg_replace;
41use function preg_replace_callback;
42use function rawurlencode;
43use function sprintf;
44use function str_replace;
45use function strlen;
46use function strpos;
47use function strspn;
48use function strtolower;
49use function substr;
50use const FILEINFO_MIME;
51use const FILTER_FLAG_IPV4;
52use const FILTER_FLAG_IPV6;
53use const FILTER_NULL_ON_FAILURE;
54use const FILTER_VALIDATE_BOOLEAN;
55use const FILTER_VALIDATE_IP;
56
57final 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 return $this->path;
1147 }
1148
1149 /**
1150 * {@inheritDoc}
1151 */
1152 public function getQuery(): ?string
1153 {
1154 return $this->query;
1155 }
1156
1157 /**
1158 * {@inheritDoc}
1159 */
1160 public function getFragment(): ?string
1161 {
1162 return $this->fragment;
1163 }
1164
1165 /**
1166 * {@inheritDoc}
1167 */
1168 public function withScheme($scheme): UriInterface
1169 {
1170 $scheme = $this->formatScheme($this->filterString($scheme));
1171 if ($scheme === $this->scheme) {
1172 return $this;
1173 }
1174
1175 $clone = clone $this;
1176 $clone->scheme = $scheme;
1177 $clone->port = $clone->formatPort($clone->port);
1178 $clone->authority = $clone->setAuthority();
1179 $clone->assertValidState();
1180
1181 return $clone;
1182 }
1183
1184 /**
1185 * Filter a string.
1186 *
1187 * @param mixed $str the value to evaluate as a string
1188 *
1189 * @throws SyntaxError if the submitted data can not be converted to string
1190 */
1191 private function filterString($str): ?string
1192 {
1193 if (null === $str) {
1194 return $str;
1195 }
1196
1197 if (is_object($str) && method_exists($str, '__toString')) {
1198 $str = (string) $str;
1199 }
1200
1201 if (!is_scalar($str)) {
1202 throw new SyntaxError(sprintf('The component must be a string, a scalar or a stringable object; `%s` given.', gettype($str)));
1203 }
1204
1205 $str = (string) $str;
1206 if (1 !== preg_match(self::REGEXP_INVALID_CHARS, $str)) {
1207 return $str;
1208 }
1209
1210 throw new SyntaxError(sprintf('The component `%s` contains invalid characters.', $str));
1211 }
1212
1213 /**
1214 * {@inheritDoc}
1215 */
1216 public function withUserInfo($user, $password = null): UriInterface
1217 {
1218 $user_info = null;
1219 $user = $this->filterString($user);
1220 if (null !== $password) {
1221 $password = $this->filterString($password);
1222 }
1223
1224 if ('' !== $user) {
1225 $user_info = $this->formatUserInfo($user, $password);
1226 }
1227
1228 if ($user_info === $this->user_info) {
1229 return $this;
1230 }
1231
1232 $clone = clone $this;
1233 $clone->user_info = $user_info;
1234 $clone->authority = $clone->setAuthority();
1235 $clone->assertValidState();
1236
1237 return $clone;
1238 }
1239
1240 /**
1241 * {@inheritDoc}
1242 */
1243 public function withHost($host): UriInterface
1244 {
1245 $host = $this->formatHost($this->filterString($host));
1246 if ($host === $this->host) {
1247 return $this;
1248 }
1249
1250 $clone = clone $this;
1251 $clone->host = $host;
1252 $clone->authority = $clone->setAuthority();
1253 $clone->assertValidState();
1254
1255 return $clone;
1256 }
1257
1258 /**
1259 * {@inheritDoc}
1260 */
1261 public function withPort($port): UriInterface
1262 {
1263 $port = $this->formatPort($port);
1264 if ($port === $this->port) {
1265 return $this;
1266 }
1267
1268 $clone = clone $this;
1269 $clone->port = $port;
1270 $clone->authority = $clone->setAuthority();
1271 $clone->assertValidState();
1272
1273 return $clone;
1274 }
1275
1276 /**
1277 * {@inheritDoc}
1278 *
1279 * @param string|object $path
1280 */
1281 public function withPath($path): UriInterface
1282 {
1283 $path = $this->filterString($path);
1284 if (null === $path) {
1285 throw new TypeError('A path must be a string NULL given.');
1286 }
1287
1288 $path = $this->formatPath($path);
1289 if ($path === $this->path) {
1290 return $this;
1291 }
1292
1293 $clone = clone $this;
1294 $clone->path = $path;
1295 $clone->assertValidState();
1296
1297 return $clone;
1298 }
1299
1300 /**
1301 * {@inheritDoc}
1302 */
1303 public function withQuery($query): UriInterface
1304 {
1305 $query = $this->formatQueryAndFragment($this->filterString($query));
1306 if ($query === $this->query) {
1307 return $this;
1308 }
1309
1310 $clone = clone $this;
1311 $clone->query = $query;
1312 $clone->assertValidState();
1313
1314 return $clone;
1315 }
1316
1317 /**
1318 * {@inheritDoc}
1319 */
1320 public function withFragment($fragment): UriInterface
1321 {
1322 $fragment = $this->formatQueryAndFragment($this->filterString($fragment));
1323 if ($fragment === $this->fragment) {
1324 return $this;
1325 }
1326
1327 $clone = clone $this;
1328 $clone->fragment = $fragment;
1329 $clone->assertValidState();
1330
1331 return $clone;
1332 }
1333}
1334