From 50a4b88f5551820e85ec39c8157d1893e5d80d38 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 21 Oct 2024 09:25:39 +0900 Subject: [PATCH 01/23] UrlRequests\Curl class Basic interface class to CURL calls Open: clean up and check code is neutral write tests, for this we need a running localhost server for tests to request to --- .../CoreLibsUrlRequestsCurlTest.php | 31 ++ www/admin/class_test.url-requests.php | 44 ++ www/lib/CoreLibs/UrlRequests/Curl.php | 405 ++++++++++++++++++ .../Interface/RequestsInterface.php | 75 ++++ 4 files changed, 555 insertions(+) create mode 100644 4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php create mode 100644 www/admin/class_test.url-requests.php create mode 100644 www/lib/CoreLibs/UrlRequests/Curl.php create mode 100644 www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php new file mode 100644 index 00000000..7c39d699 --- /dev/null +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -0,0 +1,31 @@ +markTestIncomplete( + 'UrlRequests\Curl Tests have not yet been implemented' + ); + } +} + +// __END__ diff --git a/www/admin/class_test.url-requests.php b/www/admin/class_test.url-requests.php new file mode 100644 index 00000000..94d64614 --- /dev/null +++ b/www/admin/class_test.url-requests.php @@ -0,0 +1,44 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); + +$client = new Curl(); + +$PAGE_NAME = 'TEST CLASS: URL REQUESTS CURL'; +print ""; +print "" . $PAGE_NAME . ""; +print ""; +print '
Class Test Master
'; +print '

' . $PAGE_NAME . '

'; + +$url = 'https://soba.egplusww.jp'; + +$data = $client->requestGet($url, []); + +print ""; + +// __END__ diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php new file mode 100644 index 00000000..73e013ab --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -0,0 +1,405 @@ + all the valid request type */ + private const VALID_REQUEST_TYPES = ["get", "post", "put", "delete"]; + /** @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 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 string auth ident as "email:api_token */ + private string $auth_ident; + /** @var bool if flagged to true, will raise an exception on failed authentication */ + private bool $exception_on_not_authorized = false; + + /** + * init class with auth ident token + * + * @param ?string $auth_ident [defaul=null] String to send for authentication, optional + * @param bool $exception_on_not_authorized [default=false] If set to true + * will raise excepion on http auth error + */ + public function __construct(?string $auth_ident = null, bool $exception_on_not_authorized = false) + { + if (is_string($auth_ident)) { + $this->auth_ident = $auth_ident; + } + $this->exception_on_not_authorized = $exception_on_not_authorized; + } + + // ********************************************************************* + // MARK: PRIVATE METHODS + // ********************************************************************* + + // MARK: query and params convert + + /** + * Convert Query params and combine with url + * + * @param string $url + * @param null|string|array $query + * @return string + */ + private function convertQuery(string $url, null|string|array $query = null): string + { + // conert to URL encoded query if array + if (is_array($query)) { + $query = http_build_query($query); + } + // add the params to the url + if (!empty($query)) { + // add ? if the string doesn't strt with one + // check if URL has "?", if yes, add as "&" block + $param_prefix = '?'; + if (strstr($url, '?') !== false) { + $param_prefix = '&'; + } + // if set, strip first character + if (str_starts_with($query, '?') || str_starts_with($query, '&')) { + $query = substr($query, 1); + } + // build url string + $url .= $param_prefix . $query; + } + return $url; + } + + /** + * Convert array params to json type string + * + * @param string|array $params + * @return string + */ + private function convertParams(string|array $params): string + { + // convert to string as JSON block if it is an array + if (is_array($params)) { + $params = Json::jsonConvertArrayTo($params); + } + return $params; + } + + // MARK: main curl request + + /** + * Overall reequest call + * + * @param string $type get, post, put, delete: if not set or invalid throw error + * @param string $url The URL being requested, + * including domain and protocol + * @param array $headers [default=[]] Headers to be used in the request + * @param string|null $params [default=null] Optional url parameters for post/put requests + * @return array{code:string,content:string} + */ + private function curlRequest(string $type, string $url, array $headers = [], ?string $params = null): array + { + if (!in_array($type, self::VALID_REQUEST_TYPES)) { + throw new RuntimeException( + json_encode([ + 'status' => 'FAILURE', + 'code' => 'C003', + 'type' => 'InvalidRequestType', + 'message' => 'Invalid request type set: ' . $type, + 'context' => [ + 'url' => $url, + 'type' => $type, + ], + ]) ?: '', + 0, + ); + } + // init curl handle + $handle = $this->handleCurleInit($url); + // set the standard curl options + if ($headers !== []) { + $this->setCurlOptions($handle, $headers); + } + // for post we set POST option + if ($type == "post") { + curl_setopt($handle, CURLOPT_POST, true); + } elseif (in_array($type, ["put", "delete"])) { + curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type)); + } + if (in_array($type, ["post", "put"]) && !empty($params)) { + curl_setopt($handle, CURLOPT_POSTFIELDS, $params); + } + // run curl execute + $http_result = $this->handleCurlExec($handle); + // get response code and bail on not authorized + $http_response = $this->handleCurlResponse($http_result, $handle); + // return response and result + return [ + 'code' => (string)$http_response, + 'content' => (string)$http_result + ]; + } + + // MARK: curl request helpers + + /** + * Handel curl init and errors + * + * @param string $url + * @return \CurlHandle + */ + 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_encode([ + 'status' => 'FAILURE', + 'code' => 'C001', + 'type' => 'CurlInitError', + 'message' => 'Failed to init curl with url: ' . $url, + 'context' => [ + 'url' => $url, + ], + ]) ?: '', + 0, + ); + } + + /** + * 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 + * @return void + */ + private function setCurlOptions(\CurlHandle $handle, array $headers): void + { + if (!empty($this->auth_ident)) { + curl_setopt($handle, CURLOPT_USERPWD, $this->auth_ident); + } + curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); + // curl_setopt($handle, CURLOPT_FAILONERROR, true); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); + // for debug only + curl_setopt($handle, CURLINFO_HEADER_OUT, true); + } + + // MARK: Curl Exception handler + + /** + * handles any CURL execute and on error throws a correct error message + * + * @param \CurlHandle $handle + * @return string + */ + private function handleCurlExec(\CurlHandle $handle): string + { + // execute query + $http_result = curl_exec($handle); + if ($http_result === true) { + 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_encode([ + 'status' => 'FAILURE', + 'code' => 'C002', + 'type' => 'CurlError', + 'message' => $message, + 'context' => [ + 'url' => $url, + 'errno' => $errno, + 'message' => $message, + ], + ]) ?: '', + $errno + ); + } + + // MARK: curl response hanlder + + /** + * Handle curl response and not auth 401 errors + * + * @param string $http_result + * @param \CurlHandle $handle + * @return string + */ + private function handleCurlResponse( + string $http_result, + \CurlHandle $handle + ): string { + $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); + if ( + !$this->exception_on_not_authorized || + $http_response !== self::HTTP_NOT_AUTHORIZED + ) { + return (string)$http_response; + } + $err = curl_errno($handle); + // extract all the error codes + $result_ar = json_decode((string)$http_result, true); + + $url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL); + $error_status = 'ERROR'; + $error_code = $http_response; + $error_type = 'UnauthorizedRequest'; + $message = 'Request could not be finished successfully because of an authorization error'; + + // throw Error here with all codes + throw new RuntimeException( + json_encode([ + 'status' => $error_status, + 'code' => $error_code, + 'type' => $error_type, + 'message' => $message, + 'context' => [ + 'url' => $url, + 'result' => $result_ar, + ], + ]) ?: '', + $err + ); + } + + // ********************************************************************* + // MARK: PUBLIC METHODS + // ********************************************************************* + + // MARK: request methods + + /** + * 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 $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] String to pass on as GET, + * if array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestGet(string $url, array $headers = [], null|string|array $query = null): array + { + return $this->curlRequest("get", $this->convertQuery($url, $query), $headers); + } + + /** + * 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 string|array $params String to pass on as POST + * @param array $headers Headers to be used in the request + * @param null|string|array $query Optinal query parameters, array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestPost( + string $url, + string|array $params, + array $headers, + null|string|array $query = null + ): array { + return $this->curlRequest( + "post", + $this->convertQuery($url, $query), + $headers, + $this->convertParams($params) + ); + } + + /** + * 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 string|array $params String to pass on as POST + * @param array $headers Headers to be used in the request + * @param null|string|array $query Optinal query parameters, array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestPut( + string $url, + string|array $params, + array $headers, + null|string|array $query = null + ): array { + return $this->curlRequest( + "put", + $this->convertQuery($url, $query), + $headers, + $this->convertParams($params) + ); + } + + /** + * Makes an request to the target url via curl: DELETE + * Returns result as string (json) + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] String to pass on as GET, + * if array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestDelete(string $url, array $headers = [], null|string|array $query = null): array + { + return $this->curlRequest("delete", $this->convertQuery($url, $query), $headers); + } +} + +// __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..7e06120c --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php @@ -0,0 +1,75 @@ + $headers Headers to be used in the request + * @param null|string|array $query String to pass on as GET, if array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestGet(string $url, array $headers, null|string|array $query = null): array; + + /** + * 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 string|array $params String to pass on as POST + * @param array $headers Headers to be used in the request + * @param null|string|array $query URL query parameters + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestPost( + string $url, + string|array $params, + array $headers, + null|string|array $query = null + ): array; + + /** + * 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 string|array $params String to pass on as POST + * @param array $headers Headers to be used in the request + * @param null|string|array $query String to pass on as GET, if array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestPut( + string $url, + string|array $params, + array $headers, + null|string|array $query = null + ): array; + + /** + * Makes an request to the target url via curl: DELETE + * Returns result as string (json) + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array $headers Headers to be used in the request + * @param null|string|array $query String to pass on as GET, if array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestDelete(string $url, array $headers, null|string|array $query = null): array; +} + +// __END__ From 3c5200cd99923439d4f601872e80e03b3aa5568b Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 21 Oct 2024 09:32:20 +0900 Subject: [PATCH 02/23] Test run for Curl URL Requests --- www/admin/class_test.php | 1 + ....url-requests.php => class_test.url-requests.curl.php} | 4 ++++ www/lib/CoreLibs/UrlRequests/Curl.php | 8 ++++---- 3 files changed, 9 insertions(+), 4 deletions(-) rename www/admin/{class_test.url-requests.php => class_test.url-requests.curl.php} (91%) diff --git a/www/admin/class_test.php b/www/admin/class_test.php index 315effd3..6c173f18 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -117,6 +117,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', ]; diff --git a/www/admin/class_test.url-requests.php b/www/admin/class_test.url-requests.curl.php similarity index 91% rename from www/admin/class_test.url-requests.php rename to www/admin/class_test.url-requests.curl.php index 94d64614..d34a85d6 100644 --- a/www/admin/class_test.url-requests.php +++ b/www/admin/class_test.url-requests.curl.php @@ -39,6 +39,10 @@ $url = 'https://soba.egplusww.jp'; $data = $client->requestGet($url, []); +print "
"; +print "RESPONSE:
" . print_r($data, true) . "
"; +print "
"; + print ""; // __END__ diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index 73e013ab..6e088c4e 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -139,9 +139,7 @@ class Curl implements Interface\RequestsInterface // init curl handle $handle = $this->handleCurleInit($url); // set the standard curl options - if ($headers !== []) { - $this->setCurlOptions($handle, $headers); - } + $this->setCurlOptions($handle, $headers); // for post we set POST option if ($type == "post") { curl_setopt($handle, CURLOPT_POST, true); @@ -205,7 +203,9 @@ class Curl implements Interface\RequestsInterface if (!empty($this->auth_ident)) { curl_setopt($handle, CURLOPT_USERPWD, $this->auth_ident); } - curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); + if ($headers !== []) { + curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); + } // curl_setopt($handle, CURLOPT_FAILONERROR, true); curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); // for debug only From f781b5e55f637ef1884fa72e86ea67ab547d54bb Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 21 Oct 2024 09:52:49 +0900 Subject: [PATCH 03/23] Name update for params/query Order in methods url: mandatory payload: mandatory in post/put header = [] query = "" old "params" -> "payload" --- www/lib/CoreLibs/UrlRequests/Curl.php | 34 +++++++++---------- .../Interface/RequestsInterface.php | 34 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index 6e088c4e..ed3f6294 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -93,16 +93,16 @@ class Curl implements Interface\RequestsInterface } /** - * Convert array params to json type string + * Convert array payload data to json type string * - * @param string|array $params + * @param string|array $payload * @return string */ - private function convertParams(string|array $params): string + private function convertPayloadData(string|array $payload): string { // convert to string as JSON block if it is an array - if (is_array($params)) { - $params = Json::jsonConvertArrayTo($params); + if (is_array($payload)) { + $params = Json::jsonConvertArrayTo($payload); } return $params; } @@ -341,22 +341,22 @@ class Curl implements Interface\RequestsInterface * * @param string $url The URL being requested, * including domain and protocol - * @param string|array $params String to pass on as POST - * @param array $headers Headers to be used in the request - * @param null|string|array $query Optinal query parameters, array will be converted + * @param string|array $payload Data to pass on as POST + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] Optinal query parameters, array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ public function requestPost( string $url, - string|array $params, - array $headers, + string|array $payload, + array $headers = [], null|string|array $query = null ): array { return $this->curlRequest( "post", $this->convertQuery($url, $query), $headers, - $this->convertParams($params) + $this->convertPayloadData($payload) ); } @@ -366,22 +366,22 @@ class Curl implements Interface\RequestsInterface * * @param string $url The URL being requested, * including domain and protocol - * @param string|array $params String to pass on as POST - * @param array $headers Headers to be used in the request - * @param null|string|array $query Optinal query parameters, array will be converted + * @param string|array $payload String to pass on as POST + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] Optinal query parameters, array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ public function requestPut( string $url, - string|array $params, - array $headers, + string|array $payload, + array $headers = [], null|string|array $query = null ): array { return $this->curlRequest( "put", $this->convertQuery($url, $query), $headers, - $this->convertParams($params) + $this->convertPayloadData($payload) ); } diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php index 7e06120c..1d649199 100644 --- a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php +++ b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php @@ -17,11 +17,12 @@ interface RequestsInterface * * @param string $url The URL being requested, * including domain and protocol - * @param array $headers Headers to be used in the request - * @param null|string|array $query String to pass on as GET, if array will be converted + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] String to pass on as GET, + * if array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ - public function requestGet(string $url, array $headers, null|string|array $query = null): array; + public function requestGet(string $url, array $headers = [], null|string|array $query = null): array; /** * Makes an request to the target url via curl: POST @@ -29,15 +30,15 @@ interface RequestsInterface * * @param string $url The URL being requested, * including domain and protocol - * @param string|array $params String to pass on as POST - * @param array $headers Headers to be used in the request - * @param null|string|array $query URL query parameters + * @param string|array $payload Data to pass on as POST + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] Optinal query parameters, array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ public function requestPost( string $url, - string|array $params, - array $headers, + string|array $payload, + array $headers = [], null|string|array $query = null ): array; @@ -47,15 +48,15 @@ interface RequestsInterface * * @param string $url The URL being requested, * including domain and protocol - * @param string|array $params String to pass on as POST - * @param array $headers Headers to be used in the request - * @param null|string|array $query String to pass on as GET, if array will be converted + * @param string|array $payload Data to pass on as POST + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] Optinal query parameters, array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ public function requestPut( string $url, - string|array $params, - array $headers, + string|array $payload, + array $headers = [], null|string|array $query = null ): array; @@ -65,11 +66,12 @@ interface RequestsInterface * * @param string $url The URL being requested, * including domain and protocol - * @param array $headers Headers to be used in the request - * @param null|string|array $query String to pass on as GET, if array will be converted + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] String to pass on as GET, + * if array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ - public function requestDelete(string $url, array $headers, null|string|array $query = null): array; + public function requestDelete(string $url, array $headers = [], null|string|array $query = null): array; } // __END__ From 66dc72ec671d9bfe6db5b860894ffffbbae1a67f Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 21 Oct 2024 10:19:29 +0900 Subject: [PATCH 04/23] phpunit test text doc typo fix --- 4dev/tests/DB/CoreLibsDBExtendedArrayIOTest.php | 1 - 1 file changed, 1 deletion(-) 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 From 1bff19f4b680e4eb2cb18ee211ba49c549b8c9a2 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 28 Oct 2024 17:05:49 +0900 Subject: [PATCH 05/23] Update UrlRequests with patch, admin test page for it Also update delete to have optional body (content) --- www/admin/UrlReqeusts.target.php | 35 ++++++++ www/admin/class_test.url-requests.curl.php | 83 ++++++++++++++++++- www/lib/CoreLibs/UrlRequests/Curl.php | 54 ++++++++++-- .../Interface/RequestsInterface.php | 9 +- 4 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 www/admin/UrlReqeusts.target.php diff --git a/www/admin/UrlReqeusts.target.php b/www/admin/UrlReqeusts.target.php new file mode 100644 index 00000000..79feafde --- /dev/null +++ b/www/admin/UrlReqeusts.target.php @@ -0,0 +1,35 @@ + BASE . LOG, + 'log_file_id' => $LOG_FILE_ID, + 'log_per_date' => true, +]); + +$http_headers = array_filter($_SERVER, function ($value, $key) { + if (str_starts_with($key, 'HTTP_')) { + return true; + } +}, ARRAY_FILTER_USE_BOTH); + +$file_get = file_get_contents('php://input'); +// str_replace('\"', '"', trim($file_get, '"')); + +$log->debug('SERVER', $log->prAr($_SERVER)); +$log->debug('HEADERS', $log->prAr($http_headers)); +$log->debug('POST', $log->prAr($_POST)); +$log->debug('PHP-INPUT', $log->prAr($file_get)); + +print Json::jsonConvertArrayTo([ + 'HTTP_HEADERS' => $http_headers, + "_GET" => $_GET, + "_POST" => Json::jsonConvertToArray($file_get), +]); + +$log->debug('[END]', '=========================================>'); + +// __END__ diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index d34a85d6..32d6931a 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -35,13 +35,88 @@ print ""; print ''; print '

