PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / vendor_prefixed / guzzlehttp / guzzle / src / Handler / CurlFactory.php

CurlFactory.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.5, at vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactory.php

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