1<?php 2 3namespace GuzzleHttp\Psr7; 4 5use Psr\Http\Message\UriInterface; 6 7/** 8 * PSR-7 URI implementation. 9 * 10 * @author Michael Dowling 11 * @author Tobias Schultze 12 * @author Matthew Weier O'Phinney 13 */ 14class Uri implements UriInterface 15{ 16 /** 17 * Absolute http and https URIs require a host per RFC 7230 Section 2.7 18 * but in generic URIs the host can be empty. So for http(s) URIs 19 * we apply this default host when no host is given yet to form a 20 * valid URI. 21 */ 22 const HTTP_DEFAULT_HOST = 'localhost'; 23 24 private static $defaultPorts = [ 25 'http' => 80, 26 'https' => 443, 27 'ftp' => 21, 28 'gopher' => 70, 29 'nntp' => 119, 30 'news' => 119, 31 'telnet' => 23, 32 'tn3270' => 23, 33 'imap' => 143, 34 'pop' => 110, 35 'ldap' => 389, 36 ]; 37 38 private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; 39 private static $charSubDelims = '!\$&\'\(\)\*\+,;='; 40 private static $replaceQuery = ['=' => '%3D', '&' => '%26']; 41 42 /** @var string Uri scheme. */ 43 private $scheme = ''; 44 45 /** @var string Uri user info. */ 46 private $userInfo = ''; 47 48 /** @var string Uri host. */ 49 private $host = ''; 50 51 /** @var int|null Uri port. */ 52 private $port; 53 54 /** @var string Uri path. */ 55 private $path = ''; 56 57 /** @var string Uri query string. */ 58 private $query = ''; 59 60 /** @var string Uri fragment. */ 61 private $fragment = ''; 62 63 /** 64 * @param string $uri URI to parse 65 */ 66 public function __construct($uri = '') 67 { 68 // weak type check to also accept null until we can add scalar type hints 69 if ($uri != '') { 70 $parts = self::parse($uri); 71 if ($parts === false) { 72 throw new \InvalidArgumentException("Unable to parse URI: $uri"); 73 } 74 $this->applyParts($parts); 75 } 76 } 77 78 /** 79 * UTF-8 aware \parse_url() replacement. 80 * 81 * The internal function produces broken output for non ASCII domain names 82 * (IDN) when used with locales other than "C". 83 * 84 * On the other hand, cURL understands IDN correctly only when UTF-8 locale 85 * is configured ("C.UTF-8", "en_US.UTF-8", etc.). 86 * 87 * @see https://bugs.php.net/bug.php?id=52923 88 * @see https://www.php.net/manual/en/function.parse-url.php#114817 89 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING 90 * 91 * @param string $url 92 * 93 * @return array|false 94 */ 95 private static function parse($url) 96 { 97 // If IPv6 98 $prefix = ''; 99 if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) { 100 $prefix = $matches[1]; 101 $url = $matches[2]; 102 } 103 104 $encodedUrl = preg_replace_callback( 105 '%[^:/@?&=#]+%usD', 106 static function ($matches) { 107 return urlencode($matches[0]); 108 }, 109 $url 110 ); 111 112 $result = parse_url($prefix . $encodedUrl); 113 114 if ($result === false) { 115 return false; 116 } 117 118 return array_map('urldecode', $result); 119 } 120 121 public function __toString() 122 { 123 return self::composeComponents( 124 $this->scheme, 125 $this->getAuthority(), 126 $this->path, 127 $this->query, 128 $this->fragment 129 ); 130 } 131 132 /** 133 * Composes a URI reference string from its various components. 134 * 135 * Usually this method does not need to be called manually but instead is used indirectly via 136 * `Psr\Http\Message\UriInterface::__toString`. 137 * 138 * PSR-7 UriInterface treats an empty component the same as a missing component as 139 * getQuery(), getFragment() etc. always return a string. This explains the slight 140 * difference to RFC 3986 Section 5.3. 141 * 142 * Another adjustment is that the authority separator is added even when the authority is missing/empty 143 * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with 144 * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But 145 * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to 146 * that format). 147 * 148 * @param string $scheme 149 * @param string $authority 150 * @param string $path 151 * @param string $query 152 * @param string $fragment 153 * 154 * @return string 155 * 156 * @link https://tools.ietf.org/html/rfc3986#section-5.3 157 */ 158 public static function composeComponents($scheme, $authority, $path, $query, $fragment) 159 { 160 $uri = ''; 161 162 // weak type checks to also accept null until we can add scalar type hints 163 if ($scheme != '') { 164 $uri .= $scheme . ':'; 165 } 166 167 if ($authority != ''|| $scheme === 'file') { 168 $uri .= '//' . $authority; 169 } 170 171 $uri .= $path; 172 173 if ($query != '') { 174 $uri .= '?' . $query; 175 } 176 177 if ($fragment != '') { 178 $uri .= '#' . $fragment; 179 } 180 181 return $uri; 182 } 183 184 /** 185 * Whether the URI has the default port of the current scheme. 186 * 187 * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used 188 * independently of the implementation. 189 * 190 * @param UriInterface $uri 191 * 192 * @return bool 193 */ 194 public static function isDefaultPort(UriInterface $uri) 195 { 196 return $uri->getPort() === null 197 || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); 198 } 199 200 /** 201 * Whether the URI is absolute, i.e. it has a scheme. 202 * 203 * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true 204 * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative 205 * to another URI, the base URI. Relative references can be divided into several forms: 206 * - network-path references, e.g. '//example.com/path' 207 * - absolute-path references, e.g. '/path' 208 * - relative-path references, e.g. 'subpath' 209 * 210 * @param UriInterface $uri 211 * 212 * @return bool 213 * 214 * @see Uri::isNetworkPathReference 215 * @see Uri::isAbsolutePathReference 216 * @see Uri::isRelativePathReference 217 * @link https://tools.ietf.org/html/rfc3986#section-4 218 */ 219 public static function isAbsolute(UriInterface $uri) 220 { 221 return $uri->getScheme() !== ''; 222 } 223 224 /** 225 * Whether the URI is a network-path reference. 226 * 227 * A relative reference that begins with two slash characters is termed an network-path reference. 228 * 229 * @param UriInterface $uri 230 * 231 * @return bool 232 * 233 * @link https://tools.ietf.org/html/rfc3986#section-4.2 234 */ 235 public static function isNetworkPathReference(UriInterface $uri) 236 { 237 return $uri->getScheme() === '' && $uri->getAuthority() !== ''; 238 } 239 240 /** 241 * Whether the URI is a absolute-path reference. 242 * 243 * A relative reference that begins with a single slash character is termed an absolute-path reference. 244 * 245 * @param UriInterface $uri 246 * 247 * @return bool 248 * 249 * @link https://tools.ietf.org/html/rfc3986#section-4.2 250 */ 251 public static function isAbsolutePathReference(UriInterface $uri) 252 { 253 return $uri->getScheme() === '' 254 && $uri->getAuthority() === '' 255 && isset($uri->getPath()[0]) 256 && $uri->getPath()[0] === '/'; 257 } 258 259 /** 260 * Whether the URI is a relative-path reference. 261 * 262 * A relative reference that does not begin with a slash character is termed a relative-path reference. 263 * 264 * @param UriInterface $uri 265 * 266 * @return bool 267 * 268 * @link https://tools.ietf.org/html/rfc3986#section-4.2 269 */ 270 public static function isRelativePathReference(UriInterface $uri) 271 { 272 return $uri->getScheme() === '' 273 && $uri->getAuthority() === '' 274 && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); 275 } 276 277 /** 278 * Whether the URI is a same-document reference. 279 * 280 * A same-document reference refers to a URI that is, aside from its fragment 281 * component, identical to the base URI. When no base URI is given, only an empty 282 * URI reference (apart from its fragment) is considered a same-document reference. 283 * 284 * @param UriInterface $uri The URI to check 285 * @param UriInterface|null $base An optional base URI to compare against 286 * 287 * @return bool 288 * 289 * @link https://tools.ietf.org/html/rfc3986#section-4.4 290 */ 291 public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) 292 { 293 if ($base !== null) { 294 $uri = UriResolver::resolve($base, $uri); 295 296 return ($uri->getScheme() === $base->getScheme()) 297 && ($uri->getAuthority() === $base->getAuthority()) 298 && ($uri->getPath() === $base->getPath()) 299 && ($uri->getQuery() === $base->getQuery()); 300 } 301 302 return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; 303 } 304 305 /** 306 * Removes dot segments from a path and returns the new path. 307 * 308 * @param string $path 309 * 310 * @return string 311 * 312 * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead. 313 * @see UriResolver::removeDotSegments 314 */ 315 public static function removeDotSegments($path) 316 { 317 return UriResolver::removeDotSegments($path); 318 } 319 320 /** 321 * Converts the relative URI into a new URI that is resolved against the base URI. 322 * 323 * @param UriInterface $base Base URI 324 * @param string|UriInterface $rel Relative URI 325 * 326 * @return UriInterface 327 * 328 * @deprecated since version 1.4. Use UriResolver::resolve instead. 329 * @see UriResolver::resolve 330 */ 331 public static function resolve(UriInterface $base, $rel) 332 { 333 if (!($rel instanceof UriInterface)) { 334 $rel = new self($rel); 335 } 336 337 return UriResolver::resolve($base, $rel); 338 } 339 340 /** 341 * Creates a new URI with a specific query string value removed. 342 * 343 * Any existing query string values that exactly match the provided key are 344 * removed. 345 * 346 * @param UriInterface $uri URI to use as a base. 347 * @param string $key Query string key to remove. 348 * 349 * @return UriInterface 350 */ 351 public static function withoutQueryValue(UriInterface $uri, $key) 352 { 353 $result = self::getFilteredQueryString($uri, [$key]); 354 355 return $uri->withQuery(implode('&', $result)); 356 } 357 358 /** 359 * Creates a new URI with a specific query string value. 360 * 361 * Any existing query string values that exactly match the provided key are 362 * removed and replaced with the given key value pair. 363 * 364 * A value of null will set the query string key without a value, e.g. "key" 365 * instead of "key=value". 366 * 367 * @param UriInterface $uri URI to use as a base. 368 * @param string $key Key to set. 369 * @param string|null $value Value to set 370 * 371 * @return UriInterface 372 */ 373 public static function withQueryValue(UriInterface $uri, $key, $value) 374 { 375 $result = self::getFilteredQueryString($uri, [$key]); 376 377 $result[] = self::generateQueryString($key, $value); 378 379 return $uri->withQuery(implode('&', $result)); 380 } 381 382 /** 383 * Creates a new URI with multiple specific query string values. 384 * 385 * It has the same behavior as withQueryValue() but for an associative array of key => value. 386 * 387 * @param UriInterface $uri URI to use as a base. 388 * @param array $keyValueArray Associative array of key and values 389 * 390 * @return UriInterface 391 */ 392 public static function withQueryValues(UriInterface $uri, array $keyValueArray) 393 { 394 $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); 395 396 foreach ($keyValueArray as $key => $value) { 397 $result[] = self::generateQueryString($key, $value); 398 } 399 400 return $uri->withQuery(implode('&', $result)); 401 } 402 403 /** 404 * Creates a URI from a hash of `parse_url` components. 405 * 406 * @param array $parts 407 * 408 * @return UriInterface 409 * 410 * @link http://php.net/manual/en/function.parse-url.php 411 * 412 * @throws \InvalidArgumentException If the components do not form a valid URI. 413 */ 414 public static function fromParts(array $parts) 415 { 416 $uri = new self(); 417 $uri->applyParts($parts); 418 $uri->validateState(); 419 420 return $uri; 421 } 422 423 public function getScheme() 424 { 425 return $this->scheme; 426 } 427 428 public function getAuthority() 429 { 430 $authority = $this->host; 431 if ($this->userInfo !== '') { 432 $authority = $this->userInfo . '@' . $authority; 433 } 434 435 if ($this->port !== null) { 436 $authority .= ':' . $this->port; 437 } 438 439 return $authority; 440 } 441 442 public function getUserInfo() 443 { 444 return $this->userInfo; 445 } 446 447 public function getHost() 448 { 449 return $this->host; 450 } 451 452 public function getPort() 453 { 454 return $this->port; 455 } 456 457 public function getPath() 458 { 459 return $this->path; 460 } 461 462 public function getQuery() 463 { 464 return $this->query; 465 } 466 467 public function getFragment() 468 { 469 return $this->fragment; 470 } 471 472 public function withScheme($scheme) 473 { 474 $scheme = $this->filterScheme($scheme); 475 476 if ($this->scheme === $scheme) { 477 return $this; 478 } 479 480 $new = clone $this; 481 $new->scheme = $scheme; 482 $new->removeDefaultPort(); 483 $new->validateState(); 484 485 return $new; 486 } 487 488 public function withUserInfo($user, $password = null) 489 { 490 $info = $this->filterUserInfoComponent($user); 491 if ($password !== null) { 492 $info .= ':' . $this->filterUserInfoComponent($password); 493 } 494 495 if ($this->userInfo === $info) { 496 return $this; 497 } 498 499 $new = clone $this; 500 $new->userInfo = $info; 501 $new->validateState(); 502 503 return $new; 504 } 505 506 public function withHost($host) 507 { 508 $host = $this->filterHost($host); 509 510 if ($this->host === $host) { 511 return $this; 512 } 513 514 $new = clone $this; 515 $new->host = $host; 516 $new->validateState(); 517 518 return $new; 519 } 520 521 public function withPort($port) 522 { 523 $port = $this->filterPort($port); 524 525 if ($this->port === $port) { 526 return $this; 527 } 528 529 $new = clone $this; 530 $new->port = $port; 531 $new->removeDefaultPort(); 532 $new->validateState(); 533 534 return $new; 535 } 536 537 public function withPath($path) 538 { 539 $path = $this->filterPath($path); 540 541 if ($this->path === $path) { 542 return $this; 543 } 544 545 $new = clone $this; 546 $new->path = $path; 547 $new->validateState(); 548 549 return $new; 550 } 551 552 public function withQuery($query) 553 { 554 $query = $this->filterQueryAndFragment($query); 555 556 if ($this->query === $query) { 557 return $this; 558 } 559 560 $new = clone $this; 561 $new->query = $query; 562 563 return $new; 564 } 565 566 public function withFragment($fragment) 567 { 568 $fragment = $this->filterQueryAndFragment($fragment); 569 570 if ($this->fragment === $fragment) { 571 return $this; 572 } 573 574 $new = clone $this; 575 $new->fragment = $fragment; 576 577 return $new; 578 } 579 580 /** 581 * Apply parse_url parts to a URI. 582 * 583 * @param array $parts Array of parse_url parts to apply. 584 */ 585 private function applyParts(array $parts) 586 { 587 $this->scheme = isset($parts['scheme']) 588 ? $this->filterScheme($parts['scheme']) 589 : ''; 590 $this->userInfo = isset($parts['user']) 591 ? $this->filterUserInfoComponent($parts['user']) 592 : ''; 593 $this->host = isset($parts['host']) 594 ? $this->filterHost($parts['host']) 595 : ''; 596 $this->port = isset($parts['port']) 597 ? $this->filterPort($parts['port']) 598 : null; 599 $this->path = isset($parts['path']) 600 ? $this->filterPath($parts['path']) 601 : ''; 602 $this->query = isset($parts['query']) 603 ? $this->filterQueryAndFragment($parts['query']) 604 : ''; 605 $this->fragment = isset($parts['fragment']) 606 ? $this->filterQueryAndFragment($parts['fragment']) 607 : ''; 608 if (isset($parts['pass'])) { 609 $this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']); 610 } 611 612 $this->removeDefaultPort(); 613 } 614 615 /** 616 * @param string $scheme 617 * 618 * @return string 619 * 620 * @throws \InvalidArgumentException If the scheme is invalid. 621 */ 622 private function filterScheme($scheme) 623 { 624 if (!is_string($scheme)) { 625 throw new \InvalidArgumentException('Scheme must be a string'); 626 } 627 628 return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 629 } 630 631 /** 632 * @param string $component 633 * 634 * @return string 635 * 636 * @throws \InvalidArgumentException If the user info is invalid. 637 */ 638 private function filterUserInfoComponent($component) 639 { 640 if (!is_string($component)) { 641 throw new \InvalidArgumentException('User info must be a string'); 642 } 643 644 return preg_replace_callback( 645 '/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/', 646 [$this, 'rawurlencodeMatchZero'], 647 $component 648 ); 649 } 650 651 /** 652 * @param string $host 653 * 654 * @return string 655 * 656 * @throws \InvalidArgumentException If the host is invalid. 657 */ 658 private function filterHost($host) 659 { 660 if (!is_string($host)) { 661 throw new \InvalidArgumentException('Host must be a string'); 662 } 663 664 return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 665 } 666 667 /** 668 * @param int|null $port 669 * 670 * @return int|null 671 * 672 * @throws \InvalidArgumentException If the port is invalid. 673 */ 674 private function filterPort($port) 675 { 676 if ($port === null) { 677 return null; 678 } 679 680 $port = (int) $port; 681 if (0 > $port || 0xffff < $port) { 682 throw new \InvalidArgumentException( 683 sprintf('Invalid port: %d. Must be between 0 and 65535', $port) 684 ); 685 } 686 687 return $port; 688 } 689 690 /** 691 * @param UriInterface $uri 692 * @param array $keys 693 * 694 * @return array 695 */ 696 private static function getFilteredQueryString(UriInterface $uri, array $keys) 697 { 698 $current = $uri->getQuery(); 699 700 if ($current === '') { 701 return []; 702 } 703 704 $decodedKeys = array_map('rawurldecode', $keys); 705 706 return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { 707 return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); 708 }); 709 } 710 711 /** 712 * @param string $key 713 * @param string|null $value 714 * 715 * @return string 716 */ 717 private static function generateQueryString($key, $value) 718 { 719 // Query string separators ("=", "&") within the key or value need to be encoded 720 // (while preventing double-encoding) before setting the query string. All other 721 // chars that need percent-encoding will be encoded by withQuery(). 722 $queryString = strtr($key, self::$replaceQuery); 723 724 if ($value !== null) { 725 $queryString .= '=' . strtr($value, self::$replaceQuery); 726 } 727 728 return $queryString; 729 } 730 731 private function removeDefaultPort() 732 { 733 if ($this->port !== null && self::isDefaultPort($this)) { 734 $this->port = null; 735 } 736 } 737 738 /** 739 * Filters the path of a URI 740 * 741 * @param string $path 742 * 743 * @return string 744 * 745 * @throws \InvalidArgumentException If the path is invalid. 746 */ 747 private function filterPath($path) 748 { 749 if (!is_string($path)) { 750 throw new \InvalidArgumentException('Path must be a string'); 751 } 752 753 return preg_replace_callback( 754 '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', 755 [$this, 'rawurlencodeMatchZero'], 756 $path 757 ); 758 } 759 760 /** 761 * Filters the query string or fragment of a URI. 762 * 763 * @param string $str 764 * 765 * @return string 766 * 767 * @throws \InvalidArgumentException If the query or fragment is invalid. 768 */ 769 private function filterQueryAndFragment($str) 770 { 771 if (!is_string($str)) { 772 throw new \InvalidArgumentException('Query and fragment must be a string'); 773 } 774 775 return preg_replace_callback( 776 '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', 777 [$this, 'rawurlencodeMatchZero'], 778 $str 779 ); 780 } 781 782 private function rawurlencodeMatchZero(array $match) 783 { 784 return rawurlencode($match[0]); 785 } 786 787 private function validateState() 788 { 789 if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { 790 $this->host = self::HTTP_DEFAULT_HOST; 791 } 792 793 if ($this->getAuthority() === '') { 794 if (0 === strpos($this->path, '//')) { 795 throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); 796 } 797 if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { 798 throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); 799 } 800 } elseif (isset($this->path[0]) && $this->path[0] !== '/') { 801 @trigger_error( 802 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . 803 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', 804 E_USER_DEPRECATED 805 ); 806 $this->path = '/' . $this->path; 807 //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty'); 808 } 809 } 810} 811