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

codeigniter4 / CodeIgniter4 / 15161682795

21 May 2025 12:04PM UTC coverage: 84.109% (-0.2%) from 84.265%
15161682795

push

github

paulbalandan
Merge branch 'develop' into 4.7

43 of 82 new or added lines in 4 files covered. (52.44%)

6 existing lines in 1 file now uncovered.

20827 of 24762 relevant lines covered (84.11%)

191.06 hits per line

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

96.34
/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

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

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

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

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

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

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

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

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

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

104
    /**
105
     * Takes an array of options to set the following possible class properties:
106
     *
107
     *  - baseURI
108
     *  - timeout
109
     *  - any other request options to use as defaults.
110
     *
111
     * @param array<string, mixed> $options
112
     */
113
    public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
114
    {
115
        if (! function_exists('curl_version')) {
175✔
116
            throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
×
117
        }
118

119
        parent::__construct(Method::GET, $uri);
175✔
120

121
        $this->responseOrig = $response ?? new Response($config);
175✔
122
        // Remove the default Content-Type header.
123
        $this->responseOrig->removeHeader('Content-Type');
175✔
124

125
        $this->baseURI        = $uri->useRawQueryString();
175✔
126
        $this->defaultOptions = $options;
175✔
127

128
        $this->shareOptions = config(ConfigCURLRequest::class)->shareOptions ?? true;
175✔
129

130
        $this->config = $this->defaultConfig;
175✔
131
        $this->parseOptions($options);
175✔
132
    }
133

134
    /**
135
     * Sends an HTTP request to the specified $url. If this is a relative
136
     * URL, it will be merged with $this->baseURI to form a complete URL.
137
     *
138
     * @param string $method HTTP method
139
     */
140
    public function request($method, string $url, array $options = []): ResponseInterface
141
    {
142
        $this->response = clone $this->responseOrig;
160✔
143

144
        $this->parseOptions($options);
160✔
145

146
        $url = $this->prepareURL($url);
160✔
147

148
        $method = esc(strip_tags($method));
160✔
149

150
        $this->send($method, $url);
160✔
151

152
        if ($this->shareOptions === false) {
156✔
153
            $this->resetOptions();
78✔
154
        }
155

156
        return $this->response;
156✔
157
    }
158

159
    /**
160
     * Reset all options to default.
161
     *
162
     * @return void
163
     */
164
    protected function resetOptions()
165
    {
166
        // Reset headers
167
        $this->headers   = [];
78✔
168
        $this->headerMap = [];
78✔
169

170
        // Reset body
171
        $this->body = null;
78✔
172

173
        // Reset configs
174
        $this->config = $this->defaultConfig;
78✔
175

176
        // Set the default options for next request
177
        $this->parseOptions($this->defaultOptions);
78✔
178
    }
179

180
    /**
181
     * Convenience method for sending a GET request.
182
     */
183
    public function get(string $url, array $options = []): ResponseInterface
184
    {
185
        return $this->request(Method::GET, $url, $options);
36✔
186
    }
187

188
    /**
189
     * Convenience method for sending a DELETE request.
190
     */
191
    public function delete(string $url, array $options = []): ResponseInterface
192
    {
193
        return $this->request('DELETE', $url, $options);
2✔
194
    }
195

196
    /**
197
     * Convenience method for sending a HEAD request.
198
     */
199
    public function head(string $url, array $options = []): ResponseInterface
200
    {
201
        return $this->request('HEAD', $url, $options);
2✔
202
    }
203

204
    /**
205
     * Convenience method for sending an OPTIONS request.
206
     */
207
    public function options(string $url, array $options = []): ResponseInterface
208
    {
209
        return $this->request('OPTIONS', $url, $options);
2✔
210
    }
211

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

220
    /**
221
     * Convenience method for sending a POST request.
222
     */
223
    public function post(string $url, array $options = []): ResponseInterface
224
    {
225
        return $this->request(Method::POST, $url, $options);
16✔
226
    }
227

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

236
    /**
237
     * Set the HTTP Authentication.
238
     *
239
     * @param string $type basic or digest
240
     *
241
     * @return $this
242
     */
243
    public function setAuth(string $username, string $password, string $type = 'basic')
244
    {
245
        $this->config['auth'] = [
4✔
246
            $username,
4✔
247
            $password,
4✔
248
            $type,
4✔
249
        ];
4✔
250

251
        return $this;
4✔
252
    }
