Security update for Core, with self-updated composer
[yaffs-website] / vendor / zendframework / zend-diactoros / src / Uri.php
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  */
7
8 namespace Zend\Diactoros;
9
10 use InvalidArgumentException;
11 use Psr\Http\Message\UriInterface;
12
13 /**
14  * Implementation of Psr\Http\UriInterface.
15  *
16  * Provides a value object representing a URI for HTTP requests.
17  *
18  * Instances of this class  are considered immutable; all methods that
19  * might change state are implemented such that they retain the internal
20  * state of the current instance and return a new instance that contains the
21  * changed state.
22  */
23 class Uri implements UriInterface
24 {
25     /**
26      * Sub-delimiters used in user info, query strings and fragments.
27      *
28      * @const string
29      */
30     const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
31
32     /**
33      * Unreserved characters used in user info, paths, query strings, and fragments.
34      *
35      * @const string
36      */
37     const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
38
39     /**
40      * @var int[] Array indexed by valid scheme names to their corresponding ports.
41      */
42     protected $allowedSchemes = [
43         'http'  => 80,
44         'https' => 443,
45     ];
46
47     /**
48      * @var string
49      */
50     private $scheme = '';
51
52     /**
53      * @var string
54      */
55     private $userInfo = '';
56
57     /**
58      * @var string
59      */
60     private $host = '';
61
62     /**
63      * @var int
64      */
65     private $port;
66
67     /**
68      * @var string
69      */
70     private $path = '';
71
72     /**
73      * @var string
74      */
75     private $query = '';
76
77     /**
78      * @var string
79      */
80     private $fragment = '';
81
82     /**
83      * generated uri string cache
84      * @var string|null
85      */
86     private $uriString;
87
88     /**
89      * @param string $uri
90      * @throws InvalidArgumentException on non-string $uri argument
91      */
92     public function __construct($uri = '')
93     {
94         if (! is_string($uri)) {
95             throw new InvalidArgumentException(sprintf(
96                 'URI passed to constructor must be a string; received "%s"',
97                 (is_object($uri) ? get_class($uri) : gettype($uri))
98             ));
99         }
100
101         if (! empty($uri)) {
102             $this->parseUri($uri);
103         }
104     }
105
106     /**
107      * Operations to perform on clone.
108      *
109      * Since cloning usually is for purposes of mutation, we reset the
110      * $uriString property so it will be re-calculated.
111      */
112     public function __clone()
113     {
114         $this->uriString = null;
115     }
116
117     /**
118      * {@inheritdoc}
119      */
120     public function __toString()
121     {
122         if (null !== $this->uriString) {
123             return $this->uriString;
124         }
125
126         $this->uriString = static::createUriString(
127             $this->scheme,
128             $this->getAuthority(),
129             $this->getPath(), // Absolute URIs should use a "/" for an empty path
130             $this->query,
131             $this->fragment
132         );
133
134         return $this->uriString;
135     }
136
137     /**
138      * {@inheritdoc}
139      */
140     public function getScheme()
141     {
142         return $this->scheme;
143     }
144
145     /**
146      * {@inheritdoc}
147      */
148     public function getAuthority()
149     {
150         if (empty($this->host)) {
151             return '';
152         }
153
154         $authority = $this->host;
155         if (! empty($this->userInfo)) {
156             $authority = $this->userInfo . '@' . $authority;
157         }
158
159         if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
160             $authority .= ':' . $this->port;
161         }
162
163         return $authority;
164     }
165
166     /**
167      * Retrieve the user-info part of the URI.
168      *
169      * This value is percent-encoded, per RFC 3986 Section 3.2.1.
170      *
171      * {@inheritdoc}
172      */
173     public function getUserInfo()
174     {
175         return $this->userInfo;
176     }
177
178     /**
179      * {@inheritdoc}
180      */
181     public function getHost()
182     {
183         return $this->host;
184     }
185
186     /**
187      * {@inheritdoc}
188      */
189     public function getPort()
190     {
191         return $this->isNonStandardPort($this->scheme, $this->host, $this->port)
192             ? $this->port
193             : null;
194     }
195
196     /**
197      * {@inheritdoc}
198      */
199     public function getPath()
200     {
201         return $this->path;
202     }
203
204     /**
205      * {@inheritdoc}
206      */
207     public function getQuery()
208     {
209         return $this->query;
210     }
211
212     /**
213      * {@inheritdoc}
214      */
215     public function getFragment()
216     {
217         return $this->fragment;
218     }
219
220     /**
221      * {@inheritdoc}
222      */
223     public function withScheme($scheme)
224     {
225         if (! is_string($scheme)) {
226             throw new InvalidArgumentException(sprintf(
227                 '%s expects a string argument; received %s',
228                 __METHOD__,
229                 (is_object($scheme) ? get_class($scheme) : gettype($scheme))
230             ));
231         }
232
233         $scheme = $this->filterScheme($scheme);
234
235         if ($scheme === $this->scheme) {
236             // Do nothing if no change was made.
237             return $this;
238         }
239
240         $new = clone $this;
241         $new->scheme = $scheme;
242
243         return $new;
244     }
245
246     /**
247      * Create and return a new instance containing the provided user credentials.
248      *
249      * The value will be percent-encoded in the new instance, but with measures
250      * taken to prevent double-encoding.
251      *
252      * {@inheritdoc}
253      */
254     public function withUserInfo($user, $password = null)
255     {
256         if (! is_string($user)) {
257             throw new InvalidArgumentException(sprintf(
258                 '%s expects a string user argument; received %s',
259                 __METHOD__,
260                 (is_object($user) ? get_class($user) : gettype($user))
261             ));
262         }
263         if (null !== $password && ! is_string($password)) {
264             throw new InvalidArgumentException(sprintf(
265                 '%s expects a string password argument; received %s',
266                 __METHOD__,
267                 (is_object($password) ? get_class($password) : gettype($password))
268             ));
269         }
270
271         $info = $this->filterUserInfoPart($user);
272         if ($password) {
273             $info .= ':' . $this->filterUserInfoPart($password);
274         }
275
276         if ($info === $this->userInfo) {
277             // Do nothing if no change was made.
278             return $this;
279         }
280
281         $new = clone $this;
282         $new->userInfo = $info;
283
284         return $new;
285     }
286
287     /**
288      * {@inheritdoc}
289      */
290     public function withHost($host)
291     {
292         if (! is_string($host)) {
293             throw new InvalidArgumentException(sprintf(
294                 '%s expects a string argument; received %s',
295                 __METHOD__,
296                 (is_object($host) ? get_class($host) : gettype($host))
297             ));
298         }
299
300         if ($host === $this->host) {
301             // Do nothing if no change was made.
302             return $this;
303         }
304
305         $new = clone $this;
306         $new->host = strtolower($host);
307
308         return $new;
309     }
310
311     /**
312      * {@inheritdoc}
313      */
314     public function withPort($port)
315     {
316         if (! is_numeric($port) && $port !== null) {
317             throw new InvalidArgumentException(sprintf(
318                 'Invalid port "%s" specified; must be an integer, an integer string, or null',
319                 (is_object($port) ? get_class($port) : gettype($port))
320             ));
321         }
322
323         if ($port !== null) {
324             $port = (int) $port;
325         }
326
327         if ($port === $this->port) {
328             // Do nothing if no change was made.
329             return $this;
330         }
331
332         if ($port !== null && ($port < 1 || $port > 65535)) {
333             throw new InvalidArgumentException(sprintf(
334                 'Invalid port "%d" specified; must be a valid TCP/UDP port',
335                 $port
336             ));
337         }
338
339         $new = clone $this;
340         $new->port = $port;
341
342         return $new;
343     }
344
345     /**
346      * {@inheritdoc}
347      */
348     public function withPath($path)
349     {
350         if (! is_string($path)) {
351             throw new InvalidArgumentException(
352                 'Invalid path provided; must be a string'
353             );
354         }
355
356         if (strpos($path, '?') !== false) {
357             throw new InvalidArgumentException(
358                 'Invalid path provided; must not contain a query string'
359             );
360         }
361
362         if (strpos($path, '#') !== false) {
363             throw new InvalidArgumentException(
364                 'Invalid path provided; must not contain a URI fragment'
365             );
366         }
367
368         $path = $this->filterPath($path);
369
370         if ($path === $this->path) {
371             // Do nothing if no change was made.
372             return $this;
373         }
374
375         $new = clone $this;
376         $new->path = $path;
377
378         return $new;
379     }
380
381     /**
382      * {@inheritdoc}
383      */
384     public function withQuery($query)
385     {
386         if (! is_string($query)) {
387             throw new InvalidArgumentException(
388                 'Query string must be a string'
389             );
390         }
391
392         if (strpos($query, '#') !== false) {
393             throw new InvalidArgumentException(
394                 'Query string must not include a URI fragment'
395             );
396         }
397
398         $query = $this->filterQuery($query);
399
400         if ($query === $this->query) {
401             // Do nothing if no change was made.
402             return $this;
403         }
404
405         $new = clone $this;
406         $new->query = $query;
407
408         return $new;
409     }
410
411     /**
412      * {@inheritdoc}
413      */
414     public function withFragment($fragment)
415     {
416         if (! is_string($fragment)) {
417             throw new InvalidArgumentException(sprintf(
418                 '%s expects a string argument; received %s',
419                 __METHOD__,
420                 (is_object($fragment) ? get_class($fragment) : gettype($fragment))
421             ));
422         }
423
424         $fragment = $this->filterFragment($fragment);
425
426         if ($fragment === $this->fragment) {
427             // Do nothing if no change was made.
428             return $this;
429         }
430
431         $new = clone $this;
432         $new->fragment = $fragment;
433
434         return $new;
435     }
436
437     /**
438      * Parse a URI into its parts, and set the properties
439      *
440      * @param string $uri
441      */
442     private function parseUri($uri)
443     {
444         $parts = parse_url($uri);
445
446         if (false === $parts) {
447             throw new \InvalidArgumentException(
448                 'The source URI string appears to be malformed'
449             );
450         }
451
452         $this->scheme    = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
453         $this->userInfo  = isset($parts['user']) ? $this->filterUserInfoPart($parts['user']) : '';
454         $this->host      = isset($parts['host']) ? strtolower($parts['host']) : '';
455         $this->port      = isset($parts['port']) ? $parts['port'] : null;
456         $this->path      = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
457         $this->query     = isset($parts['query']) ? $this->filterQuery($parts['query']) : '';
458         $this->fragment  = isset($parts['fragment']) ? $this->filterFragment($parts['fragment']) : '';
459
460         if (isset($parts['pass'])) {
461             $this->userInfo .= ':' . $parts['pass'];
462         }
463     }
464
465     /**
466      * Create a URI string from its various parts
467      *
468      * @param string $scheme
469      * @param string $authority
470      * @param string $path
471      * @param string $query
472      * @param string $fragment
473      * @return string
474      */
475     private static function createUriString($scheme, $authority, $path, $query, $fragment)
476     {
477         $uri = '';
478
479         if (! empty($scheme)) {
480             $uri .= sprintf('%s:', $scheme);
481         }
482
483         if (! empty($authority)) {
484             $uri .= '//' . $authority;
485         }
486
487         if ($path) {
488             if (empty($path) || '/' !== substr($path, 0, 1)) {
489                 $path = '/' . $path;
490             }
491
492             $uri .= $path;
493         }
494
495         if ($query) {
496             $uri .= sprintf('?%s', $query);
497         }
498
499         if ($fragment) {
500             $uri .= sprintf('#%s', $fragment);
501         }
502
503         return $uri;
504     }
505
506     /**
507      * Is a given port non-standard for the current scheme?
508      *
509      * @param string $scheme
510      * @param string $host
511      * @param int $port
512      * @return bool
513      */
514     private function isNonStandardPort($scheme, $host, $port)
515     {
516         if (! $scheme) {
517             if ($host && ! $port) {
518                 return false;
519             }
520             return true;
521         }
522
523         if (! $host || ! $port) {
524             return false;
525         }
526
527         return ! isset($this->allowedSchemes[$scheme]) || $port !== $this->allowedSchemes[$scheme];
528     }
529
530     /**
531      * Filters the scheme to ensure it is a valid scheme.
532      *
533      * @param string $scheme Scheme name.
534      *
535      * @return string Filtered scheme.
536      */
537     private function filterScheme($scheme)
538     {
539         $scheme = strtolower($scheme);
540         $scheme = preg_replace('#:(//)?$#', '', $scheme);
541
542         if (empty($scheme)) {
543             return '';
544         }
545
546         if (! array_key_exists($scheme, $this->allowedSchemes)) {
547             throw new InvalidArgumentException(sprintf(
548                 'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
549                 $scheme,
550                 implode(', ', array_keys($this->allowedSchemes))
551             ));
552         }
553
554         return $scheme;
555     }
556
557     /**
558      * Filters a part of user info in a URI to ensure it is properly encoded.
559      *
560      * @param string $part
561      * @return string
562      */
563     private function filterUserInfoPart($part)
564     {
565         // Note the addition of `%` to initial charset; this allows `|` portion
566         // to match and thus prevent double-encoding.
567         return preg_replace_callback(
568             '/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/u',
569             [$this, 'urlEncodeChar'],
570             $part
571         );
572     }
573
574     /**
575      * Filters the path of a URI to ensure it is properly encoded.
576      *
577      * @param string $path
578      * @return string
579      */
580     private function filterPath($path)
581     {
582         $path = preg_replace_callback(
583             '/(?:[^' . self::CHAR_UNRESERVED . ')(:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
584             [$this, 'urlEncodeChar'],
585             $path
586         );
587
588         if (empty($path)) {
589             // No path
590             return $path;
591         }
592
593         if ($path[0] !== '/') {
594             // Relative path
595             return $path;
596         }
597
598         // Ensure only one leading slash, to prevent XSS attempts.
599         return '/' . ltrim($path, '/');
600     }
601
602     /**
603      * Filter a query string to ensure it is propertly encoded.
604      *
605      * Ensures that the values in the query string are properly urlencoded.
606      *
607      * @param string $query
608      * @return string
609      */
610     private function filterQuery($query)
611     {
612         if (! empty($query) && strpos($query, '?') === 0) {
613             $query = substr($query, 1);
614         }
615
616         $parts = explode('&', $query);
617         foreach ($parts as $index => $part) {
618             list($key, $value) = $this->splitQueryValue($part);
619             if ($value === null) {
620                 $parts[$index] = $this->filterQueryOrFragment($key);
621                 continue;
622             }
623             $parts[$index] = sprintf(
624                 '%s=%s',
625                 $this->filterQueryOrFragment($key),
626                 $this->filterQueryOrFragment($value)
627             );
628         }
629
630         return implode('&', $parts);
631     }
632
633     /**
634      * Split a query value into a key/value tuple.
635      *
636      * @param string $value
637      * @return array A value with exactly two elements, key and value
638      */
639     private function splitQueryValue($value)
640     {
641         $data = explode('=', $value, 2);
642         if (1 === count($data)) {
643             $data[] = null;
644         }
645         return $data;
646     }
647
648     /**
649      * Filter a fragment value to ensure it is properly encoded.
650      *
651      * @param string $fragment
652      * @return string
653      */
654     private function filterFragment($fragment)
655     {
656         if (! empty($fragment) && strpos($fragment, '#') === 0) {
657             $fragment = '%23' . substr($fragment, 1);
658         }
659
660         return $this->filterQueryOrFragment($fragment);
661     }
662
663     /**
664      * Filter a query string key or value, or a fragment.
665      *
666      * @param string $value
667      * @return string
668      */
669     private function filterQueryOrFragment($value)
670     {
671         return preg_replace_callback(
672             '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
673             [$this, 'urlEncodeChar'],
674             $value
675         );
676     }
677
678     /**
679      * URL encode a character returned by a regex.
680      *
681      * @param array $matches
682      * @return string
683      */
684     private function urlEncodeChar(array $matches)
685     {
686         return rawurlencode($matches[0]);
687     }
688 }