Completed
Push — master ( 8d12ff...458df2 )
by Guillaume
02:56
created

CurlFactory::applyHandlerOptions()   F

Complexity

Conditions 27
Paths > 20000

Size

Total Lines 132
Code Lines 85

Duplication

Lines 26
Ratio 19.7 %

Importance

Changes 0
Metric Value
cc 27
eloc 85
nc 38173
nop 2
dl 26
loc 132
rs 2
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 new RejectedPromise(
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 new RejectedPromise($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
                    $conf[CURLOPT_CAINFO] = $options['verify'];
330
                    if (!file_exists($options['verify'])) {
331
                        throw new \InvalidArgumentException(
332
                            "SSL CA bundle not found: {$options['verify']}"
333
                        );
334
                    }
335
                }
336
            }
337
        }
338
339
        if (!empty($options['decode_content'])) {
340
            $accept = $easy->request->getHeaderLine('Accept-Encoding');
341
            if ($accept) {
342
                $conf[CURLOPT_ENCODING] = $accept;
343
            } else {
344
                $conf[CURLOPT_ENCODING] = '';
345
                // Don't let curl send the header over the wire
346
                $conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
347
            }
348
        }
349
350
        if (isset($options['sink'])) {
351
            $sink = $options['sink'];
352
            if (!is_string($sink)) {
353
                $sink = \GuzzleHttp\Psr7\stream_for($sink);
354
            } elseif (!is_dir(dirname($sink))) {
355
                // Ensure that the directory exists before failing in curl.
356
                throw new \RuntimeException(sprintf(
357
                    'Directory %s does not exist for sink value of %s',
358
                    dirname($sink),
359
                    $sink
360
                ));
361
            } else {
362
                $sink = new LazyOpenStream($sink, 'w+');
363
            }
364
            $easy->sink = $sink;
365
            $conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) {
366
                return $sink->write($write);
367
            };
368
        } else {
369
            // Use a default temp stream if no sink was set.
370
            $conf[CURLOPT_FILE] = fopen('php://temp', 'w+');
371
            $easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]);
372
        }
373
374
        if (isset($options['timeout'])) {
375
            $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
376
        }
377
378
        if (isset($options['connect_timeout'])) {
379
            $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
380
        }
381
382
        if (isset($options['proxy'])) {
383
            if (!is_array($options['proxy'])) {
384
                $conf[CURLOPT_PROXY] = $options['proxy'];
385
            } else {
386
                $scheme = $easy->request->getUri()->getScheme();
387
                if (isset($options['proxy'][$scheme])) {
388
                    $host = $easy->request->getUri()->getHost();
389
                    if (!isset($options['proxy']['no']) ||
390
                        !\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no'])
391
                    ) {
392
                        $conf[CURLOPT_PROXY] = $options['proxy'][$scheme];
393
                    }
394
                }
395
            }
396
        }
397
398 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...
399
            $cert = $options['cert'];
400
            if (is_array($cert)) {
401
                $conf[CURLOPT_SSLCERTPASSWD] = $cert[1];
402
                $cert = $cert[0];
403
            }
404
            if (!file_exists($cert)) {
405
                throw new \InvalidArgumentException(
406
                    "SSL certificate not found: {$cert}"
407
                );
408
            }
409
            $conf[CURLOPT_SSLCERT] = $cert;
410
        }
411
412 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...
413
            $sslKey = $options['ssl_key'];
414
            if (is_array($sslKey)) {
415
                $conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1];
416
                $sslKey = $sslKey[0];
417
            }
418
            if (!file_exists($sslKey)) {
419
                throw new \InvalidArgumentException(
420
                    "SSL private key not found: {$sslKey}"
421
                );
422
            }
423
            $conf[CURLOPT_SSLKEY] = $sslKey;
424
        }
425
426
        if (isset($options['progress'])) {
427
            $progress = $options['progress'];
428
            if (!is_callable($progress)) {
429
                throw new \InvalidArgumentException(
430
                    'progress client option must be callable'
431
                );
432
            }
433
            $conf[CURLOPT_NOPROGRESS] = false;
434
            $conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) {
435
                $args = func_get_args();
436
                // PHP 5.5 pushed the handle onto the start of the args
437
                if (is_resource($args[0])) {
438
                    array_shift($args);
439
                }
440
                call_user_func_array($progress, $args);
441
            };
442
        }
443
444
        if (!empty($options['debug'])) {
445
            $conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']);
446
            $conf[CURLOPT_VERBOSE] = true;
447
        }
448
    }
449
450
    /**
451
     * This function ensures that a response was set on a transaction. If one
452
     * was not set, then the request is retried if possible. This error
453
     * typically means you are sending a payload, curl encountered a
454
     * "Connection died, retrying a fresh connect" error, tried to rewind the
455
     * stream, and then encountered a "necessary data rewind wasn't possible"
456
     * error, causing the request to be sent through curl_multi_info_read()
457
     * without an error status.
458
     */
459
    private static function retryFailedRewind(
460
        callable $handler,
461
        EasyHandle $easy,
462
        array $ctx
463
    ) {
464
        try {
465
            // Only rewind if the body has been read from.
466
            $body = $easy->request->getBody();
467
            if ($body->tell() > 0) {
468
                $body->rewind();
469
            }
470
        } catch (\RuntimeException $e) {
471
            $ctx['error'] = 'The connection unexpectedly failed without '
472
                . 'providing an error. The request would have been retried, '
473
                . 'but attempting to rewind the request body failed. '
474
                . 'Exception: ' . $e;
475
            return self::createRejection($easy, $ctx);
476
        }
477
478
        // Retry no more than 3 times before giving up.
479
        if (!isset($easy->options['_curl_retries'])) {
480
            $easy->options['_curl_retries'] = 1;
481
        } elseif ($easy->options['_curl_retries'] == 2) {
482
            $ctx['error'] = 'The cURL request was retried 3 times '
483
                . 'and did not succeed. The most likely reason for the failure '
484
                . 'is that cURL was unable to rewind the body of the request '
485
                . 'and subsequent retries resulted in the same error. Turn on '
486
                . 'the debug option to see what went wrong. See '
487
                . 'https://bugs.php.net/bug.php?id=47204 for more information.';
488
            return self::createRejection($easy, $ctx);
489
        } else {
490
            $easy->options['_curl_retries']++;
491
        }
492
493
        return $handler($easy->request, $easy->options);
494
    }
495
496
    private function createHeaderFn(EasyHandle $easy)
497
    {
498
        if (isset($easy->options['on_headers'])) {
499
            $onHeaders = $easy->options['on_headers'];
500
501
            if (!is_callable($onHeaders)) {
502
                throw new \InvalidArgumentException('on_headers must be callable');
503
            }
504
        } else {
505
            $onHeaders = null;
506
        }
507
508
        return function ($ch, $h) use (
509
            $onHeaders,
510
            $easy,
511
            &$startingResponse
512
        ) {
513
            $value = trim($h);
514
            if ($value === '') {
515
                $startingResponse = true;
516
                $easy->createResponse();
517
                if ($onHeaders !== null) {
518
                    try {
519
                        $onHeaders($easy->response);
520
                    } catch (\Exception $e) {
521
                        // Associate the exception with the handle and trigger
522
                        // a curl header write error by returning 0.
523
                        $easy->onHeadersException = $e;
524
                        return -1;
525
                    }
526
                }
527
            } elseif ($startingResponse) {
528
                $startingResponse = false;
529
                $easy->headers = [$value];
530
            } else {
531
                $easy->headers[] = $value;
532
            }
533
            return strlen($h);
534
        };
535
    }
536
}
537