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

codeigniter4 / CodeIgniter4 / 15087641592

17 May 2025 05:49PM UTC coverage: 84.296% (-0.02%) from 84.32%
15087641592

Pull #9567

github

web-flow
Merge b1e65cc43 into 551c56ddd
Pull Request #9567: test: fix `PublisherInputTest::testAddUri(s)` failing due to rate limiting

20768 of 24637 relevant lines covered (84.3%)

191.33 hits per line

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

96.28
/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')) {
153✔
116
            throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
×
117
        }
118

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

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

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

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

130
        $this->config = $this->defaultConfig;
153✔
131
        $this->parseOptions($options);
153✔
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;
138✔
143

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

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

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

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

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

156
        return $this->response;
134✔
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   = [];
67✔
168
        $this->headerMap = [];
67✔
169

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

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

176
        // Set the default options for next request
177
        $this->parseOptions($this->defaultOptions);
67✔
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)) {
153✔
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'])) {
153✔
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)) {
153✔
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)) {
153✔
315
            $this->setBody($options['body']);
2✔
316
            unset($options['body']);
2✔
317
        }
318

319
        foreach ($options as $key => $value) {
153✔
320
            $this->config[$key] = $value;
76✔
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, '://')) {
140✔
332
            return $url;
72✔
333
        }
334

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

337
        // Create the string instead of casting to prevent baseURL muddling
338
        return URI::createURIString(
68✔
339
            $uri->getScheme(),
68✔
340
            $uri->getAuthority(),
68✔
341
            $uri->getPath(),
68✔
342
            $uri->getQuery(),
68✔
343
            $uri->getFragment(),
68✔
344
        );
68✔
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 = [];
140✔
356

357
        if (! empty($this->config['query']) && is_array($this->config['query'])) {
140✔
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;
140✔
366
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
140✔
367
        $curlOptions[CURLOPT_HEADER]         = true;
140✔
368
        $curlOptions[CURLOPT_FRESH_CONNECT]  = true;
140✔
369
        // Disable @file uploads in post data.
370
        $curlOptions[CURLOPT_SAFE_UPLOAD] = true;
140✔
371

372
        $curlOptions = $this->setCURLOptions($curlOptions, $this->config);
140✔
373
        $curlOptions = $this->applyMethod($method, $curlOptions);
136✔
374
        $curlOptions = $this->applyRequestHeaders($curlOptions);
136✔
375

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

381
        $output = $this->sendRequest($curlOptions);
136✔
382

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

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

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

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

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

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

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

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

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

419
        return $this->response;
136✔
420
    }
421

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

431
        $set = [];
72✔
432

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

437
        $curlOptions[CURLOPT_HTTPHEADER] = $set;
72✔
438

439
        return $curlOptions;
72✔
440
    }
441

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

450
        $size = strlen($this->body ?? '');
136✔
451

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

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

466
        return $curlOptions;
127✔
467
    }
468

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

478
        return $curlOptions;
10✔
479
    }
480

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

609
                $protocols = 0;
10✔
610

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

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

619
        // Timeout
620
        $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
136✔
621

622
        // Connection Timeout
623
        $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
136✔
624

625
        // Post Data - application/x-www-form-urlencoded
626
        if (! empty($config['form_params']) && is_array($config['form_params'])) {
136✔
627
            $postFields                      = http_build_query($config['form_params']);
10✔
628
            $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
10✔
629

630
            // Ensure content-length is set, since CURL doesn't seem to
631
            // calculate it when HTTPHEADER is set.
632
            $this->setHeader('Content-Length', (string) strlen($postFields));
10✔
633
            $this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
10✔
634
        }
635

636
        // Post Data - multipart/form-data
637
        if (! empty($config['multipart']) && is_array($config['multipart'])) {
136✔
638
            // setting the POSTFIELDS option automatically sets multipart
639
            $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
4✔
640
        }
641

642
        // HTTP Errors
643
        $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
136✔
644

645
        // JSON
646
        if (isset($config['json'])) {
136✔
647
            // Will be set as the body in `applyBody()`
648
            $json = json_encode($config['json']);
4✔
649
            $this->setBody($json);
4✔
650
            $this->setHeader('Content-Type', 'application/json');
4✔
651
            $this->setHeader('Content-Length', (string) strlen($json));
4✔
652
        }
653

654
        // Resolve IP
655
        if (array_key_exists('force_ip_resolve', $config)) {
136✔
656
            $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
6✔
657
                'v4'    => CURL_IPRESOLVE_V4,
2✔
658
                'v6'    => CURL_IPRESOLVE_V6,
2✔
659
                default => CURL_IPRESOLVE_WHATEVER,
2✔
660
            };
6✔
661
        }
662

663
        // version
664
        if (! empty($config['version'])) {
136✔
665
            $version = sprintf('%.1F', $config['version']);
10✔
666
            if ($version === '1.0') {
10✔
667
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
4✔
668
            } elseif ($version === '1.1') {
8✔
669
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
4✔
670
            } elseif ($version === '2.0') {
6✔
671
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
4✔
672
            } elseif ($version === '3.0') {
2✔
673
                if (! defined('CURL_HTTP_VERSION_3')) {
2✔
674
                    define('CURL_HTTP_VERSION_3', 30);
1✔
675
                }
676

677
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3;
2✔
678
            }
679
        }
680

681
        // Cookie
682
        if (isset($config['cookie'])) {
136✔
683
            $curlOptions[CURLOPT_COOKIEJAR]  = $config['cookie'];
2✔
684
            $curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
2✔
685
        }
686

687
        // User Agent
688
        if (isset($config['user_agent'])) {
136✔
689
            $curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
4✔
690
        }
691

692
        return $curlOptions;
136✔
693
    }
694

695
    /**
696
     * Does the actual work of initializing cURL, setting the options,
697
     * and grabbing the output.
698
     *
699
     * @codeCoverageIgnore
700
     */
701
    protected function sendRequest(array $curlOptions = []): string
702
    {
703
        $ch = curl_init();
×
704

705
        curl_setopt_array($ch, $curlOptions);
×
706

707
        // Send the request and wait for a response.
708
        $output = curl_exec($ch);
×
709

710
        if ($output === false) {
×
711
            throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
×
712
        }
713

714
        curl_close($ch);
×
715

716
        return $output;
×
717
    }
718

719
    private function handleRedirectHeaders(string $output, string $breakString): string
720
    {
721
        // Strip out multiple redirect header sections
722
        while (preg_match('/^HTTP\/\d(?:\.\d)? 3\d\d/', $output)) {
10✔
723
            $breakStringPos        = strpos($output, $breakString);
4✔
724
            $redirectHeaderSection = substr($output, 0, $breakStringPos);
4✔
725
            $redirectHeaders       = explode("\n", $redirectHeaderSection);
4✔
726
            $locationHeaderFound   = false;
4✔
727

728
            foreach ($redirectHeaders as $header) {
4✔
729
                if (str_starts_with(strtolower($header), 'location:')) {
4✔
730
                    $locationHeaderFound = true;
2✔
731
                    break;
2✔
732
                }
733
            }
734

735
            if ($locationHeaderFound) {
4✔
736
                $output = substr($output, $breakStringPos + 4);
2✔
737
            } else {
738
                break;
2✔
739
            }
740
        }
741

742
        return $output;
10✔
743
    }
744
}
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