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