Test Failed
Push — master ( 938608...7e3912 )
by kill
04:20
created

Curl::downloadComplete()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 28
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 28
rs 8.439
1
<?php
2
/**
3
 * Created by rozbo at 2017/4/15 下午2:30
4
 */
5
6
namespace puck\helpers;
7
8
use puck\tools\Arr;
9
10
class Curl {
11
12
    const VERSION = '7.2.0';
13
    const DEFAULT_TIMEOUT = 30;
14
15
    public $curl;
16
    public $id = null;
17
18
    public $error = false;
19
    public $errorCode = 0;
20
    public $errorMessage = null;
21
22
    public $curlError = false;
23
    public $curlErrorCode = 0;
24
    public $curlErrorMessage = null;
25
26
    public $httpError = false;
27
    public $httpStatusCode = 0;
28
    public $httpErrorMessage = null;
29
30
    public $baseUrl = null;
31
    public $url = null;
32
    public $requestHeaders = null;
33
    public $responseHeaders = null;
34
    public $rawResponseHeaders = '';
35
    public $responseCookies = array();
36
    public $response = null;
37
    public $rawResponse = null;
38
39
    public $beforeSendFunction = null;
40
    public $downloadCompleteFunction = null;
41
    public $successFunction = null;
42
    public $errorFunction = null;
43
    public $completeFunction = null;
44
    public $fileHandle = null;
45
46
    private $cookies = array();
47
    private $headers = array();
48
    private $options = array();
49
50
    private $jsonDecoder = null;
51
    private $jsonPattern = '/^(?:application|text)\/(?:[a-z]+(?:[\.-][0-9a-z]+){0,}[\+\.]|x-)?json(?:-[a-z]+)?/i';
52
    private $xmlDecoder = null;
53
    private $xmlPattern = '~^(?:text/|application/(?:atom\+|rss\+)?)xml~i';
54
    private $defaultDecoder = null;
55
56
    public static $RFC2616 = array(
57
        // RFC2616: "any CHAR except CTLs or separators".
58
        // CHAR           = <any US-ASCII character (octets 0 - 127)>
59
        // CTL            = <any US-ASCII control character
60
        //                  (octets 0 - 31) and DEL (127)>
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
61
        // separators     = "(" | ")" | "<" | ">" | "@"
0 ignored issues
show
Unused Code Comprehensibility introduced by
41% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
62
        //                | "," | ";" | ":" | "\" | <">
63
        //                | "/" | "[" | "]" | "?" | "="
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
64
        //                | "{" | "}" | SP | HT
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
65
        // SP             = <US-ASCII SP, space (32)>
66
        // HT             = <US-ASCII HT, horizontal-tab (9)>
67
        // <">            = <US-ASCII double-quote mark (34)>
68
        '!', '#', '$', '%', '&', "'", '*', '+', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B',
69
        'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
70
        'Y', 'Z', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
71
        'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~',
72
    );
73
    public static $RFC6265 = array(
74
        // RFC6265: "US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash".
75
        // %x21
76
        '!',
77
        // %x23-2B
78
        '#', '$', '%', '&', "'", '(', ')', '*', '+',
79
        // %x2D-3A
80
        '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':',
81
        // %x3C-5B
82
        '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
83
        'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[',
84
        // %x5D-7E
85
        ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
86
        's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~',
87
    );
88
89
    private static $deferredProperties = array(
90
        'effectiveUrl',
91
        'rfc2616',
92
        'rfc6265',
93
        'totalTime',
94
    );
95
96
    /**
97
     * Construct
98
     *
99
     * @access public
100
     * @param  $base_url
101
     * @throws \ErrorException
102
     */
103
    public function __construct($base_url = null)
104
    {
105
        if (!extension_loaded('curl')) {
106
            throw new \ErrorException('cURL library is not loaded');
107
        }
108
109
        $this->curl = curl_init();
110
        $this->id = uniqid('', true);
111
        $this->setDefaultUserAgent();
112
        $this->setDefaultJsonDecoder();
113
        $this->setDefaultXmlDecoder();
114
        $this->setDefaultTimeout();
115
        $this->setOpt(CURLINFO_HEADER_OUT, true);
116
        $this->setOpt(CURLOPT_HEADERFUNCTION, array($this, 'headerCallback'));
117
        $this->setOpt(CURLOPT_RETURNTRANSFER, true);
118
        $this->headers = [];
119
        $this->setUrl($base_url);
120
    }
121
122
    /**
123
     * Before Send
124
     *
125
     * @access public
126
     * @param  $callback
127
     */
128
    public function beforeSend($callback)
129
    {
130
        $this->beforeSendFunction = $callback;
131
    }
132
133
    /**
134
     * Build Post Data
135
     *
136
     * @access public
137
     * @param  $data
138
     *
139
     * @return array|string
140
     */
141
    public function buildPostData($data)
142
    {
143
        $binary_data = false;
144
        if (is_array($data)) {
145
            // Return JSON-encoded string when the request's content-type is JSON.
146
            if (isset($this->headers['Content-Type']) &&
147
                preg_match($this->jsonPattern, $this->headers['Content-Type'])) {
148
                $json_str = json_encode($data);
149
                if (!($json_str === false)) {
150
                    $data = $json_str;
151
                }
152
            } else {
153
                // Manually build a single-dimensional array from a multi-dimensional array as using curl_setopt($ch,
154
                // CURLOPT_POSTFIELDS, $data) doesn't correctly handle multi-dimensional arrays when files are
155
                // referenced.
156
                if (Arr::isMultidim($data)) {
157
                    $data = Arr::flatten($data);
158
                }
159
160
                // Modify array values to ensure any referenced files are properly handled depending on the support of
161
                // the @filename API or CURLFile usage. This also fixes the warning "curl_setopt(): The usage of the
162
                // @filename API for file uploading is deprecated. Please use the CURLFile class instead". Ignore
163
                // non-file values prefixed with the @ character.
164
                foreach ($data as $key => $value) {
165
                    if (is_string($value) && strpos($value, '@') === 0 && is_file(substr($value, 1))) {
166
                        $binary_data = true;
167
                        if (class_exists('CURLFile')) {
168
                            $data[$key] = new \CURLFile(substr($value, 1));
169
                        }
170
                    } elseif ($value instanceof \CURLFile) {
171
                        $binary_data = true;
172
                    }
173
                }
174
            }
175
        }
176
177
        if (!$binary_data && (is_array($data) || is_object($data))) {
178
            $data = http_build_query($data, '', '&');
179
        }
180
181
        return $data;
182
    }
183
184
    /**
185
     * Call
186
     *
187
     * @access public
188
     */
189
    public function call()
190
    {
191
        $args = func_get_args();
192
        $function = array_shift($args);
193
        if (is_callable($function)) {
194
            array_unshift($args, $this);
195
            call_user_func_array($function, $args);
196
        }
197
    }
198
199
    /**
200
     * Close
201
     *
202
     * @access public
203
     */
204
    public function close()
205
    {
206
        if (is_resource($this->curl)) {
207
            curl_close($this->curl);
208
        }
209
        $this->options = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $options.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
210
        $this->jsonDecoder = null;
211
        $this->xmlDecoder = null;
212
        $this->defaultDecoder = null;
213
    }
214
215
    /**
216
     * Complete
217
     *
218
     * @access public
219
     * @param  $callback
220
     */
221
    public function complete($callback)
222
    {
223
        $this->completeFunction = $callback;
224
    }
225
226
    /**
227
     * Progress
228
     *
229
     * @access public
230
     * @param  $callback
231
     */
232
    public function progress($callback)
233
    {
234
        $this->setOpt(CURLOPT_PROGRESSFUNCTION, $callback);
235
        $this->setOpt(CURLOPT_NOPROGRESS, false);
236
    }
237
238
    /**
239
     * Delete
240
     *
241
     * @access public
242
     * @param  $url
243
     * @param  $query_parameters
244
     * @param  $data
245
     *
246
     * @return string
247
     */
248
    public function delete($url, $query_parameters = array(), $data = array())
249
    {
250
        if (is_array($url)) {
251
            $data = $query_parameters;
252
            $query_parameters = $url;
253
            $url = $this->baseUrl;
254
        }
255
256
        $this->setUrl($url, $query_parameters);
257
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE');
258
        $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
259
        return $this->exec();
260
    }
261
262
    /**
263
     * Download Complete
264
     *
265
     * @access private
266
     * @param  $fh
267
     */
268
    private function downloadComplete($fh)
269
    {
270
        if (!$this->error && $this->downloadCompleteFunction) {
271
            rewind($fh);
272
            $this->call($this->downloadCompleteFunction, $fh);
273
            $this->downloadCompleteFunction = null;
274
        }
275
276
        if (is_resource($fh)) {
277
            fclose($fh);
278
        }
279
280
        // Fix "PHP Notice: Use of undefined constant STDOUT" when reading the
281
        // PHP script from stdin. Using null causes "Warning: curl_setopt():
282
        // supplied argument is not a valid File-Handle resource".
283
        if (!defined('STDOUT')) {
284
            define('STDOUT', fopen('php://stdout', 'w'));
285
        }
286
287
        // Reset CURLOPT_FILE with STDOUT to avoid: "curl_exec(): CURLOPT_FILE
288
        // resource has gone away, resetting to default".
289
        $this->setOpt(CURLOPT_FILE, STDOUT);
290
291
        // Reset CURLOPT_RETURNTRANSFER to tell cURL to return subsequent
292
        // responses as the return value of curl_exec(). Without this,
293
        // curl_exec() will revert to returning boolean values.
294
        $this->setOpt(CURLOPT_RETURNTRANSFER, true);
295
    }
296
297
    /**
298
     * Download
299
     *
300
     * @access public
301
     * @param  $url
302
     * @param  $mixed_filename
303
     *
304
     * @return boolean
305
     */
306
    public function download($url, $mixed_filename)
307
    {
308
        if (is_callable($mixed_filename)) {
309
            $this->downloadCompleteFunction = $mixed_filename;
310
            $this->fileHandle = tmpfile();
311
        } else {
312
            $filename = $mixed_filename;
313
314
            // Use a temporary file when downloading. Not using a temporary file can cause an error when an existing
315
            // file has already fully completed downloading and a new download is started with the same destination save
316
            // path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid,
317
            // but unsatisfiable.
318
            $download_filename = $filename . '.pccdownload';
319
320
            $mode = 'wb';
321
            // Attempt to resume download only when a temporary download file exists and is not empty.
322
            if (file_exists($download_filename) && $filesize = filesize($download_filename)) {
323
                $mode = 'ab';
324
                $first_byte_position = $filesize;
325
                $range = $first_byte_position . '-';
326
                $this->setOpt(CURLOPT_RANGE, $range);
327
            }
328
            $this->fileHandle = fopen($download_filename, $mode);
329
330
            // Move the downloaded temporary file to the destination save path.
331
            $this->downloadCompleteFunction = function ($fh) use ($download_filename, $filename) {
0 ignored issues
show
Unused Code introduced by
The parameter $fh is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
332
                rename($download_filename, $filename);
333
            };
334
        }
335
336
        $this->setOpt(CURLOPT_FILE, $this->fileHandle);
337
        $this->get($url);
338
339
        return ! $this->error;
340
    }
341
342
    /**
343
     * Error
344
     *
345
     * @access public
346
     * @param  $callback
347
     */
348
    public function error($callback)
349
    {
350
        $this->errorFunction = $callback;
351
    }
352
353
    /**
354
     * Exec
355
     *
356
     * @access public
357
     * @param  $ch
358
     *
359
     * @return mixed Returns the value provided by parseResponse.
360
     */
361
    public function exec($ch = null)
362
    {
363
        if ($ch === null) {
364
            $this->responseCookies = array();
365
            $this->call($this->beforeSendFunction);
366
            $this->rawResponse = curl_exec($this->curl);
367
            $this->curlErrorCode = curl_errno($this->curl);
368
            $this->curlErrorMessage = curl_error($this->curl);
369
        } else {
370
            $this->rawResponse = curl_multi_getcontent($ch);
371
            $this->curlErrorMessage = curl_error($ch);
372
        }
373
        $this->curlError = !($this->curlErrorCode === 0);
374
        // Include additional error code information in error message when possible.
375
        if ($this->curlError && function_exists('curl_strerror')) {
376
            $this->curlErrorMessage =
377
                curl_strerror($this->curlErrorCode) . (
378
                empty($this->curlErrorMessage) ? '' : ': ' . $this->curlErrorMessage
379
                );
380
            print_r($this->rawResponse);
381
        }
382
383
        $this->httpStatusCode = $this->getInfo(CURLINFO_HTTP_CODE);
384
        $this->httpError = in_array(floor($this->httpStatusCode / 100), array(4, 5));
385
        $this->error = $this->curlError || $this->httpError;
386
        $this->errorCode = $this->error ? ($this->curlError ? $this->curlErrorCode : $this->httpStatusCode) : 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->error ? $this->cu...his->httpStatusCode : 0 can also be of type double. However, the property $errorCode is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
387
388
        // NOTE: CURLINFO_HEADER_OUT set to true is required for requestHeaders
389
        // to not be empty (e.g. $curl->setOpt(CURLINFO_HEADER_OUT, true);).
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
390
        if ($this->getOpt(CURLINFO_HEADER_OUT) === true) {
391
            $this->requestHeaders = $this->parseRequestHeaders($this->getInfo(CURLINFO_HEADER_OUT));
392
        }
393
        $this->responseHeaders = $this->parseResponseHeaders($this->rawResponseHeaders);
394
        $this->response = $this->parseResponse($this->responseHeaders, $this->rawResponse);
395
396
        $this->httpErrorMessage = '';
397
        if ($this->error) {
398
            if (isset($this->responseHeaders['Status-Line'])) {
399
                $this->httpErrorMessage = $this->responseHeaders['Status-Line'];
400
            }
401
        }
402
        $this->errorMessage = $this->curlError ? $this->curlErrorMessage : $this->httpErrorMessage;
403
404
        if (!$this->error) {
405
            $this->call($this->successFunction);
406
        } else {
407
            $this->call($this->errorFunction);
408
        }
409
410
        $this->call($this->completeFunction);
411
412
        // Close open file handles and reset the curl instance.
413
        if (!($this->fileHandle === null)) {
414
            $this->downloadComplete($this->fileHandle);
415
        }
416
417
        return $this->response;
418
    }
419
420
    /**
421
     * Get
422
     *
423
     * @access public
424
     * @param  $url
425
     * @param  $data
426
     *
427
     * @return mixed Returns the value provided by exec.
428
     */
429
    public function get($url, $data = array())
430
    {
431
        if (is_array($url)) {
432
            $data = $url;
433
            $url = $this->baseUrl;
434
        }
435
        $this->setUrl($url, $data);
436
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
437
        $this->setOpt(CURLOPT_HTTPGET, true);
438
        return $this->exec();
439
    }
440
441
    /**
442
     * Get Info
443
     *
444
     * @access public
445
     * @param  $opt
446
     *
447
     * @return mixed
448
     */
449
    public function getInfo($opt = null)
450
    {
451
        $args = array();
452
        $args[] = $this->curl;
453
454
        if (func_num_args()) {
455
            $args[] = $opt;
456
        }
457
458
        return call_user_func_array('curl_getinfo', $args);
459
    }
460
461
    /**
462
     * Get Opt
463
     *
464
     * @access public
465
     * @param  $option
466
     *
467
     * @return mixed
468
     */
469
    public function getOpt($option)
470
    {
471
        return isset($this->options[$option]) ? $this->options[$option] : null;
472
    }
473
474
    /**
475
     * Head
476
     *
477
     * @access public
478
     * @param  $url
479
     * @param  $data
480
     *
481
     * @return string
482
     */
483
    public function head($url, $data = array())
484
    {
485
        if (is_array($url)) {
486
            $data = $url;
487
            $url = $this->baseUrl;
488
        }
489
        $this->setUrl($url, $data);
490
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
491
        $this->setOpt(CURLOPT_NOBODY, true);
492
        return $this->exec();
493
    }
494
495
    /**
496
     * Header Callback
497
     *
498
     * @access public
499
     * @param  $ch
500
     * @param  $header
501
     *
502
     * @return integer
503
     */
504
    public function headerCallback($ch, $header)
505
    {
506
        if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) === 1) {
507
            $this->responseCookies[$cookie[1]] = trim($cookie[2], " \n\r\t\0\x0B");
508
        }
