Skip to content

Commit ccc1cc9

Browse files
committed
refactor(HTTP): split setCURLOptions into category helper methods to reduce complexity
1 parent ad8526f commit ccc1cc9

3 files changed

Lines changed: 270 additions & 50 deletions

File tree

system/HTTP/CURLRequest.php

Lines changed: 170 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -526,21 +526,50 @@ protected function setResponseHeaders(array $headers = [])
526526
*/
527527
protected function setCURLOptions(array $curlOptions = [], array $config = [])
528528
{
529-
// Auth Headers
530-
if (! empty($config['auth'])) {
531-
$curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
529+
$curlOptions = $this->applyAuthOptions($curlOptions, $config);
530+
$curlOptions = $this->applySslOptions($curlOptions, $config);
531+
$curlOptions = $this->applyProxyOptions($curlOptions, $config);
532+
$curlOptions = $this->applyDebugOptions($curlOptions, $config);
533+
$curlOptions = $this->applyRedirectOptions($curlOptions, $config);
534+
$curlOptions = $this->applyConnectionOptions($curlOptions, $config);
535+
$curlOptions = $this->applyBodyOptions($curlOptions, $config);
536+
$curlOptions = $this->applyResponseOptions($curlOptions, $config);
537+
$curlOptions = $this->applyProtocolOptions($curlOptions, $config);
538+
539+
return $this->applyClientOptions($curlOptions, $config);
540+
}
532541

533-
if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
534-
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
535-
} else {
536-
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
537-
}
542+
/**
543+
* @param array<int, mixed> $curlOptions
544+
* @param array<string, mixed> $config
545+
*
546+
* @return array<int, mixed>
547+
*/
548+
private function applyAuthOptions(array $curlOptions, array $config): array
549+
{
550+
// Auth Headers
551+
if (isset($config['auth']) && is_array($config['auth']) && count($config['auth']) >= 2) {
552+
$curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
553+
$curlOptions[CURLOPT_HTTPAUTH] = (isset($config['auth'][2]) && $config['auth'][2] !== '' && strtolower($config['auth'][2]) === 'digest')
554+
? CURLAUTH_DIGEST
555+
: CURLAUTH_BASIC;
538556
}
539557

558+
return $curlOptions;
559+
}
560+
561+
/**
562+
* @param array<int, mixed> $curlOptions
563+
* @param array<string, mixed> $config
564+
*
565+
* @return array<int, mixed>
566+
*/
567+
private function applySslOptions(array $curlOptions, array $config): array
568+
{
540569
// Certificate
541-
if (! empty($config['cert'])) {
542-
$cert = $config['cert'];
570+
$cert = $config['cert'] ?? null;
543571

572+
if ((bool) $cert) {
544573
if (is_array($cert)) {
545574
$curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
546575
$cert = $cert[0];
@@ -571,30 +600,51 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
571600
}
572601
}
573602

603+
return $curlOptions;
604+
}
605+
606+
/**
607+
* @param array<int, mixed> $curlOptions
608+
* @param array<string, mixed> $config
609+
*
610+
* @return array<int, mixed>
611+
*/
612+
private function applyProxyOptions(array $curlOptions, array $config): array
613+
{
574614
// Proxy
575615
if (isset($config['proxy'])) {
576616
$curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
577617
$curlOptions[CURLOPT_PROXY] = $config['proxy'];
578618
}
579619

620+
return $curlOptions;
621+
}
622+
623+
/**
624+
* @param array<int, mixed> $curlOptions
625+
* @param array<string, mixed> $config
626+
*
627+
* @return array<int, mixed>
628+
*/
629+
private function applyDebugOptions(array $curlOptions, array $config): array
630+
{
580631
// Debug
581-
if ($config['debug']) {
632+
if ((bool) ($config['debug'] ?? false)) {
582633
$curlOptions[CURLOPT_VERBOSE] = 1;
583634
$curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
584635
}
585636

586-
// Decode Content
587-
if (! empty($config['decode_content'])) {
588-
$accept = $this->getHeaderLine('Accept-Encoding');
589-
590-
if ($accept !== '') {
591-
$curlOptions[CURLOPT_ENCODING] = $accept;
592-
} else {
593-
$curlOptions[CURLOPT_ENCODING] = '';
594-
$curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
595-
}
596-
}
637+
return $curlOptions;
638+
}
597639

640+
/**
641+
* @param array<int, mixed> $curlOptions
642+
* @param array<string, mixed> $config
643+
*
644+
* @return array<int, mixed>
645+
*/
646+
private function applyRedirectOptions(array $curlOptions, array $config): array
647+
{
598648
// Allow Redirects
599649
if (array_key_exists('allow_redirects', $config)) {
600650
$settings = $this->redirectDefaults;
@@ -623,6 +673,17 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
623673
}
624674
}
625675

676+
return $curlOptions;
677+
}
678+
679+
/**
680+
* @param array<int, mixed> $curlOptions
681+
* @param array<string, mixed> $config
682+
*
683+
* @return array<int, mixed>
684+
*/
685+
private function applyConnectionOptions(array $curlOptions, array $config): array
686+
{
626687
// DNS Cache Timeout
627688
if (isset($config['dns_cache_timeout']) && is_numeric($config['dns_cache_timeout']) && $config['dns_cache_timeout'] >= -1) {
628689
$curlOptions[CURLOPT_DNS_CACHE_TIMEOUT] = (int) $config['dns_cache_timeout'];
@@ -634,13 +695,33 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
634695
: true;
635696

636697
// Timeout
637-
$curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
698+
$curlOptions[CURLOPT_TIMEOUT_MS] = (float) ($config['timeout'] ?? 0) * 1000;
638699

639700
// Connection Timeout
640-
$curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
701+
$curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) ($config['connect_timeout'] ?? 150) * 1000;
641702

703+
// Resolve IP
704+
if (array_key_exists('force_ip_resolve', $config)) {
705+
$curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
706+
'v4' => CURL_IPRESOLVE_V4,
707+
'v6' => CURL_IPRESOLVE_V6,
708+
default => CURL_IPRESOLVE_WHATEVER,
709+
};
710+
}
711+
712+
return $curlOptions;
713+
}
714+
715+
/**
716+
* @param array<int, mixed> $curlOptions
717+
* @param array<string, mixed> $config
718+
*
719+
* @return array<int, mixed>
720+
*/
721+
private function applyBodyOptions(array $curlOptions, array $config): array
722+
{
642723
// Post Data - application/x-www-form-urlencoded
643-
if (! empty($config['form_params']) && is_array($config['form_params'])) {
724+
if (isset($config['form_params']) && is_array($config['form_params']) && $config['form_params'] !== []) {
644725
$postFields = http_build_query($config['form_params']);
645726
$curlOptions[CURLOPT_POSTFIELDS] = $postFields;
646727

@@ -651,14 +732,11 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
651732
}
652733

653734
// Post Data - multipart/form-data
654-
if (! empty($config['multipart']) && is_array($config['multipart'])) {
735+
if (isset($config['multipart']) && is_array($config['multipart']) && $config['multipart'] !== []) {
655736
// setting the POSTFIELDS option automatically sets multipart
656737
$curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
657738
}
658739

659-
// HTTP Errors
660-
$curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
661-
662740
// JSON
663741
if (isset($config['json'])) {
664742
// Will be set as the body in `applyBody()`
@@ -668,33 +746,76 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
668746
$this->setHeader('Content-Length', (string) strlen($json));
669747
}
670748

671-
// Resolve IP
672-
if (array_key_exists('force_ip_resolve', $config)) {
673-
$curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
674-
'v4' => CURL_IPRESOLVE_V4,
675-
'v6' => CURL_IPRESOLVE_V6,
676-
default => CURL_IPRESOLVE_WHATEVER,
677-
};
749+
return $curlOptions;
750+
}
751+
752+
/**
753+
* @param array<int, mixed> $curlOptions
754+
* @param array<string, mixed> $config
755+
*
756+
* @return array<int, mixed>
757+
*/
758+
private function applyResponseOptions(array $curlOptions, array $config): array
759+
{
760+
// Decode Content
761+
if ((bool) ($config['decode_content'] ?? false)) {
762+
$accept = $this->getHeaderLine('Accept-Encoding');
763+
764+
if ($accept !== '') {
765+
$curlOptions[CURLOPT_ENCODING] = $accept;
766+
} else {
767+
$curlOptions[CURLOPT_ENCODING] = '';
768+
$curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
769+
}
678770
}
679771

772+
// HTTP Errors
773+
$curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
774+
775+
return $curlOptions;
776+
}
777+
778+
/**
779+
* @param array<int, mixed> $curlOptions
780+
* @param array<string, mixed> $config
781+
*
782+
* @return array<int, mixed>
783+
*/
784+
private function applyProtocolOptions(array $curlOptions, array $config): array
785+
{
680786
// version
681-
if (! empty($config['version'])) {
682-
$version = sprintf('%.1F', $config['version']);
683-
if ($version === '1.0') {
684-
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
685-
} elseif ($version === '1.1') {
686-
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
687-
} elseif ($version === '2.0') {
688-
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
689-
} elseif ($version === '3.0') {
690-
if (! defined('CURL_HTTP_VERSION_3')) {
691-
define('CURL_HTTP_VERSION_3', 30);
692-
}
787+
$version = $config['version'] ?? null;
788+
789+
if ((bool) $version) {
790+
$version = sprintf('%.1F', $version);
791+
792+
if (! defined('CURL_HTTP_VERSION_3')) {
793+
define('CURL_HTTP_VERSION_3', 30);
794+
}
795+
796+
$versions = [
797+
'1.0' => CURL_HTTP_VERSION_1_0,
798+
'1.1' => CURL_HTTP_VERSION_1_1,
799+
'2.0' => CURL_HTTP_VERSION_2_0,
800+
'3.0' => CURL_HTTP_VERSION_3,
801+
];
693802

694-
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3;
803+
if (isset($versions[$version])) {
804+
$curlOptions[CURLOPT_HTTP_VERSION] = $versions[$version];
695805
}
696806
}
697807

808+
return $curlOptions;
809+
}
810+
811+
/**
812+
* @param array<int, mixed> $curlOptions
813+
* @param array<string, mixed> $config
814+
*
815+
* @return array<int, mixed>
816+
*/
817+
private function applyClientOptions(array $curlOptions, array $config): array
818+
{
698819
// Cookie
699820
if (isset($config['cookie'])) {
700821
$curlOptions[CURLOPT_COOKIEJAR] = $config['cookie'];

tests/system/HTTP/CURLRequestTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,4 +1566,103 @@ public function testProxyAndContinueResponses(): void
15661566

15671567
$this->assertSame($testBody, $response->getBody());
15681568
}
1569+
1570+
public function testSetCURLOptions(): void
1571+
{
1572+
$invoker = self::getPrivateMethodInvoker($this->request, 'setCURLOptions');
1573+
1574+
// Test Auth Options
1575+
$options = $invoker([], ['auth' => ['user', 'pass', 'digest']]);
1576+
$this->assertSame('user:pass', $options[CURLOPT_USERPWD]);
1577+
$this->assertSame(CURLAUTH_DIGEST, $options[CURLOPT_HTTPAUTH]);
1578+
1579+
$options2 = $invoker([], ['auth' => ['user', 'pass', 'basic']]);
1580+
$this->assertSame(CURLAUTH_BASIC, $options2[CURLOPT_HTTPAUTH]);
1581+
1582+
// Test SSL Options
1583+
$options3 = $invoker([], ['cert' => __FILE__]);
1584+
$this->assertSame(__FILE__, $options3[CURLOPT_SSLCERT]);
1585+
1586+
$options4 = $invoker([], ['verify' => false]);
1587+
$this->assertFalse($options4[CURLOPT_SSL_VERIFYPEER]);
1588+
$this->assertSame(0, $options4[CURLOPT_SSL_VERIFYHOST]);
1589+
1590+
// Test Proxy Options
1591+
$options5 = $invoker([], ['proxy' => 'http://proxy.example.com']);
1592+
$this->assertTrue($options5[CURLOPT_HTTPPROXYTUNNEL]);
1593+
$this->assertSame('http://proxy.example.com', $options5[CURLOPT_PROXY]);
1594+
1595+
// Test Debug Options
1596+
$options6 = $invoker([], ['debug' => true]);
1597+
$this->assertSame(1, $options6[CURLOPT_VERBOSE]);
1598+
$this->assertIsResource($options6[CURLOPT_STDERR]);
1599+
1600+
// Test Redirect Options
1601+
$options7 = $invoker([], ['allow_redirects' => false]);
1602+
$this->assertSame(0, $options7[CURLOPT_FOLLOWLOCATION]);
1603+
1604+
$options8 = $invoker([], ['allow_redirects' => true]);
1605+
$this->assertSame(1, $options8[CURLOPT_FOLLOWLOCATION]);
1606+
$this->assertSame(5, $options8[CURLOPT_MAXREDIRS]);
1607+
1608+
// Test Connection Options
1609+
$options9 = $invoker([], [
1610+
'dns_cache_timeout' => 120,
1611+
'fresh_connect' => false,
1612+
'timeout' => 10,
1613+
'connect_timeout' => 5,
1614+
'force_ip_resolve' => 'v4',
1615+
]);
1616+
$this->assertSame(120, $options9[CURLOPT_DNS_CACHE_TIMEOUT]);
1617+
$this->assertFalse($options9[CURLOPT_FRESH_CONNECT]);
1618+
$this->assertEqualsWithDelta(10000.0, $options9[CURLOPT_TIMEOUT_MS], PHP_FLOAT_EPSILON);
1619+
$this->assertEqualsWithDelta(5000.0, $options9[CURLOPT_CONNECTTIMEOUT_MS], PHP_FLOAT_EPSILON);
1620+
$this->assertSame(CURL_IPRESOLVE_V4, $options9[CURLOPT_IPRESOLVE]);
1621+
1622+
// Test Body Options (form_params / multipart / json)
1623+
$options10 = $invoker([], ['form_params' => ['foo' => 'bar']]);
1624+
$this->assertSame('foo=bar', $options10[CURLOPT_POSTFIELDS]);
1625+
1626+
$options11 = $invoker([], ['multipart' => ['file' => 'data']]);
1627+
$this->assertSame(['file' => 'data'], $options11[CURLOPT_POSTFIELDS]);
1628+
1629+
// Test Response Options (decode_content / http_errors)
1630+
$options12 = $invoker([], ['decode_content' => true]);
1631+
$this->assertSame('', $options12[CURLOPT_ENCODING]);
1632+
$this->assertSame('Accept-Encoding', $options12[CURLOPT_HTTPHEADER]);
1633+
1634+
// Test Protocol/Misc/Client Options
1635+
$options13 = $invoker([], [
1636+
'http_errors' => false,
1637+
'version' => '2.0',
1638+
'cookie' => 'cookies.txt',
1639+
'user_agent' => 'TestAgent',
1640+
]);
1641+
$this->assertFalse($options13[CURLOPT_FAILONERROR]);
1642+
$this->assertSame(CURL_HTTP_VERSION_2_0, $options13[CURLOPT_HTTP_VERSION]);
1643+
$this->assertSame('cookies.txt', $options13[CURLOPT_COOKIEJAR]);
1644+
$this->assertSame('cookies.txt', $options13[CURLOPT_COOKIEFILE]);
1645+
$this->assertSame('TestAgent', $options13[CURLOPT_USERAGENT]);
1646+
}
1647+
1648+
public function testCURLOptionsPreservesIntegerKeys(): void
1649+
{
1650+
// cURL options use integer constants as keys. This test ensures they are not re-indexed.
1651+
$request = $this->getRequest();
1652+
$method = self::getPrivateMethodInvoker($request, 'setCURLOptions');
1653+
1654+
$initialOptions = [
1655+
CURLOPT_RETURNTRANSFER => true,
1656+
];
1657+
1658+
$config = [
1659+
'auth' => ['user', 'pass'],
1660+
];
1661+
1662+
$options = $method($initialOptions, $config);
1663+
1664+
// Verify keys are preserved and not re-indexed to 0, 1...
1665+
$this->assertArrayHasKey(CURLOPT_RETURNTRANSFER, $options);
1666+
$this->assertArrayHasKey(CURLOPT_USERPWD, $options);
1667+
}
15691668
}

0 commit comments

Comments
 (0)