diff --git a/4dev/tests/AAASetupData/requests/http_requests.php b/4dev/tests/AAASetupData/requests/http_requests.php new file mode 100644 index 00000000..912d715f --- /dev/null +++ b/4dev/tests/AAASetupData/requests/http_requests.php @@ -0,0 +1,65 @@ + $http_headers + * @param ?string $body + * @return 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" => $body, + ]); +} + +$http_headers = array_filter($_SERVER, function ($value, $key) { + if (str_starts_with($key, 'HTTP_')) { + return true; + } +}, ARRAY_FILTER_USE_BOTH); + +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"}}'); + exit; +} + +// 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__ diff --git a/4dev/tests/DB/CoreLibsDBExtendedArrayIOTest.php b/4dev/tests/DB/CoreLibsDBExtendedArrayIOTest.php index f5204ed5..1e50e92d 100644 --- a/4dev/tests/DB/CoreLibsDBExtendedArrayIOTest.php +++ b/4dev/tests/DB/CoreLibsDBExtendedArrayIOTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase; * Test class for DB\Extended\ArrayIO * This will only test the PgSQL parts * @coversDefaultClass \CoreLibs\DB\Extended\ArrayIO - * @coversDefaultClass \CoreLibs\DB\Extended\ArrayIO * @testdox \CoreLibs\Extended\ArrayIO method tests for extended DB interface */ final class CoreLibsDBExtendedArrayIOTest extends TestCase diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php new file mode 100644 index 00000000..276b2ef6 --- /dev/null +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -0,0 +1,1232 @@ + true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + + /** + * check if we have some backend for testing + * + * @return void + */ + protected function setUp(): void + { + // check if local http servers + // or special started: + // php -S localhost:30999 \ + // -t /storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/4dev/tests/AAASetupData/requests/ + foreach ( + [ + // main dev + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/' + . '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/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 + 'localhost:30999/http_requests.php', + ] as $url + ) { + $handle = curl_init($url); + if ($handle === false) { + continue; + } + $this->url_basic = $url; + // split out the last / part for url set test + curl_close($handle); + // print "Open: $url\n"; + break; + } + } + + /** + * Undocumented function + * + * @return void + */ + protected function tearDown(): void + { + // end some httpserver + } + + /** + * Undocumented function + * + * @param string $url + * @return array + */ + private function splitUrl(string $url): array + { + + if (($lastSlashPos = strrpos($url, '/')) !== false) { + return [ + substr($url, 0, $lastSlashPos + 1), + substr($url, $lastSlashPos + 1, $lastSlashPos + 1) + ]; + } else { + return [0 => '', 1 => '']; + } + } + + // MARK: class setup tests + + /** + * Undocumented function + * + * @return array + */ + public function providerUrlRequestsCurlSetup(): array + { + return [ + // MARK: base config + 'no config' => [ + 'config' => null, + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => null, + ], + 'setup all possible configs' => [ + 'config' => [ + 'auth' => ['user', 'passowrd', 'Basic'], + 'http_errors' => false, + 'base_uri' => 'http://foo.bar.com', + 'headers' => [ + 'something' => 'other', + ], + 'query' => [ + 'foo' => 'bar', + ], + 'timeout' => 5, + 'connection_timeout' => 10, + ], + 'expected_set' => [ + 'auth' => ['user', 'passowrd', 'Basic'], + 'http_errors' => false, + 'base_uri' => 'http://foo.bar.com', + 'headers' => [ + 'something' => 'other', + ], + 'query' => [ + 'foo' => 'bar', + ], + 'timeout' => 5, + 'connection_timeout' => 10, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => null, + ], + // MARK: base url + 'setup base_uri only' => [ + 'config' => [ + 'base_uri' => 'http://bar.foo.com' + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => 'http://bar.foo.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => null, + ], + 'replace base_uri' => [ + 'config' => [ + 'base_uri' => 'http://bar.foo.com' + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => 'http://bar.foo.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => 'http://bar.baz.com', + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => 'http://bar.baz.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + // MARK: set headers + 'set header new' => [ + 'config' => null, + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => [ + 'new-header' => 'abc' + ], + 'set_header_add' => false, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'new-header' => 'abc', + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'set header overwrite' => [ + 'config' => [ + 'headers' => [ + 'existing-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => [ + 'existing-entry' => 'bar' + ], + 'set_header_add' => false, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'bar' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'set header add' => [ + 'config' => [ + 'headers' => [ + 'existing-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => [ + 'existing-entry' => 'bar' + ], + 'set_header_add' => true, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => ['foo', 'bar'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + // MARK: test remove header + 'remove header string, full match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => 'foo' + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header string, key match only' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => null + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, key match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => null + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, string match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => 'foo' + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, array match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => ['foo', 'bar',] + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + ]; + } + + // MARK: setup/config + + /** + * set setup + header, base uri change + * + * @covers ::Curl + * @covers ::setBaseUri + * @covers ::addHeader + * @covers ::removeHEader + * @dataProvider providerUrlRequestsCurlSetup + * @testdox UrlRequests\Curl Class setup tasks [$_dataName] + * + * @param null|array $config + * @param array $expected + * @param null|string $new_base_uri + * @param null|array $set_header + * @param null|bool $set_header_add + * @param null|array $remove_header + * @param null|array $expected_change + * @return void + */ + public function testUrlRequestsCurlSetupConfig( + null|array $config, + array $expected_set, + null|string $new_base_uri, + null|array $set_header, + null|bool $set_header_add, + null|array $remove_header, + null|array $expected_change + ): void { + // empty new + if ($config === null) { + $curl = new \CoreLibs\UrlRequests\Curl(); + } else { + $curl = new \CoreLibs\UrlRequests\Curl($config); + }; + // if ($new_base_uri === null && $set_header === null && $remove_header === null) { + // } + $this->assertEquals($expected_set, $curl->getConfig(), 'Class setup config mismatch'); + if ($new_base_uri !== null) { + $curl->setBaseUri($new_base_uri); + $this->assertEquals($expected_change, $curl->getConfig(), 'new base_uri not matching'); + } + if ($set_header !== null) { + if ($set_header_add !== null) { + $curl->setHeaders($set_header, $set_header_add); + } else { + $curl->setHeaders($set_header); + } + $this->assertEquals($expected_change, $curl->getConfig(), 'new headers not matching'); + } + if ($remove_header !== null) { + $curl->removeHeaders($remove_header); + $this->assertEquals($expected_change, $curl->getConfig(), 'removed headers not matching'); + } + } + + // MARK: request call tests + + /** + * Undocumented function + * + * @return array + */ + public function providerUrlRequestsCurlRequestBuild(): array + { + return [ + // MARK: config overwrite + // this would be: + // - base url + add url + // - base url + replace url + 'base url + add url' => [ + 'type' => 'get', + 'config' => [ + "base_uri" => "URL_START" + ], + 'url' => "URL_END", + 'options' => null, + 'sent_url' => "URL_FULL", + 'sent_url_parsed' => null, + 'sent_headers' => [ + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base url + replace url' => [ + 'type' => 'get', + 'config' => [ + "base_uri" => "URL_START" + ], + 'url' => "URL_FULL", + 'options' => null, + 'sent_url' => "URL_FULL", + 'sent_url_parsed' => null, + 'sent_headers' => [ + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + // - base header + add header + // - base header + reset header (null) + // - base query + add query + 'base header + add header' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => [ + "header-two" => "two", + ] + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "header-two:two", + "header-one:one", + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base header + reset header' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => null + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base header + add header (same)' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => [ + "header-one" => "one", + "header-two" => "two", + ] + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "header-two:two", + "header-one:one", + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base header + add header (replace)' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => [ + "header-one" => "three", + "header-two" => "two", + ] + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "header-two:two", + "header-one:three", + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + ]; + } + + // MARK: test call overwrite + + /** + * request build tests + * + * @covers ::request + * @dataProvider providerUrlRequestsCurlRequestBuild + * @testdox UrlRequests\Curl call with data merge [$_dataName] + * + * @param string $type + * @param array|null $config + * @param string|null $url + * @param array|null $options + * @param string|null $sent_url + * @param array|null $sent_url_parsed + * @param array $sent_headers + * @param string $return_code + * @param string $return_content + * @return void + */ + public function testUrlRequestsCurlRequestBuild( + string $type, + ?array $config, + ?string $url, + ?array $options, + ?string $sent_url, + ?array $sent_url_parsed, + array $sent_headers, + string $return_code, + string $return_content + ) { + if (!$this->url_basic) { + $this->markTestSkipped('No backend interface setup for testing: GET'); + } + if ($url) { + list($url_start, $url_end) = $this->splitUrl($this->url_basic); + $config['base_uri'] = str_replace('URL_START', $url_start, $config['base_uri']); + $url = str_replace('URL_END', $url_end, $url); + $url = str_replace('URL_FULL', $this->url_basic, $url); + $sent_url = str_replace('URL_FULL', $this->url_basic, $sent_url); + } + // init without or with config + if ($config === null) { + $curl = new \CoreLibs\UrlRequests\Curl(); + } else { + $curl = new \CoreLibs\UrlRequests\Curl($config); + }; + // set url + if ($url === null) { + $url = $this->url_basic; + } + // options + if (is_array($options)) { + $respone = $curl->request($type, $url, $options); + } else { + $respone = $curl->request($type, $url); + } + // headers + $this->assertEqualsCanonicalizing( + $sent_headers, + $curl->getHeadersSent(), + 'Headers do not metch' + ); + // url + if ($sent_url) { + $this->assertEquals( + $sent_url, + $curl->getUrlSent(), + 'Sent URL does not match' + ); + } + // check return code + $this->assertEquals( + $return_code, + $respone['code'], + 'Return code not matching' + ); + } + + // MARK: test basic call provider + + /** + * Undocumented function + * + * @return array + */ + public function providerUrlRequestsCurlRequest(): array + { + // phpcs:disable Generic.Files.LineLength + // get and delete can have null body, but only get will never have a body + $provider = []; + // MARK: get + foreach (['get'] as $type) { + $provider["basic " . $type . ", no options"] = [ + '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}' + ]; + $provider["basic " . $type . ", query options"] = [ + 'type' => $type, + 'options' => [ + "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}' + ]; + } + // MARK: delete + foreach (['delete'] as $type) { + // MARK: post + $provider["basic " . $type . ", no options"] = [ + '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":[]}' + ]; + $provider["basic " . $type . ", query options"] = [ + 'type' => $type, + 'options' => [ + "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":[]}' + ]; + $provider["basic " . $type . ", query/body options"] = [ + 'type' => $type, + 'options' => [ + "query" => ["foo" => "bar"], + "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":{"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) { + // MARK: post + $provider["basic " . $type . ", no options"] = [ + '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":[]}' + ]; + $provider["basic " . $type . ", query options"] = [ + 'type' => $type, + 'options' => [ + "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":[]}' + ]; + $provider["basic " . $type . ", query/body options"] = [ + 'type' => $type, + 'options' => [ + "query" => ["foo" => "bar"], + "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":{"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 + } + + // MARK: test basic get/post/put/patch/delete + + /** + * requests tests + * + * @covers ::request + * @covers ::get + * @covers ::post + * @covers ::put + * @covers ::patch + * @covers ::delete + * @dataProvider providerUrlRequestsCurlRequest + * @testdox UrlRequests\Curl request calls [$_dataName] + * + * @param string $type + * @param null|array $options + * @param string $return_code + * @param string $return_content + * @return void + */ + public function testUrlRequestsCurlRequest( + string $type, + null|array $options, + string $return_code, + string $return_content + ) { + if (!$this->url_basic) { + $this->markTestSkipped('No backend interface setup for testing: GET'); + } + $curl = new \CoreLibs\UrlRequests\Curl(); + // options + if (is_array($options)) { + $respone = $curl->request($type, $this->url_basic, $options); + } else { + $respone = $curl->request($type, $this->url_basic); + } + // print "REP: " . print_r($respone, true) . "\n"; + // check return code + $this->assertEquals( + $return_code, + $respone['code'], + 'request: Return code not matching' + ); + $this->assertEqualsCanonicalizing( + json_decode($return_content, true), + json_decode($respone['content'], true), + 'direct call Return content not matching' + ); + switch ($type) { + case 'get': + if (is_array($options)) { + $respone = $curl->get($this->url_basic, $options); + } else { + $respone = $curl->get($this->url_basic); + } + break; + case 'post': + if (is_array($options)) { + $respone = $curl->post($this->url_basic, $options); + } else { + $respone = $curl->post($this->url_basic, []); + } + break; + case 'put': + if (is_array($options)) { + $respone = $curl->put($this->url_basic, $options); + } else { + $respone = $curl->put($this->url_basic, []); + } + break; + case 'patch': + if (is_array($options)) { + $respone = $curl->patch($this->url_basic, $options); + } else { + $respone = $curl->patch($this->url_basic, []); + } + break; + case 'delete': + if (is_array($options)) { + $respone = $curl->delete($this->url_basic, $options); + } else { + $respone = $curl->delete($this->url_basic); + } + break; + } + // check return code + $this->assertEquals( + $return_code, + $respone['code'], + 'direct call Return code not matching' + ); + $this->assertEqualsCanonicalizing( + json_decode($return_content, true), + json_decode($respone['content'], true), + 'direct call Return content not matching' + ); + } + + // MARK: multi requests with same base connection + + /** + * Undocumented function + * + * @covers ::request + * @testdox UrlRequests\Curl multiple calls + * + * @return void + */ + public function testUrlRequestsCurlRequestMultiple() + { + $curl = new \CoreLibs\UrlRequests\Curl(); + // get + $response = $curl->get($this->url_basic, [ + "headers" => ["first-call" => "get"], + "query" => ["foo-get" => "bar"] + ]); + $this->assertEquals("200", $response["code"], "multi call: get response code not matching"); + $this->assertEquals( + '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_FIRST_CALL":"get","HTTP_ACCEPT":"*\/*",' + . '"HTTP_HOST":"soba.egplusww.jp"},' + . '"REQUEST_TYPE":"GET",' + . '"PARAMS":{"foo-get":"bar"},"BODY":null}', + $response['content'], + 'multi call: get content not matching' + ); + // post + $response = $curl->post($this->url_basic, [ + "headers" => ["second-call" => "post"], + "body" => ["foo-post" => "baz"] + ]); + $this->assertEquals("200", $response["code"], "multi call: post response code not matching"); + $this->assertEquals( + '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_SECOND_CALL":"post","HTTP_ACCEPT":"*\/*",' + . '"HTTP_HOST":"soba.egplusww.jp"},' + . '"REQUEST_TYPE":"POST",' + . '"PARAMS":[],"BODY":{"foo-post":"baz"}}', + $response['content'], + 'multi call: post content not matching' + ); + // delete + $response = $curl->delete($this->url_basic, [ + "headers" => ["third-call" => "delete"], + ]); + $this->assertEquals("200", $response["code"], "multi call: delete response code not matching"); + $this->assertEquals( + '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1",' + . '"HTTP_THIRD_CALL":"delete","HTTP_ACCEPT":"*\/*",' + . '"HTTP_HOST":"soba.egplusww.jp"},' + . '"REQUEST_TYPE":"DELETE",' + . '"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 + + /** + * Exception:InvalidRequestType + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:InvalidRequestType + * + * @return void + */ + public function testExceptionInvalidRequestType(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/InvalidRequestType/"); + $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" + ] + ]); + } + + /** + * TODO: Exception:CurlInitError + * + * @testdox UrlRequests\Curl Exception:CurlInitError + * + * @return void + */ + // public function testExceptionCurlInitError(): void + // { + // $this->markTestSkipped('Test Exception CurlInitError not implemented'); + // } + + /** + * Exception:CurlExecError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:CurlExecError + * + * @return void + */ + public function testExceptionCurlError(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/CurlExecError/"); + // invalid yrl + $curl->request('get', 'as-4939345!#$%'); + } + + /** + * Exception:ClientError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:ClientError + * + * @return void + */ + public function testExceptionBadRequest(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/ClientError/"); + $curl->request('get', $this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ] + ]); + } + + /** + * Exception:ClientError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:ClientError on call enable + * + * @return void + */ + public function testExceptionBadRequestEnable(): void + { + $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__ diff --git a/phpstan.neon b/phpstan.neon index d9c3806c..a84ae62c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,16 +1,16 @@ # PHP Stan Config includes: - phpstan-conditional.php - - ./vendor/yamadashy/phpstan-friendly-formatter/extension.neon + #- ./vendor/yamadashy/phpstan-friendly-formatter/extension.neon parameters: tmpDir: %currentWorkingDirectory%/tmp/phpstan-corelibs - errorFormat: friendly - friendly: - lineBefore: 5 - lineAfter: 3 + #errorFormat: friendly + #friendly: + # lineBefore: 3 + # lineAfter: 3 level: 8 # max is now 9 # strictRules: - # allRules: true + # allRules: false checkMissingCallableSignature: true treatPhpDocTypesAsCertain: false paths: @@ -60,6 +60,6 @@ parameters: # paths: # - ... # - ... - - - message: "#^Call to deprecated method #" - path: www/admin/class_test*.php + # - + # message: "#^Call to deprecated method #" + # path: www/admin/class_test*.php diff --git a/www/admin/UrlRequests.target.php b/www/admin/UrlRequests.target.php new file mode 100644 index 00000000..3d310ff4 --- /dev/null +++ b/www/admin/UrlRequests.target.php @@ -0,0 +1,79 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); + +/** + * build return json + * + * @param array $http_headers + * @param ?string $body + * @return 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" => $body, + // "STRING_BODY" => $body, + ]); +} + +$http_headers = array_filter($_SERVER, function ($value, $key) { + if (str_starts_with($key, 'HTTP_')) { + return true; + } +}, ARRAY_FILTER_USE_BOTH); + +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"}}'); + exit; +} + +// 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)); +$log->debug('HEADERS', $log->prAr($http_headers)); +$log->debug('REQUEST TYPE', $_SERVER['REQUEST_METHOD']); +$log->debug('GET', $log->prAr($_GET)); +$log->debug('POST', $log->prAr($_POST)); +$log->debug('PHP-INPUT', $log->prAr($file_get)); + +print buildContent($http_headers, $file_get); + +$log->debug('[END]', '=========================================>'); + +// __END__ diff --git a/www/admin/class_test.php b/www/admin/class_test.php index 7276f51d..9a56a0e8 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -118,6 +118,7 @@ $test_files = [ 'class_test.config.direct.php' => 'Class Test: CONFIG DIRECT', 'class_test.class-calls.php' => 'Class Test: CLASS CALLS', 'class_test.error_msg.php' => 'Class Test: ERROR MSG', + 'class_test.url-requests.curl.php' => 'Class Test: URL REQUESTS: CURL', 'subfolder/class_test.config.direct.php' => 'Class Test: CONFIG DIRECT SUB', ]; @@ -134,7 +135,7 @@ print "
READ _ENV ARRAY:
"; print Support::dumpVar(array_map('htmlentities', $_ENV)); // set + check edit access id $edit_access_id = 3; -if (is_object($login) && isset($login->loginGetAcl()['unit'])) { +if (isset($login->loginGetAcl()['unit'])) { print "ACL UNIT: " . print_r(array_keys($login->loginGetAcl()['unit']), true) . "
"; print "ACCESS CHECK: " . (string)$login->loginCheckEditAccess($edit_access_id) . "
"; if ($login->loginCheckEditAccess($edit_access_id)) { @@ -177,25 +178,23 @@ $log->debug('SOME MARK', 'Some error output'); // INTERNAL SET print "EDIT ACCESS ID: " . $backend->edit_access_id . "
"; -if (is_object($login)) { - // print "ACL:
".$backend->print_ar($login->loginGetAcl())."
"; - // $log->debug('ACL', "ACL: " . \CoreLibs\Debug\Support::dumpVar($login->loginGetAcl())); - // print "DEFAULT ACL:
".$backend->print_ar($login->default_acl_list)."
"; - // print "DEFAULT ACL:
".$backend->print_ar($login->default_acl_list)."
"; - // $result = array_flip( - // array_filter( - // array_flip($login->default_acl_list), - // function ($key) { - // if (is_numeric($key)) { - // return $key; - // } - // } - // ) - // ); - // print "DEFAULT ACL:
".$backend->print_ar($result)."
"; - // DEPRICATED CALL - // $backend->adbSetACL($login->loginGetAcl()); -} +// print "ACL:
".$backend->print_ar($login->loginGetAcl())."
"; +// $log->debug('ACL', "ACL: " . \CoreLibs\Debug\Support::dumpVar($login->loginGetAcl())); +// print "DEFAULT ACL:
".$backend->print_ar($login->default_acl_list)."
"; +// print "DEFAULT ACL:
".$backend->print_ar($login->default_acl_list)."
"; +// $result = array_flip( +// array_filter( +// array_flip($login->default_acl_list), +// function ($key) { +// if (is_numeric($key)) { +// return $key; +// } +// } +// ) +// ); +// print "DEFAULT ACL:
".$backend->print_ar($result)."
"; +// DEPRICATED CALL +// $backend->adbSetACL($login->loginGetAcl()); print "THIS HOST: " . HOST_NAME . ", with PROTOCOL: " . HOST_PROTOCOL . " is running SSL: " . HOST_SSL . "
"; print "DIR: " . DIR . "
"; diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php new file mode 100644 index 00000000..c43ce781 --- /dev/null +++ b/www/admin/class_test.url-requests.curl.php @@ -0,0 +1,361 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); + +$PAGE_NAME = 'TEST CLASS: URL REQUESTS CURL'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print '
Class Test Master
'; +print '

