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

voku / httpful / 25169179125

30 Apr 2026 01:50PM UTC coverage: 90.495% (+0.6%) from 89.902%
25169179125

Pull #26

github

web-flow
Merge b1d37be55 into 836887e5c
Pull Request #26: [+]: PHP 8.0+ rework

354 of 390 new or added lines in 7 files covered. (90.77%)

1 existing line in 1 file now uncovered.

2542 of 2809 relevant lines covered (90.49%)

49.27 hits per line

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

93.01
/src/Httpful/Curl/Curl.php
1
<?php declare(strict_types=1);
2

3
namespace Httpful\Curl;
4

5
use Httpful\Request;
6
use Httpful\Uri;
7
use Httpful\UriResolver;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\UriInterface;
10

11
/**
12
 * @internal
13
 */
14
final class Curl
15
{
16
    const DEFAULT_TIMEOUT = 30;
17

18
    /**
19
     * @var bool
20
     */
21
    public $error = false;
22

23
    /**
24
     * @var int
25
     */
26
    public $errorCode = 0;
27

28
    /**
29
     * @var string|null
30
     */
31
    public $errorMessage;
32

33
    /**
34
     * @var bool
35
     */
36
    public $curlError = false;
37

38
    /**
39
     * @var int
40
     */
41
    public $curlErrorCode = 0;
42

43
    /**
44
     * @var string|null
45
     */
46
    public $curlErrorMessage;
47

48
    /**
49
     * @var bool
50
     */
51
    public $httpError = false;
52

53
    /**
54
     * @var int
55
     */
56
    public $httpStatusCode = 0;
57

58
    /**
59
     * @var null|bool|string
60
     */
61
    public $rawResponse;
62

63
    /**
64
     * @var callable|null
65
     */
66
    public $beforeSendCallback;
67

68
    /**
69
     * @var callable|null
70
     */
71
    public $downloadCompleteCallback;
72

73
    /**
74
     * @var string|null
75
     */
76
    public $downloadFileName;
77

78
    /**
79
     * @var callable|null
80
     */
81
    public $successCallback;
82

83
    /**
84
     * @var callable|null
85
     */
86
    public $errorCallback;
87

88
    /**
89
     * @var callable|null
90
     */
91
    public $completeCallback;
92

93
    /**
94
     * @var false|resource|null
95
     */
96
    public $fileHandle;
97

98
    /**
99
     * @var int
100
     */
101
    public $attempts = 0;
102

103
    /**
104
     * @var int
105
     */
106
    public $retries = 0;
107

108
    /**
109
     * @var RequestInterface|null
110
     */
111
    public $request;
112

113
    private \CurlHandle|false $curl;
114

115
    /**
116
     * @var int|string|null
117
     */
118
    private $id;
119

120
    /**
121
     * @var string
122
     */
123
    private $rawResponseHeaders = '';
124

125
    /**
126
     * @var array<string, mixed>
127
     */
128
    private $responseCookies = [];
129

130
    /**
131
     * @var bool
132
     */
133
    private $childOfMultiCurl = false;
134

135
    /**
136
     * @var int
137
     */
138
    private $remainingRetries = 0;
139

140
    /**
141
     * @var callable|null
142
     */
143
    private $retryDecider;
144

145
    /**
146
     * @var UriInterface|null
147
     */
148
    private $url;
149

150
    /**
151
     * @var array<string, mixed>
152
     */
153
    private $cookies = [];
154

155
    /**
156
     * @var \stdClass|null
157
     */
158
    private $headerCallbackData;
159

160
    /**
161
     * @param string $base_url
162
     */
163
    public function __construct($base_url = '')
164
    {
165
        if (!\extension_loaded('curl')) {
428✔
166
            throw new \ErrorException('cURL library is not loaded');
×
167
        }
168

169
        $this->curl = \curl_init();
428✔
170
        $this->initialize($base_url);
428✔
171
    }
172

173
    public function __destruct()
174
    {
175
        $this->close();
428✔
176
    }
177

178
    /**
179
     * @return bool
180
     */
181
    public function attemptRetry()
182
    {
183
        // init
184
        $attempt_retry = false;
39✔
185

186
        if ($this->error) {
39✔
187
            if ($this->retryDecider === null) {
11✔
188
                $attempt_retry = $this->remainingRetries >= 1;
10✔
189
            } else {
190
                $attempt_retry = \call_user_func($this->retryDecider, $this);
1✔
191
            }
192
            if ($attempt_retry) {
11✔
193
                ++$this->retries;
2✔
194
                if ($this->remainingRetries) {
2✔
195
                    --$this->remainingRetries;
1✔
196
                }
197
            }
198
        }
199

200
        return $attempt_retry;
39✔
201
    }
202

203
    /**
204
     * @param callable|null $callback
205
     *
206
     * @return $this
207
     */
208
    public function beforeSend($callback)
209
    {
210
        $this->beforeSendCallback = $callback;
7✔
211

212
        return $this;
7✔
213
    }
214

215
    /**
216
     * @param mixed $function
217
     *
218
     * @return void
219
     */
220

221
    /**
222
     * @param callable|null $function
223
     * @param mixed         ...$args
224
     *
225
     * @return $this
226
     */
227
    public function call($function, ...$args)
228
    {
229
        if (\is_callable($function)) {
38✔
230
            \array_unshift($args, $this);
8✔
231
            \call_user_func_array($function, $args);
8✔
232
        }
233

234
        return $this;
38✔
235
    }
236

237
    /**
238
     * @return void
239
     */
240
    public function close()
241
    {
242
        if ($this->curl !== false) {
428✔
243
            \curl_close($this->curl);
428✔
244
            $this->curl = false;
428✔
245
        }
246
    }
247

248
    /**
249
     * @param callable|null $callback
250
     *
251
     * @return $this
252
     */
253
    public function complete($callback)
254
    {
255
        $this->completeCallback = $callback;
7✔
256

257
        return $this;
7✔
258
    }
259

260
    /**
261
     * @param callable|string $filename_or_callable
262
     *
263
     * @return $this
264
     */
265
    public function download($filename_or_callable)
266
    {
267
        // Use tmpfile() or php://temp to avoid "Too many open files" error.
268
        if (\is_callable($filename_or_callable)) {
4✔
269
            $this->downloadCompleteCallback = $filename_or_callable;
1✔
270
            $this->downloadFileName = null;
1✔
271
            $this->fileHandle = \tmpfile();
1✔
272
        } else {
273
            $filename = $filename_or_callable;
3✔
274

275
            // Use a temporary file when downloading. Not using a temporary file can cause an error when an existing
276
            // file has already fully completed downloading and a new download is started with the same destination save
277
            // path. The download request will include header "Range: bytes=$file_size-" which is syntactically valid,
278
            // but unsatisfiable.
279
            $download_filename = $filename . '.pccdownload';
3✔
280
            $this->downloadFileName = $download_filename;
3✔
281

282
            // Attempt to resume download only when a temporary download file exists and is not empty.
283
            if (
284
                \is_file($download_filename)
3✔
285
                &&
286
                $file_size = \filesize($download_filename)
3✔
287
            ) {
288
                $first_byte_position = $file_size;
×
289
                $range = $first_byte_position . '-';
×
290
                $this->setRange($range);
×
291
                $this->fileHandle = \fopen($download_filename, 'ab');
×
292
            } else {
293
                $this->fileHandle = \fopen($download_filename, 'wb');
3✔
294
            }
295

296
            // Move the downloaded temporary file to the destination save path.
297
            $this->downloadCompleteCallback = static function ($instance, $fh) use ($download_filename, $filename) {
3✔
298
                // Close the open file handle before renaming the file.
299
                if (\is_resource($fh)) {
1✔
300
                    \fclose($fh);
1✔
301
                }
302

303
                \rename($download_filename, $filename);
1✔
304
            };
3✔
305
        }
306

307
        if ($this->fileHandle === false) {
4✔
308
            throw new \Httpful\Exception\ClientErrorException('Unable to write to file:' . $this->downloadFileName);
×
309
        }
310

311
        $this->setFile($this->fileHandle);
4✔
312

313
        return $this;
4✔
314
    }
315

316
    /**
317
     * @param callable|null $callback
318
     *
319
     * @return $this
320
     */
321
    public function error($callback)
322
    {
323
        $this->errorCallback = $callback;
7✔
324

325
        return $this;
7✔
326
    }
327

328
    /**
329
     * @param \CurlHandle|false|null $ch
330
     *
331
     * @return mixed returns the value provided by parseResponse
332
     */
333
    public function exec($ch = null)
334
    {
335
        ++$this->attempts;
36✔
336

337
        if ($ch === false || $ch === null) {
36✔
338
            $this->responseCookies = [];
31✔
339
            $this->call($this->beforeSendCallback);
31✔
340
            $curl = $this->getCurlHandle();
31✔
341
            $this->rawResponse = \curl_exec($curl);
31✔
342
            $this->curlErrorCode = \curl_errno($curl);
31✔
343
            $this->curlErrorMessage = \curl_error($curl);
31✔
344
        } else {
345
            $this->rawResponse = \curl_multi_getcontent($ch);
6✔
346
            $this->curlErrorMessage = \curl_error($ch);
6✔
347
        }
348

349
        $this->curlError = $this->curlErrorCode !== 0;
36✔
350

351
        // Transfer the header callback data and release the temporary store to avoid memory leak.
352
        if ($this->headerCallbackData === null) {
36✔
353
            $this->headerCallbackData = new \stdClass();
×
354
        }
355
        $this->rawResponseHeaders = $this->headerCallbackData->rawResponseHeaders;
36✔
356
        $this->responseCookies = $this->headerCallbackData->responseCookies;
36✔
357
        $this->headerCallbackData->rawResponseHeaders = '';
36✔
358
        $this->headerCallbackData->responseCookies = [];
36✔
359

360
        // Include additional error code information in error message when possible.
361
        if ($this->curlError && \function_exists('curl_strerror')) {
36✔
362
            $this->curlErrorMessage = \curl_strerror($this->curlErrorCode) . (empty($this->curlErrorMessage) ? '' : ': ' . $this->curlErrorMessage);
7✔
363
        }
364

365
        $this->httpStatusCode = $this->getInfo(\CURLINFO_HTTP_CODE);
36✔
366
        $this->httpError = \in_array((int) \floor($this->httpStatusCode / 100), [4, 5], true);
36✔
367
        $this->error = $this->curlError || $this->httpError;
36✔
368
        /** @noinspection NestedTernaryOperatorInspection */
369
        $this->errorCode = $this->error ? ($this->curlError ? $this->curlErrorCode : $this->httpStatusCode) : 0;
36✔
370
        $this->errorMessage = $this->curlError ? $this->curlErrorMessage : '';
36✔
371

372
        // Reset nobody setting possibly set from a HEAD request.
373
        $this->setOpt(\CURLOPT_NOBODY, false);
36✔
374

375
        // Allow multicurl to attempt retry as needed.
376
        if ($this->isChildOfMultiCurl()) {
36✔
377
            /** @noinspection PhpInconsistentReturnPointsInspection */
378
            return;
6✔
379
        }
380

381
        if ($this->attemptRetry()) {
31✔
382
            return $this->exec($ch);
×
383
        }
384

385
        $this->execDone();
31✔
386

387
        return $this->rawResponse;
31✔
388
    }
389

390
    /**
391
     * @return void
392
     */
393
    public function execDone()
394
    {
395
        if ($this->error) {
36✔
396
            $this->call($this->errorCallback);
9✔
397
        } else {
398
            $this->call($this->successCallback);
28✔
399
        }
400

401
        $this->call($this->completeCallback);
36✔
402

403
        // Close open file handles and reset the curl instance.
404
        if (\is_resource($this->fileHandle)) {
36✔
405
            $this->downloadComplete($this->fileHandle);
1✔
406
        }
407

408
        // Free some memory + help the GC to free some more memory.
409
        if ($this->request instanceof Request) {
36✔
410
            $this->request->clearHelperData();
6✔
411
        }
412

413
        $this->request = null;
36✔
414
    }
415

416
    /**
417
     * @return int
418
     */
419
    public function getAttempts()
420
    {
421
        return $this->attempts;
1✔
422
    }
423

424
    /**
425
     * @return callable|null
426
     */
427
    public function getBeforeSendCallback()
428
    {
429
        return $this->beforeSendCallback;
1✔
430
    }
431

432
    /**
433
     * @return callable|null
434
     */
435
    public function getCompleteCallback()
436
    {
437
        return $this->completeCallback;
1✔
438
    }
439

440
    /**
441
     * @param string $key
442
     *
443
     * @return mixed
444
     */
445
    public function getCookie($key)
446
    {
447
        return $this->getResponseCookie($key);
×
448
    }
449

450
    /**
451
     * @return \CurlHandle|false
452
     */
453
    public function getCurl()
454
    {
455
        return $this->curl;
107✔
456
    }
457

458
    /**
459
     * @return int
460
     */
461
    public function getCurlErrorCode()
462
    {
463
        return $this->curlErrorCode;
4✔
464
    }
465

466
    /**
467
     * @return string|null
468
     */
469
    public function getCurlErrorMessage()
470
    {
471
        return $this->curlErrorMessage;
2✔
472
    }
473

474
    /**
475
     * @return callable|null
476
     */
477
    public function getDownloadCompleteCallback()
478
    {
479
        return $this->downloadCompleteCallback;
1✔
480
    }
481

482
    /**
483
     * @return string|null
484
     */
485
    public function getDownloadFileName()
486
    {
487
        return $this->downloadFileName;
3✔
488
    }
489

490
    /**
491
     * @return callable|null
492
     */
493
    public function getErrorCallback()
494
    {
495
        return $this->errorCallback;
1✔
496
    }
497

498
    /**
499
     * @return int
500
     */
501
    public function getErrorCode()
502
    {
503
        return $this->errorCode;
8✔
504
    }
505

506
    /**
507
     * @return string|null
508
     */
509
    public function getErrorMessage()
510
    {
511
        return $this->errorMessage;
8✔
512
    }
513

514
    /**
515
     * @return resource|false|null
516
     */
517
    public function getFileHandle()
518
    {
519
        return $this->fileHandle;
3✔
520
    }
521

522
    /**
523
     * @return int
524
     */
525
    public function getHttpStatusCode()
526
    {
527
        return $this->httpStatusCode;
6✔
528
    }
529

530
    /**
531
     * @return int|string|null
532
     */
533
    public function getId()
534
    {
535
        return $this->id;
7✔
536
    }
537

538
    /**
539
     * @param int|null $opt
540
     *
541
     * @return mixed
542
     */
543
    public function getInfo($opt = null)
544
    {
545
        $curl = $this->getCurlHandle();
37✔
546
        if (\func_num_args() === 0) {
37✔
547
            return \curl_getinfo($curl);
30✔
548
        }
549

550
        return \curl_getinfo($curl, $opt);
36✔
551
    }
552

553
    /**
554
     * @return null|bool|string
555
     */
556
    public function getRawResponse()
557
    {
558
        return $this->rawResponse;
31✔
559
    }
560

561
    /**
562
     * @return string
563
     */
564
    public function getRawResponseHeaders()
565
    {
566
        return $this->rawResponseHeaders;
31✔
567
    }
568

569
    /**
570
     * @return int
571
     */
572
    public function getRemainingRetries()
573
    {
574
        return $this->remainingRetries;
5✔
575
    }
576

577
    /**
578
     * @param string $key
579
     *
580
     * @return mixed
581
     */
582
    public function getResponseCookie($key)
583
    {
584
        return $this->responseCookies[$key] ?? null;
1✔
585
    }
586

587
    /**
588
     * @return array<string, mixed>
589
     */
590
    public function getResponseCookies()
591
    {
592
        return $this->responseCookies;
1✔
593
    }
594

595
    /**
596
     * @return int
597
     */
598
    public function getRetries()
599
    {
600
        return $this->retries;
3✔
601
    }
602

603
    /**
604
     * @return callable|null
605
     */
606
    public function getRetryDecider()
607
    {
608
        return $this->retryDecider;
11✔
609
    }
610

611
    /**
612
     * @return callable|null
613
     */
614
    public function getSuccessCallback()
615
    {
616
        return $this->successCallback;
1✔
617
    }
618

619
    /**
620
     * @return UriInterface|null
621
     */
622
    public function getUrl()
623
    {
624
        return $this->url;
6✔
625
    }
626

627
    /**
628
     * @return bool
629
     */
630
    public function isChildOfMultiCurl(): bool
631
    {
632
        return $this->childOfMultiCurl;
37✔
633
    }
634

635
    /**
636
     * @return bool
637
     */
638
    public function isCurlError(): bool
639
    {
640
        return $this->curlError;
9✔
641
    }
642

643
    /**
644
     * @return bool
645
     */
646
    public function isError(): bool
647
    {
648
        return $this->error;
1✔
649
    }
650

651
    /**
652
     * @return bool
653
     */
654
    public function isHttpError(): bool
655
    {
656
        return $this->httpError;
1✔
657
    }
658

659
    /**
660
     * @param callable|null $callback
661
     *
662
     * @return $this
663
     */
664
    public function progress($callback)
665
    {
666
        $this->setOpt(\CURLOPT_PROGRESSFUNCTION, $callback);
2✔
667
        $this->setOpt(\CURLOPT_NOPROGRESS, false);
2✔
668

669
        return $this;
2✔
670
    }
671

672
    /**
673
     * @return void
674
     */
675
    public function reset()
676
    {
677
        if ($this->curl !== false) {
2✔
678
            \curl_reset($this->curl);
1✔
679
        } else {
680
            $this->curl = \curl_init();
1✔
681
        }
682

683
        $this->initialize('');
2✔
684
    }
685

686
    /**
687
     * @param string $username
688
     * @param string $password
689
     *
690
     * @return $this
691
     */
692
    public function setBasicAuthentication($username, $password = '')
693
    {
694
        $this->setOpt(\CURLOPT_HTTPAUTH, \CURLAUTH_BASIC);
1✔
695
        $this->setOpt(\CURLOPT_USERPWD, $username . ':' . $password);
1✔
696

697
        return $this;
1✔
698
    }
699

700
    /**
701
     * @param bool $bool
702
     *
703
     * @return $this
704
     */
705
    public function setChildOfMultiCurl(bool $bool)
706
    {
707
        $this->childOfMultiCurl = $bool;
28✔
708

709
        return $this;
28✔
710
    }
711

712
    /**
713
     * @param int $seconds
714
     *
715
     * @return $this
716
     */
717
    public function setConnectTimeout($seconds)
718
    {
719
        $this->setOpt(\CURLOPT_CONNECTTIMEOUT, $seconds);
1✔
720

721
        return $this;
1✔
722
    }
723

724
    /**
725
     * @param string $key
726
     * @param mixed  $value
727
     *
728
     * @return $this
729
     */
730
    public function setCookie($key, $value)
731
    {
732
        $this->setEncodedCookie($key, $value);
1✔
733
        $this->buildCookies();
1✔
734

735
        return $this;
1✔
736
    }
737

738
    /**
739
     * @param string $cookie_file
740
     *
741
     * @return $this
742
     */
743
    public function setCookieFile($cookie_file)
744
    {
745
        $this->setOpt(\CURLOPT_COOKIEFILE, $cookie_file);
1✔
746

747
        return $this;
1✔
748
    }
749

750
    /**
751
     * @param string $cookie_jar
752
     *
753
     * @return $this
754
     */
755
    public function setCookieJar($cookie_jar)
756
    {
757
        $this->setOpt(\CURLOPT_COOKIEJAR, $cookie_jar);
1✔
758

759
        return $this;
1✔
760
    }
761

762
    /**
763
     * @param string $string
764
     *
765
     * @return $this
766
     */
767
    public function setCookieString($string)
768
    {
769
        $this->setOpt(\CURLOPT_COOKIE, $string);
1✔
770

771
        return $this;
1✔
772
    }
773

774
    /**
775
     * @param array<string, mixed> $cookies
776
     *
777
     * @return $this
778
     */
779
    public function setCookies($cookies)
780
    {
781
        foreach ($cookies as $key => $value) {
7✔
782
            $this->setEncodedCookie($key, $value);
1✔
783
        }
784
        $this->buildCookies();
7✔
785

786
        return $this;
7✔
787
    }
788

789
    /**
790
     * @return $this
791
     */
792
    public function setDefaultTimeout()
793
    {
794
        $this->setTimeout(self::DEFAULT_TIMEOUT);
428✔
795

796
        return $this;
428✔
797
    }
798

799
    /**
800
     * @param string $username
801
     * @param string $password
802
     *
803
     * @return $this
804
     */
805
    public function setDigestAuthentication($username, $password = '')
806
    {
807
        $this->setOpt(\CURLOPT_HTTPAUTH, \CURLAUTH_DIGEST);
1✔
808
        $this->setOpt(\CURLOPT_USERPWD, $username . ':' . $password);
1✔
809

810
        return $this;
1✔
811
    }
812

813
    /**
814
     * @param int|string $id
815
     *
816
     * @return $this
817
     */
818
    public function setId($id)
819
    {
820
        $this->id = $id;
428✔
821

822
        return $this;
428✔
823
    }
824

825
    /**
826
     * @param int $bytes
827
     *
828
     * @return $this
829
     */
830
    public function setMaxFilesize($bytes)
831
    {
832
        $callback = static function ($resource, $download_size, $downloaded, $upload_size, $uploaded) use ($bytes) {
1✔
833
            // Abort the transfer when $downloaded bytes exceeds maximum $bytes by returning a non-zero value.
834
            return $downloaded > $bytes ? 1 : 0;
×
835
        };
1✔
836

837
        $this->progress($callback);
1✔
838

839
        return $this;
1✔
840
    }
841

842
    /**
843
     * @param int   $option
844
     * @param mixed $value
845
     *
846
     * @return bool
847
     */
848
    public function setOpt($option, $value)
849
    {
850
        return \curl_setopt($this->getCurlHandle(), $option, $value);
428✔
851
    }
852

853
    /**
854
     * @param resource $file
855
     *
856
     * @return $this
857
     */
858
    public function setFile($file)
859
    {
860
        $this->setOpt(\CURLOPT_FILE, $file);
6✔
861

862
        return $this;
6✔
863
    }
864

865
    /**
866
     * @param array<int, mixed> $options
867
     *
868
     * @return bool
869
     *              <p>Returns true if all options were successfully set. If an option could not be successfully set,
870
     *              false is immediately returned, ignoring any future options in the options array. Similar to
871
     *              curl_setopt_array().</p>
872
     */
873
    public function setOpts($options)
874
    {
875
        foreach ($options as $option => $value) {
1✔
876
            if (!$this->setOpt($option, $value)) {
1✔
877
                return false;
×
878
            }
879
        }
880

881
        return true;
1✔
882
    }
883

884
    /**
885
     * @param int $port
886
     *
887
     * @return $this
888
     */
889
    public function setPort($port)
890
    {
891
        $this->setOpt(\CURLOPT_PORT, (int) $port);
1✔
892

893
        return $this;
1✔
894
    }
895

896
    /**
897
     * Set an HTTP proxy to tunnel requests through.
898
     *
899
     * @param string $proxy    - The HTTP proxy to tunnel requests through. May include port number.
900
     * @param int    $port     - The port number of the proxy to connect to. This port number can also be set in $proxy.
901
     * @param string $username - The username to use for the connection to the proxy
902
     * @param string $password - The password to use for the connection to the proxy
903
     *
904
     * @return $this
905
     */
906
    public function setProxy($proxy, $port = null, $username = null, $password = null)
907
    {
908
        $this->setOpt(\CURLOPT_PROXY, $proxy);
2✔
909

910
        if ($port !== null) {
2✔
911
            $this->setOpt(\CURLOPT_PROXYPORT, $port);
2✔
912
        }
913

914
        if ($username !== null && $password !== null) {
2✔
915
            $this->setOpt(\CURLOPT_PROXYUSERPWD, $username . ':' . $password);
1✔
916
        }
917

918
        return $this;
2✔
919
    }
920

921
    /**
922
     * Set the HTTP authentication method(s) to use for the proxy connection.
923
     *
924
     * @param int $auth
925
     *
926
     * @return $this
927
     */
928
    public function setProxyAuth($auth)
929
    {
930
        $this->setOpt(\CURLOPT_PROXYAUTH, $auth);
1✔
931

932
        return $this;
1✔
933
    }
934

935
    /**
936
     * Set the proxy to tunnel through HTTP proxy.
937
     *
938
     * @param bool $tunnel
939
     *
940
     * @return $this
941
     */
942
    public function setProxyTunnel($tunnel = true)
943
    {
944
        $this->setOpt(\CURLOPT_HTTPPROXYTUNNEL, $tunnel);
1✔
945

946
        return $this;
1✔
947
    }
948

949
    /**
950
     * Set the proxy protocol type.
951
     *
952
     * @param int $type
953
     *                  <p>CURLPROXY_*</p>
954
     *
955
     * @return $this
956
     */
957
    public function setProxyType($type)
958
    {
959
        $this->setOpt(\CURLOPT_PROXYTYPE, $type);
1✔
960

961
        return $this;
1✔
962
    }
963

964
    /**
965
     * @param string $range <p>e.g. "0-4096"</p>
966
     *
967
     * @return $this
968
     */
969
    public function setRange($range)
970
    {
971
        $this->setOpt(\CURLOPT_RANGE, $range);
1✔
972

973
        return $this;
1✔
974
    }
975

976
    /**
977
     * @param string $referer
978
     *
979
     * @return $this
980
     */
981
    public function setReferer($referer)
982
    {
983
        $this->setReferrer($referer);
1✔
984

985
        return $this;
1✔
986
    }
987

988
    /**
989
     * @param string $referrer
990
     *
991
     * @return $this
992
     */
993
    public function setReferrer($referrer)
994
    {
995
        $this->setOpt(\CURLOPT_REFERER, $referrer);
1✔
996

997
        return $this;
1✔
998
    }
999

1000
    /**
1001
     * Number of retries to attempt or decider callable.
1002
     *
1003
     * When using a number of retries to attempt, the maximum number of attempts
1004
     * for the request is $maximum_number_of_retries + 1.
1005
     *
1006
     * When using a callable decider, the request will be retried until the
1007
     * function returns a value which evaluates to false.
1008
     *
1009
     * @param callable|int $retry
1010
     *
1011
     * @return $this
1012
     */
1013
    public function setRetry($retry)
1014
    {
1015
        if (\is_callable($retry)) {
105✔
1016
            $this->remainingRetries = 0;
10✔
1017
            $this->retryDecider = $retry;
10✔
1018
        } elseif (\is_int($retry)) {
95✔
1019
            $maximum_number_of_retries = $retry;
95✔
1020
            $this->retryDecider = null;
95✔
1021
            $this->remainingRetries = $maximum_number_of_retries;
95✔
1022
        }
1023

1024
        return $this;
105✔
1025
    }
1026

1027
    /**
1028
     * @param int $seconds
1029
     *
1030
     * @return $this
1031
     */
1032
    public function setTimeout($seconds)
1033
    {
1034
        $this->setOpt(\CURLOPT_TIMEOUT, $seconds);
428✔
1035

1036
        return $this;
428✔
1037
    }
1038

1039
    /**
1040
     * @param string $url
1041
     * @param scalar|array<array-key,scalar> $mixed_data
1042
     *
1043
     * @return $this
1044
     */
1045
    public function setUrl($url, $mixed_data = '')
1046
    {
1047
        $built_url = new Uri($this->buildUrl($url, $mixed_data));
428✔
1048

1049
        if ($this->url === null) {
428✔
1050
            $this->url = UriResolver::resolve($built_url, new Uri(''));
428✔
1051
        } else {
1052
            $this->url = UriResolver::resolve($this->url, $built_url);
107✔
1053
        }
1054

1055
        $this->setOpt(\CURLOPT_URL, (string) $this->url);
428✔
1056

1057
        return $this;
428✔
1058
    }
1059

1060
    /**
1061
     * @param string $user_agent
1062
     *
1063
     * @return $this
1064
     */
1065
    public function setUserAgent($user_agent)
1066
    {
1067
        $this->setOpt(\CURLOPT_USERAGENT, $user_agent);
1✔
1068

1069
        return $this;
1✔
1070
    }
1071

1072
    /**
1073
     * @param callable|null $callback
1074
     *
1075
     * @return $this
1076
     */
1077
    public function success($callback)
1078
    {
1079
        $this->successCallback = $callback;
7✔
1080

1081
        return $this;
7✔
1082
    }
1083

1084
    /**
1085
     * Disable use of the proxy.
1086
     *
1087
     * @return $this
1088
     */
1089
    public function unsetProxy()
1090
    {
1091
        $this->setOpt(\CURLOPT_PROXY, null);
1✔
1092

1093
        return $this;
1✔
1094
    }
1095

1096
    /**
1097
     * @param bool          $on
1098
     * @param resource|null $output
1099
     *
1100
     * @return $this
1101
     */
1102
    public function verbose($on = true, $output = null)
1103
    {
1104
        // fallback
1105
        if ($output === null) {
1✔
1106
            if (!\defined('STDERR')) {
1✔
1107
                \define('STDERR', \fopen('php://stderr', 'wb'));
×
1108
            }
1109
            $output = \STDERR;
1✔
1110
        }
1111

1112
        $this->setOpt(\CURLOPT_VERBOSE, $on);
1✔
1113
        $this->setOpt(\CURLOPT_STDERR, $output);
1✔
1114

1115
        return $this;
1✔
1116
    }
1117

1118
    /**
1119
     * Build Cookies
1120
     *
1121
     * @return void
1122
     */
1123
    private function buildCookies()
1124
    {
1125
        // Avoid using http_build_query() as unnecessary encoding is performed.
1126
        // http_build_query($this->cookies, '', '; ');
1127
        $this->setOpt(
7✔
1128
            \CURLOPT_COOKIE,
7✔
1129
            \implode(
7✔
1130
                '; ',
7✔
1131
                \array_map(
7✔
1132
                    static function ($k, $v) {
7✔
1133
                        return $k . '=' . $v;
1✔
1134
                    },
7✔
1135
                    \array_keys($this->cookies),
7✔
1136
                    \array_values($this->cookies)
7✔
1137
                )
7✔
1138
            )
7✔
1139
        );
7✔
1140
    }
1141

1142
    /**
1143
     * @param string $url
1144
     * @param scalar|array<array-key,scalar> $mixed_data
1145
     *
1146
     * @return string
1147
     */
1148
    private function buildUrl($url, $mixed_data = '')
1149
    {
1150
        // init
1151
        $query_string = '';
428✔
1152

1153
        if (!empty($mixed_data)) {
428✔
1154
            $query_mark = \strpos($url, '?') > 0 ? '&' : '?';
2✔
1155
            if (\is_scalar($mixed_data)) {
2✔
1156
                $query_string .= $query_mark . $mixed_data;
1✔
1157
            } elseif (\is_array($mixed_data)) {
1✔
1158
                $query_string .= $query_mark . \http_build_query($mixed_data, '', '&');
1✔
1159
            }
1160
        }
1161

1162
        return $url . $query_string;
428✔
1163
    }
1164

1165
    /**
1166
     * Create Header Callback
1167
     *
1168
     * Gather headers and parse cookies as response headers are received. Keep this function separate from the class so
1169
     * that unset($curl) automatically calls __destruct() as expected. Otherwise, manually calling $curl->close() will
1170
     * be necessary to prevent a memory leak.
1171
     *
1172
     * @param \stdClass $header_callback_data
1173
     *
1174
     * @return callable
1175
     */
1176
    private function createHeaderCallback($header_callback_data)
1177
    {
1178
        return static function ($ch, $header) use ($header_callback_data) {
428✔
1179
            if (\preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) === 1) {
29✔
1180
                $header_callback_data->responseCookies[$cookie[1]] = \trim($cookie[2], " \n\r\t\0\x0B");
16✔
1181
            }
1182
            $header_callback_data->rawResponseHeaders .= $header;
29✔
1183

1184
            return \strlen($header);
29✔
1185
        };
428✔
1186
    }
