Skip to content

Commit d6de9c1

Browse files
[PHP] handle properly multiple accept headers (#13844)
* [PHP] handle properly multiple accept headers * fixup! [PHP] handle properly multiple accept headers
1 parent 6c8365c commit d6de9c1

3 files changed

Lines changed: 441 additions & 20 deletions

File tree

modules/openapi-generator/src/main/resources/php/HeaderSelector.mustache

Lines changed: 171 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,31 +42,195 @@ class HeaderSelector
4242
if ($accept !== null) {
4343
$headers['Accept'] = $accept;
4444
}
45-
if(!$isMultipart) {
45+
46+
if (!$isMultipart) {
4647
if($contentType === '') {
4748
$contentType = 'application/json';
4849
}
50+
4951
$headers['Content-Type'] = $contentType;
5052
}
5153

5254
return $headers;
5355
}
5456

5557
/**
56-
* Return the header 'Accept' based on an array of Accept provided
58+
* Return the header 'Accept' based on an array of Accept provided.
5759
*
5860
* @param string[] $accept Array of header
5961
*
6062
* @return null|string Accept (e.g. application/json)
6163
*/
62-
private function selectAcceptHeader($accept)
64+
private function selectAcceptHeader(array $accept): ?string
6365
{
64-
if (count($accept) === 0 || (count($accept) === 1 && $accept[0] === '')) {
66+
# filter out empty entries
67+
$accept = array_filter($accept);
68+
69+
if (count($accept) === 0) {
6570
return null;
66-
} elseif ($jsonAccept = preg_grep('~(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$~', $accept)) {
67-
return implode(',', $jsonAccept);
68-
} else {
71+
}
72+
73+
# If there's only one Accept header, just use it
74+
if (count($accept) === 1) {
75+
return reset($accept);
76+
}
77+
78+
# If none of the available Accept headers is of type "json", then just use all them
79+
$headersWithJson = preg_grep('~(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$~', $accept);
80+
if (count($headersWithJson) === 0) {
6981
return implode(',', $accept);
7082
}
83+
84+
# If we got here, then we need add quality values (weight), as described in IETF RFC 9110, Items 12.4.2/12.5.1,
85+
# to give the highest priority to json-like headers - recalculating the existing ones, if needed
86+
return $this->getAcceptHeaderWithAdjustedWeight($accept, $headersWithJson);
87+
}
88+
89+
/**
90+
* Create an Accept header string from the given "Accept" headers array, recalculating all weights
91+
*
92+
* @param string[] $accept Array of Accept Headers
93+
* @param string[] $headersWithJson Array of Accept Headers of type "json"
94+
*
95+
* @return string "Accept" Header (e.g. "application/json, text/html; q=0.9")
96+
*/
97+
private function getAcceptHeaderWithAdjustedWeight(array $accept, array $headersWithJson): string
98+
{
99+
$processedHeaders = [
100+
'withApplicationJson' => [],
101+
'withJson' => [],
102+
'withoutJson' => [],
103+
];
104+
105+
foreach ($accept as $header) {
106+
107+
$headerData = $this->getHeaderAndWeight($header);
108+
109+
if (stripos($headerData['header'], 'application/json') === 0) {
110+
$processedHeaders['withApplicationJson'][] = $headerData;
111+
} elseif (in_array($header, $headersWithJson, true)) {
112+
$processedHeaders['withJson'][] = $headerData;
113+
} else {
114+
$processedHeaders['withoutJson'][] = $headerData;
115+
}
116+
}
117+
118+
$acceptHeaders = [];
119+
$currentWeight = 1000;
120+
121+
$hasMoreThan28Headers = count($accept) > 28;
122+
123+
foreach($processedHeaders as $headers) {
124+
if (count($headers) > 0) {
125+
$acceptHeaders[] = $this->adjustWeight($headers, $currentWeight, $hasMoreThan28Headers);
126+
}
127+
}
128+
129+
$acceptHeaders = array_merge(...$acceptHeaders);
130+
131+
return implode(',', $acceptHeaders);
132+
}
133+
134+
/**
135+
* Given an Accept header, returns an associative array splitting the header and its weight
136+
*
137+
* @param string $header "Accept" Header
138+
*
139+
* @return array with the header and its weight
140+
*/
141+
private function getHeaderAndWeight(string $header): array
142+
{
143+
# matches headers with weight, splitting the header and the weight in $outputArray
144+
if (preg_match('/(.*);\s*q=(1(?:\.0+)?|0\.\d+)$/', $header, $outputArray) === 1) {
145+
$headerData = [
146+
'header' => $outputArray[1],
147+
'weight' => (int)($outputArray[2] * 1000),
148+
];
149+
} else {
150+
$headerData = [
151+
'header' => trim($header),
152+
'weight' => 1000,
153+
];
154+
}
155+
156+
return $headerData;
157+
}
158+
159+
/**
160+
* @param array[] $headers
161+
* @param float $currentWeight
162+
* @param bool $hasMoreThan28Headers
163+
* @return string[] array of adjusted "Accept" headers
164+
*/
165+
private function adjustWeight(array $headers, float &$currentWeight, bool $hasMoreThan28Headers): array
166+
{
167+
usort($headers, function (array $a, array $b) {
168+
return $b['weight'] - $a['weight'];
169+
});
170+
171+
$acceptHeaders = [];
172+
foreach ($headers as $index => $header) {
173+
if($index > 0 && $headers[$index - 1]['weight'] > $header['weight'])
174+
{
175+
$currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
176+
}
177+
178+
$weight = $currentWeight;
179+
180+
$acceptHeaders[] = $this->buildAcceptHeader($header['header'], $weight);
181+
}
182+
183+
$currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
184+
185+
return $acceptHeaders;
186+
}
187+
188+
/**
189+
* @param string $header
190+
* @param int $weight
191+
* @return string
192+
*/
193+
private function buildAcceptHeader(string $header, int $weight): string
194+
{
195+
if($weight === 1000) {
196+
return $header;
197+
}
198+
199+
return trim($header, '; ') . ';q=' . rtrim(sprintf('%0.3f', $weight / 1000), '0');
200+
}
201+
202+
/**
203+
* Calculate the next weight, based on the current one.
204+
*
205+
* If there are less than 28 "Accept" headers, the weights will be decreased by 1 on its highest significant digit, using the
206+
* following formula:
207+
*
208+
* next weight = current weight - 10 ^ (floor(log(current weight - 1)))
209+
*
210+
* ( current weight minus ( 10 raised to the power of ( floor of (log to the base 10 of ( current weight minus 1 ) ) ) ) )
211+
*
212+
* Starting from 1000, this generates the following series:
213+
*
214+
* 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
215+
*
216+
* 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
217+
* 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
218+
* decrement rule, which will result in quality codes like "q=0.999", "q=0.998" etc.
219+
*
220+
* @param int $currentWeight varying from 1 to 1000 (will be divided by 1000 to build the quality value)
221+
* @param bool $hasMoreThan28Headers
222+
* @return int
223+
*/
224+
public function getNextWeight(int $currentWeight, bool $hasMoreThan28Headers): int
225+
{
226+
if ($currentWeight <= 1) {
227+
return 1;
228+
}
229+
230+
if ($hasMoreThan28Headers) {
231+
return $currentWeight - 1;
232+
}
233+
234+
return $currentWeight - 10 ** floor( log10($currentWeight - 1) );
71235
}
72236
}

0 commit comments

Comments
 (0)