*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Curl.php 278226 2009-04-03 21:32:48Z avb $\r
+ * @version SVN: $Id: Curl.php 310800 2011-05-06 07:29:56Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
*/\r
class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter\r
{\r
'ssl_passphrase' => CURLOPT_SSLCERTPASSWD\r
);\r
\r
+ /**\r
+ * Mapping of CURLE_* constants to Exception subclasses and error codes\r
+ * @var array\r
+ */\r
+ protected static $errorMap = array(\r
+ CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+ CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),\r
+ // error returned from write callback\r
+ CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+ CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::TIMEOUT),\r
+ CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),\r
+ CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+ CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::TOO_MANY_REDIRECTS),\r
+ CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),\r
+ CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),\r
+ CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),\r
+ CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT),\r
+ CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),\r
+ );\r
+\r
/**\r
* Response being received\r
* @var HTTP_Request2_Response\r
*/\r
protected $lastInfo;\r
\r
+ /**\r
+ * Creates a subclass of HTTP_Request2_Exception from curl error data\r
+ *\r
+ * @param resource curl handle\r
+ * @return HTTP_Request2_Exception\r
+ */\r
+ protected static function wrapCurlError($ch)\r
+ {\r
+ $nativeCode = curl_errno($ch);\r
+ $message = 'Curl error: ' . curl_error($ch);\r
+ if (!isset(self::$errorMap[$nativeCode])) {\r
+ return new HTTP_Request2_Exception($message, 0, $nativeCode);\r
+ } else {\r
+ $class = self::$errorMap[$nativeCode][0];\r
+ $code = empty(self::$errorMap[$nativeCode][1])\r
+ ? 0 : self::$errorMap[$nativeCode][1];\r
+ return new $class($message, $code, $nativeCode);\r
+ }\r
+ }\r
+\r
/**\r
* Sends request to the remote server and returns its response\r
*\r
public function sendRequest(HTTP_Request2 $request)\r
{\r
if (!extension_loaded('curl')) {\r
- throw new HTTP_Request2_Exception('cURL extension not available');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
}\r
\r
$this->request = $request;\r
\r
try {\r
if (false === curl_exec($ch = $this->createCurlHandle())) {\r
- $errorMessage = 'Error sending request: #' . curl_errno($ch) .\r
- ' ' . curl_error($ch);\r
+ $e = self::wrapCurlError($ch);\r
}\r
} catch (Exception $e) {\r
}\r
- $this->lastInfo = curl_getinfo($ch);\r
- curl_close($ch);\r
+ if (isset($ch)) {\r
+ $this->lastInfo = curl_getinfo($ch);\r
+ curl_close($ch);\r
+ }\r
+\r
+ $response = $this->response;\r
+ unset($this->request, $this->requestBody, $this->response);\r
\r
if (!empty($e)) {\r
throw $e;\r
- } elseif (!empty($errorMessage)) {\r
- throw new HTTP_Request2_Exception($errorMessage);\r
+ }\r
+\r
+ if ($jar = $request->getCookieJar()) {\r
+ $jar->addCookiesFromResponse($response, $request->getUrl());\r
}\r
\r
if (0 < $this->lastInfo['size_download']) {\r
- $this->request->setLastEvent('receivedBody', $this->response);\r
+ $request->setLastEvent('receivedBody', $response);\r
}\r
- return $this->response;\r
+ return $response;\r
}\r
\r
/**\r
* Creates a new cURL handle and populates it with data from the request\r
*\r
* @return resource a cURL handle, as created by curl_init()\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
protected function createCurlHandle()\r
{\r
$ch = curl_init();\r
\r
curl_setopt_array($ch, array(\r
- // setup callbacks\r
- CURLOPT_READFUNCTION => array($this, 'callbackReadBody'),\r
+ // setup write callbacks\r
CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),\r
CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),\r
- // disallow redirects\r
- CURLOPT_FOLLOWLOCATION => false,\r
// buffer size\r
CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),\r
// connection timeout\r
CURLOPT_URL => $this->request->getUrl()->getUrl()\r
));\r
\r
+ // set up redirects\r
+ if (!$this->request->getConfig('follow_redirects')) {\r
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\r
+ } else {\r
+ if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
+ }\r
+ curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));\r
+ // limit redirects to http(s), works in 5.2.10+\r
+ if (defined('CURLOPT_REDIR_PROTOCOLS')) {\r
+ curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);\r
+ }\r
+ // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571\r
+ if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {\r
+ curl_setopt($ch, CURLOPT_POSTREDIR, 3);\r
+ }\r
+ }\r
+\r
// request timeout\r
if ($timeout = $this->request->getConfig('timeout')) {\r
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);\r
case HTTP_Request2::METHOD_POST:\r
curl_setopt($ch, CURLOPT_POST, true);\r
break;\r
+ case HTTP_Request2::METHOD_HEAD:\r
+ curl_setopt($ch, CURLOPT_NOBODY, true);\r
+ break;\r
+ case HTTP_Request2::METHOD_PUT:\r
+ curl_setopt($ch, CURLOPT_UPLOAD, true);\r
+ break;\r
default:\r
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());\r
}\r
// set proxy, if needed\r
if ($host = $this->request->getConfig('proxy_host')) {\r
if (!($port = $this->request->getConfig('proxy_port'))) {\r
- throw new HTTP_Request2_Exception('Proxy port not provided');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE\r
+ );\r
}\r
curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);\r
if ($user = $this->request->getConfig('proxy_user')) {\r
}\r
\r
// set SSL options\r
- if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) {\r
- foreach ($this->request->getConfig() as $name => $value) {\r
- if ('ssl_verify_host' == $name && null !== $value) {\r
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
- } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
- curl_setopt($ch, self::$sslContextMap[$name], $value);\r
- }\r
+ foreach ($this->request->getConfig() as $name => $value) {\r
+ if ('ssl_verify_host' == $name && null !== $value) {\r
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
+ } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
+ curl_setopt($ch, self::$sslContextMap[$name], $value);\r
}\r
}\r
\r
$headers['accept-encoding'] = '';\r
}\r
\r
+ if (($jar = $this->request->getCookieJar())\r
+ && ($cookies = $jar->getMatching($this->request->getUrl(), true))\r
+ ) {\r
+ $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;\r
+ }\r
+\r
// set headers having special cURL keys\r
foreach (self::$headerMap as $name => $option) {\r
if (isset($headers[$name])) {\r
}\r
\r
$this->calculateRequestLength($headers);\r
+ if (isset($headers['content-length'])) {\r
+ $this->workaroundPhpBug47204($ch, $headers);\r
+ }\r
\r
// set headers not having special keys\r
$headersFmt = array();\r
return $ch;\r
}\r
\r
+ /**\r
+ * Workaround for PHP bug #47204 that prevents rewinding request body\r
+ *\r
+ * The workaround consists of reading the entire request body into memory\r
+ * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large\r
+ * file uploads, use Socket adapter instead.\r
+ *\r
+ * @param resource cURL handle\r
+ * @param array Request headers\r
+ */\r
+ protected function workaroundPhpBug47204($ch, &$headers)\r
+ {\r
+ // no redirects, no digest auth -> probably no rewind needed\r
+ if (!$this->request->getConfig('follow_redirects')\r
+ && (!($auth = $this->request->getAuth())\r
+ || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])\r
+ ) {\r
+ curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));\r
+\r
+ // rewind may be needed, read the whole body into memory\r
+ } else {\r
+ if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {\r
+ $this->requestBody = $this->requestBody->__toString();\r
+\r
+ } elseif (is_resource($this->requestBody)) {\r
+ $fp = $this->requestBody;\r
+ $this->requestBody = '';\r
+ while (!feof($fp)) {\r
+ $this->requestBody .= fread($fp, 16384);\r
+ }\r
+ }\r
+ // curl hangs up if content-length is present\r
+ unset($headers['content-length']);\r
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);\r
+ }\r
+ }\r
+\r
/**\r
* Callback function called by cURL for reading the request body\r
*\r
* @param resource cURL handle\r
* @param resource file descriptor (not used)\r
* @param integer maximum length of data to return\r
- * @return string part of the request body, up to $length bytes \r
+ * @return string part of the request body, up to $length bytes\r
*/\r
protected function callbackReadBody($ch, $fd, $length)\r
{\r
'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
);\r
}\r
+ $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);\r
+ // if body wasn't read by a callback, send event with total body size\r
+ if ($upload > $this->position) {\r
+ $this->request->setLastEvent(\r
+ 'sentBodyPart', $upload - $this->position\r
+ );\r
+ $this->position = $upload;\r
+ }\r
+ if ($upload && (!$this->eventSentHeaders\r
+ || $this->response->getStatus() >= 200)\r
+ ) {\r
+ $this->request->setLastEvent('sentBody', $upload);\r
+ }\r
$this->eventSentHeaders = true;\r
// we'll need a new response object\r
if ($this->eventReceivedHeaders) {\r
}\r
}\r
if (empty($this->response)) {\r
- $this->response = new HTTP_Request2_Response($string, false);\r
+ $this->response = new HTTP_Request2_Response(\r
+ $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)\r
+ );\r
} else {\r
$this->response->parseHeaderLine($string);\r
if ('' == trim($string)) {\r
if (200 <= $this->response->getStatus()) {\r
$this->request->setLastEvent('receivedHeaders', $this->response);\r
}\r
+\r
+ if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {\r
+ $redirectUrl = new Net_URL2($this->response->getHeader('location'));\r
+\r
+ // for versions lower than 5.2.10, check the redirection URL protocol\r
+ if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()\r
+ && !in_array($redirectUrl->getScheme(), array('http', 'https'))\r
+ ) {\r
+ return -1;\r
+ }\r
+\r
+ if ($jar = $this->request->getCookieJar()) {\r
+ $jar->addCookiesFromResponse($this->response, $this->request->getUrl());\r
+ if (!$redirectUrl->isAbsolute()) {\r
+ $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);\r
+ }\r
+ if ($cookies = $jar->getMatching($redirectUrl, true)) {\r
+ curl_setopt($ch, CURLOPT_COOKIE, $cookies);\r
+ }\r
+ }\r
+ }\r
$this->eventReceivedHeaders = true;\r
}\r
}\r
*/\r
protected function callbackWriteBody($ch, $string)\r
{\r
- // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if \r
+ // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if\r
// response doesn't start with proper HTTP status line (see bug #15716)\r
if (empty($this->response)) {\r
- throw new HTTP_Request2_Exception("Malformed response: {$string}");\r
+ throw new HTTP_Request2_MessageException(\r
+ "Malformed response: {$string}",\r
+ HTTP_Request2_Exception::MALFORMED_RESPONSE\r
+ );\r
}\r
if ($this->request->getConfig('store_body')) {\r
$this->response->appendBody($string);\r