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 League\Uri\Contracts\UriInterface; 17use League\Uri\Exceptions\FileinfoSupportMissing; 18use League\Uri\Exceptions\IdnSupportMissing; 19use League\Uri\Exceptions\SyntaxError; 20use Psr\Http\Message\UriInterface as Psr7UriInterface; 21use function array_filter; 22use function array_map; 23use function base64_decode; 24use function base64_encode; 25use function count; 26use function defined; 27use function explode; 28use function file_get_contents; 29use function filter_var; 30use function function_exists; 31use function idn_to_ascii; 32use function implode; 33use function in_array; 34use function inet_pton; 35use function is_object; 36use function is_scalar; 37use function method_exists; 38use function preg_match; 39use function preg_replace; 40use function preg_replace_callback; 41use function rawurlencode; 42use function sprintf; 43use function str_replace; 44use function strlen; 45use function strpos; 46use function strspn; 47use function strtolower; 48use function substr; 49use const FILEINFO_MIME; 50use const FILTER_FLAG_IPV4; 51use const FILTER_FLAG_IPV6; 52use const FILTER_NULL_ON_FAILURE; 53use const FILTER_VALIDATE_BOOLEAN; 54use const FILTER_VALIDATE_IP; 55use const IDNA_CHECK_BIDI; 56use const IDNA_CHECK_CONTEXTJ; 57use const IDNA_ERROR_BIDI; 58use const IDNA_ERROR_CONTEXTJ; 59use const IDNA_ERROR_DISALLOWED; 60use const IDNA_ERROR_DOMAIN_NAME_TOO_LONG; 61use const IDNA_ERROR_EMPTY_LABEL; 62use const IDNA_ERROR_HYPHEN_3_4; 63use const IDNA_ERROR_INVALID_ACE_LABEL; 64use const IDNA_ERROR_LABEL_HAS_DOT; 65use const IDNA_ERROR_LABEL_TOO_LONG; 66use const IDNA_ERROR_LEADING_COMBINING_MARK; 67use const IDNA_ERROR_LEADING_HYPHEN; 68use const IDNA_ERROR_PUNYCODE; 69use const IDNA_ERROR_TRAILING_HYPHEN; 70use const IDNA_NONTRANSITIONAL_TO_ASCII; 71use const IDNA_NONTRANSITIONAL_TO_UNICODE; 72use const INTL_IDNA_VARIANT_UTS46; 73 74final 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