• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / httpful / 5593279780

pending completion
5593279780

push

github

voku
Merge branch 'master' of ssh://github.com/voku/httpful

* 'master' of ssh://github.com/voku/httpful:
  Fix CS again
  Fix CS
  Fix typos
  Fix curly braces in strings

12 of 12 new or added lines in 3 files covered. (100.0%)

1653 of 2504 relevant lines covered (66.01%)

80.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.35
/src/Httpful/Request.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Httpful;
6

7
use Httpful\Curl\Curl;
8
use Httpful\Curl\MultiCurl;
9
use Httpful\Exception\ClientErrorException;
10
use Httpful\Exception\NetworkErrorException;
11
use Httpful\Exception\RequestException;
12
use Psr\Http\Message\RequestInterface;
13
use Psr\Http\Message\StreamInterface;
14
use Psr\Http\Message\UriInterface;
15
use Psr\Log\LoggerInterface;
16
use voku\helper\UTF8;
17

18
class Request implements \IteratorAggregate, RequestInterface
19
{
20
    const MAX_REDIRECTS_DEFAULT = 25;
21

22
    const SERIALIZE_PAYLOAD_ALWAYS = 1;
23

24
    const SERIALIZE_PAYLOAD_NEVER = 0;
25

26
    const SERIALIZE_PAYLOAD_SMART = 2;
27

28
    /**
29
     * "Request"-template object
30
     *
31
     * @var Request|null
32
     */
33
    private $template;
34

35
    /**
36
     * @var array
37
     */
38
    private $helperData = [];
39

40
    /**
41
     * @var UriInterface|null
42
     */
43
    private $uri;
44

45
    /**
46
     * @var string
47
     */
48
    private $uri_cache;
49

50
    /**
51
     * @var string
52
     */
53
    private $ssl_key = '';
54

55
    /**
56
     * @var string
57
     */
58
    private $ssl_cert = '';
59

60
    /**
61
     * @var string
62
     */
63
    private $ssl_key_type = '';
64

65
    /**
66
     * @var string|null
67
     */
68
    private $ssl_passphrase;
69

70
    /**
71
     * @var float|int|null
72
     */
73
    private $timeout;
74

75
    /**
76
     * @var float|int|null
77
     */
78
    private $connection_timeout;
79

80
    /**
81
     * @var string
82
     */
83
    private $method = Http::GET;
84

85
    /**
86
     * @var Headers
87
     */
88
    private $headers;
89

90
    /**
91
     * @var string
92
     */
93
    private $raw_headers = '';
94

95
    /**
96
     * @var bool
97
     */
98
    private $strict_ssl = false;
99

100
    /**
101
     * @var string
102
     */
103
    private $cache_control = '';
104

105
    /**
106
     * @var string
107
     */
108
    private $content_type = '';
109

110
    /**
111
     * @var string
112
     */
113
    private $content_charset = '';
114

115
    /**
116
     * @var string
117
     *             <p>e.g.: "gzip" or "deflate"</p>
118
     */
119
    private $content_encoding = '';
120

121
    /**
122
     * @var int|null
123
     *               <p>e.g.: 80 or 443</p>
124
     */
125
    private $port;
126

127
    /**
128
     * @var int
129
     */
130
    private $keep_alive = 300;
131

132
    /**
133
     * @var string
134
     */
135
    private $expected_type = '';
136

137
    /**
138
     * @var array
139
     */
140
    private $additional_curl_opts = [];
141

142
    /**
143
     * @var bool
144
     */
145
    private $auto_parse = true;
146

147
    /**
148
     * @var int
149
     */
150
    private $serialize_payload_method = self::SERIALIZE_PAYLOAD_SMART;
151

152
    /**
153
     * @var string
154
     */
155
    private $username = '';
156

157
    /**
158
     * @var string
159
     */
160
    private $password = '';
161

162
    /**
163
     * @var string|null
164
     */
165
    private $serialized_payload;
166

167
    /**
168
     * @var \CURLFile[]|string|string[]
169
     */
170
    private $payload = [];
171

172
    /**
173
     * @var array
174
     */
175
    private $params = [];
176

177
    /**
178
     * @var callable|null
179
     */
180
    private $parse_callback;
181

182
    /**
183
     * @var callable|LoggerInterface|null
184
     */
185
    private $error_handler;
186

187
    /**
188
     * @var callable[]
189
     */
190
    private $send_callbacks = [];
191

192
    /**
193
     * @var bool
194
     */
195
    private $follow_redirects = false;
196

197
    /**
198
     * @var int
199
     */
200
    private $max_redirects = self::MAX_REDIRECTS_DEFAULT;
201

202
    /**
203
     * @var array
204
     */
205
    private $payload_serializers = [];
206

207
    /**
208
     * Curl Object
209
     *
210
     * @var Curl|null
211
     */
212
    private $curl;
213

214
    /**
215
     * MultiCurl Object
216
     *
217
     * @var MultiCurl|null
218
     */
219
    private $curlMulti;
220

221
    /**
222
     * @var bool
223
     */
224
    private $debug = false;
225

226
    /**
227
     * @var string
228
     */
229
    private $protocol_version = Http::HTTP_1_1;
230

231
    /**
232
     * @var bool
233
     */
234
    private $retry_by_possible_encoding_error = false;
235

236
    /**
237
     * @var callable|string|null
238
     */
239
    private $file_path_for_download;
240

241
    /**
242
     * The Client::get, Client::post, ... syntax is preferred as it is more readable.
243
     *
244
     * @param string|null $method   Http Method
245
     * @param string|null $mime     Mime Type to Use
246
     * @param static|null $template "Request"-template object
247
     */
248
    public function __construct(
249
        string $method = null,
250
        string $mime = null,
251
        self $template = null
252
    ) {
253
        $this->initialize();
460✔
254

255
        $this->template = $template;
460✔
256
        $this->headers = new Headers();
460✔
257

258
        // fallback
259
        if (!isset($this->template)) {
460✔
260
            $this->template = new static(Http::GET, null, $this);
460✔
261
            $this->template = $this->template->disableStrictSSL();
460✔
262
        }
263

264
        $this->_setDefaultsFromTemplate()
460✔
265
            ->_setMethod($method)
460✔
266
            ->_withContentType($mime, Mime::PLAIN)
460✔
267
            ->_withExpectedType($mime, Mime::PLAIN);
460✔
268
    }
115✔
269

270
    /**
271
     * Does the heavy lifting.  Uses de facto HTTP
272
     * library cURL to set up the HTTP request.
273
     * Note: It does NOT actually send the request
274
     *
275
     * @throws \Exception
276
     *
277
     * @return static
278
     *
279
     * @internal
280
     */
281
    public function _curlPrep(): self
282
    {
283
        // Check for required stuff.
284
        if ($this->uri === null) {
184✔
285
            throw new RequestException($this, 'Attempting to send a request before defining a URI endpoint.');
×
286
        }
287

288
        // init
289
        $this->initialize();
184✔
290
        \assert($this->curl instanceof Curl);
291

292
        if ($this->params === []) {
184✔
293
            $this->_uriPrep();
184✔
294
        }
295

296
        if ($this->payload === []) {
184✔
297
            $this->serialized_payload = null;
144✔
298
        } else {
299
            $this->serialized_payload = $this->_serializePayload($this->payload);
40✔
300

301
            if (
302
                $this->serialized_payload
40✔
303
                &&
304
                $this->content_charset
40✔
305
                &&
306
                !$this->isUpload()
40✔
307
            ) {
308
                $this->serialized_payload = UTF8::encode(
×
309
                    $this->content_charset,
×
310
                    (string) $this->serialized_payload
×
311
                );
312
            }
313
        }
314

315
        if ($this->send_callbacks !== []) {
184✔
316
            foreach ($this->send_callbacks as $callback) {
4✔
317
                /** @noinspection VariableFunctionsUsageInspection */
318
                \call_user_func($callback, $this);
4✔
319
            }
320
        }
321

322
        \assert($this->curl instanceof Curl);
323

324
        $this->curl->setUrl((string) $this->uri);
184✔
325

326
        $ch = $this->curl->getCurl();
184✔
327
        if ($ch === false) {
184✔
328
            throw new NetworkErrorException('Unable to connect to "' . $this->uri . '". => "curl_init" === false');
×
329
        }
330

331
        $this->curl->setOpt(\CURLOPT_IPRESOLVE, \CURL_IPRESOLVE_WHATEVER);
184✔
332

333
        if ($this->method === Http::POST) {
184✔
334
            // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
335
            $this->curl->setOpt(\CURLOPT_POST, true);
32✔
336
        } else {
337
            $this->curl->setOpt(\CURLOPT_CUSTOMREQUEST, $this->method);
156✔
338
        }
339

340
        if ($this->method === Http::HEAD) {
184✔
341
            $this->curl->setOpt(\CURLOPT_NOBODY, true);
4✔
342
        }
343

344
        if ($this->hasBasicAuth()) {
184✔
345
            $this->curl->setOpt(\CURLOPT_USERPWD, $this->username . ':' . $this->password);
20✔
346
        }
347

348
        if ($this->hasClientSideCert()) {
184✔
349
            if (!\file_exists($this->ssl_key)) {
×
350
                throw new RequestException($this, 'Could not read Client Key');
×
351
            }
352

353
            if (!\file_exists($this->ssl_cert)) {
×
354
                throw new RequestException($this, 'Could not read Client Certificate');
×
355
            }
356

357
            $this->curl->setOpt(\CURLOPT_SSLCERTTYPE, $this->ssl_key_type);
×
358
            $this->curl->setOpt(\CURLOPT_SSLKEYTYPE, $this->ssl_key_type);
×
359
            $this->curl->setOpt(\CURLOPT_SSLCERT, $this->ssl_cert);
×
360
            $this->curl->setOpt(\CURLOPT_SSLKEY, $this->ssl_key);
×
361
            if ($this->ssl_passphrase !== null) {
×
362
                $this->curl->setOpt(\CURLOPT_SSLKEYPASSWD, $this->ssl_passphrase);
×
363
            }
364
        }
365

366
        $this->curl->setOpt(\CURLOPT_TCP_NODELAY, true);
184✔
367

368
        if ($this->hasTimeout()) {
184✔
369
            $this->curl->setOpt(\CURLOPT_TIMEOUT_MS, \round($this->timeout * 1000));
8✔
370
        }
371

372
        if ($this->hasConnectionTimeout()) {
184✔
373
            $this->curl->setOpt(\CURLOPT_CONNECTTIMEOUT_MS, \round($this->connection_timeout * 1000));
8✔
374

375
            if (\DIRECTORY_SEPARATOR !== '\\' && $this->connection_timeout < 1) {
8✔
376
                $this->curl->setOpt(\CURLOPT_NOSIGNAL, true);
4✔
377
            }
378
        }
379

380
        if ($this->follow_redirects === true) {
184✔
381
            $this->curl->setOpt(\CURLOPT_FOLLOWLOCATION, true);
48✔
382
            $this->curl->setOpt(\CURLOPT_MAXREDIRS, $this->max_redirects);
48✔
383
        }
384

385
        $this->curl->setOpt(\CURLOPT_SSL_VERIFYPEER, $this->strict_ssl);
184✔
386
        // zero is safe for all curl versions
387
        $verifyValue = $this->strict_ssl + 0;
184✔
388
        // support for value 1 removed in cURL 7.28.1 value 2 valid in all versions
389
        if ($verifyValue > 0) {
184✔
390
            ++$verifyValue;
4✔
391
        }
392
        $this->curl->setOpt(\CURLOPT_SSL_VERIFYHOST, $verifyValue);
184✔
393

394
        $this->curl->setOpt(\CURLOPT_RETURNTRANSFER, true);
184✔
395

396
        $this->curl->setOpt(\CURLOPT_ENCODING, $this->content_encoding);
184✔
397

398
        if ($this->port !== null) {
184✔
399
            $this->curl->setOpt(\CURLOPT_PORT, $this->port);
4✔
400
        }
401

402
        $this->curl->setOpt(\CURLOPT_PROTOCOLS, \CURLPROTO_HTTP | \CURLPROTO_HTTPS);
184✔
403

404
        $this->curl->setOpt(\CURLOPT_REDIR_PROTOCOLS, \CURLPROTO_HTTP | \CURLPROTO_HTTPS);
184✔
405

406
        // set Content-Length to the size of the payload if present
407
        if ($this->serialized_payload) {
184✔
408
            $this->curl->setOpt(\CURLOPT_POSTFIELDS, $this->serialized_payload);
40✔
409

410
            if (!$this->isUpload()) {
40✔
411
                $this->headers->forceSet('Content-Length', $this->_determineLength($this->serialized_payload));
40✔
412
            }
413
        }
414

415
        // init
416
        $headers = [];
184✔
417

418
        // Solve a bug on squid proxy, NONE/411 when miss content length.
419
        if (
420
            !$this->headers->offsetExists('Content-Length')
184✔
421
            &&
422
            !$this->isUpload()
184✔
423
        ) {
424
            $this->headers->forceSet('Content-Length', 0);
144✔
425
        }
426

427
        foreach ($this->headers as $header => $value) {
184✔
428
            if (\is_array($value)) {
184✔
429
                foreach ($value as $valueInner) {
184✔
430
                    $headers[] = "{$header}: {$valueInner}";
184✔
431
                }
432
            } else {
433
                $headers[] = "{$header}: {$value}";
×
434
            }
435
        }
436

437
        if ($this->keep_alive) {
184✔
438
            $headers[] = 'Connection: Keep-Alive';
184✔
439
            $headers[] = 'Keep-Alive: ' . $this->keep_alive;
184✔
440
        } else {
441
            $headers[] = 'Connection: close';
×
442
        }
443

444
        if (!$this->headers->offsetExists('User-Agent')) {
184✔
445
            $headers[] = $this->buildUserAgent();
180✔
446
        }
447

448
        if ($this->content_charset) {
184✔
449
            $contentType = $this->content_type . '; charset=' . $this->content_charset;
×
450
        } else {
451
            $contentType = $this->content_type;
184✔
452
        }
453
        $headers[] = 'Content-Type: ' . $contentType;
184✔
454

455
        if ($this->cache_control) {
184✔
456
            $headers[] = 'Cache-Control: ' . $this->cache_control;
4✔
457
        }
458

459
        // allow custom Accept header if set
460
        if (!$this->headers->offsetExists('Accept')) {
184✔
461
            // http://pretty-rfc.herokuapp.com/RFC2616#header.accept
462
            $accept = 'Accept: */*; q=0.5, text/plain; q=0.8, text/html;level=3;';
176✔
463

464
            if (!empty($this->expected_type)) {
176✔
465
                $accept .= 'q=0.9, ' . $this->expected_type;
176✔
466
            }
467

468
            $headers[] = $accept;
176✔
469
        }
470

471
        $url = \parse_url((string) $this->uri);
184✔
472

473
        if (\is_array($url) === false) {
184✔
474
            throw new ClientErrorException('Unable to connect to "' . $this->uri . '". => "parse_url" === false');
×
475
        }
476

477
        $path = ($url['path'] ?? '/') . (isset($url['query']) ? '?' . $url['query'] : '');
184✔
478
        $this->raw_headers = "{$this->method} {$path} HTTP/{$this->protocol_version}\r\n";
184✔
479
        $this->raw_headers .= \implode("\r\n", $headers);
184✔
480
        $this->raw_headers .= "\r\n";
184✔
481

482
        // DEBUG
483
        //var_dump($this->_headers->toArray(), $this->_raw_headers);
484

485
        /** @noinspection AlterInForeachInspection */
486
        foreach ($headers as &$header) {
184✔
487
            $pos_tmp = \strpos($header, ': ');
184✔
488
            if (
489
                $pos_tmp !== false
184✔
490
                &&
491
                \strlen($header) - 2 === $pos_tmp
184✔
492
            ) {
493
                // curl requires a special syntax to send empty headers
494
                $header = \substr_replace($header, ';', -2);
4✔
495
            }
496
        }
497
        $this->curl->setOpt(\CURLOPT_HTTPHEADER, $headers);
184✔
498

499
        if ($this->debug) {
184✔
500
            $this->curl->setOpt(\CURLOPT_VERBOSE, true);
×
501
        }
502

503
        // If there are some additional curl opts that the user wants to set, we can tack them in here.
504
        foreach ($this->additional_curl_opts as $curlOpt => $curlVal) {
184✔
505
            $this->curl->setOpt($curlOpt, $curlVal);
4✔
506
        }
507

508
        switch ($this->protocol_version) {
184✔
509
            case Http::HTTP_1_0:
510
                $this->curl->setOpt(\CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_0);
×
511

512
                break;
×
513
            case Http::HTTP_1_1:
184✔
514
                $this->curl->setOpt(\CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1);
180✔
515

516
                break;
180✔
517
            case Http::HTTP_2_0:
4✔
518
                $this->curl->setOpt(\CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_2_0);
4✔
519

520
                break;
4✔
521
            default:
522
                $this->curl->setOpt(\CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_NONE);
×
523

524
                break;
×
525
        }
526

527
        if ($this->file_path_for_download) {
184✔
528
            $this->curl->download($this->file_path_for_download);
4✔
529
            $this->curl->setOpt(\CURLOPT_CUSTOMREQUEST, 'GET');
4✔
530
            $this->curl->setOpt(\CURLOPT_HTTPGET, true);
4✔
531
            $this->disableAutoParsing();
4✔
532
        }
533

534
        return $this;
184✔
535
    }
536

537
    /**
538
     * @return Curl|null
539
     */
540
    public function _curl()
541
    {
542
        return $this->curl;
24✔
543
    }
544

545
    /**
546
     * @return MultiCurl|null
547
     */
548
    public function _curlMulti()
549
    {
550
        return $this->curlMulti;
×
551
    }
552

553
    /**
554
     * Takes care of building the query string to be used in the request URI.
555
     *
556
     * Any existing query string parameters, either passed as part of the URI
557
     * via uri() method, or passed via get() and friends will be preserved,
558
     * with additional parameters (added via params() or param()) appended.
559
     *
560
     * @internal
561
     *
562
     * @return void
563
     */
564
    public function _uriPrep()
565
    {
566
        if ($this->uri === null) {
184✔
567
            throw new ClientErrorException('Unable to connect. => "uri" === null');
×
568
        }
569

570
        $url = \parse_url((string) $this->uri);
184✔
571
        $originalParams = [];
184✔
572

573
        if ($url !== false) {
184✔
574
            if (
575
                isset($url['query'])
184✔
576
                &&
577
                $url['query']
184✔
578
            ) {
579
                \parse_str($url['query'], $originalParams);
40✔
580
            }
581

582
            $params = \array_merge($originalParams, $this->params);
184✔
583
        } else {
584
            $params = $this->params;
×
585
        }
586

587
        $queryString = \http_build_query($params);
184✔
588

589
        if (\strpos((string) $this->uri, '?') !== false) {
184✔
590
            $this->_withUri(
40✔
591
                $this->uri->withQuery(
40✔
592
                    \substr(
40✔
593
                        (string) $this->uri,
40✔
594
                        0,
40✔
595
                        \strpos((string) $this->uri, '?')
40✔
596
                    )
30✔
597
                )
30✔
598
            );
30✔
599
        }
600

601
        if (\count($params)) {
184✔
602
            $this->_withUri($this->uri->withQuery($queryString));
40✔
603
        }
604
    }
46✔
605

606
    /**
607
     * Callback invoked after payload has been serialized but before the request has been built.
608
     *
609
     * @param callable $callback (Request $request)
610
     *
611
     * @return static
612
     */
613
    public function beforeSend(callable $callback): self
614
    {
615
        $this->send_callbacks[] = $callback;
4✔
616

617
        return $this;
4✔
618
    }
619

620
    /**
621
     * @return string
622
     */
623
    public function buildUserAgent(): string
624
    {
625
        $user_agent = 'User-Agent: Http/PhpClient (cURL/';
180✔
626
        $curl = \curl_version();
180✔
627

628
        if ($curl && isset($curl['version'])) {
180✔
629
            $user_agent .= $curl['version'];
180✔
630
        } else {
631
            $user_agent .= '?.?.?';
×
632
        }
633

634
        $user_agent .= ' PHP/' . \PHP_VERSION . ' (' . \PHP_OS . ')';
180✔
635

636
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
180✔
637
            $tmp = \preg_replace('~PHP/[\d\.]+~U', '', $_SERVER['SERVER_SOFTWARE']);
×
638
            if (\is_string($tmp)) {
×
639
                $user_agent .= ' ' . $tmp;
×
640
            }
641
        } else {
642
            if (isset($_SERVER['TERM_PROGRAM'])) {
180✔
643
                $user_agent .= " {$_SERVER['TERM_PROGRAM']}";
×
644
            }
645

646
            if (isset($_SERVER['TERM_PROGRAM_VERSION'])) {
180✔
647
                $user_agent .= "/{$_SERVER['TERM_PROGRAM_VERSION']}";
×
648
            }
649
        }
650

651
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
180✔
652
            $user_agent .= " {$_SERVER['HTTP_USER_AGENT']}";
×
653
        }
