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