| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743 | <?phpdeclare(strict_types=1);namespace GuzzleHttp\Psr7;use GuzzleHttp\Psr7\Exception\MalformedUriException;use Psr\Http\Message\UriInterface;/** * PSR-7 URI implementation. * * @author Michael Dowling * @author Tobias Schultze * @author Matthew Weier O'Phinney */class Uri implements UriInterface, \JsonSerializable{    /**     * Absolute http and https URIs require a host per RFC 7230 Section 2.7     * but in generic URIs the host can be empty. So for http(s) URIs     * we apply this default host when no host is given yet to form a     * valid URI.     */    private const HTTP_DEFAULT_HOST = 'localhost';    private const DEFAULT_PORTS = [        'http' => 80,        'https' => 443,        'ftp' => 21,        'gopher' => 70,        'nntp' => 119,        'news' => 119,        'telnet' => 23,        'tn3270' => 23,        'imap' => 143,        'pop' => 110,        'ldap' => 389,    ];    /**     * Unreserved characters for use in a regex.     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3     */    private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';    /**     * Sub-delims for use in a regex.     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2     */    private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';    private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26'];    /** @var string Uri scheme. */    private $scheme = '';    /** @var string Uri user info. */    private $userInfo = '';    /** @var string Uri host. */    private $host = '';    /** @var int|null Uri port. */    private $port;    /** @var string Uri path. */    private $path = '';    /** @var string Uri query string. */    private $query = '';    /** @var string Uri fragment. */    private $fragment = '';    /** @var string|null String representation */    private $composedComponents;    public function __construct(string $uri = '')    {        if ($uri !== '') {            $parts = self::parse($uri);            if ($parts === false) {                throw new MalformedUriException("Unable to parse URI: $uri");            }            $this->applyParts($parts);        }    }    /**     * UTF-8 aware \parse_url() replacement.     *     * The internal function produces broken output for non ASCII domain names     * (IDN) when used with locales other than "C".     *     * On the other hand, cURL understands IDN correctly only when UTF-8 locale     * is configured ("C.UTF-8", "en_US.UTF-8", etc.).     *     * @see https://bugs.php.net/bug.php?id=52923     * @see https://www.php.net/manual/en/function.parse-url.php#114817     * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING     *     * @return array|false     */    private static function parse(string $url)    {        // If IPv6        $prefix = '';        if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) {            /** @var array{0:string, 1:string, 2:string} $matches */            $prefix = $matches[1];            $url = $matches[2];        }        /** @var string */        $encodedUrl = preg_replace_callback(            '%[^:/@?&=#]+%usD',            static function ($matches) {                return urlencode($matches[0]);            },            $url        );        $result = parse_url($prefix.$encodedUrl);        if ($result === false) {            return false;        }        return array_map('urldecode', $result);    }    public function __toString(): string    {        if ($this->composedComponents === null) {            $this->composedComponents = self::composeComponents(                $this->scheme,                $this->getAuthority(),                $this->path,                $this->query,                $this->fragment            );        }        return $this->composedComponents;    }    /**     * Composes a URI reference string from its various components.     *     * Usually this method does not need to be called manually but instead is used indirectly via     * `Psr\Http\Message\UriInterface::__toString`.     *     * PSR-7 UriInterface treats an empty component the same as a missing component as     * getQuery(), getFragment() etc. always return a string. This explains the slight     * difference to RFC 3986 Section 5.3.     *     * Another adjustment is that the authority separator is added even when the authority is missing/empty     * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with     * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But     * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to     * that format).     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3     */    public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string    {        $uri = '';        // weak type checks to also accept null until we can add scalar type hints        if ($scheme != '') {            $uri .= $scheme.':';        }        if ($authority != '' || $scheme === 'file') {            $uri .= '//'.$authority;        }        if ($authority != '' && $path != '' && $path[0] != '/') {            $path = '/'.$path;        }        $uri .= $path;        if ($query != '') {            $uri .= '?'.$query;        }        if ($fragment != '') {            $uri .= '#'.$fragment;        }        return $uri;    }    /**     * Whether the URI has the default port of the current scheme.     *     * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used     * independently of the implementation.     */    public static function isDefaultPort(UriInterface $uri): bool    {        return $uri->getPort() === null            || (isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()]);    }    /**     * Whether the URI is absolute, i.e. it has a scheme.     *     * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true     * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative     * to another URI, the base URI. Relative references can be divided into several forms:     * - network-path references, e.g. '//example.com/path'     * - absolute-path references, e.g. '/path'     * - relative-path references, e.g. 'subpath'     *     * @see Uri::isNetworkPathReference     * @see Uri::isAbsolutePathReference     * @see Uri::isRelativePathReference     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4     */    public static function isAbsolute(UriInterface $uri): bool    {        return $uri->getScheme() !== '';    }    /**     * Whether the URI is a network-path reference.     *     * A relative reference that begins with two slash characters is termed an network-path reference.     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2     */    public static function isNetworkPathReference(UriInterface $uri): bool    {        return $uri->getScheme() === '' && $uri->getAuthority() !== '';    }    /**     * Whether the URI is a absolute-path reference.     *     * A relative reference that begins with a single slash character is termed an absolute-path reference.     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2     */    public static function isAbsolutePathReference(UriInterface $uri): bool    {        return $uri->getScheme() === ''            && $uri->getAuthority() === ''            && isset($uri->getPath()[0])            && $uri->getPath()[0] === '/';    }    /**     * Whether the URI is a relative-path reference.     *     * A relative reference that does not begin with a slash character is termed a relative-path reference.     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2     */    public static function isRelativePathReference(UriInterface $uri): bool    {        return $uri->getScheme() === ''            && $uri->getAuthority() === ''            && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');    }    /**     * Whether the URI is a same-document reference.     *     * A same-document reference refers to a URI that is, aside from its fragment     * component, identical to the base URI. When no base URI is given, only an empty     * URI reference (apart from its fragment) is considered a same-document reference.     *     * @param UriInterface      $uri  The URI to check     * @param UriInterface|null $base An optional base URI to compare against     *     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.4     */    public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool    {        if ($base !== null) {            $uri = UriResolver::resolve($base, $uri);            return ($uri->getScheme() === $base->getScheme())                && ($uri->getAuthority() === $base->getAuthority())                && ($uri->getPath() === $base->getPath())                && ($uri->getQuery() === $base->getQuery());        }        return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === '';    }    /**     * Creates a new URI with a specific query string value removed.     *     * Any existing query string values that exactly match the provided key are     * removed.     *     * @param UriInterface $uri URI to use as a base.     * @param string       $key Query string key to remove.     */    public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface    {        $result = self::getFilteredQueryString($uri, [$key]);        return $uri->withQuery(implode('&', $result));    }    /**     * Creates a new URI with a specific query string value.     *     * Any existing query string values that exactly match the provided key are     * removed and replaced with the given key value pair.     *     * A value of null will set the query string key without a value, e.g. "key"     * instead of "key=value".     *     * @param UriInterface $uri   URI to use as a base.     * @param string       $key   Key to set.     * @param string|null  $value Value to set     */    public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface    {        $result = self::getFilteredQueryString($uri, [$key]);        $result[] = self::generateQueryString($key, $value);        return $uri->withQuery(implode('&', $result));    }    /**     * Creates a new URI with multiple specific query string values.     *     * It has the same behavior as withQueryValue() but for an associative array of key => value.     *     * @param UriInterface    $uri           URI to use as a base.     * @param (string|null)[] $keyValueArray Associative array of key and values     */    public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface    {        $result = self::getFilteredQueryString($uri, array_keys($keyValueArray));        foreach ($keyValueArray as $key => $value) {            $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);        }        return $uri->withQuery(implode('&', $result));    }    /**     * Creates a URI from a hash of `parse_url` components.     *     * @see https://www.php.net/manual/en/function.parse-url.php     *     * @throws MalformedUriException If the components do not form a valid URI.     */    public static function fromParts(array $parts): UriInterface    {        $uri = new self();        $uri->applyParts($parts);        $uri->validateState();        return $uri;    }    public function getScheme(): string    {        return $this->scheme;    }    public function getAuthority(): string    {        $authority = $this->host;        if ($this->userInfo !== '') {            $authority = $this->userInfo.'@'.$authority;        }        if ($this->port !== null) {            $authority .= ':'.$this->port;        }        return $authority;    }    public function getUserInfo(): string    {        return $this->userInfo;    }    public function getHost(): string    {        return $this->host;    }    public function getPort(): ?int    {        return $this->port;    }    public function getPath(): string    {        return $this->path;    }    public function getQuery(): string    {        return $this->query;    }    public function getFragment(): string    {        return $this->fragment;    }    public function withScheme($scheme): UriInterface    {        $scheme = $this->filterScheme($scheme);        if ($this->scheme === $scheme) {            return $this;        }        $new = clone $this;        $new->scheme = $scheme;        $new->composedComponents = null;        $new->removeDefaultPort();        $new->validateState();        return $new;    }    public function withUserInfo($user, $password = null): UriInterface    {        $info = $this->filterUserInfoComponent($user);        if ($password !== null) {            $info .= ':'.$this->filterUserInfoComponent($password);        }        if ($this->userInfo === $info) {            return $this;        }        $new = clone $this;        $new->userInfo = $info;        $new->composedComponents = null;        $new->validateState();        return $new;    }    public function withHost($host): UriInterface    {        $host = $this->filterHost($host);        if ($this->host === $host) {            return $this;        }        $new = clone $this;        $new->host = $host;        $new->composedComponents = null;        $new->validateState();        return $new;    }    public function withPort($port): UriInterface    {        $port = $this->filterPort($port);        if ($this->port === $port) {            return $this;        }        $new = clone $this;        $new->port = $port;        $new->composedComponents = null;        $new->removeDefaultPort();        $new->validateState();        return $new;    }    public function withPath($path): UriInterface    {        $path = $this->filterPath($path);        if ($this->path === $path) {            return $this;        }        $new = clone $this;        $new->path = $path;        $new->composedComponents = null;        $new->validateState();        return $new;    }    public function withQuery($query): UriInterface    {        $query = $this->filterQueryAndFragment($query);        if ($this->query === $query) {            return $this;        }        $new = clone $this;        $new->query = $query;        $new->composedComponents = null;        return $new;    }    public function withFragment($fragment): UriInterface    {        $fragment = $this->filterQueryAndFragment($fragment);        if ($this->fragment === $fragment) {            return $this;        }        $new = clone $this;        $new->fragment = $fragment;        $new->composedComponents = null;        return $new;    }    public function jsonSerialize(): string    {        return $this->__toString();    }    /**     * Apply parse_url parts to a URI.     *     * @param array $parts Array of parse_url parts to apply.     */    private function applyParts(array $parts): void    {        $this->scheme = isset($parts['scheme'])            ? $this->filterScheme($parts['scheme'])            : '';        $this->userInfo = isset($parts['user'])            ? $this->filterUserInfoComponent($parts['user'])            : '';        $this->host = isset($parts['host'])            ? $this->filterHost($parts['host'])            : '';        $this->port = isset($parts['port'])            ? $this->filterPort($parts['port'])            : null;        $this->path = isset($parts['path'])            ? $this->filterPath($parts['path'])            : '';        $this->query = isset($parts['query'])            ? $this->filterQueryAndFragment($parts['query'])            : '';        $this->fragment = isset($parts['fragment'])            ? $this->filterQueryAndFragment($parts['fragment'])            : '';        if (isset($parts['pass'])) {            $this->userInfo .= ':'.$this->filterUserInfoComponent($parts['pass']);        }        $this->removeDefaultPort();    }    /**     * @param mixed $scheme     *     * @throws \InvalidArgumentException If the scheme is invalid.     */    private function filterScheme($scheme): string    {        if (!is_string($scheme)) {            throw new \InvalidArgumentException('Scheme must be a string');        }        return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');    }    /**     * @param mixed $component     *     * @throws \InvalidArgumentException If the user info is invalid.     */    private function filterUserInfoComponent($component): string    {        if (!is_string($component)) {            throw new \InvalidArgumentException('User info must be a string');        }        return preg_replace_callback(            '/(?:[^%'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',            [$this, 'rawurlencodeMatchZero'],            $component        );    }    /**     * @param mixed $host     *     * @throws \InvalidArgumentException If the host is invalid.     */    private function filterHost($host): string    {        if (!is_string($host)) {            throw new \InvalidArgumentException('Host must be a string');        }        return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');    }    /**     * @param mixed $port     *     * @throws \InvalidArgumentException If the port is invalid.     */    private function filterPort($port): ?int    {        if ($port === null) {            return null;        }        $port = (int) $port;        if (0 > $port || 0xFFFF < $port) {            throw new \InvalidArgumentException(                sprintf('Invalid port: %d. Must be between 0 and 65535', $port)            );        }        return $port;    }    /**     * @param (string|int)[] $keys     *     * @return string[]     */    private static function getFilteredQueryString(UriInterface $uri, array $keys): array    {        $current = $uri->getQuery();        if ($current === '') {            return [];        }        $decodedKeys = array_map(function ($k): string {            return rawurldecode((string) $k);        }, $keys);        return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {            return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);        });    }    private static function generateQueryString(string $key, ?string $value): string    {        // Query string separators ("=", "&") within the key or value need to be encoded        // (while preventing double-encoding) before setting the query string. All other        // chars that need percent-encoding will be encoded by withQuery().        $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);        if ($value !== null) {            $queryString .= '='.strtr($value, self::QUERY_SEPARATORS_REPLACEMENT);        }        return $queryString;    }    private function removeDefaultPort(): void    {        if ($this->port !== null && self::isDefaultPort($this)) {            $this->port = null;        }    }    /**     * Filters the path of a URI     *     * @param mixed $path     *     * @throws \InvalidArgumentException If the path is invalid.     */    private function filterPath($path): string    {        if (!is_string($path)) {            throw new \InvalidArgumentException('Path must be a string');        }        return preg_replace_callback(            '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',            [$this, 'rawurlencodeMatchZero'],            $path        );    }    /**     * Filters the query string or fragment of a URI.     *     * @param mixed $str     *     * @throws \InvalidArgumentException If the query or fragment is invalid.     */    private function filterQueryAndFragment($str): string    {        if (!is_string($str)) {            throw new \InvalidArgumentException('Query and fragment must be a string');        }        return preg_replace_callback(            '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',            [$this, 'rawurlencodeMatchZero'],            $str        );    }    private function rawurlencodeMatchZero(array $match): string    {        return rawurlencode($match[0]);    }    private function validateState(): void    {        if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {            $this->host = self::HTTP_DEFAULT_HOST;        }        if ($this->getAuthority() === '') {            if (0 === strpos($this->path, '//')) {                throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"');            }            if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {                throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon');            }        }    }}
 |