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