forked from loafle/openapi-generator-original
[PHP] handle properly multiple accept headers (#13844)
* [PHP] handle properly multiple accept headers * fixup! [PHP] handle properly multiple accept headers
This commit is contained in:
parent
6c8365cc9d
commit
d6de9c19c8
@ -42,10 +42,12 @@ class HeaderSelector
|
||||
if ($accept !== null) {
|
||||
$headers['Accept'] = $accept;
|
||||
}
|
||||
if(!$isMultipart) {
|
||||
|
||||
if (!$isMultipart) {
|
||||
if($contentType === '') {
|
||||
$contentType = 'application/json';
|
||||
}
|
||||
|
||||
$headers['Content-Type'] = $contentType;
|
||||
}
|
||||
|
||||
@ -53,20 +55,182 @@ class HeaderSelector
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header 'Accept' based on an array of Accept provided
|
||||
* Return the header 'Accept' based on an array of Accept provided.
|
||||
*
|
||||
* @param string[] $accept Array of header
|
||||
*
|
||||
* @return null|string Accept (e.g. application/json)
|
||||
*/
|
||||
private function selectAcceptHeader($accept)
|
||||
private function selectAcceptHeader(array $accept): ?string
|
||||
{
|
||||
if (count($accept) === 0 || (count($accept) === 1 && $accept[0] === '')) {
|
||||
# filter out empty entries
|
||||
$accept = array_filter($accept);
|
||||
|
||||
if (count($accept) === 0) {
|
||||
return null;
|
||||
} elseif ($jsonAccept = preg_grep('~(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$~', $accept)) {
|
||||
return implode(',', $jsonAccept);
|
||||
} else {
|
||||
}
|
||||
|
||||
# If there's only one Accept header, just use it
|
||||
if (count($accept) === 1) {
|
||||
return reset($accept);
|
||||
}
|
||||
|
||||
# If none of the available Accept headers is of type "json", then just use all them
|
||||
$headersWithJson = preg_grep('~(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$~', $accept);
|
||||
if (count($headersWithJson) === 0) {
|
||||
return implode(',', $accept);
|
||||
}
|
||||
|
||||
# If we got here, then we need add quality values (weight), as described in IETF RFC 9110, Items 12.4.2/12.5.1,
|
||||
# to give the highest priority to json-like headers - recalculating the existing ones, if needed
|
||||
return $this->getAcceptHeaderWithAdjustedWeight($accept, $headersWithJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Accept header string from the given "Accept" headers array, recalculating all weights
|
||||
*
|
||||
* @param string[] $accept Array of Accept Headers
|
||||
* @param string[] $headersWithJson Array of Accept Headers of type "json"
|
||||
*
|
||||
* @return string "Accept" Header (e.g. "application/json, text/html; q=0.9")
|
||||
*/
|
||||
private function getAcceptHeaderWithAdjustedWeight(array $accept, array $headersWithJson): string
|
||||
{
|
||||
$processedHeaders = [
|
||||
'withApplicationJson' => [],
|
||||
'withJson' => [],
|
||||
'withoutJson' => [],
|
||||
];
|
||||
|
||||
foreach ($accept as $header) {
|
||||
|
||||
$headerData = $this->getHeaderAndWeight($header);
|
||||
|
||||
if (stripos($headerData['header'], 'application/json') === 0) {
|
||||
$processedHeaders['withApplicationJson'][] = $headerData;
|
||||
} elseif (in_array($header, $headersWithJson, true)) {
|
||||
$processedHeaders['withJson'][] = $headerData;
|
||||
} else {
|
||||
$processedHeaders['withoutJson'][] = $headerData;
|
||||
}
|
||||
}
|
||||
|
||||
$acceptHeaders = [];
|
||||
$currentWeight = 1000;
|
||||
|
||||
$hasMoreThan28Headers = count($accept) > 28;
|
||||
|
||||
foreach($processedHeaders as $headers) {
|
||||
if (count($headers) > 0) {
|
||||
$acceptHeaders[] = $this->adjustWeight($headers, $currentWeight, $hasMoreThan28Headers);
|
||||
}
|
||||
}
|
||||
|
||||
$acceptHeaders = array_merge(...$acceptHeaders);
|
||||
|
||||
return implode(',', $acceptHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Accept header, returns an associative array splitting the header and its weight
|
||||
*
|
||||
* @param string $header "Accept" Header
|
||||
*
|
||||
* @return array with the header and its weight
|
||||
*/
|
||||
private function getHeaderAndWeight(string $header): array
|
||||
{
|
||||
# matches headers with weight, splitting the header and the weight in $outputArray
|
||||
if (preg_match('/(.*);\s*q=(1(?:\.0+)?|0\.\d+)$/', $header, $outputArray) === 1) {
|
||||
$headerData = [
|
||||
'header' => $outputArray[1],
|
||||
'weight' => (int)($outputArray[2] * 1000),
|
||||
];
|
||||
} else {
|
||||
$headerData = [
|
||||
'header' => trim($header),
|
||||
'weight' => 1000,
|
||||
];
|
||||
}
|
||||
|
||||
return $headerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $headers
|
||||
* @param float $currentWeight
|
||||
* @param bool $hasMoreThan28Headers
|
||||
* @return string[] array of adjusted "Accept" headers
|
||||
*/
|
||||
private function adjustWeight(array $headers, float &$currentWeight, bool $hasMoreThan28Headers): array
|
||||
{
|
||||
usort($headers, function (array $a, array $b) {
|
||||
return $b['weight'] - $a['weight'];
|
||||
});
|
||||
|
||||
$acceptHeaders = [];
|
||||
foreach ($headers as $index => $header) {
|
||||
if($index > 0 && $headers[$index - 1]['weight'] > $header['weight'])
|
||||
{
|
||||
$currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
|
||||
}
|
||||
|
||||
$weight = $currentWeight;
|
||||
|
||||
$acceptHeaders[] = $this->buildAcceptHeader($header['header'], $weight);
|
||||
}
|
||||
|
||||
$currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
|
||||
|
||||
return $acceptHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $header
|
||||
* @param int $weight
|
||||
* @return string
|
||||
*/
|
||||
private function buildAcceptHeader(string $header, int $weight): string
|
||||
{
|
||||
if($weight === 1000) {
|
||||
return $header;
|
||||
}
|
||||
|
||||
return trim($header, '; ') . ';q=' . rtrim(sprintf('%0.3f', $weight / 1000), '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the next weight, based on the current one.
|
||||
*
|
||||
* If there are less than 28 "Accept" headers, the weights will be decreased by 1 on its highest significant digit, using the
|
||||
* following formula:
|
||||
*
|
||||
* next weight = current weight - 10 ^ (floor(log(current weight - 1)))
|
||||
*
|
||||
* ( current weight minus ( 10 raised to the power of ( floor of (log to the base 10 of ( current weight minus 1 ) ) ) ) )
|
||||
*
|
||||
* Starting from 1000, this generates the following series:
|
||||
*
|
||||
* 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
|
||||
*
|
||||
* The resulting quality codes are closer to the average "normal" usage of them (like "q=0.9", "q=0.8" and so on), but it only works
|
||||
* if there is a maximum of 28 "Accept" headers. If we have more than that (which is extremely unlikely), then we fall back to a 1-by-1
|
||||
* decrement rule, which will result in quality codes like "q=0.999", "q=0.998" etc.
|
||||
*
|
||||
* @param int $currentWeight varying from 1 to 1000 (will be divided by 1000 to build the quality value)
|
||||
* @param bool $hasMoreThan28Headers
|
||||
* @return int
|
||||
*/
|
||||
public function getNextWeight(int $currentWeight, bool $hasMoreThan28Headers): int
|
||||
{
|
||||
if ($currentWeight <= 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($hasMoreThan28Headers) {
|
||||
return $currentWeight - 1;
|
||||
}
|
||||
|
||||
return $currentWeight - 10 ** floor( log10($currentWeight - 1) );
|
||||
}
|
||||
}
|
||||
|
@ -51,10 +51,12 @@ class HeaderSelector
|
||||
if ($accept !== null) {
|
||||
$headers['Accept'] = $accept;
|
||||
}
|
||||
if(!$isMultipart) {
|
||||
|
||||
if (!$isMultipart) {
|
||||
if($contentType === '') {
|
||||
$contentType = 'application/json';
|
||||
}
|
||||
|
||||
$headers['Content-Type'] = $contentType;
|
||||
}
|
||||
|
||||
@ -62,20 +64,182 @@ class HeaderSelector
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header 'Accept' based on an array of Accept provided
|
||||
* Return the header 'Accept' based on an array of Accept provided.
|
||||
*
|
||||
* @param string[] $accept Array of header
|
||||
*
|
||||
* @return null|string Accept (e.g. application/json)
|
||||
*/
|
||||
private function selectAcceptHeader($accept)
|
||||
private function selectAcceptHeader(array $accept): ?string
|
||||
{
|
||||
if (count($accept) === 0 || (count($accept) === 1 && $accept[0] === '')) {
|
||||
# filter out empty entries
|
||||
$accept = array_filter($accept);
|
||||
|
||||
if (count($accept) === 0) {
|
||||
return null;
|
||||
} elseif ($jsonAccept = preg_grep('~(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$~', $accept)) {
|
||||
return implode(',', $jsonAccept);
|
||||
} else {
|
||||
}
|
||||
|
||||
# If there's only one Accept header, just use it
|
||||
if (count($accept) === 1) {
|
||||
return reset($accept);
|
||||
}
|
||||
|
||||
# If none of the available Accept headers is of type "json", then just use all them
|
||||
$headersWithJson = preg_grep('~(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$~', $accept);
|
||||
if (count($headersWithJson) === 0) {
|
||||
return implode(',', $accept);
|
||||
}
|
||||
|
||||
# If we got here, then we need add quality values (weight), as described in IETF RFC 9110, Items 12.4.2/12.5.1,
|
||||
# to give the highest priority to json-like headers - recalculating the existing ones, if needed
|
||||
return $this->getAcceptHeaderWithAdjustedWeight($accept, $headersWithJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Accept header string from the given "Accept" headers array, recalculating all weights
|
||||
*
|
||||
* @param string[] $accept Array of Accept Headers
|
||||
* @param string[] $headersWithJson Array of Accept Headers of type "json"
|
||||
*
|
||||
* @return string "Accept" Header (e.g. "application/json, text/html; q=0.9")
|
||||
*/
|
||||
private function getAcceptHeaderWithAdjustedWeight(array $accept, array $headersWithJson): string
|
||||
{
|
||||
$processedHeaders = [
|
||||
'withApplicationJson' => [],
|
||||
'withJson' => [],
|
||||
'withoutJson' => [],
|
||||
];
|
||||
|
||||
foreach ($accept as $header) {
|
||||
|
||||
$headerData = $this->getHeaderAndWeight($header);
|
||||
|
||||
if (stripos($headerData['header'], 'application/json') === 0) {
|
||||
$processedHeaders['withApplicationJson'][] = $headerData;
|
||||
} elseif (in_array($header, $headersWithJson, true)) {
|
||||
$processedHeaders['withJson'][] = $headerData;
|
||||
} else {
|
||||
$processedHeaders['withoutJson'][] = $headerData;
|
||||
}
|
||||
}
|
||||
|
||||
$acceptHeaders = [];
|
||||
$currentWeight = 1000;
|
||||
|
||||
$hasMoreThan28Headers = count($accept) > 28;
|
||||
|
||||
foreach($processedHeaders as $headers) {
|
||||
if (count($headers) > 0) {
|
||||
$acceptHeaders[] = $this->adjustWeight($headers, $currentWeight, $hasMoreThan28Headers);
|
||||
}
|
||||
}
|
||||
|
||||
$acceptHeaders = array_merge(...$acceptHeaders);
|
||||
|
||||
return implode(',', $acceptHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Accept header, returns an associative array splitting the header and its weight
|
||||
*
|
||||
* @param string $header "Accept" Header
|
||||
*
|
||||
* @return array with the header and its weight
|
||||
*/
|
||||
private function getHeaderAndWeight(string $header): array
|
||||
{
|
||||
# matches headers with weight, splitting the header and the weight in $outputArray
|
||||
if (preg_match('/(.*);\s*q=(1(?:\.0+)?|0\.\d+)$/', $header, $outputArray) === 1) {
|
||||
$headerData = [
|
||||
'header' => $outputArray[1],
|
||||
'weight' => (int)($outputArray[2] * 1000),
|
||||
];
|
||||
} else {
|
||||
$headerData = [
|
||||
'header' => trim($header),
|
||||
'weight' => 1000,
|
||||
];
|
||||
}
|
||||
|
||||
return $headerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $headers
|
||||
* @param float $currentWeight
|
||||
* @param bool $hasMoreThan28Headers
|
||||
* @return string[] array of adjusted "Accept" headers
|
||||
*/
|
||||
private function adjustWeight(array $headers, float &$currentWeight, bool $hasMoreThan28Headers): array
|
||||
{
|
||||
usort($headers, function (array $a, array $b) {
|
||||
return $b['weight'] - $a['weight'];
|
||||
});
|
||||
|
||||
$acceptHeaders = [];
|
||||
foreach ($headers as $index => $header) {
|
||||
if($index > 0 && $headers[$index - 1]['weight'] > $header['weight'])
|
||||
{
|
||||
$currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
|
||||
}
|
||||
|
||||
$weight = $currentWeight;
|
||||
|
||||
$acceptHeaders[] = $this->buildAcceptHeader($header['header'], $weight);
|
||||
}
|
||||
|
||||
$currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
|
||||
|
||||
return $acceptHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $header
|
||||
* @param int $weight
|
||||
* @return string
|
||||
*/
|
||||
private function buildAcceptHeader(string $header, int $weight): string
|
||||
{
|
||||
if($weight === 1000) {
|
||||
return $header;
|
||||
}
|
||||
|
||||
return trim($header, '; ') . ';q=' . rtrim(sprintf('%0.3f', $weight / 1000), '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the next weight, based on the current one.
|
||||
*
|
||||
* If there are less than 28 "Accept" headers, the weights will be decreased by 1 on its highest significant digit, using the
|
||||
* following formula:
|
||||
*
|
||||
* next weight = current weight - 10 ^ (floor(log(current weight - 1)))
|
||||
*
|
||||
* ( current weight minus ( 10 raised to the power of ( floor of (log to the base 10 of ( current weight minus 1 ) ) ) ) )
|
||||
*
|
||||
* Starting from 1000, this generates the following series:
|
||||
*
|
||||
* 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
|
||||
*
|
||||
* The resulting quality codes are closer to the average "normal" usage of them (like "q=0.9", "q=0.8" and so on), but it only works
|
||||
* if there is a maximum of 28 "Accept" headers. If we have more than that (which is extremely unlikely), then we fall back to a 1-by-1
|
||||
* decrement rule, which will result in quality codes like "q=0.999", "q=0.998" etc.
|
||||
*
|
||||
* @param int $currentWeight varying from 1 to 1000 (will be divided by 1000 to build the quality value)
|
||||
* @param bool $hasMoreThan28Headers
|
||||
* @return int
|
||||
*/
|
||||
public function getNextWeight(int $currentWeight, bool $hasMoreThan28Headers): int
|
||||
{
|
||||
if ($currentWeight <= 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($hasMoreThan28Headers) {
|
||||
return $currentWeight - 1;
|
||||
}
|
||||
|
||||
return $currentWeight - 10 ** floor( log10($currentWeight - 1) );
|
||||
}
|
||||
}
|
||||
|
@ -30,41 +30,134 @@ class HeaderSelectorTest extends TestCase
|
||||
*/
|
||||
public function headersProvider(): array
|
||||
{
|
||||
return [
|
||||
$data = [
|
||||
// array $accept, string $contentType, bool $isMultipart, array $expectedHeaders
|
||||
[
|
||||
# No Accept, Content-Type
|
||||
[], 'application/xml', false, ['Content-Type' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
# No Accept, Content-Type, multipart
|
||||
[], 'application/xml', true, [],
|
||||
],
|
||||
[
|
||||
# single Accept, no Content-Type
|
||||
['application/xml'], '', false, ['Accept' => 'application/xml', 'Content-Type' => 'application/json'],
|
||||
],
|
||||
[
|
||||
# single Accept, no Content-Type, multipart
|
||||
['application/xml'], '', true, ['Accept' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
# single Accept, Content-Type
|
||||
['application/xml'], 'application/xml', false, ['Accept' => 'application/xml', 'Content-Type' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
# single Accept, Content-Type, multipart
|
||||
['application/xml'], 'application/xml', true, ['Accept' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
['application/xml', 'text/html'], 'application/xml', false, ['Accept' => 'application/xml,text/html', 'Content-Type' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
['application/json', 'text/html'], 'application/xml', false, ['Accept' => 'application/json', 'Content-Type' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
['text/html', 'application/json'], 'application/xml', false, ['Accept' => 'application/json', 'Content-Type' => 'application/xml'],
|
||||
],
|
||||
[
|
||||
# single Accept with parameters, Content-Type with parameters
|
||||
['application/json;format=flowed'], 'text/plain;format=fixed', false, ['Accept' => 'application/json;format=flowed', 'Content-Type' => 'text/plain;format=fixed'],
|
||||
],
|
||||
|
||||
# Tests for Accept headers - no change on Content-Type or multipart setting
|
||||
[
|
||||
['text/html', 'application/json;format=flowed'], 'text/plain;format=fixed', false, ['Accept' => 'application/json;format=flowed', 'Content-Type' => 'text/plain;format=fixed'],
|
||||
# Multiple Accept without Json
|
||||
['application/xml', 'text/html'], '', true, ['Accept' => 'application/xml,text/html'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept with application/json
|
||||
['application/json', 'text/html'], '', true, ['Accept' => 'application/json,text/html;q=0.9'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept with json-like
|
||||
['text/html', 'application/xml+json'], '', true, ['Accept' => 'application/xml+json,text/html;q=0.9'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept with application/json and json-like
|
||||
['text/html', 'application/json', 'application/xml+json'], '', true, ['Accept' => 'application/json,application/xml+json;q=0.9,text/html;q=0.8'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept, changing priority to application/json
|
||||
['application/xml', 'application/json;q=0.9', 'text/plain;format=flowed;q=0.8'], '', true, ['Accept' => 'application/json,application/xml;q=0.9,text/plain;format=flowed;q=0.8'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept, same priority for two headers, one being application/json
|
||||
['application/xml', 'application/json', 'text/plain;format=flowed;q=0.9'], '', true, ['Accept' => 'application/json,application/xml;q=0.9,text/plain;format=flowed;q=0.8'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept, same priority for two headers, both being application/json
|
||||
['application/json', 'application/json;IEEE754Compatible=true', 'text/plain;format=flowed;q=0.9'], '', true, ['Accept' => 'application/json,application/json;IEEE754Compatible=true,text/plain;format=flowed;q=0.9'],
|
||||
],
|
||||
[
|
||||
# Multiple Accept, same priority for three headers, two being application/json, with changing priority
|
||||
['application/json;q=0.9', 'application/json;IEEE754Compatible=true;q=0.9', 'application/xml', 'text/plain;format=flowed;q=0.9'], '', true, ['Accept' => 'application/json,application/json;IEEE754Compatible=true,application/xml;q=0.9,text/plain;format=flowed;q=0.8'],
|
||||
],
|
||||
];
|
||||
|
||||
# More than 28 Accept headers
|
||||
$data[] = $this->createTestDataForLargeNumberOfAcceptHeaders(30);
|
||||
|
||||
# More than 1000 Accept headers
|
||||
$data[] = $this->createTestDataForLargeNumberOfAcceptHeaders(1000);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numberOfHeaders
|
||||
* @return array
|
||||
*/
|
||||
private function createTestDataForLargeNumberOfAcceptHeaders(int $numberOfHeaders): array
|
||||
{
|
||||
$acceptHeaders = ['application/json;q=0.9'];
|
||||
$expected = ['application/json'];
|
||||
for ($i=1; $i<$numberOfHeaders; $i++) {
|
||||
$q = rtrim(sprintf('%0.3f', (1000 - $i) / 1000), '0');
|
||||
$acceptHeaders[] = "application/xml;option=$i;q=$q";
|
||||
$expected[] = "application/xml;option=$i;q=$q";
|
||||
}
|
||||
|
||||
return [
|
||||
$acceptHeaders,
|
||||
'',
|
||||
true,
|
||||
['Accept' => implode(',', $expected)]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider nextWeightProvider
|
||||
* @param int $currentWeight
|
||||
* @param bool $bHasMoreThan28Headers
|
||||
* @param int $expected
|
||||
*/
|
||||
public function testGetNextWeight(int $currentWeight, bool $bHasMoreThan28Headers, int $expected): void
|
||||
{
|
||||
$selector = new HeaderSelector();
|
||||
|
||||
self::assertEquals($expected, $selector->getNextWeight($currentWeight, $bHasMoreThan28Headers));
|
||||
}
|
||||
|
||||
public function nextWeightProvider(): array
|
||||
{
|
||||
return [
|
||||
[1000, true, 999],
|
||||
[999, true, 998],
|
||||
[2, true, 1],
|
||||
[1, true, 1],
|
||||
[1000, false, 900],
|
||||
[900, false, 800],
|
||||
[200, false, 100],
|
||||
[100, false, 90],
|
||||
[90, false, 80],
|
||||
[20, false, 10],
|
||||
[10, false, 9],
|
||||
[9, false, 8],
|
||||
[2, false, 1],
|
||||
[1, false, 1],
|
||||
[0, false, 1],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user