From 1bb4d5f4267ea0e634d15561b0faeef70ff8cf27 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 6 Nov 2024 18:56:17 +0900 Subject: [PATCH] UrlRequest curl class added --- src/UrlRequests/Curl.php | 1022 ++++++++++++++ src/UrlRequests/CurlTrait.php | 152 +++ .../Interface/RequestsInterface.php | 83 ++ test/configs/config.master.php | 46 - test/configs/config.path.php | 28 + test/configs/config.php | 13 - test/index.php | 3 + .../AAASetupData/requests/http_requests.php | 51 + .../DB/CoreLibsDBExtendedArrayIOTest.php | 1 - .../CoreLibsUrlRequestsCurlTest.php | 1199 +++++++++++++++++ 10 files changed, 2538 insertions(+), 60 deletions(-) create mode 100644 src/UrlRequests/Curl.php create mode 100644 src/UrlRequests/CurlTrait.php create mode 100644 src/UrlRequests/Interface/RequestsInterface.php create mode 100644 test/configs/config.path.php create mode 100644 test/phpunit/AAASetupData/requests/http_requests.php create mode 100644 test/phpunit/UrlRequests/CoreLibsUrlRequestsCurlTest.php diff --git a/src/UrlRequests/Curl.php b/src/UrlRequests/Curl.php new file mode 100644 index 0000000..d6ffab1 --- /dev/null +++ b/src/UrlRequests/Curl.php @@ -0,0 +1,1022 @@ + all the valid request type */ + private const VALID_REQUEST_TYPES = ["get", "post", "put", "patch", "delete"]; + /** @var array list of requests type that are set as custom in the curl options */ + private const CUSTOM_REQUESTS = ["put", "patch", "delete"]; + /** @var array list of requests types that have _POST type fields */ + private const HAVE_POST_FIELDS = ["post", "put", "patch", "delete"]; + /** @var array list of requests that must have a body */ + private const MANDATORY_POST_FIELDS = ["post", "put", "patch"]; + /** @var int http ok request */ + public const HTTP_OK = 200; + /** @var int http ok creted response */ + public const HTTP_CREATED = 201; + /** @var int http ok no content */ + public const HTTP_NO_CONTENT = 204; + /** @var int error bad request */ + public const HTTP_BAD_REQUEST = 400; + /** @var int error not authorized Request */ + public const HTTP_NOT_AUTHORIZED = 401; + /** @var int error forbidden */ + public const HTTP_FORBIDDEN = 403; + /** @var int error not found */ + public const HTTP_NOT_FOUND = 404; + /** @var int error conflict */ + public const HTTP_CONFLICT = 409; + /** @var int error unprocessable entity */ + public const HTTP_UNPROCESSABLE_ENTITY = 422; + /** @var int major version for user agent */ + public const MAJOR_VERSION = 1; + + // the config is set to be as much compatible to guzzelHttp as possible + // phpcs:disable Generic.Files.LineLength + /** @var array{auth?:array{0:string,1:string,2:string},http_errors:bool,base_uri:string,headers:array>,query:array,timeout:float,connection_timeout:float} config settings as + *phpcs:enable Generic.Files.LineLength + * auth: [0: user, 1: password, 2: auth type] + * http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors + * base_uri: base url to set, will prefix all urls given in calls + * headers: (array) base headers, can be overwritten by headers set in call + * timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS) + * connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS) + */ + private array $config = [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + /** @var array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} parsed base_uri */ + private array $parsed_base_uri = []; + /** @var array lower key header name matches to given header name */ + private array $headers_named = []; + /** @var int auth type from auth array in config */ + private int $auth_type = 0; + /** @var string username and password string from auth array in config */ + private string $auth_userpwd = ''; + /** @var string set if auth type basic is given, will be set as "Authorization: ..." */ + private string $auth_basic_header = ''; + + /** @var array> received headers per header name, with sub array if there are redirects */ + private array $received_headers = []; + + /** @var string the current url sent */ + private string $url = ''; + /** @var array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} parsed url to sent */ + private array $parsed_url = []; + /** @var array the current headers sent */ + private array $headers = []; + + /** + * see config allowe entries above + * + * @param array $config config settings to be set + */ + public function __construct(array $config = []) + { + $this->setConfiguration($config); + } + + // ********************************************************************* + // MARK: PRIVATE METHODS + // ********************************************************************* + + /** + * Set the main configuration + * + * phpcs:disable Generic.Files.LineLength + * @param array{auth?:array{0:string,1:string,2:string},http_errors?:bool,base_uri?:string,headers?:array>,query?:array,timeout?:float,connection_timeout?:float} $config + * @return void + * phpcs:enable Generic.Files.LineLength + */ + private function setConfiguration(array $config) + { + $default_config = [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + // auth string is array of 0: user, 1: password, 2: auth type + if (!empty($config['auth']) && is_array($config['auth'])) { + $auth_data = $this->authParser($config['auth']); + $this->auth_basic_header = $auth_data['auth_basic_header']; + $this->auth_type = $auth_data['auth_type']; + $this->auth_userpwd = $auth_data['auth_userpwd']; + } + // only set if bool + if ( + !isset($config['http_errors']) || + !is_bool($config['http_errors']) + ) { + $config['http_errors'] = true; + } + if (!empty($config['base_uri'])) { + if (($parsed_base_uri = $this->parseUrl($config['base_uri'])) !== false) { + $this->parsed_base_uri = $parsed_base_uri; + $config['base_uri'] = $config['base_uri']; + } + } + // general headers + if (!empty($config['headers'])) { + // seat the key lookup with lower keys + foreach (array_keys($config['headers']) as $key) { + if (isset($this->headers_named[strtolower((string)$key)])) { + continue; + } + $this->headers_named[strtolower((string)$key)] = (string)$key; + } + } + // timeout (must be numeric) + if (!empty($config['timeout']) && !is_numeric($config['timeout'])) { + $config['timeout'] = 0; + } + if (!empty($config['connection_timeout']) && !is_numeric($config['connection_timeout'])) { + $config['connection_timeout'] = 300; + } + + $this->config = array_merge($default_config, $config); + } + + // MARK: auth parser + + /** + * set various auth parameters and return them as array for further processing + * + * @param array{0:string,1:string,2:string} $auth + * @return array{auth_basic_header:string,auth_type:int,auth_userpwd:string} + */ + private function authParser(?array $auth): array + { + $return_auth = [ + 'auth_basic_header' => '', + 'auth_type' => 0, + 'auth_userpwd' => '', + ]; + // on empty return as is, to force defaults + if ($auth === []) { + return $return_auth; + } + // base auth sets the header actually + $type = isset($auth[2]) ? strtolower($auth[2]) : 'basic'; + $userpwd = $auth[0] . ':' . $auth[1]; + switch ($type) { + case 'basic': + $return_auth['auth_basic_header'] = 'Basic ' . base64_encode( + $userpwd + ); + break; + case 'digest': + $return_auth['auth_type'] = CURLAUTH_DIGEST; + $return_auth['auth_userpwd'] = $userpwd; + break; + case 'ntlm': + $return_auth['auth_type'] = CURLAUTH_NTLM; + $return_auth['auth_userpwd'] = $userpwd; + break; + } + return $return_auth; + } + + // MARK: parse and build url + + /** + * From: https://github.com/guzzle/psr7/blob/a70f5c95fb43bc83f07c9c948baa0dc1829bf201/src/Uri.php#L106C5-L132C6 + * guzzle/psr7::parse + * + * convert the url to valid sets + * + * @param string $url + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string}|false + */ + private function parseUrl(string $url): array|false + { + // If IPv6 + $prefix = ''; + if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) { + /** @var array{0:string, 1:string, 2:string} $matches */ + $prefix = $matches[1]; + $url = $matches[2]; + } + + /** @var string $encodedUrl */ + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%usD', + static function ($matches) { + return urlencode($matches[0]); + }, + $url + ); + + $result = parse_url($prefix . $encodedUrl); + + if ($result === false) { + return false; + } + + /** @var callable $caller */ + $caller = 'urldecode'; + return array_map($caller, $result); + } + + /** + * build back the URL based on the parsed URL scheme + * NOTE: this is only a sub implementation + * + * phpcs:disable Generic.Files.LineLength + * @param array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} $parsed_url + * @param bool $remove_until_slash [default=false] + * @param bool $add_query [default=false] + * @param bool $add_fragment [default=false] + * @return string + * phpcs:enable Generic.Files.LineLength + */ + private function buildUrl( + array $parsed_url, + bool $remove_until_slash = false, + bool $add_query = false, + bool $add_fragment = false + ): string { + $url = ''; + // scheme has : + if (!empty($parsed_url['scheme'])) { + $url .= $parsed_url['scheme'] . ':'; + } + // host + port = authority + if (!empty($parsed_url['host'])) { + $url .= '//'; + $url .= $parsed_url['host'] ?? ''; + if (!empty($parsed_url['port'])) { + $url .= ':' . $parsed_url['port']; + } + } + // remove the last part "/.." because we do not end with "/" + if ($remove_until_slash) { + $url_path = $parsed_url['path'] ?? ''; + if (($lastSlashPos = strrpos($url_path, '/')) !== false) { + $url .= substr($url_path, 0, $lastSlashPos + 1); + } + } else { + $url .= $parsed_url['path'] ?? ''; + } + // only on demand + if ($add_query && !empty($parsed_url['query'])) { + $url .= '?' . $parsed_url['query']; + } + if ($add_fragment && !empty($parsed_url['fragment'])) { + $url .= '#' . $parsed_url['fragment']; + } + return $url; + } + + // MARK: query, params and headers convert + + /** + * Build URL with base url and parameters + * + * @param string $url_req to send + * @param null|array $query any optional parameters to send + * @return string the fully build URL + */ + private function buildQuery(string $url_req, null|array $query = null): string + { + if (($parsed_url = $this->parseUrl($url_req)) !== false) { + $this->parsed_url = $parsed_url; + } + $url = $url_req; + if ( + !empty($this->config['base_uri']) && + empty($this->parsed_url['scheme']) + ) { + if (str_ends_with($this->config['base_uri'], '/')) { + $url = $this->config['base_uri'] . $url_req; + } else { + // remove until last / and add url, strip leading / if set + // remove last "/" part until we are at the domain + // if we do not start with http(s):// then assume blank + // NOTE any fragments or params will get dropped, only path will remain + $url = $this->buildUrl($this->parsed_base_uri, remove_until_slash: true) . $url_req; + } + if (($parsed_url = $this->parseUrl($url)) !== false) { + $this->parsed_url = $parsed_url; + } + } + // build query with global query + // any query set in the base_url or url_req will be overwritten + if (!empty($this->config['query'])) { + // add current query if set + // for params: if foo[0] then we ADD as php array type + // note that this has to be done on the user side, we just merge and local overrides global + $query = array_merge($this->config['query'], $query ?? []); + } + if (is_array($query)) { + $query = http_build_query($query, '', '&', PHP_QUERY_RFC3986); + } + // add the params to the url + if (!empty($query)) { + // if the url_url has a query or a a fragment, + // we need to build that url new + // $parsed_url = false; + if (!empty($this->parsed_url['query']) || !empty($this->parsed_url['framgent'])) { + $url = $this->buildUrl($this->parsed_url); + } + $url .= '?' . $query; + // fragments are ignored + // if (!empty($this->parsed_url['fragment'])) { + // $url .= '#' . $parsed_url['fragment']; + // } + } + // parse again with current url + if ($url != $url_req) { + if (($parsed_url = $this->parseUrl($url)) !== false) { + $this->parsed_url = $parsed_url; + } + } + + return $url; + } + + /** + * Convert array body data to json type string + * + * @param string|array $body + * @return string + */ + private function convertPayloadData(string|array $body): string + { + // convert to string as JSON block if it is an array + if (is_array($body)) { + $params = Json::jsonConvertArrayTo($body); + } + return $params ?? ''; + } + + /** + * header convert from array key -> value to string list + * if the key value is numeric, it is assumed this is an array string list only + * Note: this should not be the case + * + * @param array> $headers + * @return array + */ + private function convertHeaders(array $headers): array + { + $return_headers = []; + $header_keys = []; + foreach ($headers as $key => $value) { + if (!is_string($key)) { + // TODO: throw error + continue; + } + // bad if not valid header key + if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) { + throw new \UnexpectedValueException( + Json::jsonConvertArrayTo([ + 'status' => 'ERROR', + 'code' => 'R002', + 'type' => 'InvalidHeaderKey', + 'message' => 'Header key contains invalid characters', + 'context' => [ + 'key' => $key, + 'allowed' => '/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', + ], + ]), + 1 + ); + } + // if value is array, join to string + if (is_array($value)) { + $value = join(', ', $value); + } + $value = trim((string)$value, " \t"); + // header values must be valid + if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { + throw new \UnexpectedValueException( + Json::jsonConvertArrayTo([ + 'status' => 'ERROR', + 'code' => 'R003', + 'type' => 'InvalidHeaderValue', + 'message' => 'Header value contains invalid characters', + 'context' => [ + 'key' => $key, + 'value' => $value, + 'allowed' => '/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', + ], + ]), + 1 + ); + } + $return_headers[] = (string)$key . ':' . $value; + } + // remove empty entries + return $return_headers; + } + + /** + * default headers that are always set + * Authorization + * User-Agent + * + * @param array> $headers already set headers + * @param ?string $auth_basic_header + * @return array> + */ + private function buildDefaultHeaders(array $headers = [], ?string $auth_basic_header = ''): array + { + // add auth header if set, will overwrite any already set auth header + if ($auth_basic_header !== null && $auth_basic_header == '' && !empty($this->auth_basic_header)) { + $auth_basic_header = $this->auth_basic_header; + } + if (!empty($auth_basic_header)) { + // check if there is any auth header set, remove that one + if (!empty($auth_header_set = $this->headers_named[strtolower('Authorization')] ?? null)) { + unset($headers[$auth_header_set]); + } + // set new auth header + $headers['Authorization'] = $auth_basic_header; + } + // always add HTTP_HOST and HTTP_USER_AGENT + if (!isset($headers[strtolower('User-Agent')])) { + $headers['User-Agent'] = 'CoreLibsUrlRequestCurl/' . self::MAJOR_VERSION; + } + return $headers; + } + + /** + * Build headers, combine with global headers of they are set + * + * @param null|array> $headers + * @param ?string $auth_basic_header + * @return array> + */ + private function buildHeaders(null|array $headers, ?string $auth_basic_header): array + { + // if headers is null, return empty headers, do not set config default headers + // but the automatic set User-Agent and Authorization headers are always set + if ($headers === null) { + return $this->buildDefaultHeaders(auth_basic_header: $auth_basic_header); + } + // merge master headers with sub headers, sub headers overwrite master headers + if (!empty($this->config['headers'])) { + // we need to build the current headers as a lookup table + $headers_lookup = []; + foreach (array_keys($headers) as $key) { + $headers_lookup[strtolower((string)$key)] = (string)$key; + } + // add config headers if not set in local header + foreach ($this->headers_named as $header_key => $key) { + // is set local, use this, else use global + if (isset($headers_lookup[$header_key])) { + continue; + } + $headers[$key] = $this->config['headers'][$key]; + } + } + $headers = $this->buildDefaultHeaders($headers, $auth_basic_header); + return $headers; + } + + // 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} + * @throws \RuntimeException if type param is not valid + */ + 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 { + // 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, + $auth_data['auth_basic_header'] + )); + if (!in_array($type, self::VALID_REQUEST_TYPES)) { + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => 'ERROR', + 'code' => 'R001', + 'type' => 'InvalidRequestType', + 'message' => 'Invalid request type set: ' . $type, + 'context' => [ + 'type' => $type, + 'url' => $this->url, + 'headers' => $this->headers, + ], + ]), + 0, + ); + } + // init curl handle + $handle = $this->handleCurleInit($this->url); + // set the standard curl options + $this->setCurlOptions($handle, $this->headers, [ + 'auth_type' => $auth_data['auth_type'], + 'auth_userpwd' => $auth_data['auth_userpwd'], + ]); + // for post we set POST option + if ($type == "post") { + curl_setopt($handle, CURLOPT_POST, true); + } elseif (in_array($type, self::CUSTOM_REQUESTS)) { + curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type)); + } + // set body data if not null, will send empty [] for empty data + if (in_array($type, self::HAVE_POST_FIELDS) && $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($handle, $http_result, $http_errors); + // close handler + $this->handleCurlClose($handle); + // return response and result + return [ + 'code' => (string)$http_response, + 'headers' => $this->received_headers, + 'content' => (string)$http_result + ]; + } + + // MARK: curl init + + /** + * Handel curl init and errors + * + * @param string $url + * @return \CurlHandle + * @throws \RuntimeException if curl could not be initialized + */ + private function handleCurleInit(string $url): \CurlHandle + { + $handle = curl_init($url); + if ($handle !== false) { + return $handle; + } + // throw Error here with all codes + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => 'FAILURE', + 'code' => 'C001', + 'type' => 'CurlInitError', + 'message' => 'Failed to init curl with url: ' . $url, + 'context' => [ + 'url' => $url, + ], + ]), + 0, + ); + } + + // MARK: set curl options and header collector + + /** + * set the default curl options + * + * headers array: do not split into "key" => "value", they must be "key: value" + * + * @param \CurlHandle $handle + * @param array $headers list of options + * @param array{auth_type:?int,auth_userpwd:?string} $auth_data auth options to override global + * @return void + */ + private function setCurlOptions(\CurlHandle $handle, array $headers, ?array $auth_data): void + { + // for not Basic auth only, basic auth sets its own header + if ($auth_data['auth_type'] !== null || $auth_data['auth_userpwd'] !== null) { + // set global if any of the two is empty and both globals are set + if ( + (empty($auth_data['auth_type']) || empty($auth_data['auth_userpwd'])) && + !empty($this->auth_type) && !empty($this->auth_userpwd) + ) { + $auth_data['auth_type'] = $this->auth_type; + $auth_data['auth_userpwd'] = $this->auth_userpwd; + } + } + // set auth options for curl + if (!empty($auth_data['auth_type']) && !empty($auth_data['auth_userpwd'])) { + curl_setopt($handle, CURLOPT_HTTPAUTH, $auth_data['auth_type']); + curl_setopt($handle, CURLOPT_USERPWD, $auth_data['auth_userpwd']); + } + if ($headers !== []) { + curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); + } + // curl_setopt($handle, CURLOPT_FAILONERROR, true); + // return response as string and not just HTTP_OK + curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); + // for debug only + curl_setopt($handle, CURLINFO_HEADER_OUT, true); + // curl_setopt($handle, CURLOPT_HEADER, true); + // collect the current request headers + curl_setopt($handle, CURLOPT_HEADERFUNCTION, [$this, 'collectCurlHttpHeaders']); + // if any timeout <1 + $timeout_requires_no_signal = false; + // if we have a timeout signal + if (!empty($this->config['timeout'])) { + $timeout_requires_no_signal = $timeout_requires_no_signal || + $this->config['timeout'] < 1; + curl_setopt($handle, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000); + } + if (!empty($this->config['connection_timeout'])) { + $timeout_requires_no_signal = $timeout_requires_no_signal || + $this->config['connection_timeout'] < 1; + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout'] * 1000); + } + if ($timeout_requires_no_signal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + curl_setopt($handle, CURLOPT_NOSIGNAL, true); + } + } + + /** + * Collect HTTP headers + * They will be reset before each call + * + * @param \CurlHandle $curl current curl handle + * @param string $header header string to parse + * @return int size of current line of header + */ + private function collectCurlHttpHeaders(\CurlHandle $curl, string $header): int + { + $len = strlen($header); + $header = explode(':', $header, 2); + if (count($header) < 2) { + // ignore invalid headers + return $len; + } + $this->received_headers[strtolower(trim($header[0]))][] = trim($header[1]); + return $len; + } + + // MARK: Curl Exception handler + + /** + * handles any CURL execute and on error throws a correct error message + * + * @param \CurlHandle $handle Curl handler + * @return string Return content as string, if False will throw exception + * will only return HTTP_OK if CURLOPT_RETURNTRANSFER is turned off + * @throws \RuntimeException if the connection had an error + */ + private function handleCurlExec(\CurlHandle $handle): string + { + // execute query + $http_result = curl_exec($handle); + if ($http_result === true) { + // only if CURLOPT_RETURNTRANSFER is turned off + return (string)self::HTTP_OK; + } elseif ($http_result !== false) { + return $http_result; + } + $url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL); + $errno = curl_errno($handle); + $message = curl_error($handle); + switch ($errno) { + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_OPERATION_TIMEOUTED: + $message = 'Could not connect to server (' . $url . '). Please check your ' + . 'internet connection and try again. [' . $message . ']'; + break; + case CURLE_SSL_PEER_CERTIFICATE: + $message = 'Could not verify SSL certificate. Please make sure ' + . 'that your network is not intercepting certificates. ' + . '(Try going to ' . $url . 'in your browser.) ' + . '[' . $message . ']'; + break; + case 0: + default: + $message = 'Unexpected error communicating with server: ' . $message; + } + + // throw an error like in the normal reqeust, but set to CURL error + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => 'FAILURE', + 'code' => 'C002', + 'type' => 'CurlExecError', + 'message' => $message, + 'context' => [ + 'url' => $url, + 'errno' => $errno, + 'message' => $message, + ], + ]), + $errno + ); + } + + // MARK: curl response handler + + /** + * Handle curl response, will throw exception on anything that is lower 400 + * can be turned off by setting http_errors to false + * + * @param 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( + \CurlHandle $handle, + string $http_result, + ?bool $http_errors + ): string { + $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); + if ( + empty($http_errors ?? $this->config['http_errors']) || + $http_response < self::HTTP_BAD_REQUEST + ) { + return (string)$http_response; + } + // set curl error number + $err = curl_errno($handle); + // throw Error here with all codes + throw new \RuntimeException( + Json::jsonConvertArrayTo([ + 'status' => 'ERROR', + 'code' => 'H' . (string)$http_response, + 'type' => $http_response < 500 ? 'ClientError' : 'ServerError', + 'message' => 'Request could not be finished successfully because of bad request response', + 'context' => [ + 'http_response' => $http_response, + // extract all the error content if returned + 'result' => Json::jsonConvertToArray($http_result), + // curl internal error number + 'curl_errno' => $err, + // the full curl info block + 'curl_info' => curl_getinfo($handle), + ], + ]), + $err + ); + } + + /** + * close the current curl handle + * + * @param \CurlHandle $handle + * @return void + */ + private function handleCurlClose(\CurlHandle $handle): void + { + curl_close($handle); + } + + // ********************************************************************* + // MARK: PUBLIC METHODS + // ********************************************************************* + + // MARK: get class vars + + /** + * get the config array with all settings + * + * @return array all current config settings + */ + public function getConfig(): array + { + return $this->config; + } + + /** + * Return the full url as it was sent + * + * @return string url sent + */ + public function getUrlSent(): string + { + return $this->url; + } + + /** + * get the parsed url + * + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} + */ + public function getUrlParsedSent(): array + { + return $this->parsed_url; + } + + /** + * Return the full headers as they where sent + * + * @return array + */ + public function getHeadersSent(): array + { + return $this->headers; + } + + // MARK: set/remove for global headers + + /** + * set, add or overwrite header + * On default this will overwrite header, and not set + * + * @param array> $header + * @param bool $add [default=false] if set will add header to existing value + * @return void + */ + public function setHeaders(array $header, bool $add = false): void + { + foreach ($header as $key => $value) { + // check header previously set + if (isset($this->headers_named[strtolower($key)])) { + $header_key = $this->headers_named[strtolower($key)]; + if ($add) { + // for this add we always add array on the right side + if (!is_array($value)) { + $value = (array)$value; + } + // if not array, rewrite entry to array + if (!is_array($this->config['headers'][$header_key])) { + $this->config['headers'][$header_key] = [ + $this->config['headers'][$header_key] + ]; + } + $this->config['headers'][$header_key] = array_merge( + $this->config['headers'][$header_key], + $value + ); + } else { + $this->config['headers'][$header_key] = $value; + } + } else { + $this->headers_named[strtolower($key)] = $key; + $this->config['headers'][$key] = $value; + } + } + } + + /** + * remove header entry + * if key is only set then match only key, if both are set both sides must match + * + * @param array> $remove_headers + * @return void + */ + public function removeHeaders(array $remove_headers): void + { + foreach ($remove_headers as $key => $value) { + if (!isset($this->headers_named[strtolower($key)])) { + continue; + } + $header_key = $this->headers_named[strtolower($key)]; + if (!isset($this->config['headers'][$header_key])) { + continue; + } + // full remove + if ( + empty($value) || + ( + ( + // array both sides = equal + // string both sides = equal + (is_array($value) && is_array($this->config['headers'][$header_key])) || + (is_string($value) && is_string($this->config['headers'][$header_key])) + ) && + $value == $this->config['headers'][$header_key] + ) + ) { + unset($this->config['headers'][$header_key]); + unset($this->headers_named[$header_key]); + } elseif ( + // string value, array keys = in + // or both array and not a full match in the one before + (is_string($value) || is_array($value)) && + is_array($this->config['headers'][$header_key]) + ) { + // part remove of key, value must be array + if (!is_array($value)) { + $value = [$value]; + } + // array values so we rewrite the key pos + $this->config['headers'][$header_key] = array_values(array_diff( + $this->config['headers'][$header_key], + $value + )); + } + } + } + + // MARK: update/set base url + + /** + * Update or set the base url set + * if empty will unset the base url + * + * @param string $base_uri + * @return void + */ + public function setBaseUri(string $base_uri): void + { + $this->config['base_uri'] = $base_uri; + $this->parsed_base_uri = []; + if (!empty($base_uri)) { + if (($parsed_base_uri = $this->parseUrl($base_uri)) !== false) { + $this->parsed_base_uri = $parsed_base_uri; + } + } + } + + // MARK: main public call interface + + /** + * combined set call for any type of request with options type parameters + * + * phpcs:disable Generic.Files.LineLength + * @param string $type + * @param string $url + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + * @throws \UnexpectedValueException on missing body data when body data is needed + * phpcs:enable Generic.Files.LineLength + */ + public function request(string $type, string $url, array $options = []): array + { + // can have + // - headers + // - query + // 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'], + ); + } +} + +// __END__ diff --git a/src/UrlRequests/CurlTrait.php b/src/UrlRequests/CurlTrait.php new file mode 100644 index 0000000..afacf76 --- /dev/null +++ b/src/UrlRequests/CurlTrait.php @@ -0,0 +1,152 @@ +>,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: + * header: as array string:string + * query as string or array string:string + * body as string or array of any type + * + * @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{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 + */ + abstract public function request(string $type, string $url, array $options = []): array; + + /** + * Makes an request to the target url via curl: GET + * Returns result as string (json) + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} [default=[]] Result code, headers and content as array, content is json + */ + public function get(string $url, array $options = []): array + { + return $this->request( + "get", + $url, + $this->setOptions('get', $options), + ); + } + + /** + * Makes an request to the target url via curl: POST + * Returns result as string (json) + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function post(string $url, array $options): array + { + return $this->request( + "post", + $url, + $this->setOptions('post', $options), + ); + } + + /** + * Makes an request to the target url via curl: PUT + * Returns result as string (json) + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function put(string $url, array $options): array + { + return $this->request( + "put", + $url, + $this->setOptions('put', $options), + ); + } + + /** + * Makes an request to the target url via curl: PATCH + * Returns result as string (json) + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + */ + public function patch(string $url, array $options): array + { + return $this->request( + "patch", + $url, + $this->setOptions('patch', $options), + ); + } + + /** + * Makes an request to the target url via curl: DELETE + * Returns result as string (json) + * Note that DELETE body is optional + * + * @param string $url The URL being requested, + * including domain and protocol + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options Options to set + * @return array{code:string,headers:array>,content:string} [default=[]] Result code, headers and content as array, content is json + */ + public function delete(string $url, array $options = []): array + { + return $this->request( + "delete", + $url, + $this->setOptions('delete', $options), + ); + } +} + +// __END__ diff --git a/src/UrlRequests/Interface/RequestsInterface.php b/src/UrlRequests/Interface/RequestsInterface.php new file mode 100644 index 0000000..bc6bb0e --- /dev/null +++ b/src/UrlRequests/Interface/RequestsInterface.php @@ -0,0 +1,83 @@ + all current config settings + */ + public function getConfig(): array; + + /** + * Return the full url as it was sent + * + * @return string url sent + */ + public function getUrlSent(): string; + + /** + * get the parsed url + * + * @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} + */ + public function getUrlParsedSent(): array; + + /** + * Return the full headers as they where sent + * + * @return array + */ + public function getHeadersSent(): array; + + /** + * set, add or overwrite header + * On default this will overwrite header, and not set + * + * @param array> $header + * @param bool $add [default=false] if set will add header to existing value + * @return void + */ + public function setHeaders(array $header, bool $add = false): void; + + /** + * remove header entry + * if key is only set then match only key, if both are set both sides must match + * + * @param array $remove_headers + * @return void + */ + public function removeHeaders(array $remove_headers): void; + + /** + * Update the base url set, if empty will unset the base url + * + * @param string $base_uri + * @return void + */ + public function setBaseUri(string $base_uri): void; + + /** + * combined set call for any type of request with options type parameters + * + * phpcs:disable Generic.Files.LineLength + * @param string $type + * @param string $url + * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array>,query?:null|array,body?:null|string|array,http_errors?:null|bool} $options + * @return array{code:string,headers:array>,content:string} Result code, headers and content as array, content is json + * @throws \UnexpectedValueException on missing body data when body data is needed + * phpcs:enable Generic.Files.LineLength + */ + public function request(string $type, string $url, array $options = []): array; +} + +// __END__ diff --git a/test/configs/config.master.php b/test/configs/config.master.php index 355ab93..2bc5001 100644 --- a/test/configs/config.master.php +++ b/test/configs/config.master.php @@ -100,27 +100,6 @@ define('DEFAULT_ACL_LEVEL', 80); /************* LOGOUT ********************/ // logout target define('LOGOUT_TARGET', ''); -// password change allowed -define('PASSWORD_CHANGE', false); -define('PASSWORD_FORGOT', false); -// min/max password length -define('PASSWORD_MIN_LENGTH', 9); -define('PASSWORD_MAX_LENGTH', 255); -// defines allowed special characters -define('PASSWORD_SPECIAL_RANGE', '@$!%*?&'); -// password must have upper case, lower case, number, special -// comment out for not mandatory -define('PASSWORD_LOWER', '(?=.*[a-z])'); -define('PASSWORD_UPPER', '(?=.*[A-Z])'); -define('PASSWORD_NUMBER', '(?=.*\d)'); -define('PASSWORD_SPECIAL', "(?=.*[" . PASSWORD_SPECIAL_RANGE . "])"); -// define full regex -define('PASSWORD_REGEX', "/^" - . (defined('PASSWORD_LOWER') ? PASSWORD_LOWER : '') - . (defined('PASSWORD_UPPER') ? PASSWORD_UPPER : '') - . (defined('PASSWORD_NUMBER') ? PASSWORD_NUMBER : '') - . (defined('PASSWORD_SPECIAL') ? PASSWORD_SPECIAL : '') - . "[A-Za-z\d" . PASSWORD_SPECIAL_RANGE . "]{" . PASSWORD_MIN_LENGTH . "," . PASSWORD_MAX_LENGTH . "}$/"); /************* AJAX / ACCESS *************/ // ajax request type @@ -161,13 +140,6 @@ define('DEFAULT_LOCALE', 'en_US.UTF-8'); // default web page encoding setting define('DEFAULT_ENCODING', 'UTF-8'); -/************* LOGGING *******************/ -// below two can be defined here, but they should be -// defined in either the header file or the file itself -// as $LOG_FILE_ID which takes presence over LOG_FILE_ID -// see Basic class constructor -define('LOG_FILE_ID', BASE_NAME); - /************* QUEUE TABLE *************/ // if we have a dev/live system // set_live is a per page/per item @@ -291,22 +263,4 @@ if (file_exists(BASE . CONFIGS . 'config.other.php')) { require BASE . CONFIGS . 'config.other.php'; } -/************* DEBUG *******************/ -// turn off debug if debug flag is OFF -if (defined('DEBUG') && DEBUG == false) { - $ECHO_ALL = false; - $DEBUG_ALL = false; - $PRINT_ALL = false; - $DB_DEBUG = false; - $ENABLE_ERROR_HANDLING = false; - $DEBUG_ALL_OVERRIDE = false; -} else { - $ECHO_ALL = false; - $DEBUG_ALL = true; - $PRINT_ALL = true; - $DB_DEBUG = true; - $ENABLE_ERROR_HANDLING = false; - $DEBUG_ALL_OVERRIDE = false; -} - // __END__ diff --git a/test/configs/config.path.php b/test/configs/config.path.php new file mode 100644 index 0000000..9f559ce --- /dev/null +++ b/test/configs/config.path.php @@ -0,0 +1,28 @@ +"; +$curl = new CoreLibs\UrlRequests\Curl(); +print "Config: " . print_r($curl->getConfig(), true) . "
"; + // __END__ diff --git a/test/phpunit/AAASetupData/requests/http_requests.php b/test/phpunit/AAASetupData/requests/http_requests.php new file mode 100644 index 0000000..c355902 --- /dev/null +++ b/test/phpunit/AAASetupData/requests/http_requests.php @@ -0,0 +1,51 @@ + $http_headers + * @param string $body + * @return string + */ +function buildContent(array $http_headers, string $body): string +{ + return json_encode([ + 'HEADERS' => $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); + +header("Content-Type: application/json; charset=UTF-8"); + +// if the header has Authorization and RunAuthTest then exit with 401 +if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) { + header("HTTP/1.1 401 Unauthorized"); + print buildContent($http_headers, '{"code": 401, "content": {"Error": "Not Authorized"}}'); + exit; +} + +if (($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/test/phpunit/DB/CoreLibsDBExtendedArrayIOTest.php b/test/phpunit/DB/CoreLibsDBExtendedArrayIOTest.php index f5204ed..1e50e92 100644 --- a/test/phpunit/DB/CoreLibsDBExtendedArrayIOTest.php +++ b/test/phpunit/DB/CoreLibsDBExtendedArrayIOTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\TestCase; * Test class for DB\Extended\ArrayIO * This will only test the PgSQL parts * @coversDefaultClass \CoreLibs\DB\Extended\ArrayIO - * @coversDefaultClass \CoreLibs\DB\Extended\ArrayIO * @testdox \CoreLibs\Extended\ArrayIO method tests for extended DB interface */ final class CoreLibsDBExtendedArrayIOTest extends TestCase diff --git a/test/phpunit/UrlRequests/CoreLibsUrlRequestsCurlTest.php b/test/phpunit/UrlRequests/CoreLibsUrlRequestsCurlTest.php new file mode 100644 index 0000000..3d751c6 --- /dev/null +++ b/test/phpunit/UrlRequests/CoreLibsUrlRequestsCurlTest.php @@ -0,0 +1,1199 @@ + true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ]; + + /** + * check if we have some backend for testing + * + * @return void + */ + protected function setUp(): void + { + // check if local http servers + // or special started: + // php -S localhost:30999 \ + // -t /storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/4dev/tests/AAASetupData/requests/ + foreach ( + [ + // main dev + 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/' + . '4dev/tests/AAASetupData/requests/http_requests.php', + // composer package + 'https://soba.egplusww.jp/developers/clemens/core_data/composer-packages/' + . 'CoreLibs-Composer-All/test/phpunit/AAASetupData/requests/http_requests.php', + // if we run php -S localhost:30999 -t [see below] + // dev: /storage/var/www/html/developers/clemens/core_data/php_libraries/trunk/4dev/tests/AAASetupData/requests/ + // composer: /storage/var/www/html/developers/clemens/core_data/composer-packages/CoreLibs-Composer-All/test/phpunit/AAASetupData + 'localhost:30999/http_requests.php', + ] as $url + ) { + $handle = curl_init($url); + if ($handle === false) { + continue; + } + $this->url_basic = $url; + // split out the last / part for url set test + curl_close($handle); + // print "Open: $url\n"; + break; + } + } + + /** + * Undocumented function + * + * @return void + */ + protected function tearDown(): void + { + // end some httpserver + } + + /** + * Undocumented function + * + * @param string $url + * @return array + */ + private function splitUrl(string $url): array + { + + if (($lastSlashPos = strrpos($url, '/')) !== false) { + return [ + substr($url, 0, $lastSlashPos + 1), + substr($url, $lastSlashPos + 1, $lastSlashPos + 1) + ]; + } else { + return [0 => '', 1 => '']; + } + } + + // MARK: class setup tests + + /** + * Undocumented function + * + * @return array + */ + public function providerUrlRequestsCurlSetup(): array + { + return [ + // MARK: base config + 'no config' => [ + 'config' => null, + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => null, + ], + 'setup all possible configs' => [ + 'config' => [ + 'auth' => ['user', 'passowrd', 'Basic'], + 'http_errors' => false, + 'base_uri' => 'http://foo.bar.com', + 'headers' => [ + 'something' => 'other', + ], + 'query' => [ + 'foo' => 'bar', + ], + 'timeout' => 5, + 'connection_timeout' => 10, + ], + 'expected_set' => [ + 'auth' => ['user', 'passowrd', 'Basic'], + 'http_errors' => false, + 'base_uri' => 'http://foo.bar.com', + 'headers' => [ + 'something' => 'other', + ], + 'query' => [ + 'foo' => 'bar', + ], + 'timeout' => 5, + 'connection_timeout' => 10, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => null, + ], + // MARK: base url + 'setup base_uri only' => [ + 'config' => [ + 'base_uri' => 'http://bar.foo.com' + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => 'http://bar.foo.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => null, + ], + 'replace base_uri' => [ + 'config' => [ + 'base_uri' => 'http://bar.foo.com' + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => 'http://bar.foo.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => 'http://bar.baz.com', + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => 'http://bar.baz.com', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + // MARK: set headers + 'set header new' => [ + 'config' => null, + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => [ + 'new-header' => 'abc' + ], + 'set_header_add' => false, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'new-header' => 'abc', + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'set header overwrite' => [ + 'config' => [ + 'headers' => [ + 'existing-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => [ + 'existing-entry' => 'bar' + ], + 'set_header_add' => false, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'bar' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'set header add' => [ + 'config' => [ + 'headers' => [ + 'existing-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => [ + 'existing-entry' => 'bar' + ], + 'set_header_add' => true, + 'remove_header' => null, + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'existing-entry' => ['foo', 'bar'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + // MARK: test remove header + 'remove header string, full match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => 'foo' + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header string, key match only' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => 'foo' + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => 'foo' + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => null + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, key match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => null + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, string match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => 'foo' + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + 'remove header array, array match' => [ + 'config' => [ + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + ], + 'expected_set' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['foo', 'bar', 'baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + 'new_base_uri' => null, + 'set_header' => null, + 'set_header_add' => null, + 'remove_header' => [ + 'remove-entry' => ['foo', 'bar',] + ], + 'expected_change' => [ + 'http_errors' => true, + 'base_uri' => '', + 'query' => [], + 'headers' => [ + 'remove-entry' => ['baz'] + ], + 'timeout' => 0, + 'connection_timeout' => 300, + ], + ], + ]; + } + + // MARK: setup/config + + /** + * set setup + header, base uri change + * + * @covers ::Curl + * @covers ::setBaseUri + * @covers ::addHeader + * @covers ::removeHEader + * @dataProvider providerUrlRequestsCurlSetup + * @testdox UrlRequests\Curl Class setup tasks [$_dataName] + * + * @param null|array $config + * @param array $expected + * @param null|string $new_base_uri + * @param null|array $set_header + * @param null|bool $set_header_add + * @param null|array $remove_header + * @param null|array $expected_change + * @return void + */ + public function testUrlRequestsCurlSetupConfig( + null|array $config, + array $expected_set, + null|string $new_base_uri, + null|array $set_header, + null|bool $set_header_add, + null|array $remove_header, + null|array $expected_change + ): void { + // empty new + if ($config === null) { + $curl = new \CoreLibs\UrlRequests\Curl(); + } else { + $curl = new \CoreLibs\UrlRequests\Curl($config); + }; + // if ($new_base_uri === null && $set_header === null && $remove_header === null) { + // } + $this->assertEquals($expected_set, $curl->getConfig(), 'Class setup config mismatch'); + if ($new_base_uri !== null) { + $curl->setBaseUri($new_base_uri); + $this->assertEquals($expected_change, $curl->getConfig(), 'new base_uri not matching'); + } + if ($set_header !== null) { + if ($set_header_add !== null) { + $curl->setHeaders($set_header, $set_header_add); + } else { + $curl->setHeaders($set_header); + } + $this->assertEquals($expected_change, $curl->getConfig(), 'new headers not matching'); + } + if ($remove_header !== null) { + $curl->removeHeaders($remove_header); + $this->assertEquals($expected_change, $curl->getConfig(), 'removed headers not matching'); + } + } + + // MARK: request call tests + + /** + * Undocumented function + * + * @return array + */ + public function providerUrlRequestsCurlRequestBuild(): array + { + return [ + // MARK: config overwrite + // this would be: + // - base url + add url + // - base url + replace url + 'base url + add url' => [ + 'type' => 'get', + 'config' => [ + "base_uri" => "URL_START" + ], + 'url' => "URL_END", + 'options' => null, + 'sent_url' => "URL_FULL", + 'sent_url_parsed' => null, + 'sent_headers' => [ + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base url + replace url' => [ + 'type' => 'get', + 'config' => [ + "base_uri" => "URL_START" + ], + 'url' => "URL_FULL", + 'options' => null, + 'sent_url' => "URL_FULL", + 'sent_url_parsed' => null, + 'sent_headers' => [ + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + // - base header + add header + // - base header + reset header (null) + // - base query + add query + 'base header + add header' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => [ + "header-two" => "two", + ] + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "header-two:two", + "header-one:one", + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base header + reset header' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => null + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base header + add header (same)' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => [ + "header-one" => "one", + "header-two" => "two", + ] + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "header-two:two", + "header-one:one", + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + 'base header + add header (replace)' => [ + 'type' => 'get', + 'config' => [ + "headers" => [ + "header-one" => "one", + ] + ], + 'url' => null, + 'options' => [ + "headers" => [ + "header-one" => "three", + "header-two" => "two", + ] + ], + 'sent_url' => null, + 'sent_url_parsed' => null, + 'sent_headers' => [ + "header-two:two", + "header-one:three", + "User-Agent:CoreLibsUrlRequestCurl/1", + ], + 'return_code' => "200", + 'return_content' => '' + ], + ]; + } + + // MARK: test call overwrite + + /** + * request build tests + * + * @covers ::request + * @dataProvider providerUrlRequestsCurlRequestBuild + * @testdox UrlRequests\Curl call with data merge [$_dataName] + * + * @param string $type + * @param array|null $config + * @param string|null $url + * @param array|null $options + * @param string|null $sent_url + * @param array|null $sent_url_parsed + * @param array $sent_headers + * @param string $return_code + * @param string $return_content + * @return void + */ + public function testUrlRequestsCurlRequestBuild( + string $type, + ?array $config, + ?string $url, + ?array $options, + ?string $sent_url, + ?array $sent_url_parsed, + array $sent_headers, + string $return_code, + string $return_content + ) { + if (!$this->url_basic) { + $this->markTestSkipped('No backend interface setup for testing: GET'); + } + if ($url) { + list($url_start, $url_end) = $this->splitUrl($this->url_basic); + $config['base_uri'] = str_replace('URL_START', $url_start, $config['base_uri']); + $url = str_replace('URL_END', $url_end, $url); + $url = str_replace('URL_FULL', $this->url_basic, $url); + $sent_url = str_replace('URL_FULL', $this->url_basic, $sent_url); + } + // init without or with config + if ($config === null) { + $curl = new \CoreLibs\UrlRequests\Curl(); + } else { + $curl = new \CoreLibs\UrlRequests\Curl($config); + }; + // set url + if ($url === null) { + $url = $this->url_basic; + } + // options + if (is_array($options)) { + $respone = $curl->request($type, $url, $options); + } else { + $respone = $curl->request($type, $url); + } + // headers + $this->assertEqualsCanonicalizing( + $sent_headers, + $curl->getHeadersSent(), + 'Headers do not metch' + ); + // url + if ($sent_url) { + $this->assertEquals( + $sent_url, + $curl->getUrlSent(), + 'Sent URL does not match' + ); + } + // check return code + $this->assertEquals( + $return_code, + $respone['code'], + 'Return code not matching' + ); + } + + // MARK: test basic call provider + + /** + * Undocumented function + * + * @return array + */ + public function providerUrlRequestsCurlRequest(): array + { + // phpcs:disable Generic.Files.LineLength + // get and delete can have null body, but only get will never have a body + $provider = []; + // MARK: get + foreach (['get'] as $type) { + $provider["basic " . $type . ", no options"] = [ + 'type' => $type, + 'options' => null, + 'return_code' => "200", + 'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":null}' + ]; + $provider["basic " . $type . ", query options"] = [ + 'type' => $type, + 'options' => [ + "query" => ["foo" => "bar"], + ], + 'return_code' => "200", + 'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":{"foo":"bar"},"BODY":null}' + ]; + } + // MARK: delete + foreach (['delete'] as $type) { + // MARK: post + $provider["basic " . $type . ", no options"] = [ + 'type' => $type, + 'options' => null, + 'return_code' => "200", + 'return_content' => '{"HEADERS":{"HTTP_USER_AGENT":"CoreLibsUrlRequestCurl\/1","HTTP_ACCEPT":"*\/*","HTTP_HOST":"soba.egplusww.jp"},"REQUEST_TYPE":"' . strtoupper($type) . '","PARAMS":[],"BODY":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: auth header set via config + + /** + * Test auth settings and auth override + * + * @testdox UrlRequests\Curl auth test call + * + * @return void + */ + public function testUrlRequestsCurlAuthHeader() + { + $curl = new \CoreLibs\UrlRequests\Curl([ + "auth" => ["user", "pass", "basic"], + "http_errors" => false, + ]); + $curl->request('get', $this->url_basic); + // check that the auth header matches + $this->assertContains( + "Authorization:Basic dXNlcjpwYXNz", + $curl->getHeadersSent() + ); + // if we sent new request with auth header, this one should not be used + $curl->request('get', $this->url_basic, [ + "headers" => ["Authorization" => "Failed"] + ]); + // check that the auth header matches + $this->assertContains( + "Authorization:Basic dXNlcjpwYXNz", + $curl->getHeadersSent() + ); + // override auth: reset + $curl->request('get', $this->url_basic, [ + "auth" => null + ]); + $this->assertNotContains( + "Authorization:Basic dXNlcjpwYXNz", + $curl->getHeadersSent() + ); + // override auth: different auth + $curl->request('get', $this->url_basic, [ + "auth" => ["user2", "pass2", "basic"] + ]); + // check that the auth header matches + $this->assertContains( + "Authorization:Basic dXNlcjI6cGFzczI=", + $curl->getHeadersSent() + ); + } + + // MARK: test exceptions + + /** + * Exception:InvalidRequestType + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:InvalidRequestType + * + * @return void + */ + public function testExceptionInvalidRequestType(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/InvalidRequestType/"); + $curl->request('wrong', 'http://foo.bar.com'); + } + + /** + * Exception:InvalidHeaderKey + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:InvalidHeaderKey + * + * @return void + */ + public function testExceptionInvalidHeaderKey(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/InvalidHeaderKey/"); + $curl->request('get', $this->url_basic, [ + "headers" => [ + "(invalid-key)" => "key" + ] + ]); + } + + /** + * Exception:InvalidHeaderValue + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:InvalidHeaderValue + * + * @return void + */ + public function testExceptionInvalidHeaderValue(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/InvalidHeaderValue/"); + $curl->request('get', $this->url_basic, [ + "headers" => [ + "invalid-value" => "\x19\x10" + ] + ]); + } + + /** + * TODO: Exception:CurlInitError + * + * @testdox UrlRequests\Curl Exception:CurlInitError + * + * @return void + */ + // public function testExceptionCurlInitError(): void + // { + // $this->markTestSkipped('Test Exception CurlInitError not implemented'); + // } + + /** + * Exception:CurlExecError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:CurlExecError + * + * @return void + */ + public function testExceptionCurlError(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/CurlExecError/"); + // invalid yrl + $curl->request('get', 'as-4939345!#$%'); + } + + /** + * Exception:ClientError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:ClientError + * + * @return void + */ + public function testExceptionBadRequest(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/ClientError/"); + $curl->request('get', $this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ] + ]); + } + + /** + * Exception:ClientError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:ClientError on call enable + * + * @return void + */ + public function testExceptionBadRequestEnable(): void + { + $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches("/ClientError/"); + $curl->request('get', $this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ], + "http_errors" => true + ]); + } + + /** + * Exception:ClientError + * + * @covers ::request + * @testdox UrlRequests\Curl Exception:ClientError unset on call + * + * @return void + */ + public function testExceptionBadRequestUnset(): void + { + // if true, with false it has to be off + $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); + $response = $curl->request('get', $this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ], + "http_errors" => false, + ]); + $this->assertEquals( + "401", + $response['code'], + 'Unset Exception failed with false' + ); + // if false, null should not change it + $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); + $response = $curl->request('get', $this->url_basic, [ + "headers" => [ + "Authorization" => "schmalztiegel", + "RunAuthTest" => "yes", + ], + "http_errors" => null, + ]); + $this->assertEquals( + "401", + $response['code'], + 'Unset Exception failed with null' + ); + } +} + +// __END__