CurlHandle::factory()   F
last analyzed

Complexity

Conditions 31
Paths > 20000

Size

Total Lines 182
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 182
rs 2
c 0
b 0
f 0
cc 31
eloc 96
nc 101392
nop 1

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
3
namespace Guzzle\Http\Curl;
4
5
use Guzzle\Common\Exception\InvalidArgumentException;
6
use Guzzle\Common\Exception\RuntimeException;
7
use Guzzle\Common\Collection;
8
use Guzzle\Http\Message\EntityEnclosingRequest;
9
use Guzzle\Http\Message\RequestInterface;
10
use Guzzle\Parser\ParserRegistry;
11
use Guzzle\Http\Url;
12
13
/**
14
 * Immutable wrapper for a cURL handle
15
 */
16
class CurlHandle
17
{
18
    const BODY_AS_STRING = 'body_as_string';
19
    const PROGRESS = 'progress';
20
    const DEBUG = 'debug';
21
22
    /** @var Collection Curl options */
23
    protected $options;
24
25
    /** @var resource Curl resource handle */
26
    protected $handle;
27
28
    /** @var int CURLE_* error */
29
    protected $errorNo = CURLE_OK;
30
31
    /**
32
     * Factory method to create a new curl handle based on an HTTP request.
33
     *
34
     * There are some helpful options you can set to enable specific behavior:
35
     * - debug:    Set to true to enable cURL debug functionality to track the actual headers sent over the wire.
36
     * - progress: Set to true to enable progress function callbacks.
37
     *
38
     * @param RequestInterface $request Request
39
     *
40
     * @return CurlHandle
41
     * @throws RuntimeException
42
     */
43
    public static function factory(RequestInterface $request)
44
    {
45
        $requestCurlOptions = $request->getCurlOptions();
46
        $mediator = new RequestMediator($request, $requestCurlOptions->get('emit_io'));
47
        $tempContentLength = null;
48
        $method = $request->getMethod();
49
        $bodyAsString = $requestCurlOptions->get(self::BODY_AS_STRING);
50
51
        // Prepare url
52
        $url = (string)$request->getUrl();
53
        if(($pos = strpos($url, '#')) !== false ){
54
            // strip fragment from url
55
            $url = substr($url, 0, $pos);
56
        }
57
58
        // Array of default cURL options.
59
        $curlOptions = array(
60
            CURLOPT_URL            => $url,
61
            CURLOPT_CONNECTTIMEOUT => 150,
62
            CURLOPT_RETURNTRANSFER => false,
63
            CURLOPT_HEADER         => false,
64
            CURLOPT_PORT           => $request->getPort(),
65
            CURLOPT_HTTPHEADER     => array(),
66
            CURLOPT_WRITEFUNCTION  => array($mediator, 'writeResponseBody'),
67
            CURLOPT_HEADERFUNCTION => array($mediator, 'receiveResponseHeader'),
68
            CURLOPT_HTTP_VERSION   => $request->getProtocolVersion() === '1.0'
69
                ? CURL_HTTP_VERSION_1_0 : CURL_HTTP_VERSION_1_1,
70
            // Verifies the authenticity of the peer's certificate
71
            CURLOPT_SSL_VERIFYPEER => 1,
72
            // Certificate must indicate that the server is the server to which you meant to connect
73
            CURLOPT_SSL_VERIFYHOST => 2
74
        );
75
76
        if (defined('CURLOPT_PROTOCOLS')) {
77
            // Allow only HTTP and HTTPS protocols
78
            $curlOptions[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
79
        }
80
81
        // Add CURLOPT_ENCODING if Accept-Encoding header is provided
82
        if ($acceptEncodingHeader = $request->getHeader('Accept-Encoding')) {
83
            $curlOptions[CURLOPT_ENCODING] = (string) $acceptEncodingHeader;
84
            // Let cURL set the Accept-Encoding header, prevents duplicate values
85
            $request->removeHeader('Accept-Encoding');
86
        }
87
88
        // Enable curl debug information if the 'debug' param was set
89
        if ($requestCurlOptions->get('debug')) {
90
            $curlOptions[CURLOPT_STDERR] = fopen('php://temp', 'r+');
91
            // @codeCoverageIgnoreStart
92
            if (false === $curlOptions[CURLOPT_STDERR]) {
93
                throw new RuntimeException('Unable to create a stream for CURLOPT_STDERR');
94
            }
95
            // @codeCoverageIgnoreEnd
96
            $curlOptions[CURLOPT_VERBOSE] = true;
97
        }
98
99
        // Specify settings according to the HTTP method
100
        if ($method == 'GET') {
101
            $curlOptions[CURLOPT_HTTPGET] = true;
102
        } elseif ($method == 'HEAD') {
103
            $curlOptions[CURLOPT_NOBODY] = true;
104
            // HEAD requests do not use a write function
105
            unset($curlOptions[CURLOPT_WRITEFUNCTION]);
106
        } elseif (!($request instanceof EntityEnclosingRequest)) {
107
            $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
108
        } else {
109
110
            $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
111
112
            // Handle sending raw bodies in a request
113
            if ($request->getBody()) {
114
                // You can send the body as a string using curl's CURLOPT_POSTFIELDS
115
                if ($bodyAsString) {
116
                    $curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getBody();
117
                    // Allow curl to add the Content-Length for us to account for the times when
118
                    // POST redirects are followed by GET requests
119
                    if ($tempContentLength = $request->getHeader('Content-Length')) {
120
                        $tempContentLength = (int) (string) $tempContentLength;
121
                    }
122
                    // Remove the curl generated Content-Type header if none was set manually
123
                    if (!$request->hasHeader('Content-Type')) {
124
                        $curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:';
125
                    }
126
                } else {
127
                    $curlOptions[CURLOPT_UPLOAD] = true;
128
                    // Let cURL handle setting the Content-Length header
129
                    if ($tempContentLength = $request->getHeader('Content-Length')) {
130
                        $tempContentLength = (int) (string) $tempContentLength;
131
                        $curlOptions[CURLOPT_INFILESIZE] = $tempContentLength;
132
                    }
133
                    // Add a callback for curl to read data to send with the request only if a body was specified
134
                    $curlOptions[CURLOPT_READFUNCTION] = array($mediator, 'readRequestBody');
135
                    // Attempt to seek to the start of the stream
136
                    $request->getBody()->seek(0);
137
                }
138
139
            } else {
140
141
                // Special handling for POST specific fields and files
142
                $postFields = false;
143
                if (count($request->getPostFiles())) {
144
                    $postFields = $request->getPostFields()->useUrlEncoding(false)->urlEncode();
145
                    foreach ($request->getPostFiles() as $key => $data) {
146
                        $prefixKeys = count($data) > 1;
147
                        foreach ($data as $index => $file) {
148
                            // Allow multiple files in the same key
149
                            $fieldKey = $prefixKeys ? "{$key}[{$index}]" : $key;
150
                            $postFields[$fieldKey] = $file->getCurlValue();
151
                        }
152
                    }
153
                } elseif (count($request->getPostFields())) {
154
                    $postFields = (string) $request->getPostFields()->useUrlEncoding(true);
155
                }
156
157
                if ($postFields !== false) {
158
                    if ($method == 'POST') {
159
                        unset($curlOptions[CURLOPT_CUSTOMREQUEST]);
160
                        $curlOptions[CURLOPT_POST] = true;
161
                    }
162
                    $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
163
                    $request->removeHeader('Content-Length');
164
                }
165
            }
166
167
            // If the Expect header is not present, prevent curl from adding it
168
            if (!$request->hasHeader('Expect')) {
169
                $curlOptions[CURLOPT_HTTPHEADER][] = 'Expect:';
170
            }
171
        }
172
173
        // If a Content-Length header was specified but we want to allow curl to set one for us
174
        if (null !== $tempContentLength) {
175
            $request->removeHeader('Content-Length');
176
        }
177
178
        // Set custom cURL options
179
        foreach ($requestCurlOptions->toArray() as $key => $value) {
180
            if (is_numeric($key)) {
181
                $curlOptions[$key] = $value;
182
            }
183
        }
184
185
        // Do not set an Accept header by default
186
        if (!isset($curlOptions[CURLOPT_ENCODING])) {
187
            $curlOptions[CURLOPT_HTTPHEADER][] = 'Accept:';
188
        }
189
190
        // Add any custom headers to the request. Empty headers will cause curl to not send the header at all.
191
        foreach ($request->getHeaderLines() as $line) {
192
            $curlOptions[CURLOPT_HTTPHEADER][] = $line;
193
        }
194
195
        // Add the content-length header back if it was temporarily removed
196
        if (null !== $tempContentLength) {
197
            $request->setHeader('Content-Length', $tempContentLength);
198
        }
199
200
        // Apply the options to a new cURL handle.
201
        $handle = curl_init();
202
203
        // Enable the progress function if the 'progress' param was set
204
        if ($requestCurlOptions->get('progress')) {
205
            // Wrap the function in a function that provides the curl handle to the mediator's progress function
206
            // Using this rather than injecting the handle into the mediator prevents a circular reference
207
            $curlOptions[CURLOPT_PROGRESSFUNCTION] = function () use ($mediator, $handle) {
208
                $args = func_get_args();
209
                $args[] = $handle;
210
211
                // PHP 5.5 pushed the handle onto the start of the args
212
                if (is_resource($args[0])) {
213
                    array_shift($args);
214
                }
215
216
                call_user_func_array(array($mediator, 'progress'), $args);
217
            };
218
            $curlOptions[CURLOPT_NOPROGRESS] = false;
219
        }
220
221
        curl_setopt_array($handle, $curlOptions);
222
223
        return new static($handle, $curlOptions);
224
    }
225
226
    /**
227
     * Construct a new CurlHandle object that wraps a cURL handle
228
     *
229
     * @param resource         $handle  Configured cURL handle resource
230
     * @param Collection|array $options Curl options to use with the handle
231
     *
232
     * @throws InvalidArgumentException
233
     */
234
    public function __construct($handle, $options)
235
    {
236
        if (!is_resource($handle)) {
237
            throw new InvalidArgumentException('Invalid handle provided');
238
        }
239
        if (is_array($options)) {
240
            $this->options = new Collection($options);
241
        } elseif ($options instanceof Collection) {
242
            $this->options = $options;
243
        } else {
244
            throw new InvalidArgumentException('Expected array or Collection');
245
        }
246
        $this->handle = $handle;
247
    }
248
249
    /**
250
     * Destructor
251
     */
252
    public function __destruct()
253
    {
254
        $this->close();
255
    }
256
257
    /**
258
     * Close the curl handle
259
     */
260
    public function close()
261
    {
262
        if (is_resource($this->handle)) {
263
            curl_close($this->handle);
264
        }
265
        $this->handle = null;
266
    }
267
268
    /**
269
     * Check if the handle is available and still OK
270
     *
271
     * @return bool
272
     */
273
    public function isAvailable()
274
    {
275
        return is_resource($this->handle);
276
    }
277
278
    /**
279
     * Get the last error that occurred on the cURL handle
280
     *
281
     * @return string
282
     */
283
    public function getError()
284
    {
285
        return $this->isAvailable() ? curl_error($this->handle) : '';
286
    }
287
288
    /**
289
     * Get the last error number that occurred on the cURL handle
290
     *
291
     * @return int
292
     */
293
    public function getErrorNo()
294
    {
295
        if ($this->errorNo) {
296
            return $this->errorNo;
297
        }
298
299
        return $this->isAvailable() ? curl_errno($this->handle) : CURLE_OK;
300
    }
301
302
    /**
303
     * Set the curl error number
304
     *
305
     * @param int $error Error number to set
306
     *
307
     * @return CurlHandle
308
     */
309
    public function setErrorNo($error)
310
    {
311
        $this->errorNo = $error;
312
313
        return $this;
314
    }
315
316
    /**
317
     * Get cURL curl_getinfo data
318
     *
319
     * @param int $option Option to retrieve. Pass null to retrieve all data as an array.
320
     *
321
     * @return array|mixed
322
     */
323
    public function getInfo($option = null)
324
    {
325
        if (!is_resource($this->handle)) {
326
            return null;
327
        }
328
329
        if (null !== $option) {
330
            return curl_getinfo($this->handle, $option) ?: null;
331
        }
332
333
        return curl_getinfo($this->handle) ?: array();
334
    }
335
336
    /**
337
     * Get the stderr output
338
     *
339
     * @param bool $asResource Set to TRUE to get an fopen resource
340
     *
341
     * @return string|resource|null
342
     */
343
    public function getStderr($asResource = false)
344
    {
345
        $stderr = $this->getOptions()->get(CURLOPT_STDERR);
346
        if (!$stderr) {
347
            return null;
348
        }
349
350
        if ($asResource) {
351
            return $stderr;
352
        }
353
354
        fseek($stderr, 0);
355
        $e = stream_get_contents($stderr);
356
        fseek($stderr, 0, SEEK_END);
357
358
        return $e;
359
    }
360
361
    /**
362
     * Get the URL that this handle is connecting to
363
     *
364
     * @return Url
365
     */
366
    public function getUrl()
367
    {
368
        return Url::factory($this->options->get(CURLOPT_URL));
369
    }
370
371
    /**
372
     * Get the wrapped curl handle
373
     *
374
     * @return resource|null Returns the cURL handle or null if it was closed
375
     */
376
    public function getHandle()
377
    {
378
        return $this->isAvailable() ? $this->handle : null;
379
    }
380
381
    /**
382
     * Get the cURL setopt options of the handle. Changing values in the return object will have no effect on the curl
383
     * handle after it is created.
384
     *
385
     * @return Collection
386
     */
387
    public function getOptions()
388
    {
389
        return $this->options;
390
    }
391
392
    /**
393
     * Update a request based on the log messages of the CurlHandle
394
     *
395
     * @param RequestInterface $request Request to update
396
     */
397
    public function updateRequestFromTransfer(RequestInterface $request)
398
    {
399
        if (!$request->getResponse()) {
400
            return;
401
        }
402
403
        // Update the transfer stats of the response
404
        $request->getResponse()->setInfo($this->getInfo());
405
406
        if (!$log = $this->getStderr(true)) {
407
            return;
408
        }
409
410
        // Parse the cURL stderr output for outgoing requests
411
        $headers = '';
412
        fseek($log, 0);
413
        while (($line = fgets($log)) !== false) {
414
            if ($line && $line[0] == '>') {
415
                $headers = substr(trim($line), 2) . "\r\n";
416
                while (($line = fgets($log)) !== false) {
417
                    if ($line[0] == '*' || $line[0] == '<') {
418
                        break;
419
                    } else {
420
                        $headers .= trim($line) . "\r\n";
421
                    }
422
                }
423
            }
424
        }
425
426
        // Add request headers to the request exactly as they were sent
427
        if ($headers) {
428
            $parsed = ParserRegistry::getInstance()->getParser('message')->parseRequest($headers);
429
            if (!empty($parsed['headers'])) {
430
                $request->setHeaders(array());
431
                foreach ($parsed['headers'] as $name => $value) {
432
                    $request->setHeader($name, $value);
433
                }
434
            }
435
            if (!empty($parsed['version'])) {
436
                $request->setProtocolVersion($parsed['version']);
437
            }
438
        }
439
    }
440
441
    /**
442
     * Parse the config and replace curl.* configurators into the constant based values so it can be used elsewhere
443
     *
444
     * @param array|Collection $config The configuration we want to parse
445
     *
446
     * @return array
447
     */
448
    public static function parseCurlConfig($config)
449
    {
450
        $curlOptions = array();
451
        foreach ($config as $key => $value) {
452
            if (is_string($key) && defined($key)) {
453
                // Convert constants represented as string to constant int values
454
                $key = constant($key);
455
            }
456
            if (is_string($value) && defined($value)) {
457
                $value = constant($value);
458
            }
459
            $curlOptions[$key] = $value;
460
        }
461
462
        return $curlOptions;
463
    }
464
}
465