File indexing completed on 2024-12-29 05:27:24
0001 <?php 0002 namespace GuzzleHttp\Psr7; 0003 0004 use Psr\Http\Message\UriInterface; 0005 0006 /** 0007 * PSR-7 URI implementation. 0008 * 0009 * @author Michael Dowling 0010 * @author Tobias Schultze 0011 * @author Matthew Weier O'Phinney 0012 */ 0013 class Uri implements UriInterface 0014 { 0015 /** 0016 * Absolute http and https URIs require a host per RFC 7230 Section 2.7 0017 * but in generic URIs the host can be empty. So for http(s) URIs 0018 * we apply this default host when no host is given yet to form a 0019 * valid URI. 0020 */ 0021 const HTTP_DEFAULT_HOST = 'localhost'; 0022 0023 private static $defaultPorts = [ 0024 'http' => 80, 0025 'https' => 443, 0026 'ftp' => 21, 0027 'gopher' => 70, 0028 'nntp' => 119, 0029 'news' => 119, 0030 'telnet' => 23, 0031 'tn3270' => 23, 0032 'imap' => 143, 0033 'pop' => 110, 0034 'ldap' => 389, 0035 ]; 0036 0037 private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; 0038 private static $charSubDelims = '!\$&\'\(\)\*\+,;='; 0039 private static $replaceQuery = ['=' => '%3D', '&' => '%26']; 0040 0041 /** @var string Uri scheme. */ 0042 private $scheme = ''; 0043 0044 /** @var string Uri user info. */ 0045 private $userInfo = ''; 0046 0047 /** @var string Uri host. */ 0048 private $host = ''; 0049 0050 /** @var int|null Uri port. */ 0051 private $port; 0052 0053 /** @var string Uri path. */ 0054 private $path = ''; 0055 0056 /** @var string Uri query string. */ 0057 private $query = ''; 0058 0059 /** @var string Uri fragment. */ 0060 private $fragment = ''; 0061 0062 /** 0063 * @param string $uri URI to parse 0064 */ 0065 public function __construct($uri = '') 0066 { 0067 // weak type check to also accept null until we can add scalar type hints 0068 if ($uri != '') { 0069 $parts = parse_url($uri); 0070 if ($parts === false) { 0071 throw new \InvalidArgumentException("Unable to parse URI: $uri"); 0072 } 0073 $this->applyParts($parts); 0074 } 0075 } 0076 0077 public function __toString() 0078 { 0079 return self::composeComponents( 0080 $this->scheme, 0081 $this->getAuthority(), 0082 $this->path, 0083 $this->query, 0084 $this->fragment 0085 ); 0086 } 0087 0088 /** 0089 * Composes a URI reference string from its various components. 0090 * 0091 * Usually this method does not need to be called manually but instead is used indirectly via 0092 * `Psr\Http\Message\UriInterface::__toString`. 0093 * 0094 * PSR-7 UriInterface treats an empty component the same as a missing component as 0095 * getQuery(), getFragment() etc. always return a string. This explains the slight 0096 * difference to RFC 3986 Section 5.3. 0097 * 0098 * Another adjustment is that the authority separator is added even when the authority is missing/empty 0099 * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with 0100 * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But 0101 * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to 0102 * that format). 0103 * 0104 * @param string $scheme 0105 * @param string $authority 0106 * @param string $path 0107 * @param string $query 0108 * @param string $fragment 0109 * 0110 * @return string 0111 * 0112 * @link https://tools.ietf.org/html/rfc3986#section-5.3 0113 */ 0114 public static function composeComponents($scheme, $authority, $path, $query, $fragment) 0115 { 0116 $uri = ''; 0117 0118 // weak type checks to also accept null until we can add scalar type hints 0119 if ($scheme != '') { 0120 $uri .= $scheme . ':'; 0121 } 0122 0123 if ($authority != ''|| $scheme === 'file') { 0124 $uri .= '//' . $authority; 0125 } 0126 0127 $uri .= $path; 0128 0129 if ($query != '') { 0130 $uri .= '?' . $query; 0131 } 0132 0133 if ($fragment != '') { 0134 $uri .= '#' . $fragment; 0135 } 0136 0137 return $uri; 0138 } 0139 0140 /** 0141 * Whether the URI has the default port of the current scheme. 0142 * 0143 * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used 0144 * independently of the implementation. 0145 * 0146 * @param UriInterface $uri 0147 * 0148 * @return bool 0149 */ 0150 public static function isDefaultPort(UriInterface $uri) 0151 { 0152 return $uri->getPort() === null 0153 || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); 0154 } 0155 0156 /** 0157 * Whether the URI is absolute, i.e. it has a scheme. 0158 * 0159 * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true 0160 * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative 0161 * to another URI, the base URI. Relative references can be divided into several forms: 0162 * - network-path references, e.g. '//example.com/path' 0163 * - absolute-path references, e.g. '/path' 0164 * - relative-path references, e.g. 'subpath' 0165 * 0166 * @param UriInterface $uri 0167 * 0168 * @return bool 0169 * @see Uri::isNetworkPathReference 0170 * @see Uri::isAbsolutePathReference 0171 * @see Uri::isRelativePathReference 0172 * @link https://tools.ietf.org/html/rfc3986#section-4 0173 */ 0174 public static function isAbsolute(UriInterface $uri) 0175 { 0176 return $uri->getScheme() !== ''; 0177 } 0178 0179 /** 0180 * Whether the URI is a network-path reference. 0181 * 0182 * A relative reference that begins with two slash characters is termed an network-path reference. 0183 * 0184 * @param UriInterface $uri 0185 * 0186 * @return bool 0187 * @link https://tools.ietf.org/html/rfc3986#section-4.2 0188 */ 0189 public static function isNetworkPathReference(UriInterface $uri) 0190 { 0191 return $uri->getScheme() === '' && $uri->getAuthority() !== ''; 0192 } 0193 0194 /** 0195 * Whether the URI is a absolute-path reference. 0196 * 0197 * A relative reference that begins with a single slash character is termed an absolute-path reference. 0198 * 0199 * @param UriInterface $uri 0200 * 0201 * @return bool 0202 * @link https://tools.ietf.org/html/rfc3986#section-4.2 0203 */ 0204 public static function isAbsolutePathReference(UriInterface $uri) 0205 { 0206 return $uri->getScheme() === '' 0207 && $uri->getAuthority() === '' 0208 && isset($uri->getPath()[0]) 0209 && $uri->getPath()[0] === '/'; 0210 } 0211 0212 /** 0213 * Whether the URI is a relative-path reference. 0214 * 0215 * A relative reference that does not begin with a slash character is termed a relative-path reference. 0216 * 0217 * @param UriInterface $uri 0218 * 0219 * @return bool 0220 * @link https://tools.ietf.org/html/rfc3986#section-4.2 0221 */ 0222 public static function isRelativePathReference(UriInterface $uri) 0223 { 0224 return $uri->getScheme() === '' 0225 && $uri->getAuthority() === '' 0226 && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); 0227 } 0228 0229 /** 0230 * Whether the URI is a same-document reference. 0231 * 0232 * A same-document reference refers to a URI that is, aside from its fragment 0233 * component, identical to the base URI. When no base URI is given, only an empty 0234 * URI reference (apart from its fragment) is considered a same-document reference. 0235 * 0236 * @param UriInterface $uri The URI to check 0237 * @param UriInterface|null $base An optional base URI to compare against 0238 * 0239 * @return bool 0240 * @link https://tools.ietf.org/html/rfc3986#section-4.4 0241 */ 0242 public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) 0243 { 0244 if ($base !== null) { 0245 $uri = UriResolver::resolve($base, $uri); 0246 0247 return ($uri->getScheme() === $base->getScheme()) 0248 && ($uri->getAuthority() === $base->getAuthority()) 0249 && ($uri->getPath() === $base->getPath()) 0250 && ($uri->getQuery() === $base->getQuery()); 0251 } 0252 0253 return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; 0254 } 0255 0256 /** 0257 * Removes dot segments from a path and returns the new path. 0258 * 0259 * @param string $path 0260 * 0261 * @return string 0262 * 0263 * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead. 0264 * @see UriResolver::removeDotSegments 0265 */ 0266 public static function removeDotSegments($path) 0267 { 0268 return UriResolver::removeDotSegments($path); 0269 } 0270 0271 /** 0272 * Converts the relative URI into a new URI that is resolved against the base URI. 0273 * 0274 * @param UriInterface $base Base URI 0275 * @param string|UriInterface $rel Relative URI 0276 * 0277 * @return UriInterface 0278 * 0279 * @deprecated since version 1.4. Use UriResolver::resolve instead. 0280 * @see UriResolver::resolve 0281 */ 0282 public static function resolve(UriInterface $base, $rel) 0283 { 0284 if (!($rel instanceof UriInterface)) { 0285 $rel = new self($rel); 0286 } 0287 0288 return UriResolver::resolve($base, $rel); 0289 } 0290 0291 /** 0292 * Creates a new URI with a specific query string value removed. 0293 * 0294 * Any existing query string values that exactly match the provided key are 0295 * removed. 0296 * 0297 * @param UriInterface $uri URI to use as a base. 0298 * @param string $key Query string key to remove. 0299 * 0300 * @return UriInterface 0301 */ 0302 public static function withoutQueryValue(UriInterface $uri, $key) 0303 { 0304 $current = $uri->getQuery(); 0305 if ($current === '') { 0306 return $uri; 0307 } 0308 0309 $decodedKey = rawurldecode($key); 0310 $result = array_filter(explode('&', $current), function ($part) use ($decodedKey) { 0311 return rawurldecode(explode('=', $part)[0]) !== $decodedKey; 0312 }); 0313 0314 return $uri->withQuery(implode('&', $result)); 0315 } 0316 0317 /** 0318 * Creates a new URI with a specific query string value. 0319 * 0320 * Any existing query string values that exactly match the provided key are 0321 * removed and replaced with the given key value pair. 0322 * 0323 * A value of null will set the query string key without a value, e.g. "key" 0324 * instead of "key=value". 0325 * 0326 * @param UriInterface $uri URI to use as a base. 0327 * @param string $key Key to set. 0328 * @param string|null $value Value to set 0329 * 0330 * @return UriInterface 0331 */ 0332 public static function withQueryValue(UriInterface $uri, $key, $value) 0333 { 0334 $current = $uri->getQuery(); 0335 0336 if ($current === '') { 0337 $result = []; 0338 } else { 0339 $decodedKey = rawurldecode($key); 0340 $result = array_filter(explode('&', $current), function ($part) use ($decodedKey) { 0341 return rawurldecode(explode('=', $part)[0]) !== $decodedKey; 0342 }); 0343 } 0344 0345 // Query string separators ("=", "&") within the key or value need to be encoded 0346 // (while preventing double-encoding) before setting the query string. All other 0347 // chars that need percent-encoding will be encoded by withQuery(). 0348 $key = strtr($key, self::$replaceQuery); 0349 0350 if ($value !== null) { 0351 $result[] = $key . '=' . strtr($value, self::$replaceQuery); 0352 } else { 0353 $result[] = $key; 0354 } 0355 0356 return $uri->withQuery(implode('&', $result)); 0357 } 0358 0359 /** 0360 * Creates a URI from a hash of `parse_url` components. 0361 * 0362 * @param array $parts 0363 * 0364 * @return UriInterface 0365 * @link http://php.net/manual/en/function.parse-url.php 0366 * 0367 * @throws \InvalidArgumentException If the components do not form a valid URI. 0368 */ 0369 public static function fromParts(array $parts) 0370 { 0371 $uri = new self(); 0372 $uri->applyParts($parts); 0373 $uri->validateState(); 0374 0375 return $uri; 0376 } 0377 0378 public function getScheme() 0379 { 0380 return $this->scheme; 0381 } 0382 0383 public function getAuthority() 0384 { 0385 $authority = $this->host; 0386 if ($this->userInfo !== '') { 0387 $authority = $this->userInfo . '@' . $authority; 0388 } 0389 0390 if ($this->port !== null) { 0391 $authority .= ':' . $this->port; 0392 } 0393 0394 return $authority; 0395 } 0396 0397 public function getUserInfo() 0398 { 0399 return $this->userInfo; 0400 } 0401 0402 public function getHost() 0403 { 0404 return $this->host; 0405 } 0406 0407 public function getPort() 0408 { 0409 return $this->port; 0410 } 0411 0412 public function getPath() 0413 { 0414 return $this->path; 0415 } 0416 0417 public function getQuery() 0418 { 0419 return $this->query; 0420 } 0421 0422 public function getFragment() 0423 { 0424 return $this->fragment; 0425 } 0426 0427 public function withScheme($scheme) 0428 { 0429 $scheme = $this->filterScheme($scheme); 0430 0431 if ($this->scheme === $scheme) { 0432 return $this; 0433 } 0434 0435 $new = clone $this; 0436 $new->scheme = $scheme; 0437 $new->removeDefaultPort(); 0438 $new->validateState(); 0439 0440 return $new; 0441 } 0442 0443 public function withUserInfo($user, $password = null) 0444 { 0445 $info = $user; 0446 if ($password != '') { 0447 $info .= ':' . $password; 0448 } 0449 0450 if ($this->userInfo === $info) { 0451 return $this; 0452 } 0453 0454 $new = clone $this; 0455 $new->userInfo = $info; 0456 $new->validateState(); 0457 0458 return $new; 0459 } 0460 0461 public function withHost($host) 0462 { 0463 $host = $this->filterHost($host); 0464 0465 if ($this->host === $host) { 0466 return $this; 0467 } 0468 0469 $new = clone $this; 0470 $new->host = $host; 0471 $new->validateState(); 0472 0473 return $new; 0474 } 0475 0476 public function withPort($port) 0477 { 0478 $port = $this->filterPort($port); 0479 0480 if ($this->port === $port) { 0481 return $this; 0482 } 0483 0484 $new = clone $this; 0485 $new->port = $port; 0486 $new->removeDefaultPort(); 0487 $new->validateState(); 0488 0489 return $new; 0490 } 0491 0492 public function withPath($path) 0493 { 0494 $path = $this->filterPath($path); 0495 0496 if ($this->path === $path) { 0497 return $this; 0498 } 0499 0500 $new = clone $this; 0501 $new->path = $path; 0502 $new->validateState(); 0503 0504 return $new; 0505 } 0506 0507 public function withQuery($query) 0508 { 0509 $query = $this->filterQueryAndFragment($query); 0510 0511 if ($this->query === $query) { 0512 return $this; 0513 } 0514 0515 $new = clone $this; 0516 $new->query = $query; 0517 0518 return $new; 0519 } 0520 0521 public function withFragment($fragment) 0522 { 0523 $fragment = $this->filterQueryAndFragment($fragment); 0524 0525 if ($this->fragment === $fragment) { 0526 return $this; 0527 } 0528 0529 $new = clone $this; 0530 $new->fragment = $fragment; 0531 0532 return $new; 0533 } 0534 0535 /** 0536 * Apply parse_url parts to a URI. 0537 * 0538 * @param array $parts Array of parse_url parts to apply. 0539 */ 0540 private function applyParts(array $parts) 0541 { 0542 $this->scheme = isset($parts['scheme']) 0543 ? $this->filterScheme($parts['scheme']) 0544 : ''; 0545 $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; 0546 $this->host = isset($parts['host']) 0547 ? $this->filterHost($parts['host']) 0548 : ''; 0549 $this->port = isset($parts['port']) 0550 ? $this->filterPort($parts['port']) 0551 : null; 0552 $this->path = isset($parts['path']) 0553 ? $this->filterPath($parts['path']) 0554 : ''; 0555 $this->query = isset($parts['query']) 0556 ? $this->filterQueryAndFragment($parts['query']) 0557 : ''; 0558 $this->fragment = isset($parts['fragment']) 0559 ? $this->filterQueryAndFragment($parts['fragment']) 0560 : ''; 0561 if (isset($parts['pass'])) { 0562 $this->userInfo .= ':' . $parts['pass']; 0563 } 0564 0565 $this->removeDefaultPort(); 0566 } 0567 0568 /** 0569 * @param string $scheme 0570 * 0571 * @return string 0572 * 0573 * @throws \InvalidArgumentException If the scheme is invalid. 0574 */ 0575 private function filterScheme($scheme) 0576 { 0577 if (!is_string($scheme)) { 0578 throw new \InvalidArgumentException('Scheme must be a string'); 0579 } 0580 0581 return strtolower($scheme); 0582 } 0583 0584 /** 0585 * @param string $host 0586 * 0587 * @return string 0588 * 0589 * @throws \InvalidArgumentException If the host is invalid. 0590 */ 0591 private function filterHost($host) 0592 { 0593 if (!is_string($host)) { 0594 throw new \InvalidArgumentException('Host must be a string'); 0595 } 0596 0597 return strtolower($host); 0598 } 0599 0600 /** 0601 * @param int|null $port 0602 * 0603 * @return int|null 0604 * 0605 * @throws \InvalidArgumentException If the port is invalid. 0606 */ 0607 private function filterPort($port) 0608 { 0609 if ($port === null) { 0610 return null; 0611 } 0612 0613 $port = (int) $port; 0614 if (1 > $port || 0xffff < $port) { 0615 throw new \InvalidArgumentException( 0616 sprintf('Invalid port: %d. Must be between 1 and 65535', $port) 0617 ); 0618 } 0619 0620 return $port; 0621 } 0622 0623 private function removeDefaultPort() 0624 { 0625 if ($this->port !== null && self::isDefaultPort($this)) { 0626 $this->port = null; 0627 } 0628 } 0629 0630 /** 0631 * Filters the path of a URI 0632 * 0633 * @param string $path 0634 * 0635 * @return string 0636 * 0637 * @throws \InvalidArgumentException If the path is invalid. 0638 */ 0639 private function filterPath($path) 0640 { 0641 if (!is_string($path)) { 0642 throw new \InvalidArgumentException('Path must be a string'); 0643 } 0644 0645 return preg_replace_callback( 0646 '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', 0647 [$this, 'rawurlencodeMatchZero'], 0648 $path 0649 ); 0650 } 0651 0652 /** 0653 * Filters the query string or fragment of a URI. 0654 * 0655 * @param string $str 0656 * 0657 * @return string 0658 * 0659 * @throws \InvalidArgumentException If the query or fragment is invalid. 0660 */ 0661 private function filterQueryAndFragment($str) 0662 { 0663 if (!is_string($str)) { 0664 throw new \InvalidArgumentException('Query and fragment must be a string'); 0665 } 0666 0667 return preg_replace_callback( 0668 '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', 0669 [$this, 'rawurlencodeMatchZero'], 0670 $str 0671 ); 0672 } 0673 0674 private function rawurlencodeMatchZero(array $match) 0675 { 0676 return rawurlencode($match[0]); 0677 } 0678 0679 private function validateState() 0680 { 0681 if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { 0682 $this->host = self::HTTP_DEFAULT_HOST; 0683 } 0684 0685 if ($this->getAuthority() === '') { 0686 if (0 === strpos($this->path, '//')) { 0687 throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); 0688 } 0689 if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { 0690 throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); 0691 } 0692 } elseif (isset($this->path[0]) && $this->path[0] !== '/') { 0693 @trigger_error( 0694 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . 0695 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', 0696 E_USER_DEPRECATED 0697 ); 0698 $this->path = '/'. $this->path; 0699 //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty'); 0700 } 0701 } 0702 }