' . $PAGE_NAME . '

'; -$url = 'https://soba.egplusww.jp'; - -$data = $client->requestGet($url, []); +print "
"; +$data = $client->requestGet( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + . '?other=get_a', + ['test-header: ABC', 'request-type: _GET'], + ['foo' => 'BAR'] +); +print "_GET RESPONSE:
" . print_r($data, true) . "
"; print "
"; -print "RESPONSE:
" . print_r($data, true) . "
"; +$data = $client->requestPost( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + . '?other=post_a', + ['payload' => 'data post'], + [ + 'Content-Type: application/json', + 'Accept: application/json', + 'test-header: ABC', + 'info-request-type: _POST' + ], + ['foo' => 'BAR post'], +); +print "_POST RESPONSE:
" . print_r($data, true) . "
"; + print "
"; +$data = $client->requestPut( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + . '?other=put_a', + ['payload' => 'data put'], + [ + 'Content-Type: application/json', + 'Accept: application/json', + 'test-header: ABC', + 'info-request-type: _PUT' + ], + ['foo' => 'BAR put'], +); +print "_PUT RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->requestPatch( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + . '?other=patch_a', + ['payload' => 'data patch'], + [ + 'Content-Type: application/json', + 'Accept: application/json', + 'test-header: ABC', + 'info-request-type: _PATCH' + ], + ['foo' => 'BAR patch'], +); +print "_PATCH RESPONSE:
" . print_r($data, true) . "
"; + +print "
"; +$data = $client->requestDelete( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + . '?other=delete_no_body_a', + null, + [ + 'Content-Type: application/json', + 'Accept: application/json', + 'test-header: ABC', + 'info-request-type: _DELETE' + ], + ['foo' => 'BAR delete'], +); +print "_DELETE RESPONSE:
" . print_r($data, true) . "
"; +$data = $client->requestDelete( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + . '?other=delete_body_a', + ['payload' => 'data delete'], + [ + 'Content-Type: application/json', + 'Accept: application/json', + 'test-header: ABC', + 'info-request-type: _DELETE' + ], + ['foo' => 'BAR delete'], +); +print "_DELETE RESPONSE:
" . print_r($data, true) . "
"; + print ""; diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index ed3f6294..8cf0f47c 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -15,7 +15,11 @@ use CoreLibs\Convert\Json; class Curl implements Interface\RequestsInterface { /** @var array all the valid request type */ - private const VALID_REQUEST_TYPES = ["get", "post", "put", "delete"]; + 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 int error bad request */ public const HTTP_BAD_REQUEST = 400; /** @var int error not authorized Request */ @@ -143,10 +147,10 @@ class Curl implements Interface\RequestsInterface // for post we set POST option if ($type == "post") { curl_setopt($handle, CURLOPT_POST, true); - } elseif (in_array($type, ["put", "delete"])) { + } elseif (in_array($type, self::CUSTOM_REQUESTS)) { curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type)); } - if (in_array($type, ["post", "put"]) && !empty($params)) { + if (in_array($type, self::HAVE_POST_FIELDS) && !empty($params)) { curl_setopt($handle, CURLOPT_POSTFIELDS, $params); } // run curl execute @@ -386,19 +390,55 @@ class Curl implements Interface\RequestsInterface } /** - * Makes an request to the target url via curl: DELETE + * 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 string|array $payload String to pass on as POST + * @param array $headers [default=[]] Headers to be used in the request + * @param null|string|array $query [default=null] Optinal query parameters, array will be converted + * @return array{code:string,content:string} Result code and content as array, content is json + */ + public function requestPatch( + string $url, + string|array $payload, + array $headers = [], + null|string|array $query = null + ): array { + return $this->curlRequest( + "patch", + $this->convertQuery($url, $query), + $headers, + $this->convertPayloadData($payload) + ); + } + + /** + * Makes an request to the target url via curl: DELETE + * Returns result as string (json) + * Note that DELETE payload is optional + * + * @param string $url The URL being requested, + * including domain and protocol + * @param null|string|array $payload [default=null] Data to pass on as POST * @param array $headers [default=[]] Headers to be used in the request * @param null|string|array $query [default=null] String to pass on as GET, * if array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ - public function requestDelete(string $url, array $headers = [], null|string|array $query = null): array - { - return $this->curlRequest("delete", $this->convertQuery($url, $query), $headers); + public function requestDelete( + string $url, + null|string|array $payload = null, + array $headers = [], + null|string|array $query = null + ): array { + return $this->curlRequest( + "delete", + $this->convertQuery($url, $query), + $headers, + $payload !== null ? $this->convertPayloadData($payload) : null + ); } } diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php index 1d649199..ed527edc 100644 --- a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php +++ b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php @@ -63,15 +63,22 @@ interface RequestsInterface /** * Makes an request to the target url via curl: DELETE * Returns result as string (json) + * Note that DELETE payload is optional * * @param string $url The URL being requested, * including domain and protocol + * @param null|string|array $payload [default=null] Data to pass on as POST * @param array $headers [default=[]] Headers to be used in the request * @param null|string|array $query [default=null] String to pass on as GET, * if array will be converted * @return array{code:string,content:string} Result code and content as array, content is json */ - public function requestDelete(string $url, array $headers = [], null|string|array $query = null): array; + public function requestDelete( + string $url, + null|string|array $payload = null, + array $headers = [], + null|string|array $query = null + ): array; } // __END__ From 337ebb9032946a6d414d8896c291283b923d570d Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 29 Oct 2024 18:28:07 +0900 Subject: [PATCH 06/23] Add a localhost entry to the hosts config --- www/configs/config.host.php | 1 + 1 file changed, 1 insertion(+) 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__ From d89c6d1bde9ed0374dcc6d268677f69d2d8a2f9e Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Tue, 29 Oct 2024 18:28:19 +0900 Subject: [PATCH 07/23] UrlRequests target file renamed --- www/admin/{UrlReqeusts.target.php => UrlRequests.target.php} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename www/admin/{UrlReqeusts.target.php => UrlRequests.target.php} (88%) diff --git a/www/admin/UrlReqeusts.target.php b/www/admin/UrlRequests.target.php similarity index 88% rename from www/admin/UrlReqeusts.target.php rename to www/admin/UrlRequests.target.php index 79feafde..1b5b8b2f 100644 --- a/www/admin/UrlReqeusts.target.php +++ b/www/admin/UrlRequests.target.php @@ -1,5 +1,7 @@ "file_get_contents failed"}'; // str_replace('\"', '"', trim($file_get, '"')); $log->debug('SERVER', $log->prAr($_SERVER)); From 95d567545a9ae6eec49034695cdea055d7ee1da2 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 1 Nov 2024 14:41:46 +0900 Subject: [PATCH 08/23] URL Requests via curl, a simple library --- www/lib/CoreLibs/UrlRequests/Curl.php | 784 ++++++++++++++---- www/lib/CoreLibs/UrlRequests/CurlTrait.php | 147 ++++ .../Interface/RequestInterface.php | 95 +++ .../Interface/RequestsInterface.php | 84 -- 4 files changed, 875 insertions(+), 235 deletions(-) create mode 100644 www/lib/CoreLibs/UrlRequests/CurlTrait.php create mode 100644 www/lib/CoreLibs/UrlRequests/Interface/RequestInterface.php delete mode 100644 www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index 8cf0f47c..116abd8b 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -5,21 +5,36 @@ * CREATED: 2024/9/20 * DESCRIPTION: * Curl Client for get/post/put/delete requests through the php curl inteface + * + * For anything more complex use guzzlehttp/http + * https://docs.guzzlephp.org/en/stable/index.html + * + * Requests are guzzleHttp compatible + * Config for setup is guzzleHttp compatible (except the exception_on_not_authorized) + * Any setters and getters are only for this class */ +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 + use CurlTrait; + /** @var array 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 error bad request */ public const HTTP_BAD_REQUEST = 400; /** @var int error not authorized Request */ @@ -39,92 +54,389 @@ class Curl implements Interface\RequestsInterface /** @var int http ok no content */ public const HTTP_NO_CONTENT = 204; - /** @var string auth ident as "email:api_token */ - private string $auth_ident; - /** @var bool if flagged to true, will raise an exception on failed authentication */ - private bool $exception_on_not_authorized = false; + // 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},auth_type?:int|string,auth_userpwd?:string,exception_on_not_authorized: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] + * 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, + '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 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 = []; /** - * init class with auth ident token + * see config allowe entries above * - * @param ?string $auth_ident [defaul=null] String to send for authentication, optional - * @param bool $exception_on_not_authorized [default=false] If set to true - * will raise excepion on http auth error + * @param array $config config settings to be set */ - public function __construct(?string $auth_ident = null, bool $exception_on_not_authorized = false) + public function __construct(array $config = []) { - if (is_string($auth_ident)) { - $this->auth_ident = $auth_ident; - } - $this->exception_on_not_authorized = $exception_on_not_authorized; + $this->setConfiguration($config); } // ********************************************************************* // MARK: PRIVATE METHODS // ********************************************************************* - // MARK: query and params convert + /** + * Set the main configuration + * + * phpcs:disable Generic.Files.LineLength + * @param array{auth?:array{0:string,1:string,2:string},auth_type?:int|string,auth_userpwd?:string,exception_on_not_authorized?: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 = [ + 'exception_on_not_authorized' => false, + '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'])) { + // 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': + if (!isset($config['headers']['Authorization'])) { + $config['headers']['Authorization'] = 'Basic ' . base64_encode( + $userpwd + ); + } + break; + case 'digest': + $config['auth_type'] = CURLAUTH_DIGEST; + $config['auth_userpwd'] = $userpwd; + break; + case 'ntlm': + $config['auth_type'] = CURLAUTH_NTLM; + $config['auth_userpwd'] = $userpwd; + break; + } + } + // only set if bool + if ( + !isset($config['exception_on_not_authorized']) || + !is_bool($config['exception_on_not_authorized']) + ) { + $config['exception_on_not_authorized'] = false; + } + if (!empty($config['base_uri'])) { + // TODO: this should be run through Guzzle\Psr7\Utils in future + // $config['base_uri'] = Psr7\Utils::urlFor($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: parse and build url /** - * Convert Query params and combine with url + * From: https://github.com/guzzle/psr7/blob/a70f5c95fb43bc83f07c9c948baa0dc1829bf201/src/Uri.php#L106C5-L132C6 + * guzzle/psr7::parse * - * @param string $url - * @param null|string|array $query - * @return string + * 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 convertQuery(string $url, null|string|array $query = null): string + private function parseUrl(string $url): array|false { - // conert to URL encoded query if array - if (is_array($query)) { - $query = http_build_query($query); + // 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]; } - // add the params to the url - if (!empty($query)) { - // add ? if the string doesn't strt with one - // check if URL has "?", if yes, add as "&" block - $param_prefix = '?'; - if (strstr($url, '?') !== false) { - $param_prefix = '&'; + + /** @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']; } - // if set, strip first character - if (str_starts_with($query, '?') || str_starts_with($query, '&')) { - $query = substr($query, 1); + } + // 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); } - // build url string - $url .= $param_prefix . $query; + } 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 + /** - * Convert array payload data to json type string + * Build URL with base url and parameters * - * @param string|array $payload + * @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; + } + } + 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 $payload): string + private function convertPayloadData(string|array $body): string { // convert to string as JSON block if it is an array - if (is_array($payload)) { - $params = Json::jsonConvertArrayTo($payload); + if (is_array($body)) { + $params = Json::jsonConvertArrayTo($body); } - return $params; + 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 = []; + 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)) { + // TODO throw error + continue; + } + // 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)) { + // TODO throw error + continue; + } + $return_headers[] = (string)$key . ':' . $value; + } + // remove empty entries + return $return_headers; + } + + /** + * Build headers, combine with global headers of they are set + * + * @param null|array> $headers + * @return array> + */ + private function buildHeaders(null|array $headers): array + { + // if headers is null, return empty headers, do not set default headers + if ($headers === null) { + return []; + } + // 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]; + } + } + // always add HTTP_HOST and HTTP_USER_AGENT + return $headers; } // MARK: main curl request /** - * Overall reequest call + * Overall request call * - * @param string $type get, post, put, delete: if not set or invalid throw error - * @param string $url The URL being requested, - * including domain and protocol - * @param array $headers [default=[]] Headers to be used in the request - * @param string|null $params [default=null] Optional url parameters for post/put requests - * @return array{code:string,content:string} + * @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> $headers [default=[]] Headers to be used in the request + * @param null|array $query [default=null] Optinal query parameters + * @param null|string|array $body [default=null] Data body, converted to JSON + * @return array{code:string,headers:array>,content:string} */ - private function curlRequest(string $type, string $url, array $headers = [], ?string $params = null): array - { + private function curlRequest( + string $type, + string $url, + null|array $headers = [], + null|array $query = null, + null|string|array $body = null + ): array { + $this->url = $this->buildQuery($url, $query); + $this->headers = $this->convertHeaders($this->buildHeaders($headers)); if (!in_array($type, self::VALID_REQUEST_TYPES)) { throw new RuntimeException( json_encode([ @@ -133,38 +445,47 @@ class Curl implements Interface\RequestsInterface 'type' => 'InvalidRequestType', 'message' => 'Invalid request type set: ' . $type, 'context' => [ - 'url' => $url, 'type' => $type, + 'url' => $this->url, + 'headers' => $this->headers, ], ]) ?: '', 0, ); } // init curl handle - $handle = $this->handleCurleInit($url); + $handle = $this->handleCurleInit($this->url); // set the standard curl options - $this->setCurlOptions($handle, $headers); + $this->setCurlOptions($handle, $this->headers); // 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)); } - if (in_array($type, self::HAVE_POST_FIELDS) && !empty($params)) { - curl_setopt($handle, CURLOPT_POSTFIELDS, $params); + // 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)); } + // 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($http_result, $handle); + // close handler + $this->handleCurlClose($handle); // return response and result return [ 'code' => (string)$http_response, + 'headers' => $this->received_headers, 'content' => (string)$http_result ]; } - // MARK: curl request helpers + // MARK: curl init /** * Handel curl init and errors @@ -193,6 +514,8 @@ class Curl implements Interface\RequestsInterface ); } + // MARK: set curl options and header collector + /** * set the default curl options * @@ -204,8 +527,10 @@ class Curl implements Interface\RequestsInterface */ private function setCurlOptions(\CurlHandle $handle, array $headers): void { - if (!empty($this->auth_ident)) { - curl_setopt($handle, CURLOPT_USERPWD, $this->auth_ident); + // for not Basic auth, basic auth sets its own header + if (!empty($this->config['auth_type']) && !empty($this->config['auth_userpwd'])) { + curl_setopt($handle, CURLOPT_HTTPAUTH, $this->config['auth_type']); + curl_setopt($handle, CURLOPT_USERPWD, $this->config['auth_userpwd']); } if ($headers !== []) { curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); @@ -214,6 +539,43 @@ class Curl implements Interface\RequestsInterface 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']); + } + if (!empty($this->config['connection_timeout'])) { + $timeout_requires_no_signal |= $this->config['connection_timeout'] < 1; + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout']); + } + 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 @@ -271,7 +633,7 @@ class Curl implements Interface\RequestsInterface ); } - // MARK: curl response hanlder + // MARK: curl response handler /** * Handle curl response and not auth 401 errors @@ -286,7 +648,7 @@ class Curl implements Interface\RequestsInterface ): string { $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); if ( - !$this->exception_on_not_authorized || + empty($this->config['exception_on_not_authorized']) || $http_response !== self::HTTP_NOT_AUTHORIZED ) { return (string)$http_response; @@ -317,127 +679,247 @@ class Curl implements Interface\RequestsInterface ); } + /** + * close the current curl handle + * + * @param \CurlHandle $handle + * @return void + */ + private function handleCurlClose(\CurlHandle $handle): void + { + curl_close($handle); + } + // ********************************************************************* // MARK: PUBLIC METHODS // ********************************************************************* - // MARK: request methods - /** - * Makes an request to the target url via curl: GET - * Returns result as string (json) + * 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 string $url The URL being requested, - * including domain and protocol - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] String to pass on as GET, - * if array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json + * @param array $headers + * @return array + * @throws \UnexpectedValueException on duplicate header key */ - public function requestGet(string $url, array $headers = [], null|string|array $query = null): array + public function prepareHeaders(array $headers): array { - return $this->curlRequest("get", $this->convertQuery($url, $query), $headers); + $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 + + /** + * get the config array with all settings + * + * @return array all current config settings + */ + public function getConfig(): array + { + return $this->config; } /** - * Makes an request to the target url via curl: POST - * Returns result as string (json) + * Return the full url as it was sent * - * @param string $url The URL being requested, - * including domain and protocol - * @param string|array $payload Data to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] Optinal query parameters, array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json + * @return string url sent */ - public function requestPost( - string $url, - string|array $payload, - array $headers = [], - null|string|array $query = null - ): array { - return $this->curlRequest( - "post", - $this->convertQuery($url, $query), - $headers, - $this->convertPayloadData($payload) - ); + public function getUrlSent(): string + { + return $this->url; } /** - * Makes an request to the target url via curl: PUT - * Returns result as string (json) + * get the parsed url * - * @param string $url The URL being requested, - * including domain and protocol - * @param string|array $payload String to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] Optinal query parameters, array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} */ - public function requestPut( - string $url, - string|array $payload, - array $headers = [], - null|string|array $query = null - ): array { - return $this->curlRequest( - "put", - $this->convertQuery($url, $query), - $headers, - $this->convertPayloadData($payload) - ); + public function getUrlParsedSent(): array + { + return $this->parsed_url; } /** - * Makes an request to the target url via curl: PATCH - * Returns result as string (json) + * Return the full headers as they where sent * - * @param string $url The URL being requested, - * including domain and protocol - * @param string|array $payload String to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] Optinal query parameters, array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json + * @return array */ - public function requestPatch( - string $url, - string|array $payload, - array $headers = [], - null|string|array $query = null - ): array { - return $this->curlRequest( - "patch", - $this->convertQuery($url, $query), - $headers, - $this->convertPayloadData($payload) - ); + 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; + } + } } /** - * Makes an request to the target url via curl: DELETE - * Returns result as string (json) - * Note that DELETE payload is optional + * remove header entry + * if key is only set then match only key, if both are set both sides must match * - * @param string $url The URL being requested, - * including domain and protocol - * @param null|string|array $payload [default=null] Data to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] String to pass on as GET, - * if array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json + * @param array $remove_headers + * @return void */ - public function requestDelete( - string $url, - null|string|array $payload = null, - array $headers = [], - null|string|array $query = null - ): array { + 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]; + } + $this->config['headers'][$header_key] = 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{headers?:null|array>,query?:null|array,body?:null|string|array} $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 + // 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( - "delete", - $this->convertQuery($url, $query), - $headers, - $payload !== null ? $this->convertPayloadData($payload) : null + $type, + $url, + $options['headers'] ?? [], + $options['query'] ?? null, + $options['body'] ?? null ); } } diff --git a/www/lib/CoreLibs/UrlRequests/CurlTrait.php b/www/lib/CoreLibs/UrlRequests/CurlTrait.php new file mode 100644 index 00000000..e42e2a3c --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/CurlTrait.php @@ -0,0 +1,147 @@ +>,query?:null|string|array,body?:null|string|array} $options Request 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 + */ + 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{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function get(string $url, array $options): array + { + return $this->request( + "get", + $url, + [ + "headers" => $options['headers'] ?? [], + "query" => $options['query'] ?? null, + ], + ); + } + + /** + * 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{headers?:null|array>,query?:null|array,body?:null|string|array} $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, + [ + "headers" => $options['headers'] ?? [], + "query" => $options['query'] ?? null, + "body" => $options['body'] ?? null, + ], + ); + } + + /** + * 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{headers?:null|array>,query?:null|array,body?:null|string|array} $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, + [ + "headers" => $options['headers'] ?? [], + "query" => $options['query'] ?? null, + "body" => $options['body'] ?? null, + ], + ); + } + + /** + * 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{headers?:null|array>,query?:null|array,body?:null|string|array} $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, + [ + "headers" => $options['headers'] ?? [], + "query" => $options['query'] ?? null, + "body" => $options['body'] ?? null, + ], + ); + } + + /** + * 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{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function delete(string $url, array $options): array + { + return $this->request( + "delete", + $url, + [ + "headers" => $options['headers'] ?? [], + "query" => $options['query'] ?? null, + "body" => $options['body'] ?? null, + ], + ); + } +} + +// __END__ diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestInterface.php new file mode 100644 index 00000000..e59b469c --- /dev/null +++ b/www/lib/CoreLibs/UrlRequests/Interface/RequestInterface.php @@ -0,0 +1,95 @@ + "bar" type + * Skips entries that are already in key => value type, by checking if the + * key is a not a number + * + * @param array $headers + * @return array + * @throws \UnexpectedValueException on duplicate header key + */ + public function prepareHeaders(array $headers): array; + + /** + * get the config array with all settings + * + * @return array 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{headers?:null|array>,query?:null|array,body?:null|string|array} $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__ diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php deleted file mode 100644 index ed527edc..00000000 --- a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php +++ /dev/null @@ -1,84 +0,0 @@ - $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] String to pass on as GET, - * if array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json - */ - public function requestGet(string $url, array $headers = [], null|string|array $query = null): array; - - /** - * 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 string|array $payload Data to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] Optinal query parameters, array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json - */ - public function requestPost( - string $url, - string|array $payload, - array $headers = [], - null|string|array $query = null - ): array; - - /** - * 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 string|array $payload Data to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] Optinal query parameters, array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json - */ - public function requestPut( - string $url, - string|array $payload, - array $headers = [], - null|string|array $query = null - ): array; - - /** - * Makes an request to the target url via curl: DELETE - * Returns result as string (json) - * Note that DELETE payload is optional - * - * @param string $url The URL being requested, - * including domain and protocol - * @param null|string|array $payload [default=null] Data to pass on as POST - * @param array $headers [default=[]] Headers to be used in the request - * @param null|string|array $query [default=null] String to pass on as GET, - * if array will be converted - * @return array{code:string,content:string} Result code and content as array, content is json - */ - public function requestDelete( - string $url, - null|string|array $payload = null, - array $headers = [], - null|string|array $query = null - ): array; -} - -// __END__ From 0d4e959f39f251ada492490c707338b33f4d3728 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 1 Nov 2024 14:42:04 +0900 Subject: [PATCH 09/23] Remove the nice formatter for now --- phpstan.neon | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index d9c3806c..a10a656b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,13 +1,13 @@ # 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 @@ -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 From 4bc2ad8fa0359e75c9ecd39a2ee7b2f1f6f9ee02 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 1 Nov 2024 14:42:43 +0900 Subject: [PATCH 10/23] URL Requests basic tests file --- www/admin/UrlRequests.target.php | 9 +- www/admin/class_test.url-requests.curl.php | 229 ++++++++++++++++----- 2 files changed, 179 insertions(+), 59 deletions(-) diff --git a/www/admin/UrlRequests.target.php b/www/admin/UrlRequests.target.php index 1b5b8b2f..c127b97d 100644 --- a/www/admin/UrlRequests.target.php +++ b/www/admin/UrlRequests.target.php @@ -23,13 +23,16 @@ $file_get = file_get_contents('php://input') ?: '{"Error" => "file_get_contents $log->debug('SERVER', $log->prAr($_SERVER)); $log->debug('HEADERS', $log->prAr($http_headers)); +$log->debug('GET', $log->prAr($_GET)); $log->debug('POST', $log->prAr($_POST)); $log->debug('PHP-INPUT', $log->prAr($file_get)); +header("Content-Type: application/json; charset=UTF-8"); + print Json::jsonConvertArrayTo([ - 'HTTP_HEADERS' => $http_headers, - "_GET" => $_GET, - "_POST" => Json::jsonConvertToArray($file_get), + 'HEADERS' => $http_headers, + "PARAMS" => $_GET, + "BODY" => Json::jsonConvertToArray($file_get), ]); $log->debug('[END]', '=========================================>'); diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index 32d6931a..5b6b9ec7 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -26,8 +26,6 @@ $log = new CoreLibs\Logging\Logging([ 'log_per_date' => true, ]); -$client = new Curl(); - $PAGE_NAME = 'TEST CLASS: URL REQUESTS CURL'; print ""; print "" . $PAGE_NAME . ""; @@ -35,87 +33,206 @@ print ""; print ''; print '

