1 | <?php |
||
2 | |||
3 | /** |
||
4 | * League.Uri (https://uri.thephpleague.com) |
||
5 | * |
||
6 | * (c) Ignace Nyamagana Butera <[email protected]> |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | */ |
||
11 | |||
12 | declare(strict_types=1); |
||
13 | |||
14 | namespace League\Uri; |
||
15 | |||
16 | use finfo; |
||
17 | use League\Uri\Contracts\UriInterface; |
||
18 | use League\Uri\Exceptions\IdnSupportMissing; |
||
19 | use League\Uri\Exceptions\SyntaxError; |
||
20 | use Psr\Http\Message\UriInterface as Psr7UriInterface; |
||
21 | use TypeError; |
||
22 | use function array_filter; |
||
23 | use function array_map; |
||
24 | use function base64_decode; |
||
25 | use function base64_encode; |
||
26 | use function count; |
||
27 | use function defined; |
||
28 | use function explode; |
||
29 | use function file_get_contents; |
||
30 | use function filter_var; |
||
31 | use function function_exists; |
||
32 | use function idn_to_ascii; |
||
33 | use function implode; |
||
34 | use function in_array; |
||
35 | use function inet_pton; |
||
36 | use function is_scalar; |
||
37 | use function mb_detect_encoding; |
||
38 | use function method_exists; |
||
39 | use function preg_match; |
||
40 | use function preg_replace; |
||
41 | use function preg_replace_callback; |
||
42 | use function rawurlencode; |
||
43 | use function sprintf; |
||
44 | use function str_replace; |
||
45 | use function strlen; |
||
46 | use function strpos; |
||
47 | use function strtolower; |
||
48 | use function substr; |
||
49 | use const FILEINFO_MIME; |
||
50 | use const FILTER_FLAG_IPV4; |
||
51 | use const FILTER_FLAG_IPV6; |
||
52 | use const FILTER_NULL_ON_FAILURE; |
||
53 | use const FILTER_VALIDATE_BOOLEAN; |
||
54 | use const FILTER_VALIDATE_IP; |
||
55 | use const IDNA_CHECK_BIDI; |
||
56 | use const IDNA_CHECK_CONTEXTJ; |
||
57 | use const IDNA_ERROR_BIDI; |
||
58 | use const IDNA_ERROR_CONTEXTJ; |
||
59 | use const IDNA_ERROR_DISALLOWED; |
||
60 | use const IDNA_ERROR_DOMAIN_NAME_TOO_LONG; |
||
61 | use const IDNA_ERROR_EMPTY_LABEL; |
||
62 | use const IDNA_ERROR_HYPHEN_3_4; |
||
63 | use const IDNA_ERROR_INVALID_ACE_LABEL; |
||
64 | use const IDNA_ERROR_LABEL_HAS_DOT; |
||
65 | use const IDNA_ERROR_LABEL_TOO_LONG; |
||
66 | use const IDNA_ERROR_LEADING_COMBINING_MARK; |
||
67 | use const IDNA_ERROR_LEADING_HYPHEN; |
||
68 | use const IDNA_ERROR_PUNYCODE; |
||
69 | use const IDNA_ERROR_TRAILING_HYPHEN; |
||
70 | use const IDNA_NONTRANSITIONAL_TO_ASCII; |
||
71 | use const IDNA_NONTRANSITIONAL_TO_UNICODE; |
||
72 | use const INTL_IDNA_VARIANT_UTS46; |
||
73 | |||
74 | final class Uri implements UriInterface |
||
75 | { |
||
76 | /** |
||
77 | * RFC3986 invalid characters. |
||
78 | * |
||
79 | * @see http://tools.ietf.org/html/rfc3986#section-2.2 |
||
80 | * |
||
81 | * @var string |
||
82 | */ |
||
83 | private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/'; |
||
84 | |||
85 | /** |
||
86 | * RFC3986 Sub delimiter characters regular expression pattern. |
||
87 | * |
||
88 | * @see http://tools.ietf.org/html/rfc3986#section-2.2 |
||
89 | * |
||
90 | * @var string |
||
91 | */ |
||
92 | private const REGEXP_CHARS_SUBDELIM = "\!\$&'\(\)\*\+,;\=%"; |
||
93 | |||
94 | /** |
||
95 | * RFC3986 unreserved characters regular expression pattern. |
||
96 | * |
||
97 | * @see http://tools.ietf.org/html/rfc3986#section-2.3 |
||
98 | * |
||
99 | * @var string |
||
100 | */ |
||
101 | private const REGEXP_CHARS_UNRESERVED = 'A-Za-z0-9_\-\.~'; |
||
102 | |||
103 | |||
104 | private const REGEXP_SCHEME = ',^[a-z]([-a-z0-9+.]+)?$,i'; |
||
105 | |||
106 | private const REGEXP_HOST_REGNAME = '/^( |
||
107 | (?<unreserved>[a-z0-9_~\-\.])| |
||
108 | (?<sub_delims>[!$&\'()*+,;=])| |
||
109 | (?<encoded>%[A-F0-9]{2}) |
||
110 | )+$/x'; |
||
111 | |||
112 | private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space. |
||
113 | |||
114 | private const REGEXP_HOST_IPFUTURE = '/^ |
||
115 | v(?<version>[A-F0-9])+\. |
||
116 | (?: |
||
117 | (?<unreserved>[a-z0-9_~\-\.])| |
||
118 | (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character |
||
119 | )+ |
||
120 | $/ix'; |
||
121 | |||
122 | private const HOST_ADDRESS_BLOCK = "\xfe\x80"; |
||
123 | |||
124 | private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<root>[a-zA-Z][:|\|])(?<rest>.*)?,'; |
||
125 | |||
126 | private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,'; |
||
127 | |||
128 | private const REGEXP_BINARY = ',(;|^)base64$,'; |
||
129 | |||
130 | /** |
||
131 | * IDNA errors. |
||
132 | */ |
||
133 | private const IDNA_ERRORS = [ |
||
134 | IDNA_ERROR_EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty', |
||
135 | IDNA_ERROR_LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes', |
||
136 | IDNA_ERROR_DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form', |
||
137 | IDNA_ERROR_LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")', |
||
138 | IDNA_ERROR_TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")', |
||
139 | IDNA_ERROR_HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions', |
||
140 | IDNA_ERROR_LEADING_COMBINING_MARK => 'a label starts with a combining mark', |
||
141 | IDNA_ERROR_DISALLOWED => 'a label or domain name contains disallowed characters', |
||
142 | IDNA_ERROR_PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode', |
||
143 | IDNA_ERROR_LABEL_HAS_DOT => 'a label contains a dot=full stop', |
||
144 | IDNA_ERROR_INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string', |
||
145 | IDNA_ERROR_BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)', |
||
146 | IDNA_ERROR_CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements', |
||
147 | ]; |
||
148 | |||
149 | private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),'; |
||
150 | |||
151 | /** |
||
152 | * Supported schemes and corresponding default port. |
||
153 | * |
||
154 | * @var array |
||
155 | */ |
||
156 | private const SCHEME_DEFAULT_PORT = [ |
||
157 | 'data' => null, |
||
158 | 'file' => null, |
||
159 | 'ftp' => 21, |
||
160 | 'gopher' => 70, |
||
161 | 'http' => 80, |
||
162 | 'https' => 443, |
||
163 | 'ws' => 80, |
||
164 | 'wss' => 443, |
||
165 | ]; |
||
166 | |||
167 | /** |
||
168 | * URI validation methods per scheme. |
||
169 | * |
||
170 | * @var array |
||
171 | */ |
||
172 | private const SCHEME_VALIDATION_METHOD = [ |
||
173 | 'data' => 'isUriWithSchemeAndPathOnly', |
||
174 | 'file' => 'isUriWithSchemeHostAndPathOnly', |
||
175 | 'ftp' => 'isNonEmptyHostUriWithoutFragmentAndQuery', |
||
176 | 'gopher' => 'isNonEmptyHostUriWithoutFragmentAndQuery', |
||
177 | 'http' => 'isNonEmptyHostUri', |
||
178 | 'https' => 'isNonEmptyHostUri', |
||
179 | 'ws' => 'isNonEmptyHostUriWithoutFragment', |
||
180 | 'wss' => 'isNonEmptyHostUriWithoutFragment', |
||
181 | ]; |
||
182 | |||
183 | /** |
||
184 | * URI scheme component. |
||
185 | * |
||
186 | * @var string|null |
||
187 | */ |
||
188 | private $scheme; |
||
189 | |||
190 | /** |
||
191 | * URI user info part. |
||
192 | * |
||
193 | * @var string|null |
||
194 | */ |
||
195 | private $user_info; |
||
196 | |||
197 | /** |
||
198 | * URI host component. |
||
199 | * |
||
200 | * @var string|null |
||
201 | */ |
||
202 | private $host; |
||
203 | |||
204 | /** |
||
205 | * URI port component. |
||
206 | * |
||
207 | * @var int|null |
||
208 | */ |
||
209 | private $port; |
||
210 | |||
211 | /** |
||
212 | * URI authority string representation. |
||
213 | * |
||
214 | * @var string|null |
||
215 | */ |
||
216 | private $authority; |
||
217 | |||
218 | /** |
||
219 | * URI path component. |
||
220 | * |
||
221 | * @var string |
||
222 | */ |
||
223 | private $path = ''; |
||
224 | |||
225 | /** |
||
226 | * URI query component. |
||
227 | * |
||
228 | * @var string|null |
||
229 | */ |
||
230 | private $query; |
||
231 | |||
232 | /** |
||
233 | * URI fragment component. |
||
234 | * |
||
235 | * @var string|null |
||
236 | */ |
||
237 | private $fragment; |
||
238 | |||
239 | /** |
||
240 | * URI string representation. |
||
241 | * |
||
242 | * @var string|null |
||
243 | */ |
||
244 | private $uri; |
||
245 | |||
246 | /** |
||
247 | * Create a new instance. |
||
248 | * |
||
249 | * @param ?string $scheme |
||
250 | * @param ?string $user |
||
251 | * @param ?string $pass |
||
252 | * @param ?string $host |
||
253 | * @param ?int $port |
||
254 | * @param ?string $query |
||
255 | * @param ?string $fragment |
||
256 | */ |
||
257 | 300 | private function __construct( |
|
258 | ?string $scheme, |
||
259 | ?string $user, |
||
260 | ?string $pass, |
||
261 | ?string $host, |
||
262 | ?int $port, |
||
263 | string $path, |
||
264 | ?string $query, |
||
265 | ?string $fragment |
||
266 | ) { |
||
267 | 300 | $this->scheme = $this->formatScheme($scheme); |
|
268 | 300 | $this->user_info = $this->formatUserInfo($user, $pass); |
|
269 | 300 | $this->host = $this->formatHost($host); |
|
270 | 300 | $this->port = $this->formatPort($port); |
|
271 | 300 | $this->authority = $this->setAuthority(); |
|
272 | 300 | $this->path = $this->formatPath($path); |
|
273 | 300 | $this->query = $this->formatQueryAndFragment($query); |
|
274 | 300 | $this->fragment = $this->formatQueryAndFragment($fragment); |
|
275 | 300 | $this->assertValidState(); |
|
276 | 286 | } |
|
277 | |||
278 | /** |
||
279 | * Format the Scheme and Host component. |
||
280 | * |
||
281 | * @param ?string $scheme |
||
282 | * |
||
283 | * @throws SyntaxError if the scheme is invalid |
||
284 | */ |
||
285 | 306 | private function formatScheme(?string $scheme): ?string |
|
286 | { |
||
287 | 306 | if ('' === $scheme || null === $scheme) { |
|
288 | 234 | return $scheme; |
|
289 | } |
||
290 | |||
291 | 238 | $formatted_scheme = strtolower($scheme); |
|
292 | 238 | if (1 === preg_match(self::REGEXP_SCHEME, $formatted_scheme)) { |
|
293 | 238 | return $formatted_scheme; |
|
294 | } |
||
295 | |||
296 | 2 | throw new SyntaxError(sprintf('The scheme `%s` is invalid', $scheme)); |
|
297 | } |
||
298 | |||
299 | /** |
||
300 | * Set the UserInfo component. |
||
301 | * |
||
302 | * @param ?string $user |
||
303 | * @param ?string $password |
||
304 | */ |
||
305 | 308 | private function formatUserInfo(?string $user, ?string $password): ?string |
|
306 | { |
||
307 | 308 | if (null === $user) { |
|
308 | 282 | return $user; |
|
309 | } |
||
310 | |||
311 | 56 | static $user_pattern = '/(?:[^%'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/'; |
|
312 | 56 | $user = preg_replace_callback($user_pattern, [Uri::class, 'urlEncodeMatch'], $user); |
|
313 | 56 | if (null === $password) { |
|
314 | 6 | return $user; |
|
315 | } |
||
316 | |||
317 | 56 | static $password_pattern = '/(?:[^%:'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.']++|%(?![A-Fa-f0-9]{2}))/'; |
|
318 | |||
319 | 56 | return $user.':'.preg_replace_callback($password_pattern, [Uri::class, 'urlEncodeMatch'], $password); |
|
320 | } |
||
321 | |||
322 | /** |
||
323 | * Returns the RFC3986 encoded string matched. |
||
324 | */ |
||
325 | 10 | private static function urlEncodeMatch(array $matches): string |
|
326 | { |
||
327 | 10 | return rawurlencode($matches[0]); |
|
328 | } |
||
329 | |||
330 | /** |
||
331 | * Validate and Format the Host component. |
||
332 | * |
||
333 | * @param ?string $host |
||
334 | */ |
||
335 | 332 | private function formatHost(?string $host): ?string |
|
336 | { |
||
337 | 332 | if (null === $host || '' === $host) { |
|
338 | 234 | return $host; |
|
339 | } |
||
340 | |||
341 | 266 | if ('[' !== $host[0]) { |
|
342 | 266 | return $this->formatRegisteredName($host); |
|
343 | } |
||
344 | |||
345 | 2 | return $this->formatIp($host); |
|
346 | } |
||
347 | |||
348 | /** |
||
349 | * Validate and format a registered name. |
||
350 | * |
||
351 | * The host is converted to its ascii representation if needed |
||
352 | * |
||
353 | * @throws IdnSupportMissing if the submitted host required missing or misconfigured IDN support |
||
354 | * @throws SyntaxError if the submitted host is not a valid registered name |
||
355 | */ |
||
356 | 264 | private function formatRegisteredName(string $host): string |
|
357 | { |
||
358 | // @codeCoverageIgnoreStart |
||
359 | // added because it is not possible in travis to disabled the ext/intl extension |
||
360 | // see travis issue https://github.com/travis-ci/travis-ci/issues/4701 |
||
361 | static $idn_support = null; |
||
362 | $idn_support = $idn_support ?? function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46'); |
||
363 | // @codeCoverageIgnoreEnd |
||
364 | |||
365 | 264 | $formatted_host = rawurldecode($host); |
|
366 | 264 | if (1 === preg_match(self::REGEXP_HOST_REGNAME, $formatted_host)) { |
|
367 | 256 | $formatted_host = strtolower($formatted_host); |
|
368 | 256 | if (false === strpos($formatted_host, 'xn--')) { |
|
369 | 252 | return $formatted_host; |
|
370 | } |
||
371 | |||
372 | // @codeCoverageIgnoreStart |
||
373 | if (!$idn_support) { |
||
374 | throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host)); |
||
375 | } |
||
376 | // @codeCoverageIgnoreEnd |
||
377 | |||
378 | 8 | $unicode = idn_to_utf8( |
|
379 | 8 | $host, |
|
380 | 8 | IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_UNICODE, |
|
381 | 8 | INTL_IDNA_VARIANT_UTS46, |
|
382 | 4 | $arr |
|
383 | ); |
||
384 | |||
385 | 8 | if (0 !== $arr['errors']) { |
|
386 | 2 | throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors']))); |
|
387 | } |
||
388 | |||
389 | // @codeCoverageIgnoreStart |
||
390 | if (false === $unicode) { |
||
391 | throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS)); |
||
392 | } |
||
393 | // @codeCoverageIgnoreEnd |
||
394 | |||
395 | 6 | return $formatted_host; |
|
396 | } |
||
397 | |||
398 | 14 | if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, $formatted_host)) { |
|
399 | 2 | throw new SyntaxError(sprintf('The host `%s` is invalid : a registered name can not contain URI delimiters or spaces', $host)); |
|
400 | } |
||
401 | |||
402 | // @codeCoverageIgnoreStart |
||
403 | if (!$idn_support) { |
||
404 | throw new IdnSupportMissing(sprintf('the host `%s` could not be processed for IDN. Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6.', $host)); |
||
405 | } |
||
406 | // @codeCoverageIgnoreEnd |
||
407 | |||
408 | 12 | $formatted_host = idn_to_ascii( |
|
409 | 12 | $formatted_host, |
|
410 | 12 | IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ | IDNA_NONTRANSITIONAL_TO_ASCII, |
|
411 | 12 | INTL_IDNA_VARIANT_UTS46, |
|
412 | 12 | $arr |
|
413 | ); |
||
414 | 12 | if (0 !== $arr['errors']) { |
|
415 | 2 | throw new SyntaxError(sprintf('The host `%s` is invalid : %s', $host, $this->getIDNAErrors($arr['errors']))); |
|
416 | } |
||
417 | |||
418 | // @codeCoverageIgnoreStart |
||
419 | if (false === $formatted_host) { |
||
420 | throw new IdnSupportMissing(sprintf('The Intl extension is misconfigured for %s, please correct this issue before proceeding.', PHP_OS)); |
||
421 | } |
||
422 | // @codeCoverageIgnoreEnd |
||
423 | |||
424 | 10 | return $arr['result']; |
|
425 | } |
||
426 | |||
427 | /** |
||
428 | * Retrieves and format IDNA conversion error message. |
||
429 | * |
||
430 | * @see http://icu-project.org/apiref/icu4j/com/ibm/icu/text/IDNA.Error.html |
||
431 | */ |
||
432 | 4 | private function getIDNAErrors(int $error_byte): string |
|
433 | { |
||
434 | 4 | $res = []; |
|
435 | 4 | foreach (self::IDNA_ERRORS as $error => $reason) { |
|
436 | 4 | if ($error === ($error_byte & $error)) { |
|
437 | 4 | $res[] = $reason; |
|
438 | } |
||
439 | } |
||
440 | |||
441 | 4 | return [] === $res ? 'Unknown IDNA conversion error.' : implode(', ', $res).'.'; |
|
442 | } |
||
443 | |||
444 | /** |
||
445 | * Validate and Format the IPv6/IPvfuture host. |
||
446 | * |
||
447 | * @throws SyntaxError if the submitted host is not a valid IP host |
||
448 | */ |
||
449 | 16 | private function formatIp(string $host): string |
|
450 | { |
||
451 | 16 | $ip = substr($host, 1, -1); |
|
452 | 16 | if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { |
|
453 | 2 | return $host; |
|
454 | } |
||
455 | |||
456 | 14 | if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) { |
|
457 | 2 | return $host; |
|
458 | } |
||
459 | |||
460 | 12 | $pos = strpos($ip, '%'); |
|
461 | 12 | if (false === $pos) { |
|
462 | 4 | throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host)); |
|
463 | } |
||
464 | |||
465 | 8 | if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) { |
|
466 | 2 | throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host)); |
|
467 | } |
||
468 | |||
469 | 6 | $ip = substr($ip, 0, $pos); |
|
470 | 6 | if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { |
|
471 | 2 | throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host)); |
|
472 | } |
||
473 | |||
474 | //Only the address block fe80::/10 can have a Zone ID attach to |
||
475 | //let's detect the link local significant 10 bits |
||
476 | 4 | if (0 === strpos((string) inet_pton($ip), self::HOST_ADDRESS_BLOCK)) { |
|
477 | 2 | return $host; |
|
478 | } |
||
479 | |||
480 | 2 | throw new SyntaxError(sprintf('The host `%s` is invalid : the IP host is malformed', $host)); |
|
481 | } |
||
482 | |||
483 | /** |
||
484 | * Format the Port component. |
||
485 | * |
||
486 | * @param null|mixed $port |
||
487 | */ |
||
488 | 328 | private function formatPort($port = null): ?int |
|
489 | { |
||
490 | 328 | if (null === $port || '' === $port) { |
|
491 | 278 | return null; |
|
492 | } |
||
493 | |||
494 | 92 | if (!is_int($port) && !(is_string($port) && 1 === preg_match('/^\d*$/', $port))) { |
|
495 | 2 | throw new SyntaxError(sprintf('The port `%s` is invalid', $port)); |
|
496 | } |
||
497 | |||
498 | 92 | $port = (int) $port; |
|
499 | 92 | if (0 > $port) { |
|
500 | 2 | throw new SyntaxError(sprintf('The port `%s` is invalid', $port)); |
|
501 | } |
||
502 | |||
503 | 92 | $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null; |
|
504 | 92 | if ($defaultPort === $port) { |
|
505 | 14 | return null; |
|
506 | } |
||
507 | |||
508 | 84 | return $port; |
|
509 | } |
||
510 | |||
511 | /** |
||
512 | * {@inheritDoc} |
||
513 | */ |
||
514 | 18 | public static function __set_state(array $components): self |
|
515 | { |
||
516 | 18 | $components['user'] = null; |
|
517 | 18 | $components['pass'] = null; |
|
518 | 18 | if (null !== $components['user_info']) { |
|
519 | 14 | [$components['user'], $components['pass']] = explode(':', $components['user_info'], 2) + [1 => null]; |
|
520 | } |
||
521 | |||
522 | 18 | return new self( |
|
523 | 18 | $components['scheme'], |
|
524 | 18 | $components['user'], |
|
525 | 18 | $components['pass'], |
|
526 | 18 | $components['host'], |
|
527 | 18 | $components['port'], |
|
528 | 18 | $components['path'], |
|
529 | 18 | $components['query'], |
|
530 | 18 | $components['fragment'] |
|
531 | ); |
||
532 | } |
||
533 | |||
534 | /** |
||
535 | * Create a new instance from a URI and a Base URI. |
||
536 | * |
||
537 | * The returned URI must be absolute. |
||
538 | * |
||
539 | * @param mixed $uri the input URI to create |
||
540 | * @param mixed $base_uri the base URI used for reference |
||
541 | */ |
||
542 | 86 | public static function createFromBaseUri($uri, $base_uri = null): UriInterface |
|
543 | { |
||
544 | 86 | if (!$uri instanceof UriInterface) { |
|
545 | 86 | $uri = self::createFromString($uri); |
|
546 | } |
||
547 | |||
548 | 86 | if (null === $base_uri) { |
|
549 | 6 | if (null === $uri->getScheme()) { |
|
550 | 2 | throw new SyntaxError(sprintf('the URI `%s` must be absolute', (string) $uri)); |
|
551 | } |
||
552 | |||
553 | 4 | if (null === $uri->getAuthority()) { |
|
554 | 2 | return $uri; |
|
555 | } |
||
556 | |||
557 | /** @var UriInterface $uri */ |
||
558 | 2 | $uri = UriResolver::resolve($uri, $uri->withFragment(null)->withQuery(null)->withPath('')); |
|
559 | |||
560 | 2 | return $uri; |
|
561 | } |
||
562 | |||
563 | 80 | if (!$base_uri instanceof UriInterface) { |
|
564 | 80 | $base_uri = self::createFromString($base_uri); |
|
565 | } |
||
566 | |||
567 | 80 | if (null === $base_uri->getScheme()) { |
|
568 | 2 | throw new SyntaxError(sprintf('the base URI `%s` must be absolute', (string) $base_uri)); |
|
569 | } |
||
570 | |||
571 | /** @var UriInterface $uri */ |
||
572 | 78 | $uri = UriResolver::resolve($uri, $base_uri); |
|
573 | |||
574 | 78 | return $uri; |
|
575 | } |
||
576 | |||
577 | /** |
||
578 | * Create a new instance from a string. |
||
579 | * |
||
580 | * @param string|mixed $uri |
||
581 | */ |
||
582 | 278 | public static function createFromString($uri = ''): self |
|
583 | { |
||
584 | 278 | $components = UriString::parse($uri); |
|
585 | |||
586 | 278 | return new self( |
|
587 | 278 | $components['scheme'], |
|
588 | 278 | $components['user'], |
|
589 | 278 | $components['pass'], |
|
590 | 278 | $components['host'], |
|
591 | 278 | $components['port'], |
|
592 | 278 | $components['path'], |
|
593 | 278 | $components['query'], |
|
594 | 278 | $components['fragment'] |
|
595 | ); |
||
596 | } |
||
597 | |||
598 | /** |
||
599 | * Create a new instance from a hash of parse_url parts. |
||
600 | * |
||
601 | * Create an new instance from a hash representation of the URI similar |
||
602 | * to PHP parse_url function result |
||
603 | * |
||
604 | * @param array<string, mixed> $components |
||
605 | */ |
||
606 | 90 | public static function createFromComponents(array $components = []): self |
|
607 | { |
||
608 | $components += [ |
||
609 | 90 | 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, |
|
610 | 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, |
||
611 | ]; |
||
612 | |||
613 | 90 | return new self( |
|
614 | 90 | $components['scheme'], |
|
615 | 90 | $components['user'], |
|
616 | 90 | $components['pass'], |
|
617 | 90 | $components['host'], |
|
618 | 90 | $components['port'], |
|
619 | 90 | $components['path'], |
|
620 | 90 | $components['query'], |
|
621 | 90 | $components['fragment'] |
|
622 | ); |
||
623 | } |
||
624 | |||
625 | /** |
||
626 | * Create a new instance from a data file path. |
||
627 | * |
||
628 | * @param resource|null $context |
||
629 | * |
||
630 | * @throws SyntaxError If the file does not exist or is not readable |
||
631 | */ |
||
632 | 6 | public static function createFromDataPath(string $path, $context = null): self |
|
633 | { |
||
634 | 6 | $file_args = [$path, false]; |
|
635 | 6 | $mime_args = [$path, FILEINFO_MIME]; |
|
636 | 6 | if (null !== $context) { |
|
637 | 4 | $file_args[] = $context; |
|
638 | 4 | $mime_args[] = $context; |
|
639 | } |
||
640 | |||
641 | 6 | $raw = @file_get_contents(...$file_args); |
|
642 | 6 | if (false === $raw) { |
|
643 | 2 | throw new SyntaxError(sprintf('The file `%s` does not exist or is not readable', $path)); |
|
644 | } |
||
645 | |||
646 | 4 | $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mime_args); |
|
647 | |||
648 | 4 | return Uri::createFromComponents([ |
|
649 | 4 | 'scheme' => 'data', |
|
650 | 4 | 'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)), |
|
651 | ]); |
||
652 | } |
||
653 | |||
654 | /** |
||
655 | * Create a new instance from a Unix path string. |
||
656 | */ |
||
657 | 10 | public static function createFromUnixPath(string $uri = ''): self |
|
658 | { |
||
659 | 10 | $uri = implode('/', array_map('rawurlencode', explode('/', $uri))); |
|
660 | 10 | if ('/' !== ($uri[0] ?? '')) { |
|
661 | 4 | return Uri::createFromComponents(['path' => $uri]); |
|
662 | } |
||
663 | |||
664 | 6 | return Uri::createFromComponents(['path' => $uri, 'scheme' => 'file', 'host' => '']); |
|
665 | } |
||
666 | |||
667 | /** |
||
668 | * Create a new instance from a local Windows path string. |
||
669 | */ |
||
670 | 16 | public static function createFromWindowsPath(string $uri = ''): self |
|
671 | { |
||
672 | 16 | $root = ''; |
|
673 | 16 | if (1 === preg_match(self::REGEXP_WINDOW_PATH, $uri, $matches)) { |
|
674 | 8 | $root = substr($matches['root'], 0, -1).':'; |
|
675 | 8 | $uri = substr($uri, strlen($root)); |
|
676 | } |
||
677 | 16 | $uri = str_replace('\\', '/', $uri); |
|
678 | 16 | $uri = implode('/', array_map('rawurlencode', explode('/', $uri))); |
|
679 | |||
680 | //Local Windows absolute path |
||
681 | 16 | if ('' !== $root) { |
|
682 | 8 | return Uri::createFromComponents(['path' => '/'.$root.$uri, 'scheme' => 'file', 'host' => '']); |
|
683 | } |
||
684 | |||
685 | //UNC Windows Path |
||
686 | 8 | if ('//' !== substr($uri, 0, 2)) { |
|
687 | 6 | return Uri::createFromComponents(['path' => $uri]); |
|
688 | } |
||
689 | |||
690 | 2 | $parts = explode('/', substr($uri, 2), 2) + [1 => null]; |
|
691 | |||
692 | 2 | return Uri::createFromComponents(['host' => $parts[0], 'path' => '/'.$parts[1], 'scheme' => 'file']); |
|
693 | } |
||
694 | |||
695 | /** |
||
696 | * Create a new instance from a URI object. |
||
697 | * |
||
698 | * @param Psr7UriInterface|UriInterface $uri the input URI to create |
||
699 | */ |
||
700 | 4 | public static function createFromUri($uri): self |
|
701 | { |
||
702 | 4 | if ($uri instanceof UriInterface) { |
|
703 | 2 | $user_info = $uri->getUserInfo(); |
|
704 | 2 | $user = null; |
|
705 | 2 | $pass = null; |
|
706 | 2 | if (null !== $user_info) { |
|
707 | 2 | [$user, $pass] = explode(':', $user_info, 2) + [1 => null]; |
|
708 | } |
||
709 | |||
710 | 2 | return new self( |
|
711 | 2 | $uri->getScheme(), |
|
712 | 1 | $user, |
|
713 | 1 | $pass, |
|
714 | 2 | $uri->getHost(), |
|
715 | 2 | $uri->getPort(), |
|
716 | 2 | $uri->getPath(), |
|
717 | 2 | $uri->getQuery(), |
|
718 | 2 | $uri->getFragment() |
|
719 | ); |
||
720 | } |
||
721 | |||
722 | 4 | if (!$uri instanceof Psr7UriInterface) { |
|
723 | 2 | throw new TypeError(sprintf('The object must implement the `%s` or the `%s`', Psr7UriInterface::class, UriInterface::class)); |
|
724 | } |
||
725 | |||
726 | 2 | $scheme = $uri->getScheme(); |
|
727 | 2 | if ('' === $scheme) { |
|
728 | 2 | $scheme = null; |
|
729 | } |
||
730 | |||
731 | 2 | $fragment = $uri->getFragment(); |
|
732 | 2 | if ('' === $fragment) { |
|
733 | 2 | $fragment = null; |
|
734 | } |
||
735 | |||
736 | 2 | $query = $uri->getQuery(); |
|
737 | 2 | if ('' === $query) { |
|
738 | 2 | $query = null; |
|
739 | } |
||
740 | |||
741 | 2 | $host = $uri->getHost(); |
|
742 | 2 | if ('' === $host) { |
|
743 | 2 | $host = null; |
|
744 | } |
||
745 | |||
746 | 2 | $user_info = $uri->getUserInfo(); |
|
747 | 2 | $user = null; |
|
748 | 2 | $pass = null; |
|
749 | 2 | if ('' !== $user_info) { |
|
750 | 2 | [$user, $pass] = explode(':', $user_info, 2) + [1 => null]; |
|
751 | } |
||
752 | |||
753 | 2 | return new self( |
|
754 | 2 | $scheme, |
|
755 | 1 | $user, |
|
756 | 1 | $pass, |
|
757 | 1 | $host, |
|
758 | 2 | $uri->getPort(), |
|
759 | 2 | $uri->getPath(), |
|
760 | 1 | $query, |
|
761 | 1 | $fragment |
|
762 | ); |
||
763 | } |
||
764 | |||
765 | /** |
||
766 | * Create a new instance from the environment. |
||
767 | */ |
||
768 | 26 | public static function createFromServer(array $server): self |
|
769 | { |
||
770 | 26 | [$user, $pass] = self::fetchUserInfo($server); |
|
771 | 26 | [$host, $port] = self::fetchHostname($server); |
|
772 | 26 | [$path, $query] = self::fetchRequestUri($server); |
|
773 | |||
774 | 26 | return Uri::createFromComponents([ |
|
775 | 26 | 'scheme' => self::fetchScheme($server), |
|
776 | 26 | 'user' => $user, |
|
777 | 26 | 'pass' => $pass, |
|
778 | 26 | 'host' => $host, |
|
779 | 26 | 'port' => $port, |
|
780 | 26 | 'path' => $path, |
|
781 | 26 | 'query' => $query, |
|
782 | ]); |
||
783 | } |
||
784 | |||
785 | /** |
||
786 | * Returns the environment scheme. |
||
787 | */ |
||
788 | 26 | private static function fetchScheme(array $server): string |
|
789 | { |
||
790 | 26 | $server += ['HTTPS' => '']; |
|
791 | 26 | $res = filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); |
|
792 | |||
793 | 26 | return $res !== false ? 'https' : 'http'; |
|
794 | } |
||
795 | |||
796 | /** |
||
797 | * Returns the environment user info. |
||
798 | * |
||
799 | * @return array{0:?string, 1:?string} |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
800 | */ |
||
801 | 28 | private static function fetchUserInfo(array $server): array |
|
802 | { |
||
803 | 28 | $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => '']; |
|
804 | 28 | $user = $server['PHP_AUTH_USER']; |
|
805 | 28 | $pass = $server['PHP_AUTH_PW']; |
|
806 | 28 | if (0 === strpos(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) { |
|
807 | 4 | $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true); |
|
808 | 4 | if (false === $userinfo) { |
|
809 | 2 | throw new SyntaxError('The user info could not be detected'); |
|
810 | } |
||
811 | 2 | [$user, $pass] = explode(':', $userinfo, 2) + [1 => null]; |
|
812 | } |
||
813 | |||
814 | 26 | if (null !== $user) { |
|
815 | 4 | $user = rawurlencode($user); |
|
816 | } |
||
817 | |||
818 | 26 | if (null !== $pass) { |
|
819 | 4 | $pass = rawurlencode($pass); |
|
820 | } |
||
821 | |||
822 | 26 | return [$user, $pass]; |
|
823 | } |
||
824 | |||
825 | /** |
||
826 | * Returns the environment host. |
||
827 | * |
||
828 | * @throws SyntaxError If the host can not be detected |
||
829 | * |
||
830 | * @return array{0:?string, 1:?string} |
||
0 ignored issues
–
show
|
|||
831 | */ |
||
832 | 28 | private static function fetchHostname(array $server): array |
|
833 | { |
||
834 | 28 | $server += ['SERVER_PORT' => null]; |
|
835 | 28 | if (null !== $server['SERVER_PORT']) { |
|
836 | 26 | $server['SERVER_PORT'] = (int) $server['SERVER_PORT']; |
|
837 | } |
||
838 | |||
839 | 28 | if (isset($server['HTTP_HOST'])) { |
|
840 | 18 | preg_match(',^(?<host>(\[.*\]|[^:])*)(\:(?<port>[^/?\#]*))?$,x', $server['HTTP_HOST'], $matches); |
|
841 | |||
842 | return [ |
||
843 | 18 | $matches['host'], |
|
844 | 18 | isset($matches['port']) ? (int) $matches['port'] : $server['SERVER_PORT'], |
|
845 | ]; |
||
846 | } |
||
847 | |||
848 | 10 | if (!isset($server['SERVER_ADDR'])) { |
|
849 | 2 | throw new SyntaxError('The host could not be detected'); |
|
850 | } |
||
851 | |||
852 | 8 | if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { |
|
853 | 2 | $server['SERVER_ADDR'] = '['.$server['SERVER_ADDR'].']'; |
|
854 | } |
||
855 | |||
856 | 8 | return [$server['SERVER_ADDR'], $server['SERVER_PORT']]; |
|
857 | } |
||
858 | |||
859 | /** |
||
860 | * Returns the environment path. |
||
861 | * |
||
862 | * @return array{0:?string, 1:?string} |
||
0 ignored issues
–
show
|
|||
863 | */ |
||
864 | 26 | private static function fetchRequestUri(array $server): array |
|
865 | { |
||
866 | 26 | $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null]; |
|
867 | 26 | if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) { |
|
868 | /** @var array{0:?string, 1:?string} $retval */ |
||
869 | 2 | $retval = explode('?', $server['UNENCODED_URL'], 2) + [1 => null]; |
|
870 | |||
871 | 2 | return $retval; |
|
872 | } |
||
873 | |||
874 | 24 | if (isset($server['REQUEST_URI'])) { |
|
875 | 20 | [$path, ] = explode('?', $server['REQUEST_URI'], 2); |
|
876 | 20 | $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null; |
|
877 | |||
878 | 20 | return [$path, $query]; |
|
879 | } |
||
880 | |||
881 | 4 | return [$server['PHP_SELF'], $server['QUERY_STRING']]; |
|
882 | } |
||
883 | |||
884 | /** |
||
885 | * Generate the URI authority part. |
||
886 | */ |
||
887 | 300 | private function setAuthority(): ?string |
|
888 | { |
||
889 | 300 | $authority = null; |
|
890 | 300 | if (null !== $this->user_info) { |
|
891 | 48 | $authority = $this->user_info.'@'; |
|
892 | } |
||
893 | |||
894 | 300 | if (null !== $this->host) { |
|
895 | 252 | $authority .= $this->host; |
|
896 | } |
||
897 | |||
898 | 300 | if (null !== $this->port) { |
|
899 | 56 | $authority .= ':'.$this->port; |
|
900 | } |
||
901 | |||
902 | 300 | return $authority; |
|
903 | } |
||
904 | |||
905 | /** |
||
906 | * Format the Path component. |
||
907 | */ |
||
908 | 312 | private function formatPath(string $path): string |
|
909 | { |
||
910 | 312 | $path = $this->formatDataPath($path); |
|
911 | |||
912 | 312 | static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/}{]++\|%(?![A-Fa-f0-9]{2}))/'; |
|
913 | |||
914 | 312 | $path = (string) preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $path); |
|
915 | |||
916 | 312 | return $this->formatFilePath($path); |
|
917 | } |
||
918 | |||
919 | /** |
||
920 | * Filter the Path component. |
||
921 | * |
||
922 | * @see https://tools.ietf.org/html/rfc2397 |
||
923 | * |
||
924 | * @throws SyntaxError If the path is not compliant with RFC2397 |
||
925 | */ |
||
926 | 326 | private function formatDataPath(string $path): string |
|
927 | { |
||
928 | 326 | if ('data' !== $this->scheme) { |
|
929 | 298 | return $path; |
|
930 | } |
||
931 | |||
932 | 28 | if ('' == $path) { |
|
933 | 2 | return 'text/plain;charset=us-ascii,'; |
|
934 | } |
||
935 | |||
936 | 26 | if (false === mb_detect_encoding($path, 'US-ASCII', true) || false === strpos($path, ',')) { |
|
937 | 4 | throw new SyntaxError(sprintf('The path `%s` is invalid according to RFC2937', $path)); |
|
938 | } |
||
939 | |||
940 | 22 | $parts = explode(',', $path, 2) + [1 => null]; |
|
941 | 22 | $mediatype = explode(';', (string) $parts[0], 2) + [1 => null]; |
|
942 | 22 | $data = (string) $parts[1]; |
|
943 | 22 | $mimetype = $mediatype[0]; |
|
944 | 22 | if (null === $mimetype || '' === $mimetype) { |
|
945 | 4 | $mimetype = 'text/plain'; |
|
946 | } |
||
947 | |||
948 | 22 | $parameters = $mediatype[1]; |
|
949 | 22 | if (null === $parameters || '' === $parameters) { |
|
950 | 6 | $parameters = 'charset=us-ascii'; |
|
951 | } |
||
952 | |||
953 | 22 | $this->assertValidPath($mimetype, $parameters, $data); |
|
954 | |||
955 | 14 | return $mimetype.';'.$parameters.','.$data; |
|
956 | } |
||
957 | |||
958 | /** |
||
959 | * Assert the path is a compliant with RFC2397. |
||
960 | * |
||
961 | * @see https://tools.ietf.org/html/rfc2397 |
||
962 | * |
||
963 | * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397 |
||
964 | */ |
||
965 | 22 | private function assertValidPath(string $mimetype, string $parameters, string $data): void |
|
966 | { |
||
967 | 22 | if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) { |
|
968 | 2 | throw new SyntaxError(sprintf('The path mimetype `%s` is invalid', $mimetype)); |
|
969 | } |
||
970 | |||
971 | 20 | $is_binary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches); |
|
972 | 20 | if ($is_binary) { |
|
973 | 8 | $parameters = substr($parameters, 0, - strlen($matches[0])); |
|
974 | } |
||
975 | |||
976 | 20 | $res = array_filter(array_filter(explode(';', $parameters), [$this, 'validateParameter'])); |
|
977 | 20 | if ([] !== $res) { |
|
978 | 4 | throw new SyntaxError(sprintf('The path paremeters `%s` is invalid', $parameters)); |
|
979 | } |
||
980 | |||
981 | 16 | if (!$is_binary) { |
|
982 | 12 | return; |
|
983 | } |
||
984 | |||
985 | 4 | $res = base64_decode($data, true); |
|
986 | 4 | if (false === $res || $data !== base64_encode($res)) { |
|
987 | 2 | throw new SyntaxError(sprintf('The path data `%s` is invalid', $data)); |
|
988 | } |
||
989 | 2 | } |
|
990 | |||
991 | /** |
||
992 | * Validate mediatype parameter. |
||
993 | */ |
||
994 | 4 | private function validateParameter(string $parameter): bool |
|
995 | { |
||
996 | 4 | $properties = explode('=', $parameter); |
|
997 | |||
998 | 4 | return 2 != count($properties) || strtolower($properties[0]) === 'base64'; |
|
999 | } |
||
1000 | |||
1001 | 320 | private function formatFilePath(string $path): string |
|
1002 | { |
||
1003 | 320 | if ('file' !== $this->scheme) { |
|
1004 | 312 | return $path; |
|
1005 | } |
||
1006 | |||
1007 | $replace = static function (array $matches): string { |
||
1008 | 2 | return $matches['delim'].str_replace('|', ':', $matches['root']).$matches['rest']; |
|
1009 | 8 | }; |
|
1010 | |||
1011 | 8 | return (string) preg_replace_callback(self::REGEXP_FILE_PATH, $replace, $path); |
|
1012 | } |
||
1013 | |||
1014 | /** |
||
1015 | * Format the Query or the Fragment component. |
||
1016 | * |
||
1017 | * Returns a array containing: |
||
1018 | * <ul> |
||
1019 | * <li> the formatted component (a string or null)</li> |
||
1020 | * <li> a boolean flag telling wether the delimiter is to be added to the component |
||
1021 | * when building the URI string representation</li> |
||
1022 | * </ul> |
||
1023 | * |
||
1024 | * @param ?string $component |
||
1025 | */ |
||
1026 | 306 | private function formatQueryAndFragment(?string $component): ?string |
|
1027 | { |
||
1028 | 306 | if (null === $component || '' === $component) { |
|
1029 | 282 | return $component; |
|
1030 | } |
||
1031 | |||
1032 | 206 | static $pattern = '/(?:[^'.self::REGEXP_CHARS_UNRESERVED.self::REGEXP_CHARS_SUBDELIM.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/'; |
|
1033 | 206 | return preg_replace_callback($pattern, [Uri::class, 'urlEncodeMatch'], $component); |
|
1034 | } |
||
1035 | |||
1036 | /** |
||
1037 | * assert the URI internal state is valid. |
||
1038 | * |
||
1039 | * @see https://tools.ietf.org/html/rfc3986#section-3 |
||
1040 | * @see https://tools.ietf.org/html/rfc3986#section-3.3 |
||
1041 | * |
||
1042 | * @throws SyntaxError if the URI is in an invalid state according to RFC3986 |
||
1043 | * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules |
||
1044 | */ |
||
1045 | 354 | private function assertValidState(): void |
|
1046 | { |
||
1047 | 354 | if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) { |
|
1048 | 4 | throw new SyntaxError('If an authority is present the path must be empty or start with a `/`'); |
|
1049 | } |
||
1050 | |||
1051 | 354 | if (null === $this->authority && 0 === strpos($this->path, '//')) { |
|
1052 | 10 | throw new SyntaxError(sprintf('If there is no authority the path `%s` can not start with a `//`', $this->path)); |
|
1053 | } |
||
1054 | |||
1055 | 354 | $pos = strpos($this->path, ':'); |
|
1056 | 354 | if (null === $this->authority |
|
1057 | 354 | && null === $this->scheme |
|
1058 | 354 | && false !== $pos |
|
1059 | 354 | && false === strpos(substr($this->path, 0, $pos), '/') |
|
1060 | ) { |
||
1061 | 6 | throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.'); |
|
1062 | } |
||
1063 | |||
1064 | 354 | $validationMethod = self::SCHEME_VALIDATION_METHOD[$this->scheme] ?? null; |
|
1065 | 354 | if (null === $validationMethod || true === $this->$validationMethod()) { |
|
1066 | 326 | $this->uri = null; |
|
1067 | |||
1068 | 326 | return; |
|
1069 | } |
||
1070 | |||
1071 | 38 | throw new SyntaxError(sprintf('The uri `%s` is invalid for the data scheme', (string) $this)); |
|
1072 | } |
||
1073 | |||
1074 | /** |
||
1075 | * URI validation for URI schemes which allows only scheme and path components. |
||
1076 | */ |
||
1077 | 2 | private function isUriWithSchemeAndPathOnly() |
|
1078 | { |
||
1079 | 2 | return null === $this->authority |
|
1080 | 2 | && null === $this->query |
|
1081 | 2 | && null === $this->fragment; |
|
1082 | } |
||
1083 | |||
1084 | /** |
||
1085 | * URI validation for URI schemes which allows only scheme, host and path components. |
||
1086 | */ |
||
1087 | 20 | private function isUriWithSchemeHostAndPathOnly() |
|
1088 | { |
||
1089 | 20 | return null === $this->user_info |
|
1090 | 20 | && null === $this->port |
|
1091 | 20 | && null === $this->query |
|
1092 | 20 | && null === $this->fragment |
|
1093 | 20 | && !('' != $this->scheme && null === $this->host); |
|
1094 | } |
||
1095 | |||
1096 | /** |
||
1097 | * URI validation for URI schemes which disallow the empty '' host. |
||
1098 | */ |
||
1099 | 246 | private function isNonEmptyHostUri() |
|
1100 | { |
||
1101 | 246 | return '' !== $this->host |
|
1102 | 246 | && !(null !== $this->scheme && null === $this->host); |
|
1103 | } |
||
1104 | |||
1105 | /** |
||
1106 | * URI validation for URIs schemes which disallow the empty '' host |
||
1107 | * and forbids the fragment component. |
||
1108 | */ |
||
1109 | 18 | private function isNonEmptyHostUriWithoutFragment() |
|
1110 | { |
||
1111 | 18 | return $this->isNonEmptyHostUri() && null === $this->fragment; |
|
1112 | } |
||
1113 | |||
1114 | /** |
||
1115 | * URI validation for URIs schemes which disallow the empty '' host |
||
1116 | * and forbids fragment and query components. |
||
1117 | */ |
||
1118 | 22 | private function isNonEmptyHostUriWithoutFragmentAndQuery() |
|
1119 | { |
||
1120 | 22 | return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query; |
|
1121 | } |
||
1122 | |||
1123 | /** |
||
1124 | * Generate the URI string representation from its components. |
||
1125 | * |
||
1126 | * @see https://tools.ietf.org/html/rfc3986#section-5.3 |
||
1127 | * |
||
1128 | * @param ?string $scheme |
||
1129 | * @param ?string $authority |
||
1130 | * @param ?string $query |
||
1131 | * @param ?string $fragment |
||
1132 | */ |
||
1133 | 250 | private function getUriString( |
|
1134 | ?string $scheme, |
||
1135 | ?string $authority, |
||
1136 | string $path, |
||
1137 | ?string $query, |
||
1138 | ?string $fragment |
||
1139 | ): string { |
||
1140 | 250 | if (null !== $scheme) { |
|
1141 | 132 | $scheme = $scheme.':'; |
|
1142 | } |
||
1143 | |||
1144 | 250 | if (null !== $authority) { |
|
1145 | 122 | $authority = '//'.$authority; |
|
1146 | } |
||
1147 | |||
1148 | 250 | if (null !== $query) { |
|
1149 | 40 | $query = '?'.$query; |
|
1150 | } |
||
1151 | |||
1152 | 250 | if (null !== $fragment) { |
|
1153 | 34 | $fragment = '#'.$fragment; |
|
1154 | } |
||
1155 | |||
1156 | 250 | return $scheme.$authority.$path.$query.$fragment; |
|
1157 | } |
||
1158 | |||
1159 | /** |
||
1160 | * {@inheritDoc} |
||
1161 | */ |
||
1162 | 260 | public function __toString(): string |
|
1163 | { |
||
1164 | 260 | $this->uri = $this->uri ?? $this->getUriString( |
|
1165 | 260 | $this->scheme, |
|
1166 | 260 | $this->authority, |
|
1167 | 260 | $this->path, |
|
1168 | 260 | $this->query, |
|
1169 | 260 | $this->fragment |
|
1170 | ); |
||
1171 | |||
1172 | 260 | return $this->uri; |
|
1173 | } |
||
1174 | |||
1175 | /** |
||
1176 | * {@inheritDoc} |
||
1177 | */ |
||
1178 | 2 | public function jsonSerialize(): string |
|
1179 | { |
||
1180 | 2 | return $this->__toString(); |
|
1181 | } |
||
1182 | |||
1183 | /** |
||
1184 | * {@inheritDoc} |
||
1185 | * |
||
1186 | * @return array{scheme:?string, user_info:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} |
||
0 ignored issues
–
show
|
|||
1187 | */ |
||
1188 | 2 | public function __debugInfo(): array |
|
1189 | { |
||
1190 | return [ |
||
1191 | 2 | 'scheme' => $this->scheme, |
|
1192 | 2 | 'user_info' => isset($this->user_info) ? preg_replace(',\:(.*).?$,', ':***', $this->user_info) : null, |
|
1193 | 2 | 'host' => $this->host, |
|
1194 | 2 | 'port' => $this->port, |
|
1195 | 2 | 'path' => $this->path, |
|
1196 | 2 | 'query' => $this->query, |
|
1197 | 2 | 'fragment' => $this->fragment, |
|
1198 | ]; |
||
1199 | } |
||
1200 | |||
1201 | /** |
||
1202 | * {@inheritDoc} |
||
1203 | */ |
||
1204 | 230 | public function getScheme(): ?string |
|
1205 | { |
||
1206 | 230 | return $this->scheme; |
|
1207 | } |
||
1208 | |||
1209 | /** |
||
1210 | * {@inheritDoc} |
||
1211 | */ |
||
1212 | 198 | public function getAuthority(): ?string |
|
1213 | { |
||
1214 | 198 | return $this->authority; |
|
1215 | } |
||
1216 | |||
1217 | /** |
||
1218 | * {@inheritDoc} |
||
1219 | */ |
||
1220 | 96 | public function getUserInfo(): ?string |
|
1221 | { |
||
1222 | 96 | return $this->user_info; |
|
1223 | } |
||
1224 | |||
1225 | /** |
||
1226 | * {@inheritDoc} |
||
1227 | */ |
||
1228 | 208 | public function getHost(): ?string |
|
1229 | { |
||
1230 | 208 | return $this->host; |
|
1231 | } |
||
1232 | |||
1233 | /** |
||
1234 | * {@inheritDoc} |
||
1235 | */ |
||
1236 | 238 | public function getPort(): ?int |
|
1237 | { |
||
1238 | 238 | return $this->port; |
|
1239 | } |
||
1240 | |||
1241 | /** |
||
1242 | * {@inheritDoc} |
||
1243 | */ |
||
1244 | 204 | public function getPath(): string |
|
1245 | { |
||
1246 | 204 | return $this->path; |
|
1247 | } |
||
1248 | |||
1249 | /** |
||
1250 | * {@inheritDoc} |
||
1251 | */ |
||
1252 | 114 | public function getQuery(): ?string |
|
1253 | { |
||
1254 | 114 | return $this->query; |
|
1255 | } |
||
1256 | |||
1257 | /** |
||
1258 | * {@inheritDoc} |
||
1259 | */ |
||
1260 | 26 | public function getFragment(): ?string |
|
1261 | { |
||
1262 | 26 | return $this->fragment; |
|
1263 | } |
||
1264 | |||
1265 | /** |
||
1266 | * {@inheritDoc} |
||
1267 | */ |
||
1268 | 146 | public function withScheme($scheme): UriInterface |
|
1269 | { |
||
1270 | 146 | $scheme = $this->formatScheme($this->filterString($scheme)); |
|
1271 | 144 | if ($scheme === $this->scheme) { |
|
1272 | 10 | return $this; |
|
1273 | } |
||
1274 | |||
1275 | 136 | $clone = clone $this; |
|
1276 | 136 | $clone->scheme = $scheme; |
|
1277 | 136 | $clone->port = $clone->formatPort($clone->port); |
|
1278 | 136 | $clone->authority = $clone->setAuthority(); |
|
1279 | 136 | $clone->assertValidState(); |
|
1280 | |||
1281 | 136 | return $clone; |
|
1282 | } |
||
1283 | |||
1284 | /** |
||
1285 | * Filter a string. |
||
1286 | * |
||
1287 | * @param mixed $str the value to evaluate as a string |
||
1288 | * |
||
1289 | * @throws SyntaxError if the submitted data can not be converted to string |
||
1290 | */ |
||
1291 | 206 | private function filterString($str): ?string |
|
1292 | { |
||
1293 | 206 | if (null === $str) { |
|
1294 | 146 | return $str; |
|
1295 | } |
||
1296 | |||
1297 | 204 | if (!is_scalar($str) && !method_exists($str, '__toString')) { |
|
1298 | 2 | throw new TypeError(sprintf('The component must be a string, a scalar or a stringable object %s given', gettype($str))); |
|
1299 | } |
||
1300 | |||
1301 | 202 | $str = (string) $str; |
|
1302 | 202 | if (1 !== preg_match(self::REGEXP_INVALID_CHARS, $str)) { |
|
1303 | 200 | return $str; |
|
1304 | } |
||
1305 | |||
1306 | 2 | throw new SyntaxError(sprintf('The component `%s` contains invalid characters', $str)); |
|
1307 | } |
||
1308 | |||
1309 | /** |
||
1310 | * {@inheritDoc} |
||
1311 | * @param null|mixed $password |
||
1312 | */ |
||
1313 | 146 | public function withUserInfo($user, $password = null): UriInterface |
|
1314 | { |
||
1315 | 146 | $user_info = null; |
|
1316 | 146 | $user = $this->filterString($user); |
|
1317 | 146 | if (null !== $password) { |
|
1318 | 16 | $password = $this->filterString($password); |
|
1319 | } |
||
1320 | |||
1321 | 146 | if ('' !== $user) { |
|
1322 | 76 | $user_info = $this->formatUserInfo($user, $password); |
|
1323 | } |
||
1324 | |||
1325 | 146 | if ($user_info === $this->user_info) { |
|
1326 | 128 | return $this; |
|
1327 | } |
||
1328 | |||
1329 | 20 | $clone = clone $this; |
|
1330 | 20 | $clone->user_info = $user_info; |
|
1331 | 20 | $clone->authority = $clone->setAuthority(); |
|
1332 | 20 | $clone->assertValidState(); |
|
1333 | |||
1334 | 20 | return $clone; |
|
1335 | } |
||
1336 | |||
1337 | /** |
||
1338 | * {@inheritDoc} |
||
1339 | */ |
||
1340 | 178 | public function withHost($host): UriInterface |
|
1341 | { |
||
1342 | 178 | $host = $this->formatHost($this->filterString($host)); |
|
1343 | 176 | if ($host === $this->host) { |
|
1344 | 96 | return $this; |
|
1345 | } |
||
1346 | |||
1347 | 132 | $clone = clone $this; |
|
1348 | 132 | $clone->host = $host; |
|
1349 | 132 | $clone->authority = $clone->setAuthority(); |
|
1350 | 132 | $clone->assertValidState(); |
|
1351 | |||
1352 | 132 | return $clone; |
|
1353 | } |
||
1354 | |||
1355 | /** |
||
1356 | * {@inheritDoc} |
||
1357 | */ |
||
1358 | 136 | public function withPort($port): UriInterface |
|
1359 | { |
||
1360 | 136 | $port = $this->formatPort($port); |
|
1361 | 132 | if ($port === $this->port) { |
|
1362 | 130 | return $this; |
|
1363 | } |
||
1364 | |||
1365 | 4 | $clone = clone $this; |
|
1366 | 4 | $clone->port = $port; |
|
1367 | 4 | $clone->authority = $clone->setAuthority(); |
|
1368 | 4 | $clone->assertValidState(); |
|
1369 | |||
1370 | 4 | return $clone; |
|
1371 | } |
||
1372 | |||
1373 | /** |
||
1374 | * {@inheritDoc} |
||
1375 | */ |
||
1376 | 172 | public function withPath($path): UriInterface |
|
1377 | { |
||
1378 | 172 | $path = $this->filterString($path); |
|
1379 | 172 | if (null === $path) { |
|
1380 | 2 | throw new TypeError('A path must be a string NULL given'); |
|
1381 | } |
||
1382 | |||
1383 | 170 | $path = $this->formatPath($path); |
|
1384 | 170 | if ($path === $this->path) { |
|
1385 | 34 | return $this; |
|
1386 | } |
||
1387 | |||
1388 | 158 | $clone = clone $this; |
|
1389 | 158 | $clone->path = $path; |
|
1390 | 158 | $clone->assertValidState(); |
|
1391 | |||
1392 | 146 | return $clone; |
|
1393 | } |
||
1394 | |||
1395 | /** |
||
1396 | * {@inheritDoc} |
||
1397 | */ |
||
1398 | 104 | public function withQuery($query): UriInterface |
|
1399 | { |
||
1400 | 104 | $query = $this->formatQueryAndFragment($this->filterString($query)); |
|
1401 | 104 | if ($query === $this->query) { |
|
1402 | 96 | return $this; |
|
1403 | } |
||
1404 | |||
1405 | 14 | $clone = clone $this; |
|
1406 | 14 | $clone->query = $query; |
|
1407 | 14 | $clone->assertValidState(); |
|
1408 | |||
1409 | 14 | return $clone; |
|
1410 | } |
||
1411 | |||
1412 | /** |
||
1413 | * {@inheritDoc} |
||
1414 | */ |
||
1415 | 24 | public function withFragment($fragment): UriInterface |
|
1416 | { |
||
1417 | 24 | $fragment = $this->formatQueryAndFragment($this->filterString($fragment)); |
|
1418 | 24 | if ($fragment === $this->fragment) { |
|
1419 | 24 | return $this; |
|
1420 | } |
||
1421 | |||
1422 | 4 | $clone = clone $this; |
|
1423 | 4 | $clone->fragment = $fragment; |
|
1424 | 4 | $clone->assertValidState(); |
|
1425 | |||
1426 | 4 | return $clone; |
|
1427 | } |
||
1428 | } |
||
1429 |