253

254
    /**
255
     * Set form data to be sent.
256
     *
257
     * @param bool $multipart Set TRUE if you are sending CURLFiles
258
     *
259
     * @return $this
260
     */
261
    public function setForm(array $params, bool $multipart = false)
262
    {
263
        if ($multipart) {
2✔
264
            $this->config['multipart'] = $params;
2✔
265
        } else {
266
            $this->config['form_params'] = $params;
2✔
267
        }
268

269
        return $this;
2✔
270
    }
271

272
    /**
273
     * Set JSON data to be sent.
274
     *
275
     * @param array|bool|float|int|object|string|null $data
276
     *
277
     * @return $this
278
     */
279
    public function setJSON($data)
280
    {
281
        $this->config['json'] = $data;
2✔
282

283
        return $this;
2✔
284
    }
285

286
    /**
287
     * Sets the correct settings based on the options array
288
     * passed in.
289
     *
290
     * @return void
291
     */
292
    protected function parseOptions(array $options)
293
    {
294
        if (array_key_exists('baseURI', $options)) {
175✔
295
            $this->baseURI = $this->baseURI->setURI($options['baseURI']);
44✔
296
            unset($options['baseURI']);
44✔
297
        }
298

299
        if (array_key_exists('headers', $options) && is_array($options['headers'])) {
175✔
300
            foreach ($options['headers'] as $name => $value) {
6✔
301
                $this->setHeader($name, $value);
6✔
302
            }
303

304
            unset($options['headers']);
6✔
305
        }
306

307
        if (array_key_exists('delay', $options)) {
175✔
308
            // Convert from the milliseconds passed in
309
            // to the seconds that sleep requires.
310
            $this->delay = (float) $options['delay'] / 1000;
24✔
311
            unset($options['delay']);
24✔
312
        }
313

314
        if (array_key_exists('body', $options)) {
175✔
315
            $this->setBody($options['body']);
2✔
316
            unset($options['body']);
2✔
317
        }
318

319
        foreach ($options as $key => $value) {
175✔
320
            $this->config[$key] = $value;
96✔
321
        }
322
    }
323

324
    /**
325
     * If the $url is a relative URL, will attempt to create
326
     * a full URL by prepending $this->baseURI to it.
327
     */
328
    protected function prepareURL(string $url): string
329
    {
330
        // If it's a full URI, then we have nothing to do here...
331
        if (str_contains($url, '://')) {
162✔
332
            return $url;
76✔
333
        }
334

335
        $uri = $this->baseURI->resolveRelativeURI($url);
86✔
336

337
        // Create the string instead of casting to prevent baseURL muddling
338
        return URI::createURIString(
86✔
339
            $uri->getScheme(),
86✔
340
            $uri->getAuthority(),
86✔
341
            $uri->getPath(),
86✔
342
            $uri->getQuery(),
86✔
343
            $uri->getFragment(),
86✔
344
        );
86✔
345
    }
346

347
    /**
348
     * Fires the actual cURL request.
349
     *
350
     * @return ResponseInterface
351
     */
352
    public function send(string $method, string $url)
353
    {
354
        // Reset our curl options so we're on a fresh slate.
355
        $curlOptions = [];
162✔
356

357
        if (! empty($this->config['query']) && is_array($this->config['query'])) {
162✔
358
            // This is likely too naive a solution.
359
            // Should look into handling when $url already
360
            // has query vars on it.
361
            $url .= '?' . http_build_query($this->config['query']);
2✔
362
            unset($this->config['query']);
2✔
363
        }
364

365
        $curlOptions[CURLOPT_URL]            = $url;
162✔
366
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
162✔
367
        $curlOptions[CURLOPT_HEADER]         = true;
162✔
368
        // Disable @file uploads in post data.
369
        $curlOptions[CURLOPT_SAFE_UPLOAD] = true;
162✔
370

371
        $curlOptions = $this->setCURLOptions($curlOptions, $this->config);
162✔
372
        $curlOptions = $this->applyMethod($method, $curlOptions);
158✔
373
        $curlOptions = $this->applyRequestHeaders($curlOptions);
158✔
374

375
        // Do we need to delay this request?
376
        if ($this->delay > 0) {
158✔
377
            usleep((int) $this->delay * 1_000_000);
22✔
378
        }
379

380
        $output = $this->sendRequest($curlOptions);
158✔
381

382
        // Set the string we want to break our response from
383
        $breakString = "\r\n\r\n";
158✔
384

385
        if (isset($this->config['allow_redirects']) && $this->config['allow_redirects'] !== false) {
158✔
386
            $output = $this->handleRedirectHeaders($output, $breakString);
10✔
387
        }
388

389
        while (str_starts_with($output, 'HTTP/1.1 100 Continue')) {
158✔
390
            $output = substr($output, strpos($output, $breakString) + 4);
6✔
391
        }
392

393
        if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) {
158✔
394
            $output = substr($output, strpos($output, $breakString) + 4);
4✔
395
        }
