|
1 <?php |
|
2 /** |
|
3 * cURL HTTP transport |
|
4 * |
|
5 * @package Requests |
|
6 * @subpackage Transport |
|
7 */ |
|
8 |
|
9 /** |
|
10 * cURL HTTP transport |
|
11 * |
|
12 * @package Requests |
|
13 * @subpackage Transport |
|
14 */ |
|
15 class Requests_Transport_cURL implements Requests_Transport { |
|
16 const CURL_7_10_5 = 0x070A05; |
|
17 const CURL_7_16_2 = 0x071002; |
|
18 |
|
19 /** |
|
20 * Raw HTTP data |
|
21 * |
|
22 * @var string |
|
23 */ |
|
24 public $headers = ''; |
|
25 |
|
26 /** |
|
27 * Raw body data |
|
28 * |
|
29 * @var string |
|
30 */ |
|
31 public $response_data = ''; |
|
32 |
|
33 /** |
|
34 * Information on the current request |
|
35 * |
|
36 * @var array cURL information array, see {@see https://secure.php.net/curl_getinfo} |
|
37 */ |
|
38 public $info; |
|
39 |
|
40 /** |
|
41 * Version string |
|
42 * |
|
43 * @var long |
|
44 */ |
|
45 public $version; |
|
46 |
|
47 /** |
|
48 * cURL handle |
|
49 * |
|
50 * @var resource |
|
51 */ |
|
52 protected $handle; |
|
53 |
|
54 /** |
|
55 * Hook dispatcher instance |
|
56 * |
|
57 * @var Requests_Hooks |
|
58 */ |
|
59 protected $hooks; |
|
60 |
|
61 /** |
|
62 * Have we finished the headers yet? |
|
63 * |
|
64 * @var boolean |
|
65 */ |
|
66 protected $done_headers = false; |
|
67 |
|
68 /** |
|
69 * If streaming to a file, keep the file pointer |
|
70 * |
|
71 * @var resource |
|
72 */ |
|
73 protected $stream_handle; |
|
74 |
|
75 /** |
|
76 * How many bytes are in the response body? |
|
77 * |
|
78 * @var int |
|
79 */ |
|
80 protected $response_bytes; |
|
81 |
|
82 /** |
|
83 * What's the maximum number of bytes we should keep? |
|
84 * |
|
85 * @var int|bool Byte count, or false if no limit. |
|
86 */ |
|
87 protected $response_byte_limit; |
|
88 |
|
89 /** |
|
90 * Constructor |
|
91 */ |
|
92 public function __construct() { |
|
93 $curl = curl_version(); |
|
94 $this->version = $curl['version_number']; |
|
95 $this->handle = curl_init(); |
|
96 |
|
97 curl_setopt($this->handle, CURLOPT_HEADER, false); |
|
98 curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); |
|
99 if ($this->version >= self::CURL_7_10_5) { |
|
100 curl_setopt($this->handle, CURLOPT_ENCODING, ''); |
|
101 } |
|
102 if (defined('CURLOPT_PROTOCOLS')) { |
|
103 curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); |
|
104 } |
|
105 if (defined('CURLOPT_REDIR_PROTOCOLS')) { |
|
106 curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); |
|
107 } |
|
108 } |
|
109 |
|
110 /** |
|
111 * Destructor |
|
112 */ |
|
113 public function __destruct() { |
|
114 if (is_resource($this->handle)) { |
|
115 curl_close($this->handle); |
|
116 } |
|
117 } |
|
118 |
|
119 /** |
|
120 * Perform a request |
|
121 * |
|
122 * @throws Requests_Exception On a cURL error (`curlerror`) |
|
123 * |
|
124 * @param string $url URL to request |
|
125 * @param array $headers Associative array of request headers |
|
126 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD |
|
127 * @param array $options Request options, see {@see Requests::response()} for documentation |
|
128 * @return string Raw HTTP result |
|
129 */ |
|
130 public function request($url, $headers = array(), $data = array(), $options = array()) { |
|
131 $this->hooks = $options['hooks']; |
|
132 |
|
133 $this->setup_handle($url, $headers, $data, $options); |
|
134 |
|
135 $options['hooks']->dispatch('curl.before_send', array(&$this->handle)); |
|
136 |
|
137 if ($options['filename'] !== false) { |
|
138 $this->stream_handle = fopen($options['filename'], 'wb'); |
|
139 } |
|
140 |
|
141 $this->response_data = ''; |
|
142 $this->response_bytes = 0; |
|
143 $this->response_byte_limit = false; |
|
144 if ($options['max_bytes'] !== false) { |
|
145 $this->response_byte_limit = $options['max_bytes']; |
|
146 } |
|
147 |
|
148 if (isset($options['verify'])) { |
|
149 if ($options['verify'] === false) { |
|
150 curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); |
|
151 curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0); |
|
152 } |
|
153 elseif (is_string($options['verify'])) { |
|
154 curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']); |
|
155 } |
|
156 } |
|
157 |
|
158 if (isset($options['verifyname']) && $options['verifyname'] === false) { |
|
159 curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); |
|
160 } |
|
161 |
|
162 curl_exec($this->handle); |
|
163 $response = $this->response_data; |
|
164 |
|
165 $options['hooks']->dispatch('curl.after_send', array()); |
|
166 |
|
167 if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) { |
|
168 // Reset encoding and try again |
|
169 curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); |
|
170 |
|
171 $this->response_data = ''; |
|
172 $this->response_bytes = 0; |
|
173 curl_exec($this->handle); |
|
174 $response = $this->response_data; |
|
175 } |
|
176 |
|
177 $this->process_response($response, $options); |
|
178 |
|
179 // Need to remove the $this reference from the curl handle. |
|
180 // Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called. |
|
181 curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null); |
|
182 curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null); |
|
183 |
|
184 return $this->headers; |
|
185 } |
|
186 |
|
187 /** |
|
188 * Send multiple requests simultaneously |
|
189 * |
|
190 * @param array $requests Request data |
|
191 * @param array $options Global options |
|
192 * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) |
|
193 */ |
|
194 public function request_multiple($requests, $options) { |
|
195 // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ |
|
196 if (empty($requests)) { |
|
197 return array(); |
|
198 } |
|
199 |
|
200 $multihandle = curl_multi_init(); |
|
201 $subrequests = array(); |
|
202 $subhandles = array(); |
|
203 |
|
204 $class = get_class($this); |
|
205 foreach ($requests as $id => $request) { |
|
206 $subrequests[$id] = new $class(); |
|
207 $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); |
|
208 $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id])); |
|
209 curl_multi_add_handle($multihandle, $subhandles[$id]); |
|
210 } |
|
211 |
|
212 $completed = 0; |
|
213 $responses = array(); |
|
214 |
|
215 $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle)); |
|
216 |
|
217 do { |
|
218 $active = false; |
|
219 |
|
220 do { |
|
221 $status = curl_multi_exec($multihandle, $active); |
|
222 } |
|
223 while ($status === CURLM_CALL_MULTI_PERFORM); |
|
224 |
|
225 $to_process = array(); |
|
226 |
|
227 // Read the information as needed |
|
228 while ($done = curl_multi_info_read($multihandle)) { |
|
229 $key = array_search($done['handle'], $subhandles, true); |
|
230 if (!isset($to_process[$key])) { |
|
231 $to_process[$key] = $done; |
|
232 } |
|
233 } |
|
234 |
|
235 // Parse the finished requests before we start getting the new ones |
|
236 foreach ($to_process as $key => $done) { |
|
237 $options = $requests[$key]['options']; |
|
238 if (CURLE_OK !== $done['result']) { |
|
239 //get error string for handle. |
|
240 $reason = curl_error($done['handle']); |
|
241 $exception = new Requests_Exception_Transport_cURL( |
|
242 $reason, |
|
243 Requests_Exception_Transport_cURL::EASY, |
|
244 $done['handle'], |
|
245 $done['result'] |
|
246 ); |
|
247 $responses[$key] = $exception; |
|
248 $options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key])); |
|
249 } |
|
250 else { |
|
251 $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); |
|
252 |
|
253 $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key])); |
|
254 } |
|
255 |
|
256 curl_multi_remove_handle($multihandle, $done['handle']); |
|
257 curl_close($done['handle']); |
|
258 |
|
259 if (!is_string($responses[$key])) { |
|
260 $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key)); |
|
261 } |
|
262 $completed++; |
|
263 } |
|
264 } |
|
265 while ($active || $completed < count($subrequests)); |
|
266 |
|
267 $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle)); |
|
268 |
|
269 curl_multi_close($multihandle); |
|
270 |
|
271 return $responses; |
|
272 } |
|
273 |
|
274 /** |
|
275 * Get the cURL handle for use in a multi-request |
|
276 * |
|
277 * @param string $url URL to request |
|
278 * @param array $headers Associative array of request headers |
|
279 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD |
|
280 * @param array $options Request options, see {@see Requests::response()} for documentation |
|
281 * @return resource Subrequest's cURL handle |
|
282 */ |
|
283 public function &get_subrequest_handle($url, $headers, $data, $options) { |
|
284 $this->setup_handle($url, $headers, $data, $options); |
|
285 |
|
286 if ($options['filename'] !== false) { |
|
287 $this->stream_handle = fopen($options['filename'], 'wb'); |
|
288 } |
|
289 |
|
290 $this->response_data = ''; |
|
291 $this->response_bytes = 0; |
|
292 $this->response_byte_limit = false; |
|
293 if ($options['max_bytes'] !== false) { |
|
294 $this->response_byte_limit = $options['max_bytes']; |
|
295 } |
|
296 $this->hooks = $options['hooks']; |
|
297 |
|
298 return $this->handle; |
|
299 } |
|
300 |
|
301 /** |
|
302 * Setup the cURL handle for the given data |
|
303 * |
|
304 * @param string $url URL to request |
|
305 * @param array $headers Associative array of request headers |
|
306 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD |
|
307 * @param array $options Request options, see {@see Requests::response()} for documentation |
|
308 */ |
|
309 protected function setup_handle($url, $headers, $data, $options) { |
|
310 $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); |
|
311 |
|
312 // Force closing the connection for old versions of cURL (<7.22). |
|
313 if ( ! isset( $headers['Connection'] ) ) { |
|
314 $headers['Connection'] = 'close'; |
|
315 } |
|
316 |
|
317 $headers = Requests::flatten($headers); |
|
318 |
|
319 if (!empty($data)) { |
|
320 $data_format = $options['data_format']; |
|
321 |
|
322 if ($data_format === 'query') { |
|
323 $url = self::format_get($url, $data); |
|
324 $data = ''; |
|
325 } |
|
326 elseif (!is_string($data)) { |
|
327 $data = http_build_query($data, null, '&'); |
|
328 } |
|
329 } |
|
330 |
|
331 switch ($options['type']) { |
|
332 case Requests::POST: |
|
333 curl_setopt($this->handle, CURLOPT_POST, true); |
|
334 curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); |
|
335 break; |
|
336 case Requests::HEAD: |
|
337 curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); |
|
338 curl_setopt($this->handle, CURLOPT_NOBODY, true); |
|
339 break; |
|
340 case Requests::TRACE: |
|
341 curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); |
|
342 break; |
|
343 case Requests::PATCH: |
|
344 case Requests::PUT: |
|
345 case Requests::DELETE: |
|
346 case Requests::OPTIONS: |
|
347 default: |
|
348 curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); |
|
349 if (!empty($data)) { |
|
350 curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); |
|
351 } |
|
352 } |
|
353 |
|
354 // cURL requires a minimum timeout of 1 second when using the system |
|
355 // DNS resolver, as it uses `alarm()`, which is second resolution only. |
|
356 // There's no way to detect which DNS resolver is being used from our |
|
357 // end, so we need to round up regardless of the supplied timeout. |
|
358 // |
|
359 // https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609 |
|
360 $timeout = max($options['timeout'], 1); |
|
361 |
|
362 if (is_int($timeout) || $this->version < self::CURL_7_16_2) { |
|
363 curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); |
|
364 } |
|
365 else { |
|
366 curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); |
|
367 } |
|
368 |
|
369 if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { |
|
370 curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); |
|
371 } |
|
372 else { |
|
373 curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); |
|
374 } |
|
375 curl_setopt($this->handle, CURLOPT_URL, $url); |
|
376 curl_setopt($this->handle, CURLOPT_REFERER, $url); |
|
377 curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); |
|
378 if (!empty($headers)) { |
|
379 curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); |
|
380 } |
|
381 if ($options['protocol_version'] === 1.1) { |
|
382 curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); |
|
383 } |
|
384 else { |
|
385 curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); |
|
386 } |
|
387 |
|
388 if (true === $options['blocking']) { |
|
389 curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers')); |
|
390 curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body')); |
|
391 curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); |
|
392 } |
|
393 } |
|
394 |
|
395 /** |
|
396 * Process a response |
|
397 * |
|
398 * @param string $response Response data from the body |
|
399 * @param array $options Request options |
|
400 * @return string HTTP response data including headers |
|
401 */ |
|
402 public function process_response($response, $options) { |
|
403 if ($options['blocking'] === false) { |
|
404 $fake_headers = ''; |
|
405 $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); |
|
406 return false; |
|
407 } |
|
408 if ($options['filename'] !== false) { |
|
409 fclose($this->stream_handle); |
|
410 $this->headers = trim($this->headers); |
|
411 } |
|
412 else { |
|
413 $this->headers .= $response; |
|
414 } |
|
415 |
|
416 if (curl_errno($this->handle)) { |
|
417 $error = sprintf( |
|
418 'cURL error %s: %s', |
|
419 curl_errno($this->handle), |
|
420 curl_error($this->handle) |
|
421 ); |
|
422 throw new Requests_Exception($error, 'curlerror', $this->handle); |
|
423 } |
|
424 $this->info = curl_getinfo($this->handle); |
|
425 |
|
426 $options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info)); |
|
427 return $this->headers; |
|
428 } |
|
429 |
|
430 /** |
|
431 * Collect the headers as they are received |
|
432 * |
|
433 * @param resource $handle cURL resource |
|
434 * @param string $headers Header string |
|
435 * @return integer Length of provided header |
|
436 */ |
|
437 public function stream_headers($handle, $headers) { |
|
438 // Why do we do this? cURL will send both the final response and any |
|
439 // interim responses, such as a 100 Continue. We don't need that. |
|
440 // (We may want to keep this somewhere just in case) |
|
441 if ($this->done_headers) { |
|
442 $this->headers = ''; |
|
443 $this->done_headers = false; |
|
444 } |
|
445 $this->headers .= $headers; |
|
446 |
|
447 if ($headers === "\r\n") { |
|
448 $this->done_headers = true; |
|
449 } |
|
450 return strlen($headers); |
|
451 } |
|
452 |
|
453 /** |
|
454 * Collect data as it's received |
|
455 * |
|
456 * @since 1.6.1 |
|
457 * |
|
458 * @param resource $handle cURL resource |
|
459 * @param string $data Body data |
|
460 * @return integer Length of provided data |
|
461 */ |
|
462 public function stream_body($handle, $data) { |
|
463 $this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit)); |
|
464 $data_length = strlen($data); |
|
465 |
|
466 // Are we limiting the response size? |
|
467 if ($this->response_byte_limit) { |
|
468 if ($this->response_bytes === $this->response_byte_limit) { |
|
469 // Already at maximum, move on |
|
470 return $data_length; |
|
471 } |
|
472 |
|
473 if (($this->response_bytes + $data_length) > $this->response_byte_limit) { |
|
474 // Limit the length |
|
475 $limited_length = ($this->response_byte_limit - $this->response_bytes); |
|
476 $data = substr($data, 0, $limited_length); |
|
477 } |
|
478 } |
|
479 |
|
480 if ($this->stream_handle) { |
|
481 fwrite($this->stream_handle, $data); |
|
482 } |
|
483 else { |
|
484 $this->response_data .= $data; |
|
485 } |
|
486 |
|
487 $this->response_bytes += strlen($data); |
|
488 return $data_length; |
|
489 } |
|
490 |
|
491 /** |
|
492 * Format a URL given GET data |
|
493 * |
|
494 * @param string $url |
|
495 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} |
|
496 * @return string URL with data |
|
497 */ |
|
498 protected static function format_get($url, $data) { |
|
499 if (!empty($data)) { |
|
500 $url_parts = parse_url($url); |
|
501 if (empty($url_parts['query'])) { |
|
502 $query = $url_parts['query'] = ''; |
|
503 } |
|
504 else { |
|
505 $query = $url_parts['query']; |
|
506 } |
|
507 |
|
508 $query .= '&' . http_build_query($data, null, '&'); |
|
509 $query = trim($query, '&'); |
|
510 |
|
511 if (empty($url_parts['query'])) { |
|
512 $url .= '?' . $query; |
|
513 } |
|
514 else { |
|
515 $url = str_replace($url_parts['query'], $query, $url); |
|
516 } |
|
517 } |
|
518 return $url; |
|
519 } |
|
520 |
|
521 /** |
|
522 * Whether this transport is valid |
|
523 * |
|
524 * @codeCoverageIgnore |
|
525 * @return boolean True if the transport is valid, false otherwise. |
|
526 */ |
|
527 public static function test($capabilities = array()) { |
|
528 if (!function_exists('curl_init') || !function_exists('curl_exec')) { |
|
529 return false; |
|
530 } |
|
531 |
|
532 // If needed, check that our installed curl version supports SSL |
|
533 if (isset($capabilities['ssl']) && $capabilities['ssl']) { |
|
534 $curl_version = curl_version(); |
|
535 if (!(CURL_VERSION_SSL & $curl_version['features'])) { |
|
536 return false; |
|
537 } |
|
538 } |
|
539 |
|
540 return true; |
|
541 } |
|
542 } |