PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.4
Booking for Appointments and Events Calendar – Amelia v2.4.4
2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / rmccue / requests / src / Transport / Curl.php
ameliabooking / vendor / rmccue / requests / src / Transport Last commit date
Curl.php 5 months ago Fsockopen.php 5 months ago
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