654

655
        $user_agent .= ')';
180✔
656

657
        return $user_agent;
180✔
658
    }
659

660
    /**
661
     * Use Client Side Cert Authentication
662
     *
663
     * @param string      $key          file path to client key
664
     * @param string      $cert         file path to client cert
665
     * @param string|null $passphrase   for client key
666
     * @param string      $ssl_key_type default PEM
667
     *
668
     * @return static
669
     */
670
    public function clientSideCertAuth($cert, $key, $passphrase = null, $ssl_key_type = 'PEM'): self
671
    {
672
        $this->ssl_cert = $cert;
×
673
        $this->ssl_key = $key;
×
674
        $this->ssl_key_type = $ssl_key_type;
×
675
        $this->ssl_passphrase = $passphrase;
×
676

677
        return $this;
×
678
    }
679

680
    /**
681
     * @see Request::initialize()
682
     *
683
     * @return void
684
     */
685
    public function close()
686
    {
687
        if ($this->curl && $this->hasBeenInitialized()) {
×
688
            $this->curl->close();
×
689
        }
690

691
        if ($this->curlMulti && $this->hasBeenInitializedMulti()) {
×
692
            $this->curlMulti->close();
×
693
        }
694
    }
695

696
    /**
697
     * HTTP Method Get
698
     *
699
     * @param string|UriInterface $uri
700
     * @param string              $file_path
701
     *
702
     * @return static
703
     */
704
    public static function download($uri, $file_path): self
705
    {
706
        if ($uri instanceof UriInterface) {
4✔
707
            $uri = (string) $uri;
×
708
        }
709

710
        return (new self(Http::GET))
4✔
711
            ->withUriFromString($uri)
4✔
712
            ->withDownload($file_path)
4✔
713
            ->withCacheControl('no-cache')
4✔
714
            ->withContentEncoding(Encoding::NONE);
4✔
715
    }
716

717
    /**
718
     * HTTP Method Delete
719
     *
720
     * @param string|UriInterface $uri
721
     * @param array|null          $params
722
     * @param string|null         $mime
723
     *
724
     * @return static
725
     */
726
    public static function delete($uri, array $params = null, string $mime = null): self
727
    {
728
        if ($uri instanceof UriInterface) {
4✔
729
            $uri = (string) $uri;
×
730
        }
731

732
        $paramsString = '';
4✔
733
        if ($params !== null) {
4✔
734
            $paramsString = \http_build_query(
×
735
                $params,
×
736
                '',
×
737
                '&',
×
738
                \PHP_QUERY_RFC3986
×
739
            );
740
            if ($paramsString) {
×
741
                $paramsString = (\strpos($uri, '?') !== false ? '&' : '?') . $paramsString;
×
742
            }
743
        }
744

745
        return (new self(Http::DELETE))
4✔
746
            ->withUriFromString($uri . $paramsString)
4✔
747
            ->withMimeType($mime);
4✔
748
    }
749

750
    /**
751
     * @return static
752
     *
753
     * @see Request::enableAutoParsing()
754
     */
755
    public function disableAutoParsing(): self
756
    {
757
        return $this->_autoParse(false);
8✔
758
    }
759

760
    /**
761
     * @return static
762
     *
763
     * @see Request::enableKeepAlive()
764
     */
765
    public function disableKeepAlive(): self