' . $PAGE_NAME . '

'; + +$client = new Curl(); + +print "
"; +$data = $client->get( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=get_a', + [ + 'headers' => [ + 'test-header' => 'ABC', + 'info-request-type' => '_GET', + 'Funk-pop' => 'Semlly god' + ], + 'query' => ['foo' => 'BAR'] + ] +); +print "_GET RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->request( + 'get', + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=get_a', +); +print "_GET RESPONSE, nothing set:
" . print_r($data, true) . "
"; + +print "
"; +try { + $data = $client->request( + 'get', + 'soba54.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=get_a', + ); + print "_GET RESPONSE, nothing set, invalid URL:
" . print_r($data, true) . "
"; +} catch (Exception $e) { + print "Exception:
" . print_r($e, true) . "

"; +} + + +print "
"; +$data = $client->request( + "get", + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/' + . 'trunk/www/admin/UrlRequests.target.php' + . '?other=get_a', + [ + "headers" => [ + 'test-header' => 'ABC', + 'info-request-type' => '_GET', + 'Funk-pop' => 'Semlly god' + ], + "query" => ['foo' => 'BAR'], + ], +); +print "[request] _GET RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->post( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=post_a', + [ + 'body' => ['payload' => 'data post'], + 'headers' => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'test-header' => 'ABC', + 'info-request-type' => '_POST', + ], + 'query' => ['foo' => 'BAR post'], + ] +); +print "_POST RESPONSE:
" . print_r($data, true) . "
"; +print "
"; +$data = $client->request( + "post", + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=post_a', + [ + "body" => ['payload' => 'data post', 'request' => 'I am the request body'], + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'test-header' => 'ABC', + 'info-request-type' => '_POST', + ], + "query" => ['foo' => 'BAR post'], + ] +); +print "[request] _POST RESPONSE:
" . print_r($data, true) . "
"; +print "
"; +$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:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->put( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=put_a', + [ + "body" => ['payload' => 'data put'], + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'test-header' => 'ABC', + 'info-request-type' => '_PUT', + ], + 'query' => ['foo' => 'BAR put'], + ] +); +print "_PUT RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->patch( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=patch_a', + [ + "body" => ['payload' => 'data patch'], + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'test-header' => 'ABC', + 'info-request-type' => '_PATCH', + ], + 'query' => ['foo' => 'BAR patch'], + ] +); +print "_PATCH RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->delete( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=delete_no_body_a', + [ + "body" => null, + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'test-header' => 'ABC', + 'info-request-type' => '_DELETE', + ], + "query" => ['foo' => 'BAR delete'], + ] +); +print "_DELETE RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->delete( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=delete_body_a', + [ + "body" => ['payload' => 'data delete'], + "headers" => [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'test-header' => 'ABC', + 'info-request-type' => '_DELETE', + ], + "query" => ['foo' => 'BAR delete'], + ] +); +print "_DELETE RESPONSE BODY:
" . print_r($data, true) . "
"; + +print "
"; + +try { + $uc = new Curl([ + "base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/foo', + "headers" => [ + 'DEFAULT-master' => 'master-header', + 'default-header' => 'uc-get', + 'default-remove' => 'will be removed', + 'default-remove-array' => ['a', 'b'], + 'default-remove-array-part' => ['c', 'd'], + 'default-remove-array-part-alt' => ['c', 'd', 'e'], + 'default-overwrite' => 'will be overwritten', + 'default-add' => 'will be added', + ], + 'query' => [ + 'global-p' => 'glob' + ] + ]); + print "CONFIG:
" . print_r($uc->getConfig(), true) . "
"; + $uc->removeHeaders(['default-remove' => '']); + $uc->removeHeaders(['default-remove-array' => ['a', 'b']]); + $uc->removeHeaders(['default-remove-array-part' => 'c']); + $uc->removeHeaders(['default-remove-array-part-alt' => ['c', 'd']]); + $uc->setHeaders(['default-new' => 'Something new']); + $uc->setHeaders(['default-overwrite' => 'Something Overwritten']); + $uc->setHeaders(['default-add' => 'Something Added'], true); + print "CONFIG:
" . print_r($uc->getConfig(), true) . "
"; + $data = $uc->request( + 'get', + 'UrlRequests.target.php', + [ + 'headers' => [ + 'call-header' => 'call-get', + 'default-header' => 'overwrite-uc-get', + 'X-Foo' => ['bar', 'baz'], + ], + 'query' => [ + 'other' => 'get_a', + ], + ] + ); + print "[uc] _GET RESPONSE, nothing set:
" . print_r($data, true) . "
"; + print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; + print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; + print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; +} catch (Exception $e) { + print "Exception:
" . print_r(json_decode($e->getMessage(), true), true) . "

"; +} + +print "
"; +try { + $uc = new Curl([ + "base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/', + "http_errors" => false, + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ] + ]); + $response = $uc->get('UrlRequests.target.php'); + print "AUTH REQUEST:
" . print_r($response, true) . "
"; + print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; + print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; + print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; +} catch (Exception $e) { + print "Exception:
" . print_r(json_decode($e->getMessage(), true), true) . "

"; +} +print "AUTH REQUEST WITH EXCEPTION:
"; +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'); + print "AUTH REQUEST:
" . print_r($response, true) . "
"; + print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; + print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; + print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; +} catch (Exception $e) { + print "Exception:
" . print_r(json_decode($e->getMessage(), true), true) . "

"; +} +print "AUTH REQUEST WITH EXCEPTION (UNSET):
"; +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):
" . print_r($response, true) . "
"; + print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; + print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; + print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; +} catch (Exception $e) { + print "Exception:
" . print_r(json_decode($e->getMessage(), true), true) . "

"; +} +print "AUTH REQUEST HEADER SET:
"; +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):
" . print_r($response, true) . "
"; + print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; + print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; + print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; +} catch (Exception $e) { + print "Exception:
" . print_r(json_decode($e->getMessage(), true), true) . "

"; +} + +print "
"; +$uc = new Curl([ + "base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/', + "headers" => [ + "header-one" => "one" + ] +]); +$response = $uc->get('UrlRequests.target.php', ["headers" => null, "query" => ["test" => "one-test"]]); +print "HEADER RESET REQUEST:
" . print_r($response, true) . "
"; +print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; +print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; +print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; + +print "
"; +$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:
" . print_r($response, true) . "
"; +print "[uc] SENT URL: " . $uc->getUrlSent() . "
"; +print "[uc] SENT URL PARSED:
" . print_r($uc->getUrlParsedSent(), true) . "
"; +print "[uc] SENT HEADERS:
" . print_r($uc->getHeadersSent(), true) . "
"; + +print ""; + +// __END__ diff --git a/www/configs/config.host.php b/www/configs/config.host.php index e95f43a5..41dcdce6 100644 --- a/www/configs/config.host.php +++ b/www/configs/config.host.php @@ -51,6 +51,7 @@ $SITE_CONFIG = [ 'soba.tequila.jp' => $__LOCAL_CONFIG, 'soba.teq.jp' => $__LOCAL_CONFIG, 'soba-local.tokyo.tequila.jp' => $__LOCAL_CONFIG, + 'localhost' => $__LOCAL_CONFIG, ]; // __END__ diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php new file mode 100644 index 00000000..fcc8bc14 --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -0,0 +1,1041 @@ + all the valid request type */ + private const VALID_REQUEST_TYPES = ["get", "post", "put", "patch", "delete"]; + /** @var array list of requests type that are set as custom in the curl options */ + private const CUSTOM_REQUESTS = ["put", "patch", "delete"]; + /** @var array list of requests types that have _POST type fields */ + private const HAVE_POST_FIELDS = ["post", "put", "patch", "delete"]; + /** @var array 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 */ + public const HTTP_NOT_AUTHORIZED = 401; + /** @var int error forbidden */ + public const HTTP_FORBIDDEN = 403; + /** @var int error not found */ + public const HTTP_NOT_FOUND = 404; + /** @var int error conflict */ + public const HTTP_CONFLICT = 409; + /** @var int error unprocessable entity */ + public const HTTP_UNPROCESSABLE_ENTITY = 422; + /** @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},http_errors:bool,base_uri:string,headers:array>,query:array,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) + */ + private array $config = [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + /** @var array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} parsed base_uri */ + private array $parsed_base_uri = []; + /** @var array lower key header name matches to given header name */ + private array $headers_named = []; + /** @var int auth type from auth array in config */ + private int $auth_type = 0; + /** @var string username and password string from auth array in config */ + private string $auth_userpwd = ''; + /** @var string set if auth type basic is given, will be set as "Authorization: ..." */ + private string $auth_basic_header = ''; + + /** @var array> received headers per header name, with sub array if there are redirects */ + private array $received_headers = []; + + /** @var string the current url sent */ + private string $url = ''; + /** @var array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} parsed url to sent */ + private array $parsed_url = []; + /** @var array the current headers sent */ + private array $headers = []; + + /** + * see config allowe entries above + * + * @param array $config config settings to be set + */ + public function __construct(array $config = []) + { + $this->setConfiguration($config); + } + + // ********************************************************************* + // MARK: PRIVATE METHODS + // ********************************************************************* + + /** + * Set the main configuration + * + * phpcs:disable Generic.Files.LineLength + * @param array{auth?:array{0:string,1:string,2:string},http_errors?:bool,base_uri?:string,headers?:array>,query?:array,timeout?:float,connection_timeout?:float} $config + * @return void + * phpcs:enable Generic.Files.LineLength + */ + private function setConfiguration(array $config) + { + $default_config = [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + // auth string is array of 0: user, 1: password, 2: auth type + if (!empty($config['auth']) && is_array($config['auth'])) { + $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['http_errors']) || + !is_bool($config['http_errors']) + ) { + $config['http_errors'] = true; + } + if (!empty($config['base_uri'])) { + if (($parsed_base_uri = $this->parseUrl($config['base_uri'])) !== false) { + $this->parsed_base_uri = $parsed_base_uri; + $config['base_uri'] = $config['base_uri']; + } + } + // general headers + if (!empty($config['headers'])) { + // seat the key lookup with lower keys + foreach (array_keys($config['headers']) as $key) { + if (isset($this->headers_named[strtolower((string)$key)])) { + continue; + } + $this->headers_named[strtolower((string)$key)] = (string)$key; + } + } + // timeout (must be numeric) + if (!empty($config['timeout']) && !is_numeric($config['timeout'])) { + $config['timeout'] = 0; + } + if (!empty($config['connection_timeout']) && !is_numeric($config['connection_timeout'])) { + $config['connection_timeout'] = 300; + } + + $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 + + /** + * From: https://github.com/guzzle/psr7/blob/a70f5c95fb43bc83f07c9c948baa0dc1829bf201/src/Uri.php#L106C5-L132C6 + * guzzle/psr7::parse + * + * convert the url to valid sets + * + * @param string $url + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string}|false + */ + private function parseUrl(string $url): array|false + { + // If IPv6 + $prefix = ''; + if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) { + /** @var array{0:string, 1:string, 2:string} $matches */ + $prefix = $matches[1]; + $url = $matches[2]; + } + + /** @var string $encodedUrl */ + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%usD', + static function ($matches) { + return urlencode($matches[0]); + }, + $url + ); + + $result = parse_url($prefix . $encodedUrl); + + if ($result === false) { + return false; + } + + /** @var callable $caller */ + $caller = 'urldecode'; + return array_map($caller, $result); + } + + /** + * build back the URL based on the parsed URL scheme + * NOTE: this is only a sub implementation + * + * phpcs:disable Generic.Files.LineLength + * @param array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} $parsed_url + * @param bool $remove_until_slash [default=false] + * @param bool $add_query [default=false] + * @param bool $add_fragment [default=false] + * @return string + * phpcs:enable Generic.Files.LineLength + */ + private function buildUrl( + array $parsed_url, + bool $remove_until_slash = false, + bool $add_query = false, + bool $add_fragment = false + ): string { + $url = ''; + // scheme has : + if (!empty($parsed_url['scheme'])) { + $url .= $parsed_url['scheme'] . ':'; + } + // host + port = authority + if (!empty($parsed_url['host'])) { + $url .= '//'; + $url .= $parsed_url['host'] ?? ''; + if (!empty($parsed_url['port'])) { + $url .= ':' . $parsed_url['port']; + } + } + // remove the last part "/.." because we do not end with "/" + if ($remove_until_slash) { + $url_path = $parsed_url['path'] ?? ''; + if (($lastSlashPos = strrpos($url_path, '/')) !== false) { + $url .= substr($url_path, 0, $lastSlashPos + 1); + } + } else { + $url .= $parsed_url['path'] ?? ''; + } + // only on demand + if ($add_query && !empty($parsed_url['query'])) { + $url .= '?' . $parsed_url['query']; + } + if ($add_fragment && !empty($parsed_url['fragment'])) { + $url .= '#' . $parsed_url['fragment']; + } + return $url; + } + + // MARK: query, params and headers convert + + /** + * Build URL with base url and parameters + * + * @param string $url_req to send + * @param null|array $query any optional parameters to send + * @return string the fully build URL + */ + private function buildQuery(string $url_req, null|array $query = null): string + { + if (($parsed_url = $this->parseUrl($url_req)) !== false) { + $this->parsed_url = $parsed_url; + } + $url = $url_req; + if ( + !empty($this->config['base_uri']) && + empty($this->parsed_url['scheme']) + ) { + if (str_ends_with($this->config['base_uri'], '/')) { + $url = $this->config['base_uri'] . $url_req; + } else { + // remove until last / and add url, strip leading / if set + // remove last "/" part until we are at the domain + // if we do not start with http(s):// then assume blank + // NOTE any fragments or params will get dropped, only path will remain + $url = $this->buildUrl($this->parsed_base_uri, remove_until_slash: true) . $url_req; + } + if (($parsed_url = $this->parseUrl($url)) !== false) { + $this->parsed_url = $parsed_url; + } + } + // build query with global query + // any query set in the base_url or url_req will be overwritten + if (!empty($this->config['query'])) { + // add current query if set + // for params: if foo[0] then we ADD as php array type + // note that this has to be done on the user side, we just merge and local overrides global + $query = array_merge($this->config['query'], $query ?? []); + } + if (is_array($query)) { + $query = http_build_query($query, '', '&', PHP_QUERY_RFC3986); + } + // add the params to the url + if (!empty($query)) { + // if the url_url has a query or a a fragment, + // we need to build that url new + // $parsed_url = false; + if (!empty($this->parsed_url['query']) || !empty($this->parsed_url['framgent'])) { + $url = $this->buildUrl($this->parsed_url); + } + $url .= '?' . $query; + // fragments are ignored + // if (!empty($this->parsed_url['fragment'])) { + // $url .= '#' . $parsed_url['fragment']; + // } + } + // parse again with current url + if ($url != $url_req) { + if (($parsed_url = $this->parseUrl($url)) !== false) { + $this->parsed_url = $parsed_url; + } + } + + return $url; + } + + /** + * Convert array body data to json type string + * + * @param string|array $body + * @return string + */ + private function convertPayloadData(string|array $body): string + { + // 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 ?? ''; + } + + /** + * header convert from array key -> value to string list + * if the key value is numeric, it is assumed this is an array string list only + * Note: this should not be the case + * + * @param array> $headers + * @return array + */ + private function convertHeaders(array $headers): array + { + $return_headers = []; + $header_keys = []; + foreach ($headers as $key => $value) { + if (!is_string($key)) { + // TODO: throw error + continue; + } + // bad if not valid header key + if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) { + 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)) { + $value = join(', ', $value); + } + $value = trim((string)$value, " \t"); + // header values must be valid + if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { + 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; + } + // remove empty entries + return $return_headers; + } + + /** + * default headers that are always set + * Authorization + * User-Agent + * + * @param array> $headers already set headers + * @param ?string $auth_basic_header + * @return array> + */ + private function buildDefaultHeaders(array $headers = [], ?string $auth_basic_header = ''): array + { + // 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')])) { + $headers['User-Agent'] = 'CoreLibsUrlRequestCurl/' . self::MAJOR_VERSION; + } + return $headers; + } + + /** + * Build headers, combine with global headers of they are set + * + * @param null|array> $headers + * @param ?string $auth_basic_header + * @return 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(auth_basic_header: $auth_basic_header); + } + // merge master headers with sub headers, sub headers overwrite master headers + if (!empty($this->config['headers'])) { + // we need to build the current headers as a lookup table + $headers_lookup = []; + foreach (array_keys($headers) as $key) { + $headers_lookup[strtolower((string)$key)] = (string)$key; + } + // add config headers if not set in local header + foreach ($this->headers_named as $header_key => $key) { + // is set local, use this, else use global + if (isset($headers_lookup[$header_key])) { + continue; + } + $headers[$key] = $this->config['headers'][$key]; + } + } + $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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Request options + * @return array{auth:null|array{0:string,1:string,2:string},headers:null|array>,query:null|array,body:null|string|array,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 + * + * 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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Request options + * @return array{code:string,headers:array>,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, + array $options, + ): array { + // 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::jsonConvertArrayTo([ + 'status' => 'ERROR', + 'code' => 'R001', + 'type' => 'InvalidRequestType', + 'message' => 'Invalid request type set: ' . $type, + 'context' => [ + 'type' => $type, + '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, [ + '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); + } elseif (in_array($type, self::CUSTOM_REQUESTS)) { + 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) && $options['body'] !== null) { + curl_setopt($handle, CURLOPT_POSTFIELDS, $this->convertPayloadData($options['body'])); + } + // reset all headers before we start the call + $this->received_headers = []; + // run curl execute + $http_result = $this->handleCurlExec($handle); + // for debug + // print "CURLINFO_HEADER_OUT:
" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "
"; + // get response code and bail on not authorized + $http_response = $this->handleCurlResponse($handle, $http_result, $options['http_errors']); + // close handler + $this->handleCurlClose($handle); + // return response and result + return [ + 'code' => (string)$http_response, + 'headers' => $this->received_headers, + 'content' => (string)$http_result + ]; + } + + // MARK: curl init + + /** + * Handel curl init and errors + * + * @param string $url + * @return \CurlHandle + * @throws \RuntimeException if curl could not be initialized + */ + private function handleCurleInit(string $url): \CurlHandle + { + $handle = curl_init($url); + if ($handle !== false) { + return $handle; + } + // throw Error here with all codes + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => 'FAILURE', + 'code' => 'C001', + 'type' => 'CurlInitError', + 'message' => 'Failed to init curl with url: ' . $url, + 'context' => [ + 'url' => $url, + ], + ]), + 0, + ); + } + + // MARK: set curl options and header collector + + /** + * set the default curl options + * + * headers array: do not split into "key" => "value", they must be "key: value" + * + * @param \CurlHandle $handle + * @param array $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, array $auth_data): void + { + // 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); + } + // curl_setopt($handle, CURLOPT_FAILONERROR, true); + // return response as string and not just HTTP_OK + curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); + // for debug only + curl_setopt($handle, CURLINFO_HEADER_OUT, true); + // curl_setopt($handle, CURLOPT_HEADER, true); + // collect the current request headers + curl_setopt($handle, CURLOPT_HEADERFUNCTION, [$this, 'collectCurlHttpHeaders']); + // if any timeout <1 + $timeout_requires_no_signal = false; + // if we have a timeout signal + if (!empty($this->config['timeout'])) { + $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 = $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') { + curl_setopt($handle, CURLOPT_NOSIGNAL, true); + } + } + + /** + * Collect HTTP headers + * They will be reset before each call + * + * @param \CurlHandle $curl current curl handle + * @param string $header header string to parse + * @return int size of current line of header + */ + private function collectCurlHttpHeaders(\CurlHandle $curl, string $header): int + { + $len = strlen($header); + $header = explode(':', $header, 2); + if (count($header) < 2) { + // ignore invalid headers + return $len; + } + $this->received_headers[strtolower(trim($header[0]))][] = trim($header[1]); + return $len; + } + + // MARK: Curl Exception handler + + /** + * handles any CURL execute and on error throws a correct error message + * + * @param \CurlHandle $handle Curl handler + * @return string Return content as string, if False will throw exception + * will only return HTTP_OK if CURLOPT_RETURNTRANSFER is turned off + * @throws \RuntimeException if the connection had an error + */ + private function handleCurlExec(\CurlHandle $handle): string + { + // execute query + $http_result = curl_exec($handle); + if ($http_result === true) { + // only if CURLOPT_RETURNTRANSFER is turned off + return (string)self::HTTP_OK; + } elseif ($http_result !== false) { + return $http_result; + } + $url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL); + $errno = curl_errno($handle); + $message = curl_error($handle); + switch ($errno) { + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_OPERATION_TIMEOUTED: + $message = 'Could not connect to server (' . $url . '). Please check your ' + . 'internet connection and try again. [' . $message . ']'; + break; + case CURLE_SSL_PEER_CERTIFICATE: + $message = 'Could not verify SSL certificate. Please make sure ' + . 'that your network is not intercepting certificates. ' + . '(Try going to ' . $url . 'in your browser.) ' + . '[' . $message . ']'; + break; + case 0: + default: + $message = 'Unexpected error communicating with server: ' . $message; + } + + // throw an error like in the normal reqeust, but set to CURL error + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => 'FAILURE', + 'code' => 'C002', + 'type' => 'CurlExecError', + 'message' => $message, + 'context' => [ + 'url' => $url, + 'errno' => $errno, + 'message' => $message, + ], + ]), + $errno + ); + } + + // MARK: curl response handler + + /** + * Handle curl response, will throw exception on anything that is lower 400 + * can be turned off by setting http_errors to false + * + * @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 if http_errors is true then will throw exception on any response code >= 400 + */ + private function handleCurlResponse( + \CurlHandle $handle, + string $http_result, + ?bool $http_errors + ): string { + $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); + if ( + 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); + // throw Error here with all codes + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => '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' => [ + '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 + ); + } + + /** + * close the current curl handle + * + * @param \CurlHandle $handle + * @return void + */ + private function handleCurlClose(\CurlHandle $handle): void + { + curl_close($handle); + } + + // ********************************************************************* + // MARK: PUBLIC METHODS + // ********************************************************************* + + // MARK: get class vars + + /** + * get the config array with all settings + * + * @return array all current config settings + */ + public function getConfig(): array + { + return $this->config; + } + + /** + * Return the full url as it was sent + * + * @return string url sent + */ + public function getUrlSent(): string + { + return $this->url; + } + + /** + * get the parsed url + * + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} + */ + public function getUrlParsedSent(): array + { + return $this->parsed_url; + } + + /** + * Return the full headers as they where sent + * + * @return array + */ + public function getHeadersSent(): array + { + return $this->headers; + } + + // MARK: set/remove for global headers + + /** + * set, add or overwrite header + * On default this will overwrite header, and not set + * + * @param array> $header + * @param bool $add [default=false] if set will add header to existing value + * @return void + */ + public function setHeaders(array $header, bool $add = false): void + { + foreach ($header as $key => $value) { + // check header previously set + if (isset($this->headers_named[strtolower($key)])) { + $header_key = $this->headers_named[strtolower($key)]; + if ($add) { + // for this add we always add array on the right side + if (!is_array($value)) { + $value = (array)$value; + } + // if not array, rewrite entry to array + if (!is_array($this->config['headers'][$header_key])) { + $this->config['headers'][$header_key] = [ + $this->config['headers'][$header_key] + ]; + } + $this->config['headers'][$header_key] = array_merge( + $this->config['headers'][$header_key], + $value + ); + } else { + $this->config['headers'][$header_key] = $value; + } + } else { + $this->headers_named[strtolower($key)] = $key; + $this->config['headers'][$key] = $value; + } + } + } + + /** + * remove header entry + * if key is only set then match only key, if both are set both sides must match + * + * @param array> $remove_headers + * @return void + */ + public function removeHeaders(array $remove_headers): void + { + foreach ($remove_headers as $key => $value) { + if (!isset($this->headers_named[strtolower($key)])) { + continue; + } + $header_key = $this->headers_named[strtolower($key)]; + if (!isset($this->config['headers'][$header_key])) { + continue; + } + // full remove + if ( + empty($value) || + ( + ( + // array both sides = equal + // string both sides = equal + (is_array($value) && is_array($this->config['headers'][$header_key])) || + (is_string($value) && is_string($this->config['headers'][$header_key])) + ) && + $value == $this->config['headers'][$header_key] + ) + ) { + unset($this->config['headers'][$header_key]); + unset($this->headers_named[$header_key]); + } elseif ( + // string value, array keys = in + // or both array and not a full match in the one before + (is_string($value) || is_array($value)) && + is_array($this->config['headers'][$header_key]) + ) { + // part remove of key, value must be array + if (!is_array($value)) { + $value = [$value]; + } + // array values so we rewrite the key pos + $this->config['headers'][$header_key] = array_values(array_diff( + $this->config['headers'][$header_key], + $value + )); + } + } + } + + // MARK: update/set base url + + /** + * Update or set the base url set + * if empty will unset the base url + * + * @param string $base_uri + * @return void + */ + public function setBaseUri(string $base_uri): void + { + $this->config['base_uri'] = $base_uri; + $this->parsed_base_uri = []; + if (!empty($base_uri)) { + if (($parsed_base_uri = $this->parseUrl($base_uri)) !== false) { + $this->parsed_base_uri = $parsed_base_uri; + } + } + } + + // MARK: main public call interface + + /** + * combined set call for any type of request with options type parameters + * + * phpcs:disable Generic.Files.LineLength + * @param string $type + * @param string $url + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options + * @return array{code:string,headers:array>,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 + */ + public function request(string $type, string $url, array $options = []): array + { + // 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); + return $this->curlRequest( + $type, + $url, + $options, + ); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/UrlRequests/CurlTrait.php b/www/lib/CoreLibs/UrlRequests/CurlTrait.php new file mode 100644 index 00000000..a3b0cbe5 --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/CurlTrait.php @@ -0,0 +1,128 @@ +>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Request options + * @return array{code:string,headers:array>,content:string} [default=[]] Result code, headers and content as array, content is json + * @throws \UnexpectedValueException on missing body data when body data is needed + */ + abstract public function request(string $type, string $url, array $options = []): array; + + /** + * Makes an request to the target url via curl: GET + * Returns result as string (json) + * + * @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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} [default=[]] Result code, headers and content as array, content is json + */ + public function get(string $url, array $options = []): array + { + return $this->request( + "get", + $url, + $options, + ); + } + + /** + * Makes an request to the target url via curl: POST + * Returns result as string (json) + * + * @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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function post(string $url, array $options): array + { + return $this->request( + "post", + $url, + $options, + ); + } + + /** + * Makes an request to the target url via curl: PUT + * Returns result as string (json) + * + * @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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function put(string $url, array $options): array + { + return $this->request( + "put", + $url, + $options, + ); + } + + /** + * Makes an request to the target url via curl: PATCH + * Returns result as string (json) + * + * @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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function patch(string $url, array $options): array + { + return $this->request( + "patch", + $url, + $options, + ); + } + + /** + * Makes an request to the target url via curl: DELETE + * Returns result as string (json) + * Note that DELETE body is optional + * + * @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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} [default=[]] Result code, headers and content as array, content is json + */ + public function delete(string $url, array $options = []): array + { + return $this->request( + "delete", + $url, + $options, + ); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php new file mode 100644 index 00000000..bc6bb0e3 --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php @@ -0,0 +1,83 @@ + all current config settings + */ + public function getConfig(): array; + + /** + * Return the full url as it was sent + * + * @return string url sent + */ + public function getUrlSent(): string; + + /** + * get the parsed url + * + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} + */ + public function getUrlParsedSent(): array; + + /** + * Return the full headers as they where sent + * + * @return array + */ + public function getHeadersSent(): array; + + /** + * set, add or overwrite header + * On default this will overwrite header, and not set + * + * @param array> $header + * @param bool $add [default=false] if set will add header to existing value + * @return void + */ + public function setHeaders(array $header, bool $add = false): void; + + /** + * remove header entry + * if key is only set then match only key, if both are set both sides must match + * + * @param array $remove_headers + * @return void + */ + public function removeHeaders(array $remove_headers): void; + + /** + * Update the base url set, if empty will unset the base url + * + * @param string $base_uri + * @return void + */ + public function setBaseUri(string $base_uri): void; + + /** + * combined set call for any type of request with options type parameters + * + * phpcs:disable Generic.Files.LineLength + * @param string $type + * @param string $url + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options + * @return array{code:string,headers:array>,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 + */ + public function request(string $type, string $url, array $options = []): array; +} + +// __END__