3 * Zend Framework (http://framework.zend.com/)
5 * @see http://github.com/zendframework/zend-diactoros for the canonical source repository
6 * @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
10 namespace Zend\Diactoros;
12 use InvalidArgumentException;
13 use Psr\Http\Message\UriInterface;
16 * Implementation of Psr\Http\UriInterface.
18 * Provides a value object representing a URI for HTTP requests.
20 * Instances of this class are considered immutable; all methods that
21 * might change state are implemented such that they retain the internal
22 * state of the current instance and return a new instance that contains the
25 class Uri implements UriInterface
28 * Sub-delimiters used in query strings and fragments.
32 const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
35 * Unreserved characters used in paths, query strings, and fragments.
39 const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
42 * @var int[] Array indexed by valid scheme names to their corresponding ports.
44 protected $allowedSchemes = [
57 private $userInfo = '';
82 private $fragment = '';
85 * generated uri string cache
92 * @throws InvalidArgumentException on non-string $uri argument
94 public function __construct($uri = '')
96 if (! is_string($uri)) {
97 throw new InvalidArgumentException(sprintf(
98 'URI passed to constructor must be a string; received "%s"',
99 (is_object($uri) ? get_class($uri) : gettype($uri))
104 $this->parseUri($uri);
109 * Operations to perform on clone.
111 * Since cloning usually is for purposes of mutation, we reset the
112 * $uriString property so it will be re-calculated.
114 public function __clone()
116 $this->uriString = null;
122 public function __toString()
124 if (null !== $this->uriString) {
125 return $this->uriString;
128 $this->uriString = static::createUriString(
130 $this->getAuthority(),
131 $this->getPath(), // Absolute URIs should use a "/" for an empty path
136 return $this->uriString;
142 public function getScheme()
144 return $this->scheme;
150 public function getAuthority()
152 if (empty($this->host)) {
156 $authority = $this->host;
157 if (! empty($this->userInfo)) {
158 $authority = $this->userInfo . '@' . $authority;
161 if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
162 $authority .= ':' . $this->port;
171 public function getUserInfo()
173 return $this->userInfo;
179 public function getHost()
187 public function getPort()
189 return $this->isNonStandardPort($this->scheme, $this->host, $this->port)
197 public function getPath()
205 public function getQuery()
213 public function getFragment()
215 return $this->fragment;
221 public function withScheme($scheme)
223 if (! is_string($scheme)) {
224 throw new InvalidArgumentException(sprintf(
225 '%s expects a string argument; received %s',
227 (is_object($scheme) ? get_class($scheme) : gettype($scheme))
231 $scheme = $this->filterScheme($scheme);
233 if ($scheme === $this->scheme) {
234 // Do nothing if no change was made.
239 $new->scheme = $scheme;
247 public function withUserInfo($user, $password = null)
249 if (! is_string($user)) {
250 throw new InvalidArgumentException(sprintf(
251 '%s expects a string user argument; received %s',
253 (is_object($user) ? get_class($user) : gettype($user))
256 if (null !== $password && ! is_string($password)) {
257 throw new InvalidArgumentException(sprintf(
258 '%s expects a string password argument; received %s',
260 (is_object($password) ? get_class($password) : gettype($password))
266 $info .= ':' . $password;
269 if ($info === $this->userInfo) {
270 // Do nothing if no change was made.
275 $new->userInfo = $info;
283 public function withHost($host)
285 if (! is_string($host)) {
286 throw new InvalidArgumentException(sprintf(
287 '%s expects a string argument; received %s',
289 (is_object($host) ? get_class($host) : gettype($host))
293 if ($host === $this->host) {
294 // Do nothing if no change was made.
307 public function withPort($port)
309 if (! is_numeric($port) && $port !== null) {
310 throw new InvalidArgumentException(sprintf(
311 'Invalid port "%s" specified; must be an integer, an integer string, or null',
312 (is_object($port) ? get_class($port) : gettype($port))
316 if ($port !== null) {
320 if ($port === $this->port) {
321 // Do nothing if no change was made.
325 if ($port !== null && $port < 1 || $port > 65535) {
326 throw new InvalidArgumentException(sprintf(
327 'Invalid port "%d" specified; must be a valid TCP/UDP port',
341 public function withPath($path)
343 if (! is_string($path)) {
344 throw new InvalidArgumentException(
345 'Invalid path provided; must be a string'
349 if (strpos($path, '?') !== false) {
350 throw new InvalidArgumentException(
351 'Invalid path provided; must not contain a query string'
355 if (strpos($path, '#') !== false) {
356 throw new InvalidArgumentException(
357 'Invalid path provided; must not contain a URI fragment'
361 $path = $this->filterPath($path);
363 if ($path === $this->path) {
364 // Do nothing if no change was made.
377 public function withQuery($query)
379 if (! is_string($query)) {
380 throw new InvalidArgumentException(
381 'Query string must be a string'
385 if (strpos($query, '#') !== false) {
386 throw new InvalidArgumentException(
387 'Query string must not include a URI fragment'
391 $query = $this->filterQuery($query);
393 if ($query === $this->query) {
394 // Do nothing if no change was made.
399 $new->query = $query;
407 public function withFragment($fragment)
409 if (! is_string($fragment)) {
410 throw new InvalidArgumentException(sprintf(
411 '%s expects a string argument; received %s',
413 (is_object($fragment) ? get_class($fragment) : gettype($fragment))
417 $fragment = $this->filterFragment($fragment);
419 if ($fragment === $this->fragment) {
420 // Do nothing if no change was made.
425 $new->fragment = $fragment;
431 * Parse a URI into its parts, and set the properties
435 private function parseUri($uri)
437 $parts = parse_url($uri);
439 if (false === $parts) {
440 throw new \InvalidArgumentException(
441 'The source URI string appears to be malformed'
445 $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
446 $this->userInfo = isset($parts['user']) ? $parts['user'] : '';
447 $this->host = isset($parts['host']) ? $parts['host'] : '';
448 $this->port = isset($parts['port']) ? $parts['port'] : null;
449 $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
450 $this->query = isset($parts['query']) ? $this->filterQuery($parts['query']) : '';
451 $this->fragment = isset($parts['fragment']) ? $this->filterFragment($parts['fragment']) : '';
453 if (isset($parts['pass'])) {
454 $this->userInfo .= ':' . $parts['pass'];
459 * Create a URI string from its various parts
461 * @param string $scheme
462 * @param string $authority
463 * @param string $path
464 * @param string $query
465 * @param string $fragment
468 private static function createUriString($scheme, $authority, $path, $query, $fragment)
472 if (! empty($scheme)) {
473 $uri .= sprintf('%s:', $scheme);
476 if (! empty($authority)) {
477 $uri .= '//' . $authority;
481 if (empty($path) || '/' !== substr($path, 0, 1)) {
489 $uri .= sprintf('?%s', $query);
493 $uri .= sprintf('#%s', $fragment);
500 * Is a given port non-standard for the current scheme?
502 * @param string $scheme
503 * @param string $host
507 private function isNonStandardPort($scheme, $host, $port)
510 if ($host && ! $port) {
516 if (! $host || ! $port) {
520 return ! isset($this->allowedSchemes[$scheme]) || $port !== $this->allowedSchemes[$scheme];
524 * Filters the scheme to ensure it is a valid scheme.
526 * @param string $scheme Scheme name.
528 * @return string Filtered scheme.
530 private function filterScheme($scheme)
532 $scheme = strtolower($scheme);
533 $scheme = preg_replace('#:(//)?$#', '', $scheme);
535 if (empty($scheme)) {
539 if (! array_key_exists($scheme, $this->allowedSchemes)) {
540 throw new InvalidArgumentException(sprintf(
541 'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
543 implode(', ', array_keys($this->allowedSchemes))
551 * Filters the path of a URI to ensure it is properly encoded.
553 * @param string $path
556 private function filterPath($path)
558 $path = preg_replace_callback(
559 '/(?:[^' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
560 [$this, 'urlEncodeChar'],
569 if ($path[0] !== '/') {
574 // Ensure only one leading slash, to prevent XSS attempts.
575 return '/' . ltrim($path, '/');
579 * Filter a query string to ensure it is propertly encoded.
581 * Ensures that the values in the query string are properly urlencoded.
583 * @param string $query
586 private function filterQuery($query)
588 if (! empty($query) && strpos($query, '?') === 0) {
589 $query = substr($query, 1);
592 $parts = explode('&', $query);
593 foreach ($parts as $index => $part) {
594 list($key, $value) = $this->splitQueryValue($part);
595 if ($value === null) {
596 $parts[$index] = $this->filterQueryOrFragment($key);
599 $parts[$index] = sprintf(
601 $this->filterQueryOrFragment($key),
602 $this->filterQueryOrFragment($value)
606 return implode('&', $parts);
610 * Split a query value into a key/value tuple.
612 * @param string $value
613 * @return array A value with exactly two elements, key and value
615 private function splitQueryValue($value)
617 $data = explode('=', $value, 2);
618 if (1 === count($data)) {
625 * Filter a fragment value to ensure it is properly encoded.
627 * @param null|string $fragment
630 private function filterFragment($fragment)
632 if (! empty($fragment) && strpos($fragment, '#') === 0) {
633 $fragment = '%23' . substr($fragment, 1);
636 return $this->filterQueryOrFragment($fragment);
640 * Filter a query string key or value, or a fragment.
642 * @param string $value
645 private function filterQueryOrFragment($value)
647 return preg_replace_callback(
648 '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
649 [$this, 'urlEncodeChar'],
655 * URL encode a character returned by a regex.
657 * @param array $matches
660 private function urlEncodeChar(array $matches)
662 return rawurlencode($matches[0]);