509
        $this->rawResponseHeaders .= $header;
510
        return strlen($header);
511
    }
512
513
    /**
514
     * Options
515
     *
516
     * @access public
517
     * @param  $url
518
     * @param  $data
519
     *
520
     * @return string
521
     */
522
    public function options($url, $data = array())
523
    {
524
        if (is_array($url)) {
525
            $data = $url;
526
            $url = $this->baseUrl;
527
        }
528
        $this->setUrl($url, $data);
529
        $this->removeHeader('Content-Length');
530
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS');
531
        return $this->exec();
532
    }
533
534
    /**
535
     * Patch
536
     *
537
     * @access public
538
     * @param  $url
539
     * @param  $data
540
     *
541
     * @return string
542
     */
543
    public function patch($url, $data = array())
544
    {
545
        if (is_array($url)) {
546
            $data = $url;
547
            $url = $this->baseUrl;
548
        }
549
550
        if (is_array($data) && empty($data)) {
551
            $this->removeHeader('Content-Length');
552
        }
553
554
        $this->setUrl($url);
555
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH');
556
        $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
557
        return $this->exec();
558
    }
559
560
    /**
561
     * Post
562
     *
563
     * @access public
564
     * @param  $url
565
     * @param  $data
566
     * @param  $follow_303_with_post
567
     *     If true, will cause 303 redirections to be followed using a POST request (default: false).
568
     *     Notes:
569
     *       - Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true.
570
     *       - According to the HTTP specs (see [1]), a 303 redirection should be followed using
571
     *         the GET method. 301 and 302 must not.
572
     *       - In order to force a 303 redirection to be performed using the same method, the
573
     *         underlying cURL object must be set in a special state (the CURLOPT_CURSTOMREQUEST
574
     *         option must be set to the method to use after the redirection). Due to a limitation
575
     *         of the cURL extension of PHP < 5.5.11 ([2], [3]) and of HHVM, it is not possible
576
     *         to reset this option. Using these PHP engines, it is therefore impossible to
577
     *         restore this behavior on an existing php-curl-class Curl object.
578
     *
579
     * @return string
580
     *
581
     * [1] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2
582
     * [2] https://github.com/php/php-src/pull/531
583
     * [3] http://php.net/ChangeLog-5.php#5.5.11
584
     */