396

397
        // If request and response have Digest
398
        if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) {
158✔
399
            $output = substr($output, strpos($output, $breakString) + 4);
4✔
400
        }
401

402
        // Split out our headers and body
403
        $break = strpos($output, $breakString);
158✔
404

405
        if ($break !== false) {
158✔
406
            // Our headers
407
            $headers = explode("\n", substr($output, 0, $break));
26✔
408

409
            $this->setResponseHeaders($headers);
26✔
410

411
            // Our body
412
            $body = substr($output, $break + 4);
26✔
413
            $this->response->setBody($body);
26✔
414
        } else {
415
            $this->response->setBody($output);
132✔
416
        }
417

418
        return $this->response;
158✔
419
    }
420

421
    /**
422
     * Adds $this->headers to the cURL request.
423
     */
424
    protected function applyRequestHeaders(array $curlOptions = []): array
425
    {
426
        if (empty($this->headers)) {
158✔
427
            return $curlOptions;
71✔
428
        }
429

430
        $set = [];
90✔
431

432
        foreach (array_keys($this->headers) as $name) {
90✔
433
            $set[] = $name . ': ' . $this->getHeaderLine($name);
90✔
434
        }
435

436
        $curlOptions[CURLOPT_HTTPHEADER] = $set;
90✔
437

438
        return $curlOptions;
90✔
439
    }
440

441
    /**
442
     * Apply method
443
     */
444
    protected function applyMethod(string $method, array $curlOptions): array
445
    {
446
        $this->method                       = $method;
158✔
447
        $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
158✔
448

449
        $size = strlen($this->body ?? '');
158✔
450

451
        // Have content?
452
        if ($size > 0) {
158✔
453
            return $this->applyBody($curlOptions);
10✔
454
        }
455

456
        if ($method === Method::PUT || $method === Method::POST) {
149✔
457
            // See http://tools.ietf.org/html/rfc7230#section-3.3.2
458
            if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
55✔
459
                $this->setHeader('Content-Length', '0');
43✔
460
            }
461
        } elseif ($method === 'HEAD') {
98✔
462
            $curlOptions[CURLOPT_NOBODY] = 1;
2✔
463
        }
464

465
        return $curlOptions;
149✔
466
    }
467

468
    /**
469
     * Apply body
470
     */
471
    protected function applyBody(array $curlOptions = []): array
472
    {
473
        if (! empty($this->body)) {
10✔
474
            $curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody();
10✔
475
        }
476

477
        return $curlOptions;
10✔
478
    }
479

480
    /**
481
     * Parses the header retrieved from the cURL response into
482
     * our Response object.
483
     *
484
     * @return void
485
     */
486
    protected function setResponseHeaders(array $headers = [])
487
    {
488
        foreach ($headers as $header) {
26✔
489
            if (($pos = strpos($header, ':')) !== false) {
26✔
490
                $title = trim(substr($header, 0, $pos));
22✔
491
                $value = trim(substr($header, $pos + 1));
22✔
492

493
                if ($this->response instanceof Response) {
22✔
494
                    $this->response->addHeader($title, $value);
22✔
495
                } else {
496
                    $this->response->setHeader($title, $value);
×
497
                }
498
            } elseif (str_starts_with($header, 'HTTP')) {
24✔
499
                preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
24✔
500

501
                if (isset($matches[1])) {
24✔
502
                    $this->response->setProtocolVersion($matches[1]);
20✔
503
                }
504

505
                if (isset($matches[2])) {
24✔
506
                    $this->response->setStatusCode((int) $matches[2], $matches[3] ?? null);
20✔
507
                }
508
            }
509
        }
510
    }