' . $PAGE_NAME . '

'; +$client = new Curl(); + print "
"; -$data = $client->requestGet( - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' - . '?other=get_a', - ['test-header: ABC', 'request-type: _GET'], - ['foo' => 'BAR'] +$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', + 'Funk-pop' => 'Semlly god' + ]), + 'query' => ['foo' => 'BAR'] + ] ); print "_GET RESPONSE:
" . print_r($data, true) . "
"; print "
"; -$data = $client->requestPost( - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' - . '?other=post_a', - ['payload' => 'data post'], - [ - 'Content-Type: application/json', - 'Accept: application/json', - 'test-header: ABC', - 'info-request-type: _POST' - ], - ['foo' => 'BAR post'], +$data = $client->request( + 'get', + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' + . '?other=get_a', ); -print "_POST RESPONSE:
" . print_r($data, true) . "
"; +print "_GET RESPONSE, nothing set:
" . print_r($data, true) . "
"; print "
"; -$data = $client->requestPut( - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' - . '?other=put_a', - ['payload' => 'data put'], +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', [ - 'Content-Type: application/json', - 'Accept: application/json', - 'test-header: ABC', - 'info-request-type: _PUT' + "headers" => $client->prepareHeaders([ + 'test-header: ABC', + 'info-request-type: _GET', + 'Funk-pop' => 'Semlly god' + ]), + "query" => ['foo' => 'BAR'], ], - ['foo' => 'BAR put'], +); +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' => $client->prepareHeaders([ + '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" => $client->prepareHeaders([ + '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->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" => $client->prepareHeaders([ + '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->requestPatch( - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' +$data = $client->patch( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' . '?other=patch_a', - ['payload' => 'data patch'], [ - 'Content-Type: application/json', - 'Accept: application/json', - 'test-header: ABC', - 'info-request-type: _PATCH' - ], - ['foo' => 'BAR patch'], + "body" => ['payload' => 'data patch'], + "headers" => $client->prepareHeaders([ + '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->requestDelete( - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' +$data = $client->delete( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' . '?other=delete_no_body_a', - null, [ - 'Content-Type: application/json', - 'Accept: application/json', - 'test-header: ABC', - 'info-request-type: _DELETE' - ], - ['foo' => 'BAR delete'], + "body" => null, + "headers" => $client->prepareHeaders([ + '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) . "
"; -$data = $client->requestDelete( - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlReqeusts.target.php' + +print "
"; +$data = $client->delete( + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php' . '?other=delete_body_a', - ['payload' => 'data delete'], [ - 'Content-Type: application/json', - 'Accept: application/json', - 'test-header: ABC', - 'info-request-type: _DELETE' - ], - ['foo' => 'BAR delete'], + "body" => ['payload' => 'data delete'], + "headers" => $client->prepareHeaders([ + '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 "_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', + ] + ]); + 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?other=get_a', + [ + 'headers' => [ + 'call-header' => 'call-get', + 'default-header' => 'overwrite-uc-get', + 'X-Foo' => ['bar', 'baz'], + ] + ] + ); + 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 ""; From 6e7b9cd03331fc71d0019739e7e968c581f20e07 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 1 Nov 2024 14:43:10 +0900 Subject: [PATCH 11/23] phpunit URL Requests backend test file --- .../AAASetupData/requests/http_requests.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 4dev/tests/AAASetupData/requests/http_requests.php diff --git a/4dev/tests/AAASetupData/requests/http_requests.php b/4dev/tests/AAASetupData/requests/http_requests.php new file mode 100644 index 00000000..33784ed7 --- /dev/null +++ b/4dev/tests/AAASetupData/requests/http_requests.php @@ -0,0 +1,28 @@ + "file_get_contents failed"}]'; + +header("Content-Type: application/json; charset=UTF-8"); + +print json_encode([ + 'HEADERS' => $http_headers, + "PARAMS" => $_GET, + "BODY" => json_decode($file_get, true), +]); + +// __END__ From a4f16f4ca9ec399cd570c3d98de4e785f0d89bf2 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 09:58:05 +0900 Subject: [PATCH 12/23] Various updates and fixes during testing Move the build auth content to dedicated variables Add a default User-Agent that is always sent Default headers like Authorization and User-Agent are always set, even when request is sent with headers null Fix timeout, was sent as is and not converted to milliseconds Fix headers not correctly set to null if array entry was set to null --- www/lib/CoreLibs/UrlRequests/Curl.php | 115 ++++++++++++++------- www/lib/CoreLibs/UrlRequests/CurlTrait.php | 70 +++++++------ 2 files changed, 119 insertions(+), 66 deletions(-) diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index 116abd8b..9387245c 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -53,10 +53,12 @@ class Curl implements Interface\RequestsInterface 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},auth_type?:int|string,auth_userpwd?:string,exception_on_not_authorized:bool,base_uri:string,headers:array>,query:array,timeout:float,connection_timeout:float} config settings as + /** @var array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized: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] * base_uri: base url to set, will prefix all urls given in calls @@ -78,6 +80,12 @@ class Curl implements Interface\RequestsInterface 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 = []; @@ -107,7 +115,7 @@ class Curl implements Interface\RequestsInterface * Set the main configuration * * phpcs:disable Generic.Files.LineLength - * @param array{auth?:array{0:string,1:string,2:string},auth_type?:int|string,auth_userpwd?:string,exception_on_not_authorized?:bool,base_uri?:string,headers?:array>,query?:array,timeout?:float,connection_timeout?:float} $config + * @param array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized?:bool,base_uri?:string,headers?:array>,query?:array,timeout?:float,connection_timeout?:float} $config * @return void * phpcs:enable Generic.Files.LineLength */ @@ -128,19 +136,22 @@ class Curl implements Interface\RequestsInterface $userpwd = $config['auth'][0] . ':' . $config['auth'][1]; switch ($type) { case 'basic': - if (!isset($config['headers']['Authorization'])) { - $config['headers']['Authorization'] = 'Basic ' . base64_encode( - $userpwd - ); - } + $this->auth_basic_header = 'Basic ' . base64_encode( + $userpwd + ); + // if (!isset($config['headers']['Authorization'])) { + // $config['headers']['Authorization'] = 'Basic ' . base64_encode( + // $userpwd + // ); + // } break; case 'digest': - $config['auth_type'] = CURLAUTH_DIGEST; - $config['auth_userpwd'] = $userpwd; + $this->auth_type = CURLAUTH_DIGEST; + $this->auth_userpwd = $userpwd; break; case 'ntlm': - $config['auth_type'] = CURLAUTH_NTLM; - $config['auth_userpwd'] = $userpwd; + $this->auth_type = CURLAUTH_NTLM; + $this->auth_userpwd = $userpwd; break; } } @@ -152,8 +163,6 @@ class Curl implements Interface\RequestsInterface $config['exception_on_not_authorized'] = false; } if (!empty($config['base_uri'])) { - // TODO: this should be run through Guzzle\Psr7\Utils in future - // $config['base_uri'] = Psr7\Utils::urlFor($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']; @@ -303,6 +312,14 @@ class Curl implements Interface\RequestsInterface $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); } @@ -382,6 +399,27 @@ class Curl implements Interface\RequestsInterface return $return_headers; } + /** + * default headers that are always set + * Authorization + * User-Agent + * + * @return array + */ + private function buildDefaultHeaders(): array + { + $headers = []; + // add auth header if set + if (!empty($this->auth_basic_header)) { + $headers['Authorization'] = $this->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 * @@ -390,9 +428,10 @@ class Curl implements Interface\RequestsInterface */ private function buildHeaders(null|array $headers): array { - // if headers is null, return empty headers, do not set default headers + // if headers is null, return empty headers, do not set config default headers + // but the automatic set User-Agent and Authorization headers are always set if ($headers === null) { - return []; + return $this->buildDefaultHeaders(); } // merge master headers with sub headers, sub headers overwrite master headers if (!empty($this->config['headers'])) { @@ -410,7 +449,7 @@ class Curl implements Interface\RequestsInterface $headers[$key] = $this->config['headers'][$key]; } } - // always add HTTP_HOST and HTTP_USER_AGENT + $headers = array_merge($headers, $this->buildDefaultHeaders()); return $headers; } @@ -427,6 +466,7 @@ class Curl implements Interface\RequestsInterface * @param null|array $query [default=null] Optinal query parameters * @param null|string|array $body [default=null] Data body, converted to JSON * @return array{code:string,headers:array>,content:string} + * @throws \RuntimeException if type param is not valid */ private function curlRequest( string $type, @@ -492,6 +532,7 @@ class Curl implements Interface\RequestsInterface * * @param string $url * @return \CurlHandle + * @throws \RuntimeException if curl could not be initialized */ private function handleCurleInit(string $url): \CurlHandle { @@ -528,14 +569,15 @@ class Curl implements Interface\RequestsInterface private function setCurlOptions(\CurlHandle $handle, array $headers): void { // for not Basic auth, basic auth sets its own header - if (!empty($this->config['auth_type']) && !empty($this->config['auth_userpwd'])) { - curl_setopt($handle, CURLOPT_HTTPAUTH, $this->config['auth_type']); - curl_setopt($handle, CURLOPT_USERPWD, $this->config['auth_userpwd']); + 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); } 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); @@ -547,11 +589,11 @@ class Curl implements Interface\RequestsInterface // 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']); + 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; - curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout']); + 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); @@ -583,14 +625,17 @@ class Curl implements Interface\RequestsInterface /** * handles any CURL execute and on error throws a correct error message * - * @param \CurlHandle $handle - * @return string + * @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 return (string)self::HTTP_OK; } elseif ($http_result !== false) { return $http_result; @@ -640,7 +685,8 @@ class Curl implements Interface\RequestsInterface * * @param string $http_result * @param \CurlHandle $handle - * @return string + * @return string http response code + * @throws \RuntimeException Auth error */ private function handleCurlResponse( string $http_result, @@ -658,18 +704,14 @@ class Curl implements Interface\RequestsInterface $result_ar = json_decode((string)$http_result, true); $url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL); - $error_status = 'ERROR'; - $error_code = $http_response; - $error_type = 'UnauthorizedRequest'; - $message = 'Request could not be finished successfully because of an authorization error'; // throw Error here with all codes throw new RuntimeException( json_encode([ - 'status' => $error_status, - 'code' => $error_code, - 'type' => $error_type, - 'message' => $message, + 'status' => 'ERROR', + 'code' => $http_response, + 'type' => 'UnauthorizedRequest', + 'message' => 'Request could not be finished successfully because of an authorization error', 'context' => [ 'url' => $url, 'result' => $result_ar, @@ -823,7 +865,7 @@ class Curl implements Interface\RequestsInterface * remove header entry * if key is only set then match only key, if both are set both sides must match * - * @param array $remove_headers + * @param array> $remove_headers * @return void */ public function removeHeaders(array $remove_headers): void @@ -861,10 +903,11 @@ class Curl implements Interface\RequestsInterface if (!is_array($value)) { $value = [$value]; } - $this->config['headers'][$header_key] = array_diff( + // array values so we rewrite the key pos + $this->config['headers'][$header_key] = array_values(array_diff( $this->config['headers'][$header_key], $value - ); + )); } } } @@ -917,7 +960,7 @@ class Curl implements Interface\RequestsInterface return $this->curlRequest( $type, $url, - $options['headers'] ?? [], + !array_key_exists('headers', $options) ? [] : $options['headers'], $options['query'] ?? null, $options['body'] ?? null ); diff --git a/www/lib/CoreLibs/UrlRequests/CurlTrait.php b/www/lib/CoreLibs/UrlRequests/CurlTrait.php index e42e2a3c..5010e2fa 100644 --- a/www/lib/CoreLibs/UrlRequests/CurlTrait.php +++ b/www/lib/CoreLibs/UrlRequests/CurlTrait.php @@ -18,6 +18,32 @@ 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>,query?:null|array,body?:null|string|array} $options Request options + * @return array{headers?:null|array>,query?:null|array,body?:null|string|array} + */ + 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: @@ -28,10 +54,10 @@ 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>,query?:null|string|array,body?:null|string|array} $options Request options - * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + * @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; + abstract public function request(string $type, string $url, array $options = []): array; /** * Makes an request to the target url via curl: GET @@ -40,18 +66,18 @@ trait CurlTrait * @param string $url The URL being requested, * including domain and protocol * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set - * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + * @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 + public function get(string $url, array $options = []): array { return $this->request( "get", $url, - [ - "headers" => $options['headers'] ?? [], - "query" => $options['query'] ?? null, - ], + $this->setOptions('get', $options), ); + + // array{headers?: array|string>|null, query?: array|null, body?: array|string|null}, + // array{headers?: array|string>|null, query?: array|string|null, body?: array|string|null} } /** @@ -68,11 +94,7 @@ trait CurlTrait return $this->request( "post", $url, - [ - "headers" => $options['headers'] ?? [], - "query" => $options['query'] ?? null, - "body" => $options['body'] ?? null, - ], + $this->setOptions('post', $options), ); } @@ -90,11 +112,7 @@ trait CurlTrait return $this->request( "put", $url, - [ - "headers" => $options['headers'] ?? [], - "query" => $options['query'] ?? null, - "body" => $options['body'] ?? null, - ], + $this->setOptions('put', $options), ); } @@ -112,11 +130,7 @@ trait CurlTrait return $this->request( "patch", $url, - [ - "headers" => $options['headers'] ?? [], - "query" => $options['query'] ?? null, - "body" => $options['body'] ?? null, - ], + $this->setOptions('patch', $options), ); } @@ -128,18 +142,14 @@ trait CurlTrait * @param string $url The URL being requested, * including domain and protocol * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set - * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + * @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 + public function delete(string $url, array $options = []): array { return $this->request( "delete", $url, - [ - "headers" => $options['headers'] ?? [], - "query" => $options['query'] ?? null, - "body" => $options['body'] ?? null, - ], + $this->setOptions('delete', $options), ); } } From 30e2f33620ae6d2da44574b489f0085face8c192 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 10:03:33 +0900 Subject: [PATCH 13/23] Test calls update for admin area --- www/admin/UrlRequests.target.php | 37 +++++++++++--- www/admin/class_test.url-requests.curl.php | 59 +++++++++++++++++++++- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/www/admin/UrlRequests.target.php b/www/admin/UrlRequests.target.php index c127b97d..ad53829f 100644 --- a/www/admin/UrlRequests.target.php +++ b/www/admin/UrlRequests.target.php @@ -1,4 +1,4 @@ - true, ]); +/** + * build return json + * + * @param array $http_headers + * @param string $body + * @return string + */ +function buildContent(array $http_headers, string $body): string +{ + return Json::jsonConvertArrayTo([ + 'HEADERS' => $http_headers, + "REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'], + "PARAMS" => $_GET, + "BODY" => Json::jsonConvertToArray($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; +} + $file_get = file_get_contents('php://input') ?: '{"Error" => "file_get_contents failed"}'; // 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)); -header("Content-Type: application/json; charset=UTF-8"); - -print Json::jsonConvertArrayTo([ - 'HEADERS' => $http_headers, - "PARAMS" => $_GET, - "BODY" => Json::jsonConvertToArray($file_get), -]); +print buildContent($http_headers, $file_get); $log->debug('[END]', '=========================================>'); diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index 5b6b9ec7..f657e7de 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -204,6 +204,9 @@ try { '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) . "
"; @@ -217,13 +220,16 @@ try { print "CONFIG:
" . print_r($uc->getConfig(), true) . "
"; $data = $uc->request( 'get', - 'UrlRequests.target.php?other=get_a', + '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) . "
"; @@ -234,6 +240,55 @@ try { 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/', + "exception_on_not_authorized" => 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/', + "exception_on_not_authorized" => 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 "
"; +$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 "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) . "
"; print ""; From d8379a10d904dea2e3ef7abcf45fa1935e8ada8d Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 10:33:05 +0900 Subject: [PATCH 14/23] URL Request phpunit test added --- .../AAASetupData/requests/http_requests.php | 36 +- .../CoreLibsUrlRequestsCurlTest.php | 1040 ++++++++++++++++- 2 files changed, 1062 insertions(+), 14 deletions(-) diff --git a/4dev/tests/AAASetupData/requests/http_requests.php b/4dev/tests/AAASetupData/requests/http_requests.php index 33784ed7..f7b19b40 100644 --- a/4dev/tests/AAASetupData/requests/http_requests.php +++ b/4dev/tests/AAASetupData/requests/http_requests.php @@ -1,4 +1,4 @@ - $http_headers, + "REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'], + "PARAMS" => $_GET, + "BODY" => json_decode($body, true) + ]); +} + $http_headers = array_filter($_SERVER, function ($value, $key) { if (str_starts_with($key, 'HTTP_')) { return true; } }, ARRAY_FILTER_USE_BOTH); -$file_get = file_get_contents('php://input') ?: '["code": 500, "content": {"Error" => "file_get_contents failed"}]'; - header("Content-Type: application/json; charset=UTF-8"); -print json_encode([ - 'HEADERS' => $http_headers, - "PARAMS" => $_GET, - "BODY" => json_decode($file_get, true), -]); +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; +} + +print buildContent( + $http_headers, + file_get_contents('php://input') ?: '["code": 500, "content": {"Error" => "file_get_contents failed"}]' +); // __END__ diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 7c39d699..0572cd10 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -13,18 +13,1046 @@ use PHPUnit\Framework\TestCase; */ final class CoreLibsUrlRequestsCurlTest extends TestCase { + // we must launch some small test web server for the response tests + + // public static function setUpBeforeClass(): voidx + + private string $url_basic = ''; + private string $url_basic_start = ''; + private string $url_basic_end = ''; + private array $default_config = [ + 'exception_on_not_authorized' => false, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + /** - * Undocumented function - * - * @testdox UrlRequests\Curl Class tests + * check if we have some backend for testing * * @return void */ - public function testUrlRequestsCurl() + protected function setUp(): void { - $this->markTestIncomplete( - 'UrlRequests\Curl Tests have not yet been implemented' + // 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 ( + [ + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/' + . '4dev/tests/AAASetupData/requests/http_requests.php', + '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' => [ + 'exception_on_not_authorized' => false, + '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'], + 'exception_on_not_authorized' => true, + 'base_uri' => 'http://foo.bar.com', + 'headers' => [ + 'something' => 'other', + ], + 'query' => [ + 'foo' => 'bar', + ], + 'timeout' => 5, + 'connection_timeout' => 10, + ], + 'expected_set' => [ + 'auth' => ['user', 'passowrd', 'Basic'], + 'exception_on_not_authorized' => true, + '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' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + 'base_uri' => 'http://bar.baz.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + // MARK: set headers + 'set header new' => [ + 'config' => null, + 'expected_set' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'new-header' => 'abc', + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'set header overwrite' => [ + 'config' => [ + 'headers' => [ + 'existing-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'bar' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'set header add' => [ + 'config' => [ + 'headers' => [ + 'existing-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header string, key match only' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, key match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, string match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + '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' => [ + 'exception_on_not_authorized' => false, + '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 basi 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":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}' + ]; + $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"}}' + ]; + } + // 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"}}' + ]; + } + 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' + ); + } + + // TODO: 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":null}', + $response['content'], + 'multi call: delete content not matching' + ); + } + + // MARK: test exceptions + + /** + * Undocumented function + * + * @testdox UrlRequests\Curl Exception:InvalidRequestType + * + * @return void + */ + public function testExceptionInvalidRequestType(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/InvalidRequestType/"); + $response = $curl->request('wrong', 'http://foo.bar.com'); + } + + /** + * TODO: Exception:CurlInitError + * + * @testdox UrlRequests\Curl Exception:CurlInitError + * + * @return void + */ + // public function testExceptionCurlInitError(): void + // { + // $this->markTestSkipped('Test Exception CurlInitError not implemented'); + // } + + /** + * Undocumented function + * + * @testdox UrlRequests\Curl Exception:CurlError + * + * @return void + */ + public function testExceptionCurlError(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/CurlError/"); + // invalid yrl + $response = $curl->request('get', 'as-4939345!#$%'); + } + + /** + * TODO: Exception:UnauthorizedRequest + * + * @testdox UrlRequests\Curl Exception:UnauthorizedRequest + * + * @return void + */ + public function testExceptionUnauthorizedRequest(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(["exception_on_not_authorized" => true]); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/UnauthorizedRequest/"); + $response = $curl->get($this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ] + ]); + // $response = $curl->get('https://httpbin.org/bearer', [ + // "headers" => ["Authorization" => "schmalztiegel"] + // ]); + } + + /** + * Undocumented function + * + * @testdox UrlRequests\Curl Exception:DuplicatedArrayKey + * + * @return void + */ + public function testExceptionDuplicatedArrayKey(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageMatches("/DuplicatedArrayKey/"); + $curl->prepareHeaders([ + 'header-double:a', + 'header-double:b', + ]); } } From 5c8a2ef8dac5f222990948eca4981edf3d1413d0 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 10:38:30 +0900 Subject: [PATCH 15/23] Update test paths for URLRequests tests --- .../UrlRequests/CoreLibsUrlRequestsCurlTest.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 0572cd10..02beb9eb 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -42,9 +42,16 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase // -t /storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/4dev/tests/AAASetupData/requests/ foreach ( [ - 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/' - . '4dev/tests/AAASetupData/requests/http_requests.php', - 'localhost:30999/http_requests.php', + // 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/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); From c8bc0062ad8a2f6916b2d8f920a39f47a9da6415 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 12:48:01 +0900 Subject: [PATCH 16/23] 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) --- .../AAASetupData/requests/http_requests.php | 15 ++-- .../CoreLibsUrlRequestsCurlTest.php | 61 ++++++++------- www/admin/UrlRequests.target.php | 13 +++- www/admin/class_test.url-requests.curl.php | 6 +- www/lib/CoreLibs/UrlRequests/Curl.php | 74 ++++++++++--------- 5 files changed, 88 insertions(+), 81 deletions(-) diff --git a/4dev/tests/AAASetupData/requests/http_requests.php b/4dev/tests/AAASetupData/requests/http_requests.php index f7b19b40..c3559020 100644 --- a/4dev/tests/AAASetupData/requests/http_requests.php +++ b/4dev/tests/AAASetupData/requests/http_requests.php @@ -12,7 +12,7 @@ declare(strict_types=1); /** * build return json * - * @param array $http_headers + * @param array $http_headers * @param string $body * @return string */ @@ -34,15 +34,18 @@ $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 (($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/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 02beb9eb..8b33796c 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -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' => [], @@ -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' => [ @@ -1016,32 +1016,29 @@ 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!#$%'); } /** - * TODO: Exception:UnauthorizedRequest + * TODO: Exception:BadRequest * - * @testdox UrlRequests\Curl Exception:UnauthorizedRequest + * @testdox UrlRequests\Curl Exception:BadRequest * * @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/"); + $this->expectExceptionMessageMatches("/ClientError/"); $response = $curl->get($this->url_basic, [ "headers" => [ "Authorization" => "schmalztiegel", "RunAuthTest" => "yes", ] ]); - // $response = $curl->get('https://httpbin.org/bearer', [ - // "headers" => ["Authorization" => "schmalztiegel"] - // ]); } /** diff --git a/www/admin/UrlRequests.target.php b/www/admin/UrlRequests.target.php index ad53829f..c47a0fad 100644 --- a/www/admin/UrlRequests.target.php +++ b/www/admin/UrlRequests.target.php @@ -15,7 +15,7 @@ $log = new CoreLibs\Logging\Logging([ /** * build return json * - * @param array $http_headers + * @param array $http_headers * @param string $body * @return string */ @@ -25,7 +25,8 @@ function buildContent(array $http_headers, string $body): string 'HEADERS' => $http_headers, "REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'], "PARAMS" => $_GET, - "BODY" => Json::jsonConvertToArray($body) + "BODY" => Json::jsonConvertToArray($body), + // "STRING_BODY" => $body, ]); } @@ -40,11 +41,15 @@ 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 (($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)); diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index f657e7de..ed6918dd 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -244,7 +244,7 @@ print "
"; 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 +262,7 @@ print "AUTH REQUEST WITH EXCEPTION:
"; 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", @@ -285,7 +285,7 @@ $uc = new Curl([ ] ]); $response = $uc->get('UrlRequests.target.php', ["headers" => null, "query" => ["test" => "one-test"]]); -print "AUTH REQUEST:
" . print_r($response, true) . "
"; +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) . "
"; diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index 9387245c..80ab9af3 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -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 */ @@ -35,6 +35,12 @@ class Curl implements Interface\RequestsInterface 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 */ @@ -47,18 +53,12 @@ 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>,query:array,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>,query:array,timeout:float,connection_timeout:float} config settings as *phpcs:enable Generic.Files.LineLength * auth: [0: user, 1: password, 2: auth type] * base_uri: base url to set, will prefix all urls given in calls @@ -66,10 +66,10 @@ class Curl implements Interface\RequestsInterface * 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 + * http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors */ private array $config = [ - 'exception_on_not_authorized' => false, + 'http_errors' => true, 'base_uri' => '', 'query' => [], 'headers' => [], @@ -115,14 +115,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>,query?:array,timeout?:float,connection_timeout?:float} $config + * @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 = [ - 'exception_on_not_authorized' => false, + 'http_errors' => true, 'base_uri' => '', 'query' => [], 'headers' => [], @@ -157,10 +157,10 @@ class Curl implements Interface\RequestsInterface } // 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) { @@ -480,8 +480,8 @@ class Curl implements Interface\RequestsInterface if (!in_array($type, self::VALID_REQUEST_TYPES)) { throw new RuntimeException( json_encode([ - 'status' => 'FAILURE', - 'code' => 'C003', + 'status' => 'ERROR', + 'code' => 'R002', 'type' => 'InvalidRequestType', 'message' => 'Invalid request type set: ' . $type, 'context' => [ @@ -635,7 +635,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; @@ -666,7 +666,7 @@ class Curl implements Interface\RequestsInterface json_encode([ 'status' => 'FAILURE', 'code' => 'C002', - 'type' => 'CurlError', + 'type' => 'CurlExecError', 'message' => $message, 'context' => [ 'url' => $url, @@ -681,12 +681,13 @@ 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 string $http_result result string from the url call + * @param \CurlHandle $handle Curl handler * @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( string $http_result, @@ -694,27 +695,28 @@ class Curl implements Interface\RequestsInterface ): string { $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); if ( - empty($this->config['exception_on_not_authorized']) || - $http_response !== self::HTTP_NOT_AUTHORIZED + empty($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([ '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_decode((string)$http_result, true), + // curl internal error number + 'curl_errno' => $err, + // the full curl info block + 'curl_info' => curl_getinfo($handle), ], ]) ?: '', $err @@ -761,7 +763,7 @@ class Curl implements Interface\RequestsInterface throw new \UnexpectedValueException( json_encode([ 'status' => 'ERROR', - 'code' => 'C004', + 'code' => 'R001', 'type' => 'DuplicatedArrayKey', 'message' => 'Key already exists in the headers', 'context' => [ From 1653e6b684a088c6dcf393c91d627f70d7d2c064 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 13:29:19 +0900 Subject: [PATCH 17/23] 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 --- .../CoreLibsUrlRequestsCurlTest.php | 72 +++++++++++++++++-- www/admin/class_test.url-requests.curl.php | 18 +++++ www/lib/CoreLibs/UrlRequests/Curl.php | 34 +++++---- www/lib/CoreLibs/UrlRequests/CurlTrait.php | 18 ++--- 4 files changed, 114 insertions(+), 28 deletions(-) diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 8b33796c..ddc25fe6 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -990,7 +990,7 @@ 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'); } /** @@ -1018,13 +1018,13 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches("/CurlExecError/"); // invalid yrl - $response = $curl->request('get', 'as-4939345!#$%'); + $curl->request('get', 'as-4939345!#$%'); } /** - * TODO: Exception:BadRequest + * Exception:ClientError * - * @testdox UrlRequests\Curl Exception:BadRequest + * @testdox UrlRequests\Curl Exception:ClientError * * @return void */ @@ -1033,12 +1033,72 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches("/ClientError/"); + $curl->get($this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ] + ]); + } + + /** + * Exception:ClientError + * + * @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->get($this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ], + "http_errors" => true + ]); + } + + /** + * Exception:ClientError + * + * @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->get($this->url_basic, [ "headers" => [ "Authorization" => "schmalztiegel", - "RunAuthTest" => "yes", - ] + "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->get($this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ], + "http_errors" => null, + ]); + $this->assertEquals( + "401", + $response['code'], + 'Unset Exception failed with null' + ); } /** diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index ed6918dd..87fa0053 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -276,6 +276,24 @@ try { } 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 "
"; $uc = new Curl([ diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index 80ab9af3..f9110054 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -458,22 +458,25 @@ class Curl implements Interface\RequestsInterface /** * 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> $headers [default=[]] Headers to be used in the request - * @param null|array $query [default=null] Optinal query parameters - * @param null|string|array $body [default=null] Data body, converted to JSON + * @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> $headers Headers to be used in the request + * @param null|array $query Optinal query parameters + * @param null|string|array $body Data body, converted to JSON + * @param null|bool $http_errors Throw exception on http response + * 400 or higher if set to true * @return array{code:string,headers:array>,content:string} * @throws \RuntimeException if type param is not valid */ private function curlRequest( string $type, string $url, - null|array $headers = [], - null|array $query = null, - null|string|array $body = null + null|array $headers, + null|array $query, + null|string|array $body, + null|bool $http_errors, ): array { $this->url = $this->buildQuery($url, $query); $this->headers = $this->convertHeaders($this->buildHeaders($headers)); @@ -514,7 +517,7 @@ class Curl implements Interface\RequestsInterface // for debug // print "CURLINFO_HEADER_OUT:
" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "
"; // get response code and bail on not authorized - $http_response = $this->handleCurlResponse($http_result, $handle); + $http_response = $this->handleCurlResponse($http_result, $http_errors, $handle); // close handler $this->handleCurlClose($handle); // return response and result @@ -685,17 +688,19 @@ class Curl implements Interface\RequestsInterface * can be turned off by setting http_errors to false * * @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 * @param \CurlHandle $handle Curl handler * @return string http response code * @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400 */ private function handleCurlResponse( string $http_result, + ?bool $http_errors, \CurlHandle $handle ): string { $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); if ( - empty($this->config['http_errors']) || + empty($http_errors ?? $this->config['http_errors']) || $http_response < self::HTTP_BAD_REQUEST ) { return (string)$http_response; @@ -942,7 +947,7 @@ class Curl implements Interface\RequestsInterface * phpcs:disable Generic.Files.LineLength * @param string $type * @param string $url - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options + * @param array{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 @@ -964,7 +969,8 @@ class Curl implements Interface\RequestsInterface $url, !array_key_exists('headers', $options) ? [] : $options['headers'], $options['query'] ?? null, - $options['body'] ?? null + $options['body'] ?? null, + !array_key_exists('http_errors', $options) ? null : $options['http_errors'], ); } } diff --git a/www/lib/CoreLibs/UrlRequests/CurlTrait.php b/www/lib/CoreLibs/UrlRequests/CurlTrait.php index 5010e2fa..b75b3ab3 100644 --- a/www/lib/CoreLibs/UrlRequests/CurlTrait.php +++ b/www/lib/CoreLibs/UrlRequests/CurlTrait.php @@ -25,8 +25,8 @@ trait CurlTrait * "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>,query?:null|array,body?:null|string|array} $options Request options - * @return array{headers?:null|array>,query?:null|array,body?:null|string|array} + * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Request options + * @return array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} */ private function setOptions(string $type, array $options): array { @@ -34,12 +34,14 @@ trait CurlTrait return [ "headers" => !array_key_exists('headers', $options) ? [] : $options['headers'], "query" => $options['query'] ?? null, + "http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'], ]; } else { return [ "headers" => !array_key_exists('headers', $options) ? [] : $options['headers'], "query" => $options['query'] ?? null, "body" => $options['body'] ?? null, + "http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'], ]; } } @@ -53,7 +55,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>,query?:null|string|array,body?:null|string|array} $options Request options + * @param array{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} [default=[]] Result code, headers and content as array, content is json * @throws \UnexpectedValueException on missing body data when body data is needed */ @@ -65,7 +67,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @param array{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 @@ -86,7 +88,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @param array{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 @@ -104,7 +106,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @param array{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 @@ -122,7 +124,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @param array{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 @@ -141,7 +143,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array} $options Options to set + * @param array{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 From f0fae1f76d9dbe7499918e1aa805655cb3b8b1b9 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 13:35:00 +0900 Subject: [PATCH 18/23] Fix Composer package phpunit test url for UrlRequests --- 4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index ddc25fe6..5982efa1 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -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 From bacb9881acffe3ee30b1664bbbaca46206c5c833 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 14:28:15 +0900 Subject: [PATCH 19/23] 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 --- www/lib/CoreLibs/UrlRequests/Curl.php | 46 +++++++++---------- ...estInterface.php => RequestsInterface.php} | 0 2 files changed, 23 insertions(+), 23 deletions(-) rename www/lib/CoreLibs/UrlRequests/Interface/{RequestInterface.php => RequestsInterface.php} (100%) diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index f9110054..ffc428d7 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -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 @@ -404,11 +402,11 @@ class Curl implements Interface\RequestsInterface * Authorization * User-Agent * - * @return array + * @param array> $headers already set headers + * @return array> */ - private function buildDefaultHeaders(): array + private function buildDefaultHeaders($headers = []): array { - $headers = []; // add auth header if set if (!empty($this->auth_basic_header)) { $headers['Authorization'] = $this->auth_basic_header; @@ -449,7 +447,7 @@ class Curl implements Interface\RequestsInterface $headers[$key] = $this->config['headers'][$key]; } } - $headers = array_merge($headers, $this->buildDefaultHeaders()); + $headers = $this->buildDefaultHeaders($headers); return $headers; } @@ -481,8 +479,8 @@ class Curl implements Interface\RequestsInterface $this->url = $this->buildQuery($url, $query); $this->headers = $this->convertHeaders($this->buildHeaders($headers)); if (!in_array($type, self::VALID_REQUEST_TYPES)) { - throw new RuntimeException( - json_encode([ + throw new \RuntimeException( + Json::jsonConvertArrayTo([ 'status' => 'ERROR', 'code' => 'R002', 'type' => 'InvalidRequestType', @@ -492,7 +490,7 @@ class Curl implements Interface\RequestsInterface 'url' => $this->url, 'headers' => $this->headers, ], - ]) ?: '', + ]), 0, ); } @@ -544,8 +542,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', @@ -553,7 +551,7 @@ class Curl implements Interface\RequestsInterface 'context' => [ 'url' => $url, ], - ]) ?: '', + ]), 0, ); } @@ -591,11 +589,13 @@ 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 = $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') { @@ -665,8 +665,8 @@ 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' => 'CurlExecError', @@ -676,7 +676,7 @@ class Curl implements Interface\RequestsInterface 'errno' => $errno, 'message' => $message, ], - ]) ?: '', + ]), $errno ); } @@ -708,8 +708,8 @@ class Curl implements Interface\RequestsInterface // set curl error number $err = curl_errno($handle); // throw Error here with all codes - throw new RuntimeException( - json_encode([ + throw new \RuntimeException( + Json::jsonConvertArrayTo([ 'status' => 'ERROR', 'code' => 'H' . (string)$http_response, 'type' => $http_response < 500 ? 'ClientError' : 'ServerError', @@ -717,13 +717,13 @@ class Curl implements Interface\RequestsInterface 'context' => [ 'http_response' => $http_response, // extract all the error content if returned - 'result' => json_decode((string)$http_result, true), + 'result' => Json::jsonConvertToArray($http_result), // curl internal error number 'curl_errno' => $err, // the full curl info block 'curl_info' => curl_getinfo($handle), ], - ]) ?: '', + ]), $err ); } @@ -766,7 +766,7 @@ class Curl implements Interface\RequestsInterface if (array_key_exists($_key, $return_headers)) { // raise exception if key already exists throw new \UnexpectedValueException( - json_encode([ + Json::jsonConvertArrayTo([ 'status' => 'ERROR', 'code' => 'R001', 'type' => 'DuplicatedArrayKey', @@ -776,7 +776,7 @@ class Curl implements Interface\RequestsInterface 'headers' => $headers, 'return_headers' => $return_headers, ], - ]) ?: '', + ]), 1 ); } diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php similarity index 100% rename from www/lib/CoreLibs/UrlRequests/Interface/RequestInterface.php rename to www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php From f9cf36524e64d653e42c37b5836f2980d49dcfc4 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 18:42:35 +0900 Subject: [PATCH 20/23] 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 --- .../CoreLibsUrlRequestsCurlTest.php | 88 +++++-- www/admin/class_test.url-requests.curl.php | 119 +++++---- www/lib/CoreLibs/UrlRequests/Curl.php | 226 +++++++++++------- www/lib/CoreLibs/UrlRequests/CurlTrait.php | 41 ++-- .../Interface/RequestsInterface.php | 14 +- 5 files changed, 290 insertions(+), 198 deletions(-) diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 5982efa1..9e4432e4 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -732,7 +732,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase ); } - // MARK: test basi call provider + // MARK: test basic call provider /** * Undocumented function @@ -976,11 +976,61 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase ); } + // MARK: auth header set via config + + /** + * Test auth settings and auth override + * + * @testdox UrlRequests\Curl auth test call + * + * @return void + */ + public function testUrlRequestsCurlAuthHeader() + { + $curl = new \CoreLibs\UrlRequests\Curl([ + "auth" => ["user", "pass", "basic"], + "http_errors" => false, + ]); + $curl->request('get', $this->url_basic); + // check that the auth header matches + $this->assertContains( + "Authorization:Basic dXNlcjpwYXNz", + $curl->getHeadersSent() + ); + // if we sent new request with auth header, this one should not be used + $curl->request('get', $this->url_basic, [ + "headers" => ["Authorization" => "Failed"] + ]); + // check that the auth header matches + $this->assertContains( + "Authorization:Basic dXNlcjpwYXNz", + $curl->getHeadersSent() + ); + // override auth: reset + $curl->request('get', $this->url_basic, [ + "auth" => null + ]); + $this->assertNotContains( + "Authorization:Basic dXNlcjpwYXNz", + $curl->getHeadersSent() + ); + // override auth: different auth + $curl->request('get', $this->url_basic, [ + "auth" => ["user2", "pass2", "basic"] + ]); + // check that the auth header matches + $this->assertContains( + "Authorization:Basic dXNlcjI6cGFzczI=", + $curl->getHeadersSent() + ); + } + // MARK: test exceptions /** - * Undocumented function + * Exception:InvalidRequestType * + * @covers ::request * @testdox UrlRequests\Curl Exception:InvalidRequestType * * @return void @@ -1006,9 +1056,10 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase // } /** - * Undocumented function + * Exception:CurlExecError * - * @testdox UrlRequests\Curl Exception:CurlError + * @covers ::request + * @testdox UrlRequests\Curl Exception:CurlExecError * * @return void */ @@ -1024,6 +1075,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase /** * Exception:ClientError * + * @covers ::request * @testdox UrlRequests\Curl Exception:ClientError * * @return void @@ -1033,7 +1085,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches("/ClientError/"); - $curl->get($this->url_basic, [ + $curl->request('get', $this->url_basic, [ "headers" => [ "Authorization" => "schmalztiegel", "RunAuthTest" => "yes", @@ -1044,6 +1096,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase /** * Exception:ClientError * + * @covers ::request * @testdox UrlRequests\Curl Exception:ClientError on call enable * * @return void @@ -1053,7 +1106,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches("/ClientError/"); - $curl->get($this->url_basic, [ + $curl->request('get', $this->url_basic, [ "headers" => [ "Authorization" => "schmalztiegel", "RunAuthTest" => "yes", @@ -1065,6 +1118,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase /** * Exception:ClientError * + * @covers ::request * @testdox UrlRequests\Curl Exception:ClientError unset on call * * @return void @@ -1073,7 +1127,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase { // if true, with false it has to be off $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); - $response = $curl->get($this->url_basic, [ + $response = $curl->request('get', $this->url_basic, [ "headers" => [ "Authorization" => "schmalztiegel", "RunAuthTest" => "yes", @@ -1087,7 +1141,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase ); // if false, null should not change it $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); - $response = $curl->get($this->url_basic, [ + $response = $curl->request('get', $this->url_basic, [ "headers" => [ "Authorization" => "schmalztiegel", "RunAuthTest" => "yes", @@ -1100,24 +1154,6 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase 'Unset Exception failed with null' ); } - - /** - * Undocumented function - * - * @testdox UrlRequests\Curl Exception:DuplicatedArrayKey - * - * @return void - */ - public function testExceptionDuplicatedArrayKey(): void - { - $curl = new \CoreLibs\UrlRequests\Curl(); - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessageMatches("/DuplicatedArrayKey/"); - $curl->prepareHeaders([ - 'header-double:a', - 'header-double:b', - ]); - } } // __END__ diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index 87fa0053..f4a0848a 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -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,12 +111,12 @@ $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'], ] ); @@ -128,12 +128,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 +145,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 +162,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 +179,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'], ] ); @@ -294,6 +294,24 @@ try { } 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([ @@ -308,6 +326,19 @@ 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/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index ffc428d7..d6ffab13 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -59,12 +59,11 @@ class Curl implements Interface\RequestsInterface /** @var array{auth?:array{0:string,1:string,2:string},http_errors:bool,base_uri:string,headers:array>,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) - * : below is not a guzzleHttp config - * http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors */ private array $config = [ 'http_errors' => true, @@ -129,29 +128,10 @@ 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 ( @@ -187,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 /** @@ -371,6 +391,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 @@ -378,8 +399,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)) { @@ -388,8 +420,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; } @@ -402,14 +446,23 @@ class Curl implements Interface\RequestsInterface * Authorization * User-Agent * - * @param array> $headers already set headers + * @param array> $headers already set headers + * @param ?string $auth_basic_header * @return array> */ - private function buildDefaultHeaders($headers = []): array + private function buildDefaultHeaders(array $headers = [], ?string $auth_basic_header = ''): array { - // 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')])) { @@ -422,14 +475,15 @@ class Curl implements Interface\RequestsInterface * 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): 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'])) { @@ -447,7 +501,7 @@ class Curl implements Interface\RequestsInterface $headers[$key] = $this->config['headers'][$key]; } } - $headers = $this->buildDefaultHeaders($headers); + $headers = $this->buildDefaultHeaders($headers, $auth_basic_header); return $headers; } @@ -465,6 +519,7 @@ class Curl implements Interface\RequestsInterface * @param null|string|array $body Data body, converted to JSON * @param null|bool $http_errors Throw exception on http response * 400 or higher if set to true + * @param null|array{0:string,1:string,2:string} $auth auth array, if null reset global set auth * @return array{code:string,headers:array>,content:string} * @throws \RuntimeException if type param is not valid */ @@ -475,14 +530,29 @@ class Curl implements Interface\RequestsInterface null|array $query, null|string|array $body, null|bool $http_errors, + null|array $auth, ): array { + // set auth from override + if (is_array($auth)) { + $auth_data = $this->authParser($auth); + } else { + $auth_data = [ + 'auth_basic_header' => null, + 'auth_type' => null, + 'auth_userpwd' => null, + ]; + } + // build url $this->url = $this->buildQuery($url, $query); - $this->headers = $this->convertHeaders($this->buildHeaders($headers)); + $this->headers = $this->convertHeaders($this->buildHeaders( + $headers, + $auth_data['auth_basic_header'] + )); if (!in_array($type, self::VALID_REQUEST_TYPES)) { throw new \RuntimeException( Json::jsonConvertArrayTo([ 'status' => 'ERROR', - 'code' => 'R002', + 'code' => 'R001', 'type' => 'InvalidRequestType', 'message' => 'Invalid request type set: ' . $type, 'context' => [ @@ -497,7 +567,10 @@ class Curl implements Interface\RequestsInterface // 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); @@ -515,7 +588,7 @@ class Curl implements Interface\RequestsInterface // for debug // print "CURLINFO_HEADER_OUT:
" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "
"; // get response code and bail on not authorized - $http_response = $this->handleCurlResponse($http_result, $http_errors, $handle); + $http_response = $this->handleCurlResponse($handle, $http_result, $http_errors); // close handler $this->handleCurlClose($handle); // return response and result @@ -565,14 +638,26 @@ class Curl implements Interface\RequestsInterface * * @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): 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); @@ -694,9 +779,9 @@ class Curl implements Interface\RequestsInterface * @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400 */ private function handleCurlResponse( + \CurlHandle $handle, string $http_result, - ?bool $http_errors, - \CurlHandle $handle + ?bool $http_errors ): string { $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); if ( @@ -743,48 +828,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 $headers - * @return array - * @throws \UnexpectedValueException on duplicate header key - */ - public function prepareHeaders(array $headers): array - { - $return_headers = []; - foreach ($headers as $header_key => $header) { - // skip if header key is not numeric - if (!is_numeric($header_key)) { - $return_headers[$header_key] = $header; - continue; - } - list($_key, $_value) = explode(':', $header); - if (array_key_exists($_key, $return_headers)) { - // raise exception if key already exists - throw new \UnexpectedValueException( - Json::jsonConvertArrayTo([ - 'status' => 'ERROR', - 'code' => 'R001', - 'type' => 'DuplicatedArrayKey', - 'message' => 'Key already exists in the headers', - 'context' => [ - 'key' => $_key, - 'headers' => $headers, - 'return_headers' => $return_headers, - ], - ]), - 1 - ); - } - $return_headers[$_key] = $_value; - } - return $return_headers; - } - // MARK: get class vars /** @@ -947,7 +990,7 @@ class Curl implements Interface\RequestsInterface * phpcs:disable Generic.Files.LineLength * @param string $type * @param string $url - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options + * @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 @@ -971,6 +1014,7 @@ class Curl implements Interface\RequestsInterface $options['query'] ?? null, $options['body'] ?? null, !array_key_exists('http_errors', $options) ? null : $options['http_errors'], + !array_key_exists('auth', $options) ? [] : $options['auth'], ); } } diff --git a/www/lib/CoreLibs/UrlRequests/CurlTrait.php b/www/lib/CoreLibs/UrlRequests/CurlTrait.php index b75b3ab3..afacf76c 100644 --- a/www/lib/CoreLibs/UrlRequests/CurlTrait.php +++ b/www/lib/CoreLibs/UrlRequests/CurlTrait.php @@ -25,25 +25,21 @@ trait CurlTrait * "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>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Request options - * @return array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} + * @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?:array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} */ private function setOptions(string $type, array $options): array { - if ($type == "get") { - return [ - "headers" => !array_key_exists('headers', $options) ? [] : $options['headers'], - "query" => $options['query'] ?? null, - "http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'], - ]; - } else { - return [ - "headers" => !array_key_exists('headers', $options) ? [] : $options['headers'], - "query" => $options['query'] ?? null, - "body" => $options['body'] ?? null, - "http_errors" => !array_key_exists('http_errors', $options) ? null : $options['http_errors'], - ]; + $base = [ + "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'], + ]; + if ($type != "get") { + $base["body"] = $options['body'] ?? null; } + return $base; } /** @@ -55,7 +51,7 @@ trait CurlTrait * * @param string $type What type of request we send, will throw exception if not a valid one * @param string $url The url to send - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Request options + * @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} [default=[]] Result code, headers and content as array, content is json * @throws \UnexpectedValueException on missing body data when body data is needed */ @@ -67,7 +63,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @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 @@ -77,9 +73,6 @@ trait CurlTrait $url, $this->setOptions('get', $options), ); - - // array{headers?: array|string>|null, query?: array|null, body?: array|string|null}, - // array{headers?: array|string>|null, query?: array|string|null, body?: array|string|null} } /** @@ -88,7 +81,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @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 @@ -106,7 +99,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @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 @@ -124,7 +117,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @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 @@ -143,7 +136,7 @@ trait CurlTrait * * @param string $url The URL being requested, * including domain and protocol - * @param array{headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @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 diff --git a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php index e59b469c..bc6bb0e3 100644 --- a/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php +++ b/www/lib/CoreLibs/UrlRequests/Interface/RequestsInterface.php @@ -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 $headers - * @return array - * @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>,query?:null|array,body?:null|string|array} $options + * @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 From 0c51a3be872181e07119c2295cc948dbe65a1f71 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 18:49:48 +0900 Subject: [PATCH 21/23] Add phpunit tests for header key/value exceptions --- .../CoreLibsUrlRequestsCurlTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 9e4432e4..3d751c6f 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -1043,6 +1043,46 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase $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 * From 8613e8977b82ee9a7b13c3b7f71592c840c9e795 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Thu, 7 Nov 2024 11:20:37 +0900 Subject: [PATCH 22/23] 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 --- .../AAASetupData/requests/http_requests.php | 22 ++++- .../CoreLibsUrlRequestsCurlTest.php | 41 +++++++- www/admin/UrlRequests.target.php | 21 +++- www/admin/class_test.url-requests.curl.php | 17 ++++ www/lib/CoreLibs/UrlRequests/Curl.php | 95 +++++++++++-------- www/lib/CoreLibs/UrlRequests/CurlTrait.php | 34 +------ 6 files changed, 151 insertions(+), 79 deletions(-) diff --git a/4dev/tests/AAASetupData/requests/http_requests.php b/4dev/tests/AAASetupData/requests/http_requests.php index c3559020..912d715f 100644 --- a/4dev/tests/AAASetupData/requests/http_requests.php +++ b/4dev/tests/AAASetupData/requests/http_requests.php @@ -13,16 +13,26 @@ declare(strict_types=1); * build return json * * @param array $http_headers - * @param string $body + * @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, ]); } @@ -41,11 +51,15 @@ if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RU exit; } -if (($file_get = file_get_contents('php://input')) === false) { +// 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/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php index 3d751c6f..276b2ef6 100644 --- a/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php +++ b/4dev/tests/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -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,7 +1003,7 @@ 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' ); diff --git a/www/admin/UrlRequests.target.php b/www/admin/UrlRequests.target.php index c47a0fad..3d310ff4 100644 --- a/www/admin/UrlRequests.target.php +++ b/www/admin/UrlRequests.target.php @@ -16,16 +16,26 @@ $log = new CoreLibs\Logging\Logging([ * build return json * * @param array $http_headers - * @param string $body + * @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, ]); } @@ -45,7 +55,10 @@ if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RU exit; } -if (($file_get = file_get_contents('php://input')) === false) { +// 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; diff --git a/www/admin/class_test.url-requests.curl.php b/www/admin/class_test.url-requests.curl.php index f4a0848a..c43ce781 100644 --- a/www/admin/class_test.url-requests.curl.php +++ b/www/admin/class_test.url-requests.curl.php @@ -121,6 +121,23 @@ $data = $client->request( ] ); 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( diff --git a/www/lib/CoreLibs/UrlRequests/Curl.php b/www/lib/CoreLibs/UrlRequests/Curl.php index d6ffab13..fcc8bc14 100644 --- a/www/lib/CoreLibs/UrlRequests/Curl.php +++ b/www/lib/CoreLibs/UrlRequests/Curl.php @@ -175,7 +175,7 @@ class Curl implements Interface\RequestsInterface * @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 + private function authParser(array $auth): array { $return_auth = [ 'auth_basic_header' => '', @@ -376,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 ?? ''; } @@ -505,36 +507,59 @@ class Curl implements Interface\RequestsInterface 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 * - * @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> $headers Headers to be used in the request - * @param null|array $query Optinal query parameters - * @param null|string|array $body Data body, converted to JSON - * @param null|bool $http_errors Throw exception on http response - * 400 or higher if set to true - * @param null|array{0:string,1:string,2:string} $auth auth array, if null reset global set auth - * @return array{code:string,headers:array>,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>,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, - null|array $headers, - null|array $query, - null|string|array $body, - null|bool $http_errors, - null|array $auth, + 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($auth)) { - $auth_data = $this->authParser($auth); + if (is_array($options['auth'])) { + $auth_data = $this->authParser($options['auth']); } else { $auth_data = [ 'auth_basic_header' => null, @@ -543,9 +568,9 @@ class Curl implements Interface\RequestsInterface ]; } // build url - $this->url = $this->buildQuery($url, $query); + $this->url = $this->buildQuery($url, $options['query']); $this->headers = $this->convertHeaders($this->buildHeaders( - $headers, + $options['headers'], $auth_data['auth_basic_header'] )); if (!in_array($type, self::VALID_REQUEST_TYPES)) { @@ -578,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 = []; @@ -588,7 +613,7 @@ class Curl implements Interface\RequestsInterface // 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, $http_errors); + $http_response = $this->handleCurlResponse($handle, $http_result, $options['http_errors']); // close handler $this->handleCurlClose($handle); // return response and result @@ -641,7 +666,7 @@ class Curl implements Interface\RequestsInterface * @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 + 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) { @@ -674,8 +699,7 @@ 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 = $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'])) { @@ -772,9 +796,10 @@ class Curl implements Interface\RequestsInterface * 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 result string from the url call - * @param ?bool $http_errors if we should throw an exception on error, override config setting * @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 */ @@ -1000,21 +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, - !array_key_exists('http_errors', $options) ? null : $options['http_errors'], - !array_key_exists('auth', $options) ? [] : $options['auth'], + $options, ); } } diff --git a/www/lib/CoreLibs/UrlRequests/CurlTrait.php b/www/lib/CoreLibs/UrlRequests/CurlTrait.php index afacf76c..a3b0cbe5 100644 --- a/www/lib/CoreLibs/UrlRequests/CurlTrait.php +++ b/www/lib/CoreLibs/UrlRequests/CurlTrait.php @@ -18,30 +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{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?:array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} - */ - private function setOptions(string $type, array $options): array - { - $base = [ - "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'], - ]; - if ($type != "get") { - $base["body"] = $options['body'] ?? null; - } - return $base; - } - /** * combined set call for any type of request with options type parameters * The following options can be set: @@ -71,7 +47,7 @@ trait CurlTrait return $this->request( "get", $url, - $this->setOptions('get', $options), + $options, ); } @@ -89,7 +65,7 @@ trait CurlTrait return $this->request( "post", $url, - $this->setOptions('post', $options), + $options, ); } @@ -107,7 +83,7 @@ trait CurlTrait return $this->request( "put", $url, - $this->setOptions('put', $options), + $options, ); } @@ -125,7 +101,7 @@ trait CurlTrait return $this->request( "patch", $url, - $this->setOptions('patch', $options), + $options, ); } @@ -144,7 +120,7 @@ trait CurlTrait return $this->request( "delete", $url, - $this->setOptions('delete', $options), + $options, ); } } From d9bcb577d70b7bedcaa6dc50d6711f1219634c5a Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Thu, 7 Nov 2024 12:05:23 +0900 Subject: [PATCH 23/23] some minor test page code fixes --- phpstan.neon | 2 +- www/admin/class_test.php | 38 ++++++++++++++++++-------------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index a10a656b..a84ae62c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,7 +10,7 @@ parameters: # lineAfter: 3 level: 8 # max is now 9 # strictRules: - # allRules: true + # allRules: false checkMissingCallableSignature: true treatPhpDocTypesAsCertain: false paths: diff --git a/www/admin/class_test.php b/www/admin/class_test.php index 6c173f18..a5032e9a 100644 --- a/www/admin/class_test.php +++ b/www/admin/class_test.php @@ -134,7 +134,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 +177,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 . "
";