xref: /plugin/davcal/vendor/sabre/http/lib/Client.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehlernamespace Sabre\HTTP;
4*a1a3b679SAndreas Boehler
5*a1a3b679SAndreas Boehleruse Sabre\Event\EventEmitter;
6*a1a3b679SAndreas Boehleruse Sabre\Uri;
7*a1a3b679SAndreas Boehler
8*a1a3b679SAndreas Boehler/**
9*a1a3b679SAndreas Boehler * A rudimentary HTTP client.
10*a1a3b679SAndreas Boehler *
11*a1a3b679SAndreas Boehler * This object wraps PHP's curl extension and provides an easy way to send it a
12*a1a3b679SAndreas Boehler * Request object, and return a Response object.
13*a1a3b679SAndreas Boehler *
14*a1a3b679SAndreas Boehler * This is by no means intended as the next best HTTP client, but it does the
15*a1a3b679SAndreas Boehler * job and provides a simple integration with the rest of sabre/http.
16*a1a3b679SAndreas Boehler *
17*a1a3b679SAndreas Boehler * This client emits the following events:
18*a1a3b679SAndreas Boehler *   beforeRequest(RequestInterface $request)
19*a1a3b679SAndreas Boehler *   afterRequest(RequestInterface $request, ResponseInterface $response)
20*a1a3b679SAndreas Boehler *   error(RequestInterface $request, ResponseInterface $response, bool &$retry, int $retryCount)
21*a1a3b679SAndreas Boehler *
22*a1a3b679SAndreas Boehler * The beforeRequest event allows you to do some last minute changes to the
23*a1a3b679SAndreas Boehler * request before it's done, such as adding authentication headers.
24*a1a3b679SAndreas Boehler *
25*a1a3b679SAndreas Boehler * The afterRequest event will be emitted after the request is completed
26*a1a3b679SAndreas Boehler * succesfully.
27*a1a3b679SAndreas Boehler *
28*a1a3b679SAndreas Boehler * If a HTTP error is returned (status code higher than 399) the error event is
29*a1a3b679SAndreas Boehler * triggered. It's possible using this event to retry the request, by setting
30*a1a3b679SAndreas Boehler * retry to true.
31*a1a3b679SAndreas Boehler *
32*a1a3b679SAndreas Boehler * The amount of times a request has retried is passed as $retryCount, which
33*a1a3b679SAndreas Boehler * can be used to avoid retrying indefinitely. The first time the event is
34*a1a3b679SAndreas Boehler * called, this will be 0.
35*a1a3b679SAndreas Boehler *
36*a1a3b679SAndreas Boehler * It's also possible to intercept specific http errors, by subscribing to for
37*a1a3b679SAndreas Boehler * example 'error:401'.
38*a1a3b679SAndreas Boehler *
39*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
40*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/)
41*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
42*a1a3b679SAndreas Boehler */
43*a1a3b679SAndreas Boehlerclass Client extends EventEmitter {
44*a1a3b679SAndreas Boehler
45*a1a3b679SAndreas Boehler    /**
46*a1a3b679SAndreas Boehler     * List of curl settings
47*a1a3b679SAndreas Boehler     *
48*a1a3b679SAndreas Boehler     * @var array
49*a1a3b679SAndreas Boehler     */
50*a1a3b679SAndreas Boehler    protected $curlSettings = [];
51*a1a3b679SAndreas Boehler
52*a1a3b679SAndreas Boehler    /**
53*a1a3b679SAndreas Boehler     * Wether or not exceptions should be thrown when a HTTP error is returned.
54*a1a3b679SAndreas Boehler     *
55*a1a3b679SAndreas Boehler     * @var bool
56*a1a3b679SAndreas Boehler     */
57*a1a3b679SAndreas Boehler    protected $throwExceptions = false;
58*a1a3b679SAndreas Boehler
59*a1a3b679SAndreas Boehler    /**
60*a1a3b679SAndreas Boehler     * The maximum number of times we'll follow a redirect.
61*a1a3b679SAndreas Boehler     *
62*a1a3b679SAndreas Boehler     * @var int
63*a1a3b679SAndreas Boehler     */
64*a1a3b679SAndreas Boehler    protected $maxRedirects = 5;
65*a1a3b679SAndreas Boehler
66*a1a3b679SAndreas Boehler    /**
67*a1a3b679SAndreas Boehler     * Initializes the client.
68*a1a3b679SAndreas Boehler     *
69*a1a3b679SAndreas Boehler     * @return void
70*a1a3b679SAndreas Boehler     */
71*a1a3b679SAndreas Boehler    function __construct() {
72*a1a3b679SAndreas Boehler
73*a1a3b679SAndreas Boehler        $this->curlSettings = [
74*a1a3b679SAndreas Boehler            CURLOPT_RETURNTRANSFER => true,
75*a1a3b679SAndreas Boehler            CURLOPT_HEADER         => true,
76*a1a3b679SAndreas Boehler            CURLOPT_NOBODY         => false,
77*a1a3b679SAndreas Boehler        ];
78*a1a3b679SAndreas Boehler
79*a1a3b679SAndreas Boehler    }
80*a1a3b679SAndreas Boehler
81*a1a3b679SAndreas Boehler    /**
82*a1a3b679SAndreas Boehler     * Sends a request to a HTTP server, and returns a response.
83*a1a3b679SAndreas Boehler     *
84*a1a3b679SAndreas Boehler     * @param RequestInterface $request
85*a1a3b679SAndreas Boehler     * @return ResponseInterface
86*a1a3b679SAndreas Boehler     */
87*a1a3b679SAndreas Boehler    function send(RequestInterface $request) {
88*a1a3b679SAndreas Boehler
89*a1a3b679SAndreas Boehler        $this->emit('beforeRequest', [$request]);
90*a1a3b679SAndreas Boehler
91*a1a3b679SAndreas Boehler        $retryCount = 0;
92*a1a3b679SAndreas Boehler        $redirects = 0;
93*a1a3b679SAndreas Boehler
94*a1a3b679SAndreas Boehler        do {
95*a1a3b679SAndreas Boehler
96*a1a3b679SAndreas Boehler            $doRedirect = false;
97*a1a3b679SAndreas Boehler            $retry = false;
98*a1a3b679SAndreas Boehler
99*a1a3b679SAndreas Boehler            try {
100*a1a3b679SAndreas Boehler
101*a1a3b679SAndreas Boehler                $response = $this->doRequest($request);
102*a1a3b679SAndreas Boehler
103*a1a3b679SAndreas Boehler                $code = (int)$response->getStatus();
104*a1a3b679SAndreas Boehler
105*a1a3b679SAndreas Boehler                // We are doing in-PHP redirects, because curl's
106*a1a3b679SAndreas Boehler                // FOLLOW_LOCATION throws errors when PHP is configured with
107*a1a3b679SAndreas Boehler                // open_basedir.
108*a1a3b679SAndreas Boehler                //
109*a1a3b679SAndreas Boehler                // https://github.com/fruux/sabre-http/issues/12
110*a1a3b679SAndreas Boehler                if (in_array($code, [301, 302, 307, 308]) && $redirects < $this->maxRedirects) {
111*a1a3b679SAndreas Boehler
112*a1a3b679SAndreas Boehler                    $oldLocation = $request->getUrl();
113*a1a3b679SAndreas Boehler
114*a1a3b679SAndreas Boehler                    // Creating a new instance of the request object.
115*a1a3b679SAndreas Boehler                    $request = clone $request;
116*a1a3b679SAndreas Boehler
117*a1a3b679SAndreas Boehler                    // Setting the new location
118*a1a3b679SAndreas Boehler                    $request->setUrl(Uri\resolve(
119*a1a3b679SAndreas Boehler                        $oldLocation,
120*a1a3b679SAndreas Boehler                        $response->getHeader('Location')
121*a1a3b679SAndreas Boehler                    ));
122*a1a3b679SAndreas Boehler
123*a1a3b679SAndreas Boehler                    $doRedirect = true;
124*a1a3b679SAndreas Boehler                    $redirects++;
125*a1a3b679SAndreas Boehler
126*a1a3b679SAndreas Boehler                }
127*a1a3b679SAndreas Boehler
128*a1a3b679SAndreas Boehler                // This was a HTTP error
129*a1a3b679SAndreas Boehler                if ($code >= 400) {
130*a1a3b679SAndreas Boehler
131*a1a3b679SAndreas Boehler                    $this->emit('error', [$request, $response, &$retry, $retryCount]);
132*a1a3b679SAndreas Boehler                    $this->emit('error:' . $code, [$request, $response, &$retry, $retryCount]);
133*a1a3b679SAndreas Boehler
134*a1a3b679SAndreas Boehler                }
135*a1a3b679SAndreas Boehler
136*a1a3b679SAndreas Boehler            } catch (ClientException $e) {
137*a1a3b679SAndreas Boehler
138*a1a3b679SAndreas Boehler                $this->emit('exception', [$request, $e, &$retry, $retryCount]);
139*a1a3b679SAndreas Boehler
140*a1a3b679SAndreas Boehler                // If retry was still set to false, it means no event handler
141*a1a3b679SAndreas Boehler                // dealt with the problem. In this case we just re-throw the
142*a1a3b679SAndreas Boehler                // exception.
143*a1a3b679SAndreas Boehler                if (!$retry) {
144*a1a3b679SAndreas Boehler                    throw $e;
145*a1a3b679SAndreas Boehler                }
146*a1a3b679SAndreas Boehler
147*a1a3b679SAndreas Boehler            }
148*a1a3b679SAndreas Boehler
149*a1a3b679SAndreas Boehler            if ($retry) {
150*a1a3b679SAndreas Boehler                $retryCount++;
151*a1a3b679SAndreas Boehler            }
152*a1a3b679SAndreas Boehler
153*a1a3b679SAndreas Boehler        } while ($retry || $doRedirect);
154*a1a3b679SAndreas Boehler
155*a1a3b679SAndreas Boehler        $this->emit('afterRequest', [$request, $response]);
156*a1a3b679SAndreas Boehler
157*a1a3b679SAndreas Boehler        if ($this->throwExceptions && $code >= 400) {
158*a1a3b679SAndreas Boehler            throw new ClientHttpException($response);
159*a1a3b679SAndreas Boehler        }
160*a1a3b679SAndreas Boehler
161*a1a3b679SAndreas Boehler        return $response;
162*a1a3b679SAndreas Boehler
163*a1a3b679SAndreas Boehler    }
164*a1a3b679SAndreas Boehler
165*a1a3b679SAndreas Boehler    /**
166*a1a3b679SAndreas Boehler     * Sends a HTTP request asynchronously.
167*a1a3b679SAndreas Boehler     *
168*a1a3b679SAndreas Boehler     * Due to the nature of PHP, you must from time to time poll to see if any
169*a1a3b679SAndreas Boehler     * new responses came in.
170*a1a3b679SAndreas Boehler     *
171*a1a3b679SAndreas Boehler     * After calling sendAsync, you must therefore occasionally call the poll()
172*a1a3b679SAndreas Boehler     * method, or wait().
173*a1a3b679SAndreas Boehler     *
174*a1a3b679SAndreas Boehler     * @param RequestInterface $request
175*a1a3b679SAndreas Boehler     * @param callable $success
176*a1a3b679SAndreas Boehler     * @param callable $error
177*a1a3b679SAndreas Boehler     * @return void
178*a1a3b679SAndreas Boehler     */
179*a1a3b679SAndreas Boehler    function sendAsync(RequestInterface $request, callable $success = null, callable $error = null) {
180*a1a3b679SAndreas Boehler
181*a1a3b679SAndreas Boehler        $this->emit('beforeRequest', [$request]);
182*a1a3b679SAndreas Boehler        $this->sendAsyncInternal($request, $success, $error);
183*a1a3b679SAndreas Boehler        $this->poll();
184*a1a3b679SAndreas Boehler
185*a1a3b679SAndreas Boehler    }
186*a1a3b679SAndreas Boehler
187*a1a3b679SAndreas Boehler
188*a1a3b679SAndreas Boehler    /**
189*a1a3b679SAndreas Boehler     * This method checks if any http requests have gotten results, and if so,
190*a1a3b679SAndreas Boehler     * call the appropriate success or error handlers.
191*a1a3b679SAndreas Boehler     *
192*a1a3b679SAndreas Boehler     * This method will return true if there are still requests waiting to
193*a1a3b679SAndreas Boehler     * return, and false if all the work is done.
194*a1a3b679SAndreas Boehler     *
195*a1a3b679SAndreas Boehler     * @return bool
196*a1a3b679SAndreas Boehler     */
197*a1a3b679SAndreas Boehler    function poll() {
198*a1a3b679SAndreas Boehler
199*a1a3b679SAndreas Boehler        // nothing to do?
200*a1a3b679SAndreas Boehler        if (!$this->curlMultiMap) {
201*a1a3b679SAndreas Boehler            return false;
202*a1a3b679SAndreas Boehler        }
203*a1a3b679SAndreas Boehler
204*a1a3b679SAndreas Boehler        do {
205*a1a3b679SAndreas Boehler            $r = curl_multi_exec(
206*a1a3b679SAndreas Boehler                $this->curlMultiHandle,
207*a1a3b679SAndreas Boehler                $stillRunning
208*a1a3b679SAndreas Boehler            );
209*a1a3b679SAndreas Boehler        } while ($r === CURLM_CALL_MULTI_PERFORM);
210*a1a3b679SAndreas Boehler
211*a1a3b679SAndreas Boehler        do {
212*a1a3b679SAndreas Boehler
213*a1a3b679SAndreas Boehler            messageQueue:
214*a1a3b679SAndreas Boehler
215*a1a3b679SAndreas Boehler            $status = curl_multi_info_read(
216*a1a3b679SAndreas Boehler                $this->curlMultiHandle,
217*a1a3b679SAndreas Boehler                $messagesInQueue
218*a1a3b679SAndreas Boehler            );
219*a1a3b679SAndreas Boehler
220*a1a3b679SAndreas Boehler            if ($status && $status['msg'] === CURLMSG_DONE) {
221*a1a3b679SAndreas Boehler
222*a1a3b679SAndreas Boehler                $resourceId = intval($status['handle']);
223*a1a3b679SAndreas Boehler                list(
224*a1a3b679SAndreas Boehler                    $request,
225*a1a3b679SAndreas Boehler                    $successCallback,
226*a1a3b679SAndreas Boehler                    $errorCallback,
227*a1a3b679SAndreas Boehler                    $retryCount,
228*a1a3b679SAndreas Boehler                ) = $this->curlMultiMap[$resourceId];
229*a1a3b679SAndreas Boehler                unset($this->curlMultiMap[$resourceId]);
230*a1a3b679SAndreas Boehler                $curlResult = $this->parseCurlResult(curl_multi_getcontent($status['handle']), $status['handle']);
231*a1a3b679SAndreas Boehler                $retry = false;
232*a1a3b679SAndreas Boehler
233*a1a3b679SAndreas Boehler                if ($curlResult['status'] === self::STATUS_CURLERROR) {
234*a1a3b679SAndreas Boehler
235*a1a3b679SAndreas Boehler                    $e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']);
236*a1a3b679SAndreas Boehler                    $this->emit('exception', [$request, $e, &$retry, $retryCount]);
237*a1a3b679SAndreas Boehler
238*a1a3b679SAndreas Boehler                    if ($retry) {
239*a1a3b679SAndreas Boehler                        $retryCount++;
240*a1a3b679SAndreas Boehler                        $this->sendASyncInternal($request, $successCallback, $errorCallback, $retryCount);
241*a1a3b679SAndreas Boehler                        goto messageQueue;
242*a1a3b679SAndreas Boehler                    }
243*a1a3b679SAndreas Boehler
244*a1a3b679SAndreas Boehler                    $curlResult['request'] = $request;
245*a1a3b679SAndreas Boehler
246*a1a3b679SAndreas Boehler                    if ($errorCallback) {
247*a1a3b679SAndreas Boehler                        $errorCallback($curlResult);
248*a1a3b679SAndreas Boehler                    }
249*a1a3b679SAndreas Boehler
250*a1a3b679SAndreas Boehler                } elseif ($curlResult['status'] === self::STATUS_HTTPERROR) {
251*a1a3b679SAndreas Boehler
252*a1a3b679SAndreas Boehler                    $this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]);
253*a1a3b679SAndreas Boehler                    $this->emit('error:' . $curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]);
254*a1a3b679SAndreas Boehler
255*a1a3b679SAndreas Boehler                    if ($retry) {
256*a1a3b679SAndreas Boehler
257*a1a3b679SAndreas Boehler                        $retryCount++;
258*a1a3b679SAndreas Boehler                        $this->sendASyncInternal($request, $successCallback, $errorCallback, $retryCount);
259*a1a3b679SAndreas Boehler                        goto messageQueue;
260*a1a3b679SAndreas Boehler
261*a1a3b679SAndreas Boehler                    }
262*a1a3b679SAndreas Boehler
263*a1a3b679SAndreas Boehler                    $curlResult['request'] = $request;
264*a1a3b679SAndreas Boehler
265*a1a3b679SAndreas Boehler                    if ($errorCallback) {
266*a1a3b679SAndreas Boehler                        $errorCallback($curlResult);
267*a1a3b679SAndreas Boehler                    }
268*a1a3b679SAndreas Boehler
269*a1a3b679SAndreas Boehler                } else {
270*a1a3b679SAndreas Boehler
271*a1a3b679SAndreas Boehler                    $this->emit('afterRequest', [$request, $curlResult['response']]);
272*a1a3b679SAndreas Boehler
273*a1a3b679SAndreas Boehler                    if ($successCallback) {
274*a1a3b679SAndreas Boehler                        $successCallback($curlResult['response']);
275*a1a3b679SAndreas Boehler                    }
276*a1a3b679SAndreas Boehler
277*a1a3b679SAndreas Boehler                }
278*a1a3b679SAndreas Boehler            }
279*a1a3b679SAndreas Boehler
280*a1a3b679SAndreas Boehler        } while ($messagesInQueue > 0);
281*a1a3b679SAndreas Boehler
282*a1a3b679SAndreas Boehler        return count($this->curlMultiMap) > 0;
283*a1a3b679SAndreas Boehler
284*a1a3b679SAndreas Boehler    }
285*a1a3b679SAndreas Boehler
286*a1a3b679SAndreas Boehler    /**
287*a1a3b679SAndreas Boehler     * Processes every HTTP request in the queue, and waits till they are all
288*a1a3b679SAndreas Boehler     * completed.
289*a1a3b679SAndreas Boehler     *
290*a1a3b679SAndreas Boehler     * @return void
291*a1a3b679SAndreas Boehler     */
292*a1a3b679SAndreas Boehler    function wait() {
293*a1a3b679SAndreas Boehler
294*a1a3b679SAndreas Boehler        do {
295*a1a3b679SAndreas Boehler            curl_multi_select($this->curlMultiHandle);
296*a1a3b679SAndreas Boehler            $stillRunning = $this->poll();
297*a1a3b679SAndreas Boehler        } while ($stillRunning);
298*a1a3b679SAndreas Boehler
299*a1a3b679SAndreas Boehler    }
300*a1a3b679SAndreas Boehler
301*a1a3b679SAndreas Boehler    /**
302*a1a3b679SAndreas Boehler     * If this is set to true, the Client will automatically throw exceptions
303*a1a3b679SAndreas Boehler     * upon HTTP errors.
304*a1a3b679SAndreas Boehler     *
305*a1a3b679SAndreas Boehler     * This means that if a response came back with a status code greater than
306*a1a3b679SAndreas Boehler     * or equal to 400, we will throw a ClientHttpException.
307*a1a3b679SAndreas Boehler     *
308*a1a3b679SAndreas Boehler     * This only works for the send() method. Throwing exceptions for
309*a1a3b679SAndreas Boehler     * sendAsync() is not supported.
310*a1a3b679SAndreas Boehler     *
311*a1a3b679SAndreas Boehler     * @param bool $throwExceptions
312*a1a3b679SAndreas Boehler     * @return void
313*a1a3b679SAndreas Boehler     */
314*a1a3b679SAndreas Boehler    function setThrowExceptions($throwExceptions) {
315*a1a3b679SAndreas Boehler
316*a1a3b679SAndreas Boehler        $this->throwExceptions = $throwExceptions;
317*a1a3b679SAndreas Boehler
318*a1a3b679SAndreas Boehler    }
319*a1a3b679SAndreas Boehler
320*a1a3b679SAndreas Boehler    /**
321*a1a3b679SAndreas Boehler     * Adds a CURL setting.
322*a1a3b679SAndreas Boehler     *
323*a1a3b679SAndreas Boehler     * These settings will be included in every HTTP request.
324*a1a3b679SAndreas Boehler     *
325*a1a3b679SAndreas Boehler     * @param int $name
326*a1a3b679SAndreas Boehler     * @param mixed $value
327*a1a3b679SAndreas Boehler     * @return void
328*a1a3b679SAndreas Boehler     */
329*a1a3b679SAndreas Boehler    function addCurlSetting($name, $value) {
330*a1a3b679SAndreas Boehler
331*a1a3b679SAndreas Boehler        $this->curlSettings[$name] = $value;
332*a1a3b679SAndreas Boehler
333*a1a3b679SAndreas Boehler    }
334*a1a3b679SAndreas Boehler
335*a1a3b679SAndreas Boehler    /**
336*a1a3b679SAndreas Boehler     * This method is responsible for performing a single request.
337*a1a3b679SAndreas Boehler     *
338*a1a3b679SAndreas Boehler     * @param RequestInterface $request
339*a1a3b679SAndreas Boehler     * @return ResponseInterface
340*a1a3b679SAndreas Boehler     */
341*a1a3b679SAndreas Boehler    protected function doRequest(RequestInterface $request) {
342*a1a3b679SAndreas Boehler
343*a1a3b679SAndreas Boehler        $settings = $this->createCurlSettingsArray($request);
344*a1a3b679SAndreas Boehler
345*a1a3b679SAndreas Boehler        if (!$this->curlHandle) {
346*a1a3b679SAndreas Boehler            $this->curlHandle = curl_init();
347*a1a3b679SAndreas Boehler        }
348*a1a3b679SAndreas Boehler
349*a1a3b679SAndreas Boehler        curl_setopt_array($this->curlHandle, $settings);
350*a1a3b679SAndreas Boehler        $response = $this->curlExec($this->curlHandle);
351*a1a3b679SAndreas Boehler        $response = $this->parseCurlResult($response, $this->curlHandle);
352*a1a3b679SAndreas Boehler
353*a1a3b679SAndreas Boehler        if ($response['status'] === self::STATUS_CURLERROR) {
354*a1a3b679SAndreas Boehler            throw new ClientException($response['curl_errmsg'], $response['curl_errno']);
355*a1a3b679SAndreas Boehler        }
356*a1a3b679SAndreas Boehler
357*a1a3b679SAndreas Boehler        return $response['response'];
358*a1a3b679SAndreas Boehler
359*a1a3b679SAndreas Boehler    }
360*a1a3b679SAndreas Boehler
361*a1a3b679SAndreas Boehler    /**
362*a1a3b679SAndreas Boehler     * Cached curl handle.
363*a1a3b679SAndreas Boehler     *
364*a1a3b679SAndreas Boehler     * By keeping this resource around for the lifetime of this object, things
365*a1a3b679SAndreas Boehler     * like persistent connections are possible.
366*a1a3b679SAndreas Boehler     *
367*a1a3b679SAndreas Boehler     * @var resource
368*a1a3b679SAndreas Boehler     */
369*a1a3b679SAndreas Boehler    private $curlHandle;
370*a1a3b679SAndreas Boehler
371*a1a3b679SAndreas Boehler    /**
372*a1a3b679SAndreas Boehler     * Handler for curl_multi requests.
373*a1a3b679SAndreas Boehler     *
374*a1a3b679SAndreas Boehler     * The first time sendAsync is used, this will be created.
375*a1a3b679SAndreas Boehler     *
376*a1a3b679SAndreas Boehler     * @var resource
377*a1a3b679SAndreas Boehler     */
378*a1a3b679SAndreas Boehler    private $curlMultiHandle;
379*a1a3b679SAndreas Boehler
380*a1a3b679SAndreas Boehler    /**
381*a1a3b679SAndreas Boehler     * Has a list of curl handles, as well as their associated success and
382*a1a3b679SAndreas Boehler     * error callbacks.
383*a1a3b679SAndreas Boehler     *
384*a1a3b679SAndreas Boehler     * @var array
385*a1a3b679SAndreas Boehler     */
386*a1a3b679SAndreas Boehler    private $curlMultiMap = [];
387*a1a3b679SAndreas Boehler
388*a1a3b679SAndreas Boehler    /**
389*a1a3b679SAndreas Boehler     * Turns a RequestInterface object into an array with settings that can be
390*a1a3b679SAndreas Boehler     * fed to curl_setopt
391*a1a3b679SAndreas Boehler     *
392*a1a3b679SAndreas Boehler     * @param RequestInterface $request
393*a1a3b679SAndreas Boehler     * @return array
394*a1a3b679SAndreas Boehler     */
395*a1a3b679SAndreas Boehler    protected function createCurlSettingsArray(RequestInterface $request) {
396*a1a3b679SAndreas Boehler
397*a1a3b679SAndreas Boehler        $settings = $this->curlSettings;
398*a1a3b679SAndreas Boehler
399*a1a3b679SAndreas Boehler        switch ($request->getMethod()) {
400*a1a3b679SAndreas Boehler            case 'HEAD' :
401*a1a3b679SAndreas Boehler                $settings[CURLOPT_NOBODY] = true;
402*a1a3b679SAndreas Boehler                $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
403*a1a3b679SAndreas Boehler                $settings[CURLOPT_POSTFIELDS] = '';
404*a1a3b679SAndreas Boehler                $settings[CURLOPT_PUT] = false;
405*a1a3b679SAndreas Boehler                break;
406*a1a3b679SAndreas Boehler            case 'GET' :
407*a1a3b679SAndreas Boehler                $settings[CURLOPT_CUSTOMREQUEST] = 'GET';
408*a1a3b679SAndreas Boehler                $settings[CURLOPT_POSTFIELDS] = '';
409*a1a3b679SAndreas Boehler                $settings[CURLOPT_PUT] = false;
410*a1a3b679SAndreas Boehler                break;
411*a1a3b679SAndreas Boehler            default :
412*a1a3b679SAndreas Boehler                $body = $request->getBody();
413*a1a3b679SAndreas Boehler                if (is_resource($body)) {
414*a1a3b679SAndreas Boehler                    // This needs to be set to PUT, regardless of the actual
415*a1a3b679SAndreas Boehler                    // method used. Without it, INFILE will be ignored for some
416*a1a3b679SAndreas Boehler                    // reason.
417*a1a3b679SAndreas Boehler                    $settings[CURLOPT_PUT] = true;
418*a1a3b679SAndreas Boehler                    $settings[CURLOPT_INFILE] = $request->getBody();
419*a1a3b679SAndreas Boehler                } else {
420*a1a3b679SAndreas Boehler                    // For security we cast this to a string. If somehow an array could
421*a1a3b679SAndreas Boehler                    // be passed here, it would be possible for an attacker to use @ to
422*a1a3b679SAndreas Boehler                    // post local files.
423*a1a3b679SAndreas Boehler                    $settings[CURLOPT_POSTFIELDS] = (string)$body;
424*a1a3b679SAndreas Boehler                }
425*a1a3b679SAndreas Boehler                $settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
426*a1a3b679SAndreas Boehler                break;
427*a1a3b679SAndreas Boehler
428*a1a3b679SAndreas Boehler        }
429*a1a3b679SAndreas Boehler
430*a1a3b679SAndreas Boehler        $nHeaders = [];
431*a1a3b679SAndreas Boehler        foreach ($request->getHeaders() as $key => $values) {
432*a1a3b679SAndreas Boehler
433*a1a3b679SAndreas Boehler            foreach ($values as $value) {
434*a1a3b679SAndreas Boehler                $nHeaders[] = $key . ': ' . $value;
435*a1a3b679SAndreas Boehler            }
436*a1a3b679SAndreas Boehler
437*a1a3b679SAndreas Boehler        }
438*a1a3b679SAndreas Boehler        $settings[CURLOPT_HTTPHEADER] = $nHeaders;
439*a1a3b679SAndreas Boehler        $settings[CURLOPT_URL] = $request->getUrl();
440*a1a3b679SAndreas Boehler        // FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM
441*a1a3b679SAndreas Boehler        if (defined('CURLOPT_PROTOCOLS')) {
442*a1a3b679SAndreas Boehler            $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
443*a1a3b679SAndreas Boehler        }
444*a1a3b679SAndreas Boehler        // FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM
445*a1a3b679SAndreas Boehler        if (defined('CURLOPT_REDIR_PROTOCOLS')) {
446*a1a3b679SAndreas Boehler            $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
447*a1a3b679SAndreas Boehler        }
448*a1a3b679SAndreas Boehler
449*a1a3b679SAndreas Boehler        return $settings;
450*a1a3b679SAndreas Boehler
451*a1a3b679SAndreas Boehler    }
452*a1a3b679SAndreas Boehler
453*a1a3b679SAndreas Boehler    const STATUS_SUCCESS = 0;
454*a1a3b679SAndreas Boehler    const STATUS_CURLERROR = 1;
455*a1a3b679SAndreas Boehler    const STATUS_HTTPERROR = 2;
456*a1a3b679SAndreas Boehler
457*a1a3b679SAndreas Boehler    /**
458*a1a3b679SAndreas Boehler     * Parses the result of a curl call in a format that's a bit more
459*a1a3b679SAndreas Boehler     * convenient to work with.
460*a1a3b679SAndreas Boehler     *
461*a1a3b679SAndreas Boehler     * The method returns an array with the following elements:
462*a1a3b679SAndreas Boehler     *   * status - one of the 3 STATUS constants.
463*a1a3b679SAndreas Boehler     *   * curl_errno - A curl error number. Only set if status is
464*a1a3b679SAndreas Boehler     *                  STATUS_CURLERROR.
465*a1a3b679SAndreas Boehler     *   * curl_errmsg - A current error message. Only set if status is
466*a1a3b679SAndreas Boehler     *                   STATUS_CURLERROR.
467*a1a3b679SAndreas Boehler     *   * response - Response object. Only set if status is STATUS_SUCCESS, or
468*a1a3b679SAndreas Boehler     *                STATUS_HTTPERROR.
469*a1a3b679SAndreas Boehler     *   * http_code - HTTP status code, as an int. Only set if Only set if
470*a1a3b679SAndreas Boehler     *                 status is STATUS_SUCCESS, or STATUS_HTTPERROR
471*a1a3b679SAndreas Boehler     *
472*a1a3b679SAndreas Boehler     * @param string $response
473*a1a3b679SAndreas Boehler     * @param resource $curlHandle
474*a1a3b679SAndreas Boehler     * @return Response
475*a1a3b679SAndreas Boehler     */
476*a1a3b679SAndreas Boehler    protected function parseCurlResult($response, $curlHandle) {
477*a1a3b679SAndreas Boehler
478*a1a3b679SAndreas Boehler        list(
479*a1a3b679SAndreas Boehler            $curlInfo,
480*a1a3b679SAndreas Boehler            $curlErrNo,
481*a1a3b679SAndreas Boehler            $curlErrMsg
482*a1a3b679SAndreas Boehler        ) = $this->curlStuff($curlHandle);
483*a1a3b679SAndreas Boehler
484*a1a3b679SAndreas Boehler        if ($curlErrNo) {
485*a1a3b679SAndreas Boehler            return [
486*a1a3b679SAndreas Boehler                'status'      => self::STATUS_CURLERROR,
487*a1a3b679SAndreas Boehler                'curl_errno'  => $curlErrNo,
488*a1a3b679SAndreas Boehler                'curl_errmsg' => $curlErrMsg,
489*a1a3b679SAndreas Boehler            ];
490*a1a3b679SAndreas Boehler        }
491*a1a3b679SAndreas Boehler
492*a1a3b679SAndreas Boehler        $headerBlob = substr($response, 0, $curlInfo['header_size']);
493*a1a3b679SAndreas Boehler        // In the case of 204 No Content, strlen($response) == $curlInfo['header_size].
494*a1a3b679SAndreas Boehler        // This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL
495*a1a3b679SAndreas Boehler        // An exception will be thrown when calling getBodyAsString then
496*a1a3b679SAndreas Boehler        $responseBody = substr($response, $curlInfo['header_size']) ?: null;
497*a1a3b679SAndreas Boehler
498*a1a3b679SAndreas Boehler        unset($response);
499*a1a3b679SAndreas Boehler
500*a1a3b679SAndreas Boehler        // In the case of 100 Continue, or redirects we'll have multiple lists
501*a1a3b679SAndreas Boehler        // of headers for each separate HTTP response. We can easily split this
502*a1a3b679SAndreas Boehler        // because they are separated by \r\n\r\n
503*a1a3b679SAndreas Boehler        $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
504*a1a3b679SAndreas Boehler
505*a1a3b679SAndreas Boehler        // We only care about the last set of headers
506*a1a3b679SAndreas Boehler        $headerBlob = $headerBlob[count($headerBlob) - 1];
507*a1a3b679SAndreas Boehler
508*a1a3b679SAndreas Boehler        // Splitting headers
509*a1a3b679SAndreas Boehler        $headerBlob = explode("\r\n", $headerBlob);
510*a1a3b679SAndreas Boehler
511*a1a3b679SAndreas Boehler        $response = new Response();
512*a1a3b679SAndreas Boehler        $response->setStatus($curlInfo['http_code']);
513*a1a3b679SAndreas Boehler
514*a1a3b679SAndreas Boehler        foreach ($headerBlob as $header) {
515*a1a3b679SAndreas Boehler            $parts = explode(':', $header, 2);
516*a1a3b679SAndreas Boehler            if (count($parts) == 2) {
517*a1a3b679SAndreas Boehler                $response->addHeader(trim($parts[0]), trim($parts[1]));
518*a1a3b679SAndreas Boehler            }
519*a1a3b679SAndreas Boehler        }
520*a1a3b679SAndreas Boehler
521*a1a3b679SAndreas Boehler        $response->setBody($responseBody);
522*a1a3b679SAndreas Boehler
523*a1a3b679SAndreas Boehler        $httpCode = intval($response->getStatus());
524*a1a3b679SAndreas Boehler
525*a1a3b679SAndreas Boehler        return [
526*a1a3b679SAndreas Boehler            'status'    => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS,
527*a1a3b679SAndreas Boehler            'response'  => $response,
528*a1a3b679SAndreas Boehler            'http_code' => $httpCode,
529*a1a3b679SAndreas Boehler        ];
530*a1a3b679SAndreas Boehler
531*a1a3b679SAndreas Boehler    }
532*a1a3b679SAndreas Boehler
533*a1a3b679SAndreas Boehler    /**
534*a1a3b679SAndreas Boehler     * Sends an asynchronous HTTP request.
535*a1a3b679SAndreas Boehler     *
536*a1a3b679SAndreas Boehler     * We keep this in a separate method, so we can call it without triggering
537*a1a3b679SAndreas Boehler     * the beforeRequest event and don't do the poll().
538*a1a3b679SAndreas Boehler     *
539*a1a3b679SAndreas Boehler     * @param RequestInterface $request
540*a1a3b679SAndreas Boehler     * @param callable $success
541*a1a3b679SAndreas Boehler     * @param callable $error
542*a1a3b679SAndreas Boehler     * @param int $retryCount
543*a1a3b679SAndreas Boehler     */
544*a1a3b679SAndreas Boehler    protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, $retryCount = 0) {
545*a1a3b679SAndreas Boehler
546*a1a3b679SAndreas Boehler        if (!$this->curlMultiHandle) {
547*a1a3b679SAndreas Boehler            $this->curlMultiHandle = curl_multi_init();
548*a1a3b679SAndreas Boehler        }
549*a1a3b679SAndreas Boehler        $curl = curl_init();
550*a1a3b679SAndreas Boehler        curl_setopt_array(
551*a1a3b679SAndreas Boehler            $curl,
552*a1a3b679SAndreas Boehler            $this->createCurlSettingsArray($request)
553*a1a3b679SAndreas Boehler        );
554*a1a3b679SAndreas Boehler        curl_multi_add_handle($this->curlMultiHandle, $curl);
555*a1a3b679SAndreas Boehler        $this->curlMultiMap[intval($curl)] = [
556*a1a3b679SAndreas Boehler            $request,
557*a1a3b679SAndreas Boehler            $success,
558*a1a3b679SAndreas Boehler            $error,
559*a1a3b679SAndreas Boehler            $retryCount
560*a1a3b679SAndreas Boehler        ];
561*a1a3b679SAndreas Boehler
562*a1a3b679SAndreas Boehler    }
563*a1a3b679SAndreas Boehler
564*a1a3b679SAndreas Boehler    // @codeCoverageIgnoreStart
565*a1a3b679SAndreas Boehler
566*a1a3b679SAndreas Boehler    /**
567*a1a3b679SAndreas Boehler     * Calls curl_exec
568*a1a3b679SAndreas Boehler     *
569*a1a3b679SAndreas Boehler     * This method exists so it can easily be overridden and mocked.
570*a1a3b679SAndreas Boehler     *
571*a1a3b679SAndreas Boehler     * @param resource $curlHandle
572*a1a3b679SAndreas Boehler     * @return string
573*a1a3b679SAndreas Boehler     */
574*a1a3b679SAndreas Boehler    protected function curlExec($curlHandle) {
575*a1a3b679SAndreas Boehler
576*a1a3b679SAndreas Boehler        return curl_exec($curlHandle);
577*a1a3b679SAndreas Boehler
578*a1a3b679SAndreas Boehler    }
579*a1a3b679SAndreas Boehler
580*a1a3b679SAndreas Boehler    /**
581*a1a3b679SAndreas Boehler     * Returns a bunch of information about a curl request.
582*a1a3b679SAndreas Boehler     *
583*a1a3b679SAndreas Boehler     * This method exists so it can easily be overridden and mocked.
584*a1a3b679SAndreas Boehler     *
585*a1a3b679SAndreas Boehler     * @param resource $curlHandle
586*a1a3b679SAndreas Boehler     * @return array
587*a1a3b679SAndreas Boehler     */
588*a1a3b679SAndreas Boehler    protected function curlStuff($curlHandle) {
589*a1a3b679SAndreas Boehler
590*a1a3b679SAndreas Boehler        return [
591*a1a3b679SAndreas Boehler            curl_getinfo($curlHandle),
592*a1a3b679SAndreas Boehler            curl_errno($curlHandle),
593*a1a3b679SAndreas Boehler            curl_error($curlHandle),
594*a1a3b679SAndreas Boehler        ];
595*a1a3b679SAndreas Boehler
596*a1a3b679SAndreas Boehler    }
597*a1a3b679SAndreas Boehler    // @codeCoverageIgnoreEnd
598*a1a3b679SAndreas Boehler
599*a1a3b679SAndreas Boehler}
600