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