1187

1188
    /**
1189
     * @param resource $fh
1190
     *
1191
     * @return void
1192
     */
1193
    private function downloadComplete($fh)
1194
    {
1195
        if (
1196
            $this->error
1✔
1197
            &&
1198
            $this->downloadFileName
1✔
1199
            &&
1200
            \is_file($this->downloadFileName)
1✔
1201
        ) {
1202
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
1203
            @\unlink($this->downloadFileName);
×
1204
        } elseif (
1205
            !$this->error
1✔
1206
            &&
1207
            $this->downloadCompleteCallback
1✔
1208
        ) {
1209
            \rewind($fh);
1✔
1210
            $this->call($this->downloadCompleteCallback, $fh);
1✔
1211
            $this->downloadCompleteCallback = null;
1✔
1212
        }
1213

1214
        if (\is_resource($fh)) {
1✔
1215
            \fclose($fh);
×
1216
        }
1217

1218
        // Fix "PHP Notice: Use of undefined constant STDOUT" when reading the
1219
        // PHP script from stdin. Using null causes "Warning: curl_setopt():
1220
        // supplied argument is not a valid File-Handle resource".
1221
        if (!\defined('STDOUT')) {
1✔
NEW
1222
            $stdout = \fopen('php://stdout', 'wb');
×
NEW
1223
            if ($stdout === false) {
×
NEW
1224
                throw new \RuntimeException('Unable to open STDOUT stream.');
×
1225
            }
1226

NEW
1227
            \define('STDOUT', $stdout);
×
1228
        }
1229

1230
        if (!\is_resource(\STDOUT)) {
1✔
NEW
1231
            throw new \RuntimeException('STDOUT stream is not available.');
×
1232
        }
1233

1234
        // Reset CURLOPT_FILE with STDOUT to avoid: "curl_exec(): CURLOPT_FILE
1235
        // resource has gone away, resetting to default".
1236
        $this->setFile(\STDOUT);
1✔
1237

1238
        // Reset CURLOPT_RETURNTRANSFER to tell cURL to return subsequent
1239
        // responses as the return value of curl_exec(). Without this,
1240
        // curl_exec() will revert to returning boolean values.
1241
        $this->setOpt(\CURLOPT_RETURNTRANSFER, true);
1✔
1242
    }