511

512
    /**
513
     * Set CURL options
514
     *
515
     * @return array
516
     *
517
     * @throws InvalidArgumentException
518
     */
519
    protected function setCURLOptions(array $curlOptions = [], array $config = [])
520
    {
521
        // Auth Headers
522
        if (! empty($config['auth'])) {
162✔
523
            $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
10✔
524

525
            if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
10✔
526
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
4✔
527
            } else {
528
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
6✔
529
            }
530
        }
531

532
        // Certificate
533
        if (! empty($config['cert'])) {
162✔
534
            $cert = $config['cert'];
6✔
535

536
            if (is_array($cert)) {
6✔
537
                $curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
2✔
538
                $cert                               = $cert[0];
2✔
539
            }
540

541
            if (! is_file($cert)) {
6✔
542
                throw HTTPException::forSSLCertNotFound($cert);
2✔
543
            }
544

545
            $curlOptions[CURLOPT_SSLCERT] = $cert;
4✔
546
        }
547

548
        // SSL Verification
549
        if (isset($config['verify'])) {
160✔
550
            if (is_string($config['verify'])) {
160✔
551
                $file = realpath($config['verify']) ?: $config['verify'];
4✔
552

553
                if (! is_file($file)) {
4✔
554
                    throw HTTPException::forInvalidSSLKey($config['verify']);
2✔
555
                }
556

557
                $curlOptions[CURLOPT_CAINFO]         = $file;
2✔
558
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = true;
2✔
559
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = 2;
2✔
560
            } elseif (is_bool($config['verify'])) {
156✔
561
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify'];
156✔
562
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0;
156✔
563
            }
564
        }
565

566
        // Proxy
567
        if (isset($config['proxy'])) {
158✔
568
            $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
2✔
569
            $curlOptions[CURLOPT_PROXY]           = $config['proxy'];
2✔
570
        }
571

572
        // Debug
573
        if ($config['debug']) {
158✔
574
            $curlOptions[CURLOPT_VERBOSE] = 1;
4✔
575
            $curlOptions[CURLOPT_STDERR]  = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
4✔
576
        }
577

578
        // Decode Content
579
        if (! empty($config['decode_content'])) {
158✔
580
            $accept = $this->getHeaderLine('Accept-Encoding');
4✔
581

582
            if ($accept !== '') {
4✔
583
                $curlOptions[CURLOPT_ENCODING] = $accept;
2✔
584
            } else {
585
                $curlOptions[CURLOPT_ENCODING]   = '';
2✔
586
                $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
2✔
587
            }
588
        }
589

590
        // Allow Redirects
591
        if (array_key_exists('allow_redirects', $config)) {
158✔
592
            $settings = $this->redirectDefaults;
12✔
593

594
            if (is_array($config['allow_redirects'])) {
12✔
595
                $settings = array_merge($settings, $config['allow_redirects']);
2✔
596
            }
597

598
            if ($config['allow_redirects'] === false) {
12✔
599
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 0;
2✔
600
            } else {
601
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 1;
10✔
602
                $curlOptions[CURLOPT_MAXREDIRS]      = $settings['max'];
10✔
603

604
                if ($settings['strict'] === true) {
10✔
605
                    $curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4;
10✔
606
                }
607

608
                $protocols = 0;
10✔
609

610
                foreach ($settings['protocols'] as $proto) {
10✔
611
                    $protocols += constant('CURLPROTO_' . strtoupper($proto));
10✔
612
                }
613

614
                $curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols;
10✔
615
            }
616
        }
617

618
        // DNS Cache Timeout
619
        if (isset($config['dns_cache_timeout']) && is_numeric($config['dns_cache_timeout']) && $config['dns_cache_timeout'] >= -1) {
158✔
620
            $curlOptions[CURLOPT_DNS_CACHE_TIMEOUT] = (int) $config['dns_cache_timeout'];
12✔
621
        }
622

623
        // Fresh Connect (default true)
624
        $curlOptions[CURLOPT_FRESH_CONNECT] = isset($config['fresh_connect']) && is_bool($config['fresh_connect'])
158✔
625
            ? $config['fresh_connect']
2✔
626
            : true;
156✔
627

628
        // Timeout
629
        $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
158✔
630

631
        // Connection Timeout
632
        $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
158✔
633

634
        // Post Data - application/x-www-form-urlencoded
