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

codeigniter4 / CodeIgniter4 / 15071352842

16 May 2025 02:56PM UTC coverage: 84.271% (+0.006%) from 84.265%
15071352842

Pull #9557

github

web-flow
Merge 5aacc744f into 59a481934
Pull Request #9557: feat: share Connection & DNS Cache to `CURLRequest`

12 of 12 new or added lines in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

20841 of 24731 relevant lines covered (84.27%)

191.48 hits per line

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

98.82
/system/HTTP/CURLRequest.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\InvalidArgumentException;
17
use CodeIgniter\HTTP\Exceptions\HTTPException;
18
use Config\App;
19
use Config\CURLRequest as ConfigCURLRequest;
20
use CurlShareHandle;
21

22
/**
23
 * A lightweight HTTP client for sending synchronous HTTP requests via cURL.
24
 *
25
 * @see \CodeIgniter\HTTP\CURLRequestTest
26
 */
27
class CURLRequest extends OutgoingRequest
28
{
29
    /**
30
     * The response object associated with this request
31
     *
32
     * @var ResponseInterface|null
33
     */
34
    protected $response;
35

36
    /**
37
     * The original response object associated with this request
38
     *
39
     * @var ResponseInterface|null
40
     */
41
    protected $responseOrig;
42

43
    /**
44
     * The URI associated with this request
45
     *
46
     * @var URI
47
     */
48
    protected $baseURI;
49

50
    /**
51
     * The setting values
52
     *
53
     * @var array
54
     */
55
    protected $config;
56

57
    /**
58
     * The default setting values
59
     *
60
     * @var array
61
     */
62
    protected $defaultConfig = [
63
        'timeout'         => 0.0,
64
        'connect_timeout' => 150,
65
        'debug'           => false,
66
        'verify'          => true,
67
    ];
68

69
    /**
70
     * Default values for when 'allow_redirects'
71
     * option is true.
72
     *
73
     * @var array
74
     */
75
    protected $redirectDefaults = [
76
        'max'       => 5,
77
        'strict'    => true,
78
        'protocols' => [
79
            'http',
80
            'https',
81
        ],
82
    ];
83

84
    /**
85
     * The number of milliseconds to delay before
86
     * sending the request.
87
     *
88
     * @var float
89
     */
90
    protected $delay = 0.0;
91

92
    /**
93
     * The default options from the constructor. Applied to all requests.
94
     */
95
    private readonly array $defaultOptions;
96

97
    /**
98
     * Whether share options between requests or not.
99
     *
100
     * If true, all the options won't be reset between requests.
101
     * It may cause an error request with unnecessary headers.
102
     */
103
    private readonly bool $shareOptions;
104

105
    /**
106
     * Whether share options between requests or not.
107
     *
108
     * If true, all the options won't be reset between requests.
109
     * It may cause an error request with unnecessary headers.
110
     */
111
    protected ?CurlShareHandle $shareConnection = null;
112

113
    /**
114
     * Takes an array of options to set the following possible class properties:
115
     *
116
     *  - baseURI
117
     *  - timeout
118
     *  - any other request options to use as defaults.
119
     *
120
     * @param array<string, mixed> $options
121
     */
122
    public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
123
    {
124
        if (! function_exists('curl_version')) {
180✔
UNCOV
125
            throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
×
126
        }
127

128
        parent::__construct(Method::GET, $uri);
180✔
129

130
        $this->responseOrig = $response ?? new Response($config);
180✔
131
        // Remove the default Content-Type header.
132
        $this->responseOrig->removeHeader('Content-Type');
180✔
133

134
        $this->baseURI        = $uri->useRawQueryString();
180✔
135
        $this->defaultOptions = $options;
180✔
136

137
        $this->shareOptions = config(ConfigCURLRequest::class)->shareOptions ?? true;
180✔
138

139
        $this->config = $this->defaultConfig;
180✔
140
        $this->parseOptions($options);
180✔
141

142
        // Share Connection
143
        $optShareConnection = config(ConfigCURLRequest::class)->shareConnection ?? [
180✔
144
            CURL_LOCK_DATA_CONNECT,
180✔
145
            CURL_LOCK_DATA_DNS,
180✔
146
        ];
147

148
        if ($optShareConnection !== []) {
180✔
149
            $this->shareConnection = curl_share_init();
180✔
150

151
            foreach (array_unique($optShareConnection) as $opt) {
180✔
152
                curl_share_setopt($this->shareConnection, CURLSHOPT_SHARE, $opt);
180✔
153
            }
154
        }
155
    }
156

157
    /**
158
     * Sends an HTTP request to the specified $url. If this is a relative
159
     * URL, it will be merged with $this->baseURI to form a complete URL.
160
     *
161
     * @param string $method HTTP method
162
     */
163
    public function request($method, string $url, array $options = []): ResponseInterface
164
    {
165
        $this->response = clone $this->responseOrig;
166✔
166

167
        $this->parseOptions($options);
166✔
168

169
        $url = $this->prepareURL($url);
166✔
170

171
        $method = esc(strip_tags($method));
166✔
172

173
        $this->send($method, $url);
166✔
174

175
        if ($this->shareOptions === false) {
162✔
176
            $this->resetOptions();
82✔
177
        }
178

179
        return $this->response;
162✔
180
    }
181

182
    /**
183
     * Reset all options to default.
184
     *
185
     * @return void
186
     */
187
    protected function resetOptions()
188
    {
189
        // Reset headers
190
        $this->headers   = [];
82✔
191
        $this->headerMap = [];
82✔
192

193
        // Reset body
194
        $this->body = null;
82✔
195

196
        // Reset configs
197
        $this->config = $this->defaultConfig;
82✔
198

199
        // Set the default options for next request
200
        $this->parseOptions($this->defaultOptions);
82✔
201
    }
202

203
    /**
204
     * Convenience method for sending a GET request.
205
     */
206
    public function get(string $url, array $options = []): ResponseInterface
207
    {
208
        return $this->request(Method::GET, $url, $options);
38✔
209
    }
210

211
    /**
212
     * Convenience method for sending a DELETE request.
213
     */
214
    public function delete(string $url, array $options = []): ResponseInterface
215
    {
216
        return $this->request('DELETE', $url, $options);
2✔
217
    }
218

219
    /**
220
     * Convenience method for sending a HEAD request.
221
     */
222
    public function head(string $url, array $options = []): ResponseInterface
223
    {
224
        return $this->request('HEAD', $url, $options);
2✔
225
    }
226

227
    /**
228
     * Convenience method for sending an OPTIONS request.
229
     */
230
    public function options(string $url, array $options = []): ResponseInterface
231
    {
232
        return $this->request('OPTIONS', $url, $options);
2✔
233
    }
234

235
    /**
236
     * Convenience method for sending a PATCH request.
237
     */
238
    public function patch(string $url, array $options = []): ResponseInterface
239
    {
240
        return $this->request('PATCH', $url, $options);
2✔
241
    }
242

243
    /**
244
     * Convenience method for sending a POST request.
245
     */
246
    public function post(string $url, array $options = []): ResponseInterface
247
    {
248
        return $this->request(Method::POST, $url, $options);
16✔
249
    }
250

251
    /**
252
     * Convenience method for sending a PUT request.
253
     */
254
    public function put(string $url, array $options = []): ResponseInterface
255
    {
256
        return $this->request(Method::PUT, $url, $options);
2✔
257
    }
258

259
    /**
260
     * Set the HTTP Authentication.
261
     *
262
     * @param string $type basic or digest
263
     *
264
     * @return $this
265
     */
266
    public function setAuth(string $username, string $password, string $type = 'basic')
267
    {
268
        $this->config['auth'] = [
4✔
269
            $username,
4✔
270
            $password,
4✔
271
            $type,
4✔
272
        ];
4✔
273

274
        return $this;
4✔
275
    }
276

277
    /**
278
     * Set form data to be sent.
279
     *
280
     * @param bool $multipart Set TRUE if you are sending CURLFiles
281
     *
282
     * @return $this
283
     */
284
    public function setForm(array $params, bool $multipart = false)
285
    {
286
        if ($multipart) {
2✔
287
            $this->config['multipart'] = $params;
2✔
288
        } else {
289
            $this->config['form_params'] = $params;
2✔
290
        }
291

292
        return $this;
2✔
293
    }
294

295
    /**
296
     * Set JSON data to be sent.
297
     *
298
     * @param array|bool|float|int|object|string|null $data
299
     *
300
     * @return $this
301
     */
302
    public function setJSON($data)
303
    {
304
        $this->config['json'] = $data;
2✔
305

306
        return $this;
2✔
307
    }
308

309
    /**
310
     * Sets the correct settings based on the options array
311
     * passed in.
312
     *
313
     * @return void
314
     */
315
    protected function parseOptions(array $options)
316
    {
317
        if (array_key_exists('baseURI', $options)) {
181✔
318
            $this->baseURI = $this->baseURI->setURI($options['baseURI']);
44✔
319
            unset($options['baseURI']);
44✔
320
        }
321

322
        if (array_key_exists('headers', $options) && is_array($options['headers'])) {
181✔
323
            foreach ($options['headers'] as $name => $value) {
6✔
324
                $this->setHeader($name, $value);
6✔
325
            }
326

327
            unset($options['headers']);
6✔
328
        }
329

330
        if (array_key_exists('delay', $options)) {
181✔
331
            // Convert from the milliseconds passed in
332
            // to the seconds that sleep requires.
333
            $this->delay = (float) $options['delay'] / 1000;
24✔
334
            unset($options['delay']);
24✔
335
        }
336

337
        if (array_key_exists('body', $options)) {
181✔
338
            $this->setBody($options['body']);
2✔
339
            unset($options['body']);
2✔
340
        }
341

342
        foreach ($options as $key => $value) {
181✔
343
            $this->config[$key] = $value;
96✔
344
        }
345
    }
346

347
    /**
348
     * If the $url is a relative URL, will attempt to create
349
     * a full URL by prepending $this->baseURI to it.
350
     */
351
    protected function prepareURL(string $url): string
352
    {
353
        // If it's a full URI, then we have nothing to do here...
354
        if (str_contains($url, '://')) {
168✔
355
            return $url;
82✔
356
        }
357

358
        $uri = $this->baseURI->resolveRelativeURI($url);
86✔
359

360
        // Create the string instead of casting to prevent baseURL muddling
361
        return URI::createURIString(
86✔
362
            $uri->getScheme(),
86✔
363
            $uri->getAuthority(),
86✔
364
            $uri->getPath(),
86✔
365
            $uri->getQuery(),
86✔
366
            $uri->getFragment(),
86✔
367
        );
86✔
368
    }
369

370
    /**
371
     * Fires the actual cURL request.
372
     *
373
     * @return ResponseInterface
374
     */
375
    public function send(string $method, string $url)
376
    {
377
        // Reset our curl options so we're on a fresh slate.
378
        $curlOptions = [];
168✔
379

380
        if (! empty($this->config['query']) && is_array($this->config['query'])) {
168✔
381
            // This is likely too naive a solution.
382
            // Should look into handling when $url already
383
            // has query vars on it.
384
            $url .= '?' . http_build_query($this->config['query']);
2✔
385
            unset($this->config['query']);
2✔
386
        }
387

388
        $curlOptions[CURLOPT_URL]            = $url;
168✔
389
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
168✔
390

391
        if ($this->shareConnection instanceof CurlShareHandle) {
168✔
392
            $curlOptions[CURLOPT_SHARE] = $this->shareConnection;
166✔
393
        }
394

395
        $curlOptions[CURLOPT_HEADER] = true;
168✔
396
        // Disable @file uploads in post data.
397
        $curlOptions[CURLOPT_SAFE_UPLOAD] = true;
168✔
398

399
        $curlOptions = $this->setCURLOptions($curlOptions, $this->config);
168✔
400
        $curlOptions = $this->applyMethod($method, $curlOptions);
164✔
401
        $curlOptions = $this->applyRequestHeaders($curlOptions);
164✔
402

403
        // Do we need to delay this request?
404
        if ($this->delay > 0) {
164✔
405
            usleep((int) $this->delay * 1_000_000);
22✔
406
        }
407

408
        $output = $this->sendRequest($curlOptions);
164✔
409

410
        // Set the string we want to break our response from
411
        $breakString = "\r\n\r\n";
164✔
412

413
        if (isset($this->config['allow_redirects']) && $this->config['allow_redirects'] !== false) {
164✔
414
            $output = $this->handleRedirectHeaders($output, $breakString);
10✔
415
        }
416

417
        while (str_starts_with($output, 'HTTP/1.1 100 Continue')) {
164✔
418
            $output = substr($output, strpos($output, $breakString) + 4);
6✔
419
        }
420

421
        if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) {
164✔
422
            $output = substr($output, strpos($output, $breakString) + 4);
4✔
423
        }
424

425
        // If request and response have Digest
426
        if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) {
164✔
427
            $output = substr($output, strpos($output, $breakString) + 4);
4✔
428
        }
