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

voku / httpful / 5623107679

pending completion
5623107679

push

github

voku
[+]: fix test for the new release

1596 of 2486 relevant lines covered (64.2%)

81.28 hits per line

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

67.52
/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\MessageInterface;
13
use Psr\Http\Message\RequestInterface;
14
use Psr\Http\Message\StreamInterface;
15
use Psr\Http\Message\UriInterface;
16
use Psr\Log\LoggerInterface;
17
use voku\helper\UTF8;
18

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

23
    const SERIALIZE_PAYLOAD_ALWAYS = 1;
24

25
    const SERIALIZE_PAYLOAD_NEVER = 0;
26

27
    const SERIALIZE_PAYLOAD_SMART = 2;
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

525
                break;
×
526
        }
527

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

535
        return $this;
184✔
536
    }
537

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

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

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

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

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

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

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

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

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

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

618
        return $this;
4✔
619
    }
620

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

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

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

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

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

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

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

658
        return $user_agent;
180✔
659
    }
660

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

678
        return $this;
×
679
    }
680

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

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

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

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

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

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

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

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

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

770
        return $this;
×
771
    }
772

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

780
        return $this;
×
781
    }
782

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

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

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

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

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

830
        return $this;
×
831
    }
832

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

840
        return $this;
×
841
    }
842

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

971
        return $new;
52✔
972
    }
973

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

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

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

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

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

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

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

1045
            return $value;
44✔
1046
        }
1047

1048
        return [];
4✔
1049
    }
1050

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

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

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

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

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

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

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

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

1138
        return $target;
20✔
1139
    }
1140

1141
    /**
1142
     * @return null|Uri|UriInterface
1143
     */
1144
    public function getUriOrNull(): ?UriInterface
1145
    {
1146
        return $this->uri;
4✔
1147
    }
1148

1149
    /**
1150
     * @return Uri|UriInterface
1151
     */
1152
    public function getUri(): UriInterface
1153
    {
1154
        if ($this->uri === null) {
20✔
1155
            throw new RequestException($this, 'URI is not set.');
×
1156
        }
1157

1158
        return $this->uri;
20✔
1159
    }
1160

1161
    /**
1162
     * Checks if a header exists by the given case-insensitive name.
1163
     *
1164
     * @param string $name case-insensitive header field name
1165
     *
1166
     * @return bool Returns true if any header names match the given header
1167
     *              name using a case-insensitive string comparison. Returns false if
1168
     *              no matching header name is found in the message.
1169
     */
1170
    public function hasHeader($name): bool
1171
    {
1172
        return $this->headers->offsetExists($name);
×
1173
    }
1174

1175
    /**
1176
     * Return an instance with the specified header appended with the given value.
1177
     *
1178
     * Existing values for the specified header will be maintained. The new
1179
     * value(s) will be appended to the existing list. If the header did not
1180
     * exist previously, it will be added.
1181
     *
1182
     * This method MUST be implemented in such a way as to retain the
1183
     * immutability of the message, and MUST return an instance that has the
1184
     * new header and/or value.
1185
     *
1186
     * @param string          $name  case-insensitive header field name to add
1187
     * @param string|string[] $value header value(s)
1188
     *
1189
     * @throws \InvalidArgumentException for invalid header names or values
1190
     *
1191
     * @return static
1192
     */
1193
    public function withAddedHeader($name, $value): MessageInterface
1194
    {
1195
        if (!\is_string($name) || $name === '') {
24✔
1196
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
×
1197
        }
1198

1199
        $new = clone $this;
24✔
1200

1201
        if (!\is_array($value)) {
24✔
1202
            $value = [$value];
24✔
1203
        }
1204

1205
        if ($new->headers->offsetExists($name)) {
24✔
1206
            $new->headers->forceSet($name, \array_merge_recursive($new->headers->offsetGet($name), $value));
8✔
1207
        } else {
1208
            $new->headers->forceSet($name, $value);
24✔
1209
        }
1210

1211
        return $new;
24✔
1212
    }
1213

1214
    /**
1215
     * Return an instance with the specified message body.
1216
     *
1217
     * The body MUST be a StreamInterface object.
1218
     *
1219
     * This method MUST be implemented in such a way as to retain the
1220
     * immutability of the message, and MUST return a new instance that has the
1221
     * new body stream.
1222
     *
1223
     * @param StreamInterface $body
1224
     *
1225
     * @throws \InvalidArgumentException when the body is not valid
1226
     *
1227
     * @return static
1228
     */
1229
    public function withBody(StreamInterface $body): MessageInterface
1230
    {
1231
        $stream = Http::stream($body);
8✔
1232

1233
        return (clone $this)->_setBody($stream, null);
8✔
1234
    }
