UrlRequests auth set allowed in requests call

Removed the parseHeaders public call, headers must be set as array

Throw errors on invalid headers before sending them: Key/Value check
Add headers invalid check in phpunit

Auth headers can be set per call and will override global settings if matching
This commit is contained in:
Clemens Schwaighofer
2024-11-06 18:42:35 +09:00
parent bacb9881ac
commit f9cf36524e
5 changed files with 290 additions and 198 deletions

View File

@@ -732,7 +732,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
); );
} }
// MARK: test basi call provider // MARK: test basic call provider
/** /**
* Undocumented function * Undocumented function
@@ -976,11 +976,61 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
); );
} }
// MARK: auth header set via config
/**
* Test auth settings and auth override
*
* @testdox UrlRequests\Curl auth test call
*
* @return void
*/
public function testUrlRequestsCurlAuthHeader()
{
$curl = new \CoreLibs\UrlRequests\Curl([
"auth" => ["user", "pass", "basic"],
"http_errors" => false,
]);
$curl->request('get', $this->url_basic);
// check that the auth header matches
$this->assertContains(
"Authorization:Basic dXNlcjpwYXNz",
$curl->getHeadersSent()
);
// if we sent new request with auth header, this one should not be used
$curl->request('get', $this->url_basic, [
"headers" => ["Authorization" => "Failed"]
]);
// check that the auth header matches
$this->assertContains(
"Authorization:Basic dXNlcjpwYXNz",
$curl->getHeadersSent()
);
// override auth: reset
$curl->request('get', $this->url_basic, [
"auth" => null
]);
$this->assertNotContains(
"Authorization:Basic dXNlcjpwYXNz",
$curl->getHeadersSent()
);
// override auth: different auth
$curl->request('get', $this->url_basic, [
"auth" => ["user2", "pass2", "basic"]
]);
// check that the auth header matches
$this->assertContains(
"Authorization:Basic dXNlcjI6cGFzczI=",
$curl->getHeadersSent()
);
}
// MARK: test exceptions // MARK: test exceptions
/** /**
* Undocumented function * Exception:InvalidRequestType
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidRequestType * @testdox UrlRequests\Curl Exception:InvalidRequestType
* *
* @return void * @return void
@@ -1006,9 +1056,10 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
// } // }
/** /**
* Undocumented function * Exception:CurlExecError
* *
* @testdox UrlRequests\Curl Exception:CurlError * @covers ::request
* @testdox UrlRequests\Curl Exception:CurlExecError
* *
* @return void * @return void
*/ */
@@ -1024,6 +1075,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
/** /**
* Exception:ClientError * Exception:ClientError
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError * @testdox UrlRequests\Curl Exception:ClientError
* *
* @return void * @return void
@@ -1033,7 +1085,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/ClientError/"); $this->expectExceptionMessageMatches("/ClientError/");
$curl->get($this->url_basic, [ $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1044,6 +1096,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
/** /**
* Exception:ClientError * Exception:ClientError
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError on call enable * @testdox UrlRequests\Curl Exception:ClientError on call enable
* *
* @return void * @return void
@@ -1053,7 +1106,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/ClientError/"); $this->expectExceptionMessageMatches("/ClientError/");
$curl->get($this->url_basic, [ $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1065,6 +1118,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
/** /**
* Exception:ClientError * Exception:ClientError
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError unset on call * @testdox UrlRequests\Curl Exception:ClientError unset on call
* *
* @return void * @return void
@@ -1073,7 +1127,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
{ {
// if true, with false it has to be off // if true, with false it has to be off
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]);
$response = $curl->get($this->url_basic, [ $response = $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1087,7 +1141,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
); );
// if false, null should not change it // if false, null should not change it
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]);
$response = $curl->get($this->url_basic, [ $response = $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1100,24 +1154,6 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'Unset Exception failed with null' 'Unset Exception failed with null'
); );
} }
/**
* Undocumented function
*
* @testdox UrlRequests\Curl Exception:DuplicatedArrayKey
*
* @return void
*/
public function testExceptionDuplicatedArrayKey(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessageMatches("/DuplicatedArrayKey/");
$curl->prepareHeaders([
'header-double:a',
'header-double:b',
]);
}
} }
// __END__ // __END__

