Compare commits

...

4 Commits

Author SHA1 Message Date
Clemens Schwaighofer
0c51a3be87 Add phpunit tests for header key/value exceptions 2024-11-06 18:49:48 +09:00
Clemens Schwaighofer
f9cf36524e UrlRequests auth set allowed in requests call
Removed the parseHeaders public call, headers must be set as array

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

Auth headers can be set per call and will override global settings if matching
2024-11-06 18:42:35 +09:00
Clemens Schwaighofer
bacb9881ac Fix UrlRequests Interface name, fix header build
Header default build was not done well, pass original headers inside and
set them. On new default start with empty array.

Switch to CoreLibs Json calls, because we use this libarary anyway already
2024-11-06 14:28:15 +09:00
Clemens Schwaighofer
f0fae1f76d Fix Composer package phpunit test url for UrlRequests 2024-11-06 13:35:00 +09:00
5 changed files with 349 additions and 217 deletions

View File

@@ -47,7 +47,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
. '4dev/tests/AAASetupData/requests/http_requests.php', . '4dev/tests/AAASetupData/requests/http_requests.php',
// composer package // composer package
'https://soba.egplusww.jp/developers/clemens/core_data/composer-packages/' '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] // 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/ // 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 // composer: /storage/var/www/html/developers/clemens/core_data/composer-packages/CoreLibs-Composer-All/test/phpunit/AAASetupData
@@ -732,7 +732,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
); );
} }
// MARK: test basi call provider // MARK: test basic call provider
/** /**
* Undocumented function * Undocumented function
@@ -976,11 +976,61 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
); );
} }
// MARK: auth header set via config
/**
* Test auth settings and auth override
*
* @testdox UrlRequests\Curl auth test call
*
* @return void
*/
public function testUrlRequestsCurlAuthHeader()
{
$curl = new \CoreLibs\UrlRequests\Curl([
"auth" => ["user", "pass", "basic"],
"http_errors" => false,
]);
$curl->request('get', $this->url_basic);
// check that the auth header matches
$this->assertContains(
"Authorization:Basic dXNlcjpwYXNz",
$curl->getHeadersSent()
);
// if we sent new request with auth header, this one should not be used
$curl->request('get', $this->url_basic, [
"headers" => ["Authorization" => "Failed"]
]);
// check that the auth header matches
$this->assertContains(
"Authorization:Basic dXNlcjpwYXNz",
$curl->getHeadersSent()
);
// override auth: reset
$curl->request('get', $this->url_basic, [
"auth" => null
]);
$this->assertNotContains(
"Authorization:Basic dXNlcjpwYXNz",
$curl->getHeadersSent()
);
// override auth: different auth
$curl->request('get', $this->url_basic, [
"auth" => ["user2", "pass2", "basic"]
]);
// check that the auth header matches
$this->assertContains(
"Authorization:Basic dXNlcjI6cGFzczI=",
$curl->getHeadersSent()
);
}
// MARK: test exceptions // MARK: test exceptions
/** /**
* Undocumented function * Exception:InvalidRequestType
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidRequestType * @testdox UrlRequests\Curl Exception:InvalidRequestType
* *
* @return void * @return void
@@ -993,6 +1043,46 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
$curl->request('wrong', 'http://foo.bar.com'); $curl->request('wrong', 'http://foo.bar.com');
} }
/**
* Exception:InvalidHeaderKey
*
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidHeaderKey
*
* @return void
*/
public function testExceptionInvalidHeaderKey(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/InvalidHeaderKey/");
$curl->request('get', $this->url_basic, [
"headers" => [
"(invalid-key)" => "key"
]
]);
}
/**
* Exception:InvalidHeaderValue
*
* @covers ::request
* @testdox UrlRequests\Curl Exception:InvalidHeaderValue
*
* @return void
*/
public function testExceptionInvalidHeaderValue(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/InvalidHeaderValue/");
$curl->request('get', $this->url_basic, [
"headers" => [
"invalid-value" => "\x19\x10"
]
]);
}
/** /**
* TODO: Exception:CurlInitError * TODO: Exception:CurlInitError
* *
@@ -1006,9 +1096,10 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
// } // }
/** /**
* Undocumented function * Exception:CurlExecError
* *
* @testdox UrlRequests\Curl Exception:CurlError * @covers ::request
* @testdox UrlRequests\Curl Exception:CurlExecError
* *
* @return void * @return void
*/ */
@@ -1024,6 +1115,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
/** /**
* Exception:ClientError * Exception:ClientError
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError * @testdox UrlRequests\Curl Exception:ClientError
* *
* @return void * @return void
@@ -1033,7 +1125,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/ClientError/"); $this->expectExceptionMessageMatches("/ClientError/");
$curl->get($this->url_basic, [ $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1044,6 +1136,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
/** /**
* Exception:ClientError * Exception:ClientError
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError on call enable * @testdox UrlRequests\Curl Exception:ClientError on call enable
* *
* @return void * @return void
@@ -1053,7 +1146,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/ClientError/"); $this->expectExceptionMessageMatches("/ClientError/");
$curl->get($this->url_basic, [ $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1065,6 +1158,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
/** /**
* Exception:ClientError * Exception:ClientError
* *
* @covers ::request
* @testdox UrlRequests\Curl Exception:ClientError unset on call * @testdox UrlRequests\Curl Exception:ClientError unset on call
* *
* @return void * @return void
@@ -1073,7 +1167,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
{ {
// if true, with false it has to be off // if true, with false it has to be off
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => true]);
$response = $curl->get($this->url_basic, [ $response = $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1087,7 +1181,7 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
); );
// if false, null should not change it // if false, null should not change it
$curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]); $curl = new \CoreLibs\UrlRequests\Curl(["http_errors" => false]);
$response = $curl->get($this->url_basic, [ $response = $curl->request('get', $this->url_basic, [
"headers" => [ "headers" => [
"Authorization" => "schmalztiegel", "Authorization" => "schmalztiegel",
"RunAuthTest" => "yes", "RunAuthTest" => "yes",
@@ -1100,24 +1194,6 @@ final class CoreLibsUrlRequestsCurlTest extends TestCase
'Unset Exception failed with null' 'Unset Exception failed with null'
); );
} }
/**
* Undocumented function
*
* @testdox UrlRequests\Curl Exception:DuplicatedArrayKey
*
* @return void
*/
public function testExceptionDuplicatedArrayKey(): void
{
$curl = new \CoreLibs\UrlRequests\Curl();
$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessageMatches("/DuplicatedArrayKey/");
$curl->prepareHeaders([
'header-double:a',
'header-double:b',
]);
}
} }
// __END__ // __END__

