Completed
Branch master (f58c14)
by Andrey
07:50
created

CurlFactory::applyHandlerOptions()   F

Complexity

Conditions 35
Paths > 20000

Size

Total Lines 155

Duplication

Lines 34
Ratio 21.94 %

Importance

Changes 0
Metric Value
cc 35
nc 381616
nop 2
dl 34
loc 155
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace GuzzleHttp\Handler;
3
4
use GuzzleHttp\Exception\RequestException;
5
use GuzzleHttp\Exception\ConnectException;
6
use GuzzleHttp\Promise\FulfilledPromise;
7
use GuzzleHttp\Promise\RejectedPromise;
8
use GuzzleHttp\Psr7;
9
use GuzzleHttp\Psr7\LazyOpenStream;
10
use GuzzleHttp\TransferStats;
11
use Psr\Http\Message\RequestInterface;
12
13
/**
14
 * Creates curl resources from a request
15
 */
16
class CurlFactory implements CurlFactoryInterface
17
{
18
    /** @var array */
19
    private $handles = [];
20
21
    /** @var int Total number of idle handles to keep in cache */
22
    private $maxHandles;
23
24
    /**
25
     * @param int $maxHandles Maximum number of idle handles.
26
     */
27
    public function __construct($maxHandles)
28
    {
29
        $this->maxHandles = $maxHandles;
30
    }
31
32
    public function create(RequestInterface $request, array $options)
33
    {
34
        if (isset($options['curl']['body_as_string'])) {
35
            $options['_body_as_string'] = $options['curl']['body_as_string'];
36
            unset($options['curl']['body_as_string']);
37
        }
38
39
        $easy = new EasyHandle;
40
        $easy->request = $request;
41
        $easy->options = $options;
42
        $conf = $this->getDefaultConf($easy);
43
        $this->applyMethod($easy, $conf);
44
        $this->applyHandlerOptions($easy, $conf);
45
        $this->applyHeaders($easy, $conf);
46
        unset($conf['_headers']);
47
48
        // Add handler options from the request configuration options
49
        if (isset($options['curl'])) {
50
            $conf = array_replace($conf, $options['curl']);
51
        }
52
53
        $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
54
        $easy->handle = $this->handles
55
            ? array_pop($this->handles)
56
            : curl_init();
57
        curl_setopt_array($easy->handle, $conf);
58
59
        return $easy;
60
    }
61
62
    public function release(EasyHandle $easy)
63
    {
64
        $resource = $easy->handle;
65
        unset($easy->handle);
66
67
        if (count($this->handles) >= $this->maxHandles) {
68
            curl_close($resource);
69
        } else {
70
            // Remove all callback functions as they can hold onto references
71
            // and are not cleaned up by curl_reset. Using curl_setopt_array
72
            // does not work for some reason, so removing each one
73
            // individually.
74
            curl_setopt($resource, CURLOPT_HEADERFUNCTION, null);
75
            curl_setopt($resource, CURLOPT_READFUNCTION, null);
76
            curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
77
            curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
78
            curl_reset($resource);
79
            $this->handles[] = $resource;
80
        }
81
    }
82
83
    /**
84
     * Completes a cURL transaction, either returning a response promise or a
85
     * rejected promise.
86
     *
87
     * @param callable             $handler
88
     * @param EasyHandle           $easy
89
     * @param CurlFactoryInterface $factory Dictates how the handle is released
90
     *
91
     * @return \GuzzleHttp\Promise\PromiseInterface
92
     */
93
    public static function finish(
94
        callable $handler,
95
        EasyHandle $easy,
96
        CurlFactoryInterface $factory
97
    ) {
98
        if (isset($easy->options['on_stats'])) {
99
            self::invokeStats($easy);
100
        }
101
102
        if (!$easy->response || $easy->errno) {
103
            return self::finishError($handler, $easy, $factory);
104
        }
105
106
        // Return the response if it is present and there is no error.
107
        $factory->release($easy);
108
109
        // Rewind the body of the response if possible.
110
        $body = $easy->response->getBody();
111
        if ($body->isSeekable()) {
112
            $body->rewind();
113
        }
114
115
        return new FulfilledPromise($easy->response);
116
    }
117
118
    private static function invokeStats(EasyHandle $easy)
119
    {
120
        $curlStats = curl_getinfo($easy->handle);
121
        $stats = new TransferStats(
122
            $easy->request,
123
            $easy->response,
124
            $curlStats['total_time'],
125
            $easy->errno,
126
            $curlStats
127
        );
128
        call_user_func($easy->options['on_stats'], $stats);
129
    }
130
131
    private static function finishError(
132
        callable $handler,
133
        EasyHandle $easy,
134
        CurlFactoryInterface $factory
135
    ) {
136
        // Get error information and release the handle to the factory.
137
        $ctx = [
138
            'errno' => $easy->errno,
139
            'error' => curl_error($easy->handle),
140
        ] + curl_getinfo($easy->handle);
141
        $factory->release($easy);
142
143
        // Retry when nothing is present or when curl failed to rewind.
144
        if (empty($easy->options['_err_message'])
145
            && (!$easy->errno || $easy->errno == 65)
146
        ) {
147
            return self::retryFailedRewind($handler, $easy, $ctx);
148
        }
149
150
        return self::createRejection($easy, $ctx);
151
    }
152
153
    private static function createRejection(EasyHandle $easy, array $ctx)
154
    {
155
        static $connectionErrors = [
156
            CURLE_OPERATION_TIMEOUTED  => true,
157
            CURLE_COULDNT_RESOLVE_HOST => true,
158
            CURLE_COULDNT_CONNECT      => true,
159
            CURLE_SSL_CONNECT_ERROR    => true,
160
            CURLE_GOT_NOTHING          => true,
161
        ];
162
163
        // If an exception was encountered during the onHeaders event, then
164
        // return a rejected promise that wraps that exception.
165
        if ($easy->onHeadersException) {
166
            return \GuzzleHttp\Promise\rejection_for(
167
                new RequestException(
168
                    'An error was encountered during the on_headers event',
169
                    $easy->request,
170
                    $easy->response,
171
                    $easy->onHeadersException,
172
                    $ctx
173
                )
174
            );
175
        }
176
177
        $message = sprintf(
178
            'cURL error %s: %s (%s)',
179
            $ctx['errno'],
180
            $ctx['error'],
181
            'see http://curl.haxx.se/libcurl/c/libcurl-errors.html'
182
        );
183
184
        // Create a connection exception if it was a specific error code.
185
        $error = isset($connectionErrors[$easy->errno])
186
            ? new ConnectException($message, $easy->request, null, $ctx)
187
            : new RequestException($message, $easy->request, $easy->response, null, $ctx);
188
189
        return \GuzzleHttp\Promise\rejection_for($error);
190
    }
191
192
    private function getDefaultConf(EasyHandle $easy)
193
    {
194
        $conf = [
195
            '_headers'             => $easy->request->getHeaders(),
196
            CURLOPT_CUSTOMREQUEST  => $easy->request->getMethod(),
197
            CURLOPT_URL            => (string) $easy->request->getUri()->withFragment(''),
198
            CURLOPT_RETURNTRANSFER => false,
199
            CURLOPT_HEADER         => false,
200
            CURLOPT_CONNECTTIMEOUT => 150,
201
        ];
202
203
        if (defined('CURLOPT_PROTOCOLS')) {
204
            $conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
205
        }
206
207
        $version = $easy->request->getProtocolVersion();
208
        if ($version == 1.1) {
209
            $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
210
        } elseif ($version == 2.0) {
211
            $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
212
        } else {
213
            $conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
214
        }
215
216
        return $conf;
217
    }
218
219
    private function applyMethod(EasyHandle $easy, array &$conf)
220
    {
221
        $body = $easy->request->getBody();
222
        $size = $body->getSize();
223
224
        if ($size === null || $size > 0) {
225
            $this->applyBody($easy->request, $easy->options, $conf);
226
            return;
227
        }
228
229
        $method = $easy->request->getMethod();
230
        if ($method === 'PUT' || $method === 'POST') {
231
            // See http://tools.ietf.org/html/rfc7230#section-3.3.2
232
            if (!$easy->request->hasHeader('Content-Length')) {
233
                $conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
234
            }
235
        } elseif ($method === 'HEAD') {
236
            $conf[CURLOPT_NOBODY] = true;
237
            unset(
238
                $conf[CURLOPT_WRITEFUNCTION],
239
                $conf[CURLOPT_READFUNCTION],
240
                $conf[CURLOPT_FILE],
241
                $conf[CURLOPT_INFILE]
242
            );
243
        }
244
    }
245
246
    private function applyBody(RequestInterface $request, array $options, array &$conf)
247
    {
248
        $size = $request->hasHeader('Content-Length')
249
            ? (int) $request->getHeaderLine('Content-Length')
250
            : null;
251
252
        // Send the body as a string if the size is less than 1MB OR if the
253
        // [curl][body_as_string] request value is set.
254
        if (($size !== null && $size < 1000000) ||
255
            !empty($options['_body_as_string'])
256
        ) {
257
            $conf[CURLOPT_POSTFIELDS] = (string) $request->getBody();
258
            // Don't duplicate the Content-Length header
259
            $this->removeHeader('Content-Length', $conf);
260
            $this->removeHeader('Transfer-Encoding', $conf);
261
        } else {
262
            $conf[CURLOPT_UPLOAD] = true;
263
            if ($size !== null) {
264
                $conf[CURLOPT_INFILESIZE] = $size;
265
                $this->removeHeader('Content-Length', $conf);
266
            }
267
            $body = $request->getBody();
268
            if ($body->isSeekable()) {
269
                $body->rewind();
270
            }
271
            $conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
272
                return $body->read($length);
273
            };
274
        }
275
276
        // If the Expect header is not present, prevent curl from adding it
277
        if (!$request->hasHeader('Expect')) {
278
            $conf[CURLOPT_HTTPHEADER][] = 'Expect:';
279
        }
280
281
        // cURL sometimes adds a content-type by default. Prevent this.
282
        if (!$request->hasHeader('Content-Type')) {
283
            $conf[CURLOPT_HTTPHEADER][] = 'Content-Type:';
284
        }
285
    }