429

430
        // Split out our headers and body
431
        $break = strpos($output, $breakString);
164✔
432

433
        if ($break !== false) {
164✔
434
            // Our headers
435
            $headers = explode("\n", substr($output, 0, $break));
28✔
436

437
            $this->setResponseHeaders($headers);
28✔
438

439
            // Our body
440
            $body = substr($output, $break + 4);
28✔
441
            $this->response->setBody($body);
28✔
442
        } else {
443
            $this->response->setBody($output);
136✔
444
        }
445

446
        return $this->response;
164✔
447
    }
448

449
    /**
450
     * Adds $this->headers to the cURL request.
451
     */
452
    protected function applyRequestHeaders(array $curlOptions = []): array
453
    {
454
        if (empty($this->headers)) {
164✔
455
            return $curlOptions;
77✔
456
        }
457

458
        $set = [];
90✔
459

460
        foreach (array_keys($this->headers) as $name) {
90✔
461
            $set[] = $name . ': ' . $this->getHeaderLine($name);
90✔
462
        }
463

464
        $curlOptions[CURLOPT_HTTPHEADER] = $set;
90✔
465

466
        return $curlOptions;
90✔
467
    }
468

469
    /**
470
     * Apply method
471
     */
472
    protected function applyMethod(string $method, array $curlOptions): array