585
    public function post($url, $data = array(), $follow_303_with_post = false)
586
    {
587
        if (is_array($url)) {
588
            $follow_303_with_post = (bool)$data;
589
            $data = $url;
590
            $url = $this->baseUrl;
591
        }
592
593
        $this->setUrl($url);
594
595
        if ($follow_303_with_post) {
596
            $this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST');
597
        } else {
598
            if (isset($this->options[CURLOPT_CUSTOMREQUEST])) {
599
                if ((version_compare(PHP_VERSION, '5.5.11') < 0) || defined('HHVM_VERSION')) {
600
                    trigger_error(
601
                        'Due to technical limitations of PHP <= 5.5.11 and HHVM, it is not possible to '
602
                        . 'perform a post-redirect-get request using a php-curl-class Curl object that '
603
                        . 'has already been used to perform other types of requests. Either use a new '
604
                        . 'php-curl-class Curl object or upgrade your PHP engine.',
605
                        E_USER_ERROR
606
                    );
607
                } else {
608
                    $this->setOpt(CURLOPT_CUSTOMREQUEST, null);
609
                }
610
            }
611
        }
612
613
        $this->setOpt(CURLOPT_POST, true);
614
        $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data));
615
        return $this->exec();
616
    }
617
618
    /**
619
     * Put
620
     *
621
     * @access public
622
     * @param  $url
623
     * @param  $data
624
     *
625
     * @return string
626
     */