635
        if (! empty($config['form_params']) && is_array($config['form_params'])) {
158✔
636
            $postFields                      = http_build_query($config['form_params']);
10✔
637
            $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
10✔
638

639
            // Ensure content-length is set, since CURL doesn't seem to
640
            // calculate it when HTTPHEADER is set.
641
            $this->setHeader('Content-Length', (string) strlen($postFields));
10✔
642
            $this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
10✔
643
        }
644

645
        // Post Data - multipart/form-data
646
        if (! empty($config['multipart']) && is_array($config['multipart'])) {
158✔
647
            // setting the POSTFIELDS option automatically sets multipart
648
            $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
4✔
649
        }
650

651
        // HTTP Errors
652
        $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
158✔
653

654
        // JSON
655
        if (isset($config['json'])) {
158✔
656
            // Will be set as the body in `applyBody()`
657
            $json = json_encode($config['json']);
4✔
658
            $this->setBody($json);
4✔
659
            $this->setHeader('Content-Type', 'application/json');
4✔
660
            $this->setHeader('Content-Length', (string) strlen($json));
4✔
661
        }
662

663
        // Resolve IP
664
        if (array_key_exists('force_ip_resolve', $config)) {
158✔
665
            $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
6✔
666
                'v4'    => CURL_IPRESOLVE_V4,
2✔
667
                'v6'    => CURL_IPRESOLVE_V6,
2✔
668
                default => CURL_IPRESOLVE_WHATEVER,
2✔
669
            };
6✔
670
        }
671

672
        // version
673
        if (! empty($config['version'])) {
158✔
674
            $version = sprintf('%.1F', $config['version']);
10✔
675
            if ($version === '1.0') {
10✔
676
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
4✔
677
            } elseif ($version === '1.1') {
8✔
678
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
4✔
679
            } elseif ($version === '2.0') {
6✔
680
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
4✔
681
            } elseif ($version === '3.0') {
2✔
682
                if (! defined('CURL_HTTP_VERSION_3')) {
2✔
683
                    define('CURL_HTTP_VERSION_3', 30);
1✔
684
                }
685

686
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3;
2✔
687
            }
688
        }
689

690
        // Cookie
691
        if (isset($config['cookie'])) {
158✔
692
            $curlOptions[CURLOPT_COOKIEJAR]  = $config['cookie'];
2✔
693
            $curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
2✔
694
        }
695

696
        // User Agent
697
        if (isset($config['user_agent'])) {
158✔
698
            $curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
4✔
699
        }
700

701
        return $curlOptions;
158✔
702
    }
703

704
    /**
705
     * Does the actual work of initializing cURL, setting the options,
706
     * and grabbing the output.
707
     *
708
     * @codeCoverageIgnore
709
     */
710
    protected function sendRequest(array $curlOptions = []): string
711
    {
UNCOV
712
        $ch = curl_init();
×
713

UNCOV
714
        curl_setopt_array($ch, $curlOptions);
×
715

716
        // Send the request and wait for a response.
UNCOV
717
        $output = curl_exec($ch);
×
718

UNCOV
719
        if ($output === false) {
×
720
            throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
×
721
        }
722

UNCOV
723
        curl_close($ch);
×
724

UNCOV
725
        return $output;
×
726
    }
727

728
    private function handleRedirectHeaders(string $output, string $breakString): string
729
    {
730
        // Strip out multiple redirect header sections
731
        while (preg_match('/^HTTP\/\d(?:\.\d)? 3\d\d/', $output)) {
10✔
732
            $breakStringPos        = strpos($output, $breakString);
4✔
733
            $redirectHeaderSection = substr($output, 0, $breakStringPos);
4✔
734
            $redirectHeaders       = explode("\n", $redirectHeaderSection);
4✔
735
            $locationHeaderFound   = false;
4✔
736

737
            foreach ($redirectHeaders as $header) {
4✔
738
                if (str_starts_with(strtolower($header), 'location:')) {
4✔
739
                    $locationHeaderFound = true;
2✔
740
                    break;
2✔
741
                }
742
            }
743

744
            if ($locationHeaderFound) {
4✔
745
                $output = substr($output, $breakStringPos + 4);
2✔
746
            } else {
747
                break;
2✔
748
            }
749
        }
750

751
        return $output;
10✔
752
    }
753
}
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