766
    {
767
        $this->keep_alive = 0;
×
768

769
        return $this;
×
770
    }
771

772
    /**
773
     * @return static
774
     */
775
    public function disableRetryByPossibleEncodingError(): self
776
    {
777
        $this->retry_by_possible_encoding_error = false;
×
778

779
        return $this;
×
780
    }
781

782
    /**
783
     * @return static
784
     *
785
     * @see Request::enableStrictSSL()
786
     */
787
    public function disableStrictSSL(): self
788
    {
789
        return $this->_strictSSL(false);
460✔
790
    }
791

792
    /**
793
     * @return static
794
     *
795
     * @see Request::followRedirects()
796
     */
797
    public function doNotFollowRedirects(): self
798
    {
799
        return $this->followRedirects(false);
4✔
800
    }
801

802
    /**
803
     * @return static
804
     *
805
     * @see Request::disableAutoParsing()
806
     */
807
    public function enableAutoParsing(): self
808
    {
809
        return $this->_autoParse(true);
4✔
810
    }
811

812
    /**
813
     * @param int $seconds
814
     *
815
     * @return static
816
     *
817
     * @see Request::disableKeepAlive()
818
     */
819
    public function enableKeepAlive(int $seconds = 300): self
820
    {
821
        if ($seconds <= 0) {
×
822
            throw new \InvalidArgumentException(
×
823
                'Invalid keep-alive input: ' . \var_export($seconds, true)
×
824
            );
825
        }
826

827
        $this->keep_alive = $seconds;
×
828

829
        return $this;
×
830
    }
831

832
    /**
833
     * @return static
834
     */
835
    public function enableRetryByPossibleEncodingError(): self
836
    {
837
        $this->retry_by_possible_encoding_error = true;
×
838

839
        return $this;
×
840
    }
841

842
    /**
843
     * @return static
844
     *
845
     * @see Request::disableStrictSSL()
846
     */
847
    public function enableStrictSSL(): self
848
    {
849
        return $this->_strictSSL(true);
12✔
850
    }
851

852
    /**
853
     * @return static
854
     */
855
    public function expectsCsv(): self
856
    {
857
        return $this->withExpectedType(Mime::CSV);
×
858
    }
859

860
    /**
861
     * @return static
862
     */
863
    public function expectsForm(): self
864
    {
865
        return $this->withExpectedType(Mime::FORM);
×
866
    }
867

868
    /**
869
     * @return static
870
     */
871
    public function expectsHtml(): self
872
    {
873
        return $this->withExpectedType(Mime::HTML);
4✔
874
    }
875

876
    /**
877
     * @return static
878
     */
879
    public function expectsJavascript(): self
880
    {
881
        return $this->withExpectedType(Mime::JS);
×
882
    }
883

884
    /**
885
     * @return static
886
     */
887
    public function expectsJs(): self
888
    {
889
        return $this->withExpectedType(Mime::JS);
×
890
    }
891

892
    /**
893
     * @return static
894
     */
895
    public function expectsJson(): self
896
    {
897
        return $this->withExpectedType(Mime::JSON);
4✔
898
    }
899

900
    /**
901
     * @return static
902
     */
903
    public function expectsPlain(): self
904
    {
905
        return $this->withExpectedType(Mime::PLAIN);
×
906
    }
907

908
    /**
909
     * @return static
910
     */
911
    public function expectsText(): self
912
    {
913
        return $this->withExpectedType(Mime::PLAIN);
×
914
    }
915

916
    /**
917
     * @return static
918
     */
919
    public function expectsUpload(): self
920
    {
921
        return $this->withExpectedType(Mime::UPLOAD);
×
922
    }
923

924
    /**
925
     * @return static
926
     */
927
    public function expectsXhtml(): self
928
    {
929
        return $this->withExpectedType(Mime::XHTML);
×
930
    }
931

932
    /**
933
     * @return static
934
     */
935
    public function expectsXml(): self
936
    {
937
        return $this->withExpectedType(Mime::XML);
×
938
    }
939

940
    /**
941
     * @return static
942
     */
943
    public function expectsYaml(): self
944
    {
945
        return $this->withExpectedType(Mime::YAML);
×
946
    }
947

948
    /**
949
     * If the response is a 301 or 302 redirect, automatically
950
     * send off another request to that location
951
     *
952
     * @param bool $follow follow or not to follow or maximal number of redirects
953
     *
954
     * @return static
955
     */
956
    public function followRedirects(bool $follow = true): self
957
    {
958
        $new = clone $this;
52✔
959

960
        if ($follow === true) {
52✔
961
            $new->max_redirects = static::MAX_REDIRECTS_DEFAULT;
48✔
962
        } elseif ($follow === false) {
4✔
963
            $new->max_redirects = 0;
4✔
964
        } else {
965
            $new->max_redirects = \max(0, $follow);
×
966
        }
967

968
        $new->follow_redirects = $follow;
52✔
969

970
        return $new;
52✔
971
    }
972

973
    /**
974
     * HTTP Method Get
975
     *
976
     * @param string|UriInterface $uri
977
     * @param array|null          $params
978
     * @param string              $mime
979
     *
980
     * @return static
981
     */
982
    public static function get($uri, array $params = null, string $mime = null): self
983
    {
984
        if ($uri instanceof UriInterface) {
84✔
985
            $uri = (string) $uri;
×
986
        }
987

988
        $paramsString = '';
84✔
989
        if ($params !== null) {
84✔
990
            $paramsString = \http_build_query(
4✔
991
                $params,
4✔
992
                '',
4✔
993
                '&',
4✔
994
                \PHP_QUERY_RFC3986
4✔
995
            );
3✔
996
            if ($paramsString) {
4✔
997
                $paramsString = (\strpos($uri, '?') !== false ? '&' : '?') . $paramsString;
4✔
998
            }
999
        }
1000

1001
        return (new self(Http::GET))
84✔
1002
            ->withUriFromString($uri . $paramsString)
84✔
1003
            ->withMimeType($mime);
84✔
1004
    }
1005

1006
    /**
1007
     * Gets the body of the message.
1008
     *
1009
     * @return StreamInterface returns the body as a stream
1010
     */
1011
    public function getBody(): StreamInterface
1012
    {
1013
        return Http::stream($this->payload);
20✔
1014
    }
1015

1016
    /**
1017
     * Retrieves a message header value by the given case-insensitive name.
1018
     *
1019
     * This method returns an array of all the header values of the given
1020
     * case-insensitive header name.
1021
     *
1022
     * If the header does not appear in the message, this method MUST return an
1023
     * empty array.
1024
     *
1025
     * @param string $name case-insensitive header field name
1026
     *
1027
     * @return string[] An array of string values as provided for the given
1028
     *                  header. If the header does not appear in the message, this method MUST
1029
     *                  return an empty array.
1030
     */
1031
    public function getHeader($name): array
1032
    {
1033
        if ($this->headers->offsetExists($name)) {
44✔
1034
            $value = $this->headers->offsetGet($name);
44✔
1035

1036
            if (!\is_array($value)) {
44✔
1037
                return [\trim($value, " \t")];
×
1038
            }
1039

1040
            foreach ($value as $keyInner => $valueInner) {
44✔
1041
                $value[$keyInner] = \trim($valueInner, " \t");
44✔
1042
            }
1043

1044
            return $value;
44✔
1045
        }
1046

1047
        return [];
4✔
1048
    }
1049

1050
    /**
1051
     * Retrieves a comma-separated string of the values for a single header.
1052
     *
1053
     * This method returns all of the header values of the given
1054
     * case-insensitive header name as a string concatenated together using
1055
     * a comma.
1056
     *
1057
     * NOTE: Not all header values may be appropriately represented using
1058
     * comma concatenation. For such headers, use getHeader() instead
1059
     * and supply your own delimiter when concatenating.
1060
     *
1061
     * If the header does not appear in the message, this method MUST return
1062
     * an empty string.
1063
     *
1064
     * @param string $name case-insensitive header field name
1065
     *
1066
     * @return string A string of values as provided for the given header
1067
     *                concatenated together using a comma. If the header does not appear in
1068
     *                the message, this method MUST return an empty string.
1069
     */
1070
    public function getHeaderLine($name): string
1071
    {
1072
        return \implode(', ', $this->getHeader($name));
36✔
1073
    }
1074

1075
    /**
1076
     * @return array
1077
     */
1078
    public function getHeaders(): array
1079
    {
1080
        return $this->headers->toArray();
264✔
1081
    }
1082

1083
    /**
1084
     * Retrieves the HTTP method of the request.
1085
     *
1086
     * @return string returns the request method
1087
     */
1088
    public function getMethod(): string
1089
    {
1090
        return $this->method;
8✔
1091
    }
1092

1093
    /**
1094
     * Retrieves the HTTP protocol version as a string.
1095
     *
1096
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
1097
     *
1098
     * @return string HTTP protocol version
1099
     */
1100
    public function getProtocolVersion(): string
1101
    {
1102
        return $this->protocol_version;
×
1103
    }
1104

1105
    /**
1106
     * Retrieves the message's request target.
1107
     *
1108
     * Retrieves the message's request-target either as it will appear (for
1109
     * clients), as it appeared at request (for servers), or as it was
1110
     * specified for the instance (see withRequestTarget()).
1111
     *
1112
     * In most cases, this will be the origin-form of the composed URI,
1113
     * unless a value was provided to the concrete implementation (see
1114
     * withRequestTarget() below).
1115
     *
1116
     * If no URI is available, and no request-target has been specifically
1117
     * provided, this method MUST return the string "/".
1118
     *
1119
     * @return string
1120
     */
1121
    public function getRequestTarget(): string
1122
    {
1123
        if ($this->uri === null) {
20✔
1124
            return '/';
×
1125
        }
1126

1127
        $target = $this->uri->getPath();
20✔
1128

1129
        if (!$target) {
20✔
1130
            $target = '/';
8✔
1131
        }
1132

1133
        if ($this->uri->getQuery() !== '') {
20✔
1134
            $target .= '?' . $this->uri->getQuery();
12✔
1135
        }
1136

1137
        return $target;
20✔
1138
    }
1139

1140
    /**
1141
     * @return Uri|UriInterface|null
1142
     */
1143
    public function getUri()
1144
    {
1145
        return $this->uri;
20✔
1146
    }
1147

1148
    /**
1149
     * Checks if a header exists by the given case-insensitive name.
1150
     *
1151
     * @param string $name case-insensitive header field name
1152
     *
1153
     * @return bool Returns true if any header names match the given header
1154
     *              name using a case-insensitive string comparison. Returns false if
1155
     *              no matching header name is found in the message.
1156
     */
1157
    public function hasHeader($name): bool
1158
    {
1159
        return $this->headers->offsetExists($name);
×
1160
    }
1161

1162
    /**
1163
     * Return an instance with the specified header appended with the given value.
1164
     *
1165
     * Existing values for the specified header will be maintained. The new
1166
     * value(s) will be appended to the existing list. If the header did not
1167
     * exist previously, it will be added.
1168
     *
1169
     * This method MUST be implemented in such a way as to retain the
1170
     * immutability of the message, and MUST return an instance that has the
1171
     * new header and/or value.
1172
     *
1173
     * @param string          $name  case-insensitive header field name to add
1174
     * @param string|string[] $value header value(s)
1175
     *
1176
     * @throws \InvalidArgumentException for invalid header names or values
1177
     *
1178
     * @return static
1179
     */
1180
    public function withAddedHeader($name, $value)
1181
    {
1182
        if (!\is_string($name) || $name === '') {
24✔
1183
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
×
1184
        }
1185

1186
        $new = clone $this;
24✔
1187

1188
        if (!\is_array($value)) {
24✔
1189
            $value = [$value];
24✔
1190
        }
1191

1192
        if ($new->headers->offsetExists($name)) {
24✔
1193
            $new->headers->forceSet($name, \array_merge_recursive($new->headers->offsetGet($name), $value));
8✔
1194
        } else {
1195
            $new->headers->forceSet($name, $value);
24✔
1196
        }
1197

1198
        return $new;
24✔
1199
    }
1200