473
    {
474
        $this->method                       = $method;
164✔
475
        $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
164✔
476

477
        $size = strlen($this->body ?? '');
164✔
478

479
        // Have content?
480
        if ($size > 0) {
164✔
481
            return $this->applyBody($curlOptions);
10✔
482
        }
483

484
        if ($method === Method::PUT || $method === Method::POST) {
155✔
485
            // See http://tools.ietf.org/html/rfc7230#section-3.3.2
486
            if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
55✔
487
                $this->setHeader('Content-Length', '0');
43✔
488
            }
489
        } elseif ($method === 'HEAD') {
104✔
490
            $curlOptions[CURLOPT_NOBODY] = 1;
2✔
491
        }
492

493
        return $curlOptions;
155✔
494
    }
495

496
    /**
497
     * Apply body
498
     */
499
    protected function applyBody(array $curlOptions = []): array
500
    {
501
        if (! empty($this->body)) {
10✔
502
            $curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody();
10✔
503
        }
504

505
        return $curlOptions;
10✔
506
    }
507

508
    /**
509
     * Parses the header retrieved from the cURL response into
510
     * our Response object.
511
     *
512
     * @return void
513
     */
514
    protected function setResponseHeaders(array $headers = [])
515
    {
516
        foreach ($headers as $header) {
28✔
517
            if (($pos = strpos($header, ':')) !== false) {
28✔
518
                $title = trim(substr($header, 0, $pos));
24✔
519
                $value = trim(substr($header, $pos + 1));
24✔
520

521
                if ($this->response instanceof Response) {
24✔
522
                    $this->response->addHeader($title, $value);
24✔
523
                } else {
UNCOV
524
                    $this->response->setHeader($title, $value);
×
525
                }
526
            } elseif (str_starts_with($header, 'HTTP')) {
26✔
527
                preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
26✔
528

529
                if (isset($matches[1])) {
26✔
530
                    $this->response->setProtocolVersion($matches[1]);
22✔
531
                }
532

533
                if (isset($matches[2])) {
26✔
534
                    $this->response->setStatusCode((int) $matches[2], $matches[3] ?? null);
22✔
535
                }
536
            }
537
        }
538
    }