627
    public function put($url, $data = array())
628
    {
629
        if (is_array($url)) {
630
            $data = $url;
631
            $url = $this->baseUrl;
632
        }
633
        $this->setUrl($url);
634
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
635
        $put_data = $this->buildPostData($data);
636
        if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
637
            if (is_string($put_data)) {
638
                $this->setHeader('Content-Length', strlen($put_data));
639
            }
640
        }
641
        if (!empty($put_data)) {
642
            $this->setOpt(CURLOPT_POSTFIELDS, $put_data);
643
        }
644
        return $this->exec();
645
    }
646
647
    /**
648
     * Search
649
     *
650
     * @access public
651
     * @param  $url
652
     * @param  $data
653
     *
654
     * @return string
655
     */
656
    public function search($url, $data = array())
657
    {
658
        if (is_array($url)) {
659
            $data = $url;
660
            $url = $this->baseUrl;
661
        }
662
        $this->setUrl($url);
663
        $this->setOpt(CURLOPT_CUSTOMREQUEST, 'SEARCH');
664
        $put_data = $this->buildPostData($data);
665
        if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
666
            if (is_string($put_data)) {
667
                $this->setHeader('Content-Length', strlen($put_data));
668
            }
669
        }
670
        if (!empty($put_data)) {
671
            $this->setOpt(CURLOPT_POSTFIELDS, $put_data);
672
        }