1201
    /**
1202
     * Return an instance with the specified message body.
1203
     *
1204
     * The body MUST be a StreamInterface object.
1205
     *
1206
     * This method MUST be implemented in such a way as to retain the
1207
     * immutability of the message, and MUST return a new instance that has the
1208
     * new body stream.
1209
     *
1210
     * @param StreamInterface $body
1211
     *
1212
     * @throws \InvalidArgumentException when the body is not valid
1213
     *
1214
     * @return static
1215
     */
1216
    public function withBody(StreamInterface $body)
1217
    {
1218
        $stream = Http::stream($body);
8✔
1219

1220
        $new = clone $this;
8✔
1221

1222
        return $new->_setBody($stream, null);
8✔
1223
    }
1224

1225
    /**
1226
     * Return an instance with the provided value replacing the specified header.
1227
     *
1228
     * While header names are case-insensitive, the casing of the header will
1229
     * be preserved by this function, and returned from getHeaders().
1230
     *
1231
     * This method MUST be implemented in such a way as to retain the
1232
     * immutability of the message, and MUST return an instance that has the
1233
     * new and/or updated header and value.
1234
     *
1235
     * @param string          $name  case-insensitive header field name
1236
     * @param string|string[] $value header value(s)
1237
     *
1238
     * @throws \InvalidArgumentException for invalid header names or values
1239
     *
1240
     * @return static
1241
     */
1242
    public function withHeader($name, $value): self
1243
    {
1244
        $new = clone $this;
40✔
1245

1246
        if (!\is_array($value)) {
40✔
1247
            $value = [$value];
36✔
1248
        }
1249

1250
        $new->headers->forceSet($name, $value);
40✔
1251

1252
        return $new;
36✔
1253
    }
1254

1255
    /**
1256
     * Return an instance with the provided HTTP method.
1257
     *
1258
     * While HTTP method names are typically all uppercase characters, HTTP
1259
     * method names are case-sensitive and thus implementations SHOULD NOT
1260
     * modify the given string.
1261
     *
1262
     * This method MUST be implemented in such a way as to retain the
1263
     * immutability of the message, and MUST return an instance that has the
1264
     * changed request method.
1265
     *
1266
     * @param string $method
1267
     *                       <p>\Httpful\Http::GET, \Httpful\Http::POST, ...</p>
1268
     *
1269
     * @throws \InvalidArgumentException for invalid HTTP methods
1270
     *
1271
     * @return static
1272
     */
1273
    public function withMethod($method)
1274
    {
1275
        $new = clone $this;
4✔
1276

1277
        $new->_setMethod($method);
4✔
1278

1279
        return $new;
4✔
1280
    }
1281

1282
    /**
1283
     * Return an instance with the specified HTTP protocol version.
1284
     *
1285
     * The version string MUST contain only the HTTP version number (e.g.,
1286
     * "2, 1.1", "1.0").
1287
     *
1288
     * This method MUST be implemented in such a way as to retain the
1289
     * immutability of the message, and MUST return an instance that has the
1290
     * new protocol version.
1291
     *
1292
     * @param string $version
1293
     *                        <p>Http::HTTP_*</p>
1294
     *
1295
     * @return static
1296
     */
1297
    public function withProtocolVersion($version)
1298
    {
1299
        $new = clone $this;
4✔
1300

1301
        $new->protocol_version = $version;
4✔
1302

1303
        return $new;
4✔
1304
    }
1305

1306
    /**
1307
     * Return an instance with the specific request-target.
1308
     *
1309
     * If the request needs a non-origin-form request-target — e.g., for
1310
     * specifying an absolute-form, authority-form, or asterisk-form —
1311
     * this method may be used to create an instance with the specified
1312
     * request-target, verbatim.
1313
     *
1314
     * This method MUST be implemented in such a way as to retain the
1315
     * immutability of the message, and MUST return an instance that has the
1316
     * changed request target.
1317
     *
1318
     * @see http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
1319
     *     request-target forms allowed in request messages)
1320
     *
1321
     * @param mixed $requestTarget
1322
     *
1323
     * @return static
1324
     */
1325
    public function withRequestTarget($requestTarget)
1326
    {
1327
        if (\preg_match('#\\s#', $requestTarget)) {
12✔
1328
            throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
8✔
1329
        }
1330

1331
        $new = clone $this;
4✔
1332

1333
        if ($new->uri !== null) {
4✔
1334
            $new->_withUri($new->uri->withPath($requestTarget));
4✔
1335
        }
1336

1337
        return $new;
4✔
1338
    }
1339

1340
    /**
1341
     * Returns an instance with the provided URI.
1342
     *
1343
     * This method MUST update the Host header of the returned request by
1344
     * default if the URI contains a host component. If the URI does not
1345
     * contain a host component, any pre-existing Host header MUST be carried
1346
     * over to the returned request.
1347
     *
1348
     * You can opt-in to preserving the original state of the Host header by
1349
     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
1350
     * `true`, this method interacts with the Host header in the following ways:
1351
     *
1352
     * - If the Host header is missing or empty, and the new URI contains
1353
     *   a host component, this method MUST update the Host header in the returned
1354
     *   request.
1355
     * - If the Host header is missing or empty, and the new URI does not contain a
1356
     *   host component, this method MUST NOT update the Host header in the returned
1357
     *   request.
1358
     * - If a Host header is present and non-empty, this method MUST NOT update
1359
     *   the Host header in the returned request.
1360
     *
1361
     * This method MUST be implemented in such a way as to retain the
1362
     * immutability of the message, and MUST return an instance that has the
1363
     * new UriInterface instance.
1364
     *
1365
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
1366
     *
1367
     * @param UriInterface $uri          new request URI to use
1368
     * @param bool         $preserveHost preserve the original state of the Host header
1369
     *
1370
     * @return static
1371
     */
1372
    public function withUri(UriInterface $uri, $preserveHost = false)
1373
    {
1374
        $new = clone $this;
308✔
1375

1376
        return $new->_withUri($uri, $preserveHost);
308✔
1377
    }
1378

1379
    /**
1380
     * Return an instance without the specified header.
1381
     *
1382
     * Header resolution MUST be done without case-sensitivity.
1383
     *
1384
     * This method MUST be implemented in such a way as to retain the
1385
     * immutability of the message, and MUST return an instance that removes
1386
     * the named header.
1387
     *
1388
     * @param string $name case-insensitive header field name to remove
1389
     *
1390
     * @return static
1391
     */
1392
    public function withoutHeader($name): self
1393
    {
1394
        $new = clone $this;
256✔
1395

1396
        $new->headers->forceUnset($name);
256✔
1397

1398
        return $new;
256✔
1399
    }
1400

1401
    /**
1402
     * @return string
1403
     */
1404
    public function getContentType(): string
1405
    {
1406
        return $this->content_type;
16✔
1407
    }
1408

1409
    /**
1410
     * @return callable|LoggerInterface|null
1411
     */
1412
    public function getErrorHandler()
1413
    {
1414
        return $this->error_handler;
×
1415
    }
1416

1417
    /**
1418
     * @return string
1419
     */
1420
    public function getExpectedType(): string
1421
    {
1422
        return $this->expected_type;
200✔
1423
    }
1424

1425
    /**
1426
     * @return string
1427
     */
1428
    public function getHttpMethod(): string
1429
    {
1430
        return $this->method;
12✔
1431
    }
1432

1433
    /**
1434
     * @return \ArrayObject
1435
     */
1436
    public function getIterator(): \ArrayObject
1437
    {
1438
        // init
1439
        $elements = new \ArrayObject();
460✔
1440

1441
        foreach (\get_object_vars($this) as $f => $v) {
460✔
1442
            $elements[$f] = $v;
460✔
1443
        }
1444

1445
        return $elements;
460✔
1446
    }
1447

1448
    /**
1449
     * @return callable|null
1450
     */
1451
    public function getParseCallback()
1452
    {
1453
        return $this->parse_callback;
×
1454
    }
1455

1456
    /**
1457
     * @return array
1458
     */
1459
    public function getPayload(): array
1460
    {
1461
        return \is_string($this->payload) ? [$this->payload] : $this->payload;
4✔
1462
    }
1463

1464
    /**
1465
     * @return string
1466
     */
1467
    public function getRawHeaders(): string
1468
    {
1469
        return $this->raw_headers;
20✔
1470
    }
1471

1472
    /**
1473
     * @return callable[]
1474
     */
1475
    public function getSendCallback(): array
1476
    {
1477
        return $this->send_callbacks;
×
1478
    }
1479

1480
    /**
1481
     * @return int
1482
     */
1483
    public function getSerializePayloadMethod(): int
1484
    {
1485
        return $this->serialize_payload_method;
4✔
1486
    }
1487

1488
    /**
1489
     * @return mixed|null
1490
     */
1491
    public function getSerializedPayload()
1492
    {
1493
        return $this->serialized_payload;
12✔
1494
    }
1495

1496
    /**
1497
     * @return string
1498
     */
1499
    public function getUriString(): string
1500
    {
1501
        return (string) $this->uri;
8✔
1502
    }
1503

1504
    /**
1505
     * Is this request setup for basic auth?
1506
     *
1507
     * @return bool
1508
     */
1509
    public function hasBasicAuth(): bool
1510
    {
1511
        return $this->password && $this->username;
188✔
1512
    }
1513

1514
    /**
1515
     * @return bool has the internal curl (non multi) request been initialized?
1516
     */
1517
    public function hasBeenInitialized(): bool
1518
    {
1519
        if (!$this->curl) {
184✔
1520
            return false;
×
1521
        }
1522

1523
        return \is_resource($this->curl->getCurl());
184✔
1524
    }
1525

1526
    /**
1527
     * @return bool has the internal curl (multi) request been initialized?
1528
     */
1529
    public function hasBeenInitializedMulti(): bool
1530
    {
1531
        if (!$this->curlMulti) {
×
1532
            return false;
×
1533
        }
1534

1535
        return \is_resource($this->curlMulti->getMultiCurl());
×
1536
    }
1537

1538
    /**
1539
     * @return bool is this request setup for client side cert?
1540
     */
1541
    public function hasClientSideCert(): bool
1542
    {
1543
        return $this->ssl_cert && $this->ssl_key;
184✔
1544
    }
1545

1546
    /**
1547
     * @return bool does the request have a connection timeout?
1548
     */
1549
    public function hasConnectionTimeout(): bool
1550
    {
1551
        return isset($this->connection_timeout);
184✔
1552
    }
1553

1554
    /**
1555
     * Is this request setup for digest auth?
1556
     *
1557
     * @return bool
1558
     */
1559
    public function hasDigestAuth(): bool
1560
    {
1561
        return $this->password
4✔
1562
               &&
3✔
1563
               $this->username
4✔
1564
               &&
3✔
1565
               $this->additional_curl_opts[\CURLOPT_HTTPAUTH] === \CURLAUTH_DIGEST;
4✔
1566
    }
1567

1568
    /**
1569
     * @return bool
1570
     */
1571
    public function hasParseCallback(): bool
1572
    {
1573
        return isset($this->parse_callback)
184✔
1574
               &&
138✔
1575
               \is_callable($this->parse_callback);
184✔
1576
    }
1577

1578
    /**
1579
     * @return bool is this request setup for using proxy?
1580
     */
1581
    public function hasProxy(): bool
1582
    {
1583
        /**
1584
         *  We must be aware that proxy variables could come from environment also.
1585
         *  In curl extension, http proxy can be specified not only via CURLOPT_PROXY option,
1586
         *  but also by environment variable called http_proxy.
1587
         */
1588
        return (
9✔
1589
            isset($this->additional_curl_opts[\CURLOPT_PROXY])
12✔
1590
            && \is_string($this->additional_curl_opts[\CURLOPT_PROXY])
10✔
1591
        ) || \getenv('http_proxy');
12✔
1592
    }
1593

1594
    /**
1595
     * @return bool does the request have a timeout?
1596
     */
1597
    public function hasTimeout(): bool
1598
    {
1599
        return isset($this->timeout);
184✔
1600
    }
1601

1602
    /**
1603
     * HTTP Method Head
1604
     *
1605
     * @param string|UriInterface $uri
1606
     *
1607
     * @return static
1608
     */
1609
    public static function head($uri): self
1610
    {
1611
        if ($uri instanceof UriInterface) {
8✔
1612
            $uri = (string) $uri;
×
1613
        }
1614

1615
        return (new self(Http::HEAD))
8✔
1616
            ->withUriFromString($uri)
8✔
1617
            ->withMimeType(Mime::PLAIN);
8✔
1618
    }