1235

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

1257
        if (!\is_array($value)) {
40✔
1258
            $value = [$value];
36✔
1259
        }
1260

1261
        $new->headers->forceSet($name, $value);
40✔
1262

1263
        return $new;
36✔
1264
    }
1265

1266
    /**
1267
     * Return an instance with the provided HTTP method.
1268
     *
1269
     * While HTTP method names are typically all uppercase characters, HTTP
1270
     * method names are case-sensitive and thus implementations SHOULD NOT
1271
     * modify the given string.
1272
     *
1273
     * This method MUST be implemented in such a way as to retain the
1274
     * immutability of the message, and MUST return an instance that has the
1275
     * changed request method.
1276
     *
1277
     * @param string $method
1278
     *                       <p>\Httpful\Http::GET, \Httpful\Http::POST, ...</p>
1279
     *
1280
     * @throws \InvalidArgumentException for invalid HTTP methods
1281
     *
1282
     * @return static
1283
     */
1284
    public function withMethod($method): RequestInterface
1285
    {
1286
        $new = clone $this;
4✔
1287

1288
        $new->_setMethod($method);
4✔
1289

1290
        return $new;
4✔
1291
    }
1292

1293
    /**
1294
     * Return an instance with the specified HTTP protocol version.
1295
     *
1296
     * The version string MUST contain only the HTTP version number (e.g.,
1297
     * "2, 1.1", "1.0").
1298
     *
1299
     * This method MUST be implemented in such a way as to retain the
1300
     * immutability of the message, and MUST return an instance that has the
1301
     * new protocol version.
1302
     *
1303
     * @param string $version
1304
     *                        <p>Http::HTTP_*</p>
1305
     *
1306
     * @return static
1307
     */
1308
    public function withProtocolVersion($version): MessageInterface
1309
    {
1310
        $new = clone $this;
4✔
1311

1312
        $new->protocol_version = $version;
4✔
1313

1314
        return $new;
4✔
1315
    }
1316

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

1342
        $new = clone $this;
4✔
1343

1344
        if ($new->uri !== null) {
4✔
1345
            $new->_withUri($new->uri->withPath($requestTarget));
4✔
1346
        }
1347

1348
        return $new;
4✔
1349
    }
1350

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

1388
    /**
1389
     * Return an instance without the specified header.
1390
     *
1391
     * Header resolution MUST be done without case-sensitivity.
1392
     *
1393
     * This method MUST be implemented in such a way as to retain the
1394
     * immutability of the message, and MUST return an instance that removes
1395
     * the named header.
1396
     *
1397
     * @param string $name case-insensitive header field name to remove
1398
     *
1399
     * @return static
1400
     */
1401
    public function withoutHeader($name): self
1402
    {
1403
        $new = clone $this;
256✔
1404

1405
        $new->headers->forceUnset($name);
256✔
1406

1407
        return $new;
256✔
1408
    }
1409

1410
    /**
1411
     * @return string
1412
     */
1413
    public function getContentType(): string
1414
    {
1415
        return $this->content_type;
16✔
1416
    }
1417

1418
    /**
1419
     * @return callable|LoggerInterface|null
1420
     */
1421
    public function getErrorHandler()
1422
    {
1423
        return $this->error_handler;
×
1424
    }
1425

1426
    /**
1427
     * @return string
1428
     */
1429
    public function getExpectedType(): string
1430
    {
1431
        return $this->expected_type;
200✔
1432
    }
1433

1434
    /**
1435
     * @return string
1436
     */
1437
    public function getHttpMethod(): string
1438
    {
1439
        return $this->method;
12✔
1440
    }
1441

1442
    /**
1443
     * @return \ArrayObject
1444
     */
1445
    public function getIterator(): \ArrayObject
1446
    {
1447
        // init
1448
        $elements = new \ArrayObject();
460✔
1449

1450
        foreach (\get_object_vars($this) as $f => $v) {
460✔
1451
            $elements[$f] = $v;
460✔
1452
        }
1453

1454
        return $elements;
460✔
1455
    }
1456

1457
    /**
1458
     * @return callable|null
1459
     */
1460
    public function getParseCallback()
1461
    {
1462
        return $this->parse_callback;
×
1463
    }
1464

1465
    /**
1466
     * @return array
1467
     */
1468
    public function getPayload(): array
1469
    {
1470
        return \is_string($this->payload) ? [$this->payload] : $this->payload;
4✔
1471
    }
1472

1473
    /**
1474
     * @return string
1475
     */
1476
    public function getRawHeaders(): string
1477
    {
1478
        return $this->raw_headers;
20✔
1479
    }
1480

1481
    /**
1482
     * @return callable[]
1483
     */