539

540
    /**
541
     * Set CURL options
542
     *
543
     * @return array
544
     *
545
     * @throws InvalidArgumentException
546
     */
547
    protected function setCURLOptions(array $curlOptions = [], array $config = [])
548
    {
549
        // Auth Headers
550
        if (! empty($config['auth'])) {
168✔
551
            $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
10✔
552

553
            if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
10✔
554
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
4✔
555
            } else {
556
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
6✔
557
            }
558
        }
559

560
        // Certificate
561
        if (! empty($config['cert'])) {
168✔
562
            $cert = $config['cert'];
6✔
563

564
            if (is_array($cert)) {
6✔
565
                $curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
2✔
566
                $cert                               = $cert[0];
2✔
567
            }
568

569
            if (! is_file($cert)) {
6✔
570
                throw HTTPException::forSSLCertNotFound($cert);
2✔
571
            }
572

573
            $curlOptions[CURLOPT_SSLCERT] = $cert;
4✔
574
        }
575

576
        // SSL Verification
577
        if (isset($config['verify'])) {
166✔
578
            if (is_string($config['verify'])) {
166✔
579
                $file = realpath($config['verify']) ?: $config['verify'];
4✔
580

581
                if (! is_file($file)) {
4✔
582
                    throw HTTPException::forInvalidSSLKey($config['verify']);
2✔
583
                }
584

585
                $curlOptions[CURLOPT_CAINFO]         = $file;
2✔
586
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = true;
2✔
587
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = 2;
2✔
588
            } elseif (is_bool($config['verify'])) {
162✔
589
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify'];
162✔
590
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0;
162✔
591
            }
592
        }
