curlSettings = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_NOBODY => false, ]; } /** * Sends a request to a HTTP server, and returns a response. * * @param RequestInterface $request * @return ResponseInterface */ function send(RequestInterface $request) { $this->emit('beforeRequest', [$request]); $retryCount = 0; $redirects = 0; do { $doRedirect = false; $retry = false; try { $response = $this->doRequest($request); $code = (int)$response->getStatus(); // We are doing in-PHP redirects, because curl's // FOLLOW_LOCATION throws errors when PHP is configured with // open_basedir. // // https://github.com/fruux/sabre-http/issues/12 if (in_array($code, [301, 302, 307, 308]) && $redirects < $this->maxRedirects) { $oldLocation = $request->getUrl(); // Creating a new instance of the request object. $request = clone $request; // Setting the new location $request->setUrl(Uri\resolve( $oldLocation, $response->getHeader('Location') )); $doRedirect = true; $redirects++; } // This was a HTTP error if ($code >= 400) { $this->emit('error', [$request, $response, &$retry, $retryCount]); $this->emit('error:' . $code, [$request, $response, &$retry, $retryCount]); } } catch (ClientException $e) { $this->emit('exception', [$request, $e, &$retry, $retryCount]); // If retry was still set to false, it means no event handler // dealt with the problem. In this case we just re-throw the // exception. if (!$retry) { throw $e; } } if ($retry) { $retryCount++; } } while ($retry || $doRedirect); $this->emit('afterRequest', [$request, $response]); if ($this->throwExceptions && $code >= 400) { throw new ClientHttpException($response); } return $response; } /** * Sends a HTTP request asynchronously. * * Due to the nature of PHP, you must from time to time poll to see if any * new responses came in. * * After calling sendAsync, you must therefore occasionally call the poll() * method, or wait(). * * @param RequestInterface $request * @param callable $success * @param callable $error * @return void */ function sendAsync(RequestInterface $request, callable $success = null, callable $error = null) { $this->emit('beforeRequest', [$request]); $this->sendAsyncInternal($request, $success, $error); $this->poll(); } /** * This method checks if any http requests have gotten results, and if so, * call the appropriate success or error handlers. * * This method will return true if there are still requests waiting to * return, and false if all the work is done. * * @return bool */ function poll() { // nothing to do? if (!$this->curlMultiMap) { return false; } do { $r = curl_multi_exec( $this->curlMultiHandle, $stillRunning ); } while ($r === CURLM_CALL_MULTI_PERFORM); do { messageQueue: $status = curl_multi_info_read( $this->curlMultiHandle, $messagesInQueue ); if ($status && $status['msg'] === CURLMSG_DONE) { $resourceId = intval($status['handle']); list( $request, $successCallback, $errorCallback, $retryCount, ) = $this->curlMultiMap[$resourceId]; unset($this->curlMultiMap[$resourceId]); $curlResult = $this->parseCurlResult(curl_multi_getcontent($status['handle']), $status['handle']); $retry = false; if ($curlResult['status'] === self::STATUS_CURLERROR) { $e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']); $this->emit('exception', [$request, $e, &$retry, $retryCount]); if ($retry) { $retryCount++; $this->sendASyncInternal($request, $successCallback, $errorCallback, $retryCount); goto messageQueue; } $curlResult['request'] = $request; if ($errorCallback) { $errorCallback($curlResult); } } elseif ($curlResult['status'] === self::STATUS_HTTPERROR) { $this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]); $this->emit('error:' . $curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]); if ($retry) { $retryCount++; $this->sendASyncInternal($request, $successCallback, $errorCallback, $retryCount); goto messageQueue; } $curlResult['request'] = $request; if ($errorCallback) { $errorCallback($curlResult); } } else { $this->emit('afterRequest', [$request, $curlResult['response']]); if ($successCallback) { $successCallback($curlResult['response']); } } } } while ($messagesInQueue > 0); return count($this->curlMultiMap) > 0; } /** * Processes every HTTP request in the queue, and waits till they are all * completed. * * @return void */ function wait() { do { curl_multi_select($this->curlMultiHandle); $stillRunning = $this->poll(); } while ($stillRunning); } /** * If this is set to true, the Client will automatically throw exceptions * upon HTTP errors. * * This means that if a response came back with a status code greater than * or equal to 400, we will throw a ClientHttpException. * * This only works for the send() method. Throwing exceptions for * sendAsync() is not supported. * * @param bool $throwExceptions * @return void */ function setThrowExceptions($throwExceptions) { $this->throwExceptions = $throwExceptions; } /** * Adds a CURL setting. * * These settings will be included in every HTTP request. * * @param int $name * @param mixed $value * @return void */ function addCurlSetting($name, $value) { $this->curlSettings[$name] = $value; } /** * This method is responsible for performing a single request. * * @param RequestInterface $request * @return ResponseInterface */ protected function doRequest(RequestInterface $request) { $settings = $this->createCurlSettingsArray($request); if (!$this->curlHandle) { $this->curlHandle = curl_init(); } curl_setopt_array($this->curlHandle, $settings); $response = $this->curlExec($this->curlHandle); $response = $this->parseCurlResult($response, $this->curlHandle); if ($response['status'] === self::STATUS_CURLERROR) { throw new ClientException($response['curl_errmsg'], $response['curl_errno']); } return $response['response']; } /** * Cached curl handle. * * By keeping this resource around for the lifetime of this object, things * like persistent connections are possible. * * @var resource */ private $curlHandle; /** * Handler for curl_multi requests. * * The first time sendAsync is used, this will be created. * * @var resource */ private $curlMultiHandle; /** * Has a list of curl handles, as well as their associated success and * error callbacks. * * @var array */ private $curlMultiMap = []; /** * Turns a RequestInterface object into an array with settings that can be * fed to curl_setopt * * @param RequestInterface $request * @return array */ protected function createCurlSettingsArray(RequestInterface $request) { $settings = $this->curlSettings; switch ($request->getMethod()) { case 'HEAD' : $settings[CURLOPT_NOBODY] = true; $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD'; $settings[CURLOPT_POSTFIELDS] = ''; $settings[CURLOPT_PUT] = false; break; case 'GET' : $settings[CURLOPT_CUSTOMREQUEST] = 'GET'; $settings[CURLOPT_POSTFIELDS] = ''; $settings[CURLOPT_PUT] = false; break; default : $body = $request->getBody(); if (is_resource($body)) { // This needs to be set to PUT, regardless of the actual // method used. Without it, INFILE will be ignored for some // reason. $settings[CURLOPT_PUT] = true; $settings[CURLOPT_INFILE] = $request->getBody(); } else { // For security we cast this to a string. If somehow an array could // be passed here, it would be possible for an attacker to use @ to // post local files. $settings[CURLOPT_POSTFIELDS] = (string)$body; } $settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); break; } $nHeaders = []; foreach ($request->getHeaders() as $key => $values) { foreach ($values as $value) { $nHeaders[] = $key . ': ' . $value; } } $settings[CURLOPT_HTTPHEADER] = $nHeaders; $settings[CURLOPT_URL] = $request->getUrl(); // FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM if (defined('CURLOPT_PROTOCOLS')) { $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; } // FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM if (defined('CURLOPT_REDIR_PROTOCOLS')) { $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; } return $settings; } const STATUS_SUCCESS = 0; const STATUS_CURLERROR = 1; const STATUS_HTTPERROR = 2; /** * Parses the result of a curl call in a format that's a bit more * convenient to work with. * * The method returns an array with the following elements: * * status - one of the 3 STATUS constants. * * curl_errno - A curl error number. Only set if status is * STATUS_CURLERROR. * * curl_errmsg - A current error message. Only set if status is * STATUS_CURLERROR. * * response - Response object. Only set if status is STATUS_SUCCESS, or * STATUS_HTTPERROR. * * http_code - HTTP status code, as an int. Only set if Only set if * status is STATUS_SUCCESS, or STATUS_HTTPERROR * * @param string $response * @param resource $curlHandle * @return Response */ protected function parseCurlResult($response, $curlHandle) { list( $curlInfo, $curlErrNo, $curlErrMsg ) = $this->curlStuff($curlHandle); if ($curlErrNo) { return [ 'status' => self::STATUS_CURLERROR, 'curl_errno' => $curlErrNo, 'curl_errmsg' => $curlErrMsg, ]; } $headerBlob = substr($response, 0, $curlInfo['header_size']); // In the case of 204 No Content, strlen($response) == $curlInfo['header_size]. // This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL // An exception will be thrown when calling getBodyAsString then $responseBody = substr($response, $curlInfo['header_size']) ?: null; unset($response); // In the case of 100 Continue, or redirects we'll have multiple lists // of headers for each separate HTTP response. We can easily split this // because they are separated by \r\n\r\n $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n")); // We only care about the last set of headers $headerBlob = $headerBlob[count($headerBlob) - 1]; // Splitting headers $headerBlob = explode("\r\n", $headerBlob); $response = new Response(); $response->setStatus($curlInfo['http_code']); foreach ($headerBlob as $header) { $parts = explode(':', $header, 2); if (count($parts) == 2) { $response->addHeader(trim($parts[0]), trim($parts[1])); } } $response->setBody($responseBody); $httpCode = intval($response->getStatus()); return [ 'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS, 'response' => $response, 'http_code' => $httpCode, ]; } /** * Sends an asynchronous HTTP request. * * We keep this in a separate method, so we can call it without triggering * the beforeRequest event and don't do the poll(). * * @param RequestInterface $request * @param callable $success * @param callable $error * @param int $retryCount */ protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, $retryCount = 0) { if (!$this->curlMultiHandle) { $this->curlMultiHandle = curl_multi_init(); } $curl = curl_init(); curl_setopt_array( $curl, $this->createCurlSettingsArray($request) ); curl_multi_add_handle($this->curlMultiHandle, $curl); $this->curlMultiMap[intval($curl)] = [ $request, $success, $error, $retryCount ]; } // @codeCoverageIgnoreStart /** * Calls curl_exec * * This method exists so it can easily be overridden and mocked. * * @param resource $curlHandle * @return string */ protected function curlExec($curlHandle) { return curl_exec($curlHandle); } /** * Returns a bunch of information about a curl request. * * This method exists so it can easily be overridden and mocked. * * @param resource $curlHandle * @return array */ protected function curlStuff($curlHandle) { return [ curl_getinfo($curlHandle), curl_errno($curlHandle), curl_error($curlHandle), ]; } // @codeCoverageIgnoreEnd }