6 * @subpackage Transport
13 * @subpackage Transport
15 class Requests_Transport_cURL implements Requests_Transport {
16 const CURL_7_10_5 = 0x070A05;
17 const CURL_7_16_2 = 0x071002;
31 public $response_data = '';
34 * Information on the current request
36 * @var array cURL information array, see {@see http://php.net/curl_getinfo}
55 * Have we finished the headers yet?
59 protected $done_headers = false;
62 * If streaming to a file, keep the file pointer
66 protected $stream_handle;
69 * How many bytes are in the response body?
73 protected $response_bytes;
76 * What's the maximum number of bytes we should keep?
78 * @var int|bool Byte count, or false if no limit.
80 protected $response_byte_limit;
85 public function __construct() {
86 $curl = curl_version();
87 $this->version = $curl['version_number'];
88 $this->fp = curl_init();
90 curl_setopt($this->fp, CURLOPT_HEADER, false);
91 curl_setopt($this->fp, CURLOPT_RETURNTRANSFER, 1);
92 if ($this->version >= self::CURL_7_10_5) {
93 curl_setopt($this->fp, CURLOPT_ENCODING, '');
95 if (defined('CURLOPT_PROTOCOLS')) {
96 curl_setopt($this->fp, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
98 if (defined('CURLOPT_REDIR_PROTOCOLS')) {
99 curl_setopt($this->fp, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
106 * @throws Requests_Exception On a cURL error (`curlerror`)
108 * @param string $url URL to request
109 * @param array $headers Associative array of request headers
110 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
111 * @param array $options Request options, see {@see Requests::response()} for documentation
112 * @return string Raw HTTP result
114 public function request($url, $headers = array(), $data = array(), $options = array()) {
115 $this->setup_handle($url, $headers, $data, $options);
117 $options['hooks']->dispatch('curl.before_send', array(&$this->fp));
119 if ($options['filename'] !== false) {
120 $this->stream_handle = fopen($options['filename'], 'wb');
123 $this->response_data = '';
124 $this->response_bytes = 0;
125 $this->response_byte_limit = false;
126 if ($options['max_bytes'] !== false) {
127 $this->response_byte_limit = $options['max_bytes'];
130 if (isset($options['verify'])) {
131 if ($options['verify'] === false) {
132 curl_setopt($this->fp, CURLOPT_SSL_VERIFYHOST, 0);
133 curl_setopt($this->fp, CURLOPT_SSL_VERIFYPEER, 0);
135 } elseif (is_string($options['verify'])) {
136 curl_setopt($this->fp, CURLOPT_CAINFO, $options['verify']);
140 if (isset($options['verifyname']) && $options['verifyname'] === false) {
141 curl_setopt($this->fp, CURLOPT_SSL_VERIFYHOST, 0);
144 curl_exec($this->fp);
145 $response = $this->response_data;
147 $options['hooks']->dispatch('curl.after_send', array(&$fake_headers));
149 if (curl_errno($this->fp) === 23 || curl_errno($this->fp) === 61) {
150 // Reset encoding and try again
151 curl_setopt($this->fp, CURLOPT_ENCODING, 'none');
153 $this->response_data = '';
154 $this->response_bytes = 0;
155 curl_exec($this->fp);
156 $response = $this->response_data;
159 $this->process_response($response, $options);
160 curl_close($this->fp);
161 return $this->headers;
165 * Send multiple requests simultaneously
167 * @param array $requests Request data
168 * @param array $options Global options
169 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
171 public function request_multiple($requests, $options) {
172 $multihandle = curl_multi_init();
173 $subrequests = array();
174 $subhandles = array();
176 $class = get_class($this);
177 foreach ($requests as $id => $request) {
178 $subrequests[$id] = new $class();
179 $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
180 $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
181 curl_multi_add_handle($multihandle, $subhandles[$id]);
185 $responses = array();
187 $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle));
193 $status = curl_multi_exec($multihandle, $active);
195 while ($status === CURLM_CALL_MULTI_PERFORM);
197 $to_process = array();
199 // Read the information as needed
200 while ($done = curl_multi_info_read($multihandle)) {
201 $key = array_search($done['handle'], $subhandles, true);
202 if (!isset($to_process[$key])) {
203 $to_process[$key] = $done;
207 // Parse the finished requests before we start getting the new ones
208 foreach ($to_process as $key => $done) {
209 $options = $requests[$key]['options'];
210 $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
212 $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key]));
214 curl_multi_remove_handle($multihandle, $done['handle']);
215 curl_close($done['handle']);
217 if (!is_string($responses[$key])) {
218 $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
223 while ($active || $completed < count($subrequests));
225 $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
227 curl_multi_close($multihandle);
233 * Get the cURL handle for use in a multi-request
235 * @param string $url URL to request
236 * @param array $headers Associative array of request headers
237 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
238 * @param array $options Request options, see {@see Requests::response()} for documentation
239 * @return resource Subrequest's cURL handle
241 public function &get_subrequest_handle($url, $headers, $data, $options) {
242 $this->setup_handle($url, $headers, $data, $options);
244 if ($options['filename'] !== false) {
245 $this->stream_handle = fopen($options['filename'], 'wb');
246 curl_setopt($this->fp, CURLOPT_FILE, $this->stream_handle);
249 $this->response_data = '';
250 $this->response_bytes = 0;
251 $this->response_byte_limit = false;
252 if ($options['max_bytes'] !== false) {
253 $this->response_byte_limit = $options['max_bytes'];
260 * Setup the cURL handle for the given data
262 * @param string $url URL to request
263 * @param array $headers Associative array of request headers
264 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
265 * @param array $options Request options, see {@see Requests::response()} for documentation
267 protected function setup_handle($url, $headers, $data, $options) {
268 $options['hooks']->dispatch('curl.before_request', array(&$this->fp));
270 $headers = Requests::flatten($headers);
271 if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) {
272 $url = self::format_get($url, $data);
274 elseif (!empty($data) && !is_string($data)) {
275 $data = http_build_query($data, null, '&');
278 switch ($options['type']) {
280 curl_setopt($this->fp, CURLOPT_POST, true);
281 curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data);
283 case Requests::PATCH:
285 curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, $options['type']);
286 curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data);
288 case Requests::DELETE:
289 curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, 'DELETE');
292 curl_setopt($this->fp, CURLOPT_NOBODY, true);
296 if( is_int($options['timeout']) or $this->version < self::CURL_7_16_2 ) {
297 curl_setopt($this->fp, CURLOPT_TIMEOUT, ceil($options['timeout']));
299 curl_setopt($this->fp, CURLOPT_TIMEOUT_MS, round($options['timeout'] * 1000) );
301 if( is_int($options['connect_timeout']) or $this->version < self::CURL_7_16_2 ) {
302 curl_setopt($this->fp, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
304 curl_setopt($this->fp, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
306 curl_setopt($this->fp, CURLOPT_URL, $url);
307 curl_setopt($this->fp, CURLOPT_REFERER, $url);
308 curl_setopt($this->fp, CURLOPT_USERAGENT, $options['useragent']);
309 curl_setopt($this->fp, CURLOPT_HTTPHEADER, $headers);
311 if (true === $options['blocking']) {
312 curl_setopt($this->fp, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
313 curl_setopt($this->fp, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
314 curl_setopt($this->fp, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
318 public function process_response($response, $options) {
319 if ($options['blocking'] === false) {
321 $options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
324 if ($options['filename'] !== false) {
325 fclose($this->stream_handle);
326 $this->headers = trim($this->headers);
329 $this->headers .= $response;
332 if (curl_errno($this->fp)) {
333 throw new Requests_Exception('cURL error ' . curl_errno($this->fp) . ': ' . curl_error($this->fp), 'curlerror', $this->fp);
335 $this->info = curl_getinfo($this->fp);
337 $options['hooks']->dispatch('curl.after_request', array(&$this->headers));
338 return $this->headers;
342 * Collect the headers as they are received
344 * @param resource $handle cURL resource
345 * @param string $headers Header string
346 * @return integer Length of provided header
348 public function stream_headers($handle, $headers) {
349 // Why do we do this? cURL will send both the final response and any
350 // interim responses, such as a 100 Continue. We don't need that.
351 // (We may want to keep this somewhere just in case)
352 if ($this->done_headers) {
354 $this->done_headers = false;
356 $this->headers .= $headers;
358 if ($headers === "\r\n") {
359 $this->done_headers = true;
361 return strlen($headers);
365 * Collect data as it's received
369 * @param resource $handle cURL resource
370 * @param string $data Body data
371 * @return integer Length of provided data
373 protected function stream_body($handle, $data) {
374 $data_length = strlen($data);
376 // Are we limiting the response size?
377 if ($this->response_byte_limit) {
378 if ($this->response_bytes === $this->response_byte_limit) {
379 // Already at maximum, move on
383 if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
385 $limited_length = ($this->response_byte_limit - $this->response_bytes);
386 $data = substr($data, 0, $limited_length);
390 if ($this->stream_handle) {
391 fwrite($this->stream_handle, $data);
394 $this->response_data .= $data;
397 $this->response_bytes += strlen($data);
402 * Format a URL given GET data
405 * @param array|object $data Data to build query using, see {@see http://php.net/http_build_query}
406 * @return string URL with data
408 protected static function format_get($url, $data) {
410 $url_parts = parse_url($url);
411 if (empty($url_parts['query'])) {
412 $query = $url_parts['query'] = '';
415 $query = $url_parts['query'];
418 $query .= '&' . http_build_query($data, null, '&');
419 $query = trim($query, '&');
421 if (empty($url_parts['query'])) {
422 $url .= '?' . $query;
425 $url = str_replace($url_parts['query'], $query, $url);
432 * Whether this transport is valid
434 * @codeCoverageIgnore
435 * @return boolean True if the transport is valid, false otherwise.
437 public static function test($capabilities = array()) {
438 if (!function_exists('curl_init') && !function_exists('curl_exec'))
441 // If needed, check that our installed curl version supports SSL
442 if (isset( $capabilities['ssl'] ) && $capabilities['ssl']) {
443 $curl_version = curl_version();
444 if (!(CURL_VERSION_SSL & $curl_version['features']))