PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.4
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 18.4, at vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlFactory.php

437 lines 20.8 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\FulfilledPromise;
8 use YoastSEO_Vendor\GuzzleHttp\Psr7;
9 use YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream;
10 use YoastSEO_Vendor\GuzzleHttp\TransferStats;
11 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
12 /**
13 * Creates curl resources from a request
14 */
15 class CurlFactory implements \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactoryInterface
16 {
17 const CURL_VERSION_STR = 'curl_version';
18 const LOW_CURL_VERSION_NUMBER = '7.21.2';
19 /** @var array */
20 private $handles = [];
21 /** @var int Total number of idle handles to keep in cache */
22 private $maxHandles;
23 /**
24 * @param int $maxHandles Maximum number of idle handles.
25 */
26 public function __construct($maxHandles)
27 {
28 $this->maxHandles = $maxHandles;
29 }
30 public function create(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
31 {
32 if (isset($options['curl']['body_as_string'])) {
33 $options['_body_as_string'] = $options['curl']['body_as_string'];
34 unset($options['curl']['body_as_string']);
35 }
36 $easy = new \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle();
37 $easy->request = $request;
38 $easy->options = $options;
39 $conf = $this->getDefaultConf($easy);
40 $this->applyMethod($easy, $conf);
41 $this->applyHandlerOptions($easy, $conf);
42 $this->applyHeaders($easy, $conf);
43 unset($conf['_headers']);
44 // Add handler options from the request configuration options
45 if (isset($options['curl'])) {
46 $conf = \array_replace($conf, $options['curl']);
47 }
48 $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
49 $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init();
50 \curl_setopt_array($easy->handle, $conf);
51 return $easy;
52 }
53 public function release(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy)
54 {
55 $resource = $easy->handle;
56 unset($easy->handle);
57 if (\count($this->handles) >= $this->maxHandles) {
58 \curl_close($resource);
59 } else {
60 // Remove all callback functions as they can hold onto references
61 // and are not cleaned up by curl_reset. Using curl_setopt_array
62 // does not work for some reason, so removing each one
63 // individually.
64 \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null);
65 \curl_setopt($resource, \CURLOPT_READFUNCTION, null);
66 \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null);
67 \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null);
68 \curl_reset($resource);
69 $this->handles[] = $resource;
70 }
71 }
72 /**
73 * Completes a cURL transaction, either returning a response promise or a
74 * rejected promise.
75 *
76 * @param callable $handler
77 * @param EasyHandle $easy
78 * @param CurlFactoryInterface $factory Dictates how the handle is released
79 *
80 * @return \GuzzleHttp\Promise\PromiseInterface
81 */
82 public static function finish(callable $handler, \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactoryInterface $factory)
83 {
84 if (isset($easy->options['on_stats'])) {
85 self::invokeStats($easy);
86 }
87 if (!$easy->response || $easy->errno) {
88 return self::finishError($handler, $easy, $factory);
89 }
90 // Return the response if it is present and there is no error.
91 $factory->release($easy);
92 // Rewind the body of the response if possible.
93 $body = $easy->response->getBody();
94 if ($body->isSeekable()) {
95 $body->rewind();
96 }
97 return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($easy->response);
98 }
99 private static function invokeStats(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy)
100 {
101 $curlStats = \curl_getinfo($easy->handle);
102 $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME);
103 $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats);
104 \call_user_func($easy->options['on_stats'], $stats);
105 }
106 private static function finishError(callable $handler, \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactoryInterface $factory)
107 {
108 // Get error information and release the handle to the factory.
109 $ctx = ['errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME)] + \curl_getinfo($easy->handle);
110 $ctx[self::CURL_VERSION_STR] = \curl_version()['version'];
111 $factory->release($easy);
112 // Retry when nothing is present or when curl failed to rewind.
113 if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
114 return self::retryFailedRewind($handler, $easy, $ctx);
115 }
116 return self::createRejection($easy, $ctx);
117 }
118 private static function createRejection(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $ctx)
119 {
120 static $connectionErrors = [\CURLE_OPERATION_TIMEOUTED => \true, \CURLE_COULDNT_RESOLVE_HOST => \true, \CURLE_COULDNT_CONNECT => \true, \CURLE_SSL_CONNECT_ERROR => \true, \CURLE_GOT_NOTHING => \true];
121 // If an exception was encountered during the onHeaders event, then
122 // return a rejected promise that wraps that exception.
123 if ($easy->onHeadersException) {
124 return \YoastSEO_Vendor\GuzzleHttp\Promise\rejection_for(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx));
125 }
126 if (\version_compare($ctx[self::CURL_VERSION_STR], self::LOW_CURL_VERSION_NUMBER)) {
127 $message = \sprintf('cURL error %s: %s (%s)', $ctx['errno'], $ctx['error'], 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html');
128 } else {
129 $message = \sprintf('cURL error %s: %s (%s) for %s', $ctx['errno'], $ctx['error'], 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html', $easy->request->getUri());
130 }
131 // Create a connection exception if it was a specific error code.
132 $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);
133 return \YoastSEO_Vendor\GuzzleHttp\Promise\rejection_for($error);
134 }
135 private function getDefaultConf(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy)
136 {
137 $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 => 150];
138 if (\defined('CURLOPT_PROTOCOLS')) {
139 $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS;
140 }
141 $version = $easy->request->getProtocolVersion();
142 if ($version == 1.1) {
143 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
144 } elseif ($version == 2.0) {
145 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
146 } else {
147 $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
148 }
149 return $conf;
150 }
151 private function applyMethod(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array &$conf)
152 {
153 $body = $easy->request->getBody();
154 $size = $body->getSize();
155 if ($size === null || $size > 0) {
156 $this->applyBody($easy->request, $easy->options, $conf);
157 return;
158 }
159 $method = $easy->request->getMethod();
160 if ($method === 'PUT' || $method === 'POST') {
161 // See http://tools.ietf.org/html/rfc7230#section-3.3.2
162 if (!$easy->request->hasHeader('Content-Length')) {
163 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
164 }
165 } elseif ($method === 'HEAD') {
166 $conf[\CURLOPT_NOBODY] = \true;
167 unset($conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE]);
168 }
169 }
170 private function applyBody(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, array &$conf)
171 {
172 $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null;
173 // Send the body as a string if the size is less than 1MB OR if the
174 // [curl][body_as_string] request value is set.
175 if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) {
176 $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
177 // Don't duplicate the Content-Length header
178 $this->removeHeader('Content-Length', $conf);
179 $this->removeHeader('Transfer-Encoding', $conf);
180 } else {
181 $conf[\CURLOPT_UPLOAD] = \true;
182 if ($size !== null) {
183 $conf[\CURLOPT_INFILESIZE] = $size;
184 $this->removeHeader('Content-Length', $conf);
185 }
186 $body = $request->getBody();
187 if ($body->isSeekable()) {
188 $body->rewind();
189 }
190 $conf[\CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use($body) {
191 return $body->read($length);
192 };
193 }
194 // If the Expect header is not present, prevent curl from adding it
195 if (!$request->hasHeader('Expect')) {
196 $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
197 }
198 // cURL sometimes adds a content-type by default. Prevent this.
199 if (!$request->hasHeader('Content-Type')) {
200 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
201 }
202 }
203 private function applyHeaders(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array &$conf)
204 {
205 foreach ($conf['_headers'] as $name => $values) {
206 foreach ($values as $value) {
207 $value = (string) $value;
208 if ($value === '') {
209 // cURL requires a special format for empty headers.
210 // See https://github.com/guzzle/guzzle/issues/1882 for more details.
211 $conf[\CURLOPT_HTTPHEADER][] = "{$name};";
212 } else {
213 $conf[\CURLOPT_HTTPHEADER][] = "{$name}: {$value}";
214 }
215 }
216 }
217 // Remove the Accept header if one was not set
218 if (!$easy->request->hasHeader('Accept')) {
219 $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
220 }
221 }
222 /**
223 * Remove a header from the options array.
224 *
225 * @param string $name Case-insensitive header to remove
226 * @param array $options Array of options to modify
227 */
228 private function removeHeader($name, array &$options)
229 {
230 foreach (\array_keys($options['_headers']) as $key) {
231 if (!\strcasecmp($key, $name)) {
232 unset($options['_headers'][$key]);
233 return;
234 }
235 }
236 }
237 private function applyHandlerOptions(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array &$conf)
238 {
239 $options = $easy->options;
240 if (isset($options['verify'])) {
241 if ($options['verify'] === \false) {
242 unset($conf[\CURLOPT_CAINFO]);
243 $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
244 $conf[\CURLOPT_SSL_VERIFYPEER] = \false;
245 } else {
246 $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
247 $conf[\CURLOPT_SSL_VERIFYPEER] = \true;
248 if (\is_string($options['verify'])) {
249 // Throw an error if the file/folder/link path is not valid or doesn't exist.
250 if (!\file_exists($options['verify'])) {
251 throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
252 }
253 // If it's a directory or a link to a directory use CURLOPT_CAPATH.
254 // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
255 if (\is_dir($options['verify']) || \is_link($options['verify']) && \is_dir(\readlink($options['verify']))) {
256 $conf[\CURLOPT_CAPATH] = $options['verify'];
257 } else {
258 $conf[\CURLOPT_CAINFO] = $options['verify'];
259 }
260 }
261 }
262 }
263 if (!empty($options['decode_content'])) {
264 $accept = $easy->request->getHeaderLine('Accept-Encoding');
265 if ($accept) {
266 $conf[\CURLOPT_ENCODING] = $accept;
267 } else {
268 $conf[\CURLOPT_ENCODING] = '';
269 // Don't let curl send the header over the wire
270 $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
271 }
272 }
273 if (isset($options['sink'])) {
274 $sink = $options['sink'];
275 if (!\is_string($sink)) {
276 $sink = \YoastSEO_Vendor\GuzzleHttp\Psr7\stream_for($sink);
277 } elseif (!\is_dir(\dirname($sink))) {
278 // Ensure that the directory exists before failing in curl.
279 throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink));
280 } else {
281 $sink = new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+');
282 }
283 $easy->sink = $sink;
284 $conf[\CURLOPT_WRITEFUNCTION] = function ($ch, $write) use($sink) {
285 return $sink->write($write);
286 };
287 } else {
288 // Use a default temp stream if no sink was set.
289 $conf[\CURLOPT_FILE] = \fopen('php://temp', 'w+');
290 $easy->sink = \YoastSEO_Vendor\GuzzleHttp\Psr7\stream_for($conf[\CURLOPT_FILE]);
291 }
292 $timeoutRequiresNoSignal = \false;
293 if (isset($options['timeout'])) {
294 $timeoutRequiresNoSignal |= $options['timeout'] < 1;
295 $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
296 }
297 // CURL default value is CURL_IPRESOLVE_WHATEVER
298 if (isset($options['force_ip_resolve'])) {
299 if ('v4' === $options['force_ip_resolve']) {
300 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
301 } elseif ('v6' === $options['force_ip_resolve']) {
302 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
303 }
304 }
305 if (isset($options['connect_timeout'])) {
306 $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
307 $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
308 }
309 if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') {
310 $conf[\CURLOPT_NOSIGNAL] = \true;
311 }
312 if (isset($options['proxy'])) {
313 if (!\is_array($options['proxy'])) {
314 $conf[\CURLOPT_PROXY] = $options['proxy'];
315 } else {
316 $scheme = $easy->request->getUri()->getScheme();
317 if (isset($options['proxy'][$scheme])) {
318 $host = $easy->request->getUri()->getHost();
319 if (!isset($options['proxy']['no']) || !\YoastSEO_Vendor\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])) {
320 $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
321 }
322 }
323 }
324 }
325 if (isset($options['cert'])) {
326 $cert = $options['cert'];
327 if (\is_array($cert)) {
328 $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
329 $cert = $cert[0];
330 }
331 if (!\file_exists($cert)) {
332 throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
333 }
334 $conf[\CURLOPT_SSLCERT] = $cert;
335 }
336 if (isset($options['ssl_key'])) {
337 if (\is_array($options['ssl_key'])) {
338 if (\count($options['ssl_key']) === 2) {
339 list($sslKey, $conf[\CURLOPT_SSLKEYPASSWD]) = $options['ssl_key'];
340 } else {
341 list($sslKey) = $options['ssl_key'];
342 }
343 }
344 $sslKey = isset($sslKey) ? $sslKey : $options['ssl_key'];
345 if (!\file_exists($sslKey)) {
346 throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
347 }
348 $conf[\CURLOPT_SSLKEY] = $sslKey;
349 }
350 if (isset($options['progress'])) {
351 $progress = $options['progress'];
352 if (!\is_callable($progress)) {
353 throw new \InvalidArgumentException('progress client option must be callable');
354 }
355 $conf[\CURLOPT_NOPROGRESS] = \false;
356 $conf[\CURLOPT_PROGRESSFUNCTION] = function () use($progress) {
357 $args = \func_get_args();
358 // PHP 5.5 pushed the handle onto the start of the args
359 if (\is_resource($args[0])) {
360 \array_shift($args);
361 }
362 \call_user_func_array($progress, $args);
363 };
364 }
365 if (!empty($options['debug'])) {
366 $conf[\CURLOPT_STDERR] = \YoastSEO_Vendor\GuzzleHttp\debug_resource($options['debug']);
367 $conf[\CURLOPT_VERBOSE] = \true;
368 }
369 }
370 /**
371 * This function ensures that a response was set on a transaction. If one
372 * was not set, then the request is retried if possible. This error
373 * typically means you are sending a payload, curl encountered a
374 * "Connection died, retrying a fresh connect" error, tried to rewind the
375 * stream, and then encountered a "necessary data rewind wasn't possible"
376 * error, causing the request to be sent through curl_multi_info_read()
377 * without an error status.
378 */
379 private static function retryFailedRewind(callable $handler, \YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $ctx)
380 {
381 try {
382 // Only rewind if the body has been read from.
383 $body = $easy->request->getBody();
384 if ($body->tell() > 0) {
385 $body->rewind();
386 }
387 } catch (\RuntimeException $e) {
388 $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;
389 return self::createRejection($easy, $ctx);
390 }
391 // Retry no more than 3 times before giving up.
392 if (!isset($easy->options['_curl_retries'])) {
393 $easy->options['_curl_retries'] = 1;
394 } elseif ($easy->options['_curl_retries'] == 2) {
395 $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.';
396 return self::createRejection($easy, $ctx);
397 } else {
398 $easy->options['_curl_retries']++;
399 }
400 return $handler($easy->request, $easy->options);
401 }
402 private function createHeaderFn(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy)
403 {
404 if (isset($easy->options['on_headers'])) {
405 $onHeaders = $easy->options['on_headers'];
406 if (!\is_callable($onHeaders)) {
407 throw new \InvalidArgumentException('on_headers must be callable');
408 }
409 } else {
410 $onHeaders = null;
411 }
412 return function ($ch, $h) use($onHeaders, $easy, &$startingResponse) {
413 $value = \trim($h);
414 if ($value === '') {
415 $startingResponse = \true;
416 $easy->createResponse();
417 if ($onHeaders !== null) {
418 try {
419 $onHeaders($easy->response);
420 } catch (\Exception $e) {
421 // Associate the exception with the handle and trigger
422 // a curl header write error by returning 0.
423 $easy->onHeadersException = $e;
424 return -1;
425 }
426 }
427 } elseif ($startingResponse) {
428 $startingResponse = \false;
429 $easy->headers = [$value];
430 } else {
431 $easy->headers[] = $value;
432 }
433 return \strlen($h);
434 };
435 }
436 }
437