1484
    public function getSendCallback(): array
1485
    {
1486
        return $this->send_callbacks;
×
1487
    }
1488

1489
    /**
1490
     * @return int
1491
     */
1492
    public function getSerializePayloadMethod(): int
1493
    {
1494
        return $this->serialize_payload_method;
4✔
1495
    }
1496

1497
    /**
1498
     * @return mixed|null
1499
     */
1500
    public function getSerializedPayload()
1501
    {
1502
        return $this->serialized_payload;
12✔
1503
    }
1504

1505
    /**
1506
     * @return string
1507
     */
1508
    public function getUriString(): string
1509
    {
1510
        return (string) $this->uri;
8✔
1511
    }
1512

1513
    /**
1514
     * Is this request setup for basic auth?
1515
     *
1516
     * @return bool
1517
     */
1518
    public function hasBasicAuth(): bool
1519
    {
1520
        return $this->password && $this->username;
188✔
1521
    }
1522

1523
    /**
1524
     * @return bool has the internal curl (non-multi) request been initialized?
1525
     */
1526
    public function hasBeenInitialized(): bool
1527
    {
1528
        if (!$this->curl) {
184✔
1529
            return false;
×
1530
        }
1531

1532
        return \is_resource($this->curl->getCurl());
184✔
1533
    }
1534

1535
    /**
1536
     * @return bool has the internal curl (multi) request been initialized?
1537
     */
1538
    public function hasBeenInitializedMulti(): bool
1539
    {
1540
        if (!$this->curlMulti) {
×
1541
            return false;
×
1542
        }
1543

1544
        return \is_resource($this->curlMulti->getMultiCurl());
×
1545
    }
1546

1547
    /**
1548
     * @return bool is this request setup for client side cert?
1549
     */
1550
    public function hasClientSideCert(): bool
1551
    {
1552
        return $this->ssl_cert && $this->ssl_key;
184✔
1553
    }
1554

1555
    /**
1556
     * @return bool does the request have a connection timeout?
1557
     */
1558
    public function hasConnectionTimeout(): bool
1559
    {
1560
        return isset($this->connection_timeout);
184✔
1561
    }
1562

1563
    /**
1564
     * Is this request setup for digest auth?
1565
     *
1566
     * @return bool
1567
     */
1568
    public function hasDigestAuth(): bool
1569
    {
1570
        return $this->password
4✔
1571
               &&
4✔
1572
               $this->username
4✔
1573
               &&
4✔
1574
               $this->additional_curl_opts[\CURLOPT_HTTPAUTH] === \CURLAUTH_DIGEST;
4✔
1575
    }
1576

1577
    /**
1578
     * @return bool
1579
     */
1580
    public function hasParseCallback(): bool
1581
    {
1582
        return isset($this->parse_callback)
184✔
1583
               &&
184✔
1584
               \is_callable($this->parse_callback);
184✔
1585
    }
1586

1587
    /**
1588
     * @return bool is this request setup for using proxy?
1589
     */
1590
    public function hasProxy(): bool
1591
    {
1592
        /**
1593
         *  We must be aware that proxy variables could come from environment also.
1594
         *  In curl extension, http proxy can be specified not only via CURLOPT_PROXY option,
1595
         *  but also by environment variable called http_proxy.
1596
         */
1597
        return (
12✔
1598
            isset($this->additional_curl_opts[\CURLOPT_PROXY])
12✔
1599
            && \is_string($this->additional_curl_opts[\CURLOPT_PROXY])
12✔
1600
        ) || \getenv('http_proxy');
12✔
1601
    }
1602

1603
    /**
1604
     * @return bool does the request have a timeout?
1605
     */
1606
    public function hasTimeout(): bool
1607
    {
1608
        return isset($this->timeout);
184✔
1609
    }
1610

1611
    /**
1612
     * HTTP Method Head
1613
     *
1614
     * @param string|UriInterface $uri
1615
     *
1616
     * @return static
1617
     */
1618
    public static function head($uri): self
1619
    {
1620
        if ($uri instanceof UriInterface) {
8✔
1621
            $uri = (string) $uri;
×
1622
        }
1623

1624
        return (new self(Http::HEAD))
8✔
1625
            ->withUriFromString($uri)
8✔
1626
            ->withMimeType(Mime::PLAIN);
8✔
1627
    }
1628

1629
    /**
1630
     * @see Request::close()
1631
     *
1632
     * @return void
1633
     */
1634
    public function initializeMulti()
1635
    {
1636
        if (!$this->curlMulti || $this->hasBeenInitializedMulti()) {
24✔
1637
            $this->curlMulti = new MultiCurl();
24✔
1638
        }
1639
    }
1640