673
        return $this->exec();
674
    }
675
676
    /**
677
     * Set Basic Authentication
678
     *
679
     * @access public
680
     * @param  $username
681
     * @param  $password
682
     */
683
    public function setBasicAuthentication($username, $password = '')
684
    {
685
        $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
686
        $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
687
    }
688
689
    /**
690
     * Set Digest Authentication
691
     *
692
     * @access public
693
     * @param  $username
694
     * @param  $password
695
     */
696
    public function setDigestAuthentication($username, $password = '')
697
    {
698
        $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
699
        $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
700
    }
701
702
    /**
703
     * Set Cookie
704
     *
705
     * @access public
706
     * @param  $key
707
     * @param  $value
708
     */
709
    public function setCookie($key, $value)
710
    {
711
        $name_chars = array();
712
        foreach (str_split($key) as $name_char) {
713
            if (!isset($this->rfc2616[$name_char])) {
0 ignored issues
show
Bug introduced by
The property rfc2616 does not seem to exist. Did you mean RFC2616?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
714
                $name_chars[] = rawurlencode($name_char);
715
            } else {
716
                $name_chars[] = $name_char;
717
            }
718
        }
719
720
        $value_chars = array();
721
        foreach (str_split($value) as $value_char) {
722
            if (!isset($this->rfc6265[$value_char])) {
0 ignored issues
show
Bug introduced by
The property rfc6265 does not seem to exist. Did you mean RFC6265?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
723
                $value_chars[] = rawurlencode($value_char);
724
            } else {
725
                $value_chars[] = $value_char;
726
            }
727
        }
728
729
        $this->cookies[implode('', $name_chars)] = implode('', $value_chars);
730
        $this->setOpt(CURLOPT_COOKIE, implode('; ', array_map(function ($k, $v) {
731
            return $k . '=' . $v;
732
        }, array_keys($this->cookies), array_values($this->cookies))));
733
    }
734
735
    /**
736
     * Set Cookies
737
     *
738
     * @access public
739
     * @param  $cookies
740
     */
741
    public function setCookies($cookies)
742
    {
743
        foreach ($cookies as $key => $value) {
744
            $name_chars = array();
745
            foreach (str_split($key) as $name_char) {
746
                if (!isset($this->rfc2616[$name_char])) {
0 ignored issues
show
Bug introduced by
The property rfc2616 does not seem to exist. Did you mean RFC2616?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
747
                    $name_chars[] = rawurlencode($name_char);
748
                } else {
749
                    $name_chars[] = $name_char;
750
                }
751
            }
752
753
            $value_chars = array();
754
            foreach (str_split($value) as $value_char) {
755
                if (!isset($this->rfc6265[$value_char])) {
0 ignored issues
show
Bug introduced by
The property rfc6265 does not seem to exist. Did you mean RFC6265?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
756
                    $value_chars[] = rawurlencode($value_char);
757
                } else {
758
                    $value_chars[] = $value_char;
759
                }
760
            }
761
762
            $this->cookies[implode('', $name_chars)] = implode('', $value_chars);
763
        }
764
765
        $this->setOpt(CURLOPT_COOKIE, implode('; ', array_map(function ($k, $v) {
766
            return $k . '=' . $v;
767
        }, array_keys($this->cookies), array_values($this->cookies))));
768
    }
769
770
    /**
771
     * Get Cookie
772
     *
773
     * @access public
774
     * @param  $key
775
     *
776
     * @return mixed
777
     */
778
    public function getCookie($key)
779
    {
780
        return $this->getResponseCookie($key);
781
    }
782
783
    /**
784
     * Get Response Cookie
785
     *
786
     * @access public
787
     * @param  $key
788
     *
789
     * @return mixed
790
     */
791
    public function getResponseCookie($key)
792
    {
793
        return isset($this->responseCookies[$key]) ? $this->responseCookies[$key] : null;
794
    }
795
796
    /**
797
     * Set Max Filesize
798
     *
799
     * @access public
800
     * @param  $bytes
801
     */
802
    public function setMaxFilesize($bytes)
803
    {
804
        // Make compatible with PHP version both before and after 5.5.0. PHP 5.5.0 added the cURL resource as the first
805
        // argument to the CURLOPT_PROGRESSFUNCTION callback.
806
        $gte_v550 = version_compare(PHP_VERSION, '5.5.0') >= 0;
807
        if ($gte_v550) {
808
            $callback = function ($resource, $download_size, $downloaded, $upload_size, $uploaded) use ($bytes) {
0 ignored issues
show
Unused Code introduced by
The parameter $upload_size is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $uploaded is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
809
                // Abort the transfer when $downloaded bytes exceeds maximum $bytes by returning a non-zero value.
810
                return $downloaded > $bytes ? 1 : 0;
811
            };
812
        } else {
813
            $callback = function ($download_size, $downloaded, $upload_size, $uploaded) use ($bytes) {
0 ignored issues
show
Unused Code introduced by
The parameter $upload_size is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $uploaded is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
814
                return $downloaded > $bytes ? 1 : 0;
815
            };
816
        }
817
818
        $this->progress($callback);
819
    }
820
821
    /**
822
     * Set Port
823
     *
824
     * @access public
825
     * @param  $port
826
     */
