1<?php
2
3namespace GuzzleHttp;
4
5use GuzzleHttp\Exception\InvalidArgumentException;
6use GuzzleHttp\Handler\CurlHandler;
7use GuzzleHttp\Handler\CurlMultiHandler;
8use GuzzleHttp\Handler\Proxy;
9use GuzzleHttp\Handler\StreamHandler;
10use Psr\Http\Message\UriInterface;
11
12final class Utils
13{
14    /**
15     * Debug function used to describe the provided value type and class.
16     *
17     * @param mixed $input
18     *
19     * @return string Returns a string containing the type of the variable and
20     *                if a class is provided, the class name.
21     */
22    public static function describeType($input): string
23    {
24        switch (\gettype($input)) {
25            case 'object':
26                return 'object(' . \get_class($input) . ')';
27            case 'array':
28                return 'array(' . \count($input) . ')';
29            default:
30                \ob_start();
31                \var_dump($input);
32                // normalize float vs double
33                /** @var string $varDumpContent */
34                $varDumpContent = \ob_get_clean();
35
36                return \str_replace('double(', 'float(', \rtrim($varDumpContent));
37        }
38    }
39
40    /**
41     * Parses an array of header lines into an associative array of headers.
42     *
43     * @param iterable $lines Header lines array of strings in the following
44     *                        format: "Name: Value"
45     */
46    public static function headersFromLines(iterable $lines): array
47    {
48        $headers = [];
49
50        foreach ($lines as $line) {
51            $parts = \explode(':', $line, 2);
52            $headers[\trim($parts[0])][] = isset($parts[1]) ? \trim($parts[1]) : null;
53        }
54
55        return $headers;
56    }
57
58    /**
59     * Returns a debug stream based on the provided variable.
60     *
61     * @param mixed $value Optional value
62     *
63     * @return resource
64     */
65    public static function debugResource($value = null)
66    {
67        if (\is_resource($value)) {
68            return $value;
69        }
70        if (\defined('STDOUT')) {
71            return \STDOUT;
72        }
73
74        $resource = \fopen('php://output', 'w');
75        if (false === $resource) {
76            throw new \RuntimeException('Can not open php output for writing to debug the resource.');
77        }
78
79        return $resource;
80    }
81
82    /**
83     * Chooses and creates a default handler to use based on the environment.
84     *
85     * The returned handler is not wrapped by any default middlewares.
86     *
87     * @throws \RuntimeException if no viable Handler is available.
88     *
89     * @return callable(\Psr\Http\Message\RequestInterface, array): \GuzzleHttp\Promise\PromiseInterface Returns the best handler for the given system.
90     */
91    public static function chooseHandler(): callable
92    {
93        $handler = null;
94        if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
95            $handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
96        } elseif (\function_exists('curl_exec')) {
97            $handler = new CurlHandler();
98        } elseif (\function_exists('curl_multi_exec')) {
99            $handler = new CurlMultiHandler();
100        }
101
102        if (\ini_get('allow_url_fopen')) {
103            $handler = $handler
104                ? Proxy::wrapStreaming($handler, new StreamHandler())
105                : new StreamHandler();
106        } elseif (!$handler) {
107            throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
108        }
109
110        return $handler;
111    }
112
113    /**
114     * Get the default User-Agent string to use with Guzzle.
115     */
116    public static function defaultUserAgent(): string
117    {
118        return sprintf('GuzzleHttp/%d', ClientInterface::MAJOR_VERSION);
119    }
120
121    /**
122     * Returns the default cacert bundle for the current system.
123     *
124     * First, the openssl.cafile and curl.cainfo php.ini settings are checked.
125     * If those settings are not configured, then the common locations for
126     * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X
127     * and Windows are checked. If any of these file locations are found on
128     * disk, they will be utilized.
129     *
130     * Note: the result of this function is cached for subsequent calls.
131     *
132     * @throws \RuntimeException if no bundle can be found.
133     *
134     * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+.
135     */
136    public static function defaultCaBundle(): string
137    {
138        static $cached = null;
139        static $cafiles = [
140            // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
141            '/etc/pki/tls/certs/ca-bundle.crt',
142            // Ubuntu, Debian (provided by the ca-certificates package)
143            '/etc/ssl/certs/ca-certificates.crt',
144            // FreeBSD (provided by the ca_root_nss package)
145            '/usr/local/share/certs/ca-root-nss.crt',
146            // SLES 12 (provided by the ca-certificates package)
147            '/var/lib/ca-certificates/ca-bundle.pem',
148            // OS X provided by homebrew (using the default path)
149            '/usr/local/etc/openssl/cert.pem',
150            // Google app engine
151            '/etc/ca-certificates.crt',
152            // Windows?
153            'C:\\windows\\system32\\curl-ca-bundle.crt',
154            'C:\\windows\\curl-ca-bundle.crt',
155        ];
156
157        if ($cached) {
158            return $cached;
159        }
160
161        if ($ca = \ini_get('openssl.cafile')) {
162            return $cached = $ca;
163        }
164
165        if ($ca = \ini_get('curl.cainfo')) {
166            return $cached = $ca;
167        }
168
169        foreach ($cafiles as $filename) {
170            if (\file_exists($filename)) {
171                return $cached = $filename;
172            }
173        }
174
175        throw new \RuntimeException(
176            <<< EOT
177No system CA bundle could be found in any of the the common system locations.
178PHP versions earlier than 5.6 are not properly configured to use the system's
179CA bundle by default. In order to verify peer certificates, you will need to
180supply the path on disk to a certificate bundle to the 'verify' request
181option: http://docs.guzzlephp.org/en/latest/clients.html#verify. If you do not
182need a specific certificate bundle, then Mozilla provides a commonly used CA
183bundle which can be downloaded here (provided by the maintainer of cURL):
184https://curl.haxx.se/ca/cacert.pem. Once
185you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP
186ini setting to point to the path to the file, allowing you to omit the 'verify'
187request option. See https://curl.haxx.se/docs/sslcerts.html for more
188information.
189EOT
190        );
191    }
192
193    /**
194     * Creates an associative array of lowercase header names to the actual
195     * header casing.
196     */
197    public static function normalizeHeaderKeys(array $headers): array
198    {
199        $result = [];
200        foreach (\array_keys($headers) as $key) {
201            $result[\strtolower($key)] = $key;
202        }
203
204        return $result;
205    }
206
207    /**
208     * Returns true if the provided host matches any of the no proxy areas.
209     *
210     * This method will strip a port from the host if it is present. Each pattern
211     * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a
212     * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" ==
213     * "baz.foo.com", but ".foo.com" != "foo.com").
214     *
215     * Areas are matched in the following cases:
216     * 1. "*" (without quotes) always matches any hosts.
217     * 2. An exact match.
218     * 3. The area starts with "." and the area is the last part of the host. e.g.
219     *    '.mit.edu' will match any host that ends with '.mit.edu'.
220     *
221     * @param string   $host         Host to check against the patterns.
222     * @param string[] $noProxyArray An array of host patterns.
223     *
224     * @throws InvalidArgumentException
225     */
226    public static function isHostInNoProxy(string $host, array $noProxyArray): bool
227    {
228        if (\strlen($host) === 0) {
229            throw new InvalidArgumentException('Empty host provided');
230        }
231
232        // Strip port if present.
233        if (\strpos($host, ':')) {
234            /** @var string[] $hostParts will never be false because of the checks above */
235            $hostParts = \explode($host, ':', 2);
236            $host = $hostParts[0];
237        }
238
239        foreach ($noProxyArray as $area) {
240            // Always match on wildcards.
241            if ($area === '*') {
242                return true;
243            } elseif (empty($area)) {
244                // Don't match on empty values.
245                continue;
246            } elseif ($area === $host) {
247                // Exact matches.
248                return true;
249            }
250            // Special match if the area when prefixed with ".". Remove any
251            // existing leading "." and add a new leading ".".
252            $area = '.' . \ltrim($area, '.');
253            if (\substr($host, -(\strlen($area))) === $area) {
254                return true;
255            }
256        }
257
258        return false;
259    }
260
261    /**
262     * Wrapper for json_decode that throws when an error occurs.
263     *
264     * @param string $json    JSON data to parse
265     * @param bool   $assoc   When true, returned objects will be converted
266     *                        into associative arrays.
267     * @param int    $depth   User specified recursion depth.
268     * @param int    $options Bitmask of JSON decode options.
269     *
270     * @return object|array|string|int|float|bool|null
271     *
272     * @throws InvalidArgumentException if the JSON cannot be decoded.
273     *
274     * @link https://www.php.net/manual/en/function.json-decode.php
275     */
276    public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
277    {
278        $data = \json_decode($json, $assoc, $depth, $options);
279        if (\JSON_ERROR_NONE !== \json_last_error()) {
280            throw new InvalidArgumentException('json_decode error: ' . \json_last_error_msg());
281        }
282
283        return $data;
284    }
285
286    /**
287     * Wrapper for JSON encoding that throws when an error occurs.
288     *
289     * @param mixed $value   The value being encoded
290     * @param int   $options JSON encode option bitmask
291     * @param int   $depth   Set the maximum depth. Must be greater than zero.
292     *
293     * @throws InvalidArgumentException if the JSON cannot be encoded.
294     *
295     * @link https://www.php.net/manual/en/function.json-encode.php
296     */
297    public static function jsonEncode($value, int $options = 0, int $depth = 512): string
298    {
299        $json = \json_encode($value, $options, $depth);
300        if (\JSON_ERROR_NONE !== \json_last_error()) {
301            throw new InvalidArgumentException('json_encode error: ' . \json_last_error_msg());
302        }
303
304        /** @var string */
305        return $json;
306    }
307
308    /**
309     * Wrapper for the hrtime() or microtime() functions
310     * (depending on the PHP version, one of the two is used)
311     *
312     * @return float UNIX timestamp
313     *
314     * @internal
315     */
316    public static function currentTime(): float
317    {
318        return (float) \function_exists('hrtime') ? \hrtime(true) / 1e9 : \microtime(true);
319    }
320
321    /**
322     * @throws InvalidArgumentException
323     *
324     * @internal
325     */
326    public static function idnUriConvert(UriInterface $uri, int $options = 0): UriInterface
327    {
328        if ($uri->getHost()) {
329            $asciiHost = self::idnToAsci($uri->getHost(), $options, $info);
330            if ($asciiHost === false) {
331                $errorBitSet = $info['errors'] ?? 0;
332
333                $errorConstants = array_filter(array_keys(get_defined_constants()), static function ($name) {
334                    return substr($name, 0, 11) === 'IDNA_ERROR_';
335                });
336
337                $errors = [];
338                foreach ($errorConstants as $errorConstant) {
339                    if ($errorBitSet & constant($errorConstant)) {
340                        $errors[] = $errorConstant;
341                    }
342                }
343
344                $errorMessage = 'IDN conversion failed';
345                if ($errors) {
346                    $errorMessage .= ' (errors: ' . implode(', ', $errors) . ')';
347                }
348
349                throw new InvalidArgumentException($errorMessage);
350            }
351            if ($uri->getHost() !== $asciiHost) {
352                // Replace URI only if the ASCII version is different
353                $uri = $uri->withHost($asciiHost);
354            }
355        }
356
357        return $uri;
358    }
359
360    /**
361     * @internal
362     */
363    public static function getenv(string $name): ?string
364    {
365        if (isset($_SERVER[$name])) {
366            return (string) $_SERVER[$name];
367        }
368
369        if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== false && $value !== null) {
370            return (string) $value;
371        }
372
373        return null;
374    }
375
376    /**
377     * @return string|false
378     */
379    private static function idnToAsci(string $domain, int $options, ?array &$info = [])
380    {
381        if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) {
382            return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info);
383        }
384
385        throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old');
386    }
387}
388