286
287
    private function applyHeaders(EasyHandle $easy, array &$conf)
288
    {
289
        foreach ($conf['_headers'] as $name => $values) {
290
            foreach ($values as $value) {
291
                $conf[CURLOPT_HTTPHEADER][] = "$name: $value";
292
            }
293
        }
294
295
        // Remove the Accept header if one was not set
296
        if (!$easy->request->hasHeader('Accept')) {
297
            $conf[CURLOPT_HTTPHEADER][] = 'Accept:';
298
        }
299
    }
300
301
    /**
302
     * Remove a header from the options array.
303
     *
304
     * @param string $name    Case-insensitive header to remove
305
     * @param array  $options Array of options to modify
306
     */
307
    private function removeHeader($name, array &$options)
308
    {
309
        foreach (array_keys($options['_headers']) as $key) {
310
            if (!strcasecmp($key, $name)) {
311
                unset($options['_headers'][$key]);
312
                return;
313
            }
314
        }
315
    }
316
317
    private function applyHandlerOptions(EasyHandle $easy, array &$conf)
318
    {
319
        $options = $easy->options;
320
        if (isset($options['verify'])) {
321
            if ($options['verify'] === false) {
322
                unset($conf[CURLOPT_CAINFO]);
323
                $conf[CURLOPT_SSL_VERIFYHOST] = 0;
324
                $conf[CURLOPT_SSL_VERIFYPEER] = false;
325
            } else {
326
                $conf[CURLOPT_SSL_VERIFYHOST] = 2;
327
                $conf[CURLOPT_SSL_VERIFYPEER] = true;
328
                if (is_string($options['verify'])) {
329
                    // Throw an error if the file/folder/link path is not valid or doesn't exist.
330
                    if (!file_exists($options['verify'])) {
331
                        throw new \InvalidArgumentException(
332
                            "SSL CA bundle not found: {$options['verify']}"
333
                        );
334
                    }
335
                    // If it's a directory or a link to a directory use CURLOPT_CAPATH.
336
                    // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
337
                    if (is_dir($options['verify']) ||
338
                        (is_link($options['verify']) && is_dir(readlink($options['verify'])))) {
339
                        $conf[CURLOPT_CAPATH] = $options['verify'];
340
                    } else {
341
                        $conf[CURLOPT_CAINFO] = $options['verify'];
342
                    }
343
                }
344
            }
345
        }
346
347
        if (!empty($options['decode_content'])) {
348
            $accept = $easy->request->getHeaderLine('Accept-Encoding');
349
            if ($accept) {
350
                $conf[CURLOPT_ENCODING] = $accept;
351
            } else {
352
                $conf[CURLOPT_ENCODING] = '';
353
                // Don't let curl send the header over the wire
354
                $conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
355
            }
356
        }
357
358
        if (isset($options['sink'])) {
359
            $sink = $options['sink'];
360
            if (!is_string($sink)) {
361
                $sink = \GuzzleHttp\Psr7\stream_for($sink);
362
            } elseif (!is_dir(dirname($sink))) {
363
                // Ensure that the directory exists before failing in curl.
364
                throw new \RuntimeException(sprintf(
365
                    'Directory %s does not exist for sink value of %s',
366
                    dirname($sink),
367
                    $sink
368
                ));
369
            } else {
370
                $sink = new LazyOpenStream($sink, 'w+');
371
            }
372
            $easy->sink = $sink;
373
            $conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
374
                return $sink->write($write);
375
            };
376
        } else {
377
            // Use a default temp stream if no sink was set.
378
            $conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
379
            $easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]);