1641
    /**
1642
     * @see Request::close()
1643
     *
1644
     * @return void
1645
     */
1646
    public function initialize()
1647
    {
1648
        if (!$this->curl || !$this->hasBeenInitialized()) {
460✔
1649
            $this->curl = new Curl();
460✔
1650
        }
1651
    }
1652

1653
    /**
1654
     * @return bool
1655
     */
1656
    public function isAutoParse(): bool
1657
    {
1658
        return $this->auto_parse;
184✔
1659
    }
1660

1661
    /**
1662
     * @return bool
1663
     */
1664
    public function isJson(): bool
1665
    {
1666
        return $this->content_type === Mime::JSON;
×
1667
    }
1668

1669
    /**
1670
     * @return bool
1671
     */
1672
    public function isStrictSSL(): bool
1673
    {
1674
        return $this->strict_ssl;
12✔
1675
    }
1676

1677
    /**
1678
     * @return bool
1679
     */
1680
    public function isUpload(): bool
1681
    {
1682
        return $this->content_type === Mime::UPLOAD;
460✔
1683
    }
1684

1685
    /**
1686
     * @return static
1687
     *
1688
     * @see Request::serializePayloadMode()
1689
     */
1690
    public function neverSerializePayload(): self
1691
    {
1692
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_NEVER);
12✔
1693
    }
1694

1695
    /**
1696
     * HTTP Method Options
1697
     *
1698
     * @param string|UriInterface $uri
1699
     *
1700
     * @return static
1701
     */
1702
    public static function options($uri): self
1703
    {
1704
        if ($uri instanceof UriInterface) {
4✔
1705
            $uri = (string) $uri;
×
1706
        }
1707

1708
        return (new self(Http::OPTIONS))->withUriFromString($uri);
4✔
1709
    }
1710

1711
    /**
1712
     * HTTP Method Patch
1713
     *
1714
     * @param string|UriInterface $uri
1715
     * @param mixed               $payload data to send in body of request
1716
     * @param string              $mime    MIME to use for Content-Type
1717
     *
1718
     * @return static
1719
     */
1720
    public static function patch($uri, $payload = null, string $mime = null): self
1721
    {
1722
        if ($uri instanceof UriInterface) {
4✔
1723
            $uri = (string) $uri;
×
1724
        }
1725

1726
        return (new self(Http::PATCH))
4✔
1727
            ->withUriFromString($uri)
4✔
1728
            ->_setBody($payload, null, $mime);
4✔
1729
    }
1730

1731
    /**
1732
     * HTTP Method Post
1733
     *
1734
     * @param string|UriInterface $uri
1735
     * @param mixed               $payload data to send in body of request
1736
     * @param string              $mime    MIME to use for Content-Type
1737
     *
1738
     * @return static
1739
     */
1740
    public static function post($uri, $payload = null, string $mime = null): self
1741
    {
1742
        if ($uri instanceof UriInterface) {
32✔
1743
            $uri = (string) $uri;
×
1744
        }
1745

1746
        return (new self(Http::POST))
32✔
1747
            ->withUriFromString($uri)
32✔
1748
            ->_setBody($payload, null, $mime);
32✔
1749
    }
1750

1751
    /**
1752
     * HTTP Method Put
1753
     *
1754
     * @param string|UriInterface $uri
1755
     * @param mixed               $payload data to send in body of request
1756
     * @param string              $mime    MIME to use for Content-Type
1757
     *
1758
     * @return static
1759
     */
1760
    public static function put($uri, $payload = null, string $mime = null): self
1761
    {
1762
        if ($uri instanceof UriInterface) {
8✔
1763
            $uri = (string) $uri;
×
1764
        }
1765

1766
        return (new self(Http::PUT))
8✔
1767
            ->withUriFromString($uri)
8✔
1768
            ->_setBody($payload, null, $mime);
8✔
1769
    }
1770

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

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

1790
        return $new;
×
1791
    }
1792

1793
    /**
1794
     * @return void
1795
     */
1796
    public function reset()
1797
    {
1798
        $this->headers = new Headers();
×
1799

1800
        $this->close();
×
1801
        $this->initialize();
×
1802
    }
1803

1804
    /**
1805
     * Actually send off the request, and parse the response.
1806
     *
1807
     * @param callable|null $onSuccessCallback
1808
     * @param callable|null $onCompleteCallback
1809
     * @param callable|null $onBeforeSendCallback
1810
     * @param callable|null $onErrorCallback
1811
     *
1812
     * @throws NetworkErrorException when unable to parse or communicate w server
1813
     *
1814
     * @return MultiCurl
1815
     */