593

594
        // Proxy
595
        if (isset($config['proxy'])) {
164✔
596
            $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
2✔
597
            $curlOptions[CURLOPT_PROXY]           = $config['proxy'];
2✔
598
        }
599

600
        // Debug
601
        if ($config['debug']) {
164✔
602
            $curlOptions[CURLOPT_VERBOSE] = 1;
4✔
603
            $curlOptions[CURLOPT_STDERR]  = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
4✔
604
        }
605

606
        // Decode Content
607
        if (! empty($config['decode_content'])) {
164✔
608
            $accept = $this->getHeaderLine('Accept-Encoding');
4✔
609

610
            if ($accept !== '') {
4✔
611
                $curlOptions[CURLOPT_ENCODING] = $accept;
2✔
612
            } else {
613
                $curlOptions[CURLOPT_ENCODING]   = '';
2✔
614
                $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
2✔
615
            }
616
        }
617

618
        // Allow Redirects
619
        if (array_key_exists('allow_redirects', $config)) {
164✔
620
            $settings = $this->redirectDefaults;
12✔
621

622
            if (is_array($config['allow_redirects'])) {
12✔
623
                $settings = array_merge($settings, $config['allow_redirects']);
2✔
624
            }
625

626
            if ($config['allow_redirects'] === false) {
12✔
627
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 0;
2✔
628
            } else {
629
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 1;
10✔
630
                $curlOptions[CURLOPT_MAXREDIRS]      = $settings['max'];
10✔
631

632
                if ($settings['strict'] === true) {
10✔
633
                    $curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4;
10✔
634
                }
635

636
                $protocols = 0;
10✔
637

638
                foreach ($settings['protocols'] as $proto) {
10✔
639
                    $protocols += constant('CURLPROTO_' . strtoupper($proto));
10✔
640
                }
641

642
                $curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols;
10✔
643
            }
644
        }
645

646
        // DNS Cache Timeout
647
        if (isset($config['dns_cache_timeout']) && is_numeric($config['dns_cache_timeout']) && $config['dns_cache_timeout'] >= -1) {
164✔
648
            $curlOptions[CURLOPT_DNS_CACHE_TIMEOUT] = (int) $config['dns_cache_timeout'];
12✔
649
        }
650

651
        // Fresh Connect (default true)
652
        $curlOptions[CURLOPT_FRESH_CONNECT] = isset($config['fresh_connect']) && is_bool($config['fresh_connect'])
164✔
653
            ? $config['fresh_connect']
2✔
654
            : true;
162✔
655

656
        // Timeout
657
        $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
164✔
658

659
        // Connection Timeout
660
        $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
164✔
661

662
        // Post Data - application/x-www-form-urlencoded
