Blame | Last modification | View Log | RSS feed
<?phpnamespace Guzzle\Http\Curl;use Guzzle\Common\Exception\InvalidArgumentException;use Guzzle\Common\Exception\RuntimeException;use Guzzle\Common\Collection;use Guzzle\Http\Message\EntityEnclosingRequest;use Guzzle\Http\Message\RequestInterface;use Guzzle\Parser\ParserRegistry;use Guzzle\Http\Url;/*** Immutable wrapper for a cURL handle*/class CurlHandle{const BODY_AS_STRING = 'body_as_string';const PROGRESS = 'progress';const DEBUG = 'debug';/** @var Collection Curl options */protected $options;/** @var resource Curl resource handle */protected $handle;/** @var int CURLE_* error */protected $errorNo = CURLE_OK;/*** Factory method to create a new curl handle based on an HTTP request.** There are some helpful options you can set to enable specific behavior:* - debug: Set to true to enable cURL debug functionality to track the actual headers sent over the wire.* - progress: Set to true to enable progress function callbacks.** @param RequestInterface $request Request** @return CurlHandle* @throws RuntimeException*/public static function factory(RequestInterface $request){$requestCurlOptions = $request->getCurlOptions();$mediator = new RequestMediator($request, $requestCurlOptions->get('emit_io'));$tempContentLength = null;$method = $request->getMethod();$bodyAsString = $requestCurlOptions->get(self::BODY_AS_STRING);// Prepare url$url = (string)$request->getUrl();if(($pos = strpos($url, '#')) !== false ){// strip fragment from url$url = substr($url, 0, $pos);}// Array of default cURL options.$curlOptions = array(CURLOPT_URL => $url,CURLOPT_CONNECTTIMEOUT => 150,CURLOPT_RETURNTRANSFER => false,CURLOPT_HEADER => false,CURLOPT_PORT => $request->getPort(),CURLOPT_HTTPHEADER => array(),CURLOPT_WRITEFUNCTION => array($mediator, 'writeResponseBody'),CURLOPT_HEADERFUNCTION => array($mediator, 'receiveResponseHeader'),CURLOPT_HTTP_VERSION => $request->getProtocolVersion() === '1.0'? CURL_HTTP_VERSION_1_0 : CURL_HTTP_VERSION_1_1,// Verifies the authenticity of the peer's certificateCURLOPT_SSL_VERIFYPEER => 1,// Certificate must indicate that the server is the server to which you meant to connectCURLOPT_SSL_VERIFYHOST => 2);if (defined('CURLOPT_PROTOCOLS')) {// Allow only HTTP and HTTPS protocols$curlOptions[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;}// Add CURLOPT_ENCODING if Accept-Encoding header is providedif ($acceptEncodingHeader = $request->getHeader('Accept-Encoding')) {$curlOptions[CURLOPT_ENCODING] = (string) $acceptEncodingHeader;// Let cURL set the Accept-Encoding header, prevents duplicate values$request->removeHeader('Accept-Encoding');}// Enable curl debug information if the 'debug' param was setif ($requestCurlOptions->get('debug')) {$curlOptions[CURLOPT_STDERR] = fopen('php://temp', 'r+');// @codeCoverageIgnoreStartif (false === $curlOptions[CURLOPT_STDERR]) {throw new RuntimeException('Unable to create a stream for CURLOPT_STDERR');}// @codeCoverageIgnoreEnd$curlOptions[CURLOPT_VERBOSE] = true;}// Specify settings according to the HTTP methodif ($method == 'GET') {$curlOptions[CURLOPT_HTTPGET] = true;} elseif ($method == 'HEAD') {$curlOptions[CURLOPT_NOBODY] = true;// HEAD requests do not use a write functionunset($curlOptions[CURLOPT_WRITEFUNCTION]);} elseif (!($request instanceof EntityEnclosingRequest)) {$curlOptions[CURLOPT_CUSTOMREQUEST] = $method;} else {$curlOptions[CURLOPT_CUSTOMREQUEST] = $method;// Handle sending raw bodies in a requestif ($request->getBody()) {// You can send the body as a string using curl's CURLOPT_POSTFIELDSif ($bodyAsString) {$curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getBody();// Allow curl to add the Content-Length for us to account for the times when// POST redirects are followed by GET requestsif ($tempContentLength = $request->getHeader('Content-Length')) {$tempContentLength = (int) (string) $tempContentLength;}// Remove the curl generated Content-Type header if none was set manuallyif (!$request->hasHeader('Content-Type')) {$curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:';}} else {$curlOptions[CURLOPT_UPLOAD] = true;// Let cURL handle setting the Content-Length headerif ($tempContentLength = $request->getHeader('Content-Length')) {$tempContentLength = (int) (string) $tempContentLength;$curlOptions[CURLOPT_INFILESIZE] = $tempContentLength;}// Add a callback for curl to read data to send with the request only if a body was specified$curlOptions[CURLOPT_READFUNCTION] = array($mediator, 'readRequestBody');// Attempt to seek to the start of the stream$request->getBody()->seek(0);}} else {// Special handling for POST specific fields and files$postFields = false;if (count($request->getPostFiles())) {$postFields = $request->getPostFields()->useUrlEncoding(false)->urlEncode();foreach ($request->getPostFiles() as $key => $data) {$prefixKeys = count($data) > 1;foreach ($data as $index => $file) {// Allow multiple files in the same key$fieldKey = $prefixKeys ? "{$key}[{$index}]" : $key;$postFields[$fieldKey] = $file->getCurlValue();}}} elseif (count($request->getPostFields())) {$postFields = (string) $request->getPostFields()->useUrlEncoding(true);}if ($postFields !== false) {if ($method == 'POST') {unset($curlOptions[CURLOPT_CUSTOMREQUEST]);$curlOptions[CURLOPT_POST] = true;}$curlOptions[CURLOPT_POSTFIELDS] = $postFields;$request->removeHeader('Content-Length');}}// If the Expect header is not present, prevent curl from adding itif (!$request->hasHeader('Expect')) {$curlOptions[CURLOPT_HTTPHEADER][] = 'Expect:';}}// If a Content-Length header was specified but we want to allow curl to set one for usif (null !== $tempContentLength) {$request->removeHeader('Content-Length');}// Set custom cURL optionsforeach ($requestCurlOptions->toArray() as $key => $value) {if (is_numeric($key)) {$curlOptions[$key] = $value;}}// Do not set an Accept header by defaultif (!isset($curlOptions[CURLOPT_ENCODING])) {$curlOptions[CURLOPT_HTTPHEADER][] = 'Accept:';}// Add any custom headers to the request. Empty headers will cause curl to not send the header at all.foreach ($request->getHeaderLines() as $line) {$curlOptions[CURLOPT_HTTPHEADER][] = $line;}// Add the content-length header back if it was temporarily removedif ($tempContentLength) {$request->setHeader('Content-Length', $tempContentLength);}// Apply the options to a new cURL handle.$handle = curl_init();// Enable the progress function if the 'progress' param was setif ($requestCurlOptions->get('progress')) {// Wrap the function in a function that provides the curl handle to the mediator's progress function// Using this rather than injecting the handle into the mediator prevents a circular reference$curlOptions[CURLOPT_PROGRESSFUNCTION] = function () use ($mediator, $handle) {$args = func_get_args();$args[] = $handle;// PHP 5.5 pushed the handle onto the start of the argsif (is_resource($args[0])) {array_shift($args);}call_user_func_array(array($mediator, 'progress'), $args);};$curlOptions[CURLOPT_NOPROGRESS] = false;}curl_setopt_array($handle, $curlOptions);return new static($handle, $curlOptions);}/*** Construct a new CurlHandle object that wraps a cURL handle** @param resource $handle Configured cURL handle resource* @param Collection|array $options Curl options to use with the handle** @throws InvalidArgumentException*/public function __construct($handle, $options){if (!is_resource($handle)) {throw new InvalidArgumentException('Invalid handle provided');}if (is_array($options)) {$this->options = new Collection($options);} elseif ($options instanceof Collection) {$this->options = $options;} else {throw new InvalidArgumentException('Expected array or Collection');}$this->handle = $handle;}/*** Destructor*/public function __destruct(){$this->close();}/*** Close the curl handle*/public function close(){if (is_resource($this->handle)) {curl_close($this->handle);}$this->handle = null;}/*** Check if the handle is available and still OK** @return bool*/public function isAvailable(){return is_resource($this->handle);}/*** Get the last error that occurred on the cURL handle** @return string*/public function getError(){return $this->isAvailable() ? curl_error($this->handle) : '';}/*** Get the last error number that occurred on the cURL handle** @return int*/public function getErrorNo(){if ($this->errorNo) {return $this->errorNo;}return $this->isAvailable() ? curl_errno($this->handle) : CURLE_OK;}/*** Set the curl error number** @param int $error Error number to set** @return CurlHandle*/public function setErrorNo($error){$this->errorNo = $error;return $this;}/*** Get cURL curl_getinfo data** @param int $option Option to retrieve. Pass null to retrieve all data as an array.** @return array|mixed*/public function getInfo($option = null){if (!is_resource($this->handle)) {return null;}if (null !== $option) {return curl_getinfo($this->handle, $option) ?: null;}return curl_getinfo($this->handle) ?: array();}/*** Get the stderr output** @param bool $asResource Set to TRUE to get an fopen resource** @return string|resource|null*/public function getStderr($asResource = false){$stderr = $this->getOptions()->get(CURLOPT_STDERR);if (!$stderr) {return null;}if ($asResource) {return $stderr;}fseek($stderr, 0);$e = stream_get_contents($stderr);fseek($stderr, 0, SEEK_END);return $e;}/*** Get the URL that this handle is connecting to** @return Url*/public function getUrl(){return Url::factory($this->options->get(CURLOPT_URL));}/*** Get the wrapped curl handle** @return resource|null Returns the cURL handle or null if it was closed*/public function getHandle(){return $this->isAvailable() ? $this->handle : null;}/*** Get the cURL setopt options of the handle. Changing values in the return object will have no effect on the curl* handle after it is created.** @return Collection*/public function getOptions(){return $this->options;}/*** Update a request based on the log messages of the CurlHandle** @param RequestInterface $request Request to update*/public function updateRequestFromTransfer(RequestInterface $request){if (!$request->getResponse()) {return;}// Update the transfer stats of the response$request->getResponse()->setInfo($this->getInfo());if (!$log = $this->getStderr(true)) {return;}// Parse the cURL stderr output for outgoing requests$headers = '';fseek($log, 0);while (($line = fgets($log)) !== false) {if ($line && $line[0] == '>') {$headers = substr(trim($line), 2) . "\r\n";while (($line = fgets($log)) !== false) {if ($line[0] == '*' || $line[0] == '<') {break;} else {$headers .= trim($line) . "\r\n";}}}}// Add request headers to the request exactly as they were sentif ($headers) {$parsed = ParserRegistry::getInstance()->getParser('message')->parseRequest($headers);if (!empty($parsed['headers'])) {$request->setHeaders(array());foreach ($parsed['headers'] as $name => $value) {$request->setHeader($name, $value);}}if (!empty($parsed['version'])) {$request->setProtocolVersion($parsed['version']);}}}/*** Parse the config and replace curl.* configurators into the constant based values so it can be used elsewhere** @param array|Collection $config The configuration we want to parse** @return array*/public static function parseCurlConfig($config){$curlOptions = array();foreach ($config as $key => $value) {if (is_string($key) && defined($key)) {// Convert constants represented as string to constant int values$key = constant($key);}if (is_string($value) && defined($value)) {$value = constant($value);}$curlOptions[$key] = $value;}return $curlOptions;}}