1619

1620
    /**
1621
     * @see Request::close()
1622
     *
1623
     * @return void
1624
     */
1625
    public function initializeMulti()
1626
    {
1627
        if (!$this->curlMulti || $this->hasBeenInitializedMulti()) {
24✔
1628
            $this->curlMulti = new MultiCurl();
24✔
1629
        }
1630
    }
6✔
1631

1632
    /**
1633
     * @see Request::close()
1634
     *
1635
     * @return void
1636
     */
1637
    public function initialize()
1638
    {
1639
        if (!$this->curl || !$this->hasBeenInitialized()) {
460✔
1640
            $this->curl = new Curl();
460✔
1641
        }
1642
    }
115✔
1643

1644
    /**
1645
     * @return bool
1646
     */
1647
    public function isAutoParse(): bool
1648
    {
1649
        return $this->auto_parse;
184✔
1650
    }
1651

1652
    /**
1653
     * @return bool
1654
     */
1655
    public function isJson(): bool
1656
    {
1657
        return $this->content_type === Mime::JSON;
×
1658
    }
1659

1660
    /**
1661
     * @return bool
1662
     */
1663
    public function isStrictSSL(): bool
1664
    {
1665
        return $this->strict_ssl;
12✔
1666
    }
1667

1668
    /**
1669
     * @return bool
1670
     */
1671
    public function isUpload(): bool
1672
    {
1673
        return $this->content_type === Mime::UPLOAD;
460✔
1674
    }
1675

1676
    /**
1677
     * @return static
1678
     *
1679
     * @see Request::serializePayloadMode()
1680
     */
1681
    public function neverSerializePayload(): self
1682
    {
1683
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_NEVER);
12✔
1684
    }
1685

1686
    /**
1687
     * HTTP Method Options
1688
     *
1689
     * @param string|UriInterface $uri
1690
     *
1691
     * @return static
1692
     */
1693
    public static function options($uri): self
1694
    {
1695
        if ($uri instanceof UriInterface) {
4✔
1696
            $uri = (string) $uri;
×
1697
        }
1698

1699
        return (new self(Http::OPTIONS))->withUriFromString($uri);
4✔
1700
    }
1701

1702
    /**
1703
     * HTTP Method Patch
1704
     *
1705
     * @param string|UriInterface $uri
1706
     * @param mixed               $payload data to send in body of request
1707
     * @param string              $mime    MIME to use for Content-Type
1708
     *
1709
     * @return static
1710
     */
1711
    public static function patch($uri, $payload = null, string $mime = null): self
1712
    {
1713
        if ($uri instanceof UriInterface) {
4✔
1714
            $uri = (string) $uri;
×
1715
        }
1716

1717
        return (new self(Http::PATCH))
4✔
1718
            ->withUriFromString($uri)
4✔
1719
            ->_setBody($payload, null, $mime);
4✔
1720
    }
1721

1722
    /**
1723
     * HTTP Method Post
1724
     *
1725
     * @param string|UriInterface $uri
1726
     * @param mixed               $payload data to send in body of request
1727
     * @param string              $mime    MIME to use for Content-Type
1728
     *
1729
     * @return static
1730
     */
1731
    public static function post($uri, $payload = null, string $mime = null): self
1732
    {
1733
        if ($uri instanceof UriInterface) {
32✔
1734
            $uri = (string) $uri;
×
1735
        }
1736

1737
        return (new self(Http::POST))
32✔
1738
            ->withUriFromString($uri)
32✔
1739
            ->_setBody($payload, null, $mime);
32✔
1740
    }
1741

1742
    /**
1743
     * HTTP Method Put
1744
     *
1745
     * @param string|UriInterface $uri
1746
     * @param mixed               $payload data to send in body of request
1747
     * @param string              $mime    MIME to use for Content-Type
1748
     *
1749
     * @return static
1750
     */
1751
    public static function put($uri, $payload = null, string $mime = null): self
1752
    {
1753
        if ($uri instanceof UriInterface) {
8✔
1754
            $uri = (string) $uri;
×
1755
        }
1756

1757
        return (new self(Http::PUT))
8✔
1758
            ->withUriFromString($uri)
8✔
1759
            ->_setBody($payload, null, $mime);
8✔
1760
    }
1761

1762
    /**
1763
     * Register a callback that will be used to serialize the payload
1764
     * for a particular mime type.  When using "*" for the mime
1765
     * type, it will use that parser for all responses regardless of the mime
1766
     * type.  If a custom '*' and 'application/json' exist, the custom
1767
     * 'application/json' would take precedence over the '*' callback.
1768
     *
1769
     * @param string   $mime     mime type we're registering
1770
     * @param callable $callback takes one argument, $payload,
1771
     *                           which is the payload that we'll be
1772
     *
1773
     * @return static
1774
     */
1775
    public function registerPayloadSerializer($mime, callable $callback): self
1776
    {
1777
        $new = clone $this;
×
1778

1779
        $new->payload_serializers[Mime::getFullMime($mime)] = $callback;
×
1780

1781
        return $new;
×
1782
    }
1783

1784
    /**
1785
     * @return void
1786
     */
1787
    public function reset()
1788
    {
1789
        $this->headers = new Headers();
×
1790

1791
        $this->close();
×
1792
        $this->initialize();
×
1793
    }
1794

1795
    /**
1796
     * Actually send off the request, and parse the response.
1797
     *
1798
     * @param callable|null $onSuccessCallback
1799
     * @param callable|null $onCompleteCallback
1800
     * @param callable|null $onBeforeSendCallback
1801
     * @param callable|null $onErrorCallback
1802
     *
1803
     * @throws NetworkErrorException when unable to parse or communicate w server
1804
     *
1805
     * @return MultiCurl
1806
     */
1807
    public function initMulti(
1808
        $onSuccessCallback = null,
1809
        $onCompleteCallback = null,
1810
        $onBeforeSendCallback = null,
1811
        $onErrorCallback = null
1812
    ) {
1813
        $this->initializeMulti();
24✔
1814
        \assert($this->curlMulti instanceof MultiCurl);
1815

1816
        if ($onSuccessCallback !== null) {
24✔
1817
            $this->curlMulti->success(
12✔
1818
                static function (Curl $instance) use ($onSuccessCallback) {
9✔
1819
                    if ($instance->request instanceof self) {
12✔
1820
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
12✔
1821
                    } else {
1822
                        $response = $instance->rawResponse;
×
1823
                    }
1824

1825
                    $onSuccessCallback(
12✔
1826
                        $response,
12✔
1827
                        $instance->request,
12✔
1828
                        $instance
12✔
1829
                    );
9✔
1830
                }
12✔
1831
            );
9✔
1832
        }
1833

1834
        if ($onCompleteCallback !== null) {
24✔
1835
            $this->curlMulti->complete(
×
1836
                static function (Curl $instance) use ($onCompleteCallback) {
1837
                    if ($instance->request instanceof self) {
×
1838
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1839
                    } else {
1840
                        $response = $instance->rawResponse;
×
1841
                    }
1842

1843
                    $onCompleteCallback(
×
1844
                        $response,
×
1845
                        $instance->request,
×
1846
                        $instance
×
1847
                    );
1848
                }
×
1849
            );
1850
        }
1851

1852
        if ($onBeforeSendCallback !== null) {
24✔
1853
            $this->curlMulti->beforeSend(
×
1854
                static function (Curl $instance) use ($onBeforeSendCallback) {
1855
                    if ($instance->request instanceof self) {
×
1856
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1857
                    } else {
1858
                        $response = $instance->rawResponse;
×
1859
                    }
1860

1861
                    $onBeforeSendCallback(
×
1862
                        $response,
×
1863
                        $instance->request,
×
1864
                        $instance
×
1865
                    );
1866
                }
×
1867
            );
1868
        }
1869

1870
        if ($onErrorCallback !== null) {
24✔
1871
            $this->curlMulti->error(
×
1872
                static function (Curl $instance) use ($onErrorCallback) {
1873
                    if ($instance->request instanceof self) {
×
1874
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1875
                    } else {
1876
                        $response = $instance->rawResponse;
×
1877
                    }
1878

1879
                    $onErrorCallback(
×
1880
                        $response,
×
1881
                        $instance->request,
×
1882
                        $instance
×
1883
                    );
1884
                }
×
1885
            );
1886
        }
1887

1888
        return $this->curlMulti;
24✔
1889
    }
1890

1891
    /**
1892
     * Actually send off the request, and parse the response.
1893
     *
1894
     * @throws NetworkErrorException when unable to parse or communicate w server
1895
     *
1896
     * @return Response
1897
     */
1898
    public function send(): Response
1899
    {
1900
        $this->_curlPrep();
128✔
1901
        \assert($this->curl instanceof Curl);
1902

1903
        $result = $this->curl->exec();
128✔
1904

1905
        if (
1906
            $result === false
128✔
1907
            &&
1908
            $this->retry_by_possible_encoding_error
128✔
1909
        ) {
1910
            // Possibly a gzip issue makes curl unhappy.
1911
            if (
1912
                $this->curl->errorCode === \CURLE_WRITE_ERROR
×
1913
                ||
1914
                $this->curl->errorCode === \CURLE_BAD_CONTENT_ENCODING
×
1915
            ) {
1916
                // Docs say 'identity,' but 'none' seems to work (sometimes?).
1917
                $this->curl->setOpt(\CURLOPT_ENCODING, 'none');
×
1918

1919
                $result = $this->curl->exec();
×
1920

1921
                if ($result === false) {
×
1922
                    /** @noinspection NotOptimalIfConditionsInspection */
1923
                    if (
1924
                        /* @phpstan-ignore-next-line | FP? */
1925
                        $this->curl->errorCode === \CURLE_WRITE_ERROR
×
1926
                        ||
1927
                        $this->curl->errorCode === \CURLE_BAD_CONTENT_ENCODING
×
1928
                    ) {
1929
                        $this->curl->setOpt(\CURLOPT_ENCODING, 'identity');
×
1930

1931
                        $result = $this->curl->exec();
×
1932
                    }
1933
                }
1934
            }
1935
        }
1936

1937
        if (!$this->keep_alive) {
128✔
1938
            $this->close();
×
1939
        }
1940

1941
        return $this->_buildResponse($result);
128✔
1942
    }
1943

1944
    /**
1945
     * @return static
1946
     */
1947
    public function sendsCsv(): self
1948
    {
1949
        return $this->withContentType(Mime::CSV);
×
1950
    }
1951

1952
    /**
1953
     * @return static
1954
     */
1955
    public function sendsForm(): self
1956
    {
1957
        return $this->withContentType(Mime::FORM);
×
1958
    }
1959

1960
    /**
1961
     * @return static
1962
     */
1963
    public function sendsHtml(): self
1964
    {
1965
        return $this->withContentType(Mime::HTML);
×
1966
    }
1967

1968
    /**
1969
     * @return static
1970
     */
1971
    public function sendsJavascript(): self
1972
    {
1973
        return $this->withContentType(Mime::JS);
×
1974
    }
1975

1976
    /**
1977
     * @return static
1978
     */
1979
    public function sendsJs(): self
1980
    {
1981
        return $this->withContentType(Mime::JS);
×
1982
    }
1983

1984
    /**
1985
     * @return static
1986
     */
1987
    public function sendsJson(): self
1988
    {
1989
        return $this->withContentType(Mime::JSON);
×
1990
    }
1991

1992
    /**
1993
     * @return static
1994
     */
1995
    public function sendsPlain(): self
1996
    {
1997
        return $this->withContentType(Mime::PLAIN);
×
1998
    }
1999

2000
    /**
2001
     * @return static
2002
     */
2003
    public function sendsText(): self
2004
    {
2005
        return $this->withContentType(Mime::PLAIN);
×
2006
    }
2007

2008
    /**
2009
     * @return static
2010
     */
2011
    public function sendsUpload(): self
2012
    {
2013
        return $this->withContentType(Mime::UPLOAD);
×
2014
    }
2015

2016
    /**
2017
     * @return static
2018
     */
2019
    public function sendsXhtml(): self
