Compare commits

...

7 Commits

Author SHA1 Message Date
Clemens Schwaighofer
8613e8977b UrlRequests curl: move options set logic to main curl wrapper call
change the curlRequest call to options array and build the options array
there.
Remove any options check + pre build from the get/request calls

Update phpunit tests with string type body return
2024-11-07 11:22:36 +09:00
Clemens Schwaighofer
0c51a3be87 Add phpunit tests for header key/value exceptions 2024-11-06 18:49:48 +09:00
Clemens Schwaighofer
f9cf36524e 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
2024-11-06 18:42:35 +09:00
Clemens Schwaighofer
bacb9881ac Fix UrlRequests Interface name, fix header build
Header default build was not done well, pass original headers inside and
set them. On new default start with empty array.

Switch to CoreLibs Json calls, because we use this libarary anyway already
2024-11-06 14:28:15 +09:00
Clemens Schwaighofer
f0fae1f76d Fix Composer package phpunit test url for UrlRequests 2024-11-06 13:35:00 +09:00
Clemens Schwaighofer
1653e6b684 Allow http_errors unset/set on each call
If set or not set, on each call this option can be set.
If set to null on call, the original value or default config value is used
2024-11-06 13:29:19 +09:00
Clemens Schwaighofer
c8bc0062ad URL Requests change error response
Instead of just throwing exception on 401 auth, throw exception for any
error code from 400 on
This can be turned off with the option "http_errors" set to false

Also updaed the exception content to match 400 or 500 error type with
more information attached

General Exception error codes:
Cnnn: Curl errors (FAILURE)
Rnnn: general class errors (ERROR)
Hnnn: http response errors (ERROR)
2024-11-06 12:48:01 +09:00
7 changed files with 627 additions and 330 deletions

View File

@@ -12,17 +12,27 @@ declare(strict_types=1);
/**
* build return json
*
* @param array $http_headers
* @param string $body
* @param array<string,mixed> $http_headers
* @param ?string $body
* @return string
*/
function buildContent(array $http_headers, string $body): string
function buildContent(array $http_headers, ?string $body): string
{
if (is_string($body) && !empty($body)) {
$_body = json_decode($body, true);
if (!is_array($_body)) {
$body = [$body];
} else {
$body = $_body;
}
} elseif (is_string($body)) {
$body = [];
}
return json_encode([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => json_decode($body, true)
"BODY" => $body,
]);
}
@@ -34,15 +44,22 @@ $http_headers = array_filter($_SERVER, function ($value, $key) {
header("Content-Type: application/json; charset=UTF-8");
// if the header has Authorization and RunAuthTest then exit with 401
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '["code": 401, "content": {"Error" => "Not Authorized"}]');
print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}');
exit;
}
print buildContent(
$http_headers,
file_get_contents('php://input') ?: '["code": 500, "content": {"Error" => "file_get_contents failed"}]'
);
// if server request type is get set file_get to null -> no body
if ($_SERVER['REQUEST_METHOD'] == "GET") {
$file_get = null;
} elseif (($file_get = file_get_contents('php://input')) === false) {
header("HTTP/1.1 404 Not Found");
print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}');
exit;
}
print buildContent($http_headers, $file_get);
// __END__

View File