663
        if (! empty($config['form_params']) && is_array($config['form_params'])) {
164✔
664
            $postFields                      = http_build_query($config['form_params']);
10✔
665
            $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
10✔
666

667
            // Ensure content-length is set, since CURL doesn't seem to
668
            // calculate it when HTTPHEADER is set.
669
            $this->setHeader('Content-Length', (string) strlen($postFields));
10✔
670
            $this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
10✔
671
        }
672

673
        // Post Data - multipart/form-data
674
        if (! empty($config['multipart']) && is_array($config['multipart'])) {
164✔
675
            // setting the POSTFIELDS option automatically sets multipart
676
            $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
4✔
677
        }
678

679
        // HTTP Errors
680
        $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
164✔
681

682
        // JSON
683
        if (isset($config['json'])) {
164✔
684
            // Will be set as the body in `applyBody()`
685
            $json = json_encode($config['json']);
4✔
686
            $this->setBody($json);
4✔
687
            $this->setHeader('Content-Type', 'application/json');
4✔
688
            $this->setHeader('Content-Length', (string) strlen($json));
4✔
689
        }
690

691
        // Resolve IP
692
        if (array_key_exists('force_ip_resolve', $config)) {
164✔
693
            $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
6✔
694
                'v4'    => CURL_IPRESOLVE_V4,
2✔
695
                'v6'    => CURL_IPRESOLVE_V6,
2✔
696
                default => CURL_IPRESOLVE_WHATEVER,
2✔
697
            };
6✔
698
        }
699

700
        // version
701
        if (! empty($config['version'])) {
164✔
702
            $version = sprintf('%.1F', $config['version']);
10✔
703
            if ($version === '1.0') {
10✔
704
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
4✔
705
            } elseif ($version === '1.1') {
8✔
706
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
4✔
707
            } elseif ($version === '2.0') {
6✔
708
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
4✔
709
            } elseif ($version === '3.0') {
2✔
710
                if (! defined('CURL_HTTP_VERSION_3')) {
2✔
711
                    define('CURL_HTTP_VERSION_3', 30);
1✔
712
                }
713

714
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3;
2✔
715
            }
716
        }
717

718
        // Cookie
719
        if (isset($config['cookie'])) {
164✔
720
            $curlOptions[CURLOPT_COOKIEJAR]  = $config['cookie'];
2✔
721
            $curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
2✔
722
        }
723

724
        // User Agent
725
        if (isset($config['user_agent'])) {
164✔
726
            $curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
4✔
727
        }
728

729
        return $curlOptions;
164✔
730
    }
731

732
    /**
733
     * Does the actual work of initializing cURL, setting the options,
734
     * and grabbing the output.
735
     *
736
     * @codeCoverageIgnore
737
     */
738
    protected function sendRequest(array $curlOptions = []): string
739
    {
740
        $ch = curl_init();
2✔
741

742
        curl_setopt_array($ch, $curlOptions);
2✔
743

744
        // Send the request and wait for a response.
745
        $output = curl_exec($ch);
2✔
746

747
        if ($output === false) {
2✔
UNCOV
748
            throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
×
749
        }
750

751
        curl_close($ch);
2✔
752

753
        return $output;
2✔
754
    }
755

756
    private function handleRedirectHeaders(string $output, string $breakString): string
757
    {
758
        // Strip out multiple redirect header sections
759
        while (preg_match('/^HTTP\/\d(?:\.\d)? 3\d\d/', $output)) {
10✔
760
            $breakStringPos        = strpos($output, $breakString);
4✔
761
            $redirectHeaderSection = substr($output, 0, $breakStringPos);
4✔
762
            $redirectHeaders       = explode("\n", $redirectHeaderSection);
4✔
763
            $locationHeaderFound   = false;
4✔
764

765
            foreach ($redirectHeaders as $header) {
4✔
766
                if (str_starts_with(strtolower($header), 'location:')) {
4✔
767
                    $locationHeaderFound = true;
2✔
768
                    break;
2✔
769
                }
770
            }
771

772
            if ($locationHeaderFound) {
4✔
773
                $output = substr($output, $breakStringPos + 4);
2✔
774
            } else {
775
                break;
2✔
776
            }
777
        }
778

779
        return $output;
10✔
780
    }
781
}
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