View File

@@ -40,11 +40,11 @@ $data = $client->get(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=get_a', . '?other=get_a',
[ [
'headers' => $client->prepareHeaders([ 'headers' => [
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _GET', 'info-request-type' => '_GET',
'Funk-pop' => 'Semlly god' 'Funk-pop' => 'Semlly god'
]), ],
'query' => ['foo' => 'BAR'] 'query' => ['foo' => 'BAR']
] ]
); );
@@ -78,11 +78,11 @@ $data = $client->request(
. 'trunk/www/admin/UrlRequests.target.php' . 'trunk/www/admin/UrlRequests.target.php'
. '?other=get_a', . '?other=get_a',
[ [
"headers" => $client->prepareHeaders([ "headers" => [
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _GET', 'info-request-type' => '_GET',
'Funk-pop' => 'Semlly god' 'Funk-pop' => 'Semlly god'
]), ],
"query" => ['foo' => 'BAR'], "query" => ['foo' => 'BAR'],
], ],
); );
@@ -94,12 +94,12 @@ $data = $client->post(
. '?other=post_a', . '?other=post_a',
[ [
'body' => ['payload' => 'data post'], 'body' => ['payload' => 'data post'],
'headers' => $client->prepareHeaders([ 'headers' => [
'Content-Type: application/json', 'Content-Type' => 'application/json',
'Accept: application/json', 'Accept' => 'application/json',
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _POST' 'info-request-type' => '_POST',
]), ],
'query' => ['foo' => 'BAR post'], 'query' => ['foo' => 'BAR post'],
] ]
); );
@@ -111,12 +111,12 @@ $data = $client->request(
. '?other=post_a', . '?other=post_a',
[ [
"body" => ['payload' => 'data post', 'request' => 'I am the request body'], "body" => ['payload' => 'data post', 'request' => 'I am the request body'],
"headers" => $client->prepareHeaders([ "headers" => [
'Content-Type: application/json', 'Content-Type' => 'application/json',
'Accept: application/json', 'Accept' => 'application/json',
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _POST' 'info-request-type' => '_POST',
]), ],
"query" => ['foo' => 'BAR post'], "query" => ['foo' => 'BAR post'],
] ]
); );
@@ -128,12 +128,12 @@ $data = $client->put(
. '?other=put_a', . '?other=put_a',
[ [
"body" => ['payload' => 'data put'], "body" => ['payload' => 'data put'],
"headers" => $client->prepareHeaders([ "headers" => [
'Content-Type: application/json', 'Content-Type' => 'application/json',
'Accept: application/json', 'Accept' => 'application/json',
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _PUT' 'info-request-type' => '_PUT',
]), ],
'query' => ['foo' => 'BAR put'], 'query' => ['foo' => 'BAR put'],
] ]
); );
@@ -145,12 +145,12 @@ $data = $client->patch(
. '?other=patch_a', . '?other=patch_a',
[ [
"body" => ['payload' => 'data patch'], "body" => ['payload' => 'data patch'],
"headers" => $client->prepareHeaders([ "headers" => [
'Content-Type: application/json', 'Content-Type' => 'application/json',
'Accept: application/json', 'Accept' => 'application/json',
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _PATCH' 'info-request-type' => '_PATCH',
]), ],
'query' => ['foo' => 'BAR patch'], 'query' => ['foo' => 'BAR patch'],
] ]
); );
@@ -162,12 +162,12 @@ $data = $client->delete(
. '?other=delete_no_body_a', . '?other=delete_no_body_a',
[ [
"body" => null, "body" => null,
"headers" => $client->prepareHeaders([ "headers" => [
'Content-Type: application/json', 'Content-Type' => 'application/json',
'Accept: application/json', 'Accept' => 'application/json',
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _DELETE' 'info-request-type' => '_DELETE',
]), ],
"query" => ['foo' => 'BAR delete'], "query" => ['foo' => 'BAR delete'],
] ]
); );
@@ -179,12 +179,12 @@ $data = $client->delete(
. '?other=delete_body_a', . '?other=delete_body_a',
[ [
"body" => ['payload' => 'data delete'], "body" => ['payload' => 'data delete'],
"headers" => $client->prepareHeaders([ "headers" => [
'Content-Type: application/json', 'Content-Type' => 'application/json',
'Accept: application/json', 'Accept' => 'application/json',
'test-header: ABC', 'test-header' => 'ABC',
'info-request-type: _DELETE' 'info-request-type' => '_DELETE',
]), ],
"query" => ['foo' => 'BAR delete'], "query" => ['foo' => 'BAR delete'],
] ]
); );
@@ -294,6 +294,24 @@ try {
} catch (Exception $e) { } catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>"; print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
} }
print "AUTH REQUEST HEADER SET:<br>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"auth" => ["user", "pass", "basic"],
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
]
]);
$response = $uc->get('UrlRequests.target.php');
print "AUTH REQUEST (HEADER): <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "<hr>"; print "<hr>";
$uc = new Curl([ $uc = new Curl([
@@ -308,6 +326,19 @@ print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>"; print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>"; print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
print "<hr>";
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"headers" => [
'bar' => 'foo:bar'
]
]);
$response = $uc->get('UrlRequests.target.php');
print "HEADER SET TEST REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
print "</body></html>"; print "</body></html>";
// __END__ // __END__

View File

@@ -59,12 +59,11 @@ class Curl implements Interface\RequestsInterface
/** @var array{auth?:array{0:string,1:string,2:string},http_errors:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as /** @var array{auth?:array{0:string,1:string,2:string},http_errors:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as
*phpcs:enable Generic.Files.LineLength *phpcs:enable Generic.Files.LineLength
* auth: [0: user, 1: password, 2: auth type] * auth: [0: user, 1: password, 2: auth type]
* http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors
* base_uri: base url to set, will prefix all urls given in calls * base_uri: base url to set, will prefix all urls given in calls
* headers: (array) base headers, can be overwritten by headers set in call * headers: (array) base headers, can be overwritten by headers set in call
* timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS) * timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS)
* connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS) * connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS)
* : below is not a guzzleHttp config
* http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors
*/ */
private array $config = [ private array $config = [
'http_errors' => true, 'http_errors' => true,
@@ -129,29 +128,10 @@ class Curl implements Interface\RequestsInterface
]; ];
// auth string is array of 0: user, 1: password, 2: auth type // auth string is array of 0: user, 1: password, 2: auth type
if (!empty($config['auth']) && is_array($config['auth'])) { if (!empty($config['auth']) && is_array($config['auth'])) {
// base auth sets the header actually $auth_data = $this->authParser($config['auth']);
$type = isset($config['auth'][2]) ? strtolower($config['auth'][2]) : 'basic'; $this->auth_basic_header = $auth_data['auth_basic_header'];
$userpwd = $config['auth'][0] . ':' . $config['auth'][1]; $this->auth_type = $auth_data['auth_type'];
switch ($type) { $this->auth_userpwd = $auth_data['auth_userpwd'];
case 'basic':
$this->auth_basic_header = 'Basic ' . base64_encode(
$userpwd
);
// if (!isset($config['headers']['Authorization'])) {
// $config['headers']['Authorization'] = 'Basic ' . base64_encode(
// $userpwd
// );
// }
break;
case 'digest':
$this->auth_type = CURLAUTH_DIGEST;
$this->auth_userpwd = $userpwd;
break;
case 'ntlm':
$this->auth_type = CURLAUTH_NTLM;
$this->auth_userpwd = $userpwd;
break;
}
} }
// only set if bool // only set if bool
if ( if (
@@ -187,6 +167,46 @@ class Curl implements Interface\RequestsInterface
$this->config = array_merge($default_config, $config); $this->config = array_merge($default_config, $config);
} }
// MARK: auth parser
/**
* set various auth parameters and return them as array for further processing
*
* @param array{0:string,1:string,2:string} $auth
* @return array{auth_basic_header:string,auth_type:int,auth_userpwd:string}
*/
private function authParser(?array $auth): array
{
$return_auth = [
'auth_basic_header' => '',
'auth_type' => 0,
'auth_userpwd' => '',
];
// on empty return as is, to force defaults
if ($auth === []) {
return $return_auth;
}
// base auth sets the header actually
$type = isset($auth[2]) ? strtolower($auth[2]) : 'basic';
$userpwd = $auth[0] . ':' . $auth[1];
switch ($type) {
case 'basic':
$return_auth['auth_basic_header'] = 'Basic ' . base64_encode(
$userpwd
);
break;
case 'digest':
$return_auth['auth_type'] = CURLAUTH_DIGEST;
$return_auth['auth_userpwd'] = $userpwd;
break;
case 'ntlm':
$return_auth['auth_type'] = CURLAUTH_NTLM;
$return_auth['auth_userpwd'] = $userpwd;
break;
}
return $return_auth;
}
// MARK: parse and build url // MARK: parse and build url
/** /**
@@ -371,6 +391,7 @@ class Curl implements Interface\RequestsInterface
private function convertHeaders(array $headers): array private function convertHeaders(array $headers): array
{ {
$return_headers = []; $return_headers = [];
$header_keys = [];
foreach ($headers as $key => $value) { foreach ($headers as $key => $value) {
if (!is_string($key)) { if (!is_string($key)) {
// TODO: throw error // TODO: throw error
@@ -378,8 +399,19 @@ class Curl implements Interface\RequestsInterface
} }
// bad if not valid header key // bad if not valid header key
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) { if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) {
// TODO throw error throw new \UnexpectedValueException(
continue; Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => 'R002',
'type' => 'InvalidHeaderKey',
'message' => 'Header key contains invalid characters',
'context' => [
'key' => $key,
'allowed' => '/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D',
],
]),
1
);
} }
// if value is array, join to string // if value is array, join to string
if (is_array($value)) { if (is_array($value)) {
@@ -388,8 +420,20 @@ class Curl implements Interface\RequestsInterface
$value = trim((string)$value, " \t"); $value = trim((string)$value, " \t");
// header values must be valid // header values must be valid
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
// TODO throw error throw new \UnexpectedValueException(
continue; Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => 'R003',
'type' => 'InvalidHeaderValue',
'message' => 'Header value contains invalid characters',
'context' => [
'key' => $key,
'value' => $value,
'allowed' => '/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D',
],
]),
1
);
} }
$return_headers[] = (string)$key . ':' . $value; $return_headers[] = (string)$key . ':' . $value;
} }
@@ -402,14 +446,23 @@ class Curl implements Interface\RequestsInterface
* Authorization * Authorization
* User-Agent * User-Agent
* *
* @param array<string,string|array<string>> $headers already set headers * @param array<string,string|array<string>> $headers already set headers
* @param ?string $auth_basic_header
* @return array<string,string|array<string>> * @return array<string,string|array<string>>
*/ */
private function buildDefaultHeaders($headers = []): array private function buildDefaultHeaders(array $headers = [], ?string $auth_basic_header = ''): array
{ {
// add auth header if set // add auth header if set, will overwrite any already set auth header
if (!empty($this->auth_basic_header)) { if ($auth_basic_header !== null && $auth_basic_header == '' && !empty($this->auth_basic_header)) {
$headers['Authorization'] = $this->auth_basic_header; $auth_basic_header = $this->auth_basic_header;
}
if (!empty($auth_basic_header)) {
// check if there is any auth header set, remove that one
if (!empty($auth_header_set = $this->headers_named[strtolower('Authorization')] ?? null)) {
unset($headers[$auth_header_set]);
}
// set new auth header
$headers['Authorization'] = $auth_basic_header;
} }
// always add HTTP_HOST and HTTP_USER_AGENT // always add HTTP_HOST and HTTP_USER_AGENT
if (!isset($headers[strtolower('User-Agent')])) { if (!isset($headers[strtolower('User-Agent')])) {
@@ -422,14 +475,15 @@ class Curl implements Interface\RequestsInterface
* Build headers, combine with global headers of they are set * Build headers, combine with global headers of they are set
* *
* @param null|array<string,string|array<string>> $headers * @param null|array<string,string|array<string>> $headers
* @param ?string $auth_basic_header
* @return array<string,string|array<string>> * @return array<string,string|array<string>>
*/ */
private function buildHeaders(null|array $headers): array private function buildHeaders(null|array $headers, ?string $auth_basic_header): array
{ {
// if headers is null, return empty headers, do not set config default headers // if headers is null, return empty headers, do not set config default headers
// but the automatic set User-Agent and Authorization headers are always set // but the automatic set User-Agent and Authorization headers are always set
if ($headers === null) { if ($headers === null) {
return $this->buildDefaultHeaders(); return $this->buildDefaultHeaders(auth_basic_header: $auth_basic_header);
} }
// merge master headers with sub headers, sub headers overwrite master headers // merge master headers with sub headers, sub headers overwrite master headers
if (!empty($this->config['headers'])) { if (!empty($this->config['headers'])) {
@@ -447,7 +501,7 @@ class Curl implements Interface\RequestsInterface
$headers[$key] = $this->config['headers'][$key]; $headers[$key] = $this->config['headers'][$key];
} }
} }
$headers = $this->buildDefaultHeaders($headers); $headers = $this->buildDefaultHeaders($headers, $auth_basic_header);
return $headers; return $headers;
} }
@@ -465,6 +519,7 @@ class Curl implements Interface\RequestsInterface
* @param null|string|array<string,mixed> $body Data body, converted to JSON * @param null|string|array<string,mixed> $body Data body, converted to JSON
* @param null|bool $http_errors Throw exception on http response * @param null|bool $http_errors Throw exception on http response
* 400 or higher if set to true * 400 or higher if set to true
* @param null|array{0:string,1:string,2:string} $auth auth array, if null reset global set auth
* @return array{code:string,headers:array<string,array<string>>,content:string} * @return array{code:string,headers:array<string,array<string>>,content:string}
* @throws \RuntimeException if type param is not valid * @throws \RuntimeException if type param is not valid
*/ */
@@ -475,14 +530,29 @@ class Curl implements Interface\RequestsInterface
null|array $query, null|array $query,
null|string|array $body, null|string|array $body,
null|bool $http_errors, null|bool $http_errors,
null|array $auth,
): array { ): array {
// set auth from override
if (is_array($auth)) {
$auth_data = $this->authParser($auth);
} else {
$auth_data = [
'auth_basic_header' => null,
'auth_type' => null,
'auth_userpwd' => null,
];
}
// build url
$this->url = $this->buildQuery($url, $query); $this->url = $this->buildQuery($url, $query);
$this->headers = $this->convertHeaders($this->buildHeaders($headers)); $this->headers = $this->convertHeaders($this->buildHeaders(
$headers,
$auth_data['auth_basic_header']
));
if (!in_array($type, self::VALID_REQUEST_TYPES)) { if (!in_array($type, self::VALID_REQUEST_TYPES)) {
throw new \RuntimeException( throw new \RuntimeException(
Json::jsonConvertArrayTo([ Json::jsonConvertArrayTo([
'status' => 'ERROR', 'status' => 'ERROR',
'code' => 'R002', 'code' => 'R001',
'type' => 'InvalidRequestType', 'type' => 'InvalidRequestType',
'message' => 'Invalid request type set: ' . $type, 'message' => 'Invalid request type set: ' . $type,
'context' => [ 'context' => [
@@ -497,7 +567,10 @@ class Curl implements Interface\RequestsInterface
// init curl handle // init curl handle
$handle = $this->handleCurleInit($this->url); $handle = $this->handleCurleInit($this->url);
// set the standard curl options // set the standard curl options
$this->setCurlOptions($handle, $this->headers); $this->setCurlOptions($handle, $this->headers, [
'auth_type' => $auth_data['auth_type'],
'auth_userpwd' => $auth_data['auth_userpwd'],
]);
// for post we set POST option // for post we set POST option
if ($type == "post") { if ($type == "post") {
curl_setopt($handle, CURLOPT_POST, true); curl_setopt($handle, CURLOPT_POST, true);
@@ -515,7 +588,7 @@ class Curl implements Interface\RequestsInterface
// for debug // for debug
// print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>"; // print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>";
// get response code and bail on not authorized // get response code and bail on not authorized
$http_response = $this->handleCurlResponse($http_result, $http_errors, $handle); $http_response = $this->handleCurlResponse($handle, $http_result, $http_errors);
// close handler // close handler
$this->handleCurlClose($handle); $this->handleCurlClose($handle);
// return response and result // return response and result
@@ -565,14 +638,26 @@ class Curl implements Interface\RequestsInterface
* *
* @param \CurlHandle $handle * @param \CurlHandle $handle
* @param array<string> $headers list of options * @param array<string> $headers list of options
* @param array{auth_type:?int,auth_userpwd:?string} $auth_data auth options to override global
* @return void * @return void
*/ */
private function setCurlOptions(\CurlHandle $handle, array $headers): void private function setCurlOptions(\CurlHandle $handle, array $headers, ?array $auth_data): void
{ {
// for not Basic auth, basic auth sets its own header // for not Basic auth only, basic auth sets its own header
if (!empty($this->auth_type) && !empty($this->auth_userpwd)) { if ($auth_data['auth_type'] !== null || $auth_data['auth_userpwd'] !== null) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $this->auth_type); // set global if any of the two is empty and both globals are set
curl_setopt($handle, CURLOPT_USERPWD, $this->auth_userpwd); if (
(empty($auth_data['auth_type']) || empty($auth_data['auth_userpwd'])) &&
!empty($this->auth_type) && !empty($this->auth_userpwd)
) {
$auth_data['auth_type'] = $this->auth_type;
$auth_data['auth_userpwd'] = $this->auth_userpwd;
}
}
// set auth options for curl
if (!empty($auth_data['auth_type']) && !empty($auth_data['auth_userpwd'])) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $auth_data['auth_type']);
curl_setopt($handle, CURLOPT_USERPWD, $auth_data['auth_userpwd']);
} }
if ($headers !== []) { if ($headers !== []) {
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
@@ -694,9 +779,9 @@ class Curl implements Interface\RequestsInterface
* @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400 * @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400
*/ */
private function handleCurlResponse( private function handleCurlResponse(
\CurlHandle $handle,
string $http_result, string $http_result,
?bool $http_errors, ?bool $http_errors
\CurlHandle $handle
): string { ): string {
$http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ( if (
@@ -743,48 +828,6 @@ class Curl implements Interface\RequestsInterface
// MARK: PUBLIC METHODS // MARK: PUBLIC METHODS
// ********************************************************************* // *********************************************************************
/**
* Convert an array with header strings like "foo: bar" to the interface
* needed "foo" => "bar" type
* Skips entries that are already in key => value type, by checking if the
* key is a not a number
*
* @param array<int|string,string> $headers
* @return array<string,string>
* @throws \UnexpectedValueException on duplicate header key
*/
public function prepareHeaders(array $headers): array
{
$return_headers = [];
foreach ($headers as $header_key => $header) {
// skip if header key is not numeric
if (!is_numeric($header_key)) {
$return_headers[$header_key] = $header;
continue;
}
list($_key, $_value) = explode(':', $header);
if (array_key_exists($_key, $return_headers)) {
// raise exception if key already exists
throw new \UnexpectedValueException(
Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => 'R001',
'type' => 'DuplicatedArrayKey',
'message' => 'Key already exists in the headers',
'context' => [
'key' => $_key,
'headers' => $headers,
'return_headers' => $return_headers,
],
]),
1
);
}
$return_headers[$_key] = $_value;
}
return $return_headers;
}
// MARK: get class vars // MARK: get class vars
/** /**
@@ -947,7 +990,7 @@ class Curl implements Interface\RequestsInterface
* phpcs:disable Generic.Files.LineLength * phpcs:disable Generic.Files.LineLength
* @param string $type * @param string $type
* @param string $url * @param string $url
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed * @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength * phpcs:enable Generic.Files.LineLength
@@ -971,6 +1014,7 @@ class Curl implements Interface\RequestsInterface
$options['query'] ?? null, $options['query'] ?? null,
$options['body'] ?? null, $options['body'] ?? null,
!array_key_exists('http_errors', $options) ? null : $options['http_errors'], !array_key_exists('http_errors', $options) ? null : $options['http_errors'],
!array_key_exists('auth', $options) ? [] : $options['auth'],
); );
} }
} }

View File

@@ -25,25 +25,21 @@ trait CurlTrait
* "get" calls do not set any body * "get" calls do not set any body
* *
* @param string $type if set as get do not add body, else add body * @param string $type if set as get do not add body, else add body
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Request options * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Request options
* @return array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} * @return array{auth?:array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool}
*/ */
private function setOptions(string $type, array $options): array private function setOptions(string $type, array $options): array
{ {
if ($type == "get") { $base = [
return [ "auth" => !array_key_exists('auth', $options) ? [] : $options['auth'],
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'], "headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null, "query" => $options['query'] ?? null,
"http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'], "http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'],
]; ];
} else { if ($type != "get") {
return [ $base["body"] = $options['body'] ?? null;
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
"http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'],
];
} }
return $base;
} }
/** /**
@@ -55,7 +51,7 @@ trait CurlTrait
* *
* @param string $type What type of request we send, will throw exception if not a valid one * @param string $type What type of request we send, will throw exception if not a valid one
* @param string $url The url to send * @param string $url The url to send
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Request options * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Request options
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed * @throws \UnexpectedValueException on missing body data when body data is needed
*/ */
@@ -67,7 +63,7 @@ trait CurlTrait
* *
* @param string $url The URL being requested, * @param string $url The URL being requested,
* including domain and protocol * including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/ */
public function get(string $url, array $options = []): array public function get(string $url, array $options = []): array
@@ -77,9 +73,6 @@ trait CurlTrait
$url, $url,
$this->setOptions('get', $options), $this->setOptions('get', $options),
); );
// array{headers?: array<string, array<string>|string>|null, query?: array<string, string>|null, body?: array<string, mixed>|string|null},
// array{headers?: array<string, array<string>|string>|null, query?: array<string, mixed>|string|null, body?: array<string, mixed>|string|null}
} }
/** /**
@@ -88,7 +81,7 @@ trait CurlTrait
* *
* @param string $url The URL being requested, * @param string $url The URL being requested,
* including domain and protocol * including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/ */
public function post(string $url, array $options): array public function post(string $url, array $options): array
@@ -106,7 +99,7 @@ trait CurlTrait
* *
* @param string $url The URL being requested, * @param string $url The URL being requested,
* including domain and protocol * including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/ */
public function put(string $url, array $options): array public function put(string $url, array $options): array
@@ -124,7 +117,7 @@ trait CurlTrait
* *
* @param string $url The URL being requested, * @param string $url The URL being requested,
* including domain and protocol * including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/ */
public function patch(string $url, array $options): array public function patch(string $url, array $options): array
@@ -143,7 +136,7 @@ trait CurlTrait
* *
* @param string $url The URL being requested, * @param string $url The URL being requested,
* including domain and protocol * including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/ */
public function delete(string $url, array $options = []): array public function delete(string $url, array $options = []): array

View File

@@ -11,18 +11,6 @@ namespace CoreLibs\UrlRequests\Interface;
interface RequestsInterface interface RequestsInterface
{ {
/**
* Convert an array with header strings like "foo: bar" to the interface
* needed "foo" => "bar" type
* Skips entries that are already in key => value type, by checking if the
* key is a not a number
*
* @param array<int|string,string> $headers
* @return array<string,string>
* @throws \UnexpectedValueException on duplicate header key
*/
public function prepareHeaders(array $headers): array;
/** /**
* get the config array with all settings * get the config array with all settings
* *
@@ -84,7 +72,7 @@ interface RequestsInterface
* phpcs:disable Generic.Files.LineLength * phpcs:disable Generic.Files.LineLength
* @param string $type * @param string $type
* @param string $url * @param string $url
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<string,mixed>} $options * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed * @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength * phpcs:enable Generic.Files.LineLength