2020
    {
2021
        return $this->withContentType(Mime::XHTML);
×
2022
    }
2023

2024
    /**
2025
     * @return static
2026
     */
2027
    public function sendsXml(): self
2028
    {
2029
        return $this->withContentType(Mime::XML);
×
2030
    }
2031

2032
    /**
2033
     * Determine how/if we use the built in serialization by
2034
     * setting the serialize_payload_method
2035
     * The default (SERIALIZE_PAYLOAD_SMART) is...
2036
     *  - if payload is not a scalar (object/array)
2037
     *    use the appropriate serialize method according to
2038
     *    the Content-Type of this request.
2039
     *  - if the payload IS a scalar (int, float, string, bool)
2040
     *    than just return it as is.
2041
     * When this option is set SERIALIZE_PAYLOAD_ALWAYS,
2042
     * it will always use the appropriate
2043
     * serialize option regardless of whether payload is scalar or not
2044
     * When this option is set SERIALIZE_PAYLOAD_NEVER,
2045
     * it will never use any of the serialization methods.
2046
     * Really the only use for this is if you want the serialize methods
2047
     * to handle strings or not (e.g. Blah is not valid JSON, but "Blah"
2048
     * is).  Forcing the serialization helps prevent that kind of error from
2049
     * happening.
2050
     *
2051
     * @param int $mode Request::SERIALIZE_PAYLOAD_*
2052
     *
2053
     * @return static
2054
     */
2055
    public function serializePayloadMode(int $mode): self
2056
    {
2057
        $this->serialize_payload_method = $mode;
12✔
2058

2059
        return $this;
12✔
2060
    }
2061

2062
    /**
2063
     * This method is the default behavior
2064
     *
2065
     * @return static
2066
     *
2067
     * @see Request::serializePayloadMode()
2068
     */
2069
    public function smartSerializePayload(): self
2070
    {
2071
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_SMART);
×
2072
    }
2073

2074
    /**
2075
     * Specify a HTTP timeout
2076
     *
2077
     * @param float|int $timeout seconds to timeout the HTTP call
2078
     *
2079
     * @return static
2080
     */
2081
    public function withTimeout($timeout): self
2082
    {
2083
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $timeout)) {
12✔
2084
            throw new \InvalidArgumentException(
×
2085
                'Invalid timeout provided: ' . \var_export($timeout, true)
×
2086
            );
2087
        }
2088

2089
        $new = clone $this;
12✔
2090

2091
        $new->timeout = $timeout;
12✔
2092

2093
        return $new;
12✔
2094
    }
2095

2096
    /**
2097
     * Shortcut for useProxy to configure SOCKS 4 proxy
2098
     *
2099
     * @param string   $proxy_host    Hostname or address of the proxy
2100
     * @param int      $proxy_port    Port of the proxy. Default 80
2101
     * @param int|null $auth_type     Authentication type or null. Accepted values are CURLAUTH_BASIC, CURLAUTH_NTLM.
2102
     *                                Default null, no authentication
2103
     * @param string   $auth_username Authentication username. Default null
2104
     * @param string   $auth_password Authentication password. Default null
2105
     *
2106
     * @return static
2107
     *
2108
     * @see Request::withProxy
2109
     */
2110
    public function useSocks4Proxy(
2111
        $proxy_host,
2112
        $proxy_port = 80,
2113
        $auth_type = null,
2114
        $auth_username = null,
2115
        $auth_password = null
2116
    ): self {
2117
        return $this->withProxy(
×
2118
            $proxy_host,
×
2119
            $proxy_port,
×
2120
            $auth_type,
×
2121
            $auth_username,
×
2122
            $auth_password,
×
2123
            Proxy::SOCKS4
×
2124
        );
2125
    }
2126

2127
    /**
2128
     * Shortcut for useProxy to configure SOCKS 5 proxy
2129
     *
2130
     * @param string      $proxy_host
2131
     * @param int         $proxy_port
2132
     * @param int|null    $auth_type
2133
     * @param string|null $auth_username
2134
     * @param string|null $auth_password
2135
     *
2136
     * @return static
2137
     *
2138
     * @see Request::withProxy
2139
     */
2140
    public function useSocks5Proxy(
2141
        $proxy_host,
2142
        $proxy_port = 80,
2143
        $auth_type = null,
2144
        $auth_username = null,
2145
        $auth_password = null
2146
    ): self {
2147
        return $this->withProxy(
×
2148
            $proxy_host,
×
2149
            $proxy_port,
×
2150
            $auth_type,
×
2151
            $auth_username,
×
2152
            $auth_password,
×
2153
            Proxy::SOCKS5
×
2154
        );
2155
    }
2156

2157
    /**
2158
     * @param string $name
2159
     * @param string $value
2160
     *
2161
     * @return static
2162
     */
2163
    public function withAddedCookie(string $name, string $value): self
2164
    {
2165
        return $this->withAddedHeader('Cookie', "{$name}={$value}");
8✔
2166
    }
2167

2168
    /**
2169
     * @param array<string,string> $files
2170
     *
2171
     * @return static
2172
     */
2173
    public function withAttachment($files): self
2174
    {
2175
        $new = clone $this;
4✔
2176

2177
        $fInfo = \finfo_open(\FILEINFO_MIME_TYPE);
4✔
2178
        if ($fInfo === false) {
4✔
2179
            /** @noinspection ForgottenDebugOutputInspection */
2180
            \error_log('finfo_open() did not work', \E_USER_WARNING);
×
2181

2182
            return $new;
×
2183
        }
2184

2185
        foreach ($files as $key => $file) {
4✔
2186
            $mimeType = \finfo_file($fInfo, $file);
4✔
2187
            if ($mimeType !== false) {
4✔
2188
                if (\is_string($new->payload)) {
4✔
2189
                    $new->payload = []; // reset
×
2190
                }
2191
                $new->payload[$key] = \curl_file_create($file, $mimeType, \basename($file));
4✔
2192
            }
2193
        }
2194

2195
        \finfo_close($fInfo);
4✔
2196

2197
        return $new->_withContentType(Mime::UPLOAD);
4✔
2198
    }
2199

2200
    /**
2201
     * User Basic Auth.
2202
     *
2203
     * Only use when over SSL/TSL/HTTPS.
2204
     *
2205
     * @param string $username
2206
     * @param string $password
2207
     *
2208
     * @return static
2209
     */
2210
    public function withBasicAuth($username, $password): self
2211
    {
2212
        $new = clone $this;
28✔
2213
        $new->username = $username;
28✔
2214
        $new->password = $password;
28✔
2215

2216
        return $new;
28✔
2217
    }
2218

2219
    /**
2220
     * @param array $body
2221
     *
2222
     * @return static
2223
     */
2224
    public function withBodyFromArray(array $body)
2225
    {
2226
        return $this->_setBody($body, null);
4✔
2227
    }
2228

2229
    /**
2230
     * @param string $body
2231
     *
2232
     * @return static
2233
     */
2234
    public function withBodyFromString(string $body)
2235
    {
2236
        $stream = Http::stream($body);
36✔
2237

2238
        return $this->_setBody($stream->getContents(), null);
36✔
2239
    }
2240

2241
    /**
2242
     * Specify a HTTP connection timeout
2243
     *
2244
     * @param float|int $connection_timeout seconds to timeout the HTTP connection
2245
     *
2246
     * @throws \InvalidArgumentException
2247
     *
2248
     * @return static
2249
     */
2250
    public function withConnectionTimeoutInSeconds($connection_timeout): self
2251
    {
2252
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $connection_timeout)) {
12✔
2253
            throw new \InvalidArgumentException(
×
2254
                'Invalid connection timeout provided: ' . \var_export($connection_timeout, true)
×
2255
            );
2256
        }
2257

2258
        $new = clone $this;
12✔
2259

2260
        $new->connection_timeout = $connection_timeout;
12✔
2261

2262
        return $new;
12✔
2263
    }
2264

2265
    /**
2266
     * @param string $cache_control
2267
     *                              <p>e.g. 'no-cache', 'public', ...</p>
2268
     *
2269
     * @return static
2270
     */
2271
    public function withCacheControl(string $cache_control): self
2272
    {
2273
        $new = clone $this;
4✔
2274

2275
        if (empty($cache_control)) {
4✔
2276
            return $new;
×
2277
        }
2278

2279
        $new->cache_control = $cache_control;
4✔
2280

2281
        return $new;
4✔
2282
    }
2283

2284
    /**
2285
     * @param string $charset
2286
     *                        <p>e.g. "UTF-8"</p>
2287
     *
2288
     * @return static
2289
     */
2290
    public function withContentCharset(string $charset): self
2291
    {
2292
        $new = clone $this;
×
2293

2294
        if (empty($charset)) {
×
2295
            return $new;
×
2296
        }
2297

2298
        $new->content_charset = UTF8::normalize_encoding($charset);
×
2299

2300
        return $new;
×
2301
    }
2302

2303
    /**
2304
     * @param int $port
2305
     *
2306
     * @return static
2307
     */
2308
    public function withPort(int $port): self
2309
    {
2310
        $new = clone $this;
8✔
2311

2312
        $new->port = $port;
8✔
2313
        if ($new->uri) {
8✔
2314
            $new->uri = $new->uri->withPort($port);
8✔
2315
            $new->_updateHostFromUri();
8✔
2316
        }
2317

2318
        return $new;
8✔
2319
    }
2320

2321
    /**
2322
     * @param string $encoding
2323
     *
2324
     * @return static
2325
     */
2326
    public function withContentEncoding(string $encoding): self
2327
    {
2328
        $new = clone $this;
12✔
2329

2330
        $new->content_encoding = $encoding;
12✔
2331

2332
        return $new;
12✔
2333
    }
2334

2335
    /**
2336
     * @param string|null $mime     use a constant from Mime::*
2337
     * @param string|null $fallback use a constant from Mime::*
2338
     *
2339
     * @return static
2340
     */
2341
    public function withContentType($mime, string $fallback = null): self
2342
    {
2343
        $new = clone $this;
12✔
2344

2345
        return $new->_withContentType($mime, $fallback);
12✔
2346
    }
2347

2348
    /**
2349
     * @return static
2350
     */
2351
    public function withContentTypeCsv(): self
2352
    {
2353
        $new = clone $this;
×
2354
        $new->content_type = Mime::getFullMime(Mime::CSV);
×
2355

2356
        return $new;
×
2357
    }
2358

2359
    /**
2360
     * @return static
2361
     */
2362
    public function withContentTypeForm(): self
2363
    {
2364
        $new = clone $this;
4✔
2365
        $new->content_type = Mime::getFullMime(Mime::FORM);
4✔
2366

2367
        return $new;
4✔
2368
    }
2369

2370
    /**
2371
     * @return static
2372
     */
2373
    public function withContentTypeHtml(): self
2374
    {
2375
        $new = clone $this;
×
2376
        $new->content_type = Mime::getFullMime(Mime::HTML);
×
2377

2378
        return $new;
×
2379
    }
2380

2381
    /**
2382
     * @return static
2383
     */
2384
    public function withContentTypeJson(): self
2385
    {
2386
        $new = clone $this;
×
2387
        $new->content_type = Mime::getFullMime(Mime::JSON);
×
2388

2389
        return $new;
×
2390
    }
2391

2392
    /**
2393
     * @return static
2394
     */
2395
    public function withContentTypePlain(): self
2396
    {
2397
        $new = clone $this;
×
2398
        $new->content_type = Mime::getFullMime(Mime::PLAIN);
×
2399

2400
        return $new;
×
2401
    }
2402

2403
    /**
2404
     * @return static
2405
     */
2406
    public function withContentTypeXml(): self
2407
    {
2408
        $new = clone $this;
×
2409
        $new->content_type = Mime::getFullMime(Mime::XML);
×
2410

2411
        return $new;
×
2412
    }
2413

2414
    /**
2415
     * @return static
2416
     */
2417
    public function withContentTypeYaml(): self
2418
    {
2419
        return $this->withContentType(Mime::YAML);
×
2420
    }
2421

2422
    /**
2423
     * @param string $name
2424
     * @param string $value
2425
     *
2426
     * @return static
2427
     */
2428
    public function withCookie(string $name, string $value): self
2429
    {
2430
        return $this->withHeader('Cookie', "{$name}={$value}");
×
2431
    }