1243

1244
    /**
1245
     * @param string $base_url
1246
     *
1247
     * @return void
1248
     */
1249
    private function initialize($base_url)
1250
    {
1251
        $this->setId(\uniqid('', true));
428✔
1252
        $this->setDefaultTimeout();
428✔
1253
        $this->setOpt(\CURLINFO_HEADER_OUT, true);
428✔
1254

1255
        // Create a placeholder to temporarily store the header callback data.
1256
        $header_callback_data = new \stdClass();
428✔
1257
        $header_callback_data->rawResponseHeaders = '';
428✔
1258
        $header_callback_data->responseCookies = [];
428✔
1259
        $this->headerCallbackData = $header_callback_data;
428✔
1260
        $this->setOpt(\CURLOPT_HEADERFUNCTION, $this->createHeaderCallback($header_callback_data));
428✔
1261

1262
        $this->setOpt(\CURLOPT_RETURNTRANSFER, true);
428✔
1263
        $this->setUrl($base_url);
428✔
1264
    }
1265

1266
    /**
1267
     * @param string $key
1268
     * @param mixed  $value
1269
     *
1270
     * @return $this
1271
     */
1272
    private function setEncodedCookie($key, $value)
1273
    {
1274
        $name_chars = [];
1✔
1275
        foreach (\str_split($key) as $name_char) {
1✔
1276
            $name_chars[] = \rawurlencode($name_char);
1✔
1277
        }
1278

1279
        $value_chars = [];
1✔
1280
        foreach (\str_split($value) as $value_char) {
1✔
1281
            $value_chars[] = \rawurlencode($value_char);
1✔
1282
        }
1283

1284
        $this->cookies[\implode('', $name_chars)] = \implode('', $value_chars);
1✔
1285

1286
        return $this;
1✔
1287
    }
1288

1289
    private function getCurlHandle(): \CurlHandle
1290
    {
1291
        if ($this->curl === false) {
428✔
NEW
1292
            throw new \LogicException('cURL handle is not initialized.');
×
1293
        }
1294

1295
        return $this->curl;
428✔
1296
    }
1297
}
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

© 2026 Coveralls, Inc