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