88 |
88 |
89 /** |
89 /** |
90 * Constructor |
90 * Constructor |
91 */ |
91 */ |
92 public function __construct() { |
92 public function __construct() { |
93 $curl = curl_version(); |
93 $curl = curl_version(); |
94 $this->version = $curl['version_number']; |
94 $this->version = $curl['version_number']; |
95 $this->handle = curl_init(); |
95 $this->handle = curl_init(); |
96 |
96 |
97 curl_setopt($this->handle, CURLOPT_HEADER, false); |
97 curl_setopt($this->handle, CURLOPT_HEADER, false); |
98 curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); |
98 curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); |
99 if ($this->version >= self::CURL_7_10_5) { |
99 if ($this->version >= self::CURL_7_10_5) { |
100 curl_setopt($this->handle, CURLOPT_ENCODING, ''); |
100 curl_setopt($this->handle, CURLOPT_ENCODING, ''); |
101 } |
101 } |
102 if (defined('CURLOPT_PROTOCOLS')) { |
102 if (defined('CURLOPT_PROTOCOLS')) { |
|
103 // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound |
103 curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); |
104 curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); |
104 } |
105 } |
105 if (defined('CURLOPT_REDIR_PROTOCOLS')) { |
106 if (defined('CURLOPT_REDIR_PROTOCOLS')) { |
|
107 // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound |
106 curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); |
108 curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); |
107 } |
109 } |
108 } |
110 } |
109 |
111 |
110 /** |
112 /** |
136 |
138 |
137 if ($options['filename'] !== false) { |
139 if ($options['filename'] !== false) { |
138 $this->stream_handle = fopen($options['filename'], 'wb'); |
140 $this->stream_handle = fopen($options['filename'], 'wb'); |
139 } |
141 } |
140 |
142 |
141 $this->response_data = ''; |
143 $this->response_data = ''; |
142 $this->response_bytes = 0; |
144 $this->response_bytes = 0; |
143 $this->response_byte_limit = false; |
145 $this->response_byte_limit = false; |
144 if ($options['max_bytes'] !== false) { |
146 if ($options['max_bytes'] !== false) { |
145 $this->response_byte_limit = $options['max_bytes']; |
147 $this->response_byte_limit = $options['max_bytes']; |
146 } |
148 } |
147 |
149 |
166 |
168 |
167 if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) { |
169 if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) { |
168 // Reset encoding and try again |
170 // Reset encoding and try again |
169 curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); |
171 curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); |
170 |
172 |
171 $this->response_data = ''; |
173 $this->response_data = ''; |
172 $this->response_bytes = 0; |
174 $this->response_bytes = 0; |
173 curl_exec($this->handle); |
175 curl_exec($this->handle); |
174 $response = $this->response_data; |
176 $response = $this->response_data; |
175 } |
177 } |
176 |
178 |
197 return array(); |
199 return array(); |
198 } |
200 } |
199 |
201 |
200 $multihandle = curl_multi_init(); |
202 $multihandle = curl_multi_init(); |
201 $subrequests = array(); |
203 $subrequests = array(); |
202 $subhandles = array(); |
204 $subhandles = array(); |
203 |
205 |
204 $class = get_class($this); |
206 $class = get_class($this); |
205 foreach ($requests as $id => $request) { |
207 foreach ($requests as $id => $request) { |
206 $subrequests[$id] = new $class(); |
208 $subrequests[$id] = new $class(); |
207 $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); |
209 $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])); |
210 $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id])); |
209 curl_multi_add_handle($multihandle, $subhandles[$id]); |
211 curl_multi_add_handle($multihandle, $subhandles[$id]); |
210 } |
212 } |
211 |
213 |
212 $completed = 0; |
214 $completed = 0; |
213 $responses = array(); |
215 $responses = array(); |
|
216 $subrequestcount = count($subrequests); |
214 |
217 |
215 $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle)); |
218 $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle)); |
216 |
219 |
217 do { |
220 do { |
218 $active = false; |
221 $active = 0; |
219 |
222 |
220 do { |
223 do { |
221 $status = curl_multi_exec($multihandle, $active); |
224 $status = curl_multi_exec($multihandle, $active); |
222 } |
225 } |
223 while ($status === CURLM_CALL_MULTI_PERFORM); |
226 while ($status === CURLM_CALL_MULTI_PERFORM); |
233 } |
236 } |
234 |
237 |
235 // Parse the finished requests before we start getting the new ones |
238 // Parse the finished requests before we start getting the new ones |
236 foreach ($to_process as $key => $done) { |
239 foreach ($to_process as $key => $done) { |
237 $options = $requests[$key]['options']; |
240 $options = $requests[$key]['options']; |
238 if (CURLE_OK !== $done['result']) { |
241 if ($done['result'] !== CURLE_OK) { |
239 //get error string for handle. |
242 //get error string for handle. |
240 $reason = curl_error($done['handle']); |
243 $reason = curl_error($done['handle']); |
241 $exception = new Requests_Exception_Transport_cURL( |
244 $exception = new Requests_Exception_Transport_cURL( |
242 $reason, |
245 $reason, |
243 Requests_Exception_Transport_cURL::EASY, |
246 Requests_Exception_Transport_cURL::EASY, |
244 $done['handle'], |
247 $done['handle'], |
245 $done['result'] |
248 $done['result'] |
246 ); |
249 ); |
247 $responses[$key] = $exception; |
250 $responses[$key] = $exception; |
248 $options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key])); |
251 $options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key])); |
249 } |
252 } |
250 else { |
253 else { |
251 $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); |
254 $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); |
260 $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key)); |
263 $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key)); |
261 } |
264 } |
262 $completed++; |
265 $completed++; |
263 } |
266 } |
264 } |
267 } |
265 while ($active || $completed < count($subrequests)); |
268 while ($active || $completed < $subrequestcount); |
266 |
269 |
267 $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle)); |
270 $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle)); |
268 |
271 |
269 curl_multi_close($multihandle); |
272 curl_multi_close($multihandle); |
270 |
273 |
285 |
288 |
286 if ($options['filename'] !== false) { |
289 if ($options['filename'] !== false) { |
287 $this->stream_handle = fopen($options['filename'], 'wb'); |
290 $this->stream_handle = fopen($options['filename'], 'wb'); |
288 } |
291 } |
289 |
292 |
290 $this->response_data = ''; |
293 $this->response_data = ''; |
291 $this->response_bytes = 0; |
294 $this->response_bytes = 0; |
292 $this->response_byte_limit = false; |
295 $this->response_byte_limit = false; |
293 if ($options['max_bytes'] !== false) { |
296 if ($options['max_bytes'] !== false) { |
294 $this->response_byte_limit = $options['max_bytes']; |
297 $this->response_byte_limit = $options['max_bytes']; |
295 } |
298 } |
296 $this->hooks = $options['hooks']; |
299 $this->hooks = $options['hooks']; |
308 */ |
311 */ |
309 protected function setup_handle($url, $headers, $data, $options) { |
312 protected function setup_handle($url, $headers, $data, $options) { |
310 $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); |
313 $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); |
311 |
314 |
312 // Force closing the connection for old versions of cURL (<7.22). |
315 // Force closing the connection for old versions of cURL (<7.22). |
313 if ( ! isset( $headers['Connection'] ) ) { |
316 if (!isset($headers['Connection'])) { |
314 $headers['Connection'] = 'close'; |
317 $headers['Connection'] = 'close'; |
|
318 } |
|
319 |
|
320 /** |
|
321 * Add "Expect" header. |
|
322 * |
|
323 * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can |
|
324 * add as much as a second to the time it takes for cURL to perform a request. To |
|
325 * prevent this, we need to set an empty "Expect" header. To match the behaviour of |
|
326 * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use |
|
327 * HTTP/1.1. |
|
328 * |
|
329 * https://curl.se/mail/lib-2017-07/0013.html |
|
330 */ |
|
331 if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) { |
|
332 $headers['Expect'] = $this->get_expect_header($data); |
315 } |
333 } |
316 |
334 |
317 $headers = Requests::flatten($headers); |
335 $headers = Requests::flatten($headers); |
318 |
336 |
319 if (!empty($data)) { |
337 if (!empty($data)) { |
320 $data_format = $options['data_format']; |
338 $data_format = $options['data_format']; |
321 |
339 |
322 if ($data_format === 'query') { |
340 if ($data_format === 'query') { |
323 $url = self::format_get($url, $data); |
341 $url = self::format_get($url, $data); |
324 $data = ''; |
342 $data = ''; |
325 } |
343 } |
326 elseif (!is_string($data)) { |
344 elseif (!is_string($data)) { |
327 $data = http_build_query($data, null, '&'); |
345 $data = http_build_query($data, null, '&'); |
328 } |
346 } |
361 |
379 |
362 if (is_int($timeout) || $this->version < self::CURL_7_16_2) { |
380 if (is_int($timeout) || $this->version < self::CURL_7_16_2) { |
363 curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); |
381 curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); |
364 } |
382 } |
365 else { |
383 else { |
|
384 // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound |
366 curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); |
385 curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); |
367 } |
386 } |
368 |
387 |
369 if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { |
388 if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { |
370 curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); |
389 curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); |
371 } |
390 } |
372 else { |
391 else { |
|
392 // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound |
373 curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); |
393 curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); |
374 } |
394 } |
375 curl_setopt($this->handle, CURLOPT_URL, $url); |
395 curl_setopt($this->handle, CURLOPT_URL, $url); |
376 curl_setopt($this->handle, CURLOPT_REFERER, $url); |
396 curl_setopt($this->handle, CURLOPT_REFERER, $url); |
377 curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); |
397 curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); |
383 } |
403 } |
384 else { |
404 else { |
385 curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); |
405 curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); |
386 } |
406 } |
387 |
407 |
388 if (true === $options['blocking']) { |
408 if ($options['blocking'] === true) { |
389 curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers')); |
409 curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array($this, 'stream_headers')); |
390 curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body')); |
410 curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array($this, 'stream_body')); |
391 curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); |
411 curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); |
392 } |
412 } |
393 } |
413 } |
394 |
414 |
395 /** |
415 /** |
396 * Process a response |
416 * Process a response |
397 * |
417 * |
398 * @param string $response Response data from the body |
418 * @param string $response Response data from the body |
399 * @param array $options Request options |
419 * @param array $options Request options |
400 * @return string HTTP response data including headers |
420 * @return string|false HTTP response data including headers. False if non-blocking. |
|
421 * @throws Requests_Exception |
401 */ |
422 */ |
402 public function process_response($response, $options) { |
423 public function process_response($response, $options) { |
403 if ($options['blocking'] === false) { |
424 if ($options['blocking'] === false) { |
404 $fake_headers = ''; |
425 $fake_headers = ''; |
405 $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); |
426 $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); |
406 return false; |
427 return false; |
407 } |
428 } |
408 if ($options['filename'] !== false) { |
429 if ($options['filename'] !== false && $this->stream_handle) { |
409 fclose($this->stream_handle); |
430 fclose($this->stream_handle); |
410 $this->headers = trim($this->headers); |
431 $this->headers = trim($this->headers); |
411 } |
432 } |
412 else { |
433 else { |
413 $this->headers .= $response; |
434 $this->headers .= $response; |
437 public function stream_headers($handle, $headers) { |
458 public function stream_headers($handle, $headers) { |
438 // Why do we do this? cURL will send both the final response and any |
459 // 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. |
460 // interim responses, such as a 100 Continue. We don't need that. |
440 // (We may want to keep this somewhere just in case) |
461 // (We may want to keep this somewhere just in case) |
441 if ($this->done_headers) { |
462 if ($this->done_headers) { |
442 $this->headers = ''; |
463 $this->headers = ''; |
443 $this->done_headers = false; |
464 $this->done_headers = false; |
444 } |
465 } |
445 $this->headers .= $headers; |
466 $this->headers .= $headers; |
446 |
467 |
447 if ($headers === "\r\n") { |
468 if ($headers === "\r\n") { |
471 } |
492 } |
472 |
493 |
473 if (($this->response_bytes + $data_length) > $this->response_byte_limit) { |
494 if (($this->response_bytes + $data_length) > $this->response_byte_limit) { |
474 // Limit the length |
495 // Limit the length |
475 $limited_length = ($this->response_byte_limit - $this->response_bytes); |
496 $limited_length = ($this->response_byte_limit - $this->response_bytes); |
476 $data = substr($data, 0, $limited_length); |
497 $data = substr($data, 0, $limited_length); |
477 } |
498 } |
478 } |
499 } |
479 |
500 |
480 if ($this->stream_handle) { |
501 if ($this->stream_handle) { |
481 fwrite($this->stream_handle, $data); |
502 fwrite($this->stream_handle, $data); |
495 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} |
516 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} |
496 * @return string URL with data |
517 * @return string URL with data |
497 */ |
518 */ |
498 protected static function format_get($url, $data) { |
519 protected static function format_get($url, $data) { |
499 if (!empty($data)) { |
520 if (!empty($data)) { |
|
521 $query = ''; |
500 $url_parts = parse_url($url); |
522 $url_parts = parse_url($url); |
501 if (empty($url_parts['query'])) { |
523 if (empty($url_parts['query'])) { |
502 $query = $url_parts['query'] = ''; |
524 $url_parts['query'] = ''; |
503 } |
525 } |
504 else { |
526 else { |
505 $query = $url_parts['query']; |
527 $query = $url_parts['query']; |
506 } |
528 } |
507 |
529 |
508 $query .= '&' . http_build_query($data, null, '&'); |
530 $query .= '&' . http_build_query($data, null, '&'); |
509 $query = trim($query, '&'); |
531 $query = trim($query, '&'); |
510 |
532 |
511 if (empty($url_parts['query'])) { |
533 if (empty($url_parts['query'])) { |
512 $url .= '?' . $query; |
534 $url .= '?' . $query; |
513 } |
535 } |
514 else { |
536 else { |
537 } |
559 } |
538 } |
560 } |
539 |
561 |
540 return true; |
562 return true; |
541 } |
563 } |
|
564 |
|
565 /** |
|
566 * Get the correct "Expect" header for the given request data. |
|
567 * |
|
568 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD. |
|
569 * @return string The "Expect" header. |
|
570 */ |
|
571 protected function get_expect_header($data) { |
|
572 if (!is_array($data)) { |
|
573 return strlen((string) $data) >= 1048576 ? '100-Continue' : ''; |
|
574 } |
|
575 |
|
576 $bytesize = 0; |
|
577 $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data)); |
|
578 |
|
579 foreach ($iterator as $datum) { |
|
580 $bytesize += strlen((string) $datum); |
|
581 |
|
582 if ($bytesize >= 1048576) { |
|
583 return '100-Continue'; |
|
584 } |
|
585 } |
|
586 |
|
587 return ''; |
|
588 } |
542 } |
589 } |