[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/Requests/Transport/ -> cURL.php (source)

   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  }


Generated: Thu Dec 15 01:00:02 2022 Cross-referenced by PHPXref 0.7.1