View File

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

View File

@@ -18,10 +18,8 @@ declare(strict_types=1);
namespace CoreLibs\UrlRequests; namespace CoreLibs\UrlRequests;
use RuntimeException;
use CoreLibs\Convert\Json; use CoreLibs\Convert\Json;
/** @package CoreLibs\UrlRequests */
class Curl implements Interface\RequestsInterface class Curl implements Interface\RequestsInterface
{ {
// all general calls: get/post/put/patch/delete // all general calls: get/post/put/patch/delete
@@ -61,12 +59,11 @@ class Curl implements Interface\RequestsInterface
/** @var array{auth?:array{0:string,1:string,2:string},http_errors:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as /** @var array{auth?:array{0:string,1:string,2:string},http_errors:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as
*phpcs:enable Generic.Files.LineLength *phpcs:enable Generic.Files.LineLength
* auth: [0: user, 1: password, 2: auth type] * auth: [0: user, 1: password, 2: auth type]
* http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors
* base_uri: base url to set, will prefix all urls given in calls * base_uri: base url to set, will prefix all urls given in calls
* headers: (array) base headers, can be overwritten by headers set in call * headers: (array) base headers, can be overwritten by headers set in call
* timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS) * timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS)
* connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS) * connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS)
* : below is not a guzzleHttp config
* http_errors: default true, bool true/false for throwing exception on >= 400 HTTP errors
*/ */
private array $config = [ private array $config = [
'http_errors' => true, 'http_errors' => true,
@@ -131,29 +128,10 @@ class Curl implements Interface\RequestsInterface
]; ];
// auth string is array of 0: user, 1: password, 2: auth type // auth string is array of 0: user, 1: password, 2: auth type
if (!empty($config['auth']) && is_array($config['auth'])) { if (!empty($config['auth']) && is_array($config['auth'])) {
// base auth sets the header actually $auth_data = $this->authParser($config['auth']);
$type = isset($config['auth'][2]) ? strtolower($config['auth'][2]) : 'basic'; $this->auth_basic_header = $auth_data['auth_basic_header'];
$userpwd = $config['auth'][0] . ':' . $config['auth'][1]; $this->auth_type = $auth_data['auth_type'];
switch ($type) { $this->auth_userpwd = $auth_data['auth_userpwd'];
case 'basic':
$this->auth_basic_header = 'Basic ' . base64_encode(
$userpwd
);
// if (!isset($config['headers']['Authorization'])) {
// $config['headers']['Authorization'] = 'Basic ' . base64_encode(
// $userpwd
// );
// }
break;
case 'digest':
$this->auth_type = CURLAUTH_DIGEST;
$this->auth_userpwd = $userpwd;
break;
case 'ntlm':
$this->auth_type = CURLAUTH_NTLM;
$this->auth_userpwd = $userpwd;
break;
}
} }
// only set if bool // only set if bool
if ( if (
@@ -189,6 +167,46 @@ class Curl implements Interface\RequestsInterface
$this->config = array_merge($default_config, $config); $this->config = array_merge($default_config, $config);
} }
// MARK: auth parser
/**
* set various auth parameters and return them as array for further processing
*
* @param array{0:string,1:string,2:string} $auth
* @return array{auth_basic_header:string,auth_type:int,auth_userpwd:string}
*/
private function authParser(?array $auth): array
{
$return_auth = [
'auth_basic_header' => '',
'auth_type' => 0,
'auth_userpwd' => '',
];
// on empty return as is, to force defaults
if ($auth === []) {
return $return_auth;
}
// base auth sets the header actually
$type = isset($auth[2]) ? strtolower($auth[2]) : 'basic';
$userpwd = $auth[0] . ':' . $auth[1];
switch ($type) {
case 'basic':
$return_auth['auth_basic_header'] = 'Basic ' . base64_encode(
$userpwd
);
break;
case 'digest':
$return_auth['auth_type'] = CURLAUTH_DIGEST;
$return_auth['auth_userpwd'] = $userpwd;
break;
case 'ntlm':
$return_auth['auth_type'] = CURLAUTH_NTLM;
$return_auth['auth_userpwd'] = $userpwd;
break;
}
return $return_auth;
}
// MARK: parse and build url // MARK: parse and build url
/** /**
@@ -373,6 +391,7 @@ class Curl implements Interface\RequestsInterface
private function convertHeaders(array $headers): array private function convertHeaders(array $headers): array
{ {
$return_headers = []; $return_headers = [];
$header_keys = [];
foreach ($headers as $key => $value) { foreach ($headers as $key => $value) {
if (!is_string($key)) { if (!is_string($key)) {
// TODO: throw error // TODO: throw error
@@ -380,8 +399,19 @@ class Curl implements Interface\RequestsInterface
} }
// bad if not valid header key // bad if not valid header key
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) { if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) {
// TODO throw error throw new \UnexpectedValueException(
continue; Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => 'R002',
'type' => 'InvalidHeaderKey',
'message' => 'Header key contains invalid characters',
'context' => [
'key' => $key,
'allowed' => '/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D',
],
]),
1
);
} }
// if value is array, join to string // if value is array, join to string
if (is_array($value)) { if (is_array($value)) {
@@ -390,8 +420,20 @@ class Curl implements Interface\RequestsInterface
$value = trim((string)$value, " \t"); $value = trim((string)$value, " \t");
// header values must be valid // header values must be valid
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
// TODO throw error throw new \UnexpectedValueException(
continue; Json::jsonConvertArrayTo([
'status' => 'ERROR',
'code' => 'R003',
'type' => 'InvalidHeaderValue',
'message' => 'Header value contains invalid characters',
'context' => [
'key' => $key,
'value' => $value,
'allowed' => '/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D',
],
]),
1
);
} }
$return_headers[] = (string)$key . ':' . $value; $return_headers[] = (string)$key . ':' . $value;
} }
@@ -404,14 +446,23 @@ class Curl implements Interface\RequestsInterface
* Authorization * Authorization
* User-Agent * User-Agent
* *
* @return array<string,string> * @param array<string,string|array<string>> $headers already set headers
* @param ?string $auth_basic_header
* @return array<string,string|array<string>>
*/ */
private function buildDefaultHeaders(): array private function buildDefaultHeaders(array $headers = [], ?string $auth_basic_header = ''): array
{ {
$headers = []; // add auth header if set, will overwrite any already set auth header
// add auth header if set if ($auth_basic_header !== null && $auth_basic_header == '' && !empty($this->auth_basic_header)) {
if (!empty($this->auth_basic_header)) { $auth_basic_header = $this->auth_basic_header;
$headers['Authorization'] = $this->auth_basic_header; }
if (!empty($auth_basic_header)) {
// check if there is any auth header set, remove that one
if (!empty($auth_header_set = $this->headers_named[strtolower('Authorization')] ?? null)) {
unset($headers[$auth_header_set]);
}
// set new auth header
$headers['Authorization'] = $auth_basic_header;
} }
// always add HTTP_HOST and HTTP_USER_AGENT // always add HTTP_HOST and HTTP_USER_AGENT
if (!isset($headers[strtolower('User-Agent')])) { if (!isset($headers[strtolower('User-Agent')])) {
@@ -424,14 +475,15 @@ class Curl implements Interface\RequestsInterface
* Build headers, combine with global headers of they are set * Build headers, combine with global headers of they are set
* *
* @param null|array<string,string|array<string>> $headers * @param null|array<string,string|array<string>> $headers
* @param ?string $auth_basic_header
* @return array<string,string|array<string>> * @return array<string,string|array<string>>
*/ */
private function buildHeaders(null|array $headers): array private function buildHeaders(null|array $headers, ?string $auth_basic_header): array
{ {
// if headers is null, return empty headers, do not set config default headers // if headers is null, return empty headers, do not set config default headers
// but the automatic set User-Agent and Authorization headers are always set // but the automatic set User-Agent and Authorization headers are always set
if ($headers === null) { if ($headers === null) {
return $this->buildDefaultHeaders(); return $this->buildDefaultHeaders(auth_basic_header: $auth_basic_header);
} }
// merge master headers with sub headers, sub headers overwrite master headers // merge master headers with sub headers, sub headers overwrite master headers
if (!empty($this->config['headers'])) { if (!empty($this->config['headers'])) {
@@ -449,7 +501,7 @@ class Curl implements Interface\RequestsInterface
$headers[$key] = $this->config['headers'][$key]; $headers[$key] = $this->config['headers'][$key];
} }
} }
$headers = array_merge($headers, $this->buildDefaultHeaders()); $headers = $this->buildDefaultHeaders($headers, $auth_basic_header);
return $headers; return $headers;
} }
@@ -467,6 +519,7 @@ class Curl implements Interface\RequestsInterface
* @param null|string|array<string,mixed> $body Data body, converted to JSON * @param null|string|array<string,mixed> $body Data body, converted to JSON
* @param null|bool $http_errors Throw exception on http response * @param null|bool $http_errors Throw exception on http response
* 400 or higher if set to true * 400 or higher if set to true
* @param null|array{0:string,1:string,2:string} $auth auth array, if null reset global set auth
* @return array{code:string,headers:array<string,array<string>>,content:string} * @return array{code:string,headers:array<string,array<string>>,content:string}
* @throws \RuntimeException if type param is not valid * @throws \RuntimeException if type param is not valid
*/ */
@@ -477,14 +530,29 @@ class Curl implements Interface\RequestsInterface
null|array $query, null|array $query,
null|string|array $body, null|string|array $body,
null|bool $http_errors, null|bool $http_errors,
null|array $auth,
): array { ): array {
// set auth from override
if (is_array($auth)) {
$auth_data = $this->authParser($auth);
} else {
$auth_data = [
'auth_basic_header' => null,
'auth_type' => null,
'auth_userpwd' => null,
];
}
// build url
$this->url = $this->buildQuery($url, $query); $this->url = $this->buildQuery($url, $query);
$this->headers = $this->convertHeaders($this->buildHeaders($headers)); $this->headers = $this->convertHeaders($this->buildHeaders(
$headers,
$auth_data['auth_basic_header']
));
if (!in_array($type, self::VALID_REQUEST_TYPES)) { if (!in_array($type, self::VALID_REQUEST_TYPES)) {
throw new RuntimeException( throw new \RuntimeException(
json_encode([ Json::jsonConvertArrayTo([
'status' => 'ERROR', 'status' => 'ERROR',
'code' => 'R002', 'code' => 'R001',
'type' => 'InvalidRequestType', 'type' => 'InvalidRequestType',
'message' => 'Invalid request type set: ' . $type, 'message' => 'Invalid request type set: ' . $type,
'context' => [ 'context' => [
@@ -492,14 +560,17 @@ class Curl implements Interface\RequestsInterface
'url' => $this->url, 'url' => $this->url,
'headers' => $this->headers, 'headers' => $this->headers,
], ],
]) ?: '', ]),
0, 0,
); );
} }
// init curl handle // init curl handle
$handle = $this->handleCurleInit($this->url); $handle = $this->handleCurleInit($this->url);
// set the standard curl options // set the standard curl options
$this->setCurlOptions($handle, $this->headers); $this->setCurlOptions($handle, $this->headers, [
'auth_type' => $auth_data['auth_type'],
'auth_userpwd' => $auth_data['auth_userpwd'],
]);
// for post we set POST option // for post we set POST option
if ($type == "post") { if ($type == "post") {
curl_setopt($handle, CURLOPT_POST, true); curl_setopt($handle, CURLOPT_POST, true);
@@ -517,7 +588,7 @@ class Curl implements Interface\RequestsInterface
// for debug // for debug
// print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>"; // print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>";
// get response code and bail on not authorized // get response code and bail on not authorized
$http_response = $this->handleCurlResponse($http_result, $http_errors, $handle); $http_response = $this->handleCurlResponse($handle, $http_result, $http_errors);
// close handler // close handler
$this->handleCurlClose($handle); $this->handleCurlClose($handle);
// return response and result // return response and result
@@ -544,8 +615,8 @@ class Curl implements Interface\RequestsInterface
return $handle; return $handle;
} }
// throw Error here with all codes // throw Error here with all codes
throw new RuntimeException( throw new \RuntimeException(
json_encode([ Json::jsonConvertArrayTo([
'status' => 'FAILURE', 'status' => 'FAILURE',
'code' => 'C001', 'code' => 'C001',
'type' => 'CurlInitError', 'type' => 'CurlInitError',
@@ -553,7 +624,7 @@ class Curl implements Interface\RequestsInterface
'context' => [ 'context' => [
'url' => $url, 'url' => $url,
], ],
]) ?: '', ]),
0, 0,
); );
} }
@@ -567,14 +638,26 @@ class Curl implements Interface\RequestsInterface
* *
* @param \CurlHandle $handle * @param \CurlHandle $handle
* @param array<string> $headers list of options * @param array<string> $headers list of options
* @param array{auth_type:?int,auth_userpwd:?string} $auth_data auth options to override global
* @return void * @return void
*/ */
private function setCurlOptions(\CurlHandle $handle, array $headers): void private function setCurlOptions(\CurlHandle $handle, array $headers, ?array $auth_data): void
{ {
// for not Basic auth, basic auth sets its own header // for not Basic auth only, basic auth sets its own header
if (!empty($this->auth_type) && !empty($this->auth_userpwd)) { if ($auth_data['auth_type'] !== null || $auth_data['auth_userpwd'] !== null) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $this->auth_type); // set global if any of the two is empty and both globals are set
curl_setopt($handle, CURLOPT_USERPWD, $this->auth_userpwd); if (
(empty($auth_data['auth_type']) || empty($auth_data['auth_userpwd'])) &&
!empty($this->auth_type) && !empty($this->auth_userpwd)
) {
$auth_data['auth_type'] = $this->auth_type;
$auth_data['auth_userpwd'] = $this->auth_userpwd;
}
}
// set auth options for curl
if (!empty($auth_data['auth_type']) && !empty($auth_data['auth_userpwd'])) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $auth_data['auth_type']);
curl_setopt($handle, CURLOPT_USERPWD, $auth_data['auth_userpwd']);
} }
if ($headers !== []) { if ($headers !== []) {
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
@@ -591,11 +674,13 @@ class Curl implements Interface\RequestsInterface
$timeout_requires_no_signal = false; $timeout_requires_no_signal = false;
// if we have a timeout signal // if we have a timeout signal
if (!empty($this->config['timeout'])) { 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); curl_setopt($handle, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000);
} }
if (!empty($this->config['connection_timeout'])) { 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); curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout'] * 1000);
} }
if ($timeout_requires_no_signal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { if ($timeout_requires_no_signal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
@@ -665,8 +750,8 @@ class Curl implements Interface\RequestsInterface
} }
// throw an error like in the normal reqeust, but set to CURL error // throw an error like in the normal reqeust, but set to CURL error
throw new RuntimeException( throw new \RuntimeException(
json_encode([ Json::jsonConvertArrayTo([
'status' => 'FAILURE', 'status' => 'FAILURE',
'code' => 'C002', 'code' => 'C002',
'type' => 'CurlExecError', 'type' => 'CurlExecError',
@@ -676,7 +761,7 @@ class Curl implements Interface\RequestsInterface
'errno' => $errno, 'errno' => $errno,
'message' => $message, 'message' => $message,
], ],
]) ?: '', ]),
$errno $errno
); );
} }
@@ -694,9 +779,9 @@ class Curl implements Interface\RequestsInterface
* @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400 * @throws \RuntimeException if http_errors is true then will throw exception on any response code >= 400
*/ */
private function handleCurlResponse( private function handleCurlResponse(
\CurlHandle $handle,
string $http_result, string $http_result,
?bool $http_errors, ?bool $http_errors
\CurlHandle $handle
): string { ): string {
$http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); $http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if ( if (
@@ -708,8 +793,8 @@ class Curl implements Interface\RequestsInterface
// set curl error number // set curl error number
$err = curl_errno($handle); $err = curl_errno($handle);
// throw Error here with all codes // throw Error here with all codes
throw new RuntimeException( throw new \RuntimeException(
json_encode([ Json::jsonConvertArrayTo([
'status' => 'ERROR', 'status' => 'ERROR',
'code' => 'H' . (string)$http_response, 'code' => 'H' . (string)$http_response,
'type' => $http_response < 500 ? 'ClientError' : 'ServerError', 'type' => $http_response < 500 ? 'ClientError' : 'ServerError',
@@ -717,13 +802,13 @@ class Curl implements Interface\RequestsInterface
'context' => [ 'context' => [
'http_response' => $http_response, 'http_response' => $http_response,
// extract all the error content if returned // extract all the error content if returned
'result' => json_decode((string)$http_result, true), 'result' => Json::jsonConvertToArray($http_result),
// curl internal error number // curl internal error number
'curl_errno' => $err, 'curl_errno' => $err,
// the full curl info block // the full curl info block
'curl_info' => curl_getinfo($handle), 'curl_info' => curl_getinfo($handle),
], ],
]) ?: '', ]),
$err $err
); );
} }
@@ -743,48 +828,6 @@ class Curl implements Interface\RequestsInterface
// MARK: PUBLIC METHODS // MARK: PUBLIC METHODS
// ********************************************************************* // *********************************************************************
/**
* Convert an array with header strings like "foo: bar" to the interface
* needed "foo" => "bar" type
* Skips entries that are already in key => value type, by checking if the
* key is a not a number
*
* @param array<int|string,string> $headers
* @return array<string,string>
* @throws \UnexpectedValueException on duplicate header key
*/
public function prepareHeaders(array $headers): array
{
$return_headers = [];
foreach ($headers as $header_key => $header) {
// skip if header key is not numeric
if (!is_numeric($header_key)) {
$return_headers[$header_key] = $header;
continue;
}
list($_key, $_value) = explode(':', $header);
if (array_key_exists($_key, $return_headers)) {
// raise exception if key already exists
throw new \UnexpectedValueException(
json_encode([
'status' => 'ERROR',
'code' => 'R001',
'type' => 'DuplicatedArrayKey',
'message' => 'Key already exists in the headers',
'context' => [
'key' => $_key,
'headers' => $headers,
'return_headers' => $return_headers,
],
]) ?: '',
1
);
}
$return_headers[$_key] = $_value;
}
return $return_headers;
}
// MARK: get class vars // MARK: get class vars
/** /**
@@ -947,7 +990,7 @@ class Curl implements Interface\RequestsInterface
* phpcs:disable Generic.Files.LineLength * phpcs:disable Generic.Files.LineLength
* @param string $type * @param string $type
* @param string $url * @param string $url
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options * @param array{auth?:null|array{0:string,1:string,2:string},headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>,http_errors?:null|bool} $options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json * @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed * @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength * phpcs:enable Generic.Files.LineLength
@@ -971,6 +1014,7 @@ class Curl implements Interface\RequestsInterface
$options['query'] ?? null, $options['query'] ?? null,
$options['body'] ?? null, $options['body'] ?? null,
!array_key_exists('http_errors', $options) ? null : $options['http_errors'], !array_key_exists('http_errors', $options) ? null : $options['http_errors'],
!array_key_exists('auth', $options) ? [] : $options['auth'],
); );
} }
} }

View File

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

View File

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