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