PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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 3.1.0, at vendor/league/uri/src/Uri.php

1,502 lines 43.5 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 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 * <volume> contains the volume but not the volume separator.
149 * The volume separator may be URL-encoded (`|` as `%7C`) by ::formatPath(),
150 * so we account for that here.
151 */
152 private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<volume>[a-zA-Z])(?:[:|\|]|%7C)(?<rest>.*)?,';
153
154 /**
155 * Mimetype regular expression pattern.
156 *
157 * @link https://tools.ietf.org/html/rfc2397
158 */
159 private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
160
161 /**
162 * Base64 content regular expression pattern.
163 *
164 * @link https://tools.ietf.org/html/rfc2397
165 */
166 private const REGEXP_BINARY = ',(;|^)base64$,';
167
168 /**
169 * Windows file path string regular expression pattern.
170 * <root> contains both the volume and volume separator.
171 */
172 private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),';
173
174
175 /**
176 * Supported schemes and corresponding default port.
177 *
178 * @var array
179 */
180 private const SCHEME_DEFAULT_PORT = [
181 'data' => null,
182 'file' => null,
183 'ftp' => 21,
184 'gopher' => 70,
185 'http' => 80,
186 'https' => 443,
187 'ws' => 80,
188 'wss' => 443,
189 ];
190
191 /**
192 * URI validation methods per scheme.
193 *
194 * @var array
195 */
196 private const SCHEME_VALIDATION_METHOD = [
197 'data' => 'isUriWithSchemeAndPathOnly',
198 'file' => 'isUriWithSchemeHostAndPathOnly',
199 'ftp' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
200 'gopher' => 'isNonEmptyHostUriWithoutFragmentAndQuery',
201 'http' => 'isNonEmptyHostUri',
202 'https' => 'isNonEmptyHostUri',
203 'ws' => 'isNonEmptyHostUriWithoutFragment',
204 'wss' => 'isNonEmptyHostUriWithoutFragment',
205 ];
206
207 /**
208 * All ASCII letters sorted by typical frequency of occurrence.
209 *
210 * @var string
211 */
212 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";
213
214 /**
215 * URI scheme component.
216 *
217 * @var string|null
218 */
219 private $scheme;
220
221 /**
222 * URI user info part.
223 *
224 * @var string|null
225 */
226 private $user_info;
227
228 /**
229 * URI host component.
230 *
231 * @var string|null
232 */
233 private $host;
234
235 /**
236 * URI port component.
237 *
238 * @var int|null
239 */
240 private $port;
241
242 /**
243 * URI authority string representation.
244 *
245 * @var string|null
246 */
247 private $authority;
248
249 /**
250 * URI path component.
251 *
252 * @var string
253 */
254 private $path = '';
255
256 /**
257 * URI query component.
258 *
259 * @var string|null
260 */
261 private $query;
262
263 /**
264 * URI fragment component.
265 *
266 * @var string|null
267 */
268 private $fragment;
269
270 /**
271 * URI string representation.
272 *
273 * @var string|null
274 */
275 private $uri;
276
277 /**
278 * Create a new instance.
279 *
280 * @param ?string $scheme
281 * @param ?string $user
282 * @param ?string $pass
283 * @param ?string $host
284 * @param ?int $port
285 * @param ?string $query
286 * @param ?string $fragment
287 */
288 private function __construct(
289 ?string $scheme,
290 ?string $user,
291 ?string $pass,
292 ?string $host,
293 ?int $port,
294 string $path,
295 ?string $query,
296 ?string $fragment
297 ) {
298 $this->scheme = $this->formatScheme($scheme);
299 $this->user_info = $this->formatUserInfo($user, $pass);
300 $this->host = $this->formatHost($host);
301 $this->port = $this->formatPort($port);
302 $this->authority = $this->setAuthority();
303 $this->path = $this->formatPath($path);
304 $this->query = $this->formatQueryAndFragment($query);
305 $this->fragment = $this->formatQueryAndFragment($fragment);
306 $this->assertValidState();
307 }
308
309 /**
310 * Format the Scheme and Host component.
311 *
312 * @param ?string $scheme
313 *
314 * @throws SyntaxError if the scheme is invalid
315 */
316 private function formatScheme(?string $scheme): ?string
317 {
318 if (null === $scheme) {
319 return $scheme;
320 }
321
322 $formatted_scheme = strtolower($scheme);
323 if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) {
324 return $formatted_scheme;
325 }
326
327 throw new SyntaxError(sprintf('The scheme `%s` is invalid.', $scheme));
328 }
329
330 /**
331 * Set the UserInfo component.
332 *
333 * @param ?string $user
334 * @param ?string $password
335 */
336 private function formatUserInfo(?string $user, ?string $password): ?string
337 {
338 if (null === $user) {
339 return $user;
340 }
341
342 static $user_pattern = '/(?:[^%'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
343 $user = preg_replace_callback($user_pattern, [Uri::class, 'urlEncodeMatch'], $user);
344 if (null === $password) {
345 return $user;
346 }
347
348 static $password_pattern = '/(?:[^%:'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/';
349
350 return $user.':'.preg_replace_callback($password_pattern, [Uri::class, 'urlEncodeMatch'], $password);
351 }
352
353 /**
354 * Returns the RFC3986 encoded string matched.
355 */
356 private static function urlEncodeMatch(array $matches): string
357 {
358 return rawurlencode($matches[0]);
359 }
360
361 /**
362 * Validate and Format the Host component.
363 *
364 * @param ?string $host
365 */
366 private function formatHost(?string $host): ?string
367 {
368 if (null === $host || '' === $host) {
369 return $host;
370 }
371
372 if ('[' !== $host[0]) {
373 return $this->formatRegisteredName($host);
374 }
375
376 return $this->formatIp($host);
377 }
378
379 /**
380 * Validate and format a registered name.
381 *
382 * The host is converted to its ascii representation if needed
383 *
384 * @throws IdnSupportMissing if the submitted host required missing or misconfigured IDN support
385 * @throws SyntaxError if the submitted host is not a valid registered name
386 */
387 private function formatRegisteredName(string $host): string
388 {
389 // @codeCoverageIgnoreStart
390 // added because it is not possible in travis to disabled the ext/intl extension
391 // see travis issue https://github.com/travis-ci/travis-ci/issues/4701
392 static $idn_support = null;
393 $idn_support = $idn_support ?? function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46');
394 // @codeCoverageIgnoreEnd
395
396 $formatted_host = rawurldecode($host);
397 if (1 === preg_match(self::REGEXP_HOST_REGNAME, $formatted_host)) {
398 $formatted_host = strtolower($formatted_host);
399 if (false === strpos($formatted_host, 'xn--')) {
400 return $formatted_host;
401 }
402
403 // @codeCoverageIgnoreStart
404 if (!$idn_support) {
405 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));
406 }
407 // @codeCoverageIgnoreEnd
408
409 $unicode = idn_to_utf8(
410 $host,
411 IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_UNICODE,
412 INTL_IDNA_VARIANT_UTS46,
413 $arr
414 );
415
416 if (0 !== $arr['errors']) {
417 throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
418 }
419
420 // @codeCoverageIgnoreStart
421 if (false === $unicode) {
422 throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
423 }
424 // @codeCoverageIgnoreEnd
425
426 return $formatted_host;
427 }
428
429 if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, $formatted_host)) {
430 throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host));
431 }
432
433 // @codeCoverageIgnoreStart
434 if (!$idn_support) {
435 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));
436 }
437 // @codeCoverageIgnoreEnd
438
439 $formatted_host = idn_to_ascii(
440 $formatted_host,
441 IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_ASCII,
442 INTL_IDNA_VARIANT_UTS46,
443 $arr
444 );
445
446 if ([] === $arr) {
447 throw new SyntaxError(sprintf('Host `%s` is invalid', $host));
448 }
449
450 if (0 !== $arr['errors']) {
451 throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors'])));
452 }
453
454 // @codeCoverageIgnoreStart
455 if (false === $formatted_host) {
456 throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS));
457 }
458 // @codeCoverageIgnoreEnd
459
460 return $arr['result'];
461 }
462
463 /**
464 * Retrieves and format IDNA conversion error message.
465 *
466 * @link http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html
467 */
468 private function getIDNAErrors(int $error_byte): string
469 {
470 /**
471 * IDNA errors.
472 */
473 static $idnErrors = [
474 IDNA_ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
475 IDNA_ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
476 IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
477 IDNA_ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
478 IDNA_ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
479 IDNA_ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
480 IDNA_ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark',
481 IDNA_ERROR_DISALLOWED => 'a label or domain name contains disallowed characters',
482 IDNA_ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
483 IDNA_ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop',
484 IDNA_ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
485 IDNA_ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
486 IDNA_ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
487 ];
488
489 $res = [];
490 foreach ($idnErrors as $error => $reason) {
491 if ($error === ($error_byte & $error)) {
492 $res[] = $reason;
493 }
494 }
495
496 return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.';
497 }
498
499 /**
500 * Validate and Format the IPv6/IPvfuture host.
501 *
502 * @throws SyntaxError if the submitted host is not a valid IP host
503 */
504 private function formatIp(string $host): string
505 {
506 $ip = substr($host, 1, -1);
507 if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
508 return $host;
509 }
510
511 if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) {
512 return $host;
513 }
514
515 $pos = strpos($ip, '%');
516 if (false === $pos) {
517 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
518 }
519
520 if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) {
521 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
522 }
523
524 $ip = substr($ip, 0, $pos);
525 if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
526 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
527 }
528
529 //Only the address block fe80::/10 can have a Zone ID attach to
530 //let's detect the link local significant 10 bits
531 if (0 === strpos((string) inet_pton($ip), self::HOST_ADDRESS_BLOCK)) {
532 return $host;
533 }
534
535 throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host));
536 }
537
538 /**
539 * Format the Port component.
540 *
541 * @param null|mixed $port
542 *
543 * @throws SyntaxError
544 */
545 private function formatPort($port = null): ?int
546 {
547 if (null === $port || '' === $port) {
548 return null;
549 }
550
551 if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) {
552 throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
553 }
554
555 $port = (int) $port;
556 if (0 > $port) {
557 throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
558 }
559
560 $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null;
561 if ($defaultPort === $port) {
562 return null;
563 }
564
565 return $port;
566 }
567
568 /**
569 * {@inheritDoc}
570 */
571 public static function __set_state(array $components): self
572 {
573 $components['user'] = null;
574 $components['pass'] = null;
575 if (null !== $components['user_info']) {
576 [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null];
577 }
578
579 return new self(
580 $components['scheme'],
581 $components['user'],
582 $components['pass'],
583 $components['host'],
584 $components['port'],
585 $components['path'],
586 $components['query'],
587 $components['fragment']
588 );
589 }
590
591 /**
592 * Create a new instance from a URI and a Base URI.
593 *
594 * The returned URI must be absolute.
595 *
596 * @param mixed $uri the input URI to create
597 * @param null|mixed $base_uri the base URI used for reference
598 */
599 public static function createFromBaseUri($uri, $base_uri = null): UriInterface
600 {
601 if (!$uri instanceof UriInterface) {
602 $uri = self::createFromString($uri);
603 }
604
605 if (null === $base_uri) {
606 if (null === $uri->getScheme()) {
607 throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri));
608 }
609
610 if (null === $uri->getAuthority()) {
611 return $uri;
612 }
613
614 /** @var UriInterface $uri */
615 $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath(''));
616
617 return $uri;
618 }
619
620 if (!$base_uri instanceof UriInterface) {
621 $base_uri = self::createFromString($base_uri);
622 }
623
624 if (null === $base_uri->getScheme()) {
625 throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri));
626 }
627
628 /** @var UriInterface $uri */
629 $uri = UriResolver::resolve($uri, $base_uri);
630
631 return $uri;
632 }
633
634 /**
635 * Create a new instance from a string.
636 *
637 * @param string|mixed $uri
638 */
639 public static function createFromString($uri = ''): self
640 {
641 $components = UriString::parse($uri);
642
643 return new self(
644 $components['scheme'],
645 $components['user'],
646 $components['pass'],
647 $components['host'],
648 $components['port'],
649 $components['path'],
650 $components['query'],
651 $components['fragment']
652 );
653 }
654
655 /**
656 * Create a new instance from a hash of parse_url parts.
657 *
658 * Create an new instance from a hash representation of the URI similar
659 * to PHP parse_url function result
660 *
661 * @param array<string, mixed> $components
662 */
663 public static function createFromComponents(array $components = []): self
664 {
665 $components += [
666 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
667 'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
668 ];
669
670 return new self(
671 $components['scheme'],
672 $components['user'],
673 $components['pass'],
674 $components['host'],
675 $components['port'],
676 $components['path'],
677 $components['query'],
678 $components['fragment']
679 );
680 }
681
682 /**
683 * Create a new instance from a data file path.
684 *
685 * @param resource|null $context
686 *
687 * @throws FileinfoSupportMissing If ext/fileinfo is not installed
688 * @throws SyntaxError If the file does not exist or is not readable
689 */
690 public static function createFromDataPath(string $path, $context = null): self
691 {
692 static $finfo_support = null;
693 $finfo_support = $finfo_support ?? class_exists(\finfo::class);
694
695 // @codeCoverageIgnoreStart
696 if (!$finfo_support) {
697 throw new FileinfoSupportMissing(sprintf('Please install ext/fileinfo to use the %s() method.', __METHOD__));
698 }
699 // @codeCoverageIgnoreEnd
700
701 $file_args = [$path, false];
702 $mime_args = [$path, FILEINFO_MIME];
703 if (null !== $context) {
704 $file_args[] = $context;
705 $mime_args[] = $context;
706 }
707
708 $raw = @file_get_contents(...$file_args);
709 if (false === $raw) {
710 throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path));
711 }
712
713 $mimetype = (string) (new \finfo(FILEINFO_MIME))->file(...$mime_args);
714
715 return Uri::createFromComponents([
716 'scheme' => 'data',
717 'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)),
718 ]);
719 }
720
721 /**
722 * Create a new instance from a Unix path string.
723 */
724 public static function createFromUnixPath(string $uri = ''): self
725 {
726 $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
727 if ('/' !== ($uri[0] ?? '')) {
728 return Uri::createFromComponents(['path' => $uri]);
729 }
730
731 return Uri::createFromComponents(['path' => $uri, 'scheme' => 'file', 'host' => '']);
732 }
733
734 /**
735 * Create a new instance from a local Windows path string.
736 */
737 public static function createFromWindowsPath(string $uri = ''): self
738 {
739 $root = '';
740 if (1 === preg_match(self::REGEXP_WINDOW_PATH, $uri, $matches)) {
741 $root = substr($matches['root'], 0, -1).':';
742 $uri = substr($uri, strlen($root));
743 }
744 $uri = str_replace('\\', '/', $uri);
745 $uri = implode('/', array_map('rawurlencode', explode('/', $uri)));
746
747 //Local Windows absolute path
748 if ('' !== $root) {
749 return Uri::createFromComponents(['path' => '/'.$root.$uri, 'scheme' => 'file', 'host' => '']);
750 }
751
752 //UNC Windows Path
753 if ('//' !== substr($uri, 0, 2)) {
754 return Uri::createFromComponents(['path' => $uri]);
755 }
756
757 $parts = explode('/', substr($uri, 2), 2) + [1 => null];
758
759 return Uri::createFromComponents(['host' => $parts[0], 'path' => '/'.$parts[1], 'scheme' => 'file']);
760 }
761
762 /**
763 * Create a new instance from a URI object.
764 *
765 * @param Psr7UriInterface|UriInterface $uri the input URI to create
766 */
767 public static function createFromUri($uri): self
768 {
769 if ($uri instanceof UriInterface) {
770 $user_info = $uri->getUserInfo();
771 $user = null;
772 $pass = null;
773 if (null !== $user_info) {
774 [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
775 }
776
777 return new self(
778 $uri->getScheme(),
779 $user,
780 $pass,
781 $uri->getHost(),
782 $uri->getPort(),
783 $uri->getPath(),
784 $uri->getQuery(),
785 $uri->getFragment()
786 );
787 }
788
789 if (!$uri instanceof Psr7UriInterface) {
790 throw new \TypeError(sprintf('The object must implement the `%s` or the `%s`', Psr7UriInterface::class, UriInterface::class));
791 }
792
793 $scheme = $uri->getScheme();
794 if ('' === $scheme) {
795 $scheme = null;
796 }
797
798 $fragment = $uri->getFragment();
799 if ('' === $fragment) {
800 $fragment = null;
801 }
802
803 $query = $uri->getQuery();
804 if ('' === $query) {
805 $query = null;
806 }
807
808 $host = $uri->getHost();
809 if ('' === $host) {
810 $host = null;
811 }
812
813 $user_info = $uri->getUserInfo();
814 $user = null;
815 $pass = null;
816 if ('' !== $user_info) {
817 [$user, $pass] = explode(':', $user_info, 2) + [1 => null];
818 }
819
820 return new self(
821 $scheme,
822 $user,
823 $pass,
824 $host,
825 $uri->getPort(),
826 $uri->getPath(),
827 $query,
828 $fragment
829 );
830 }
831
832 /**
833 * Create a new instance from the environment.
834 */
835 public static function createFromServer(array $server): self
836 {
837 [$user, $pass] = self::fetchUserInfo($server);
838 [$host, $port] = self::fetchHostname($server);
839 [$path, $query] = self::fetchRequestUri($server);
840
841 return Uri::createFromComponents([
842 'scheme' => self::fetchScheme($server),
843 'user' => $user,
844 'pass' => $pass,
845 'host' => $host,
846 'port' => $port,
847 'path' => $path,
848 'query' => $query,
849 ]);
850 }
851
852 /**
853 * Returns the environment scheme.
854 */
855 private static function fetchScheme(array $server): string
856 {
857 $server += ['HTTPS' => ''];
858 $res = filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
859
860 return false !== $res ? 'https' : 'http';
861 }
862
863 /**
864 * Returns the environment user info.
865 *
866 * @return array{0:?string, 1:?string}
867 */
868 private static function fetchUserInfo(array $server): array
869 {
870 $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => ''];
871 $user = $server['PHP_AUTH_USER'];
872 $pass = $server['PHP_AUTH_PW'];
873 if (0 === strpos(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) {
874 $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true);
875 if (false === $userinfo) {
876 throw new SyntaxError('The user info could not be detected');
877 }
878 [$user, $pass] = explode(':', $userinfo, 2) + [1 => null];
879 }
880
881 if (null !== $user) {
882 $user = rawurlencode($user);
883 }
884
885 if (null !== $pass) {
886 $pass = rawurlencode($pass);
887 }
888
889 return [$user, $pass];
890 }
891
892 /**
893 * Returns the environment host.
894 *
895 * @throws SyntaxError If the host can not be detected
896 *
897 * @return array{0:?string, 1:?string}
898 */
899 private static function fetchHostname(array $server): array
900 {
901 $server += ['SERVER_PORT' => null];
902 if (null !== $server['SERVER_PORT']) {
903 $server['SERVER_PORT'] = (int) $server['SERVER_PORT'];
904 }
905
906 if (isset($server['HTTP_HOST'])) {
907 preg_match(',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x', $server['HTTP_HOST'], $matches);
908
909 return [
910 $matches['host'],
911 isset($matches['port']) ? (int) $matches['port'] : $server['SERVER_PORT'],
912 ];
913 }
914
915 if (!isset($server['SERVER_ADDR'])) {
916 throw new SyntaxError('The host could not be detected');
917 }
918
919 if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
920 $server['SERVER_ADDR'] = '['.$server['SERVER_ADDR'].']';
921 }
922
923 return [$server['SERVER_ADDR'], $server['SERVER_PORT']];
924 }
925
926 /**
927 * Returns the environment path.
928 *
929 * @return array{0:?string, 1:?string}
930 */
931 private static function fetchRequestUri(array $server): array
932 {
933 $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null];
934 if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) {
935 /** @var array{0:?string, 1:?string} $retval */
936 $retval = explode('?', $server['UNENCODED_URL'], 2) + [1 => null];
937
938 return $retval;
939 }
940
941 if (isset($server['REQUEST_URI'])) {
942 [$path, ] = explode('?', $server['REQUEST_URI'], 2);
943 $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null;
944
945 return [$path, $query];
946 }
947
948 return [$server['PHP_SELF'], $server['QUERY_STRING']];
949 }
950
951 /**
952 * Generate the URI authority part.
953 */
954 private function setAuthority(): ?string
955 {
956 $authority = null;
957 if (null !== $this->user_info) {
958 $authority = $this->user_info.'@';
959 }
960
961 if (null !== $this->host) {
962 $authority .= $this->host;
963 }
964
965 if (null !== $this->port) {
966 $authority .= ':'.$this->port;
967 }
968
969 return $authority;
970 }
971
972 /**
973 * Format the Path component.
974 */
975 private function formatPath(string $path): string
976 {
977 $path = $this->formatDataPath($path);
978
979 static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/}{]++|%(?![A-Fa-f0-9]{2}))/';
980
981 $path = (string) preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $path);
982
983 return $this->formatFilePath($path);
984 }
985
986 /**
987 * Filter the Path component.
988 *
989 * @link https://tools.ietf.org/html/rfc2397
990 *
991 * @throws SyntaxError If the path is not compliant with RFC2397
992 */
993 private function formatDataPath(string $path): string
994 {
995 if ('data' !== $this->scheme) {
996 return $path;
997 }
998
999 if ('' == $path) {
1000 return 'text/plain;charset=us-ascii,';
1001 }
1002
1003 if (strlen($path) !== strspn($path, self::ASCII) || false === strpos($path, ',')) {
1004 throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937', $path));
1005 }
1006
1007 $parts = explode(',', $path, 2) + [1 => null];
1008 $mediatype = explode(';', (string) $parts[0], 2) + [1 => null];
1009 $data = (string) $parts[1];
1010 $mimetype = $mediatype[0];
1011 if (null === $mimetype || '' === $mimetype) {
1012 $mimetype = 'text/plain';
1013 }
1014
1015 $parameters = $mediatype[1];
1016 if (null === $parameters || '' === $parameters) {
1017 $parameters = 'charset=us-ascii';
1018 }
1019
1020 $this->assertValidPath($mimetype, $parameters, $data);
1021
1022 return $mimetype.';'.$parameters.','.$data;
1023 }
1024
1025 /**
1026 * Assert the path is a compliant with RFC2397.
1027 *
1028 * @link https://tools.ietf.org/html/rfc2397
1029 *
1030 * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
1031 */
1032 private function assertValidPath(string $mimetype, string $parameters, string $data): void
1033 {
1034 if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
1035 throw new SyntaxError(sprintf('The path mimetype `%s` is invalid', $mimetype));
1036 }
1037
1038 $is_binary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches);
1039 if ($is_binary) {
1040 $parameters = substr($parameters, 0, - strlen($matches[0]));
1041 }
1042
1043 $res = array_filter(array_filter(explode(';', $parameters), [$this, 'validateParameter']));
1044 if ([] !== $res) {
1045 throw new SyntaxError(sprintf('The path paremeters `%s` is invalid', $parameters));
1046 }
1047
1048 if (!$is_binary) {
1049 return;
1050 }
1051
1052 $res = base64_decode($data, true);
1053 if (false === $res || $data !== base64_encode($res)) {
1054 throw new SyntaxError(sprintf('The path data `%s` is invalid', $data));
1055 }
1056 }
1057
1058 /**
1059 * Validate mediatype parameter.
1060 */
1061 private function validateParameter(string $parameter): bool
1062 {
1063 $properties = explode('=', $parameter);
1064
1065 return 2 != count($properties) || 'base64' === strtolower($properties[0]);
1066 }
1067
1068 /**
1069 * Format path component for file scheme.
1070 */
1071 private function formatFilePath(string $path): string
1072 {
1073 if ('file' !== $this->scheme) {
1074 return $path;
1075 }
1076
1077 $replace = static function (array $matches): string {
1078 return $matches['delim'].$matches['volume'].':'.$matches['rest'];
1079 };
1080
1081 return (string) preg_replace_callback(self::REGEXP_FILE_PATH, $replace, $path);
1082 }
1083
1084 /**
1085 * Format the Query or the Fragment component.
1086 *
1087 * Returns a array containing:
1088 * <ul>
1089 * <li> the formatted component (a string or null)</li>
1090 * <li> a boolean flag telling wether the delimiter is to be added to the component
1091 * when building the URI string representation</li>
1092 * </ul>
1093 *
1094 * @param ?string $component
1095 */
1096 private function formatQueryAndFragment(?string $component): ?string
1097 {
1098 if (null === $component || '' === $component) {
1099 return $component;
1100 }
1101
1102 static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/';
1103 return preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $component);
1104 }
1105
1106 /**
1107 * assert the URI internal state is valid.
1108 *
1109 * @link https://tools.ietf.org/html/rfc3986#section-3
1110 * @link https://tools.ietf.org/html/rfc3986#section-3.3
1111 *
1112 * @throws SyntaxError if the URI is in an invalid state according to RFC3986
1113 * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules
1114 */
1115 private function assertValidState(): void
1116 {
1117 if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) {
1118 throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.');
1119 }
1120
1121 if (null === $this->authority && 0 === strpos($this->path, '//')) {
1122 throw new SyntaxError(sprintf('If there is no authority the path `%s` can not start with a `//`.', $this->path));
1123 }
1124
1125 $pos = strpos($this->path, ':');
1126 if (null === $this->authority
1127 && null === $this->scheme
1128 && false !== $pos
1129 && false === strpos(substr($this->path, 0, $pos), '/')
1130 ) {
1131 throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
1132 }
1133
1134 $validationMethod = self::SCHEME_VALIDATION_METHOD[$this->scheme] ?? null;
1135 if (null === $validationMethod || true === $this->$validationMethod()) {
1136 $this->uri = null;
1137
1138 return;
1139 }
1140
1141 throw new SyntaxError(sprintf('The uri `%s` is invalid for the `%s` scheme.', (string) $this, $this->scheme));
1142 }
1143
1144 /**
1145 * URI validation for URI schemes which allows only scheme and path components.
1146 */
1147 private function isUriWithSchemeAndPathOnly(): bool
1148 {
1149 return null === $this->authority
1150 && null === $this->query
1151 && null === $this->fragment;
1152 }
1153
1154 /**
1155 * URI validation for URI schemes which allows only scheme, host and path components.
1156 */
1157 private function isUriWithSchemeHostAndPathOnly(): bool
1158 {
1159 return null === $this->user_info
1160 && null === $this->port
1161 && null === $this->query
1162 && null === $this->fragment
1163 && !('' != $this->scheme && null === $this->host);
1164 }
1165
1166 /**
1167 * URI validation for URI schemes which disallow the empty '' host.
1168 */
1169 private function isNonEmptyHostUri(): bool
1170 {
1171 return '' !== $this->host
1172 && !(null !== $this->scheme && null === $this->host);
1173 }
1174
1175 /**
1176 * URI validation for URIs schemes which disallow the empty '' host
1177 * and forbids the fragment component.
1178 */
1179 private function isNonEmptyHostUriWithoutFragment(): bool
1180 {
1181 return $this->isNonEmptyHostUri() && null === $this->fragment;
1182 }
1183
1184 /**
1185 * URI validation for URIs schemes which disallow the empty '' host
1186 * and forbids fragment and query components.
1187 */
1188 private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool
1189 {
1190 return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
1191 }
1192
1193 /**
1194 * Generate the URI string representation from its components.
1195 *
1196 * @link https://tools.ietf.org/html/rfc3986#section-5.3
1197 *
1198 * @param ?string $scheme
1199 * @param ?string $authority
1200 * @param ?string $query
1201 * @param ?string $fragment
1202 */
1203 private function getUriString(
1204 ?string $scheme,
1205 ?string $authority,
1206 string $path,
1207 ?string $query,
1208 ?string $fragment
1209 ): string {
1210 if (null !== $scheme) {
1211 $scheme = $scheme.':';
1212 }
1213
1214 if (null !== $authority) {
1215 $authority = '//'.$authority;
1216 }
1217
1218 if (null !== $query) {
1219 $query = '?'.$query;
1220 }
1221
1222 if (null !== $fragment) {
1223 $fragment = '#'.$fragment;
1224 }
1225
1226 return $scheme.$authority.$path.$query.$fragment;
1227 }
1228
1229 /**
1230 * {@inheritDoc}
1231 */
1232 public function __toString(): string
1233 {
1234 $this->uri = $this->uri ?? $this->getUriString(
1235 $this->scheme,
1236 $this->authority,
1237 $this->path,
1238 $this->query,
1239 $this->fragment
1240 );
1241
1242 return $this->uri;
1243 }
1244
1245 /**
1246 * {@inheritDoc}
1247 */
1248 public function jsonSerialize(): string
1249 {
1250 return $this->__toString();
1251 }
1252
1253 /**
1254 * {@inheritDoc}
1255 *
1256 * @return array{scheme:?string, user_info:?string, host:?string, port:?int, path:string, query:?string, fragment:?string}
1257 */
1258 public function __debugInfo(): array
1259 {
1260 return [
1261 'scheme' => $this->scheme,
1262 'user_info' => isset($this->user_info) ? preg_replace(',:(.*).?$,', ':***', $this->user_info) : null,
1263 'host' => $this->host,
1264 'port' => $this->port,
1265 'path' => $this->path,
1266 'query' => $this->query,
1267 'fragment' => $this->fragment,
1268 ];
1269 }
1270
1271 /**
1272 * {@inheritDoc}
1273 */
1274 public function getScheme(): ?string
1275 {
1276 return $this->scheme;
1277 }
1278
1279 /**
1280 * {@inheritDoc}
1281 */
1282 public function getAuthority(): ?string
1283 {
1284 return $this->authority;
1285 }
1286
1287 /**
1288 * {@inheritDoc}
1289 */
1290 public function getUserInfo(): ?string
1291 {
1292 return $this->user_info;
1293 }
1294
1295 /**
1296 * {@inheritDoc}
1297 */
1298 public function getHost(): ?string
1299 {
1300 return $this->host;
1301 }
1302
1303 /**
1304 * {@inheritDoc}
1305 */
1306 public function getPort(): ?int
1307 {
1308 return $this->port;
1309 }
1310
1311 /**
1312 * {@inheritDoc}
1313 */
1314 public function getPath(): string
1315 {
1316 return $this->path;
1317 }
1318
1319 /**
1320 * {@inheritDoc}
1321 */
1322 public function getQuery(): ?string
1323 {
1324 return $this->query;
1325 }
1326
1327 /**
1328 * {@inheritDoc}
1329 */
1330 public function getFragment(): ?string
1331 {
1332 return $this->fragment;
1333 }
1334
1335 /**
1336 * {@inheritDoc}
1337 */
1338 public function withScheme($scheme): UriInterface
1339 {
1340 $scheme = $this->formatScheme($this->filterString($scheme));
1341 if ($scheme === $this->scheme) {
1342 return $this;
1343 }
1344
1345 $clone = clone $this;
1346 $clone->scheme = $scheme;
1347 $clone->port = $clone->formatPort($clone->port);
1348 $clone->authority = $clone->setAuthority();
1349 $clone->assertValidState();
1350
1351 return $clone;
1352 }
1353
1354 /**
1355 * Filter a string.
1356 *
1357 * @param mixed $str the value to evaluate as a string
1358 *
1359 * @throws SyntaxError if the submitted data can not be converted to string
1360 */
1361 private function filterString($str): ?string
1362 {
1363 if (null === $str) {
1364 return $str;
1365 }
1366
1367 if (is_object($str) && method_exists($str, '__toString')) {
1368 $str = (string) $str;
1369 }
1370
1371 if (!is_scalar($str)) {
1372 throw new \TypeError(sprintf('The component must be a string, a scalar or a stringable object %s given.', gettype($str)));
1373 }
1374
1375 $str = (string) $str;
1376 if (1 !== preg_match(self::REGEXP_INVALID_CHARS, $str)) {
1377 return $str;
1378 }
1379
1380 throw new SyntaxError(sprintf('The component `%s` contains invalid characters.', $str));
1381 }
1382
1383 /**
1384 * {@inheritDoc}
1385 */
1386 public function withUserInfo($user, $password = null): UriInterface
1387 {
1388 $user_info = null;
1389 $user = $this->filterString($user);
1390 if (null !== $password) {
1391 $password = $this->filterString($password);
1392 }
1393
1394 if ('' !== $user) {
1395 $user_info = $this->formatUserInfo($user, $password);
1396 }
1397
1398 if ($user_info === $this->user_info) {
1399 return $this;
1400 }
1401
1402 $clone = clone $this;
1403 $clone->user_info = $user_info;
1404 $clone->authority = $clone->setAuthority();
1405 $clone->assertValidState();
1406
1407 return $clone;
1408 }
1409
1410 /**
1411 * {@inheritDoc}
1412 */
1413 public function withHost($host): UriInterface
1414 {
1415 $host = $this->formatHost($this->filterString($host));
1416 if ($host === $this->host) {
1417 return $this;
1418 }
1419
1420 $clone = clone $this;
1421 $clone->host = $host;
1422 $clone->authority = $clone->setAuthority();
1423 $clone->assertValidState();
1424
1425 return $clone;
1426 }
1427
1428 /**
1429 * {@inheritDoc}
1430 */
1431 public function withPort($port): UriInterface
1432 {
1433 $port = $this->formatPort($port);
1434 if ($port === $this->port) {
1435 return $this;
1436 }
1437
1438 $clone = clone $this;
1439 $clone->port = $port;
1440 $clone->authority = $clone->setAuthority();
1441 $clone->assertValidState();
1442
1443 return $clone;
1444 }
1445
1446 /**
1447 * {@inheritDoc}
1448 */
1449 public function withPath($path): UriInterface
1450 {
1451 $path = $this->filterString($path);
1452 if (null === $path) {
1453 throw new \TypeError('A path must be a string NULL given.');
1454 }
1455
1456 $path = $this->formatPath($path);
1457 if ($path === $this->path) {
1458 return $this;
1459 }
1460
1461 $clone = clone $this;
1462 $clone->path = $path;
1463 $clone->assertValidState();
1464
1465 return $clone;
1466 }
1467
1468 /**
1469 * {@inheritDoc}
1470 */
1471 public function withQuery($query): UriInterface
1472 {
1473 $query = $this->formatQueryAndFragment($this->filterString($query));
1474 if ($query === $this->query) {
1475 return $this;
1476 }
1477
1478 $clone = clone $this;
1479 $clone->query = $query;
1480 $clone->assertValidState();
1481
1482 return $clone;
1483 }
1484
1485 /**
1486 * {@inheritDoc}
1487 */
1488 public function withFragment($fragment): UriInterface
1489 {
1490 $fragment = $this->formatQueryAndFragment($this->filterString($fragment));
1491 if ($fragment === $this->fragment) {
1492 return $this;
1493 }
1494
1495 $clone = clone $this;
1496 $clone->fragment = $fragment;
1497 $clone->assertValidState();
1498
1499 return $clone;
1500 }
1501 }
1502