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