@@ -21,7 +21,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
private string $url_basic_start = '';
private string $url_basic_end = '';
private array $default_config = [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -47,7 +47,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
. '4dev/tests/AAASetupData/requests/http_requests.php',
// composer package
'https://soba.egplusww.jp/developers/clemens/core_data/composer-packages/'
. 'CoreLibs-Composer-All/test/phpunit/AAASetupData/http_requests.php',
. 'CoreLibs-Composer-All/test/phpunit/AAASetupData/requests/http_requests.php',
// if we run php -S localhost:30999 -t [see below]
// dev: /storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/4dev/tests/AAASetupData/requests/
// composer: /storage/var/www/html/developers/clemens/core_data/composer-packages/CoreLibs-Composer-All/test/phpunit/AAASetupData
@@ -109,7 +109,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'no config' => [
'config' => null,
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -125,7 +125,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'setup all possible configs' => [
'config' => [
'auth' => ['user', 'passowrd', 'Basic'],
'exception_on_not_authorized' => true,
'http_errors' => false,
'base_uri' => 'http://foo.bar.com',
'headers' => [
'something' => 'other',
@@ -138,7 +138,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
'expected_set' => [
'auth' => ['user', 'passowrd', 'Basic'],
'exception_on_not_authorized' => true,
'http_errors' => false,
'base_uri' => 'http://foo.bar.com',
'headers' => [
'something' => 'other',
@@ -161,7 +161,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'base_uri' => 'http://bar.foo.com'
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => 'http://bar.foo.com',
'query' => [],
'headers' => [],
@@ -179,7 +179,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'base_uri' => 'http://bar.foo.com'
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => 'http://bar.foo.com',
'query' => [],
'headers' => [],
@@ -191,7 +191,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'set_header_add' => null,
'remove_header' => null,
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => 'http://bar.baz.com',
'query' => [],
'headers' => [],
@@ -203,7 +203,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'set header new' => [
'config' => null,
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -217,7 +217,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'set_header_add' => false,
'remove_header' => null,
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -234,7 +234,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -250,7 +250,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'set_header_add' => false,
'remove_header' => null,
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -267,7 +267,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -283,7 +283,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'set_header_add' => true,
'remove_header' => null,
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -301,7 +301,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -317,7 +317,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'remove-entry' => 'foo'
],
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -332,7 +332,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -348,7 +348,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'remove-entry' => null
],
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -363,7 +363,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -379,7 +379,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'remove-entry' => null
],
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -394,7 +394,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -410,7 +410,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'remove-entry' => 'foo'
],
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -427,7 +427,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
],
],
'expected_set' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -443,7 +443,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'remove-entry' => ['foo', 'bar',]
],
'expected_change' => [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [
@@ -732,7 +732,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
);
}
// MARK: test basi call provider
// MARK: test basic call provider
/**
* Undocumented function
@@ -768,7 +768,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'type' => $type,
'options' => null,
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":null}'
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":[]}'
];
$provider["basic " . $type . ", query options"] = [
'type' => $type,
@@ -776,7 +776,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
"query" => ["foo" => "bar"],
],
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":{"foo":"bar"},"BODY":null}'
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":{"foo":"bar"},"BODY":[]}'
];
$provider["basic " . $type . ", query/body options"] = [
'type' => $type,
@@ -787,6 +787,22 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":{"foo":"bar"},"BODY":{"foobar":"barbaz"}}'
];
$provider["basic " . $type . ", body options"] = [
'type' => $type,
'options' => [
"body" => ["foobar" => "barbaz"],
],
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":{"foobar":"barbaz"}}'
];
$provider["basic " . $type . ", body options as string"] = [
'type' => $type,
'options' => [
"body" => "body is a string",
],
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":["body is a string"]}'
];
}
// MARK: post/put/patch
foreach (['post', 'put', 'patch'] as $type) {
@@ -814,7 +830,24 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":{"foo":"bar"},"BODY":{"foobar":"barbaz"}}'
];
$provider["basic " . $type . ", body options"] = [
'type' => $type,
'options' => [
"body" => ["foobar" => "barbaz"],
],
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":{"foobar":"barbaz"}}'
];
$provider["basic " . $type . ", body option as string"] = [
'type' => $type,
'options' => [
"body" => "body is a string",
],
'return_code' => "200",
'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":["body is a string"]}'
];
}
// $provider['"basic post']
return $provider;
// phpcs:enable Generic.Files.LineLength
}
@@ -917,7 +950,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
);
}
// TODO: multi requests with same base connection
// MARK: multi requests with same base connection
/**
* Undocumented function
@@ -970,17 +1003,67 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
. '"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*",'
. '"HTTP_HOST":"soba.egplusww.jp"},'
. '"REQUEST_TYPE":"DELETE",'
. '"PARAMS":[],"BODY":null}',
. '"PARAMS":[],"BODY":[]}',
$response['content'],
'multi call: delete content not matching'
);
}
// 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
/**
* Undocumented function
* Exception:InvalidRequestType
*
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidRequestType
*
* @return void
@@ -990,7 +1073,47 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/InvalidRequestType/");
$response = $curl->request('wrong', 'http://foo.bar.com');
$curl->request('wrong', 'http://foo.bar.com');
}
/**
* Exception:InvalidHeaderKey
*
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidHeaderKey
*
* @return void
*/
public function testExceptionInvalidHeaderKey(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/InvalidHeaderKey/");
$curl->request('get', $this->url_basic, [
"headers" => [
"(invalid-key)" => "key"
]
]);
}
/**
* Exception:InvalidHeaderValue
*
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidHeaderValue
*
* @return void
*/
public function testExceptionInvalidHeaderValue(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/InvalidHeaderValue/");
$curl->request('get', $this->url_basic, [
"headers" => [
"invalid-value" => "\x19\x10"
]
]);
}
/**
@@ -1006,9 +1129,10 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
// }
/**
* Undocumented function
* Exception:CurlExecError
*
* @testdox UrlRequests\Curl Exception:CurlError
* @covers ::request
* @testdox UrlRequests\Curl Exception:CurlExecError
*
* @return void
*/
@@ -1016,51 +1140,93 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/CurlError/");
$this->expectExceptionMessageMatches("/CurlExecError/");
// invalid yrl
$response = $curl->request('get', 'as-4939345!#$%');
$curl->request('get', 'as-4939345!#$%');
}
/**
* TODO: Exception:UnauthorizedRequest
* Exception:ClientError
*
* @testdox UrlRequests\Curl Exception:UnauthorizedRequest
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError
*
* @return void
*/
public function testExceptionUnauthorizedRequest(): void
public function testExceptionBadRequest(): void
{
$curl = new \CoreLibs\UrlRequests\Curl(["exception_on_not_authorized" => true]);
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/UnauthorizedRequest/");
$response = $curl->get($this->url_basic, [
$this->expectExceptionMessageMatches("/ClientError/");
$curl->request('get', $this->url_basic, [
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
"RunAuthTest" => "yes",
]
]);
// $response = $curl->get('https://httpbin.org/bearer', [
// "headers" => ["Authorization" => "schmalztiegel"]
// ]);
}
/**
* Undocumented function
* Exception:ClientError
*
* @testdox UrlRequests\Curl Exception:DuplicatedArrayKey
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError on call enable
*
* @return void
*/
public function testExceptionDuplicatedArrayKey(): void
public function testExceptionBadRequestEnable(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessageMatches("/DuplicatedArrayKey/");
$curl->prepareHeaders([
'header-double:a',
'header-double:b',
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/ClientError/");
$curl->request('get', $this->url_basic, [
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
],
"http_errors" => true
]);
}
/**
* Exception:ClientError
*
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError unset on call
*
* @return void
*/
public function testExceptionBadRequestUnset(): void
{
// if true, with false it has to be off
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]);
$response = $curl->request('get', $this->url_basic, [
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
],
"http_errors" => false,
]);
$this->assertEquals(
"401",
$response['code'],
'Unset Exception failed with false'
);
// if false, null should not change it
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]);
$response = $curl->request('get', $this->url_basic, [
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
],
"http_errors" => null,
]);
$this->assertEquals(
"401",
$response['code'],
'Unset Exception failed with null'
);
}
}
// __END__

