composer package updates
[openemr.git] / vendor / zendframework / zend-uri / src / Uri.php
blob423eda6bdb0f6abb33fc3565b080ca9bcbeffd62
1 <?php
2 /**
3 * @see https://github.com/zendframework/zend-uri for the canonical source repository
4 * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (https://www.zend.com)
5 * @license https://github.com/zendframework/zend-uri/blob/master/LICENSE.md New BSD License
6 */
8 namespace Zend\Uri;
10 use Zend\Escaper\Escaper;
11 use Zend\Validator;
13 /**
14 * Generic URI handler
16 class Uri implements UriInterface
18 /**
19 * Character classes defined in RFC-3986
21 const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
22 const CHAR_GEN_DELIMS = ':\/\?#\[\]@';
23 const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
24 const CHAR_RESERVED = ':\/\?#\[\]@!\$&\'\(\)\*\+,;=';
25 /**
26 * Not in the spec - those characters have special meaning in urlencoded query parameters
28 const CHAR_QUERY_DELIMS = '!\$\'\(\)\*\,';
30 /**
31 * Host part types represented as binary masks
32 * The binary mask consists of 5 bits in the following order:
33 * <RegName> | <DNS> | <IPvFuture> | <IPv6> | <IPv4>
34 * Place 1 or 0 in the different positions for enable or disable the part.
35 * Finally use a hexadecimal representation.
37 const HOST_IPV4 = 0x01; //00001
38 const HOST_IPV6 = 0x02; //00010
39 const HOST_IPVFUTURE = 0x04; //00100
40 const HOST_IPVANY = 0x07; //00111
41 const HOST_DNS = 0x08; //01000
42 const HOST_DNS_OR_IPV4 = 0x09; //01001
43 const HOST_DNS_OR_IPV6 = 0x0A; //01010
44 const HOST_DNS_OR_IPV4_OR_IPV6 = 0x0B; //01011
45 const HOST_DNS_OR_IPVANY = 0x0F; //01111
46 const HOST_REGNAME = 0x10; //10000
47 const HOST_DNS_OR_IPV4_OR_IPV6_OR_REGNAME = 0x1B; //11011
48 const HOST_ALL = 0x1F; //11111
50 /**
51 * URI scheme
53 * @var string
55 protected $scheme;
57 /**
58 * URI userInfo part (usually user:password in HTTP URLs)
60 * @var string
62 protected $userInfo;
64 /**
65 * URI hostname
67 * @var string
69 protected $host;
71 /**
72 * URI port
74 * @var int
76 protected $port;
78 /**
79 * URI path
81 * @var string
83 protected $path;
85 /**
86 * URI query string
88 * @var string
90 protected $query;
92 /**
93 * URI fragment
95 * @var string
97 protected $fragment;
99 /**
100 * Which host part types are valid for this URI?
102 * @var int
104 protected $validHostTypes = self::HOST_ALL;
107 * Array of valid schemes.
109 * Subclasses of this class that only accept specific schemes may set the
110 * list of accepted schemes here. If not empty, when setScheme() is called
111 * it will only accept the schemes listed here.
113 * @var array
115 protected static $validSchemes = [];
118 * List of default ports per scheme
120 * Inheriting URI classes may set this, and the normalization methods will
121 * automatically remove the port if it is equal to the default port for the
122 * current scheme
124 * @var array
126 protected static $defaultPorts = [];
129 * @var Escaper
131 protected static $escaper;
134 * Create a new URI object
136 * @param Uri|string|null $uri
137 * @throws Exception\InvalidArgumentException
139 public function __construct($uri = null)
141 if (is_string($uri)) {
142 $this->parse($uri);
143 } elseif ($uri instanceof UriInterface) {
144 // Copy constructor
145 $this->setScheme($uri->getScheme());
146 $this->setUserInfo($uri->getUserInfo());
147 $this->setHost($uri->getHost());
148 $this->setPort($uri->getPort());
149 $this->setPath($uri->getPath());
150 $this->setQuery($uri->getQuery());
151 $this->setFragment($uri->getFragment());
152 } elseif ($uri !== null) {
153 throw new Exception\InvalidArgumentException(sprintf(
154 'Expecting a string or a URI object, received "%s"',
155 (is_object($uri) ? get_class($uri) : gettype($uri))
161 * Set Escaper instance
163 * @param Escaper $escaper
165 public static function setEscaper(Escaper $escaper)
167 static::$escaper = $escaper;
171 * Retrieve Escaper instance
173 * Lazy-loads one if none provided
175 * @return Escaper
177 public static function getEscaper()
179 if (null === static::$escaper) {
180 static::setEscaper(new Escaper());
182 return static::$escaper;
186 * Check if the URI is valid
188 * Note that a relative URI may still be valid
190 * @return bool
192 public function isValid()
194 if ($this->host) {
195 if (strlen($this->path) > 0 && substr($this->path, 0, 1) != '/') {
196 return false;
198 return true;
201 if ($this->userInfo || $this->port) {
202 return false;
205 if ($this->path) {
206 // Check path-only (no host) URI
207 if (substr($this->path, 0, 2) == '//') {
208 return false;
210 return true;
213 if (! ($this->query || $this->fragment)) {
214 // No host, path, query or fragment - this is not a valid URI
215 return false;
218 return true;
222 * Check if the URI is a valid relative URI
224 * @return bool
226 public function isValidRelative()
228 if ($this->scheme || $this->host || $this->userInfo || $this->port) {
229 return false;
232 if ($this->path) {
233 // Check path-only (no host) URI
234 if (substr($this->path, 0, 2) == '//') {
235 return false;
237 return true;
240 if (! ($this->query || $this->fragment)) {
241 // No host, path, query or fragment - this is not a valid URI
242 return false;
245 return true;
249 * Check if the URI is an absolute or relative URI
251 * @return bool
253 public function isAbsolute()
255 return ($this->scheme !== null);
259 * Reset URI parts
261 protected function reset()
263 $this->setScheme(null);
264 $this->setPort(null);
265 $this->setUserInfo(null);
266 $this->setHost(null);
267 $this->setPath(null);
268 $this->setFragment(null);
269 $this->setQuery(null);
273 * Parse a URI string
275 * @param string $uri
276 * @return Uri
278 public function parse($uri)
280 $this->reset();
282 // Capture scheme
283 if (($scheme = self::parseScheme($uri)) !== null) {
284 $this->setScheme($scheme);
285 $uri = substr($uri, strlen($scheme) + 1) ?: '';
288 // Capture authority part
289 if (preg_match('|^//([^/\?#]*)|', $uri, $match)) {
290 $authority = $match[1];
291 $uri = substr($uri, strlen($match[0]));
293 // Split authority into userInfo and host
294 if (strpos($authority, '@') !== false) {
295 // The userInfo can also contain '@' symbols; split $authority
296 // into segments, and set it to the last segment.
297 $segments = explode('@', $authority);
298 $authority = array_pop($segments);
299 $userInfo = implode('@', $segments);
300 unset($segments);
301 $this->setUserInfo($userInfo);
304 $nMatches = preg_match('/:[\d]{1,5}$/', $authority, $matches);
305 if ($nMatches === 1) {
306 $portLength = strlen($matches[0]);
307 $port = substr($matches[0], 1);
309 $this->setPort((int) $port);
310 $authority = substr($authority, 0, -$portLength);
313 $this->setHost($authority);
316 if (! $uri) {
317 return $this;
320 // Capture the path
321 if (preg_match('|^[^\?#]*|', $uri, $match)) {
322 $this->setPath($match[0]);
323 $uri = substr($uri, strlen($match[0]));
326 if (! $uri) {
327 return $this;
330 // Capture the query
331 if (preg_match('|^\?([^#]*)|', $uri, $match)) {
332 $this->setQuery($match[1]);
333 $uri = substr($uri, strlen($match[0]));
335 if (! $uri) {
336 return $this;
339 // All that's left is the fragment
340 if ($uri && substr($uri, 0, 1) == '#') {
341 $this->setFragment(substr($uri, 1));
344 return $this;
348 * Compose the URI into a string
350 * @return string
351 * @throws Exception\InvalidUriException
353 public function toString()
355 if (! $this->isValid()) {
356 if ($this->isAbsolute() || ! $this->isValidRelative()) {
357 throw new Exception\InvalidUriException(
358 'URI is not valid and cannot be converted into a string'
363 $uri = '';
365 if ($this->scheme) {
366 $uri .= $this->scheme . ':';
369 if ($this->host !== null) {
370 $uri .= '//';
371 if ($this->userInfo) {
372 $uri .= $this->userInfo . '@';
374 $uri .= $this->host;
375 if ($this->port) {
376 $uri .= ':' . $this->port;
380 if ($this->path) {
381 $uri .= static::encodePath($this->path);
382 } elseif ($this->host && ($this->query || $this->fragment)) {
383 $uri .= '/';
386 if ($this->query) {
387 $uri .= "?" . static::encodeQueryFragment($this->query);
390 if ($this->fragment) {
391 $uri .= "#" . static::encodeQueryFragment($this->fragment);
394 return $uri;
398 * Normalize the URI
400 * Normalizing a URI includes removing any redundant parent directory or
401 * current directory references from the path (e.g. foo/bar/../baz becomes
402 * foo/baz), normalizing the scheme case, decoding any over-encoded
403 * characters etc.
405 * Eventually, two normalized URLs pointing to the same resource should be
406 * equal even if they were originally represented by two different strings
408 * @return Uri
410 public function normalize()
412 if ($this->scheme) {
413 $this->scheme = static::normalizeScheme($this->scheme);
416 if ($this->host) {
417 $this->host = static::normalizeHost($this->host);
420 if ($this->port) {
421 $this->port = static::normalizePort($this->port, $this->scheme);
424 if ($this->path) {
425 $this->path = static::normalizePath($this->path);
428 if ($this->query) {
429 $this->query = static::normalizeQuery($this->query);
432 if ($this->fragment) {
433 $this->fragment = static::normalizeFragment($this->fragment);
436 // If path is empty (and we have a host), path should be '/'
437 // Isn't this valid ONLY for HTTP-URI?
438 if ($this->host && empty($this->path)) {
439 $this->path = '/';
442 return $this;
446 * Convert a relative URI into an absolute URI using a base absolute URI as
447 * a reference.
449 * This is similar to merge() - only it uses the supplied URI as the
450 * base reference instead of using the current URI as the base reference.
452 * Merging algorithm is adapted from RFC-3986 section 5.2
453 * (@link http://tools.ietf.org/html/rfc3986#section-5.2)
455 * @param Uri|string $baseUri
456 * @throws Exception\InvalidArgumentException
457 * @return Uri
459 public function resolve($baseUri)
461 // Ignore if URI is absolute
462 if ($this->isAbsolute()) {
463 return $this;
466 if (is_string($baseUri)) {
467 $baseUri = new static($baseUri);
468 } elseif (! $baseUri instanceof Uri) {
469 throw new Exception\InvalidArgumentException(
470 'Provided base URI must be a string or a Uri object'
474 // Merging starts here...
475 if ($this->getHost()) {
476 $this->setPath(static::removePathDotSegments($this->getPath()));
477 } else {
478 $basePath = $baseUri->getPath();
479 $relPath = $this->getPath();
480 if (! $relPath) {
481 $this->setPath($basePath);
482 if (! $this->getQuery()) {
483 $this->setQuery($baseUri->getQuery());
485 } else {
486 if (substr($relPath, 0, 1) == '/') {
487 $this->setPath(static::removePathDotSegments($relPath));
488 } else {
489 if ($baseUri->getHost() && ! $basePath) {
490 $mergedPath = '/';
491 } else {
492 $mergedPath = substr($basePath, 0, strrpos($basePath, '/') + 1);
494 $this->setPath(static::removePathDotSegments($mergedPath . $relPath));
498 // Set the authority part
499 $this->setUserInfo($baseUri->getUserInfo());
500 $this->setHost($baseUri->getHost());
501 $this->setPort($baseUri->getPort());
504 $this->setScheme($baseUri->getScheme());
505 return $this;
509 * Convert the link to a relative link by substracting a base URI
511 * This is the opposite of resolving a relative link - i.e. creating a
512 * relative reference link from an original URI and a base URI.
514 * If the two URIs do not intersect (e.g. the original URI is not in any
515 * way related to the base URI) the URI will not be modified.
517 * @param Uri|string $baseUri
518 * @return Uri
520 public function makeRelative($baseUri)
522 // Copy base URI, we should not modify it
523 $baseUri = new static($baseUri);
525 $this->normalize();
526 $baseUri->normalize();
528 $host = $this->getHost();
529 $baseHost = $baseUri->getHost();
530 if ($host && $baseHost && ($host != $baseHost)) {
531 // Not the same hostname
532 return $this;
535 $port = $this->getPort();
536 $basePort = $baseUri->getPort();
537 if ($port && $basePort && ($port != $basePort)) {
538 // Not the same port
539 return $this;
542 $scheme = $this->getScheme();
543 $baseScheme = $baseUri->getScheme();
544 if ($scheme && $baseScheme && ($scheme != $baseScheme)) {
545 // Not the same scheme (e.g. HTTP vs. HTTPS)
546 return $this;
549 // Remove host, port and scheme
550 $this->setHost(null)
551 ->setPort(null)
552 ->setScheme(null);
554 // Is path the same?
555 if ($this->getPath() == $baseUri->getPath()) {
556 $this->setPath('');
557 return $this;
560 $pathParts = preg_split('|(/)|', $this->getPath(), null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
561 $baseParts = preg_split('|(/)|', $baseUri->getPath(), null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
563 // Get the intersection of existing path parts and those from the
564 // provided URI
565 $matchingParts = array_intersect_assoc($pathParts, $baseParts);
567 // Loop through the matches
568 foreach ($matchingParts as $index => $segment) {
569 // If we skip an index at any point, we have parent traversal, and
570 // need to prepend the path accordingly
571 if ($index && ! isset($matchingParts[$index - 1])) {
572 array_unshift($pathParts, '../');
573 continue;
576 // Otherwise, we simply unset the given path segment
577 unset($pathParts[$index]);
580 // Reset the path by imploding path segments
581 $this->setPath(implode($pathParts));
583 return $this;
587 * Get the scheme part of the URI
589 * @return string|null
591 public function getScheme()
593 return $this->scheme;
597 * Get the User-info (usually user:password) part
599 * @return string|null
601 public function getUserInfo()
603 return $this->userInfo;
607 * Get the URI host
609 * @return string|null
611 public function getHost()
613 return $this->host;
617 * Get the URI port
619 * @return int|null
621 public function getPort()
623 return $this->port;
627 * Get the URI path
629 * @return string|null
631 public function getPath()
633 return $this->path;
637 * Get the URI query
639 * @return string|null
641 public function getQuery()
643 return $this->query;
647 * Return the query string as an associative array of key => value pairs
649 * This is an extension to RFC-3986 but is quite useful when working with
650 * most common URI types
652 * @return array
654 public function getQueryAsArray()
656 $query = [];
657 if ($this->query) {
658 parse_str($this->query, $query);
661 return $query;
665 * Get the URI fragment
667 * @return string|null
669 public function getFragment()
671 return $this->fragment;
675 * Set the URI scheme
677 * If the scheme is not valid according to the generic scheme syntax or
678 * is not acceptable by the specific URI class (e.g. 'http' or 'https' are
679 * the only acceptable schemes for the Zend\Uri\Http class) an exception
680 * will be thrown.
682 * You can check if a scheme is valid before setting it using the
683 * validateScheme() method.
685 * @param string $scheme
686 * @throws Exception\InvalidUriPartException
687 * @return Uri
689 public function setScheme($scheme)
691 if (($scheme !== null) && (! self::validateScheme($scheme))) {
692 throw new Exception\InvalidUriPartException(sprintf(
693 'Scheme "%s" is not valid or is not accepted by %s',
694 $scheme,
695 get_class($this)
696 ), Exception\InvalidUriPartException::INVALID_SCHEME);
699 $this->scheme = $scheme;
700 return $this;
704 * Set the URI User-info part (usually user:password)
706 * @param string $userInfo
707 * @return Uri
708 * @throws Exception\InvalidUriPartException If the schema definition
709 * does not have this part
711 public function setUserInfo($userInfo)
713 $this->userInfo = $userInfo;
714 return $this;
718 * Set the URI host
720 * Note that the generic syntax for URIs allows using host names which
721 * are not necessarily IPv4 addresses or valid DNS host names. For example,
722 * IPv6 addresses are allowed as well, and also an abstract "registered name"
723 * which may be any name composed of a valid set of characters, including,
724 * for example, tilda (~) and underscore (_) which are not allowed in DNS
725 * names.
727 * Subclasses of Uri may impose more strict validation of host names - for
728 * example the HTTP RFC clearly states that only IPv4 and valid DNS names
729 * are allowed in HTTP URIs.
731 * @param string $host
732 * @throws Exception\InvalidUriPartException
733 * @return Uri
735 public function setHost($host)
737 if (($host !== '')
738 && ($host !== null)
739 && ! self::validateHost($host, $this->validHostTypes)
741 throw new Exception\InvalidUriPartException(sprintf(
742 'Host "%s" is not valid or is not accepted by %s',
743 $host,
744 get_class($this)
745 ), Exception\InvalidUriPartException::INVALID_HOSTNAME);
748 $this->host = $host;
749 return $this;
753 * Set the port part of the URI
755 * @param int $port
756 * @return Uri
758 public function setPort($port)
760 $this->port = $port;
761 return $this;
765 * Set the path
767 * @param string $path
768 * @return Uri
770 public function setPath($path)
772 $this->path = $path;
773 return $this;
777 * Set the query string
779 * If an array is provided, will encode this array of parameters into a
780 * query string. Array values will be represented in the query string using
781 * PHP's common square bracket notation.
783 * @param string|array $query
784 * @return Uri
786 public function setQuery($query)
788 if (is_array($query)) {
789 // We replace the + used for spaces by http_build_query with the
790 // more standard %20.
791 $query = str_replace('+', '%20', http_build_query($query));
794 $this->query = $query;
795 return $this;
799 * Set the URI fragment part
801 * @param string $fragment
802 * @return Uri
803 * @throws Exception\InvalidUriPartException If the schema definition
804 * does not have this part
806 public function setFragment($fragment)
808 $this->fragment = $fragment;
809 return $this;
813 * Magic method to convert the URI to a string
815 * @return string
817 public function __toString()
819 try {
820 return $this->toString();
821 } catch (\Exception $e) {
822 return '';
827 * Encoding and Validation Methods
831 * Check if a scheme is valid or not
833 * Will check $scheme to be valid against the generic scheme syntax defined
834 * in RFC-3986. If the class also defines specific acceptable schemes, will
835 * also check that $scheme is one of them.
837 * @param string $scheme
838 * @return bool
840 public static function validateScheme($scheme)
842 if (! empty(static::$validSchemes)
843 && ! in_array(strtolower($scheme), static::$validSchemes)
845 return false;
848 return (bool) preg_match('/^[A-Za-z][A-Za-z0-9\-\.+]*$/', $scheme);
852 * Check that the userInfo part of a URI is valid
854 * @param string $userInfo
855 * @return bool
857 public static function validateUserInfo($userInfo)
859 $regex = '/^(?:[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ':]+|%[A-Fa-f0-9]{2})*$/';
860 return (bool) preg_match($regex, $userInfo);
864 * Validate the host part
866 * Users may control which host types to allow by passing a second parameter
867 * with a bitmask of HOST_* constants which are allowed. If not specified,
868 * all address types will be allowed.
870 * Note that the generic URI syntax allows different host representations,
871 * including IPv4 addresses, IPv6 addresses and future IP address formats
872 * enclosed in square brackets, and registered names which may be DNS names
873 * or even more complex names. This is different (and is much more loose)
874 * from what is commonly accepted as valid HTTP URLs for example.
876 * @param string $host
877 * @param int $allowed bitmask of allowed host types
878 * @return bool
880 public static function validateHost($host, $allowed = self::HOST_ALL)
883 * "first-match-wins" algorithm (RFC 3986):
884 * If host matches the rule for IPv4address, then it should be
885 * considered an IPv4 address literal and not a reg-name
887 if ($allowed & self::HOST_IPVANY) {
888 if (static::isValidIpAddress($host, $allowed)) {
889 return true;
893 if ($allowed & self::HOST_REGNAME) {
894 if (static::isValidRegName($host)) {
895 return true;
899 if ($allowed & self::HOST_DNS) {
900 if (static::isValidDnsHostname($host)) {
901 return true;
905 return false;
909 * Validate the port
911 * Valid values include numbers between 1 and 65535, and empty values
913 * @param int $port
914 * @return bool
916 public static function validatePort($port)
918 if ($port === 0) {
919 return false;
922 if ($port) {
923 $port = (int) $port;
924 if ($port < 1 || $port > 0xffff) {
925 return false;
929 return true;
933 * Validate the path
935 * @param string $path
936 * @return bool
938 public static function validatePath($path)
940 $pchar = '(?:[' . self::CHAR_UNRESERVED . ':@&=\+\$,]+|%[A-Fa-f0-9]{2})*';
941 $segment = $pchar . "(?:;{$pchar})*";
942 $regex = "/^{$segment}(?:\/{$segment})*$/";
943 return (bool) preg_match($regex, $path);
947 * Check if a URI query or fragment part is valid or not
949 * Query and Fragment parts are both restricted by the same syntax rules,
950 * so the same validation method can be used for both.
952 * You can encode a query or fragment part to ensure it is valid by passing
953 * it through the encodeQueryFragment() method.
955 * @param string $input
956 * @return bool
958 public static function validateQueryFragment($input)
960 $regex = '/^(?:[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ':@\/\?]+|%[A-Fa-f0-9]{2})*$/';
961 return (bool) preg_match($regex, $input);
965 * URL-encode the user info part of a URI
967 * @param string $userInfo
968 * @return string
969 * @throws Exception\InvalidArgumentException
971 public static function encodeUserInfo($userInfo)
973 if (! is_string($userInfo)) {
974 throw new Exception\InvalidArgumentException(sprintf(
975 'Expecting a string, got %s',
976 (is_object($userInfo) ? get_class($userInfo) : gettype($userInfo))
980 $regex = '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:]|%(?![A-Fa-f0-9]{2}))/';
981 $escaper = static::getEscaper();
982 $replace = function ($match) use ($escaper) {
983 return $escaper->escapeUrl($match[0]);
986 return preg_replace_callback($regex, $replace, $userInfo);
990 * Encode the path
992 * Will replace all characters which are not strictly allowed in the path
993 * part with percent-encoded representation
995 * @param string $path
996 * @throws Exception\InvalidArgumentException
997 * @return string
999 public static function encodePath($path)
1001 if (! is_string($path)) {
1002 throw new Exception\InvalidArgumentException(sprintf(
1003 'Expecting a string, got %s',
1004 (is_object($path) ? get_class($path) : gettype($path))
1008 $regex = '/(?:[^' . self::CHAR_UNRESERVED . ')(:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/';
1009 $escaper = static::getEscaper();
1010 $replace = function ($match) use ($escaper) {
1011 return $escaper->escapeUrl($match[0]);
1014 return preg_replace_callback($regex, $replace, $path);
1018 * URL-encode a query string or fragment based on RFC-3986 guidelines.
1020 * Note that query and fragment encoding allows more unencoded characters
1021 * than the usual rawurlencode() function would usually return - for example
1022 * '/' and ':' are allowed as literals.
1024 * @param string $input
1025 * @return string
1026 * @throws Exception\InvalidArgumentException
1028 public static function encodeQueryFragment($input)
1030 if (! is_string($input)) {
1031 throw new Exception\InvalidArgumentException(sprintf(
1032 'Expecting a string, got %s',
1033 (is_object($input) ? get_class($input) : gettype($input))
1037 $regex = '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/';
1038 $escaper = static::getEscaper();
1039 $replace = function ($match) use ($escaper) {
1040 return $escaper->escapeUrl($match[0]);
1043 return preg_replace_callback($regex, $replace, $input);
1047 * Extract only the scheme part out of a URI string.
1049 * This is used by the parse() method, but is useful as a standalone public
1050 * method if one wants to test a URI string for it's scheme before doing
1051 * anything with it.
1053 * Will return the scheme if found, or NULL if no scheme found (URI may
1054 * still be valid, but not full)
1056 * @param string $uriString
1057 * @throws Exception\InvalidArgumentException
1058 * @return string|null
1060 public static function parseScheme($uriString)
1062 if (! is_string($uriString)) {
1063 throw new Exception\InvalidArgumentException(sprintf(
1064 'Expecting a string, got %s',
1065 (is_object($uriString) ? get_class($uriString) : gettype($uriString))
1069 if (preg_match('/^([A-Za-z][A-Za-z0-9\.\+\-]*):/', $uriString, $match)) {
1070 return $match[1];
1073 return;
1077 * Remove any extra dot segments (/../, /./) from a path
1079 * Algorithm is adapted from RFC-3986 section 5.2.4
1080 * (@link http://tools.ietf.org/html/rfc3986#section-5.2.4)
1082 * @todo consider optimizing
1084 * @param string $path
1085 * @return string
1087 public static function removePathDotSegments($path)
1089 $output = '';
1091 while ($path) {
1092 if ($path == '..' || $path == '.') {
1093 break;
1096 switch (true) {
1097 case ($path == '/.'):
1098 $path = '/';
1099 break;
1100 case ($path == '/..'):
1101 $path = '/';
1102 $lastSlashPos = strrpos($output, '/', -1);
1103 if (false === $lastSlashPos) {
1104 break;
1106 $output = substr($output, 0, $lastSlashPos);
1107 break;
1108 case (substr($path, 0, 4) == '/../'):
1109 $path = '/' . substr($path, 4);
1110 $lastSlashPos = strrpos($output, '/', -1);
1111 if (false === $lastSlashPos) {
1112 break;
1114 $output = substr($output, 0, $lastSlashPos);
1115 break;
1116 case (substr($path, 0, 3) == '/./'):
1117 $path = substr($path, 2);
1118 break;
1119 case (substr($path, 0, 2) == './'):
1120 $path = substr($path, 2);
1121 break;
1122 case (substr($path, 0, 3) == '../'):
1123 $path = substr($path, 3);
1124 break;
1125 default:
1126 $slash = strpos($path, '/', 1);
1127 if ($slash === false) {
1128 $seg = $path;
1129 } else {
1130 $seg = substr($path, 0, $slash);
1133 $output .= $seg;
1134 $path = substr($path, strlen($seg));
1135 break;
1139 return $output;
1143 * Merge a base URI and a relative URI into a new URI object
1145 * This convenience method wraps ::resolve() to allow users to quickly
1146 * create new absolute URLs without the need to instantiate and clone
1147 * URI objects.
1149 * If objects are passed in, none of the passed objects will be modified.
1151 * @param Uri|string $baseUri
1152 * @param Uri|string $relativeUri
1153 * @return Uri
1155 public static function merge($baseUri, $relativeUri)
1157 $uri = new static($relativeUri);
1158 return $uri->resolve($baseUri);
1162 * Check if a host name is a valid IP address, depending on allowed IP address types
1164 * @param string $host
1165 * @param int $allowed allowed address types
1166 * @return bool
1168 protected static function isValidIpAddress($host, $allowed)
1170 $validatorParams = [
1171 'allowipv4' => (bool) ($allowed & self::HOST_IPV4),
1172 'allowipv6' => false,
1173 'allowipvfuture' => false,
1174 'allowliteral' => false,
1177 // Test only IPv4
1178 $validator = new Validator\Ip($validatorParams);
1179 $return = $validator->isValid($host);
1180 if ($return) {
1181 return true;
1184 // IPv6 & IPvLiteral must be in literal format
1185 $validatorParams = [
1186 'allowipv4' => false,
1187 'allowipv6' => (bool) ($allowed & self::HOST_IPV6),
1188 'allowipvfuture' => (bool) ($allowed & self::HOST_IPVFUTURE),
1189 'allowliteral' => true,
1191 static $regex = '/^\[.*\]$/';
1192 $validator->setOptions($validatorParams);
1193 return (preg_match($regex, $host) && $validator->isValid($host));
1197 * Check if an address is a valid DNS hostname
1199 * @param string $host
1200 * @return bool
1202 protected static function isValidDnsHostname($host)
1204 $validator = new Validator\Hostname([
1205 'allow' => Validator\Hostname::ALLOW_DNS | Validator\Hostname::ALLOW_LOCAL,
1208 return $validator->isValid($host);
1212 * Check if an address is a valid registered name (as defined by RFC-3986) address
1214 * @param string $host
1215 * @return bool
1217 protected static function isValidRegName($host)
1219 $regex = '/^(?:[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ':@\/\?]+|%[A-Fa-f0-9]{2})+$/';
1220 return (bool) preg_match($regex, $host);
1224 * Part normalization methods
1226 * These are called by normalize() using static::_normalize*() so they may
1227 * be extended or overridden by extending classes to implement additional
1228 * scheme specific normalization rules
1232 * Normalize the scheme
1234 * Usually this means simply converting the scheme to lower case
1236 * @param string $scheme
1237 * @return string
1239 protected static function normalizeScheme($scheme)
1241 return strtolower($scheme);
1245 * Normalize the host part
1247 * By default this converts host names to lower case
1249 * @param string $host
1250 * @return string
1252 protected static function normalizeHost($host)
1254 return strtolower($host);
1258 * Normalize the port
1260 * If the class defines a default port for the current scheme, and the
1261 * current port is default, it will be unset.
1263 * @param int $port
1264 * @param string $scheme
1265 * @return int|null
1267 protected static function normalizePort($port, $scheme = null)
1269 if ($scheme
1270 && isset(static::$defaultPorts[$scheme])
1271 && ($port == static::$defaultPorts[$scheme])
1273 return;
1276 return $port;
1280 * Normalize the path
1282 * This involves removing redundant dot segments, decoding any over-encoded
1283 * characters and encoding everything that needs to be encoded and is not
1285 * @param string $path
1286 * @return string
1288 protected static function normalizePath($path)
1290 $path = self::encodePath(
1291 self::decodeUrlEncodedChars(
1292 self::removePathDotSegments($path),
1293 '/[' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]/'
1297 return $path;
1301 * Normalize the query part
1303 * This involves decoding everything that doesn't need to be encoded, and
1304 * encoding everything else
1306 * @param string $query
1307 * @return string
1309 protected static function normalizeQuery($query)
1311 $query = self::encodeQueryFragment(
1312 self::decodeUrlEncodedChars(
1313 $query,
1314 '/[' . self::CHAR_UNRESERVED . self::CHAR_QUERY_DELIMS . ':@\/\?]/'
1318 return $query;
1322 * Normalize the fragment part
1324 * Currently this is exactly the same as normalizeQuery().
1326 * @param string $fragment
1327 * @return string
1329 protected static function normalizeFragment($fragment)
1331 $fragment = self::encodeQueryFragment(
1332 self::decodeUrlEncodedChars(
1333 $fragment,
1334 '/[' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]/'
1338 return $fragment;
1342 * Decode all percent encoded characters which are allowed to be represented literally
1344 * Will not decode any characters which are not listed in the 'allowed' list
1346 * @param string $input
1347 * @param string $allowed Pattern of allowed characters
1348 * @return mixed
1350 protected static function decodeUrlEncodedChars($input, $allowed = '')
1352 $decodeCb = function ($match) use ($allowed) {
1353 $char = rawurldecode($match[0]);
1354 if (preg_match($allowed, $char)) {
1355 return $char;
1357 return strtoupper($match[0]);
1360 return preg_replace_callback('/%[A-Fa-f0-9]{2}/', $decodeCb, $input);