PluginProbe
Gmail SMTP / trunk
Gmail SMTP vtrunk
1.2.3.21 1.2.3.20 trunk 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.9 1.2.0 1.2.3.14 1.2.3.15 1.2.3.16 1.2.3.18 1.2.3.5
gmail-smtp / google-api-php-client / vendor / guzzlehttp / guzzle / src / Utils.php

Utils.php in Gmail SMTP trunk, at google-api-php-client/vendor/guzzlehttp/guzzle/src/Utils.php

834 lines 26.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace GuzzleHttp;
4
5 use GuzzleHttp\Exception\InvalidArgumentException;
6 use GuzzleHttp\Handler\CurlHandler;
7 use GuzzleHttp\Handler\CurlMultiHandler;
8 use GuzzleHttp\Handler\CurlShareHandleState;
9 use GuzzleHttp\Handler\CurlVersion;
10 use GuzzleHttp\Handler\Proxy;
11 use GuzzleHttp\Handler\StreamHandler;
12 use Psr\Http\Message\RequestInterface;
13 use Psr\Http\Message\UriInterface;
14
15 final class Utils
16 {
17 /**
18 * Debug function used to describe the provided value type and class.
19 *
20 * @param mixed $input
21 *
22 * @return string Returns a string containing the type of the variable and
23 * if a class is provided, the class name.
24 *
25 * @deprecated Utils::describeType() will be removed in guzzlehttp/guzzle:8.0. Use get_debug_type() instead.
26 */
27 public static function describeType($input): string
28 {
29 \trigger_deprecation(
30 'guzzlehttp/guzzle',
31 '7.12',
32 '%s() is deprecated and will be removed in 8.0. Use get_debug_type() instead.',
33 __METHOD__
34 );
35
36 switch (\gettype($input)) {
37 case 'object':
38 return 'object('.\get_class($input).')';
39 case 'array':
40 return 'array('.\count($input).')';
41 default:
42 \ob_start();
43 \var_dump($input);
44 // normalize float vs double
45 /** @var string $varDumpContent */
46 $varDumpContent = \ob_get_clean();
47
48 return \str_replace('double(', 'float(', \rtrim($varDumpContent));
49 }
50 }
51
52 /**
53 * Parses an array of header lines into an associative array of headers.
54 *
55 * @param iterable $lines Header lines array of strings in the following
56 * format: "Name: Value"
57 */
58 public static function headersFromLines(iterable $lines): array
59 {
60 $headers = [];
61
62 foreach ($lines as $line) {
63 $parts = \explode(':', $line, 2);
64 $headers[\trim($parts[0])][] = isset($parts[1]) ? \trim($parts[1]) : null;
65 }
66
67 return $headers;
68 }
69
70 /**
71 * Returns a debug stream based on the provided variable.
72 *
73 * @param mixed $value Optional value
74 *
75 * @return resource
76 */
77 public static function debugResource($value = null)
78 {
79 if (\is_resource($value)) {
80 return $value;
81 }
82 if (\defined('STDOUT')) {
83 return \STDOUT;
84 }
85
86 return Psr7\Utils::tryFopen('php://output', 'w');
87 }
88
89 /**
90 * Chooses and creates a default handler to use based on the environment.
91 *
92 * The returned handler is not wrapped by any default middlewares.
93 *
94 * @param array{transport_sharing?: mixed} $handlerOptions Handler constructor options.
95 *
96 * @return callable(RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
97 *
98 * @throws \RuntimeException if no viable Handler is available.
99 */
100 public static function chooseHandler(array $handlerOptions = []): callable
101 {
102 $handler = null;
103 $sharingMode = CurlShareHandleState::normalizeMode($handlerOptions['transport_sharing'] ?? null, 'transport_sharing');
104 $sharingRequested = $sharingMode !== TransportSharing::NONE;
105 $sharingRequired = $sharingMode === TransportSharing::HANDLER_REQUIRE;
106 $curlHandlerOptions = [];
107 $curlSupported = \defined('CURLOPT_CUSTOMREQUEST')
108 && CurlVersion::supportsCurlHandler()
109 && (\function_exists('curl_multi_exec') || \function_exists('curl_exec'));
110
111 if ($sharingRequired && !$curlSupported) {
112 throw new \RuntimeException('Required transport sharing requires the PHP cURL extension, curl_exec() or curl_multi_exec(), and libcurl 7.21.2 or higher.');
113 }
114
115 if ($curlSupported) {
116 if ($sharingRequested) {
117 $shareState = CurlShareHandleState::fromOption($sharingMode);
118 if ($shareState !== null) {
119 $curlHandlerOptions['transport_sharing'] = $shareState;
120 }
121 }
122
123 if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
124 $handler = Proxy::wrapSync(new CurlMultiHandler($curlHandlerOptions), new CurlHandler($curlHandlerOptions));
125 } elseif (\function_exists('curl_exec')) {
126 $handler = new CurlHandler($curlHandlerOptions);
127 } elseif (\function_exists('curl_multi_exec')) {
128 $handler = new CurlMultiHandler($curlHandlerOptions);
129 }
130 }
131
132 if (\ini_get('allow_url_fopen')) {
133 $streamHandler = new StreamHandler(['transport_sharing' => $sharingMode]);
134
135 $handler = $handler
136 ? Proxy::wrapStreaming($handler, $streamHandler)
137 : $streamHandler;
138 } elseif (!$handler) {
139 throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
140 }
141
142 return $handler;
143 }
144
145 /**
146 * Get the default User-Agent string to use with Guzzle.
147 */
148 public static function defaultUserAgent(): string
149 {
150 return sprintf('GuzzleHttp/%d', ClientInterface::MAJOR_VERSION);
151 }
152
153 /**
154 * Returns the default cacert bundle for the current system.
155 *
156 * First, the openssl.cafile and curl.cainfo php.ini settings are checked.
157 * If those settings are not configured, then the common locations for
158 * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X
159 * and Windows are checked. If any of these file locations are found on
160 * disk, they will be utilized.
161 *
162 * Note: the result of this function is cached for subsequent calls.
163 *
164 * @throws \RuntimeException if no bundle can be found.
165 *
166 * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+.
167 */
168 public static function defaultCaBundle(): string
169 {
170 static $cached = null;
171 static $cafiles = [
172 // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
173 '/etc/pki/tls/certs/ca-bundle.crt',
174 // Ubuntu, Debian (provided by the ca-certificates package)
175 '/etc/ssl/certs/ca-certificates.crt',
176 // FreeBSD (provided by the ca_root_nss package)
177 '/usr/local/share/certs/ca-root-nss.crt',
178 // SLES 12 (provided by the ca-certificates package)
179 '/var/lib/ca-certificates/ca-bundle.pem',
180 // OS X provided by homebrew (using the default path)
181 '/usr/local/etc/openssl/cert.pem',
182 // Google app engine
183 '/etc/ca-certificates.crt',
184 // Windows?
185 'C:\\windows\\system32\\curl-ca-bundle.crt',
186 'C:\\windows\\curl-ca-bundle.crt',
187 ];
188
189 if ($cached) {
190 return $cached;
191 }
192
193 if ($ca = \ini_get('openssl.cafile')) {
194 return $cached = $ca;
195 }
196
197 if ($ca = \ini_get('curl.cainfo')) {
198 return $cached = $ca;
199 }
200
201 foreach ($cafiles as $filename) {
202 if (\file_exists($filename)) {
203 return $cached = $filename;
204 }
205 }
206
207 throw new \RuntimeException(
208 <<< EOT
209 No system CA bundle could be found in any of the the common system locations.
210 PHP versions earlier than 5.6 are not properly configured to use the system's
211 CA bundle by default. In order to verify peer certificates, you will need to
212 supply the path on disk to a certificate bundle to the 'verify' request option:
213 https://github.com/guzzle/guzzle/blob/7.12/docs/request-options.md#verify. If
214 you do not need a specific certificate bundle, then Mozilla provides a commonly
215 used CA bundle which can be downloaded here (provided by the maintainer of
216 cURL): https://curl.se/ca/cacert.pem. Once you have a CA bundle available on
217 disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to
218 the file, allowing you to omit the 'verify' request option. See
219 https://curl.se/docs/sslcerts.html for more information.
220 EOT
221 );
222 }
223
224 /**
225 * Creates an associative array of lowercase header names to the actual
226 * header casing.
227 */
228 public static function normalizeHeaderKeys(array $headers): array
229 {
230 $result = [];
231 foreach (\array_keys($headers) as $key) {
232 $result[\strtolower((string) $key)] = $key;
233 }
234
235 return $result;
236 }
237
238 /**
239 * @param mixed $protocols
240 *
241 * @return string[]
242 *
243 * @throws InvalidArgumentException
244 */
245 public static function normalizeProtocols($protocols): array
246 {
247 if (!\is_array($protocols) || $protocols === []) {
248 throw new InvalidArgumentException('protocols must be a non-empty array of "http" and/or "https"');
249 }
250
251 $normalized = [];
252
253 foreach ($protocols as $protocol) {
254 if (!\is_string($protocol)) {
255 throw new InvalidArgumentException('protocols must contain only strings');
256 }
257
258 if ($protocol !== 'http' && $protocol !== 'https') {
259 throw new InvalidArgumentException('protocols may only contain "http" and "https"');
260 }
261
262 $normalized[$protocol] = true;
263 }
264
265 return \array_keys($normalized);
266 }
267
268 /**
269 * Returns true if the provided host matches any of the no proxy areas.
270 *
271 * This method will strip a port from the host if it is present. Domain
272 * patterns are matched case-insensitively. Exact IP literal patterns are
273 * matched by their normalized binary address.
274 *
275 * Areas are matched in the following cases:
276 * 1. "*" (without quotes) always matches any hosts.
277 * 2. An exact domain or IP literal match.
278 * 3. A bare domain matches itself and its subdomains. e.g. 'mit.edu' will
279 * match 'mit.edu' and 'foo.mit.edu'.
280 * 4. The area starts with "." and the area is the last part of the host. e.g.
281 * '.mit.edu' will match any host that ends with '.mit.edu'.
282 * 5. IP CIDR entries match IP literal hosts. e.g. '192.168.0.0/16' will
283 * match '192.168.1.10' and 'fd00::/8' will match '[fd00::1]'.
284 *
285 * @param string $host Host to check against the patterns.
286 * @param string[] $noProxyArray An array of host or CIDR patterns.
287 *
288 * @throws InvalidArgumentException
289 */
290 public static function isHostInNoProxy(string $host, array $noProxyArray): bool
291 {
292 if (\strlen($host) === 0) {
293 throw new InvalidArgumentException('Empty host provided');
294 }
295
296 $target = self::parseNoProxyHostString($host);
297 if ($target === null) {
298 return false;
299 }
300
301 return self::matchesNoProxyList($target, $noProxyArray);
302 }
303
304 /**
305 * Returns true if the provided URI matches any of the no proxy areas.
306 *
307 * Matching follows the same rules as isHostInNoProxy(), with the
308 * addition that areas may carry a port (e.g. "example.com:8080" or
309 * "[::1]:8080") which is compared against the URI port (or the scheme
310 * default port when the URI has none).
311 *
312 * @param mixed $noProxy No-proxy host, host-and-port, or CIDR patterns.
313 *
314 * @internal
315 */
316 public static function isUriInNoProxy(UriInterface $uri, $noProxy): bool
317 {
318 if (\is_string($noProxy)) {
319 $noProxy = \explode(',', $noProxy);
320 }
321
322 if (!\is_array($noProxy)) {
323 return false;
324 }
325
326 $target = self::parseNoProxyTarget($uri);
327 if ($target === null) {
328 return false;
329 }
330
331 return self::matchesNoProxyList($target, $noProxy);
332 }
333
334 /**
335 * @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
336 * @param array<array-key, mixed> $noProxy
337 */
338 private static function matchesNoProxyList(array $target, array $noProxy): bool
339 {
340 foreach ($noProxy as $area) {
341 if (!\is_string($area)) {
342 continue;
343 }
344
345 $area = \trim($area);
346
347 // Always match on wildcards.
348 if ($area === '*') {
349 return true;
350 }
351
352 $rule = self::parseNoProxyRule($area);
353 if ($rule !== null && self::noProxyRuleMatches($target, $rule)) {
354 return true;
355 }
356 }
357
358 return false;
359 }
360
361 /**
362 * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
363 */
364 private static function parseNoProxyTarget(UriInterface $uri): ?array
365 {
366 $host = $uri->getHost();
367 if ($host === '') {
368 return null;
369 }
370
371 return self::parseNoProxyHost($host, $uri->getPort() ?? self::getDefaultPort($uri->getScheme()), true);
372 }
373
374 /**
375 * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
376 */
377 private static function parseNoProxyHostString(string $host): ?array
378 {
379 $hostAndPort = self::splitNoProxyHostAndPort($host);
380 if ($hostAndPort === null) {
381 return null;
382 }
383
384 [$host] = $hostAndPort;
385
386 return self::parseNoProxyHost($host, null, true);
387 }
388
389 /**
390 * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|array{type: string, value: string, prefix: int}|null
391 */
392 private static function parseNoProxyRule(string $area): ?array
393 {
394 $area = \trim($area);
395 if ($area === '' || $area === '*') {
396 return null;
397 }
398
399 if (\strpos($area, '/') !== false) {
400 return self::parseNoProxyCidrRule($area);
401 }
402
403 $matchesRoot = true;
404 if ($area[0] === '.') {
405 $matchesRoot = false;
406 $area = \substr($area, 1);
407 }
408
409 $hostAndPort = self::splitNoProxyHostAndPort($area);
410 if ($hostAndPort === null) {
411 return null;
412 }
413
414 [$host, $port] = $hostAndPort;
415
416 if ($host === '*') {
417 if (!$matchesRoot) {
418 return null;
419 }
420
421 return [
422 'type' => 'wildcard',
423 'value' => '*',
424 'port' => $port,
425 'matchesRoot' => true,
426 ];
427 }
428
429 $rule = self::parseNoProxyHost($host, $port, $matchesRoot);
430 if ($rule !== null && !$matchesRoot && $rule['type'] === 'ip') {
431 return null;
432 }
433
434 return $rule;
435 }
436
437 /**
438 * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
439 */
440 private static function parseNoProxyHost(string $host, ?int $port, bool $matchesRoot): ?array
441 {
442 if ($host !== '' && $host[0] === '[') {
443 if (\substr($host, -1) !== ']') {
444 return null;
445 }
446
447 $address = \substr($host, 1, -1);
448 if (!\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
449 return null;
450 }
451
452 $host = $address;
453 }
454
455 $packedIp = self::packIpAddress($host);
456 if ($packedIp !== false) {
457 return [
458 'type' => 'ip',
459 'value' => $packedIp,
460 'port' => $port,
461 'matchesRoot' => $matchesRoot,
462 ];
463 }
464
465 if ($host === '' || \strpos($host, ':') !== false) {
466 return null;
467 }
468
469 // Normalize a single DNS root dot for no-proxy domain matching.
470 if (\substr($host, -1) === '.') {
471 $host = \substr($host, 0, -1);
472 if ($host === '') {
473 return null;
474 }
475 }
476
477 return [
478 'type' => 'domain',
479 'value' => \strtolower($host),
480 'port' => $port,
481 'matchesRoot' => $matchesRoot,
482 ];
483 }
484
485 /**
486 * @return array{0: string, 1: int|null}|null
487 */
488 private static function splitNoProxyHostAndPort(string $area): ?array
489 {
490 if ($area !== '' && $area[0] === '[') {
491 $closingBracket = \strpos($area, ']');
492 if ($closingBracket === false) {
493 return null;
494 }
495
496 $host = \substr($area, 0, $closingBracket + 1);
497 $tail = \substr($area, $closingBracket + 1);
498 if ($tail === '') {
499 return [$host, null];
500 }
501
502 if ($tail[0] !== ':') {
503 return null;
504 }
505
506 $port = self::parseNoProxyPort(\substr($tail, 1));
507
508 return $port === null ? null : [$host, $port];
509 }
510
511 if (self::packIpAddress($area) !== false) {
512 return [$area, null];
513 }
514
515 $colon = \strrpos($area, ':');
516 if ($colon === false) {
517 return [$area, null];
518 }
519
520 $port = self::parseNoProxyPort(\substr($area, $colon + 1));
521 if ($port === null) {
522 return null;
523 }
524
525 return [\substr($area, 0, $colon), $port];
526 }
527
528 private static function parseNoProxyPort(string $port): ?int
529 {
530 return self::parseBoundedUnsignedInteger($port, 65535);
531 }
532
533 /**
534 * @return array{type: string, value: string, prefix: int}|null
535 */
536 private static function parseNoProxyCidrRule(string $area): ?array
537 {
538 $slash = \strpos($area, '/');
539 if ($slash === false) {
540 return null;
541 }
542
543 $prefix = \substr($area, $slash + 1);
544
545 $network = \substr($area, 0, $slash);
546 if ($network !== '' && $network[0] === '[' && \substr($network, -1) === ']') {
547 $network = \substr($network, 1, -1);
548 }
549
550 $network = self::packIpAddress($network);
551 if ($network === false) {
552 return null;
553 }
554
555 $prefix = self::parseBoundedUnsignedInteger($prefix, \strlen($network) * 8);
556 if ($prefix === null) {
557 return null;
558 }
559
560 return [
561 'type' => 'cidr',
562 'value' => $network,
563 'prefix' => $prefix,
564 ];
565 }
566
567 private static function parseBoundedUnsignedInteger(string $value, int $max): ?int
568 {
569 if ($value === '' || !\ctype_digit($value)) {
570 return null;
571 }
572
573 $normalized = \ltrim($value, '0');
574 $normalized = $normalized === '' ? '0' : $normalized;
575 $limit = (string) $max;
576
577 if (\strlen($normalized) > \strlen($limit) || (\strlen($normalized) === \strlen($limit) && \strcmp($normalized, $limit) > 0)) {
578 return null;
579 }
580
581 return (int) $normalized;
582 }
583
584 /**
585 * @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
586 * @param array{type: string, value: string, port?: int|null, matchesRoot?: bool, prefix?: int|null} $rule
587 */
588 private static function noProxyRuleMatches(array $target, array $rule): bool
589 {
590 if ($rule['type'] === 'wildcard') {
591 return ($rule['port'] ?? null) === null || $rule['port'] === $target['port'];
592 }
593
594 if ($rule['type'] === 'cidr') {
595 if ($target['type'] !== 'ip' || !isset($rule['prefix'])) {
596 return false;
597 }
598
599 if (\strlen($target['value']) !== \strlen($rule['value'])) {
600 return false;
601 }
602
603 return self::ipMatchesPrefix($target['value'], $rule['value'], $rule['prefix']);
604 }
605
606 if (($rule['port'] ?? null) !== null && $rule['port'] !== $target['port']) {
607 return false;
608 }
609
610 if ($rule['type'] !== $target['type']) {
611 return false;
612 }
613
614 if ($rule['type'] === 'ip') {
615 return $rule['value'] === $target['value'];
616 }
617
618 if (($rule['matchesRoot'] ?? false) && $target['value'] === $rule['value']) {
619 return true;
620 }
621
622 $suffix = '.'.$rule['value'];
623
624 return \substr($target['value'], -\strlen($suffix)) === $suffix;
625 }
626
627 /**
628 * @return string|false
629 */
630 private static function packIpAddress(string $ip)
631 {
632 if (!\filter_var($ip, \FILTER_VALIDATE_IP)) {
633 return false;
634 }
635
636 return \inet_pton($ip);
637 }
638
639 private static function ipMatchesPrefix(string $address, string $network, int $prefix): bool
640 {
641 $fullBytes = \intdiv($prefix, 8);
642 $remainingBits = $prefix % 8;
643
644 if ($fullBytes > 0 && \substr($address, 0, $fullBytes) !== \substr($network, 0, $fullBytes)) {
645 return false;
646 }
647
648 if ($remainingBits === 0) {
649 return true;
650 }
651
652 $mask = (0xFF << (8 - $remainingBits)) & 0xFF;
653
654 return (\ord($address[$fullBytes]) & $mask) === (\ord($network[$fullBytes]) & $mask);
655 }
656
657 private static function getDefaultPort(string $scheme): ?int
658 {
659 if ($scheme === 'http') {
660 return 80;
661 }
662
663 if ($scheme === 'https') {
664 return 443;
665 }
666
667 return null;
668 }
669
670 /**
671 * Wrapper for json_decode that throws when an error occurs.
672 *
673 * @param string $json JSON data to parse
674 * @param bool $assoc When true, returned objects will be converted
675 * into associative arrays.
676 * @param int $depth User specified recursion depth.
677 * @param int $options Bitmask of JSON decode options.
678 *
679 * @return object|array|string|int|float|bool|null
680 *
681 * @throws InvalidArgumentException if the JSON cannot be decoded.
682 *
683 * @see https://www.php.net/manual/en/function.json-decode.php
684 */
685 public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
686 {
687 if ($depth < 1) {
688 throw new InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
689 }
690
691 $data = \json_decode($json, $assoc, $depth, $options);
692 if (\JSON_ERROR_NONE !== \json_last_error()) {
693 throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg());
694 }
695
696 return $data;
697 }
698
699 /**
700 * Wrapper for JSON encoding that throws when an error occurs.
701 *
702 * @param mixed $value The value being encoded
703 * @param int $options JSON encode option bitmask
704 * @param int $depth Set the maximum depth. Must be greater than zero.
705 *
706 * @throws InvalidArgumentException if the JSON cannot be encoded.
707 *
708 * @see https://www.php.net/manual/en/function.json-encode.php
709 */
710 public static function jsonEncode($value, int $options = 0, int $depth = 512): string
711 {
712 $json = \json_encode($value, $options, $depth);
713 if (\JSON_ERROR_NONE !== \json_last_error()) {
714 throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
715 }
716
717 /** @var string */
718 return $json;
719 }
720
721 /**
722 * Wrapper for the hrtime() or microtime() functions
723 * (depending on the PHP version, one of the two is used)
724 *
725 * @return float UNIX timestamp
726 *
727 * @internal
728 */
729 public static function currentTime(): float
730 {
731 return (float) \function_exists('hrtime') ? \hrtime(true) / 1e9 : \microtime(true);
732 }
733
734 /**
735 * @param mixed $value
736 *
737 * @internal
738 */
739 public static function normalizeIdnConversionOption($value): ?int
740 {
741 if ($value === null || $value === false) {
742 return null;
743 }
744
745 if ($value === true) {
746 return \IDNA_DEFAULT;
747 }
748
749 if (\is_int($value)) {
750 return $value;
751 }
752
753 if ((\is_string($value) && \is_numeric($value)) || (\is_float($value) && \is_finite($value))) {
754 \trigger_deprecation(
755 'guzzlehttp/guzzle',
756 '7.11',
757 'Passing %s as the "idn_conversion" request option is deprecated; guzzlehttp/guzzle 8.0 will reject values that are not true, false, null, or an integer IDNA_* bitmask.',
758 \get_debug_type($value)
759 );
760
761 return (int) $value;
762 }
763
764 throw new InvalidArgumentException('idn_conversion must be true, false, null, or an integer IDNA_* bitmask');
765 }
766
767 /**
768 * @throws InvalidArgumentException
769 *
770 * @internal
771 */
772 public static function idnUriConvert(UriInterface $uri, int $options = 0): UriInterface
773 {
774 if ($uri->getHost()) {
775 $asciiHost = self::idnToAsci($uri->getHost(), $options, $info);
776 if ($asciiHost === false) {
777 $errorBitSet = $info['errors'] ?? 0;
778
779 $errorConstants = array_filter(array_keys(get_defined_constants()), static function (string $name): bool {
780 return substr($name, 0, 11) === 'IDNA_ERROR_';
781 });
782
783 $errors = [];
784 foreach ($errorConstants as $errorConstant) {
785 if ($errorBitSet & constant($errorConstant)) {
786 $errors[] = $errorConstant;
787 }
788 }
789
790 $errorMessage = 'IDN conversion failed';
791 if ($errors) {
792 $errorMessage .= ' (errors: '.implode(', ', $errors).')';
793 }
794
795 throw new InvalidArgumentException($errorMessage);
796 }
797 if ($uri->getHost() !== $asciiHost) {
798 // Replace URI only if the ASCII version is different
799 $uri = $uri->withHost($asciiHost);
800 }
801 }
802
803 return $uri;
804 }
805
806 /**
807 * @internal
808 */
809 public static function getenv(string $name): ?string
810 {
811 if (isset($_SERVER[$name])) {
812 return (string) $_SERVER[$name];
813 }
814
815 if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== false && $value !== null) {
816 return (string) $value;
817 }
818
819 return null;
820 }
821
822 /**
823 * @return string|false
824 */
825 private static function idnToAsci(string $domain, int $options, ?array &$info = [])
826 {
827 if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) {
828 return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info);
829 }
830
831 throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old');
832 }
833 }
834