827
    public function setPort($port)
828
    {
829
        $this->setOpt(CURLOPT_PORT, intval($port));
830
    }
831
832
    /**
833
     * Set Connect Timeout
834
     *
835
     * @access public
836
     * @param  $seconds
837
     */
838
    public function setConnectTimeout($seconds)
839
    {
840
        $this->setOpt(CURLOPT_CONNECTTIMEOUT, $seconds);
841
    }
842
843
    /**
844
     * Set Cookie String
845
     *
846
     * @access public
847
     * @param  $string
848
     *
849
     * @return bool
850
     */
851
    public function setCookieString($string)
852
    {
853
        return $this->setOpt(CURLOPT_COOKIE, $string);
854
    }
855
856
    /**
857
     * Set Cookie File
858
     *
859
     * @access public
860
     * @param  $cookie_file
861
     *
862
     * @return boolean
863
     */
864
    public function setCookieFile($cookie_file)
865
    {
866
        return $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file);
867
    }
868
869
    /**
870
     * Set Cookie Jar
871
     *
872
     * @access public
873
     * @param  $cookie_jar
874
     *
875
     * @return boolean
876
     */
877
    public function setCookieJar($cookie_jar)
878
    {
879
        return $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar);
880
    }
881
882
    /**
883
     * Set Default JSON Decoder
884
     *
885
     * @access public
886
     * @param  $assoc
887
     * @param  $depth
888
     * @param  $options
889
     */
890
    public function setDefaultJsonDecoder()
891
    {
892
        $args = func_get_args();
893
        $this->jsonDecoder = function ($response) use ($args) {
894
            array_unshift($args, $response);
895
896
            // Call json_decode() without the $options parameter in PHP
897
            // versions less than 5.4.0 as the $options parameter was added in
898
            // PHP version 5.4.0.
899
            if (version_compare(PHP_VERSION, '5.4.0', '<')) {
900
                $args = array_slice($args, 0, 3);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $args, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
901
            }
902
903
            $json_obj = call_user_func_array('json_decode', $args);
904
            if (!($json_obj === null)) {
905
                $response = $json_obj;
906
            }
907
            return $response;
908
        };
909
    }
910
911
    /**
912
     * Set Default XML Decoder
913
     *
914
     * @access public
915
     */
916
    public function setDefaultXmlDecoder()
917
    {
918
        $this->xmlDecoder = function ($response) {
919
            $xml_obj = @simplexml_load_string($response);
920
            if (!($xml_obj === false)) {
921
                $response = $xml_obj;
922
            }
923
            return $response;
924
        };
925
    }
926
927
    /**
928
     * Set Default Decoder
929
     *
930
     * @access public
931
     * @param  $decoder string|callable
932
     */
933
    public function setDefaultDecoder($decoder = 'json')
934
    {
935
        if (is_callable($decoder)) {
936
            $this->defaultDecoder = $decoder;
937
        } else {
938
            if ($decoder === 'json') {
939
                $this->defaultDecoder = $this->jsonDecoder;
940
            } elseif ($decoder === 'xml') {
941
                $this->defaultDecoder = $this->xmlDecoder;
942
            }
943
        }
944
    }
945
946
    /**
947
     * Set Default Timeout
948
     *
949
     * @access public
950
     */
951
    public function setDefaultTimeout()
952
    {
953
        $this->setTimeout(self::DEFAULT_TIMEOUT);
954
    }
955
956
    /**
957
     * Set Default User Agent
958
     *
959
     * @access public
960
     */
961
    public function setDefaultUserAgent()
962
    {
963
        $user_agent = 'puck client' . self::VERSION . ' mini';
964
        $user_agent .= ' PHP/' . PHP_VERSION;
965
        $curl_version = curl_version();
966
        $user_agent .= ' curl/' . $curl_version['version'];
967
        $this->setUserAgent($user_agent);
968
    }
969
970
    /**
971
     * Set Header
972
     *
973
     * Add extra header to include in the request.
974
     *
975
     * @access public
976
     * @param  $key
977
     * @param  $value
978
     */
979
    public function setHeader($key, $value)
980
    {
981
        $this->headers[$key] = $value;
982
        $headers = array();
983
        foreach ($this->headers as $key => $value) {
984
            $headers[] = $key . ': ' . $value;
985
        }
986
        $this->setOpt(CURLOPT_HTTPHEADER, $headers);
987
    }
988
989
    /**
990
     * Set Headers
991
     *
992
     * Add extra headers to include in the request.
993
     *
994
     * @access public
995
     * @param  $headers
996
     */
997
    public function setHeaders($headers)
998
    {
999
        foreach ($headers as $key => $value) {
1000
            $this->headers[$key] = $value;
1001
        }
1002
1003
        $headers = array();
1004
        foreach ($this->headers as $key => $value) {
1005
            $headers[] = $key . ': ' . $value;
1006
        }
1007
        $this->setOpt(CURLOPT_HTTPHEADER, $headers);
1008
    }
1009
1010
    /**
1011
     * Set JSON Decoder
1012
     *
1013
     * @access public
1014
     * @param  $function
1015
     */
1016
    public function setJsonDecoder($function)
1017
    {
1018
        if (is_callable($function)) {
1019
            $this->jsonDecoder = $function;
1020
        }
1021
    }
1022
1023
    /**
1024
     * Set XML Decoder
1025
     *
1026
     * @access public
1027
     * @param  $function
1028
     */
