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