380
        }
381
        $timeoutRequiresNoSignal = false;
382 View Code Duplication
        if (isset($options['timeout'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
383
            $timeoutRequiresNoSignal |= $options['timeout'] < 1;
384
            $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
385
        }
386
387
        // CURL default value is CURL_IPRESOLVE_WHATEVER
388
        if (isset($options['force_ip_resolve'])) {
389
            if ('v4' === $options['force_ip_resolve']) {
390
                $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
391
            } else if ('v6' === $options['force_ip_resolve']) {
392
                $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
393
            }
394
        }
395
396 View Code Duplication
        if (isset($options['connect_timeout'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
397
            $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
398
            $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
399
        }
400
401
        if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
402
            $conf[CURLOPT_NOSIGNAL] = true;
403
        }
404
405
        if (isset($options['proxy'])) {
406
            if (!is_array($options['proxy'])) {
407
                $conf[CURLOPT_PROXY] = $options['proxy'];
408
            } else {
409
                $scheme = $easy->request->getUri()->getScheme();
410
                if (isset($options['proxy'][$scheme])) {
411
                    $host = $easy->request->getUri()->getHost();
412
                    if (!isset($options['proxy']['no']) ||
413
                        !\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
414
                    ) {
415
                        $conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
416
                    }
417
                }
418
            }
419
        }
420
421 View Code Duplication
        if (isset($options['cert'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
422
            $cert = $options['cert'];
423
            if (is_array($cert)) {
424
                $conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
425
                $cert = $cert[0];
426
            }
427
            if (!file_exists($cert)) {
428
                throw new \InvalidArgumentException(
429
                    "SSL certificate not found: {$cert}"
430
                );
431
            }
432
            $conf[CURLOPT_SSLCERT] = $cert;
433
        }
434
435 View Code Duplication
        if (isset($options['ssl_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
436
            $sslKey = $options['ssl_key'];
437
            if (is_array($sslKey)) {
438
                $conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1];
439
                $sslKey = $sslKey[0];
440
            }
441
            if (!file_exists($sslKey)) {
442
                throw new \InvalidArgumentException(
443
                    "SSL private key not found: {$sslKey}"
444
                );
445
            }
446
            $conf[CURLOPT_SSLKEY] = $sslKey;
447
        }
448
449
        if (isset($options['progress'])) {
450
            $progress = $options['progress'];
451
            if (!is_callable($progress)) {
452
                throw new \InvalidArgumentException(
453
                    'progress client option must be callable'
454
                );
455
            }
456
            $conf[CURLOPT_NOPROGRESS] = false;
457
            $conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
458
                $args = func_get_args();
459
                // PHP 5.5 pushed the handle onto the start of the args
460
                if (is_resource($args[0])) {
461
                    array_shift($args);
462
                }
463
                call_user_func_array($progress, $args);
464
            };
465
        }
466
467
        if (!empty($options['debug'])) {
468
            $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
469
            $conf[CURLOPT_VERBOSE] = true;
470
        }
471
    }
472
473
    /**
474
     * This function ensures that a response was set on a transaction. If one
475
     * was not set, then the request is retried if possible. This error
476
     * typically means you are sending a payload, curl encountered a
477
     * "Connection died, retrying a fresh connect" error, tried to rewind the
478
     * stream, and then encountered a "necessary data rewind wasn't possible"
479
     * error, causing the request to be sent through curl_multi_info_read()
480
     * without an error status.
481
     */
482
    private static function retryFailedRewind(
483
        callable $handler,
484
        EasyHandle $easy,
485
        array $ctx
486
    ) {
487
        try {
488
            // Only rewind if the body has been read from.
489
            $body = $easy->request->getBody();
490
            if ($body->tell() > 0) {
491
                $body->rewind();
492
            }
493
        } catch (\RuntimeException $e) {
494
            $ctx['error'] = 'The connection unexpectedly failed without '
495
                . 'providing an error. The request would have been retried, '
496
                . 'but attempting to rewind the request body failed. '
497
                . 'Exception: ' . $e;
498
            return self::createRejection($easy, $ctx);
499
        }
500
501
        // Retry no more than 3 times before giving up.
502
        if (!isset($easy->options['_curl_retries'])) {
503
            $easy->options['_curl_retries'] = 1;
504
        } elseif ($easy->options['_curl_retries'] == 2) {
505
            $ctx['error'] = 'The cURL request was retried 3 times '
506
                . 'and did not succeed. The most likely reason for the failure '
507
                . 'is that cURL was unable to rewind the body of the request '
508
                . 'and subsequent retries resulted in the same error. Turn on '
509
                . 'the debug option to see what went wrong. See '
510
                . '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
516
        return $handler($easy->request, $easy->options);
517
    }
518
519
    private function createHeaderFn(EasyHandle $easy)
520
    {
521
        if (isset($easy->options['on_headers'])) {
522
            $onHeaders = $easy->options['on_headers'];
523
524
            if (!is_callable($onHeaders)) {
525
                throw new \InvalidArgumentException('on_headers must be callable');
526
            }
527
        } else {
528
            $onHeaders = null;
529
        }
530
531
        return function ($ch, $h) use (
532
            $onHeaders,
533
            $easy,
534
            &$startingResponse
535
        ) {
536
            $value = trim($h);
537
            if ($value === '') {
538
                $startingResponse = true;
539
                $easy->createResponse();
540
                if ($onHeaders !== null) {
541
                    try {
542
                        $onHeaders($easy->response);
543
                    } catch (\Exception $e) {
544
                        // Associate the exception with the handle and trigger
545
                        // a curl header write error by returning 0.
546
                        $easy->onHeadersException = $e;
547
                        return -1;
548
                    }
549
                }
550
            } elseif ($startingResponse) {
551
                $startingResponse = false;
552
                $easy->headers = [$value];
553
            } else {
554
                $easy->headers[] = $value;
555
            }
556
            return strlen($h);
557
        };
558
    }
559
}
560