Chris@0
|
1 <?php
|
Chris@0
|
2 namespace GuzzleHttp\Psr7;
|
Chris@0
|
3
|
Chris@0
|
4 use Psr\Http\Message\UriInterface;
|
Chris@0
|
5
|
Chris@0
|
6 /**
|
Chris@0
|
7 * PSR-7 URI implementation.
|
Chris@0
|
8 *
|
Chris@0
|
9 * @author Michael Dowling
|
Chris@0
|
10 * @author Tobias Schultze
|
Chris@0
|
11 * @author Matthew Weier O'Phinney
|
Chris@0
|
12 */
|
Chris@0
|
13 class Uri implements UriInterface
|
Chris@0
|
14 {
|
Chris@0
|
15 /**
|
Chris@0
|
16 * Absolute http and https URIs require a host per RFC 7230 Section 2.7
|
Chris@0
|
17 * but in generic URIs the host can be empty. So for http(s) URIs
|
Chris@0
|
18 * we apply this default host when no host is given yet to form a
|
Chris@0
|
19 * valid URI.
|
Chris@0
|
20 */
|
Chris@0
|
21 const HTTP_DEFAULT_HOST = 'localhost';
|
Chris@0
|
22
|
Chris@0
|
23 private static $defaultPorts = [
|
Chris@0
|
24 'http' => 80,
|
Chris@0
|
25 'https' => 443,
|
Chris@0
|
26 'ftp' => 21,
|
Chris@0
|
27 'gopher' => 70,
|
Chris@0
|
28 'nntp' => 119,
|
Chris@0
|
29 'news' => 119,
|
Chris@0
|
30 'telnet' => 23,
|
Chris@0
|
31 'tn3270' => 23,
|
Chris@0
|
32 'imap' => 143,
|
Chris@0
|
33 'pop' => 110,
|
Chris@0
|
34 'ldap' => 389,
|
Chris@0
|
35 ];
|
Chris@0
|
36
|
Chris@0
|
37 private static $charUnreserved = 'a-zA-Z0-9_\-\.~';
|
Chris@0
|
38 private static $charSubDelims = '!\$&\'\(\)\*\+,;=';
|
Chris@0
|
39 private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
|
Chris@0
|
40
|
Chris@0
|
41 /** @var string Uri scheme. */
|
Chris@0
|
42 private $scheme = '';
|
Chris@0
|
43
|
Chris@0
|
44 /** @var string Uri user info. */
|
Chris@0
|
45 private $userInfo = '';
|
Chris@0
|
46
|
Chris@0
|
47 /** @var string Uri host. */
|
Chris@0
|
48 private $host = '';
|
Chris@0
|
49
|
Chris@0
|
50 /** @var int|null Uri port. */
|
Chris@0
|
51 private $port;
|
Chris@0
|
52
|
Chris@0
|
53 /** @var string Uri path. */
|
Chris@0
|
54 private $path = '';
|
Chris@0
|
55
|
Chris@0
|
56 /** @var string Uri query string. */
|
Chris@0
|
57 private $query = '';
|
Chris@0
|
58
|
Chris@0
|
59 /** @var string Uri fragment. */
|
Chris@0
|
60 private $fragment = '';
|
Chris@0
|
61
|
Chris@0
|
62 /**
|
Chris@0
|
63 * @param string $uri URI to parse
|
Chris@0
|
64 */
|
Chris@0
|
65 public function __construct($uri = '')
|
Chris@0
|
66 {
|
Chris@0
|
67 // weak type check to also accept null until we can add scalar type hints
|
Chris@0
|
68 if ($uri != '') {
|
Chris@0
|
69 $parts = parse_url($uri);
|
Chris@0
|
70 if ($parts === false) {
|
Chris@0
|
71 throw new \InvalidArgumentException("Unable to parse URI: $uri");
|
Chris@0
|
72 }
|
Chris@0
|
73 $this->applyParts($parts);
|
Chris@0
|
74 }
|
Chris@0
|
75 }
|
Chris@0
|
76
|
Chris@0
|
77 public function __toString()
|
Chris@0
|
78 {
|
Chris@0
|
79 return self::composeComponents(
|
Chris@0
|
80 $this->scheme,
|
Chris@0
|
81 $this->getAuthority(),
|
Chris@0
|
82 $this->path,
|
Chris@0
|
83 $this->query,
|
Chris@0
|
84 $this->fragment
|
Chris@0
|
85 );
|
Chris@0
|
86 }
|
Chris@0
|
87
|
Chris@0
|
88 /**
|
Chris@0
|
89 * Composes a URI reference string from its various components.
|
Chris@0
|
90 *
|
Chris@0
|
91 * Usually this method does not need to be called manually but instead is used indirectly via
|
Chris@0
|
92 * `Psr\Http\Message\UriInterface::__toString`.
|
Chris@0
|
93 *
|
Chris@0
|
94 * PSR-7 UriInterface treats an empty component the same as a missing component as
|
Chris@0
|
95 * getQuery(), getFragment() etc. always return a string. This explains the slight
|
Chris@0
|
96 * difference to RFC 3986 Section 5.3.
|
Chris@0
|
97 *
|
Chris@0
|
98 * Another adjustment is that the authority separator is added even when the authority is missing/empty
|
Chris@0
|
99 * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with
|
Chris@0
|
100 * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But
|
Chris@0
|
101 * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to
|
Chris@0
|
102 * that format).
|
Chris@0
|
103 *
|
Chris@0
|
104 * @param string $scheme
|
Chris@0
|
105 * @param string $authority
|
Chris@0
|
106 * @param string $path
|
Chris@0
|
107 * @param string $query
|
Chris@0
|
108 * @param string $fragment
|
Chris@0
|
109 *
|
Chris@0
|
110 * @return string
|
Chris@0
|
111 *
|
Chris@0
|
112 * @link https://tools.ietf.org/html/rfc3986#section-5.3
|
Chris@0
|
113 */
|
Chris@0
|
114 public static function composeComponents($scheme, $authority, $path, $query, $fragment)
|
Chris@0
|
115 {
|
Chris@0
|
116 $uri = '';
|
Chris@0
|
117
|
Chris@0
|
118 // weak type checks to also accept null until we can add scalar type hints
|
Chris@0
|
119 if ($scheme != '') {
|
Chris@0
|
120 $uri .= $scheme . ':';
|
Chris@0
|
121 }
|
Chris@0
|
122
|
Chris@0
|
123 if ($authority != ''|| $scheme === 'file') {
|
Chris@0
|
124 $uri .= '//' . $authority;
|
Chris@0
|
125 }
|
Chris@0
|
126
|
Chris@0
|
127 $uri .= $path;
|
Chris@0
|
128
|
Chris@0
|
129 if ($query != '') {
|
Chris@0
|
130 $uri .= '?' . $query;
|
Chris@0
|
131 }
|
Chris@0
|
132
|
Chris@0
|
133 if ($fragment != '') {
|
Chris@0
|
134 $uri .= '#' . $fragment;
|
Chris@0
|
135 }
|
Chris@0
|
136
|
Chris@0
|
137 return $uri;
|
Chris@0
|
138 }
|
Chris@0
|
139
|
Chris@0
|
140 /**
|
Chris@0
|
141 * Whether the URI has the default port of the current scheme.
|
Chris@0
|
142 *
|
Chris@0
|
143 * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
|
Chris@0
|
144 * independently of the implementation.
|
Chris@0
|
145 *
|
Chris@0
|
146 * @param UriInterface $uri
|
Chris@0
|
147 *
|
Chris@0
|
148 * @return bool
|
Chris@0
|
149 */
|
Chris@0
|
150 public static function isDefaultPort(UriInterface $uri)
|
Chris@0
|
151 {
|
Chris@0
|
152 return $uri->getPort() === null
|
Chris@0
|
153 || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]);
|
Chris@0
|
154 }
|
Chris@0
|
155
|
Chris@0
|
156 /**
|
Chris@0
|
157 * Whether the URI is absolute, i.e. it has a scheme.
|
Chris@0
|
158 *
|
Chris@0
|
159 * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
|
Chris@0
|
160 * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
|
Chris@0
|
161 * to another URI, the base URI. Relative references can be divided into several forms:
|
Chris@0
|
162 * - network-path references, e.g. '//example.com/path'
|
Chris@0
|
163 * - absolute-path references, e.g. '/path'
|
Chris@0
|
164 * - relative-path references, e.g. 'subpath'
|
Chris@0
|
165 *
|
Chris@0
|
166 * @param UriInterface $uri
|
Chris@0
|
167 *
|
Chris@0
|
168 * @return bool
|
Chris@0
|
169 * @see Uri::isNetworkPathReference
|
Chris@0
|
170 * @see Uri::isAbsolutePathReference
|
Chris@0
|
171 * @see Uri::isRelativePathReference
|
Chris@0
|
172 * @link https://tools.ietf.org/html/rfc3986#section-4
|
Chris@0
|
173 */
|
Chris@0
|
174 public static function isAbsolute(UriInterface $uri)
|
Chris@0
|
175 {
|
Chris@0
|
176 return $uri->getScheme() !== '';
|
Chris@0
|
177 }
|
Chris@0
|
178
|
Chris@0
|
179 /**
|
Chris@0
|
180 * Whether the URI is a network-path reference.
|
Chris@0
|
181 *
|
Chris@0
|
182 * A relative reference that begins with two slash characters is termed an network-path reference.
|
Chris@0
|
183 *
|
Chris@0
|
184 * @param UriInterface $uri
|
Chris@0
|
185 *
|
Chris@0
|
186 * @return bool
|
Chris@0
|
187 * @link https://tools.ietf.org/html/rfc3986#section-4.2
|
Chris@0
|
188 */
|
Chris@0
|
189 public static function isNetworkPathReference(UriInterface $uri)
|
Chris@0
|
190 {
|
Chris@0
|
191 return $uri->getScheme() === '' && $uri->getAuthority() !== '';
|
Chris@0
|
192 }
|
Chris@0
|
193
|
Chris@0
|
194 /**
|
Chris@0
|
195 * Whether the URI is a absolute-path reference.
|
Chris@0
|
196 *
|
Chris@0
|
197 * A relative reference that begins with a single slash character is termed an absolute-path reference.
|
Chris@0
|
198 *
|
Chris@0
|
199 * @param UriInterface $uri
|
Chris@0
|
200 *
|
Chris@0
|
201 * @return bool
|
Chris@0
|
202 * @link https://tools.ietf.org/html/rfc3986#section-4.2
|
Chris@0
|
203 */
|
Chris@0
|
204 public static function isAbsolutePathReference(UriInterface $uri)
|
Chris@0
|
205 {
|
Chris@0
|
206 return $uri->getScheme() === ''
|
Chris@0
|
207 && $uri->getAuthority() === ''
|
Chris@0
|
208 && isset($uri->getPath()[0])
|
Chris@0
|
209 && $uri->getPath()[0] === '/';
|
Chris@0
|
210 }
|
Chris@0
|
211
|
Chris@0
|
212 /**
|
Chris@0
|
213 * Whether the URI is a relative-path reference.
|
Chris@0
|
214 *
|
Chris@0
|
215 * A relative reference that does not begin with a slash character is termed a relative-path reference.
|
Chris@0
|
216 *
|
Chris@0
|
217 * @param UriInterface $uri
|
Chris@0
|
218 *
|
Chris@0
|
219 * @return bool
|
Chris@0
|
220 * @link https://tools.ietf.org/html/rfc3986#section-4.2
|
Chris@0
|
221 */
|
Chris@0
|
222 public static function isRelativePathReference(UriInterface $uri)
|
Chris@0
|
223 {
|
Chris@0
|
224 return $uri->getScheme() === ''
|
Chris@0
|
225 && $uri->getAuthority() === ''
|
Chris@0
|
226 && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');
|
Chris@0
|
227 }
|
Chris@0
|
228
|
Chris@0
|
229 /**
|
Chris@0
|
230 * Whether the URI is a same-document reference.
|
Chris@0
|
231 *
|
Chris@0
|
232 * A same-document reference refers to a URI that is, aside from its fragment
|
Chris@0
|
233 * component, identical to the base URI. When no base URI is given, only an empty
|
Chris@0
|
234 * URI reference (apart from its fragment) is considered a same-document reference.
|
Chris@0
|
235 *
|
Chris@0
|
236 * @param UriInterface $uri The URI to check
|
Chris@0
|
237 * @param UriInterface|null $base An optional base URI to compare against
|
Chris@0
|
238 *
|
Chris@0
|
239 * @return bool
|
Chris@0
|
240 * @link https://tools.ietf.org/html/rfc3986#section-4.4
|
Chris@0
|
241 */
|
Chris@0
|
242 public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null)
|
Chris@0
|
243 {
|
Chris@0
|
244 if ($base !== null) {
|
Chris@0
|
245 $uri = UriResolver::resolve($base, $uri);
|
Chris@0
|
246
|
Chris@0
|
247 return ($uri->getScheme() === $base->getScheme())
|
Chris@0
|
248 && ($uri->getAuthority() === $base->getAuthority())
|
Chris@0
|
249 && ($uri->getPath() === $base->getPath())
|
Chris@0
|
250 && ($uri->getQuery() === $base->getQuery());
|
Chris@0
|
251 }
|
Chris@0
|
252
|
Chris@0
|
253 return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === '';
|
Chris@0
|
254 }
|
Chris@0
|
255
|
Chris@0
|
256 /**
|
Chris@0
|
257 * Removes dot segments from a path and returns the new path.
|
Chris@0
|
258 *
|
Chris@0
|
259 * @param string $path
|
Chris@0
|
260 *
|
Chris@0
|
261 * @return string
|
Chris@0
|
262 *
|
Chris@0
|
263 * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead.
|
Chris@0
|
264 * @see UriResolver::removeDotSegments
|
Chris@0
|
265 */
|
Chris@0
|
266 public static function removeDotSegments($path)
|
Chris@0
|
267 {
|
Chris@0
|
268 return UriResolver::removeDotSegments($path);
|
Chris@0
|
269 }
|
Chris@0
|
270
|
Chris@0
|
271 /**
|
Chris@0
|
272 * Converts the relative URI into a new URI that is resolved against the base URI.
|
Chris@0
|
273 *
|
Chris@0
|
274 * @param UriInterface $base Base URI
|
Chris@0
|
275 * @param string|UriInterface $rel Relative URI
|
Chris@0
|
276 *
|
Chris@0
|
277 * @return UriInterface
|
Chris@0
|
278 *
|
Chris@0
|
279 * @deprecated since version 1.4. Use UriResolver::resolve instead.
|
Chris@0
|
280 * @see UriResolver::resolve
|
Chris@0
|
281 */
|
Chris@0
|
282 public static function resolve(UriInterface $base, $rel)
|
Chris@0
|
283 {
|
Chris@0
|
284 if (!($rel instanceof UriInterface)) {
|
Chris@0
|
285 $rel = new self($rel);
|
Chris@0
|
286 }
|
Chris@0
|
287
|
Chris@0
|
288 return UriResolver::resolve($base, $rel);
|
Chris@0
|
289 }
|
Chris@0
|
290
|
Chris@0
|
291 /**
|
Chris@0
|
292 * Creates a new URI with a specific query string value removed.
|
Chris@0
|
293 *
|
Chris@0
|
294 * Any existing query string values that exactly match the provided key are
|
Chris@0
|
295 * removed.
|
Chris@0
|
296 *
|
Chris@0
|
297 * @param UriInterface $uri URI to use as a base.
|
Chris@0
|
298 * @param string $key Query string key to remove.
|
Chris@0
|
299 *
|
Chris@0
|
300 * @return UriInterface
|
Chris@0
|
301 */
|
Chris@0
|
302 public static function withoutQueryValue(UriInterface $uri, $key)
|
Chris@0
|
303 {
|
Chris@17
|
304 $result = self::getFilteredQueryString($uri, [$key]);
|
Chris@0
|
305
|
Chris@0
|
306 return $uri->withQuery(implode('&', $result));
|
Chris@0
|
307 }
|
Chris@0
|
308
|
Chris@0
|
309 /**
|
Chris@0
|
310 * Creates a new URI with a specific query string value.
|
Chris@0
|
311 *
|
Chris@0
|
312 * Any existing query string values that exactly match the provided key are
|
Chris@0
|
313 * removed and replaced with the given key value pair.
|
Chris@0
|
314 *
|
Chris@0
|
315 * A value of null will set the query string key without a value, e.g. "key"
|
Chris@0
|
316 * instead of "key=value".
|
Chris@0
|
317 *
|
Chris@0
|
318 * @param UriInterface $uri URI to use as a base.
|
Chris@0
|
319 * @param string $key Key to set.
|
Chris@0
|
320 * @param string|null $value Value to set
|
Chris@0
|
321 *
|
Chris@0
|
322 * @return UriInterface
|
Chris@0
|
323 */
|
Chris@0
|
324 public static function withQueryValue(UriInterface $uri, $key, $value)
|
Chris@0
|
325 {
|
Chris@17
|
326 $result = self::getFilteredQueryString($uri, [$key]);
|
Chris@0
|
327
|
Chris@17
|
328 $result[] = self::generateQueryString($key, $value);
|
Chris@0
|
329
|
Chris@17
|
330 return $uri->withQuery(implode('&', $result));
|
Chris@17
|
331 }
|
Chris@0
|
332
|
Chris@17
|
333 /**
|
Chris@17
|
334 * Creates a new URI with multiple specific query string values.
|
Chris@17
|
335 *
|
Chris@17
|
336 * It has the same behavior as withQueryValue() but for an associative array of key => value.
|
Chris@17
|
337 *
|
Chris@17
|
338 * @param UriInterface $uri URI to use as a base.
|
Chris@17
|
339 * @param array $keyValueArray Associative array of key and values
|
Chris@17
|
340 *
|
Chris@17
|
341 * @return UriInterface
|
Chris@17
|
342 */
|
Chris@17
|
343 public static function withQueryValues(UriInterface $uri, array $keyValueArray)
|
Chris@17
|
344 {
|
Chris@17
|
345 $result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
|
Chris@17
|
346
|
Chris@17
|
347 foreach ($keyValueArray as $key => $value) {
|
Chris@17
|
348 $result[] = self::generateQueryString($key, $value);
|
Chris@0
|
349 }
|
Chris@0
|
350
|
Chris@0
|
351 return $uri->withQuery(implode('&', $result));
|
Chris@0
|
352 }
|
Chris@0
|
353
|
Chris@0
|
354 /**
|
Chris@0
|
355 * Creates a URI from a hash of `parse_url` components.
|
Chris@0
|
356 *
|
Chris@0
|
357 * @param array $parts
|
Chris@0
|
358 *
|
Chris@0
|
359 * @return UriInterface
|
Chris@0
|
360 * @link http://php.net/manual/en/function.parse-url.php
|
Chris@0
|
361 *
|
Chris@0
|
362 * @throws \InvalidArgumentException If the components do not form a valid URI.
|
Chris@0
|
363 */
|
Chris@0
|
364 public static function fromParts(array $parts)
|
Chris@0
|
365 {
|
Chris@0
|
366 $uri = new self();
|
Chris@0
|
367 $uri->applyParts($parts);
|
Chris@0
|
368 $uri->validateState();
|
Chris@0
|
369
|
Chris@0
|
370 return $uri;
|
Chris@0
|
371 }
|
Chris@0
|
372
|
Chris@0
|
373 public function getScheme()
|
Chris@0
|
374 {
|
Chris@0
|
375 return $this->scheme;
|
Chris@0
|
376 }
|
Chris@0
|
377
|
Chris@0
|
378 public function getAuthority()
|
Chris@0
|
379 {
|
Chris@0
|
380 $authority = $this->host;
|
Chris@0
|
381 if ($this->userInfo !== '') {
|
Chris@0
|
382 $authority = $this->userInfo . '@' . $authority;
|
Chris@0
|
383 }
|
Chris@0
|
384
|
Chris@0
|
385 if ($this->port !== null) {
|
Chris@0
|
386 $authority .= ':' . $this->port;
|
Chris@0
|
387 }
|
Chris@0
|
388
|
Chris@0
|
389 return $authority;
|
Chris@0
|
390 }
|
Chris@0
|
391
|
Chris@0
|
392 public function getUserInfo()
|
Chris@0
|
393 {
|
Chris@0
|
394 return $this->userInfo;
|
Chris@0
|
395 }
|
Chris@0
|
396
|
Chris@0
|
397 public function getHost()
|
Chris@0
|
398 {
|
Chris@0
|
399 return $this->host;
|
Chris@0
|
400 }
|
Chris@0
|
401
|
Chris@0
|
402 public function getPort()
|
Chris@0
|
403 {
|
Chris@0
|
404 return $this->port;
|
Chris@0
|
405 }
|
Chris@0
|
406
|
Chris@0
|
407 public function getPath()
|
Chris@0
|
408 {
|
Chris@0
|
409 return $this->path;
|
Chris@0
|
410 }
|
Chris@0
|
411
|
Chris@0
|
412 public function getQuery()
|
Chris@0
|
413 {
|
Chris@0
|
414 return $this->query;
|
Chris@0
|
415 }
|
Chris@0
|
416
|
Chris@0
|
417 public function getFragment()
|
Chris@0
|
418 {
|
Chris@0
|
419 return $this->fragment;
|
Chris@0
|
420 }
|
Chris@0
|
421
|
Chris@0
|
422 public function withScheme($scheme)
|
Chris@0
|
423 {
|
Chris@0
|
424 $scheme = $this->filterScheme($scheme);
|
Chris@0
|
425
|
Chris@0
|
426 if ($this->scheme === $scheme) {
|
Chris@0
|
427 return $this;
|
Chris@0
|
428 }
|
Chris@0
|
429
|
Chris@0
|
430 $new = clone $this;
|
Chris@0
|
431 $new->scheme = $scheme;
|
Chris@0
|
432 $new->removeDefaultPort();
|
Chris@0
|
433 $new->validateState();
|
Chris@0
|
434
|
Chris@0
|
435 return $new;
|
Chris@0
|
436 }
|
Chris@0
|
437
|
Chris@0
|
438 public function withUserInfo($user, $password = null)
|
Chris@0
|
439 {
|
Chris@0
|
440 $info = $user;
|
Chris@0
|
441 if ($password != '') {
|
Chris@0
|
442 $info .= ':' . $password;
|
Chris@0
|
443 }
|
Chris@0
|
444
|
Chris@0
|
445 if ($this->userInfo === $info) {
|
Chris@0
|
446 return $this;
|
Chris@0
|
447 }
|
Chris@0
|
448
|
Chris@0
|
449 $new = clone $this;
|
Chris@0
|
450 $new->userInfo = $info;
|
Chris@0
|
451 $new->validateState();
|
Chris@0
|
452
|
Chris@0
|
453 return $new;
|
Chris@0
|
454 }
|
Chris@0
|
455
|
Chris@0
|
456 public function withHost($host)
|
Chris@0
|
457 {
|
Chris@0
|
458 $host = $this->filterHost($host);
|
Chris@0
|
459
|
Chris@0
|
460 if ($this->host === $host) {
|
Chris@0
|
461 return $this;
|
Chris@0
|
462 }
|
Chris@0
|
463
|
Chris@0
|
464 $new = clone $this;
|
Chris@0
|
465 $new->host = $host;
|
Chris@0
|
466 $new->validateState();
|
Chris@0
|
467
|
Chris@0
|
468 return $new;
|
Chris@0
|
469 }
|
Chris@0
|
470
|
Chris@0
|
471 public function withPort($port)
|
Chris@0
|
472 {
|
Chris@0
|
473 $port = $this->filterPort($port);
|
Chris@0
|
474
|
Chris@0
|
475 if ($this->port === $port) {
|
Chris@0
|
476 return $this;
|
Chris@0
|
477 }
|
Chris@0
|
478
|
Chris@0
|
479 $new = clone $this;
|
Chris@0
|
480 $new->port = $port;
|
Chris@0
|
481 $new->removeDefaultPort();
|
Chris@0
|
482 $new->validateState();
|
Chris@0
|
483
|
Chris@0
|
484 return $new;
|
Chris@0
|
485 }
|
Chris@0
|
486
|
Chris@0
|
487 public function withPath($path)
|
Chris@0
|
488 {
|
Chris@0
|
489 $path = $this->filterPath($path);
|
Chris@0
|
490
|
Chris@0
|
491 if ($this->path === $path) {
|
Chris@0
|
492 return $this;
|
Chris@0
|
493 }
|
Chris@0
|
494
|
Chris@0
|
495 $new = clone $this;
|
Chris@0
|
496 $new->path = $path;
|
Chris@0
|
497 $new->validateState();
|
Chris@0
|
498
|
Chris@0
|
499 return $new;
|
Chris@0
|
500 }
|
Chris@0
|
501
|
Chris@0
|
502 public function withQuery($query)
|
Chris@0
|
503 {
|
Chris@0
|
504 $query = $this->filterQueryAndFragment($query);
|
Chris@0
|
505
|
Chris@0
|
506 if ($this->query === $query) {
|
Chris@0
|
507 return $this;
|
Chris@0
|
508 }
|
Chris@0
|
509
|
Chris@0
|
510 $new = clone $this;
|
Chris@0
|
511 $new->query = $query;
|
Chris@0
|
512
|
Chris@0
|
513 return $new;
|
Chris@0
|
514 }
|
Chris@0
|
515
|
Chris@0
|
516 public function withFragment($fragment)
|
Chris@0
|
517 {
|
Chris@0
|
518 $fragment = $this->filterQueryAndFragment($fragment);
|
Chris@0
|
519
|
Chris@0
|
520 if ($this->fragment === $fragment) {
|
Chris@0
|
521 return $this;
|
Chris@0
|
522 }
|
Chris@0
|
523
|
Chris@0
|
524 $new = clone $this;
|
Chris@0
|
525 $new->fragment = $fragment;
|
Chris@0
|
526
|
Chris@0
|
527 return $new;
|
Chris@0
|
528 }
|
Chris@0
|
529
|
Chris@0
|
530 /**
|
Chris@0
|
531 * Apply parse_url parts to a URI.
|
Chris@0
|
532 *
|
Chris@0
|
533 * @param array $parts Array of parse_url parts to apply.
|
Chris@0
|
534 */
|
Chris@0
|
535 private function applyParts(array $parts)
|
Chris@0
|
536 {
|
Chris@0
|
537 $this->scheme = isset($parts['scheme'])
|
Chris@0
|
538 ? $this->filterScheme($parts['scheme'])
|
Chris@0
|
539 : '';
|
Chris@0
|
540 $this->userInfo = isset($parts['user']) ? $parts['user'] : '';
|
Chris@0
|
541 $this->host = isset($parts['host'])
|
Chris@0
|
542 ? $this->filterHost($parts['host'])
|
Chris@0
|
543 : '';
|
Chris@0
|
544 $this->port = isset($parts['port'])
|
Chris@0
|
545 ? $this->filterPort($parts['port'])
|
Chris@0
|
546 : null;
|
Chris@0
|
547 $this->path = isset($parts['path'])
|
Chris@0
|
548 ? $this->filterPath($parts['path'])
|
Chris@0
|
549 : '';
|
Chris@0
|
550 $this->query = isset($parts['query'])
|
Chris@0
|
551 ? $this->filterQueryAndFragment($parts['query'])
|
Chris@0
|
552 : '';
|
Chris@0
|
553 $this->fragment = isset($parts['fragment'])
|
Chris@0
|
554 ? $this->filterQueryAndFragment($parts['fragment'])
|
Chris@0
|
555 : '';
|
Chris@0
|
556 if (isset($parts['pass'])) {
|
Chris@0
|
557 $this->userInfo .= ':' . $parts['pass'];
|
Chris@0
|
558 }
|
Chris@0
|
559
|
Chris@0
|
560 $this->removeDefaultPort();
|
Chris@0
|
561 }
|
Chris@0
|
562
|
Chris@0
|
563 /**
|
Chris@0
|
564 * @param string $scheme
|
Chris@0
|
565 *
|
Chris@0
|
566 * @return string
|
Chris@0
|
567 *
|
Chris@0
|
568 * @throws \InvalidArgumentException If the scheme is invalid.
|
Chris@0
|
569 */
|
Chris@0
|
570 private function filterScheme($scheme)
|
Chris@0
|
571 {
|
Chris@0
|
572 if (!is_string($scheme)) {
|
Chris@0
|
573 throw new \InvalidArgumentException('Scheme must be a string');
|
Chris@0
|
574 }
|
Chris@0
|
575
|
Chris@0
|
576 return strtolower($scheme);
|
Chris@0
|
577 }
|
Chris@0
|
578
|
Chris@0
|
579 /**
|
Chris@0
|
580 * @param string $host
|
Chris@0
|
581 *
|
Chris@0
|
582 * @return string
|
Chris@0
|
583 *
|
Chris@0
|
584 * @throws \InvalidArgumentException If the host is invalid.
|
Chris@0
|
585 */
|
Chris@0
|
586 private function filterHost($host)
|
Chris@0
|
587 {
|
Chris@0
|
588 if (!is_string($host)) {
|
Chris@0
|
589 throw new \InvalidArgumentException('Host must be a string');
|
Chris@0
|
590 }
|
Chris@0
|
591
|
Chris@0
|
592 return strtolower($host);
|
Chris@0
|
593 }
|
Chris@0
|
594
|
Chris@0
|
595 /**
|
Chris@0
|
596 * @param int|null $port
|
Chris@0
|
597 *
|
Chris@0
|
598 * @return int|null
|
Chris@0
|
599 *
|
Chris@0
|
600 * @throws \InvalidArgumentException If the port is invalid.
|
Chris@0
|
601 */
|
Chris@0
|
602 private function filterPort($port)
|
Chris@0
|
603 {
|
Chris@0
|
604 if ($port === null) {
|
Chris@0
|
605 return null;
|
Chris@0
|
606 }
|
Chris@0
|
607
|
Chris@0
|
608 $port = (int) $port;
|
Chris@0
|
609 if (1 > $port || 0xffff < $port) {
|
Chris@0
|
610 throw new \InvalidArgumentException(
|
Chris@0
|
611 sprintf('Invalid port: %d. Must be between 1 and 65535', $port)
|
Chris@0
|
612 );
|
Chris@0
|
613 }
|
Chris@0
|
614
|
Chris@0
|
615 return $port;
|
Chris@0
|
616 }
|
Chris@0
|
617
|
Chris@17
|
618 /**
|
Chris@17
|
619 * @param UriInterface $uri
|
Chris@17
|
620 * @param array $keys
|
Chris@17
|
621 *
|
Chris@17
|
622 * @return array
|
Chris@17
|
623 */
|
Chris@17
|
624 private static function getFilteredQueryString(UriInterface $uri, array $keys)
|
Chris@17
|
625 {
|
Chris@17
|
626 $current = $uri->getQuery();
|
Chris@17
|
627
|
Chris@17
|
628 if ($current === '') {
|
Chris@17
|
629 return [];
|
Chris@17
|
630 }
|
Chris@17
|
631
|
Chris@17
|
632 $decodedKeys = array_map('rawurldecode', $keys);
|
Chris@17
|
633
|
Chris@17
|
634 return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
|
Chris@17
|
635 return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
|
Chris@17
|
636 });
|
Chris@17
|
637 }
|
Chris@17
|
638
|
Chris@17
|
639 /**
|
Chris@17
|
640 * @param string $key
|
Chris@17
|
641 * @param string|null $value
|
Chris@17
|
642 *
|
Chris@17
|
643 * @return string
|
Chris@17
|
644 */
|
Chris@17
|
645 private static function generateQueryString($key, $value)
|
Chris@17
|
646 {
|
Chris@17
|
647 // Query string separators ("=", "&") within the key or value need to be encoded
|
Chris@17
|
648 // (while preventing double-encoding) before setting the query string. All other
|
Chris@17
|
649 // chars that need percent-encoding will be encoded by withQuery().
|
Chris@17
|
650 $queryString = strtr($key, self::$replaceQuery);
|
Chris@17
|
651
|
Chris@17
|
652 if ($value !== null) {
|
Chris@17
|
653 $queryString .= '=' . strtr($value, self::$replaceQuery);
|
Chris@17
|
654 }
|
Chris@17
|
655
|
Chris@17
|
656 return $queryString;
|
Chris@17
|
657 }
|
Chris@17
|
658
|
Chris@0
|
659 private function removeDefaultPort()
|
Chris@0
|
660 {
|
Chris@0
|
661 if ($this->port !== null && self::isDefaultPort($this)) {
|
Chris@0
|
662 $this->port = null;
|
Chris@0
|
663 }
|
Chris@0
|
664 }
|
Chris@0
|
665
|
Chris@0
|
666 /**
|
Chris@0
|
667 * Filters the path of a URI
|
Chris@0
|
668 *
|
Chris@0
|
669 * @param string $path
|
Chris@0
|
670 *
|
Chris@0
|
671 * @return string
|
Chris@0
|
672 *
|
Chris@0
|
673 * @throws \InvalidArgumentException If the path is invalid.
|
Chris@0
|
674 */
|
Chris@0
|
675 private function filterPath($path)
|
Chris@0
|
676 {
|
Chris@0
|
677 if (!is_string($path)) {
|
Chris@0
|
678 throw new \InvalidArgumentException('Path must be a string');
|
Chris@0
|
679 }
|
Chris@0
|
680
|
Chris@0
|
681 return preg_replace_callback(
|
Chris@0
|
682 '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
|
Chris@0
|
683 [$this, 'rawurlencodeMatchZero'],
|
Chris@0
|
684 $path
|
Chris@0
|
685 );
|
Chris@0
|
686 }
|
Chris@0
|
687
|
Chris@0
|
688 /**
|
Chris@0
|
689 * Filters the query string or fragment of a URI.
|
Chris@0
|
690 *
|
Chris@0
|
691 * @param string $str
|
Chris@0
|
692 *
|
Chris@0
|
693 * @return string
|
Chris@0
|
694 *
|
Chris@0
|
695 * @throws \InvalidArgumentException If the query or fragment is invalid.
|
Chris@0
|
696 */
|
Chris@0
|
697 private function filterQueryAndFragment($str)
|
Chris@0
|
698 {
|
Chris@0
|
699 if (!is_string($str)) {
|
Chris@0
|
700 throw new \InvalidArgumentException('Query and fragment must be a string');
|
Chris@0
|
701 }
|
Chris@0
|
702
|
Chris@0
|
703 return preg_replace_callback(
|
Chris@0
|
704 '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
|
Chris@0
|
705 [$this, 'rawurlencodeMatchZero'],
|
Chris@0
|
706 $str
|
Chris@0
|
707 );
|
Chris@0
|
708 }
|
Chris@0
|
709
|
Chris@0
|
710 private function rawurlencodeMatchZero(array $match)
|
Chris@0
|
711 {
|
Chris@0
|
712 return rawurlencode($match[0]);
|
Chris@0
|
713 }
|
Chris@0
|
714
|
Chris@0
|
715 private function validateState()
|
Chris@0
|
716 {
|
Chris@0
|
717 if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
|
Chris@0
|
718 $this->host = self::HTTP_DEFAULT_HOST;
|
Chris@0
|
719 }
|
Chris@0
|
720
|
Chris@0
|
721 if ($this->getAuthority() === '') {
|
Chris@0
|
722 if (0 === strpos($this->path, '//')) {
|
Chris@0
|
723 throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"');
|
Chris@0
|
724 }
|
Chris@0
|
725 if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
|
Chris@0
|
726 throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
|
Chris@0
|
727 }
|
Chris@0
|
728 } elseif (isset($this->path[0]) && $this->path[0] !== '/') {
|
Chris@0
|
729 @trigger_error(
|
Chris@0
|
730 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' .
|
Chris@0
|
731 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.',
|
Chris@0
|
732 E_USER_DEPRECATED
|
Chris@0
|
733 );
|
Chris@0
|
734 $this->path = '/'. $this->path;
|
Chris@0
|
735 //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty');
|
Chris@0
|
736 }
|
Chris@0
|
737 }
|
Chris@0
|
738 }
|