2432

2433
    /**
2434
     * Semi-reluctantly added this as a way to add in curl opts
2435
     * that are not otherwise accessible from the rest of the API.
2436
     *
2437
     * @param int   $curl_opt
2438
     * @param mixed $curl_opt_val
2439
     *
2440
     * @return static
2441
     */
2442
    public function withCurlOption($curl_opt, $curl_opt_val): self
2443
    {
2444
        $new = clone $this;
12✔
2445

2446
        $new->additional_curl_opts[$curl_opt] = $curl_opt_val;
12✔
2447

2448
        return $new;
12✔
2449
    }
2450

2451
    /**
2452
     * User Digest Auth.
2453
     *
2454
     * @param string $username
2455
     * @param string $password
2456
     *
2457
     * @return static
2458
     */
2459
    public function withDigestAuth($username, $password): self
2460
    {
2461
        $new = clone $this;
8✔
2462

2463
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_DIGEST);
8✔
2464

2465
        return $new->withBasicAuth($username, $password);
8✔
2466
    }
2467

2468
    /**
2469
     * Callback called to handle HTTP errors. When nothing is set, defaults
2470
     * to logging via `error_log`.
2471
     *
2472
     * @param callable|LoggerInterface|null $error_handler
2473
     *
2474
     * @return static
2475
     */
2476
    public function withErrorHandler($error_handler): self
2477
    {
2478
        $new = clone $this;
12✔
2479

2480
        $new->error_handler = $error_handler;
12✔
2481

2482
        return $new;
12✔
2483
    }
2484

2485
    /**
2486
     * @param string|null $mime     use a constant from Mime::*
2487
     * @param string|null $fallback use a constant from Mime::*
2488
     *
2489
     * @return static
2490
     */
2491
    public function withExpectedType($mime, string $fallback = null): self
2492
    {
2493
        $new = clone $this;
20✔
2494

2495
        return $new->_withExpectedType($mime, $fallback);
20✔
2496
    }
2497

2498
    /**
2499
     * @param string[]|string[][] $header
2500
     *
2501
     * @return static
2502
     */
2503
    public function withHeaders(array $header): self
2504
    {
2505
        $new = clone $this;
12✔
2506

2507
        foreach ($header as $name => $value) {
12✔
2508
            $new = $new->withAddedHeader($name, $value);
12✔
2509
        }
2510

2511
        return $new;
12✔
2512
    }
2513

2514
    /**
2515
     * Helper function to set the Content type and Expected as same in one swoop.
2516
     *
2517
     * @param string|null $mime
2518
     *                          <p>\Httpful\Mime::JSON, \Httpful\Mime::XML, ...</p>
2519
     *
2520
     * @return static
2521
     */
2522
    public function withMimeType($mime): self
2523
    {
2524
        $new = clone $this;
140✔
2525

2526
        return $new->_withMimeType($mime);
140✔
2527
    }
2528

2529
    /**
2530
     * @param string $username
2531
     * @param string $password
2532
     *
2533
     * @return static
2534
     */
2535
    public function withNtlmAuth($username, $password): self
2536
    {
2537
        $new = clone $this;
×
2538

2539
        $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_NTLM);
×
2540

2541
        return $new->withBasicAuth($username, $password);
×
2542
    }
2543

2544
    /**
2545
     * Add additional parameter to be appended to the query string.
2546
     *
2547
     * @param int|string|null $key
2548
     * @param int|string|null $value
2549
     *
2550
     * @return static
2551
     */
2552
    public function withParam($key, $value): self
2553
    {
2554
        $new = clone $this;
4✔
2555

2556
        if (
2557
            isset($key, $value)
4✔
2558
            &&
2559
            $key !== ''
4✔
2560
        ) {
2561
            $new->params[$key] = $value;
4✔
2562
        }
2563

2564
        return $new;
4✔
2565
    }
2566

2567
    /**
2568
     * Add additional parameters to be appended to the query string.
2569
     *
2570
     * Takes an associative array of key/value pairs as an argument.
2571
     *
2572
     * @param array $params
2573
     *
2574
     * @return static this
2575
     */
2576
    public function withParams(array $params): self
2577
    {
2578
        $new = clone $this;
×
2579

2580
        $new->params = \array_merge($new->params, $params);
×
2581

2582
        return $new;
×
2583
    }
2584

2585
    /**
2586
     * Use a custom function to parse the response.
2587
     *
2588
     * @param callable $callback Takes the raw body of
2589
     *                           the http response and returns a mixed
2590
     *
2591
     * @return static
2592
     */
2593
    public function withParseCallback(callable $callback): self
2594
    {
2595
        $new = clone $this;
×
2596

2597
        $new->parse_callback = $callback;
×
2598

2599
        return $new;
×
2600
    }
2601

2602
    /**
2603
     * Use proxy configuration
2604
     *
2605
     * @param string   $proxy_host    Hostname or address of the proxy
2606
     * @param int      $proxy_port    Port of the proxy. Default 80
2607
     * @param int|null $auth_type     Authentication type or null. Accepted values are CURLAUTH_BASIC, CURLAUTH_NTLM.
2608
     *                                Default null, no authentication
2609
     * @param string   $auth_username Authentication username. Default null
2610
     * @param string   $auth_password Authentication password. Default null
2611
     * @param int      $proxy_type    Proxy-Type for Curl. Default is "Proxy::HTTP"
2612
     *
2613
     * @return static
2614
     */
2615
    public function withProxy(
2616
        $proxy_host,
2617
        $proxy_port = 80,
2618
        $auth_type = null,
2619
        $auth_username = null,
2620
        $auth_password = null,
2621
        $proxy_type = Proxy::HTTP
2622
    ): self {
2623
        $new = clone $this;
4✔
2624

2625
        $new = $new->withCurlOption(\CURLOPT_PROXY, "{$proxy_host}:{$proxy_port}");
4✔
2626
        $new = $new->withCurlOption(\CURLOPT_PROXYTYPE, $proxy_type);
4✔
2627

2628
        if (\in_array($auth_type, [\CURLAUTH_BASIC, \CURLAUTH_NTLM], true)) {
4✔
2629
            $new = $new->withCurlOption(\CURLOPT_PROXYAUTH, $auth_type);
×
2630
            $new = $new->withCurlOption(\CURLOPT_PROXYUSERPWD, "{$auth_username}:{$auth_password}");
×
2631
        }
2632

2633
        return $new;
4✔
2634
    }
2635

2636
    /**
2637
     * @param string|null $key
2638
     * @param mixed|null  $fallback
2639
     *
2640
     * @return mixed
2641
     */
2642
    public function getHelperData($key = null, $fallback = null)
2643
    {
2644
        if ($key !== null) {
4✔
2645
            return $this->helperData[$key] ?? $fallback;
4✔
2646
        }
2647

2648
        return $this->helperData;
×
2649
    }
2650

2651
    /**
2652
     * @return void
2653
     */
2654
    public function clearHelperData()
2655
    {
2656
        $this->helperData = [];
24✔
2657
    }
6✔
2658

2659
    /**
2660
     * @param string $key
2661
     * @param mixed  $value
2662
     *
2663
     * @return static
2664
     */
2665
    public function addHelperData(string $key, $value): self
2666
    {
2667
        $this->helperData[$key] = $value;
4✔
2668

2669
        return $this;
4✔
2670
    }
2671

2672
    /**
2673
     * @param callable|null $send_callback
2674
     *
2675
     * @return static
2676
     */
2677
    public function withSendCallback($send_callback): self
2678
    {
2679
        $new = clone $this;
×
2680

2681
        if (!empty($send_callback)) {
×
2682
            $new->send_callbacks[] = $send_callback;
×
2683
        }
2684

2685
        return $new;
×
2686
    }
2687

2688
    /**
2689
     * @param callable $callback
2690
     *
2691
     * @return static
2692
     */
2693
    public function withSerializePayload(callable $callback): self
2694
    {
2695
        return $this->registerPayloadSerializer('*', $callback);
×
2696
    }
2697

2698
    /**
2699
     * @param string $file_path
2700
     *
2701
     * @return Request
2702
     */
2703
    public function withDownload($file_path): self
2704
    {
2705
        $new = clone $this;
4✔
2706

2707
        $new->file_path_for_download = $file_path;
4✔
2708

2709
        return $new;
4✔
2710
    }
2711

2712
    /**
2713
     * @param string $uri
2714
     * @param bool   $useClone
2715
     *
2716
     * @return static
2717
     */
2718
    public function withUriFromString(string $uri, bool $useClone = true): self
2719
    {
2720
        if ($useClone) {
304✔
2721
            $new = clone $this;
304✔
2722

2723
            return $new->withUri(new Uri($uri));
304✔
2724
        }
2725

2726
        return $this->_withUri(new Uri($uri));
4✔
2727
    }
2728

2729
    /**
2730
     * Sets user agent.
2731
     *
2732
     * @param string $userAgent
2733
     *
2734
     * @return static
2735
     */
2736
    public function withUserAgent($userAgent): self
2737
    {
2738
        return $this->withHeader('User-Agent', $userAgent);
4✔
2739
    }
2740

2741
    /**
2742
     * Takes a curl result and generates a Response from it.
2743
     *
2744
     * @param false|mixed $result
2745
     * @param Curl|null   $curl
2746
     *
2747
     * @throws NetworkErrorException
2748
     *
2749
     * @return Response
2750
     *
2751
     * @internal
2752
     */
2753
    public function _buildResponse($result, Curl $curl = null): Response
2754
    {
2755
        // fallback
2756
        if ($curl === null) {
148✔
2757
            $curl = $this->curl;
128✔
2758
        }
2759

2760
        if ($curl === null) {
148✔
2761
            throw new NetworkErrorException('Unable to build the response for "' . $this->uri . '". => "curl" === null');
×
2762
        }
2763

2764
        if ($result === false) {
148✔
2765
            $curlErrorNumber = $curl->getErrorCode();
28✔
2766
            if ($curlErrorNumber) {
28✔
2767
                $curlErrorString = (string) $curl->getErrorMessage();
28✔
2768

2769
                $this->_error($curlErrorString);
28✔
2770

2771
                $exception = new NetworkErrorException(
28✔
2772
                    'Unable to connect to "' . $this->uri . '": ' . $curlErrorNumber . ' ' . $curlErrorString,
28✔
2773
                    $curlErrorNumber,
28✔
2774
                    null,
28✔
2775
                    $curl,
28✔
2776
                    $this
28✔
2777
                );
21✔
2778

2779
                $exception->setCurlErrorNumber($curlErrorNumber)->setCurlErrorString($curlErrorString);
28✔
2780

2781
                throw $exception;
28✔
2782
            }
2783

2784
            $this->_error('Unable to connect to "' . $this->uri . '".');
×
2785

2786
            throw new NetworkErrorException('Unable to connect to "' . $this->uri . '".');
×
2787
        }
2788

2789
        $curl_info = $curl->getInfo();
120✔
2790

2791
        $headers = $curl->getRawResponseHeaders();
120✔
2792
        $rawResponse = $curl->getRawResponse();
120✔
2793

2794
        if ($rawResponse === false) {
120✔
2795
            $body = '';
×
2796
        } elseif ($rawResponse === true && $this->file_path_for_download && \is_string($this->file_path_for_download)) {
120✔
2797
            $body = \file_get_contents($this->file_path_for_download);
4✔
2798
            if ($body === false) {
4✔
2799
                throw new \ErrorException('file_get_contents return false for: ' . $this->file_path_for_download);
4✔
2800
            }
2801
        } else {
2802
            $body = UTF8::remove_left(
116✔
2803
                (string) $rawResponse,
116✔
2804
                $headers
116✔
2805
            );
87✔
2806
        }
2807

2808
        // get the protocol + version
2809
        $protocol_version_regex = "/HTTP\/(?<version>[\d\.]*+)/i";
120✔
2810
        $protocol_version_matches = [];
120✔
2811
        $protocol_version = null;
120✔
2812
        \preg_match($protocol_version_regex, $headers, $protocol_version_matches);
120✔
2813
        if (isset($protocol_version_matches['version'])) {
120✔
2814
            $protocol_version = $protocol_version_matches['version'];
120✔
2815
        }
2816
        $curl_info['protocol_version'] = $protocol_version;
120✔
2817

2818
        // DEBUG
2819
        //var_dump($body, $headers);
2820

2821
        return new Response(
120✔
2822
            $body,
120✔
2823
            $headers,
120✔
2824
            $this,
120✔
2825
            $curl_info
120✔
2826
        );
90✔
2827
    }