1816
    public function initMulti(
1817
        $onSuccessCallback = null,
1818
        $onCompleteCallback = null,
1819
        $onBeforeSendCallback = null,
1820
        $onErrorCallback = null
1821
    ) {
1822
        $this->initializeMulti();
24✔
1823
        \assert($this->curlMulti instanceof MultiCurl);
1824

1825
        if ($onSuccessCallback !== null) {
24✔
1826
            $this->curlMulti->success(
12✔
1827
                static function (Curl $instance) use ($onSuccessCallback) {
12✔
1828
                    if ($instance->request instanceof self) {
12✔
1829
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
12✔
1830
                    } else {
1831
                        $response = $instance->rawResponse;
×
1832
                    }
1833

1834
                    $onSuccessCallback(
12✔
1835
                        $response,
12✔
1836
                        $instance->request,
12✔
1837
                        $instance
12✔
1838
                    );
12✔
1839
                }
12✔
1840
            );
12✔
1841
        }
1842

1843
        if ($onCompleteCallback !== null) {
24✔
1844
            $this->curlMulti->complete(
×
1845
                static function (Curl $instance) use ($onCompleteCallback) {
×
1846
                    if ($instance->request instanceof self) {
×
1847
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1848
                    } else {
1849
                        $response = $instance->rawResponse;
×
1850
                    }
1851

1852
                    $onCompleteCallback(
×
1853
                        $response,
×
1854
                        $instance->request,
×
1855
                        $instance
×
1856
                    );
×
1857
                }
×
1858
            );
×
1859
        }
1860

1861
        if ($onBeforeSendCallback !== null) {
24✔
1862
            $this->curlMulti->beforeSend(
×
1863
                static function (Curl $instance) use ($onBeforeSendCallback) {
×
1864
                    if ($instance->request instanceof self) {
×
1865
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1866
                    } else {
1867
                        $response = $instance->rawResponse;
×
1868
                    }
1869

1870
                    $onBeforeSendCallback(
×
1871
                        $response,
×
1872
                        $instance->request,
×
1873
                        $instance
×
1874
                    );
×
1875
                }
×
1876
            );
×
1877
        }
1878

1879
        if ($onErrorCallback !== null) {
24✔
1880
            $this->curlMulti->error(
×
1881
                static function (Curl $instance) use ($onErrorCallback) {
×
1882
                    if ($instance->request instanceof self) {
×
1883
                        $response = $instance->request->_buildResponse($instance->rawResponse, $instance);
×
1884
                    } else {
1885
                        $response = $instance->rawResponse;
×
1886
                    }
1887

1888
                    $onErrorCallback(
×
1889
                        $response,
×
1890
                        $instance->request,
×
1891
                        $instance
×
1892
                    );
×
1893
                }
×
1894
            );
×
1895
        }
1896

1897
        return $this->curlMulti;
24✔
1898
    }
1899

1900
    /**
1901
     * Actually send off the request, and parse the response.
1902
     *
1903
     * @throws NetworkErrorException when unable to parse or communicate w server
1904
     *
1905
     * @return Response
1906
     */
1907
    public function send(): Response
1908
    {
1909
        $this->_curlPrep();
128✔
1910
        \assert($this->curl instanceof Curl);
1911

1912
        $result = $this->curl->exec();
128✔
1913

1914
        if (
1915
            $result === false
128✔
1916
            &&
1917
            $this->retry_by_possible_encoding_error
128✔
1918
        ) {
1919
            // Possibly a gzip issue makes curl unhappy.
1920
            if (
1921
                $this->curl->errorCode === \CURLE_WRITE_ERROR
×
1922
                ||
1923
                $this->curl->errorCode === \CURLE_BAD_CONTENT_ENCODING
×
1924
            ) {
1925
                // Docs say 'identity,' but 'none' seems to work (sometimes?).
1926
                $this->curl->setOpt(\CURLOPT_ENCODING, 'none');
×
1927

1928
                $result = $this->curl->exec();
×
1929

1930
                if ($result === false) {
×
1931
                    if (
1932
                        /* @phpstan-ignore-next-line | FP? */
1933
                        $this->curl->errorCode === \CURLE_WRITE_ERROR
×
1934
                        ||
1935
                        $this->curl->errorCode === \CURLE_BAD_CONTENT_ENCODING
×
1936
                    ) {
1937
                        $this->curl->setOpt(\CURLOPT_ENCODING, 'identity');
×
1938

1939
                        $result = $this->curl->exec();
×
1940
                    }
1941
                }
1942
            }
1943
        }
1944

1945
        if (!$this->keep_alive) {
128✔
1946
            $this->close();
×
1947
        }
1948

1949
        return $this->_buildResponse($result);
128✔
1950
    }
1951

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

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

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

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

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

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

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

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

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

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

