Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 170 additions & 49 deletions system/HTTP/CURLRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -526,21 +526,50 @@ protected function setResponseHeaders(array $headers = [])
*/
protected function setCURLOptions(array $curlOptions = [], array $config = [])
{
// Auth Headers
if (! empty($config['auth'])) {
$curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
$curlOptions = $this->applyAuthOptions($curlOptions, $config);
$curlOptions = $this->applySslOptions($curlOptions, $config);
$curlOptions = $this->applyProxyOptions($curlOptions, $config);
$curlOptions = $this->applyDebugOptions($curlOptions, $config);
$curlOptions = $this->applyRedirectOptions($curlOptions, $config);
$curlOptions = $this->applyConnectionOptions($curlOptions, $config);
$curlOptions = $this->applyBodyOptions($curlOptions, $config);
$curlOptions = $this->applyResponseOptions($curlOptions, $config);
$curlOptions = $this->applyProtocolOptions($curlOptions, $config);

return $this->applyClientOptions($curlOptions, $config);
}

if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
} else {
$curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
}
/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyAuthOptions(array $curlOptions, array $config): array
{
// Auth Headers
if (isset($config['auth']) && is_array($config['auth']) && count($config['auth']) >= 2) {
$curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
$curlOptions[CURLOPT_HTTPAUTH] = (isset($config['auth'][2]) && $config['auth'][2] !== '' && strtolower($config['auth'][2]) === 'digest')
? CURLAUTH_DIGEST
: CURLAUTH_BASIC;
}

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applySslOptions(array $curlOptions, array $config): array
{
// Certificate
if (! empty($config['cert'])) {
$cert = $config['cert'];
$cert = $config['cert'] ?? null;

if ((bool) $cert) {
if (is_array($cert)) {
$curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
$cert = $cert[0];
Expand Down Expand Up @@ -571,30 +600,51 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
}
}

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyProxyOptions(array $curlOptions, array $config): array
{
// Proxy
if (isset($config['proxy'])) {
$curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
$curlOptions[CURLOPT_PROXY] = $config['proxy'];
}

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyDebugOptions(array $curlOptions, array $config): array
{
// Debug
if ($config['debug']) {
if ((bool) ($config['debug'] ?? false)) {
$curlOptions[CURLOPT_VERBOSE] = 1;
$curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
}

// Decode Content
if (! empty($config['decode_content'])) {
$accept = $this->getHeaderLine('Accept-Encoding');

if ($accept !== '') {
$curlOptions[CURLOPT_ENCODING] = $accept;
} else {
$curlOptions[CURLOPT_ENCODING] = '';
$curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
}
}
return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyRedirectOptions(array $curlOptions, array $config): array
{
// Allow Redirects
if (array_key_exists('allow_redirects', $config)) {
$settings = $this->redirectDefaults;
Expand Down Expand Up @@ -623,6 +673,17 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
}
}

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyConnectionOptions(array $curlOptions, array $config): array
{
// DNS Cache Timeout
if (isset($config['dns_cache_timeout']) && is_numeric($config['dns_cache_timeout']) && $config['dns_cache_timeout'] >= -1) {
$curlOptions[CURLOPT_DNS_CACHE_TIMEOUT] = (int) $config['dns_cache_timeout'];
Expand All @@ -634,13 +695,33 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
: true;

// Timeout
$curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
$curlOptions[CURLOPT_TIMEOUT_MS] = (float) ($config['timeout'] ?? 0) * 1000;

// Connection Timeout
$curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
$curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) ($config['connect_timeout'] ?? 150) * 1000;

// Resolve IP
if (array_key_exists('force_ip_resolve', $config)) {
$curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
'v4' => CURL_IPRESOLVE_V4,
'v6' => CURL_IPRESOLVE_V6,
default => CURL_IPRESOLVE_WHATEVER,
};
}

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyBodyOptions(array $curlOptions, array $config): array
{
// Post Data - application/x-www-form-urlencoded
if (! empty($config['form_params']) && is_array($config['form_params'])) {
if (isset($config['form_params']) && is_array($config['form_params']) && $config['form_params'] !== []) {
$postFields = http_build_query($config['form_params']);
$curlOptions[CURLOPT_POSTFIELDS] = $postFields;

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

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

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

// JSON
if (isset($config['json'])) {
// Will be set as the body in `applyBody()`
Expand All @@ -668,33 +746,76 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
$this->setHeader('Content-Length', (string) strlen($json));
}

// Resolve IP
if (array_key_exists('force_ip_resolve', $config)) {
$curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) {
'v4' => CURL_IPRESOLVE_V4,
'v6' => CURL_IPRESOLVE_V6,
default => CURL_IPRESOLVE_WHATEVER,
};
return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyResponseOptions(array $curlOptions, array $config): array
{
// Decode Content
if ((bool) ($config['decode_content'] ?? false)) {
$accept = $this->getHeaderLine('Accept-Encoding');

if ($accept !== '') {
$curlOptions[CURLOPT_ENCODING] = $accept;
} else {
$curlOptions[CURLOPT_ENCODING] = '';
$curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
}
}

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

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyProtocolOptions(array $curlOptions, array $config): array
{
// version
if (! empty($config['version'])) {
$version = sprintf('%.1F', $config['version']);
if ($version === '1.0') {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
} elseif ($version === '1.1') {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
} elseif ($version === '2.0') {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
} elseif ($version === '3.0') {
if (! defined('CURL_HTTP_VERSION_3')) {
define('CURL_HTTP_VERSION_3', 30);
}
$version = $config['version'] ?? null;

if ((bool) $version) {
$version = sprintf('%.1F', $version);

if (! defined('CURL_HTTP_VERSION_3')) {
define('CURL_HTTP_VERSION_3', 30);
}

$versions = [
'1.0' => CURL_HTTP_VERSION_1_0,
'1.1' => CURL_HTTP_VERSION_1_1,
'2.0' => CURL_HTTP_VERSION_2_0,
'3.0' => CURL_HTTP_VERSION_3,
];

$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3;
if (isset($versions[$version])) {
$curlOptions[CURLOPT_HTTP_VERSION] = $versions[$version];
}
}

return $curlOptions;
}

/**
* @param array<int, mixed> $curlOptions
* @param array<string, mixed> $config
*
* @return array<int, mixed>
*/
private function applyClientOptions(array $curlOptions, array $config): array
{
// Cookie
if (isset($config['cookie'])) {
$curlOptions[CURLOPT_COOKIEJAR] = $config['cookie'];
Expand Down
99 changes: 99 additions & 0 deletions tests/system/HTTP/CURLRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1566,4 +1566,103 @@ public function testProxyAndContinueResponses(): void

$this->assertSame($testBody, $response->getBody());
}

public function testSetCURLOptions(): void
{
$invoker = self::getPrivateMethodInvoker($this->request, 'setCURLOptions');

// Test Auth Options
$options = $invoker([], ['auth' => ['user', 'pass', 'digest']]);
$this->assertSame('user:pass', $options[CURLOPT_USERPWD]);
$this->assertSame(CURLAUTH_DIGEST, $options[CURLOPT_HTTPAUTH]);

$options2 = $invoker([], ['auth' => ['user', 'pass', 'basic']]);
$this->assertSame(CURLAUTH_BASIC, $options2[CURLOPT_HTTPAUTH]);

// Test SSL Options
$options3 = $invoker([], ['cert' => __FILE__]);
$this->assertSame(__FILE__, $options3[CURLOPT_SSLCERT]);

$options4 = $invoker([], ['verify' => false]);
$this->assertFalse($options4[CURLOPT_SSL_VERIFYPEER]);
$this->assertSame(0, $options4[CURLOPT_SSL_VERIFYHOST]);

// Test Proxy Options
$options5 = $invoker([], ['proxy' => 'http://proxy.example.com']);
$this->assertTrue($options5[CURLOPT_HTTPPROXYTUNNEL]);
$this->assertSame('http://proxy.example.com', $options5[CURLOPT_PROXY]);

// Test Debug Options
$options6 = $invoker([], ['debug' => true]);
$this->assertSame(1, $options6[CURLOPT_VERBOSE]);
$this->assertIsResource($options6[CURLOPT_STDERR]);

// Test Redirect Options
$options7 = $invoker([], ['allow_redirects' => false]);
$this->assertSame(0, $options7[CURLOPT_FOLLOWLOCATION]);

$options8 = $invoker([], ['allow_redirects' => true]);
$this->assertSame(1, $options8[CURLOPT_FOLLOWLOCATION]);
$this->assertSame(5, $options8[CURLOPT_MAXREDIRS]);

// Test Connection Options
$options9 = $invoker([], [
'dns_cache_timeout' => 120,
'fresh_connect' => false,
'timeout' => 10,
'connect_timeout' => 5,
'force_ip_resolve' => 'v4',
]);
$this->assertSame(120, $options9[CURLOPT_DNS_CACHE_TIMEOUT]);
$this->assertFalse($options9[CURLOPT_FRESH_CONNECT]);
$this->assertEqualsWithDelta(10000.0, $options9[CURLOPT_TIMEOUT_MS], PHP_FLOAT_EPSILON);
$this->assertEqualsWithDelta(5000.0, $options9[CURLOPT_CONNECTTIMEOUT_MS], PHP_FLOAT_EPSILON);
$this->assertSame(CURL_IPRESOLVE_V4, $options9[CURLOPT_IPRESOLVE]);

// Test Body Options (form_params / multipart / json)
$options10 = $invoker([], ['form_params' => ['foo' => 'bar']]);
$this->assertSame('foo=bar', $options10[CURLOPT_POSTFIELDS]);

$options11 = $invoker([], ['multipart' => ['file' => 'data']]);
$this->assertSame(['file' => 'data'], $options11[CURLOPT_POSTFIELDS]);

// Test Response Options (decode_content / http_errors)
$options12 = $invoker([], ['decode_content' => true]);
$this->assertSame('', $options12[CURLOPT_ENCODING]);
$this->assertSame('Accept-Encoding', $options12[CURLOPT_HTTPHEADER]);

// Test Protocol/Misc/Client Options
$options13 = $invoker([], [
'http_errors' => false,
'version' => '2.0',
'cookie' => 'cookies.txt',
'user_agent' => 'TestAgent',
]);
$this->assertFalse($options13[CURLOPT_FAILONERROR]);
$this->assertSame(CURL_HTTP_VERSION_2_0, $options13[CURLOPT_HTTP_VERSION]);
$this->assertSame('cookies.txt', $options13[CURLOPT_COOKIEJAR]);
$this->assertSame('cookies.txt', $options13[CURLOPT_COOKIEFILE]);
$this->assertSame('TestAgent', $options13[CURLOPT_USERAGENT]);
}

public function testCURLOptionsPreservesIntegerKeys(): void
{
// cURL options use integer constants as keys. This test ensures they are not re-indexed.
$request = $this->getRequest();
$method = self::getPrivateMethodInvoker($request, 'setCURLOptions');

$initialOptions = [
CURLOPT_RETURNTRANSFER => true,
];

$config = [
'auth' => ['user', 'pass'],
];

$options = $method($initialOptions, $config);

// Verify keys are preserved and not re-indexed to 0, 1...
$this->assertArrayHasKey(CURLOPT_RETURNTRANSFER, $options);
$this->assertArrayHasKey(CURLOPT_USERPWD, $options);
}
}
Loading
Loading