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