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