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 }