2032
    /**
2033
     * @return static
2034
     */
2035
    public function sendsXml(): self
2036
    {
2037
        return $this->withContentType(Mime::XML);
×
2038
    }
2039

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

2067
        return $this;
12✔
2068
    }
2069

2070
    /**
2071
     * This method is the default behavior
2072
     *
2073
     * @return static
2074
     *
2075
     * @see Request::serializePayloadMode()
2076
     */
2077
    public function smartSerializePayload(): self
2078
    {
2079
        return $this->serializePayloadMode(static::SERIALIZE_PAYLOAD_SMART);
×
2080
    }
2081

2082
    /**
2083
     * Specify a HTTP timeout
2084
     *
2085
     * @param float|int $timeout seconds to timeout the HTTP call
2086
     *
2087
     * @return static
2088
     */
2089
    public function withTimeout($timeout): self
2090
    {
2091
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $timeout)) {
16✔
2092
            throw new \InvalidArgumentException(
×
2093
                'Invalid timeout provided: ' . \var_export($timeout, true)
×
2094
            );
×
2095
        }
2096

2097
        $new = clone $this;
16✔
2098

2099
        $new->timeout = $timeout;
16✔
2100

2101
        return $new;
16✔
2102
    }
2103

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

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

2165
    /**
2166
     * @param string $name
2167
     * @param string $value
2168
     *
2169
     * @return static
2170
     */
2171
    public function withAddedCookie(string $name, string $value): self
2172
    {
2173
        return $this->withAddedHeader('Cookie', "{$name}={$value}");
8✔
2174
    }
2175

2176
    /**
2177
     * @param array<string,string> $files
2178
     *
2179
     * @return static
2180
     */
2181
    public function withAttachment($files): self
2182
    {
2183
        $new = clone $this;
4✔
2184

2185
        $fInfo = \finfo_open(\FILEINFO_MIME_TYPE);
4✔
2186
        if ($fInfo === false) {
4✔
2187
            /** @noinspection ForgottenDebugOutputInspection */
2188
            \error_log('finfo_open() did not work', \E_USER_WARNING);
×
2189

2190
            return $new;
×
2191
        }
2192

2193
        foreach ($files as $key => $file) {
4✔
2194
            $mimeType = \finfo_file($fInfo, $file);
4✔
2195
            if ($mimeType !== false) {
4✔
2196
                if (\is_string($new->payload)) {
4✔
2197
                    $new->payload = []; // reset
×
2198
                }
2199
                $new->payload[$key] = \curl_file_create($file, $mimeType, \basename($file));
4✔
2200
            }
2201
        }
2202

2203
        \finfo_close($fInfo);
4✔
2204

2205
        return $new->_withContentType(Mime::UPLOAD);
4✔
2206
    }
2207

2208
    /**
2209
     * User Basic Auth.
2210
     *
2211
     * Only use when over SSL/TSL/HTTPS.
2212
     *
2213
     * @param string $username
2214
     * @param string $password
2215
     *
2216
     * @return static
2217
     */
2218
    public function withBasicAuth($username, $password): self
2219
    {
2220
        $new = clone $this;
28✔
2221
        $new->username = $username;
28✔
2222
        $new->password = $password;
28✔
2223

2224
        return $new;
28✔
2225
    }
2226

2227
    /**
2228
     * @param array $body
2229
     *
2230
     * @return static
2231
     */
2232
    public function withBodyFromArray(array $body)
2233
    {
2234
        return $this->_setBody($body, null);
4✔
2235
    }
2236

2237
    /**
2238
     * @param string $body
2239
     *
2240
     * @return static
2241
     */
2242
    public function withBodyFromString(string $body)
2243
    {
2244
        $stream = Http::stream($body);
36✔
2245

2246
        return $this->_setBody($stream->getContents(), null);
36✔
2247
    }
2248

2249
    /**
2250
     * Specify a HTTP connection timeout
2251
     *
2252
     * @param float|int $connection_timeout seconds to timeout the HTTP connection
2253
     *
2254
     * @throws \InvalidArgumentException
2255
     *
2256
     * @return static
2257
     */
2258
    public function withConnectionTimeoutInSeconds($connection_timeout): self
2259
    {
2260
        if (!\preg_match('/^\d+(\.\d+)?/', (string) $connection_timeout)) {
12✔
2261
            throw new \InvalidArgumentException(
×
2262
                'Invalid connection timeout provided: ' . \var_export($connection_timeout, true)
×
2263
            );
×
2264
        }
2265

2266
        $new = clone $this;
12✔
2267

2268
        $new->connection_timeout = $connection_timeout;
12✔
2269

2270
        return $new;
12✔
2271
    }
2272