View File

@@ -15,17 +15,28 @@ $log = new CoreLibs\Logging\Logging([
/**
* build return json
*
* @param array<string,mixed> $http_headers
* @param string $body
* @param array<string,mixed> $http_headers
* @param ?string $body
* @return string
*/
function buildContent(array $http_headers, string $body): string
function buildContent(array $http_headers, ?string $body): string
{
if (is_string($body) && !empty($body)) {
$_body = Json::jsonConvertToArray($body);
if (Json::jsonGetLastError()) {
$body = [$body];
} else {
$body = $_body;
}
} elseif (is_string($body)) {
$body = [];
}
return Json::jsonConvertArrayTo([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => Json::jsonConvertToArray($body)
"BODY" => $body,
// "STRING_BODY" => $body,
]);
}
@@ -40,11 +51,18 @@ header("Content-Type: application/json; charset=UTF-8");
// if the header has Authorization and RunAuthTest then exit with 401
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '["code": 401, "content": {"Error" => "Not Authorized"}]');
print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}');
exit;
}
$file_get = file_get_contents('php://input') ?: '{"Error" => "file_get_contents failed"}';
// if server request type is get set file_get to null -> no body
if ($_SERVER['REQUEST_METHOD'] == "GET") {
$file_get = null;
} elseif (($file_get = file_get_contents('php://input')) === false) {
header("HTTP/1.1 404 Not Found");
print buildContent($http_headers, '{"code": 404, "content": {"Error": "file_get_contents failed"}}');
exit;
}
// str_replace('\"', '"', trim($file_get, '"'));
$log->debug('SERVER', $log->prAr($_SERVER));

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'
. '?other=get_a',
[
'headers' => $client->prepareHeaders([
'test-header: ABC',
'info-request-type: _GET',
'headers' => [
'test-header' => 'ABC',
'info-request-type' => '_GET',
'Funk-pop' => 'Semlly god'
]),
],
'query' => ['foo' => 'BAR']
]
);
@@ -78,11 +78,11 @@ $data = $client->request(
. 'trunk/www/admin/UrlRequests.target.php'
. '?other=get_a',
[
"headers" => $client->prepareHeaders([
'test-header: ABC',
'info-request-type: _GET',
"headers" => [
'test-header' => 'ABC',
'info-request-type' => '_GET',
'Funk-pop' => 'Semlly god'
]),
],
"query" => ['foo' => 'BAR'],
],
);
@@ -94,12 +94,12 @@ $data = $client->post(
. '?other=post_a',
[
'body' => ['payload' => 'data post'],
'headers' => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _POST'
]),
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_POST',
],
'query' => ['foo' => 'BAR post'],
]
);
@@ -111,16 +111,33 @@ $data = $client->request(
. '?other=post_a',
[
"body" => ['payload' => 'data post', 'request' => 'I am the request body'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _POST'
]),
"headers" => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_POST',
],
"query" => ['foo' => 'BAR post'],
]
);
print "[request] _POST RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->request(
"post",
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=post_a',
[
"body" => 'string body here',
"headers" => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_POST',
],
"query" => ['foo' => 'BAR post'],
]
);
print "[request|string body] _POST RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->put(
@@ -128,12 +145,12 @@ $data = $client->put(
. '?other=put_a',
[
"body" => ['payload' => 'data put'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _PUT'
]),
"headers" => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_PUT',
],
'query' => ['foo' => 'BAR put'],
]
);
@@ -145,12 +162,12 @@ $data = $client->patch(
. '?other=patch_a',
[
"body" => ['payload' => 'data patch'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _PATCH'
]),
"headers" => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_PATCH',
],
'query' => ['foo' => 'BAR patch'],
]
);
@@ -162,12 +179,12 @@ $data = $client->delete(
. '?other=delete_no_body_a',
[
"body" => null,
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _DELETE'
]),
"headers" => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_DELETE',
],
"query" => ['foo' => 'BAR delete'],
]
);
@@ -179,12 +196,12 @@ $data = $client->delete(
. '?other=delete_body_a',
[
"body" => ['payload' => 'data delete'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _DELETE'
]),
"headers" => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'test-header' => 'ABC',
'info-request-type' => '_DELETE',
],
"query" => ['foo' => 'BAR delete'],
]
);
@@ -244,7 +261,7 @@ print "<hr>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"exception_on_not_authorized" => false,
"http_errors" => false,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
@@ -262,7 +279,7 @@ print "AUTH REQUEST WITH EXCEPTION:<br>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"exception_on_not_authorized" => true,
"http_errors" => true,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
@@ -276,6 +293,42 @@ try {
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "AUTH REQUEST WITH EXCEPTION (UNSET):<br>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"http_errors" => true,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
]
]);
$response = $uc->get('UrlRequests.target.php', ['http_errors' => false]);
print "AUTH REQUEST (UNSET): <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 "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>";
$uc = new Curl([
@@ -285,7 +338,20 @@ $uc = new Curl([
]
]);
$response = $uc->get('UrlRequests.target.php', ["headers" => null, "query" => ["test" => "one-test"]]);
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "HEADER RESET 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 "<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>";

View File

@@ -10,7 +10,7 @@
* https://docs.guzzlephp.org/en/stable/index.html
*
* Requests are guzzleHttp compatible
* Config for setup is guzzleHttp compatible (except the exception_on_not_authorized)
* Config for setup is guzzleHttp compatible (except the http_errors)
* Any setters and getters are only for this class
*/
@@ -18,10 +18,8 @@ declare(strict_types=1);
namespace CoreLibs\UrlRequests;
use RuntimeException;
use CoreLibs\Convert\Json;
/** @package CoreLibs\UrlRequests */
class Curl implements Interface\RequestsInterface
{
// all general calls: get/post/put/patch/delete
@@ -35,6 +33,12 @@ class Curl implements Interface\RequestsInterface
private const HAVE_POST_FIELDS = ["post", "put", "patch", "delete"];
/** @var array<string> list of requests that must have a body */
private const MANDATORY_POST_FIELDS = ["post", "put", "patch"];
/** @var int http ok request */
public const HTTP_OK = 200;
/** @var int http ok creted response */
public const HTTP_CREATED = 201;
/** @var int http ok no content */
public const HTTP_NO_CONTENT = 204;
/** @var int error bad request */
public const HTTP_BAD_REQUEST = 400;
/** @var int error not authorized Request */
@@ -47,29 +51,22 @@ class Curl implements Interface\RequestsInterface
public const HTTP_CONFLICT = 409;
/** @var int error unprocessable entity */
public const HTTP_UNPROCESSABLE_ENTITY = 422;
/** @var int http ok request */
public const HTTP_OK = 200;
/** @var int http ok creted response */
public const HTTP_CREATED = 201;
/** @var int http ok no content */
public const HTTP_NO_CONTENT = 204;
/** @var int major version for user agent */
public const MAJOR_VERSION = 1;
// the config is set to be as much compatible to guzzelHttp as possible
// phpcs:disable Generic.Files.LineLength
/** @var array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized: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
* 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
* headers: (array) base headers, can be overwritten by headers set in call
* timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS)
* connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS)
* : below is not a guzzleHttp config
* exception_on_not_authorized: bool true/false for throwing exception on auth error
*/
private array $config = [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -115,14 +112,14 @@ class Curl implements Interface\RequestsInterface
* Set the main configuration
*
* phpcs:disable Generic.Files.LineLength
* @param array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized?:bool,base_uri?:string,headers?:array<string,string|array<string>>,query?:array<string,string>,timeout?:float,connection_timeout?:float} $config
* @param 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
* @return void
* phpcs:enable Generic.Files.LineLength
*/
private function setConfiguration(array $config)
{
$default_config = [
'exception_on_not_authorized' => false,
'http_errors' => true,
'base_uri' => '',
'query' => [],
'headers' => [],
@@ -131,36 +128,17 @@ class Curl implements Interface\RequestsInterface
];
// auth string is array of 0: user, 1: password, 2: auth type
if (!empty($config['auth']) && is_array($config['auth'])) {
// base auth sets the header actually
$type = isset($config['auth'][2]) ? strtolower($config['auth'][2]) : 'basic';
$userpwd = $config['auth'][0] . ':' . $config['auth'][1];
switch ($type) {
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;
}
$auth_data = $this->authParser($config['auth']);
$this->auth_basic_header = $auth_data['auth_basic_header'];
$this->auth_type = $auth_data['auth_type'];
$this->auth_userpwd = $auth_data['auth_userpwd'];
}
// only set if bool
if (
!isset($config['exception_on_not_authorized']) ||
!is_bool($config['exception_on_not_authorized'])
!isset($config['http_errors']) ||
!is_bool($config['http_errors'])
) {
$config['exception_on_not_authorized'] = false;
$config['http_errors'] = true;
}
if (!empty($config['base_uri'])) {
if (($parsed_base_uri = $this->parseUrl($config['base_uri'])) !== false) {
@@ -189,6 +167,46 @@ class Curl implements Interface\RequestsInterface
$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
/**
@@ -358,6 +376,8 @@ class Curl implements Interface\RequestsInterface
// convert to string as JSON block if it is an array
if (is_array($body)) {
$params = Json::jsonConvertArrayTo($body);
} elseif (is_string($body)) {
$params = $body;
}
return $params ?? '';
}
@@ -373,6 +393,7 @@ class Curl implements Interface\RequestsInterface
private function convertHeaders(array $headers): array
{
$return_headers = [];
$header_keys = [];
foreach ($headers as $key => $value) {
if (!is_string($key)) {
// TODO: throw error
@@ -380,8 +401,19 @@ class Curl implements Interface\RequestsInterface
}
// bad if not valid header key
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) {
// TODO throw error
continue;
throw new \UnexpectedValueException(
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 (is_array($value)) {
@@ -390,8 +422,20 @@ class Curl implements Interface\RequestsInterface
$value = trim((string)$value, " \t");
// header values must be valid
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
// TODO throw error
continue;
throw new \UnexpectedValueException(
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;
}
@@ -404,14 +448,23 @@ class Curl implements Interface\RequestsInterface
* Authorization
* User-Agent
*
* @return array<string,string>
* @param array<string,string|array<string>> $headers already set headers
* @param ?string $auth_basic_header
* @return array<string,string|array<string>>
*/
private function buildDefaultHeaders(): array
private function buildDefaultHeaders(array $headers = [], ?string $auth_basic_header = ''): array
{
$headers = [];
// add auth header if set
if (!empty($this->auth_basic_header)) {
$headers['Authorization'] = $this->auth_basic_header;
// add auth header if set, will overwrite any already set auth header
if ($auth_basic_header !== null && $auth_basic_header == '' && !empty($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
if (!isset($headers[strtolower('User-Agent')])) {
@@ -424,14 +477,15 @@ class Curl implements Interface\RequestsInterface
* Build headers, combine with global headers of they are set
*
* @param null|array<string,string|array<string>> $headers
* @param ?string $auth_basic_header
* @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
// but the automatic set User-Agent and Authorization headers are always set
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
if (!empty($this->config['headers'])) {
@@ -449,39 +503,81 @@ class Curl implements Interface\RequestsInterface
$headers[$key] = $this->config['headers'][$key];
}
}
$headers = array_merge($headers, $this->buildDefaultHeaders());
$headers = $this->buildDefaultHeaders($headers, $auth_basic_header);
return $headers;
}
/**
* Set the array block that is sent to the request call
* Make sure that if headers is set as key but null it stays null and set to empty array
* if headers key is missing
* "get" calls do not set any body (null)
*
* phpcs:disable Generic.Files.LineLength
* @param string $type if set as get do not add body, else add body
* @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{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}
* phpcs:enable Generic.Files.LineLength
*/
private function setOptions(string $type, array $options): array
{
return [
"auth" => !array_key_exists('auth', $options) ? ['', '', ''] : $options['auth'],
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
"http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'],
"body" => $options["body"] ??
// check if we need a payload data set, set empty on not set
(in_array($type, self::MANDATORY_POST_FIELDS) && !isset($options['body']) ? [] : null)
];
}
// MARK: main curl request
/**
* Overall request call
*
* @param string $type get, post, pathc, put, delete:
* if not set or invalid throw error
* @param string $url The URL being requested,
* including domain and protocol
* @param null|array<string,string|array<string>> $headers [default=[]] Headers to be used in the request
* @param null|array<string,string> $query [default=null] Optinal query parameters
* @param null|string|array<string,mixed> $body [default=null] Data body, converted to JSON
* @return array{code:string,headers:array<string,array<string>>,content:string}
* phpcs:disable Generic.Files.LineLength
* @param string $type get, post, pathc, put, delete:
* if not set or invalid throw error
* @param string $url The URL being requested,
* including domain and protocol
* @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} Return content
* code: HTTP code, if http_errors if off, this can also hold 400 or 500 type codes
* headers: earch header entry has an array of the entries, can be more than one if proxied, etc
* content: content string as is, if JSON type must be decoded afterwards
* @throws \RuntimeException if type param is not valid
* phpcs:enable Generic.Files.LineLength
*/
private function curlRequest(
string $type,
string $url,
null|array $headers = [],
null|array $query = null,
null|string|array $body = null
array $options,
): array {
$this->url = $this->buildQuery($url, $query);
$this->headers = $this->convertHeaders($this->buildHeaders($headers));
// check if we need a payload data set, set empty on not set
$options = $this->setOptions($type, $options);
// set auth from override
if (is_array($options['auth'])) {
$auth_data = $this->authParser($options['auth']);
} else {
$auth_data = [
'auth_basic_header' => null,
'auth_type' => null,
'auth_userpwd' => null,
];
}
// build url
$this->url = $this->buildQuery($url, $options['query']);
$this->headers = $this->convertHeaders($this->buildHeaders(
$options['headers'],
$auth_data['auth_basic_header']
));
if (!in_array($type, self::VALID_REQUEST_TYPES)) {
throw new RuntimeException(
json_encode([
'status' => 'FAILURE',
'code' => 'C003',
throw new \RuntimeException(
Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => 'R001',
'type' => 'InvalidRequestType',
'message' => 'Invalid request type set: ' . $type,
'context' => [
@@ -489,14 +585,17 @@ class Curl implements Interface\RequestsInterface
'url' => $this->url,
'headers' => $this->headers,
],
]) ?: '',
]),
0,
);
}
// init curl handle
$handle = $this->handleCurleInit($this->url);
// 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
if ($type == "post") {
curl_setopt($handle, CURLOPT_POST, true);
@@ -504,8 +603,8 @@ class Curl implements Interface\RequestsInterface
curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type));
}
// set body data if not null, will send empty [] for empty data
if (in_array($type, self::HAVE_POST_FIELDS) && $body !== null) {
curl_setopt($handle, CURLOPT_POSTFIELDS, $this->convertPayloadData($body));
if (in_array($type, self::HAVE_POST_FIELDS) && $options['body'] !== null) {
curl_setopt($handle, CURLOPT_POSTFIELDS, $this->convertPayloadData($options['body']));
}
// reset all headers before we start the call
$this->received_headers = [];
@@ -514,7 +613,7 @@ class Curl implements Interface\RequestsInterface
// for debug
// print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>";
// get response code and bail on not authorized
$http_response = $this->handleCurlResponse($http_result, $handle);
$http_response = $this->handleCurlResponse($handle, $http_result, $options['http_errors']);
// close handler
$this->handleCurlClose($handle);
// return response and result
@@ -541,8 +640,8 @@ class Curl implements Interface\RequestsInterface
return $handle;
}
// throw Error here with all codes
throw new RuntimeException(
json_encode([
throw new \RuntimeException(
Json::jsonConvertArrayTo([
'status' => 'FAILURE',
'code' => 'C001',
'type' => 'CurlInitError',
@@ -550,7 +649,7 @@ class Curl implements Interface\RequestsInterface
'context' => [
'url' => $url,
],
]) ?: '',
]),
0,
);
}
@@ -564,14 +663,26 @@ class Curl implements Interface\RequestsInterface
*
* @param \CurlHandle $handle
* @param array<string> $headers list of options
* @param array{auth_type:?int,auth_userpwd:?string} $auth_data auth options to override global
* @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
if (!empty($this->auth_type) && !empty($this->auth_userpwd)) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $this->auth_type);
curl_setopt($handle, CURLOPT_USERPWD, $this->auth_userpwd);
// for not Basic auth only, basic auth sets its own header
if ($auth_data['auth_type'] !== null || $auth_data['auth_userpwd'] !== null) {
// set global if any of the two is empty and both globals are set
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 !== []) {
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
@@ -588,11 +699,12 @@ class Curl implements Interface\RequestsInterface
$timeout_requires_no_signal = false;
// if we have a timeout signal
if (!empty($this->config['timeout'])) {
$timeout_requires_no_signal |= $this->config['timeout'] < 1;
$timeout_requires_no_signal = $this->config['timeout'] < 1;
curl_setopt($handle, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000);
}
if (!empty($this->config['connection_timeout'])) {
$timeout_requires_no_signal |= $this->config['connection_timeout'] < 1;
$timeout_requires_no_signal = $timeout_requires_no_signal ||
$this->config['connection_timeout'] < 1;
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout'] * 1000);
}
if ($timeout_requires_no_signal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
@@ -635,7 +747,7 @@ class Curl implements Interface\RequestsInterface
// execute query
$http_result = curl_exec($handle);
if ($http_result === true) {
// only if CURLOPT_RETURNTRANSFER
// only if CURLOPT_RETURNTRANSFER is turned off
return (string)self::HTTP_OK;
} elseif ($http_result !== false) {
return $http_result;
@@ -662,18 +774,18 @@ class Curl implements Interface\RequestsInterface
}
// throw an error like in the normal reqeust, but set to CURL error
throw new RuntimeException(
json_encode([
throw new \RuntimeException(
Json::jsonConvertArrayTo([
'status' => 'FAILURE',
'code' => 'C002',
'type' => 'CurlError',
'type' => 'CurlExecError',
'message' => $message,
'context' => [
'url' => $url,
'errno' => $errno,
'message' => $message,
],
]) ?: '',
]),
$errno
);
}
@@ -681,42 +793,47 @@ class Curl implements Interface\RequestsInterface
// MARK: curl response handler
/**
* Handle curl response and not auth 401 errors
* Handle curl response, will throw exception on anything that is lower 400
* can be turned off by setting http_errors to false
*
* @param string $http_result
* @param \CurlHandle $handle
* @param \CurlHandle $handle Curl handler
* @param string $http_result result string from the url call
* @param ?bool $http_errors if we should throw an exception on error,
* override config setting
* @return string http response code
* @throws \RuntimeException Auth error
* @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400
*/
private function handleCurlResponse(
\CurlHandle $handle,
string $http_result,
\CurlHandle $handle
?bool $http_errors
): string {
$http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if (
empty($this->config['exception_on_not_authorized']) ||
$http_response !== self::HTTP_NOT_AUTHORIZED
empty($http_errors ?? $this->config['http_errors']) ||
$http_response < self::HTTP_BAD_REQUEST
) {
return (string)$http_response;
}
// set curl error number
$err = curl_errno($handle);
// extract all the error codes
$result_ar = json_decode((string)$http_result, true);
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
// throw Error here with all codes
throw new RuntimeException(
json_encode([
throw new \RuntimeException(
Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => $http_response,
'type' => 'UnauthorizedRequest',
'message' => 'Request could not be finished successfully because of an authorization error',
'code' => 'H' . (string)$http_response,
'type' => $http_response < 500 ? 'ClientError' : 'ServerError',
'message' => 'Request could not be finished successfully because of bad request response',
'context' => [
'url' => $url,
'result' => $result_ar,
'http_response' => $http_response,
// extract all the error content if returned
'result' => Json::jsonConvertToArray($http_result),
// curl internal error number
'curl_errno' => $err,
// the full curl info block
'curl_info' => curl_getinfo($handle),
],
]) ?: '',
]),
$err
);
}
@@ -736,48 +853,6 @@ class Curl implements Interface\RequestsInterface
// 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_encode([
'status' => 'ERROR',
'code' => 'C004',
'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
/**
@@ -940,7 +1015,7 @@ class Curl implements Interface\RequestsInterface
* phpcs:disable Generic.Files.LineLength
* @param string $type
* @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
* @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength
@@ -950,19 +1025,15 @@ class Curl implements Interface\RequestsInterface
// can have
// - headers
// - query
// - auth: null for no auth at all
// - http_errors: false for no exception on http error
// depending on type, must have (post/put/patch), optional for (delete)
// - body
$type = strtolower($type);
// check if we need a payload data set, set empty on not set
if (in_array($type, self::MANDATORY_POST_FIELDS) && !isset($options['body'])) {
$options['body'] = [];
}
return $this->curlRequest(
$type,
$url,
!array_key_exists('headers', $options) ? [] : $options['headers'],
$options['query'] ?? null,
$options['body'] ?? null
$options,
);
}
}

View File

@@ -18,32 +18,6 @@ namespace CoreLibs\UrlRequests;
trait CurlTrait
{
/**
* Set the array block that is sent to the request call
* Make sure that if headers is set as key but null it stays null and set to empty array
* if headers key is missing
* "get" calls do not set any 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>} $options Request options
* @return array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>}
*/
private function setOptions(string $type, array $options): array
{
if ($type == "get") {
return [
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
];
} else {
return [
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
];
}
}
/**
* combined set call for any type of request with options type parameters
* The following options can be set:
@@ -53,7 +27,7 @@ trait CurlTrait
*
* @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 array{headers?:null|array<string,string|array<string>>,query?:null|string|array<string,mixed>,body?:null|string|array<string,mixed>} $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
* @throws \UnexpectedValueException on missing body data when body data is needed
*/
@@ -65,7 +39,7 @@ trait CurlTrait
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $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
*/
public function get(string $url, array $options = []): array
@@ -73,11 +47,8 @@ trait CurlTrait
return $this->request(
"get",
$url,
$this->setOptions('get', $options),
$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}
}
/**
@@ -86,7 +57,7 @@ trait CurlTrait
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $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
*/
public function post(string $url, array $options): array
@@ -94,7 +65,7 @@ trait CurlTrait
return $this->request(
"post",
$url,
$this->setOptions('post', $options),
$options,
);
}
@@ -104,7 +75,7 @@ trait CurlTrait
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $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
*/
public function put(string $url, array $options): array
@@ -112,7 +83,7 @@ trait CurlTrait
return $this->request(
"put",
$url,
$this->setOptions('put', $options),
$options,
);
}
@@ -122,7 +93,7 @@ trait CurlTrait
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $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
*/
public function patch(string $url, array $options): array
@@ -130,7 +101,7 @@ trait CurlTrait
return $this->request(
"patch",
$url,
$this->setOptions('patch', $options),
$options,
);
}
@@ -141,7 +112,7 @@ trait CurlTrait
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $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
*/
public function delete(string $url, array $options = []): array
@@ -149,7 +120,7 @@ trait CurlTrait
return $this->request(
"delete",
$url,
$this->setOptions('delete', $options),
$options,
);
}
}

View File

@@ -11,18 +11,6 @@ namespace CoreLibs\UrlRequests\Interface;
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
*
@@ -84,7 +72,7 @@ interface RequestsInterface
* phpcs:disable Generic.Files.LineLength
* @param string $type
* @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
* @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength