comparison vendor/zendframework/zend-diactoros/src/Uri.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 7a779792577d
comparison
equal deleted inserted replaced
-1:000000000000 0:4c8ae668cc8c
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 }