2273
    /**
2274
     * @param string $cache_control
2275
     *                              <p>e.g. 'no-cache', 'public', ...</p>
2276
     *
2277
     * @return static
2278
     */
2279
    public function withCacheControl(string $cache_control): self
2280
    {
2281
        $new = clone $this;
4✔
2282

2283
        if (empty($cache_control)) {
4✔
2284
            return $new;
×
2285
        }
2286

2287
        $new->cache_control = $cache_control;
4✔
2288

2289
        return $new;
4✔
2290
    }
2291

2292
    /**
2293
     * @param string $charset
2294
     *                        <p>e.g. "UTF-8"</p>
2295
     *
2296
     * @return static
2297
     */
2298
    public function withContentCharset(string $charset): self
2299
    {
2300
        $new = clone $this;
×
2301

2302
        if (empty($charset)) {
×
2303
            return $new;
×
2304
        }
2305

2306
        $new->content_charset = UTF8::normalize_encoding($charset);
×
2307

2308
        return $new;
×
2309
    }
2310

2311
    /**
2312
     * @param int $port
2313
     *
2314
     * @return static
2315
     */
2316
    public function withPort(int $port): self
2317
    {
2318
        $new = clone $this;
8✔
2319

2320
        $new->port = $port;
8✔
2321
        if ($new->uri) {
8✔
2322
            $new->uri = $new->uri->withPort($port);
8✔
2323
            $new->_updateHostFromUri();
8✔
2324
        }
2325

2326
        return $new;
8✔
2327
    }
2328

2329
    /**
2330
     * @param string $encoding
2331
     *
2332
     * @return static
2333
     */
2334
    public function withContentEncoding(string $encoding): self
2335
    {
2336
        $new = clone $this;
12✔
2337

2338
        $new->content_encoding = $encoding;
12✔
2339

2340
        return $new;
12✔
2341
    }
2342

2343
    /**
2344
     * @param string|null $mime     use a constant from Mime::*
2345
     * @param string|null $fallback use a constant from Mime::*
2346
     *
2347
     * @return static
2348
     */
2349
    public function withContentType($mime, string $fallback = null): self
2350
    {
2351
        return (clone $this)->_withContentType($mime, $fallback);
12✔
2352
    }
2353

2354
    /**
2355
     * @return static
2356
     */
2357
    public function withContentTypeCsv(): self
2358
    {
2359
        $new = clone $this;
×
2360
        $new->content_type = Mime::getFullMime(Mime::CSV);
×
2361

2362
        return $new;
×
2363
    }
2364

2365
    /**
2366
     * @return static
2367
     */
2368
    public function withContentTypeForm(): self
2369
    {
2370
        $new = clone $this;
4✔
2371
        $new->content_type = Mime::getFullMime(Mime::FORM);
4✔
2372

2373
        return $new;
4✔
2374
    }
2375

2376
    /**
2377
     * @return static
2378
     */
2379
    public function withContentTypeHtml(): self
2380
    {
2381
        $new = clone $this;
×
2382
        $new->content_type = Mime::getFullMime(Mime::HTML);
×
2383

2384
        return $new;
×
2385
    }
2386

2387
    /**
2388
     * @return static
2389
     */
2390
    public function withContentTypeJson(): self
2391
    {
2392
        $new = clone $this;
×
2393
        $new->content_type = Mime::getFullMime(Mime::JSON);
×
2394

2395
        return $new;
×
2396
    }
2397

2398
    /**
2399
     * @return static
2400
     */
2401
    public function withContentTypePlain(): self
2402
    {
2403
        $new = clone $this;
×
2404
        $new->content_type = Mime::getFullMime(Mime::PLAIN);
×
2405

2406
        return $new;
×
2407
    }
2408

2409
    /**
2410
     * @return static
2411
     */
2412
    public function withContentTypeXml(): self
2413
    {
2414
        $new = clone $this;
×
2415
        $new->content_type = Mime::getFullMime(Mime::XML);
×
2416

2417
        return $new;
×
2418
    }
2419

2420
    /**
2421
     * @return static
2422
     */
2423
    public function withContentTypeYaml(): self
2424
    {
2425
        return $this->withContentType(Mime::YAML);
×
2426
    }
2427

2428
    /**
2429
     * @param string $name
2430
     * @param string $value
2431
     *
2432
     * @return static
2433
     */
2434
    public function withCookie(string $name, string $value): self
2435
    {
2436
        return $this->withHeader('Cookie', "{$name}={$value}");
×
2437
    }
2438

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

2452
        $new->additional_curl_opts[$curl_opt] = $curl_opt_val;
12✔
2453

2454
        return $new;
12✔
2455
    }
2456

