PluginProbe
StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more / 2.4.1
StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more v2.4.1
3.7.2 3.7.1 3.7.0 3.6.0 3.6.1 3.5.2 trunk 2.0 2.1 2.2 2.3 2.4 2.4.1 2.4.2 2.4.3 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.6.0 2.6.1 All 73 releases
stockpack / vendor / guzzlehttp / guzzle / src / Handler / CurlFactory.php

CurlFactory.php in StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more 2.4.1, at vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php

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