1029
    public function setXmlDecoder($function)
1030
    {
1031
        if (is_callable($function)) {
1032
            $this->xmlDecoder = $function;
1033
        }
1034
    }
1035
1036
    /**
1037
     * Set Opt
1038
     *
1039
     * @access public
1040
     * @param  $option
1041
     * @param  $value
1042
     *
1043
     * @return boolean
1044
     */
1045
    public function setOpt($option, $value)
1046
    {
1047
        $required_options = array(
1048
            CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER',
1049
        );
1050
1051
        if (in_array($option, array_keys($required_options), true) && !($value === true)) {
1052
            trigger_error($required_options[$option] . ' is a required option', E_USER_WARNING);
1053
        }
1054
1055
        $success = curl_setopt($this->curl, $option, $value);
1056
        if ($success) {
1057
            $this->options[$option] = $value;
1058
        }
1059
        return $success;
1060
    }
1061
1062
    /**
1063
     * Set Opts
1064
     *
1065
     * @access public
1066
     * @param  $options
1067
     *
1068
     * @return boolean
1069
     *   Returns true if all options were successfully set. If an option could not be successfully set, false is
1070
     *   immediately returned, ignoring any future options in the options array. Similar to curl_setopt_array().
1071
     */
1072
    public function setOpts($options)
1073
    {
1074
        foreach ($options as $option => $value) {
1075
            if (!$this->setOpt($option, $value)) {
1076
                return false;
1077
            }
1078
        }
1079
        return true;
1080
    }
1081
1082
    /**
1083
     * Set Referer
1084
     *
1085
     * @access public
1086
     * @param  $referer
1087
     */
1088
    public function setReferer($referer)
1089
    {
1090
        $this->setReferrer($referer);
1091
    }
1092
1093
    /**
1094
     * Set Referrer
1095
     *
1096
     * @access public
1097
     * @param  $referrer
1098
     */
1099
    public function setReferrer($referrer)
1100
    {
1101
        $this->setOpt(CURLOPT_REFERER, $referrer);
1102
    }
1103
1104
    /**
1105
     * Set Timeout
1106
     *
1107
     * @access public
1108
     * @param  $seconds
1109
     */
1110
    public function setTimeout($seconds)
1111
    {
1112
        $this->setOpt(CURLOPT_TIMEOUT, $seconds);
1113
    }
1114
1115
    /**
1116
     * Set Url
1117
     *
1118
     * @access public
1119
     * @param  $url
1120
     * @param  $data
1121
     */
1122
    public function setUrl($url, $data = array())
1123
    {
1124
        $this->baseUrl = $url;
1125
        $this->url = $this->buildURL($url, $data);
1126
        $this->setOpt(CURLOPT_URL, $this->url);
1127
    }
1128
1129
    /**
1130
     * Set User Agent
1131
     *
1132
     * @access public
1133
     * @param  $user_agent
1134
     */
1135
    public function setUserAgent($user_agent)
1136
    {
1137
        $this->setOpt(CURLOPT_USERAGENT, $user_agent);
1138
    }
1139
1140
    /**
1141
     * Success
1142
     *
1143
     * @access public
1144
     * @param  $callback
1145
     */
1146
    public function success($callback)
1147
    {
1148
        $this->successFunction = $callback;
1149
    }
1150
1151
    /**
1152
     * Unset Header
1153
     *
1154
     * Remove extra header previously set using Curl::setHeader().
1155
     *
1156
     * @access public
1157
     * @param  $key
1158
     */
1159
    public function unsetHeader($key)
1160
    {
1161
        unset($this->headers[$key]);
1162
        $headers = array();
1163
        foreach ($this->headers as $key => $value) {
1164
            $headers[] = $key . ': ' . $value;
1165
        }
1166
        $this->setOpt(CURLOPT_HTTPHEADER, $headers);
1167
    }
1168
1169
    /**
1170
     * Remove Header
1171
     *
1172
     * Remove an internal header from the request.
1173
     * Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');.
1174
     *
1175
     * @access public
1176
     * @param  $key
1177
     */
1178
    public function removeHeader($key)
1179
    {
1180
        $this->setHeader($key, '');
1181
    }
1182
1183
    /**
1184
     * Verbose
1185
     *
1186
     * @access public
1187
     * @param  bool $on
1188
     * @param  resource $output
1189
     */
1190
    public function verbose($on = true, $output = STDERR)
1191
    {
1192
        // Turn off CURLINFO_HEADER_OUT for verbose to work. This has the side
1193
        // effect of causing Curl::requestHeaders to be empty.
1194
        if ($on) {
1195
            $this->setOpt(CURLINFO_HEADER_OUT, false);
1196
        }
1197
        $this->setOpt(CURLOPT_VERBOSE, $on);
1198
        $this->setOpt(CURLOPT_STDERR, $output);
1199
    }
1200
1201
    /**
1202
     * Destruct
1203
     *
1204
     * @access public
1205
     */
1206
    public function __destruct()
1207
    {
1208
        $this->close();
1209
    }
1210
1211
    public function __get($name)
1212
    {
1213
        $return = null;
1214
        if (in_array($name, self::$deferredProperties) && is_callable(array($this, $getter = '__get_' . $name))) {
1215
            $return = $this->$name = $this->$getter();
1216
        }
1217
        return $return;
1218
    }
1219
1220
    /**
1221
     * Get Effective Url
1222
     *
1223
     * @access private
1224
     */