2457
    /**
2458
     * User Digest Auth.
2459
     *
2460
     * @param string $username
2461
     * @param string $password
2462
     *
2463
     * @return static
2464
     */
2465
    public function withDigestAuth($username, $password): self
2466
    {
2467
        $new = clone $this;
8✔
2468

2469
        $new = $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_DIGEST);
8✔
2470

2471
        return $new->withBasicAuth($username, $password);
8✔
2472
    }
2473

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

2486
        $new->error_handler = $error_handler;
12✔
2487

2488
        return $new;
12✔
2489
    }
2490

2491
    /**
2492
     * @param string|null $mime     use a constant from Mime::*
2493
     * @param string|null $fallback use a constant from Mime::*
2494
     *
2495
     * @return static
2496
     */
2497
    public function withExpectedType($mime, string $fallback = null): self
2498
    {
2499
        return (clone $this)->_withExpectedType($mime, $fallback);
20✔
2500
    }
2501

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

2511
        foreach ($header as $name => $value) {
12✔
2512
            $new = $new->withAddedHeader($name, $value);
12✔
2513
        }
2514

2515
        return $new;
12✔
2516
    }
2517

2518
    /**
2519
     * Helper function to set the Content type and Expected as same in one swoop.
2520
     *
2521
     * @param string|null $mime
2522
     *                          <p>\Httpful\Mime::JSON, \Httpful\Mime::XML, ...</p>
2523
     *
2524
     * @return static
2525
     */
2526
    public function withMimeType($mime): self
2527
    {
2528
        return (clone $this)->_withMimeType($mime);
140✔
2529
    }
2530

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

2541
        $new->withCurlOption(\CURLOPT_HTTPAUTH, \CURLAUTH_NTLM);
×
2542

2543
        return $new->withBasicAuth($username, $password);
×
2544
    }
2545

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

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

2566
        return $new;
4✔
2567
    }
2568

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

2582
        $new->params = \array_merge($new->params, $params);
×
2583

2584
        return $new;
×
2585
    }
2586

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

2599
        $new->parse_callback = $callback;
×
2600

2601
        return $new;
×
2602
    }
2603

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

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

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

2635
        return $new;
4✔
2636
    }
2637

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

2650
        return $this->helperData;
×
2651
    }
2652

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

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

2671
        return $this;
4✔
2672
    }
2673

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

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

2687
        return $new;
×
2688
    }
2689

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

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

2709
        $new->file_path_for_download = $file_path;
4✔
2710

2711
        return $new;
4✔
2712
    }
2713

2714
    /**
2715
     * @param string $uri
2716
     * @param bool   $useClone
2717
     *
2718
     * @return static
2719
     */
2720
    public function withUriFromString(string $uri, bool $useClone = true): self
2721
    {
2722
        if ($useClone) {
304✔
2723
            return (clone $this)->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
                );
28✔
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
            );
116✔
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
        );
120✔
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
    }
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
    }
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
        $this->content_type = Mime::getFullMime($mime);
460✔
3119

3120
        if ($this->isUpload()) {
460✔
3121
            $this->neverSerializePayload();
12✔
3122
        }
3123

3124
        return $this;
460✔
3125
    }
3126

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

3139
        if (empty($mime)) {
460✔
3140
            $mime = $fallback;
460✔
3141
        }
3142

3143
        $this->expected_type = Mime::getFullMime($mime);
460✔
3144

3145
        return $this;
460✔
3146
    }
3147

3148
    /**
3149
     * Helper function to set the Content type and Expected as same in one swoop.
3150
     *
3151
     * @param string|null $mime mime type to use for content type and expected return type
3152
     *
3153
     * @return static
3154
     */
3155
    private function _withMimeType($mime): self
3156
    {
3157
        if (empty($mime)) {
212✔
3158
            return $this;
116✔
3159
        }
3160

3161
        $this->expected_type = Mime::getFullMime($mime);
104✔
3162
        $this->content_type = $this->expected_type;
104✔
3163

3164
        if ($this->isUpload()) {
104✔
3165
            $this->neverSerializePayload();
×
3166
        }
3167

3168
        return $this;
104✔
3169
    }
3170

3171
    /**
3172
     * @param UriInterface $uri
3173
     * @param bool         $preserveHost
3174
     *
3175
     * @return static
3176
     */
3177
    private function _withUri(UriInterface $uri, $preserveHost = false): self
3178
    {
3179
        if ($this->uri === $uri) {
308✔
3180
            return $this;
8✔
3181
        }
3182

3183
        $this->uri = $uri;
308✔
3184

3185
        if (!$preserveHost) {
308✔
3186
            $this->_updateHostFromUri();
308✔
3187
        }
3188

3189
        return $this;
308✔
3190
    }
3191
}
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