1<?php
2namespace GuzzleHttp\Ring;
3
4use GuzzleHttp\Stream\StreamInterface;
5use GuzzleHttp\Ring\Future\FutureArrayInterface;
6use GuzzleHttp\Ring\Future\FutureArray;
7
8/**
9 * Provides core functionality of Ring handlers and middleware.
10 */
11class Core
12{
13    /**
14     * Returns a function that calls all of the provided functions, in order,
15     * passing the arguments provided to the composed function to each function.
16     *
17     * @param callable[] $functions Array of functions to proxy to.
18     *
19     * @return callable
20     */
21    public static function callArray(array $functions)
22    {
23        return function () use ($functions) {
24            $args = func_get_args();
25            foreach ($functions as $fn) {
26                call_user_func_array($fn, $args);
27            }
28        };
29    }
30
31    /**
32     * Gets an array of header line values from a message for a specific header
33     *
34     * This method searches through the "headers" key of a message for a header
35     * using a case-insensitive search.
36     *
37     * @param array  $message Request or response hash.
38     * @param string $header  Header to retrieve
39     *
40     * @return array
41     */
42    public static function headerLines($message, $header)
43    {
44        $result = [];
45
46        if (!empty($message['headers'])) {
47            foreach ($message['headers'] as $name => $value) {
48                if (!strcasecmp($name, $header)) {
49                    $result = array_merge($result, $value);
50                }
51            }
52        }
53
54        return $result;
55    }
56
57    /**
58     * Gets a header value from a message as a string or null
59     *
60     * This method searches through the "headers" key of a message for a header
61     * using a case-insensitive search. The lines of the header are imploded
62     * using commas into a single string return value.
63     *
64     * @param array  $message Request or response hash.
65     * @param string $header  Header to retrieve
66     *
67     * @return string|null Returns the header string if found, or null if not.
68     */
69    public static function header($message, $header)
70    {
71        $match = self::headerLines($message, $header);
72        return $match ? implode(', ', $match) : null;
73    }
74
75    /**
76     * Returns the first header value from a message as a string or null. If
77     * a header line contains multiple values separated by a comma, then this
78     * function will return the first value in the list.
79     *
80     * @param array  $message Request or response hash.
81     * @param string $header  Header to retrieve
82     *
83     * @return string|null Returns the value as a string if found.
84     */
85    public static function firstHeader($message, $header)
86    {
87        if (!empty($message['headers'])) {
88            foreach ($message['headers'] as $name => $value) {
89                if (!strcasecmp($name, $header)) {
90                    // Return the match itself if it is a single value.
91                    $pos = strpos($value[0], ',');
92                    return $pos ? substr($value[0], 0, $pos) : $value[0];
93                }
94            }
95        }
96
97        return null;
98    }
99
100    /**
101     * Returns true if a message has the provided case-insensitive header.
102     *
103     * @param array  $message Request or response hash.
104     * @param string $header  Header to check
105     *
106     * @return bool
107     */
108    public static function hasHeader($message, $header)
109    {
110        if (!empty($message['headers'])) {
111            foreach ($message['headers'] as $name => $value) {
112                if (!strcasecmp($name, $header)) {
113                    return true;
114                }
115            }
116        }
117
118        return false;
119    }
120
121    /**
122     * Parses an array of header lines into an associative array of headers.
123     *
124     * @param array $lines Header lines array of strings in the following
125     *                     format: "Name: Value"
126     * @return array
127     */
128    public static function headersFromLines($lines)
129    {
130        $headers = [];
131
132        foreach ($lines as $line) {
133            $parts = explode(':', $line, 2);
134            $headers[trim($parts[0])][] = isset($parts[1])
135                ? trim($parts[1])
136                : null;
137        }
138
139        return $headers;
140    }
141
142    /**
143     * Removes a header from a message using a case-insensitive comparison.
144     *
145     * @param array  $message Message that contains 'headers'
146     * @param string $header  Header to remove
147     *
148     * @return array
149     */
150    public static function removeHeader(array $message, $header)
151    {
152        if (isset($message['headers'])) {
153            foreach (array_keys($message['headers']) as $key) {
154                if (!strcasecmp($header, $key)) {
155                    unset($message['headers'][$key]);
156                }
157            }
158        }
159
160        return $message;
161    }
162
163    /**
164     * Replaces any existing case insensitive headers with the given value.
165     *
166     * @param array  $message Message that contains 'headers'
167     * @param string $header  Header to set.
168     * @param array  $value   Value to set.
169     *
170     * @return array
171     */
172    public static function setHeader(array $message, $header, array $value)
173    {
174        $message = self::removeHeader($message, $header);
175        $message['headers'][$header] = $value;
176
177        return $message;
178    }
179
180    /**
181     * Creates a URL string from a request.
182     *
183     * If the "url" key is present on the request, it is returned, otherwise
184     * the url is built up based on the scheme, host, uri, and query_string
185     * request values.
186     *
187     * @param array $request Request to get the URL from
188     *
189     * @return string Returns the request URL as a string.
190     * @throws \InvalidArgumentException if no Host header is present.
191     */
192    public static function url(array $request)
193    {
194        if (isset($request['url'])) {
195            return $request['url'];
196        }
197
198        $uri = (isset($request['scheme'])
199                ? $request['scheme'] : 'http') . '://';
200
201        if ($host = self::header($request, 'host')) {
202            $uri .= $host;
203        } else {
204            throw new \InvalidArgumentException('No Host header was provided');
205        }
206
207        if (isset($request['uri'])) {
208            $uri .= $request['uri'];
209        }
210
211        if (isset($request['query_string'])) {
212            $uri .= '?' . $request['query_string'];
213        }
214
215        return $uri;
216    }
217
218    /**
219     * Reads the body of a message into a string.
220     *
221     * @param array|FutureArrayInterface $message Array containing a "body" key
222     *
223     * @return null|string Returns the body as a string or null if not set.
224     * @throws \InvalidArgumentException if a request body is invalid.
225     */
226    public static function body($message)
227    {
228        if (!isset($message['body'])) {
229            return null;
230        }
231
232        if ($message['body'] instanceof StreamInterface) {
233            return (string) $message['body'];
234        }
235
236        switch (gettype($message['body'])) {
237            case 'string':
238                return $message['body'];
239            case 'resource':
240                return stream_get_contents($message['body']);
241            case 'object':
242                if ($message['body'] instanceof \Iterator) {
243                    return implode('', iterator_to_array($message['body']));
244                } elseif (method_exists($message['body'], '__toString')) {
245                    return (string) $message['body'];
246                }
247            default:
248                throw new \InvalidArgumentException('Invalid request body: '
249                    . self::describeType($message['body']));
250        }
251    }
252
253    /**
254     * Rewind the body of the provided message if possible.
255     *
256     * @param array $message Message that contains a 'body' field.
257     *
258     * @return bool Returns true on success, false on failure
259     */
260    public static function rewindBody($message)
261    {
262        if ($message['body'] instanceof StreamInterface) {
263            return $message['body']->seek(0);
264        }
265
266        if ($message['body'] instanceof \Generator) {
267            return false;
268        }
269
270        if ($message['body'] instanceof \Iterator) {
271            $message['body']->rewind();
272            return true;
273        }
274
275        if (is_resource($message['body'])) {
276            return rewind($message['body']);
277        }
278
279        return is_string($message['body'])
280            || (is_object($message['body'])
281                && method_exists($message['body'], '__toString'));
282    }
283
284    /**
285     * Debug function used to describe the provided value type and class.
286     *
287     * @param mixed $input
288     *
289     * @return string Returns a string containing the type of the variable and
290     *                if a class is provided, the class name.
291     */
292    public static function describeType($input)
293    {
294        switch (gettype($input)) {
295            case 'object':
296                return 'object(' . get_class($input) . ')';
297            case 'array':
298                return 'array(' . count($input) . ')';
299            default:
300                ob_start();
301                var_dump($input);
302                // normalize float vs double
303                return str_replace('double(', 'float(', rtrim(ob_get_clean()));
304        }
305    }
306
307    /**
308     * Sleep for the specified amount of time specified in the request's
309     * ['client']['delay'] option if present.
310     *
311     * This function should only be used when a non-blocking sleep is not
312     * possible.
313     *
314     * @param array $request Request to sleep
315     */
316    public static function doSleep(array $request)
317    {
318        if (isset($request['client']['delay'])) {
319            usleep($request['client']['delay'] * 1000);
320        }
321    }
322
323    /**
324     * Returns a proxied future that modifies the dereferenced value of another
325     * future using a promise.
326     *
327     * @param FutureArrayInterface $future      Future to wrap with a new future
328     * @param callable    $onFulfilled Invoked when the future fulfilled
329     * @param callable    $onRejected  Invoked when the future rejected
330     * @param callable    $onProgress  Invoked when the future progresses
331     *
332     * @return FutureArray
333     */
334    public static function proxy(
335        FutureArrayInterface $future,
336        callable $onFulfilled = null,
337        callable $onRejected = null,
338        callable $onProgress = null
339    ) {
340        return new FutureArray(
341            $future->then($onFulfilled, $onRejected, $onProgress),
342            [$future, 'wait'],
343            [$future, 'cancel']
344        );
345    }
346
347    /**
348     * Returns a debug stream based on the provided variable.
349     *
350     * @param mixed $value Optional value
351     *
352     * @return resource
353     */
354    public static function getDebugResource($value = null)
355    {
356        if (is_resource($value)) {
357            return $value;
358        } elseif (defined('STDOUT')) {
359            return STDOUT;
360        } else {
361            return fopen('php://output', 'w');
362        }
363    }
364}
365