composer package updates
[openemr.git] / vendor / zendframework / zend-diactoros / src / Uri.php
blobd9833f63b260ca9989756a6ef71c0b0c6839e39f
1 <?php
2 /**
3 * @see https://github.com/zendframework/zend-diactoros for the canonical source repository
4 * @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
5 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
6 */
8 namespace Zend\Diactoros;
10 use InvalidArgumentException;
11 use Psr\Http\Message\UriInterface;
13 use function array_key_exists;
14 use function array_keys;
15 use function count;
16 use function explode;
17 use function get_class;
18 use function gettype;
19 use function implode;
20 use function is_numeric;
21 use function is_object;
22 use function is_string;
23 use function ltrim;
24 use function parse_url;
25 use function preg_replace;
26 use function preg_replace_callback;
27 use function rawurlencode;
28 use function sprintf;
29 use function strpos;
30 use function strtolower;
31 use function substr;
33 /**
34 * Implementation of Psr\Http\UriInterface.
36 * Provides a value object representing a URI for HTTP requests.
38 * Instances of this class are considered immutable; all methods that
39 * might change state are implemented such that they retain the internal
40 * state of the current instance and return a new instance that contains the
41 * changed state.
43 class Uri implements UriInterface
45 /**
46 * Sub-delimiters used in user info, query strings and fragments.
48 * @const string
50 const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
52 /**
53 * Unreserved characters used in user info, paths, query strings, and fragments.
55 * @const string
57 const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
59 /**
60 * @var int[] Array indexed by valid scheme names to their corresponding ports.
62 protected $allowedSchemes = [
63 'http' => 80,
64 'https' => 443,
67 /**
68 * @var string
70 private $scheme = '';
72 /**
73 * @var string
75 private $userInfo = '';
77 /**
78 * @var string
80 private $host = '';
82 /**
83 * @var int
85 private $port;
87 /**
88 * @var string
90 private $path = '';
92 /**
93 * @var string
95 private $query = '';
97 /**
98 * @var string
100 private $fragment = '';
103 * generated uri string cache
104 * @var string|null
106 private $uriString;
109 * @param string $uri
110 * @throws InvalidArgumentException on non-string $uri argument
112 public function __construct($uri = '')
114 if (! is_string($uri)) {
115 throw new InvalidArgumentException(sprintf(
116 'URI passed to constructor must be a string; received "%s"',
117 (is_object($uri) ? get_class($uri) : gettype($uri))
121 if ('' !== $uri) {
122 $this->parseUri($uri);
127 * Operations to perform on clone.
129 * Since cloning usually is for purposes of mutation, we reset the
130 * $uriString property so it will be re-calculated.
132 public function __clone()
134 $this->uriString = null;
138 * {@inheritdoc}
140 public function __toString()
142 if (null !== $this->uriString) {
143 return $this->uriString;
146 $this->uriString = static::createUriString(
147 $this->scheme,
148 $this->getAuthority(),
149 $this->getPath(), // Absolute URIs should use a "/" for an empty path
150 $this->query,
151 $this->fragment
154 return $this->uriString;
158 * {@inheritdoc}
160 public function getScheme()
162 return $this->scheme;
166 * {@inheritdoc}
168 public function getAuthority()
170 if ('' === $this->host) {
171 return '';
174 $authority = $this->host;
175 if ('' !== $this->userInfo) {
176 $authority = $this->userInfo . '@' . $authority;
179 if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
180 $authority .= ':' . $this->port;
183 return $authority;
187 * Retrieve the user-info part of the URI.
189 * This value is percent-encoded, per RFC 3986 Section 3.2.1.
191 * {@inheritdoc}
193 public function getUserInfo()
195 return $this->userInfo;
199 * {@inheritdoc}
201 public function getHost()
203 return $this->host;
207 * {@inheritdoc}
209 public function getPort()
211 return $this->isNonStandardPort($this->scheme, $this->host, $this->port)
212 ? $this->port
213 : null;
217 * {@inheritdoc}
219 public function getPath()
221 return $this->path;
225 * {@inheritdoc}
227 public function getQuery()
229 return $this->query;
233 * {@inheritdoc}
235 public function getFragment()
237 return $this->fragment;
241 * {@inheritdoc}
243 public function withScheme($scheme)
245 if (! is_string($scheme)) {
246 throw new InvalidArgumentException(sprintf(
247 '%s expects a string argument; received %s',
248 __METHOD__,
249 (is_object($scheme) ? get_class($scheme) : gettype($scheme))
253 $scheme = $this->filterScheme($scheme);
255 if ($scheme === $this->scheme) {
256 // Do nothing if no change was made.
257 return $this;
260 $new = clone $this;
261 $new->scheme = $scheme;
263 return $new;
267 * Create and return a new instance containing the provided user credentials.
269 * The value will be percent-encoded in the new instance, but with measures
270 * taken to prevent double-encoding.
272 * {@inheritdoc}
274 public function withUserInfo($user, $password = null)
276 if (! is_string($user)) {
277 throw new InvalidArgumentException(sprintf(
278 '%s expects a string user argument; received %s',
279 __METHOD__,
280 (is_object($user) ? get_class($user) : gettype($user))
283 if (null !== $password && ! is_string($password)) {
284 throw new InvalidArgumentException(sprintf(
285 '%s expects a string password argument; received %s',
286 __METHOD__,
287 (is_object($password) ? get_class($password) : gettype($password))
291 $info = $this->filterUserInfoPart($user);
292 if ($password) {
293 $info .= ':' . $this->filterUserInfoPart($password);
296 if ($info === $this->userInfo) {
297 // Do nothing if no change was made.
298 return $this;
301 $new = clone $this;
302 $new->userInfo = $info;
304 return $new;
308 * {@inheritdoc}
310 public function withHost($host)
312 if (! is_string($host)) {
313 throw new InvalidArgumentException(sprintf(
314 '%s expects a string argument; received %s',
315 __METHOD__,
316 (is_object($host) ? get_class($host) : gettype($host))
320 if ($host === $this->host) {
321 // Do nothing if no change was made.
322 return $this;
325 $new = clone $this;
326 $new->host = strtolower($host);
328 return $new;
332 * {@inheritdoc}
334 public function withPort($port)
336 if (! is_numeric($port) && $port !== null) {
337 throw new InvalidArgumentException(sprintf(
338 'Invalid port "%s" specified; must be an integer, an integer string, or null',
339 (is_object($port) ? get_class($port) : gettype($port))
343 if ($port !== null) {
344 $port = (int) $port;
347 if ($port === $this->port) {
348 // Do nothing if no change was made.
349 return $this;
352 if ($port !== null && ($port < 1 || $port > 65535)) {
353 throw new InvalidArgumentException(sprintf(
354 'Invalid port "%d" specified; must be a valid TCP/UDP port',
355 $port
359 $new = clone $this;
360 $new->port = $port;
362 return $new;
366 * {@inheritdoc}
368 public function withPath($path)
370 if (! is_string($path)) {
371 throw new InvalidArgumentException(
372 'Invalid path provided; must be a string'
376 if (strpos($path, '?') !== false) {
377 throw new InvalidArgumentException(
378 'Invalid path provided; must not contain a query string'
382 if (strpos($path, '#') !== false) {
383 throw new InvalidArgumentException(
384 'Invalid path provided; must not contain a URI fragment'
388 $path = $this->filterPath($path);
390 if ($path === $this->path) {
391 // Do nothing if no change was made.
392 return $this;
395 $new = clone $this;
396 $new->path = $path;
398 return $new;
402 * {@inheritdoc}
404 public function withQuery($query)
406 if (! is_string($query)) {
407 throw new InvalidArgumentException(
408 'Query string must be a string'
412 if (strpos($query, '#') !== false) {
413 throw new InvalidArgumentException(
414 'Query string must not include a URI fragment'
418 $query = $this->filterQuery($query);
420 if ($query === $this->query) {
421 // Do nothing if no change was made.
422 return $this;
425 $new = clone $this;
426 $new->query = $query;
428 return $new;
432 * {@inheritdoc}
434 public function withFragment($fragment)
436 if (! is_string($fragment)) {
437 throw new InvalidArgumentException(sprintf(
438 '%s expects a string argument; received %s',
439 __METHOD__,
440 (is_object($fragment) ? get_class($fragment) : gettype($fragment))
444 $fragment = $this->filterFragment($fragment);
446 if ($fragment === $this->fragment) {
447 // Do nothing if no change was made.
448 return $this;
451 $new = clone $this;
452 $new->fragment = $fragment;
454 return $new;
458 * Parse a URI into its parts, and set the properties
460 * @param string $uri
462 private function parseUri($uri)
464 $parts = parse_url($uri);
466 if (false === $parts) {
467 throw new \InvalidArgumentException(
468 'The source URI string appears to be malformed'
472 $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
473 $this->userInfo = isset($parts['user']) ? $this->filterUserInfoPart($parts['user']) : '';
474 $this->host = isset($parts['host']) ? strtolower($parts['host']) : '';
475 $this->port = isset($parts['port']) ? $parts['port'] : null;
476 $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
477 $this->query = isset($parts['query']) ? $this->filterQuery($parts['query']) : '';
478 $this->fragment = isset($parts['fragment']) ? $this->filterFragment($parts['fragment']) : '';
480 if (isset($parts['pass'])) {
481 $this->userInfo .= ':' . $parts['pass'];
486 * Create a URI string from its various parts
488 * @param string $scheme
489 * @param string $authority
490 * @param string $path
491 * @param string $query
492 * @param string $fragment
493 * @return string
495 private static function createUriString($scheme, $authority, $path, $query, $fragment)
497 $uri = '';
499 if ('' !== $scheme) {
500 $uri .= sprintf('%s:', $scheme);
503 if ('' !== $authority) {
504 $uri .= '//' . $authority;
507 if ('' !== $path && '/' !== substr($path, 0, 1)) {
508 $path = '/' . $path;
511 $uri .= $path;
514 if ('' !== $query) {
515 $uri .= sprintf('?%s', $query);
518 if ('' !== $fragment) {
519 $uri .= sprintf('#%s', $fragment);
522 return $uri;
526 * Is a given port non-standard for the current scheme?
528 * @param string $scheme
529 * @param string $host
530 * @param int $port
531 * @return bool
533 private function isNonStandardPort($scheme, $host, $port)
535 if ('' === $scheme) {
536 return '' === $host || null !== $port;
539 if ('' === $host || null === $port) {
540 return false;
543 return ! isset($this->allowedSchemes[$scheme]) || $port !== $this->allowedSchemes[$scheme];
547 * Filters the scheme to ensure it is a valid scheme.
549 * @param string $scheme Scheme name.
551 * @return string Filtered scheme.
553 private function filterScheme($scheme)
555 $scheme = strtolower($scheme);
556 $scheme = preg_replace('#:(//)?$#', '', $scheme);
558 if ('' === $scheme) {
559 return '';
562 if (! array_key_exists($scheme, $this->allowedSchemes)) {
563 throw new InvalidArgumentException(sprintf(
564 'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
565 $scheme,
566 implode(', ', array_keys($this->allowedSchemes))
570 return $scheme;
574 * Filters a part of user info in a URI to ensure it is properly encoded.
576 * @param string $part
577 * @return string
579 private function filterUserInfoPart($part)
581 // Note the addition of `%` to initial charset; this allows `|` portion
582 // to match and thus prevent double-encoding.
583 return preg_replace_callback(
584 '/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/u',
585 [$this, 'urlEncodeChar'],
586 $part
591 * Filters the path of a URI to ensure it is properly encoded.
593 * @param string $path
594 * @return string
596 private function filterPath($path)
598 $path = preg_replace_callback(
599 '/(?:[^' . self::CHAR_UNRESERVED . ')(:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
600 [$this, 'urlEncodeChar'],
601 $path
604 if ('' === $path) {
605 // No path
606 return $path;
609 if ($path[0] !== '/') {
610 // Relative path
611 return $path;
614 // Ensure only one leading slash, to prevent XSS attempts.
615 return '/' . ltrim($path, '/');
619 * Filter a query string to ensure it is propertly encoded.
621 * Ensures that the values in the query string are properly urlencoded.
623 * @param string $query
624 * @return string
626 private function filterQuery($query)
628 if ('' !== $query && strpos($query, '?') === 0) {
629 $query = substr($query, 1);
632 $parts = explode('&', $query);
633 foreach ($parts as $index => $part) {
634 list($key, $value) = $this->splitQueryValue($part);
635 if ($value === null) {
636 $parts[$index] = $this->filterQueryOrFragment($key);
637 continue;
639 $parts[$index] = sprintf(
640 '%s=%s',
641 $this->filterQueryOrFragment($key),
642 $this->filterQueryOrFragment($value)
646 return implode('&', $parts);
650 * Split a query value into a key/value tuple.
652 * @param string $value
653 * @return array A value with exactly two elements, key and value
655 private function splitQueryValue($value)
657 $data = explode('=', $value, 2);
658 if (1 === count($data)) {
659 $data[] = null;
661 return $data;
665 * Filter a fragment value to ensure it is properly encoded.
667 * @param string $fragment
668 * @return string
670 private function filterFragment($fragment)
672 if ('' !== $fragment && strpos($fragment, '#') === 0) {
673 $fragment = '%23' . substr($fragment, 1);
676 return $this->filterQueryOrFragment($fragment);
680 * Filter a query string key or value, or a fragment.
682 * @param string $value
683 * @return string
685 private function filterQueryOrFragment($value)
687 return preg_replace_callback(
688 '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
689 [$this, 'urlEncodeChar'],
690 $value
695 * URL encode a character returned by a regex.
697 * @param array $matches
698 * @return string
700 private function urlEncodeChar(array $matches)
702 return rawurlencode($matches[0]);