2828

2829
    /**
2830
     * @param bool $auto_parse perform automatic "smart"
2831
     *                         parsing based on Content-Type or "expectedType"
2832
     *                         If not auto parsing, Response->body returns the body
2833
     *                         as a string
2834
     *
2835
     * @return static
2836
     */
2837
    private function _autoParse(bool $auto_parse = true): self
2838
    {
2839
        $new = clone $this;
8✔
2840

2841
        $new->auto_parse = $auto_parse;
8✔
2842

2843
        return $new;
8✔
2844
    }
2845

2846
    /**
2847
     * @param string|null $str payload
2848
     *
2849
     * @return int length of payload in bytes
2850
     */
2851
    private function _determineLength($str): int
2852
    {
2853
        if ($str === null) {
40✔
2854
            return 0;
×
2855
        }
2856

2857
        return \strlen($str);
40✔
2858
    }
2859

2860
    /**
2861
     * @param string $error
2862
     *
2863
     * @return void
2864
     */
2865
    private function _error($error)
2866
    {
2867
        // global error handling
2868

2869
        $global_error_handler = Setup::getGlobalErrorHandler();
28✔
2870
        if ($global_error_handler) {
28✔
2871
            if ($global_error_handler instanceof LoggerInterface) {
×
2872
                // PSR-3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
2873
                $global_error_handler->error($error);
×
2874
            } elseif (\is_callable($global_error_handler)) {
×
2875
                // error callback
2876
                /** @noinspection VariableFunctionsUsageInspection */
2877
                \call_user_func($global_error_handler, $error);
×
2878
            }
2879
        }
2880

2881
        // local error handling
2882

2883
        if (isset($this->error_handler)) {
28✔
2884
            if ($this->error_handler instanceof LoggerInterface) {
12✔
2885
                // PSR-3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
2886
                $this->error_handler->error($error);
×
2887
            } elseif (\is_callable($this->error_handler)) {
12✔
2888
                // error callback
2889
                \call_user_func($this->error_handler, $error);
12✔
2890
            }
2891
        } else {
2892
            /** @noinspection ForgottenDebugOutputInspection */
2893
            \error_log($error);
16✔
2894
        }
2895
    }
7✔
2896

2897
    /**
2898
     * Turn payload from structured data into a string based on the current Mime type.
2899
     * This uses the auto_serialize option to determine it's course of action.
2900
     *
2901
     * See serialize method for more.
2902
     *
2903
     * Added in support for custom payload serializers.
2904
     * The serialize_payload_method stuff still holds true though.
2905
     *
2906
     * @param array|string $payload
2907
     *
2908
     * @return mixed
2909
     *
2910
     * @see Request::registerPayloadSerializer()
2911
     */
2912
    private function _serializePayload($payload)
2913
    {
2914
        if (empty($payload)) {
40✔
2915
            return '';
×
2916
        }
2917

2918
        if ($this->serialize_payload_method === static::SERIALIZE_PAYLOAD_NEVER) {
40✔
2919
            return $payload;
×
2920
        }
2921

2922
        // When we are in "smart" mode, don't serialize strings/scalars, assume they are already serialized.
2923
        if (
2924
            $this->serialize_payload_method === static::SERIALIZE_PAYLOAD_SMART
40✔
2925
            &&
2926
            \is_array($payload)
40✔
2927
            &&
2928
            \count($payload) === 1
40✔
2929
            &&
2930
            \array_keys($payload)[0] === 0
40✔
2931
            &&
2932
            \is_scalar($payload_first = \array_values($payload)[0])
40✔
2933
        ) {
2934
            return $payload_first;
16✔
2935
        }
2936

2937
        // Use a custom serializer if one is registered for this mime type.
2938
        $issetContentType = isset($this->payload_serializers[$this->content_type]);
24✔
2939
        if (
2940
            $issetContentType
24✔
2941
            ||
2942
            isset($this->payload_serializers['*'])
24✔
2943
        ) {
2944
            if ($issetContentType) {
×
2945
                $key = $this->content_type;
×
2946
            } else {
2947
                $key = '*';
×
2948
            }
2949

2950
            return \call_user_func($this->payload_serializers[$key], $payload);
×
2951
        }
2952

2953
        return Setup::setupGlobalMimeType($this->content_type)->serialize($payload);
24✔
2954
    }
2955

2956
    /**
2957
     * Set the body of the request.
2958
     *
2959
     * @param mixed|null  $payload
2960
     * @param mixed|null  $key
2961
     * @param string|null $mimeType currently, sets the sends AND expects mime type although this
2962
     *                              behavior may change in the next minor release (as it is a potential breaking change)
2963
     *
2964
     * @return static
2965
     */
2966
    private function _setBody($payload, $key = null, string $mimeType = null): self
2967
    {
2968
        $this->_withMimeType($mimeType);
84✔
2969

2970
        if (!empty($payload)) {
84✔
2971
            if (\is_array($payload)) {
52✔
2972
                foreach ($payload as $keyInner => $valueInner) {
20✔
2973
                    $this->_setBody($valueInner, $keyInner, $mimeType);
20✔
2974
                }
2975

2976
                return $this;
20✔
2977
            }
2978

2979
            if ($payload instanceof StreamInterface) {
52✔
2980
                $this->payload = (string) $payload;
8✔
2981
            } elseif ($key === null) {
44✔
2982
                if (\is_string($this->payload)) {
24✔
2983
                    $tmpPayload = $this->payload;
×
2984
                    $this->payload = [];
×
2985
                    $this->payload[] = $tmpPayload;
×
2986
                }
2987

2988
                $this->payload[] = $payload;
24✔
2989
            } else {
2990
                if (\is_string($this->payload)) {
20✔
2991
                    $tmpPayload = $this->payload;
×
2992
                    $this->payload = [];
×
2993
                    $this->payload[] = $tmpPayload;
×
2994
                }
2995

2996
                $this->payload[$key] = $payload;
20✔
2997
            }
2998
        }
2999

3000
        // Don't call _serializePayload yet.
3001
        // Wait until we actually send off the request to convert payload to string.
3002
        // At that time, the `serialized_payload` is set accordingly.
3003

3004
        return $this;
84✔
3005
    }
3006

3007
    /**
3008
     * Set the defaults on a newly instantiated object
3009
     * Doesn't copy variables prefixed with _
3010
     *
3011
     * @return static
3012
     */
3013
    private function _setDefaultsFromTemplate(): self
3014
    {
3015
        if ($this->template !== null) {
460✔
3016
            if (\function_exists('gzdecode')) {
460✔
3017
                $this->template->content_encoding = 'gzip';
460✔
3018
            } elseif (\function_exists('gzinflate')) {
×
3019
                $this->template->content_encoding = 'deflate';
×
3020
            }
3021

3022
            foreach ($this->template as $k => $v) {
460✔
3023
                if ($k[0] !== '_') {
460✔
3024
                    $this->{$k} = $v;
460✔
3025
                }
3026
            }
3027
        }
3028

3029
        return $this;
460✔
3030
    }
3031

3032
    /**
3033
     * Set the method.  Shouldn't be called often as the preferred syntax
3034
     * for instantiation is the method specific factory methods.
3035
     *
3036
     * @param string|null $method
3037
     *
3038
     * @return static
3039
     */
3040
    private function _setMethod($method): self
3041
    {
3042
        if (empty($method)) {
460✔
3043
            return $this;
120✔
3044
        }
3045

3046
        if (!\in_array($method, Http::allMethods(), true)) {
460✔
3047
            throw new RequestException($this, 'Unknown HTTP method: \'' . \strip_tags($method) . '\'');
4✔
3048
        }
3049

3050
        $this->method = $method;
460✔
3051

3052
        return $this;
460✔
3053
    }
3054

3055
    /**
3056
     * Do we strictly enforce SSL verification?
3057
     *
3058
     * @param bool $strict
3059
     *
3060
     * @return static
3061
     */
3062
    private function _strictSSL($strict): self
3063
    {
3064
        $new = clone $this;
460✔
3065

3066
        $new->strict_ssl = $strict;
460✔
3067

3068
        return $new;
460✔
3069
    }
3070

3071
    /**
3072
     * @return void
3073
     */
3074
    private function _updateHostFromUri()
3075
    {
3076
        if ($this->uri === null) {
308✔
3077
            return;
×
3078
        }
3079

3080
        if ($this->uri_cache === \serialize($this->uri)) {
308✔
3081
            return;
4✔
3082
        }
3083

3084
        $host = $this->uri->getHost();
308✔
3085

3086
        if ($host === '') {
308✔
3087
            return;
60✔
3088
        }
3089

3090
        $port = $this->uri->getPort();
256✔
3091
        if ($port !== null) {
256✔
3092
            $host .= ':' . $port;
20✔
3093
        }
3094

3095
        // Ensure Host is the first header.
3096
        // See: http://tools.ietf.org/html/rfc7230#section-5.4
3097
        $this->headers = new Headers(['Host' => [$host]] + $this->withoutHeader('Host')->getHeaders());
256✔
3098

3099
        $this->uri_cache = \serialize($this->uri);
256✔
3100
    }
64✔
3101

3102
    /**
3103
     * @param string|null $mime     use a constant from Mime::*
3104
     * @param string|null $fallback use a constant from Mime::*
3105
     *
3106
     * @return static
3107
     */
3108
    private function _withContentType($mime, string $fallback = null): self
3109
    {
3110
        if (empty($mime) && empty($fallback)) {
460✔
3111
            return $this;
×
3112
        }
3113

3114
        if (empty($mime)) {
460✔
3115
            $mime = $fallback;
460✔
3116
        }
3117

3118
        if (empty($mime)) {
460✔
3119
            return $this;
×
3120
        }
3121

3122
        $this->content_type = Mime::getFullMime($mime);
460✔
3123

3124
        if ($this->isUpload()) {
460✔
3125
            $this->neverSerializePayload();
12✔
3126
        }
3127

3128
        return $this;
460✔
3129
    }
3130

3131
    /**
3132
     * @param string|null $mime     use a constant from Mime::*
3133
     * @param string|null $fallback use a constant from Mime::*
3134
     *
3135
     * @return static
3136
     */
3137
    private function _withExpectedType($mime, string $fallback = null): self
3138
    {
3139
        if (empty($mime) && empty($fallback)) {
460✔
3140
            return $this;
×
3141
        }
3142

3143
        if (empty($mime)) {
460✔
3144
            $mime = $fallback;
460✔
3145
        }
3146

3147
        if (empty($mime)) {
460✔
3148
            return $this;
×
3149
        }
3150

3151
        $this->expected_type = Mime::getFullMime($mime);
460✔
3152

3153
        return $this;
460✔
3154
    }
3155

3156
    /**
3157
     * Helper function to set the Content type and Expected as same in one swoop.
3158
     *
3159
     * @param string|null $mime mime type to use for content type and expected return type
3160
     *
3161
     * @return static
3162
     */
3163
    private function _withMimeType($mime): self
3164
    {
3165
        if (empty($mime)) {
212✔
3166
            return $this;
116✔
3167
        }
3168

3169
        $this->expected_type = Mime::getFullMime($mime);
104✔
3170
        $this->content_type = $this->expected_type;
104✔
3171

3172
        if ($this->isUpload()) {
104✔
3173
            $this->neverSerializePayload();
×
3174
        }
3175

3176
        return $this;
104✔
3177
    }
3178

3179
    /**
3180
     * @param UriInterface $uri
3181
     * @param bool         $preserveHost
3182
     *
3183
     * @return static
3184
     */
3185
    private function _withUri(UriInterface $uri, $preserveHost = false): self
3186
    {
3187
        if ($this->uri === $uri) {
308✔
3188
            return $this;
8✔
3189
        }
3190

3191
        $this->uri = $uri;
308✔
3192

3193
        if (!$preserveHost) {
308✔
3194
            $this->_updateHostFromUri();
308✔
3195
        }
3196

3197
        return $this;
308✔
3198
    }
3199
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc