PluginProbe
Media Cloud Sync / 1.2.9
Media Cloud Sync v1.2.9
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
media-cloud-sync / includes / sdk / google / guzzlehttp / guzzle / src / Handler / CurlFactory.php

CurlFactory.php in Media Cloud Sync 1.2.9, at includes/sdk/google/guzzlehttp/guzzle/src/Handler/CurlFactory.php

564 lines 26.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\GuzzleHttp\Handler;
4
5 use Dudlewebs\WPMCS\GuzzleHttp\Exception\ConnectException;
6 use Dudlewebs\WPMCS\GuzzleHttp\Exception\RequestException;
7 use Dudlewebs\WPMCS\GuzzleHttp\Promise as P;
8 use Dudlewebs\WPMCS\GuzzleHttp\Promise\FulfilledPromise;
9 use Dudlewebs\WPMCS\GuzzleHttp\Promise\PromiseInterface;
10 use Dudlewebs\WPMCS\GuzzleHttp\Psr7\LazyOpenStream;
11 use Dudlewebs\WPMCS\GuzzleHttp\TransferStats;
12 use Dudlewebs\WPMCS\GuzzleHttp\Utils;
13 use Dudlewebs\WPMCS\Psr\Http\Message\RequestInterface;
14 use Dudlewebs\WPMCS\Psr\Http\Message\UriInterface;
15 /**
16 * Creates curl resources from a request
17 *
18 * @final
19 */
20 class CurlFactory implements CurlFactoryInterface
21 {
22 public const CURL_VERSION_STR = 'curl_version';
23 /**
24 * @deprecated
25 */
26 public const LOW_CURL_VERSION_NUMBER = '7.21.2';
27 /**
28 * @var resource[]|\CurlHandle[]
29 */
30 private $handles = [];
31 /**
32 * @var int Total number of idle handles to keep in cache
33 */
34 private $maxHandles;
35 /**
36 * @param int $maxHandles Maximum number of idle handles.
37 */
38 public function __construct(int $maxHandles)
39 {
40 $this->maxHandles = $maxHandles;
41 }
42 public function create(RequestInterface $request, array $options): EasyHandle
43 {
44 $protocolVersion = $request->getProtocolVersion();
45 if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
46 if (!self::supportsHttp2()) {
47 throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request);
48 }
49 } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
50 throw new ConnectException(sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request);
51 }
52 if (isset($options['curl']['body_as_string'])) {
53 $options['_body_as_string'] = $options['curl']['body_as_string'];
54 unset($options['curl']['body_as_string']);
55 }
56 $easy = new EasyHandle();
57 $easy->request = $request;
58 $easy->options = $options;
59 $conf = $this->getDefaultConf($easy);
60 $this->applyMethod($easy, $conf);
61 $this->applyHandlerOptions($easy, $conf);
62 $this->applyHeaders($easy, $conf);
63 unset($conf['_headers']);
64 // Add handler options from the request configuration options
65 if (isset($options['curl'])) {
66 $conf = \array_replace($conf, $options['curl']);
67 }
68 $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
69 $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
70 curl_setopt_array($easy->handle, $conf);
71 return $easy;
72 }
73 private static function supportsHttp2(): bool
74 {
75 static $supportsHttp2 = null;
76 if (null === $supportsHttp2) {
77 $supportsHttp2 = self::supportsTls12() && defined('CURL_VERSION_HTTP2') && \CURL_VERSION_HTTP2 & \curl_version()['features'];
78 }
79 return $supportsHttp2;
80 }
81 private static function supportsTls12(): bool
82 {
83 static $supportsTls12 = null;
84 if (null === $supportsTls12) {
85 $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features'];
86 }
87 return $supportsTls12;
88 }
89 private static function supportsTls13(): bool
90 {
91 static $supportsTls13 = null;
92 if (null === $supportsTls13) {
93 $supportsTls13 = defined('CURL_SSLVERSION_TLSv1_3') && \CURL_SSLVERSION_TLSv1_3 & \curl_version()['features'];
94 }
95 return $supportsTls13;
96 }
97 public function release(EasyHandle $easy): void
98 {
99 $resource = $easy->handle;
100 unset($easy->handle);
101 if (\count($this->handles) >= $this->maxHandles) {
102 \curl_close($resource);
103 } else {
104 // Remove all callback functions as they can hold onto references
105 // and are not cleaned up by curl_reset. Using curl_setopt_array
106 // does not work for some reason, so removing each one
107 // individually.
108 \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
109 \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
110 \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
111 \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
112 \curl_reset($resource);
113 $this->handles[] = $resource;
114 }
115 }
116 /**
117 * Completes a cURL transaction, either returning a response promise or a
118 * rejected promise.
119 *
120 * @param callable(RequestInterface, array): PromiseInterface $handler
121 * @param CurlFactoryInterface $factory Dictates how the handle is released
122 */
123 public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
124 {
125 if (isset($easy->options['on_stats'])) {
126 self::invokeStats($easy);
127 }
128 if (!$easy->response || $easy->errno) {
129 return self::finishError($handler, $easy, $factory);
130 }
131 // Return the response if it is present and there is no error.
132 $factory->release($easy);
133 // Rewind the body of the response if possible.
134 $body = $easy->response->getBody();
135 if ($body->isSeekable()) {
136 $body->rewind();
137 }
138 return new FulfilledPromise($easy->response);
139 }
140 private static function invokeStats(EasyHandle $easy): void
141 {
142 $curlStats = \curl_getinfo($easy->handle);
143 $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
144 $stats = new TransferStats($easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats);
145 $easy->options['on_stats']($stats);
146 }
147 /**
148 * @param callable(RequestInterface, array): PromiseInterface $handler
149 */
150 private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface
151 {
152 // Get error information and release the handle to the factory.
153 $ctx = ['errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME)] + \curl_getinfo($easy->handle);
154 $ctx[self::CURL_VERSION_STR] = self::getCurlVersion();
155 $factory->release($easy);
156 // Retry when nothing is present or when curl failed to rewind.
157 if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
158 return self::retryFailedRewind($handler, $easy, $ctx);
159 }
160 return self::createRejection($easy, $ctx);
161 }
162 private static function getCurlVersion(): string
163 {
164 static $curlVersion = null;
165 if (null === $curlVersion) {
166 $curlVersion = \curl_version()['version'];
167 }
168 return $curlVersion;
169 }
170 private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
171 {
172 static $connectionErrors = [\CURLE_OPERATION_TIMEOUTED => \true, \CURLE_COULDNT_RESOLVE_HOST => \true, \CURLE_COULDNT_CONNECT => \true, \CURLE_SSL_CONNECT_ERROR => \true, \CURLE_GOT_NOTHING => \true];
173 if ($easy->createResponseException) {
174 return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $easy->request, $easy->response, $easy->createResponseException, $ctx));
175 }
176 // If an exception was encountered during the onHeaders event, then
177 // return a rejected promise that wraps that exception.
178 if ($easy->onHeadersException) {
179 return P\Create::rejectionFor(new RequestException('An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx));
180 }
181 $uri = $easy->request->getUri();
182 $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri);
183 $message = \sprintf('cURL error %s: %s (%s)', $ctx['errno'], $sanitizedError, 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html');
184 if ('' !== $sanitizedError) {
185 $redactedUriString = \Dudlewebs\WPMCS\GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString();
186 if ($redactedUriString !== '' && \false === \strpos($sanitizedError, $redactedUriString)) {
187 $message .= \sprintf(' for %s', $redactedUriString);
188 }
189 }
190 // Create a connection exception if it was a specific error code.
191 $error = isset($connectionErrors[$easy->errno]) ? new ConnectException($message, $easy->request, null, $ctx) : new RequestException($message, $easy->request, $easy->response, null, $ctx);
192 return P\Create::rejectionFor($error);
193 }
194 private static function sanitizeCurlError(string $error, UriInterface $uri): string
195 {
196 if ('' === $error) {
197 return $error;
198 }
199 $baseUri = $uri->withQuery('')->withFragment('');
200 $baseUriString = $baseUri->__toString();
201 if ('' === $baseUriString) {
202 return $error;
203 }
204 $redactedUriString = \Dudlewebs\WPMCS\GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString();
205 return str_replace($baseUriString, $redactedUriString, $error);
206 }
207 /**
208 * @return array<int|string, mixed>
209 */
210 private function getDefaultConf(EasyHandle $easy): array
211 {
212 $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300];
213 if (\defined('CURLOPT_PROTOCOLS')) {
214 $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
215 }
216 $version = $easy->request->getProtocolVersion();
217 if ('2' === $version || '2.0' === $version) {
218 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
219 } elseif ('1.1' === $version) {
220 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
221 } else {
222 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
223 }
224 return $conf;
225 }
226 private function applyMethod(EasyHandle $easy, array &$conf): void
227 {
228 $body = $easy->request->getBody();
229 $size = $body->getSize();
230 if ($size === null || $size > 0) {
231 $this->applyBody($easy->request, $easy->options, $conf);
232 return;
233 }
234 $method = $easy->request->getMethod();
235 if ($method === 'PUT' || $method === 'POST') {
236 // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
237 if (!$easy->request->hasHeader('Content-Length')) {
238 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
239 }
240 } elseif ($method === 'HEAD') {
241 $conf[\CURLOPT_NOBODY] = \true;
242 unset($conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE]);
243 }
244 }
245 private function applyBody(RequestInterface $request, array $options, array &$conf): void
246 {
247 $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null;
248 // Send the body as a string if the size is less than 1MB OR if the
249 // [curl][body_as_string] request value is set.
250 if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) {
251 $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
252 // Don't duplicate the Content-Length header
253 $this->removeHeader('Content-Length', $conf);
254 $this->removeHeader('Transfer-Encoding', $conf);
255 } else {
256 $conf[\CURLOPT_UPLOAD] = \true;
257 if ($size !== null) {
258 $conf[\CURLOPT_INFILESIZE] = $size;
259 $this->removeHeader('Content-Length', $conf);
260 }
261 $body = $request->getBody();
262 if ($body->isSeekable()) {
263 $body->rewind();
264 }
265 $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) {
266 return $body->read($length);
267 };
268 }
269 // If the Expect header is not present, prevent curl from adding it
270 if (!$request->hasHeader('Expect')) {
271 $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
272 }
273 // cURL sometimes adds a content-type by default. Prevent this.
274 if (!$request->hasHeader('Content-Type')) {
275 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
276 }
277 }
278 private function applyHeaders(EasyHandle $easy, array &$conf): void
279 {
280 foreach ($conf['_headers'] as $name => $values) {
281 foreach ($values as $value) {
282 $value = (string) $value;
283 if ($value === '') {
284 // cURL requires a special format for empty headers.
285 // See https://github.com/guzzle/guzzle/issues/1882 for more details.
286 $conf[\CURLOPT_HTTPHEADER][] = "{$name};";
287 } else {
288 $conf[\CURLOPT_HTTPHEADER][] = "{$name}: {$value}";
289 }
290 }
291 }
292 // Remove the Accept header if one was not set
293 if (!$easy->request->hasHeader('Accept')) {
294 $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
295 }
296 }
297 /**
298 * Remove a header from the options array.
299 *
300 * @param string $name Case-insensitive header to remove
301 * @param array $options Array of options to modify
302 */
303 private function removeHeader(string $name, array &$options): void
304 {
305 foreach (\array_keys($options['_headers']) as $key) {
306 if (!\strcasecmp($key, $name)) {
307 unset($options['_headers'][$key]);
308 return;
309 }
310 }
311 }
312 private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
313 {
314 $options = $easy->options;
315 if (isset($options['verify'])) {
316 if ($options['verify'] === \false) {
317 unset($conf[\CURLOPT_CAINFO]);
318 $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
319 $conf[\CURLOPT_SSL_VERIFYPEER] = \false;
320 } else {
321 $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
322 $conf[\CURLOPT_SSL_VERIFYPEER] = \true;
323 if (\is_string($options['verify'])) {
324 // Throw an error if the file/folder/link path is not valid or doesn't exist.
325 if (!\file_exists($options['verify'])) {
326 throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
327 }
328 // If it's a directory or a link to a directory use CURLOPT_CAPATH.
329 // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
330 if (\is_dir($options['verify']) || \is_link($options['verify']) === \true && ($verifyLink = \readlink($options['verify'])) !== \false && \is_dir($verifyLink)) {
331 $conf[\CURLOPT_CAPATH] = $options['verify'];
332 } else {
333 $conf[\CURLOPT_CAINFO] = $options['verify'];
334 }
335 }
336 }
337 }
338 if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
339 $accept = $easy->request->getHeaderLine('Accept-Encoding');
340 if ($accept) {
341 $conf[\CURLOPT_ENCODING] = $accept;
342 } else {
343 // The empty string enables all available decoders and implicitly
344 // sets a matching 'Accept-Encoding' header.
345 $conf[\CURLOPT_ENCODING] = '';
346 // But as the user did not specify any encoding preference,
347 // let's leave it up to server by preventing curl from sending
348 // the header, which will be interpreted as 'Accept-Encoding: *'.
349 // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding
350 $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
351 }
352 }
353 if (!isset($options['sink'])) {
354 // Use a default temp stream if no sink was set.
355 $options['sink'] = \Dudlewebs\WPMCS\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+');
356 }
357 $sink = $options['sink'];
358 if (!\is_string($sink)) {
359 $sink = \Dudlewebs\WPMCS\GuzzleHttp\Psr7\Utils::streamFor($sink);
360 } elseif (!\is_dir(\dirname($sink))) {
361 // Ensure that the directory exists before failing in curl.
362 throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
363 } else {
364 $sink = new LazyOpenStream($sink, 'w+');
365 }
366 $easy->sink = $sink;
367 $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int {
368 return $sink->write($write);
369 };
370 $timeoutRequiresNoSignal = \false;
371 if (isset($options['timeout'])) {
372 $timeoutRequiresNoSignal |= $options['timeout'] < 1;
373 $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
374 }
375 // CURL default value is CURL_IPRESOLVE_WHATEVER
376 if (isset($options['force_ip_resolve'])) {
377 if ('v4' === $options['force_ip_resolve']) {
378 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
379 } elseif ('v6' === $options['force_ip_resolve']) {
380 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
381 }
382 }
383 if (isset($options['connect_timeout'])) {
384 $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
385 $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
386 }
387 if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
388 $conf[\CURLOPT_NOSIGNAL] = \true;
389 }
390 if (isset($options['proxy'])) {
391 if (!\is_array($options['proxy'])) {
392 $conf[\CURLOPT_PROXY] = $options['proxy'];
393 } else {
394 $scheme = $easy->request->getUri()->getScheme();
395 if (isset($options['proxy'][$scheme])) {
396 $host = $easy->request->getUri()->getHost();
397 if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) {
398 unset($conf[\CURLOPT_PROXY]);
399 } else {
400 $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
401 }
402 }
403 }
404 }
405 if (isset($options['crypto_method'])) {
406 $protocolVersion = $easy->request->getProtocolVersion();
407 // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2
408 if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
409 if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
410 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
411 } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
412 if (!self::supportsTls13()) {
413 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
414 }
415 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
416 } else {
417 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
418 }
419 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
420 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
421 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
422 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
423 } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
424 if (!self::supportsTls12()) {
425 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
426 }
427 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
428 } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
429 if (!self::supportsTls13()) {
430 throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
431 }
432 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
433 } else {
434 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
435 }
436 }
437 if (isset($options['cert'])) {
438 $cert = $options['cert'];
439 if (\is_array($cert)) {
440 $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
441 $cert = $cert[0];
442 }
443 if (!\file_exists($cert)) {
444 throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
445 }
446 // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
447 // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
448 $ext = pathinfo($cert, \PATHINFO_EXTENSION);
449 if (preg_match('#^(der|p12)$#i', $ext)) {
450 $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
451 }
452 $conf[\CURLOPT_SSLCERT] = $cert;
453 }
454 if (isset($options['ssl_key'])) {
455 if (\is_array($options['ssl_key'])) {
456 if (\count($options['ssl_key']) === 2) {
457 [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
458 } else {
459 [$sslKey] = $options['ssl_key'];
460 }
461 }
462 $sslKey = $sslKey ?? $options['ssl_key'];
463 if (!\file_exists($sslKey)) {
464 throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
465 }
466 $conf[\CURLOPT_SSLKEY] = $sslKey;
467 }
468 if (isset($options['progress'])) {
469 $progress = $options['progress'];
470 if (!\is_callable($progress)) {
471 throw new \InvalidArgumentException('progress client option must be callable');
472 }
473 $conf[\CURLOPT_NOPROGRESS] = \false;
474 $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) {
475 $progress($downloadSize, $downloaded, $uploadSize, $uploaded);
476 };
477 }
478 if (!empty($options['debug'])) {
479 $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
480 $conf[\CURLOPT_VERBOSE] = \true;
481 }
482 }
483 /**
484 * This function ensures that a response was set on a transaction. If one
485 * was not set, then the request is retried if possible. This error
486 * typically means you are sending a payload, curl encountered a
487 * "Connection died, retrying a fresh connect" error, tried to rewind the
488 * stream, and then encountered a "necessary data rewind wasn't possible"
489 * error, causing the request to be sent through curl_multi_info_read()
490 * without an error status.
491 *
492 * @param callable(RequestInterface, array): PromiseInterface $handler
493 */
494 private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface
495 {
496 try {
497 // Only rewind if the body has been read from.
498 $body = $easy->request->getBody();
499 if ($body->tell() > 0) {
500 $body->rewind();
501 }
502 } catch (\RuntimeException $e) {
503 $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e;
504 return self::createRejection($easy, $ctx);
505 }
506 // Retry no more than 3 times before giving up.
507 if (!isset($easy->options['_curl_retries'])) {
508 $easy->options['_curl_retries'] = 1;
509 } elseif ($easy->options['_curl_retries'] == 2) {
510 $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.';
511 return self::createRejection($easy, $ctx);
512 } else {
513 ++$easy->options['_curl_retries'];
514 }
515 return $handler($easy->request, $easy->options);
516 }
517 private function createHeaderFn(EasyHandle $easy): callable
518 {
519 if (isset($easy->options['on_headers'])) {
520 $onHeaders = $easy->options['on_headers'];
521 if (!\is_callable($onHeaders)) {
522 throw new \InvalidArgumentException('on_headers must be callable');
523 }
524 } else {
525 $onHeaders = null;
526 }
527 return static function ($ch, $h) use ($onHeaders, $easy, &$startingResponse) {
528 $value = \trim($h);
529 if ($value === '') {
530 $startingResponse = \true;
531 try {
532 $easy->createResponse();
533 } catch (\Exception $e) {
534 $easy->createResponseException = $e;
535 return -1;
536 }
537 if ($onHeaders !== null) {
538 try {
539 $onHeaders($easy->response);
540 } catch (\Exception $e) {
541 // Associate the exception with the handle and trigger
542 // a curl header write error by returning 0.
543 $easy->onHeadersException = $e;
544 return -1;
545 }
546 }
547 } elseif ($startingResponse) {
548 $startingResponse = \false;
549 $easy->headers = [$value];
550 } else {
551 $easy->headers[] = $value;
552 }
553 return \strlen($h);
554 };
555 }
556 public function __destruct()
557 {
558 foreach ($this->handles as $id => $handle) {
559 \curl_close($handle);
560 unset($this->handles[$id]);
561 }
562 }
563 }
564