PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.1.0
Defender Security – Malware Scanner, Login Security & Firewall v6.1.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / vendor / league / uri / src / UriString.php

UriString.php in Defender Security – Malware Scanner, Login Security & Firewall 6.1.0, at vendor/league/uri/src/UriString.php

468 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 League\Uri\Exceptions\IdnaConversionFailed;
17 use League\Uri\Exceptions\IdnSupportMissing;
18 use League\Uri\Exceptions\SyntaxError;
19 use League\Uri\Idna\Idna;
20 use TypeError;
21 use function array_merge;
22 use function explode;
23 use function filter_var;
24 use function gettype;
25 use function inet_pton;
26 use function is_object;
27 use function is_scalar;
28 use function method_exists;
29 use function preg_match;
30 use function rawurldecode;
31 use function sprintf;
32 use function strpos;
33 use function substr;
34 use const FILTER_FLAG_IPV6;
35 use const FILTER_VALIDATE_IP;
36
37 /**
38 * A class to parse a URI string according to RFC3986.
39 *
40 * @link https://tools.ietf.org/html/rfc3986
41 * @package League\Uri
42 * @author Ignace Nyamagana Butera <[email protected]>
43 * @since 6.0.0
44 */
45 final class UriString
46 {
47 /**
48 * Default URI component values.
49 */
50 private const URI_COMPONENTS = [
51 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
52 'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
53 ];
54
55 /**
56 * Simple URI which do not need any parsing.
57 */
58 private const URI_SCHORTCUTS = [
59 '' => [],
60 '#' => ['fragment' => ''],
61 '?' => ['query' => ''],
62 '?#' => ['query' => '', 'fragment' => ''],
63 '/' => ['path' => '/'],
64 '//' => ['host' => ''],
65 ];
66
67 /**
68 * Range of invalid characters in URI string.
69 */
70 private const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
71
72 /**
73 * RFC3986 regular expression URI splitter.
74 *
75 * @link https://tools.ietf.org/html/rfc3986#appendix-B
76 */
77 private const REGEXP_URI_PARTS = ',^
78 (?<scheme>(?<scontent>[^:/?\#]+):)? # URI scheme component
79 (?<authority>//(?<acontent>[^/?\#]*))? # URI authority part
80 (?<path>[^?\#]*) # URI path component
81 (?<query>\?(?<qcontent>[^\#]*))? # URI query component
82 (?<fragment>\#(?<fcontent>.*))? # URI fragment component
83 ,x';
84
85 /**
86 * URI scheme regular expresssion.
87 *
88 * @link https://tools.ietf.org/html/rfc3986#section-3.1
89 */
90 private const REGEXP_URI_SCHEME = '/^([a-z][a-z\d\+\.\-]*)?$/i';
91
92 /**
93 * IPvFuture regular expression.
94 *
95 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
96 */
97 private const REGEXP_IP_FUTURE = '/^
98 v(?<version>[A-F0-9])+\.
99 (?:
100 (?<unreserved>[a-z0-9_~\-\.])|
101 (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
102 )+
103 $/ix';
104
105 /**
106 * General registered name regular expression.
107 *
108 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
109 */
110 private const REGEXP_REGISTERED_NAME = '/(?(DEFINE)
111 (?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
112 (?<sub_delims>[!$&\'()*+,;=])
113 (?<encoded>%[A-F0-9]{2})
114 (?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
115 )
116 ^(?:(?&reg_name)\.)*(?&reg_name)\.?$/ix';
117
118 /**
119 * Invalid characters in host regular expression.
120 *
121 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
122 */
123 private const REGEXP_INVALID_HOST_CHARS = '/
124 [:\/?#\[\]@ ] # gen-delims characters as well as the space character
125 /ix';
126
127 /**
128 * Invalid path for URI without scheme and authority regular expression.
129 *
130 * @link https://tools.ietf.org/html/rfc3986#section-3.3
131 */
132 private const REGEXP_INVALID_PATH = ',^(([^/]*):)(.*)?/,';
133
134 /**
135 * Host and Port splitter regular expression.
136 */
137 private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,';
138
139 /**
140 * IDN Host detector regular expression.
141 */
142 private const REGEXP_IDN_PATTERN = '/[^\x20-\x7f]/';
143
144 /**
145 * Only the address block fe80::/10 can have a Zone ID attach to
146 * let's detect the link local significant 10 bits.
147 */
148 private const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80";
149
150 /**
151 * Generate an URI string representation from its parsed representation
152 * returned by League\UriString::parse() or PHP's parse_url.
153 *
154 * If you supply your own array, you are responsible for providing
155 * valid components without their URI delimiters.
156 *
157 * @link https://tools.ietf.org/html/rfc3986#section-5.3
158 * @link https://tools.ietf.org/html/rfc3986#section-7.5
159 *
160 * @param array{
161 * scheme:?string,
162 * user:?string,
163 * pass:?string,
164 * host:?string,
165 * port:?int,
166 * path:?string,
167 * query:?string,
168 * fragment:?string
169 * } $components
170 */
171 public static function build(array $components): string
172 {
173 $result = $components['path'] ?? '';
174 if (isset($components['query'])) {
175 $result .= '?'.$components['query'];
176 }
177
178 if (isset($components['fragment'])) {
179 $result .= '#'.$components['fragment'];
180 }
181
182 $scheme = null;
183 if (isset($components['scheme'])) {
184 $scheme = $components['scheme'].':';
185 }
186
187 if (!isset($components['host'])) {
188 return $scheme.$result;
189 }
190
191 $scheme .= '//';
192 $authority = $components['host'];
193 if (isset($components['port'])) {
194 $authority .= ':'.$components['port'];
195 }
196
197 if (!isset($components['user'])) {
198 return $scheme.$authority.$result;
199 }
200
201 $authority = '@'.$authority;
202 if (!isset($components['pass'])) {
203 return $scheme.$components['user'].$authority.$result;
204 }
205
206 return $scheme.$components['user'].':'.$components['pass'].$authority.$result;
207 }
208
209 /**
210 * Parse an URI string into its components.
211 *
212 * This method parses a URI and returns an associative array containing any
213 * of the various components of the URI that are present.
214 *
215 * <code>
216 * $components = (new Parser())->parse('http://[email protected]:42?query#');
217 * var_export($components);
218 * //will display
219 * array(
220 * 'scheme' => 'http', // the URI scheme component
221 * 'user' => 'foo', // the URI user component
222 * 'pass' => null, // the URI pass component
223 * 'host' => 'test.example.com', // the URI host component
224 * 'port' => 42, // the URI port component
225 * 'path' => '', // the URI path component
226 * 'query' => 'query', // the URI query component
227 * 'fragment' => '', // the URI fragment component
228 * );
229 * </code>
230 *
231 * The returned array is similar to PHP's parse_url return value with the following
232 * differences:
233 *
234 * <ul>
235 * <li>All components are always present in the returned array</li>
236 * <li>Empty and undefined component are treated differently. And empty component is
237 * set to the empty string while an undefined component is set to the `null` value.</li>
238 * <li>The path component is never undefined</li>
239 * <li>The method parses the URI following the RFC3986 rules but you are still
240 * required to validate the returned components against its related scheme specific rules.</li>
241 * </ul>
242 *
243 * @link https://tools.ietf.org/html/rfc3986
244 *
245 * @param mixed $uri any scalar or stringable object
246 *
247 * @throws SyntaxError if the URI contains invalid characters
248 * @throws SyntaxError if the URI contains an invalid scheme
249 * @throws SyntaxError if the URI contains an invalid path
250 *
251 * @return array{
252 * scheme:?string,
253 * user:?string,
254 * pass:?string,
255 * host:?string,
256 * port:?int,
257 * path:string,
258 * query:?string,
259 * fragment:?string
260 * }
261 */
262 public static function parse($uri): array
263 {
264 if (is_object($uri) && method_exists($uri, '__toString')) {
265 $uri = (string) $uri;
266 }
267
268 if (!is_scalar($uri)) {
269 throw new TypeError(sprintf('The uri must be a scalar or a stringable object `%s` given', gettype($uri)));
270 }
271
272 $uri = (string) $uri;
273
274 if (isset(self::URI_SCHORTCUTS[$uri])) {
275 /** @var array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} $components */
276 $components = array_merge(self::URI_COMPONENTS, self::URI_SCHORTCUTS[$uri]);
277
278 return $components;
279 }
280
281 if (1 === preg_match(self::REGEXP_INVALID_URI_CHARS, $uri)) {
282 throw new SyntaxError(sprintf('The uri `%s` contains invalid characters', $uri));
283 }
284
285 //if the first character is a known URI delimiter parsing can be simplified
286 $first_char = $uri[0];
287
288 //The URI is made of the fragment only
289 if ('#' === $first_char) {
290 [, $fragment] = explode('#', $uri, 2);
291 $components = self::URI_COMPONENTS;
292 $components['fragment'] = $fragment;
293
294 return $components;
295 }
296
297 //The URI is made of the query and fragment
298 if ('?' === $first_char) {
299 [, $partial] = explode('?', $uri, 2);
300 [$query, $fragment] = explode('#', $partial, 2) + [1 => null];
301 $components = self::URI_COMPONENTS;
302 $components['query'] = $query;
303 $components['fragment'] = $fragment;
304
305 return $components;
306 }
307
308 //use RFC3986 URI regexp to split the URI
309 preg_match(self::REGEXP_URI_PARTS, $uri, $parts);
310 $parts += ['query' => '', 'fragment' => ''];
311
312 if (':' === $parts['scheme'] || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'])) {
313 throw new SyntaxError(sprintf('The uri `%s` contains an invalid scheme', $uri));
314 }
315
316 if ('' === $parts['scheme'].$parts['authority'] && 1 === preg_match(self::REGEXP_INVALID_PATH, $parts['path'])) {
317 throw new SyntaxError(sprintf('The uri `%s` contains an invalid path.', $uri));
318 }
319
320 /** @var array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} $components */
321 $components = array_merge(
322 self::URI_COMPONENTS,
323 '' === $parts['authority'] ? [] : self::parseAuthority($parts['acontent']),
324 [
325 'path' => $parts['path'],
326 'scheme' => '' === $parts['scheme'] ? null : $parts['scontent'],
327 'query' => '' === $parts['query'] ? null : $parts['qcontent'],
328 'fragment' => '' === $parts['fragment'] ? null : $parts['fcontent'],
329 ]
330 );
331
332 return $components;
333 }
334
335 /**
336 * Parses the URI authority part.
337 *
338 * @link https://tools.ietf.org/html/rfc3986#section-3.2
339 *
340 * @throws SyntaxError If the port component is invalid
341 *
342 * @return array{user:?string, pass:?string, host:?string, port:?int}
343 */
344 private static function parseAuthority(string $authority): array
345 {
346 $components = ['user' => null, 'pass' => null, 'host' => '', 'port' => null];
347 if ('' === $authority) {
348 return $components;
349 }
350
351 $parts = explode('@', $authority, 2);
352 if (isset($parts[1])) {
353 [$components['user'], $components['pass']] = explode(':', $parts[0], 2) + [1 => null];
354 }
355
356 preg_match(self::REGEXP_HOST_PORT, $parts[1] ?? $parts[0], $matches);
357 $matches += ['port' => ''];
358
359 $components['port'] = self::filterPort($matches['port']);
360 $components['host'] = self::filterHost($matches['host']);
361
362 return $components;
363 }
364
365 /**
366 * Filter and format the port component.
367 *
368 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
369 *
370 * @throws SyntaxError if the registered name is invalid
371 */
372 private static function filterPort(string $port): ?int
373 {
374 if ('' === $port) {
375 return null;
376 }
377
378 if (1 === preg_match('/^\d*$/', $port)) {
379 return (int) $port;
380 }
381
382 throw new SyntaxError(sprintf('The port `%s` is invalid', $port));
383 }
384
385 /**
386 * Returns whether a hostname is valid.
387 *
388 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
389 *
390 * @throws SyntaxError if the registered name is invalid
391 */
392 private static function filterHost(string $host): string
393 {
394 if ('' === $host) {
395 return $host;
396 }
397
398 if ('[' !== $host[0] || ']' !== substr($host, -1)) {
399 return self::filterRegisteredName($host);
400 }
401
402 if (!self::isIpHost(substr($host, 1, -1))) {
403 throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
404 }
405
406 return $host;
407 }
408
409 /**
410 * Returns whether the host is an IPv4 or a registered named.
411 *
412 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
413 *
414 * @throws SyntaxError if the registered name is invalid
415 * @throws IdnSupportMissing if IDN support or ICU requirement are not available or met.
416 */
417 private static function filterRegisteredName(string $host): string
418 {
419 $formatted_host = rawurldecode($host);
420 if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formatted_host)) {
421 return $host;
422 }
423
424 //to test IDN host non-ascii characters must be present in the host
425 if (1 !== preg_match(self::REGEXP_IDN_PATTERN, $formatted_host)) {
426 throw new SyntaxError(sprintf('Host `%s` is invalid : the host is not a valid registered name', $host));
427 }
428
429 $info = Idna::toAscii($host, Idna::IDNA2008_ASCII);
430 if (0 !== $info->errors()) {
431 throw IdnaConversionFailed::dueToIDNAError($host, $info);
432 }
433
434 return $host;
435 }
436
437 /**
438 * Validates a IPv6/IPvfuture host.
439 *
440 * @link https://tools.ietf.org/html/rfc3986#section-3.2.2
441 * @link https://tools.ietf.org/html/rfc6874#section-2
442 * @link https://tools.ietf.org/html/rfc6874#section-4
443 */
444 private static function isIpHost(string $ip_host): bool
445 {
446 if (false !== filter_var($ip_host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
447 return true;
448 }
449
450 if (1 === preg_match(self::REGEXP_IP_FUTURE, $ip_host, $matches)) {
451 return !in_array($matches['version'], ['4', '6'], true);
452 }
453
454 $pos = strpos($ip_host, '%');
455 if (false === $pos || 1 === preg_match(
456 self::REGEXP_INVALID_HOST_CHARS,
457 rawurldecode(substr($ip_host, $pos))
458 )) {
459 return false;
460 }
461
462 $ip_host = substr($ip_host, 0, $pos);
463
464 return false !== filter_var($ip_host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
465 && 0 === strpos((string) inet_pton($ip_host), self::ZONE_ID_ADDRESS_BLOCK);
466 }
467 }
468