1225
    private function __get_effectiveUrl()
1226
    {
1227
        return $this->getInfo(CURLINFO_EFFECTIVE_URL);
1228
    }
1229
1230
    /**
1231
     * Get RFC 2616
1232
     *
1233
     * @access private
1234
     */
1235
    private function __get_rfc2616()
1236
    {
1237
        return array_fill_keys(self::$RFC2616, true);
1238
    }
1239
1240
    /**
1241
     * Get RFC 6265
1242
     *
1243
     * @access private
1244
     */
1245
    private function __get_rfc6265()
1246
    {
1247
        return array_fill_keys(self::$RFC6265, true);
1248
    }
1249
1250
    /**
1251
     * Get Total Time
1252
     *
1253
     * @access private
1254
     */
1255
    private function __get_totalTime()
1256
    {
1257
        return $this->getInfo(CURLINFO_TOTAL_TIME);
1258
    }
1259
1260
    /**
1261
     * Build Url
1262
     *
1263
     * @access private
1264
     * @param  $url
1265
     * @param  $data
1266
     *
1267
     * @return string
1268
     */
1269
    private function buildURL($url, $data = array())
1270
    {
1271
        return $url . (empty($data) ? '' : '?' . http_build_query($data, '', '&'));
1272
    }
1273
1274
    /**
1275
     * Parse Headers
1276
     *
1277
     * @access private
1278
     * @param  $raw_headers
1279
     *
1280
     * @return array
1281
     */
1282
    private function parseHeaders($raw_headers)
1283
    {
1284
        $raw_headers = preg_split('/\r\n/', $raw_headers, null, PREG_SPLIT_NO_EMPTY);
1285
        $http_headers = [];
1286
1287
        $raw_headers_count = count($raw_headers);
1288
        for ($i = 1; $i < $raw_headers_count; $i++) {
1289
            list($key, $value) = explode(':', $raw_headers[$i], 2);
1290
            $key = trim($key);
1291
            $value = trim($value);
1292
            // Use isset() as array_key_exists() and ArrayAccess are not compatible.
1293
            if (isset($http_headers[$key])) {
1294
                $http_headers[$key] .= ',' . $value;
1295
            } else {
1296
                $http_headers[$key] = $value;
1297
            }
1298
        }
1299
1300
        return array(isset($raw_headers['0']) ? $raw_headers['0'] : '', $http_headers);
1301
    }
1302
1303
    /**
1304
     * Parse Request Headers
1305
     *
1306
     * @access private
1307
     * @param  $raw_headers
1308
     *
1309
     * @return array
1310
     */
1311
    private function parseRequestHeaders($raw_headers)
1312
    {
1313
        $request_headers = [];
1314
        list($first_line, $headers) = $this->parseHeaders($raw_headers);
1315
        $request_headers['Request-Line'] = $first_line;
1316
        foreach ($headers as $key => $value) {
1317
            $request_headers[$key] = $value;
1318
        }
1319
        return $request_headers;
1320
    }
1321
1322
    /**
1323
     * Parse Response
1324
     *
1325
     * @access private
1326
     * @param  $response_headers
1327
     * @param  $raw_response
1328
     *
1329
     * @return mixed
1330
     *   Provided the content-type is determined to be json or xml:
1331
     *     Returns stdClass object when the default json decoder is used and the content-type is json.
1332
     *     Returns SimpleXMLElement object when the default xml decoder is used and the content-type is xml.
1333
     */
1334
    private function parseResponse($response_headers, $raw_response)
1335
    {
1336
        $response = $raw_response;
1337
        if (isset($response_headers['Content-Type'])) {
1338
            if (preg_match($this->jsonPattern, $response_headers['Content-Type'])) {
1339
                $json_decoder = $this->jsonDecoder;
1340
                if (is_callable($json_decoder)) {
1341
                    $response = $json_decoder($response);
1342
                }
1343
            } elseif (preg_match($this->xmlPattern, $response_headers['Content-Type'])) {
1344
                $xml_decoder = $this->xmlDecoder;
1345
                if (is_callable($xml_decoder)) {
1346
                    $response = $xml_decoder($response);
1347
                }
1348
            } else {
1349
                $decoder = $this->defaultDecoder;
1350
                if (is_callable($decoder)) {
1351
                    $response = $decoder($response);
1352
                }
1353
            }
1354
        }
1355
1356
        return $response;
1357
    }
1358
1359
    /**
1360
     * Parse Response Headers
1361
     *
1362
     * @access private
1363
     * @param  $raw_response_headers
1364
     *
1365
     * @return array
1366
     */
1367
    private function parseResponseHeaders($raw_response_headers)
1368
    {
1369
        $response_header_array = explode("\r\n\r\n", $raw_response_headers);
1370
        $response_header  = '';
1371
        for ($i = count($response_header_array) - 1; $i >= 0; $i--) {
1372
            if (stripos($response_header_array[$i], 'HTTP/') === 0) {
1373
                $response_header = $response_header_array[$i];
1374
                break;
1375
            }
1376
        }
1377
1378
        $response_headers = [];
1379
        list($first_line, $headers) = $this->parseHeaders($response_header);
1380
        $response_headers['Status-Line'] = $first_line;
1381
        foreach ($headers as $key => $value) {
1382
            $response_headers[$key] = $value;
1383
        }
1384
        return $response_headers;
1385
    }
1386
}