HeaderSelector.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <?php
  2. /**
  3. * HeaderSelector
  4. * PHP version 8.1
  5. *
  6. * @category Class
  7. * @package WikipaliApi
  8. * @author OpenAPI Generator team
  9. * @link https://openapi-generator.tech
  10. */
  11. /**
  12. * MINT API
  13. *
  14. * International Academy Of Pali Tipitaka(国际巴利三藏学院) 由 `php openapi/scripts/generate.php` 依据 api-v13 的路由与控制器源码生成。 响应统一封装为 `{ok, message, data}`;`ok=false` 时 `message` 为错误信息。
  15. *
  16. * The version of the OpenAPI document: 2026.10.8
  17. * Generated by: https://openapi-generator.tech
  18. * Generator version: 7.25.0
  19. */
  20. /**
  21. * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  22. * https://openapi-generator.tech
  23. * Do not edit the class manually.
  24. */
  25. namespace WikipaliApi;
  26. /**
  27. * HeaderSelector Class Doc Comment
  28. *
  29. * @category Class
  30. * @package WikipaliApi
  31. * @author OpenAPI Generator team
  32. * @link https://openapi-generator.tech
  33. */
  34. class HeaderSelector
  35. {
  36. /**
  37. * @param string[] $accept
  38. * @param string $contentType
  39. * @param bool $isMultipart
  40. * @return string[]
  41. */
  42. public function selectHeaders(array $accept, string $contentType, bool $isMultipart): array
  43. {
  44. $headers = [];
  45. $accept = $this->selectAcceptHeader($accept);
  46. if ($accept !== null) {
  47. $headers['Accept'] = $accept;
  48. }
  49. if (!$isMultipart) {
  50. if($contentType === '') {
  51. $contentType = 'application/json';
  52. }
  53. $headers['Content-Type'] = $contentType;
  54. }
  55. return $headers;
  56. }
  57. /**
  58. * Return the header 'Accept' based on an array of Accept provided.
  59. *
  60. * @param string[] $accept Array of header
  61. *
  62. * @return null|string Accept (e.g. application/json)
  63. */
  64. private function selectAcceptHeader(array $accept): ?string
  65. {
  66. # filter out empty entries
  67. $accept = array_filter($accept);
  68. if (count($accept) === 0) {
  69. return null;
  70. }
  71. # If there's only one Accept header, just use it
  72. if (count($accept) === 1) {
  73. return reset($accept);
  74. }
  75. # If none of the available Accept headers is of type "json", then just use all them
  76. $headersWithJson = $this->selectJsonMimeList($accept);
  77. if (count($headersWithJson) === 0) {
  78. return implode(',', $accept);
  79. }
  80. # If we got here, then we need add quality values (weight), as described in IETF RFC 9110, Items 12.4.2/12.5.1,
  81. # to give the highest priority to json-like headers - recalculating the existing ones, if needed
  82. return $this->getAcceptHeaderWithAdjustedWeight($accept, $headersWithJson);
  83. }
  84. /**
  85. * Detects whether a string contains a valid JSON mime type
  86. *
  87. * @param string $searchString
  88. * @return bool
  89. */
  90. public function isJsonMime(string $searchString): bool
  91. {
  92. return preg_match('~^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)~', $searchString) === 1;
  93. }
  94. /**
  95. * Select all items from a list containing a JSON mime type
  96. *
  97. * @param array $mimeList
  98. * @return array
  99. */
  100. private function selectJsonMimeList(array $mimeList): array {
  101. $jsonMimeList = [];
  102. foreach ($mimeList as $mime) {
  103. if($this->isJsonMime($mime)) {
  104. $jsonMimeList[] = $mime;
  105. }
  106. }
  107. return $jsonMimeList;
  108. }
  109. /**
  110. * Create an Accept header string from the given "Accept" headers array, recalculating all weights
  111. *
  112. * @param string[] $accept Array of Accept Headers
  113. * @param string[] $headersWithJson Array of Accept Headers of type "json"
  114. *
  115. * @return string "Accept" Header (e.g. "application/json, text/html; q=0.9")
  116. */
  117. private function getAcceptHeaderWithAdjustedWeight(array $accept, array $headersWithJson): string
  118. {
  119. $processedHeaders = [
  120. 'withApplicationJson' => [],
  121. 'withJson' => [],
  122. 'withoutJson' => [],
  123. ];
  124. foreach ($accept as $header) {
  125. $headerData = $this->getHeaderAndWeight($header);
  126. if (stripos($headerData['header'], 'application/json') === 0) {
  127. $processedHeaders['withApplicationJson'][] = $headerData;
  128. } elseif (in_array($header, $headersWithJson, true)) {
  129. $processedHeaders['withJson'][] = $headerData;
  130. } else {
  131. $processedHeaders['withoutJson'][] = $headerData;
  132. }
  133. }
  134. $acceptHeaders = [];
  135. $currentWeight = 1000;
  136. $hasMoreThan28Headers = count($accept) > 28;
  137. foreach($processedHeaders as $headers) {
  138. if (count($headers) > 0) {
  139. $acceptHeaders[] = $this->adjustWeight($headers, $currentWeight, $hasMoreThan28Headers);
  140. }
  141. }
  142. $acceptHeaders = array_merge(...$acceptHeaders);
  143. return implode(',', $acceptHeaders);
  144. }
  145. /**
  146. * Given an Accept header, returns an associative array splitting the header and its weight
  147. *
  148. * @param string $header "Accept" Header
  149. *
  150. * @return array with the header and its weight
  151. */
  152. private function getHeaderAndWeight(string $header): array
  153. {
  154. # matches headers with weight, splitting the header and the weight in $outputArray
  155. if (preg_match('/(.*);\s*q=(1(?:\.0+)?|0\.\d+)$/', $header, $outputArray) === 1) {
  156. $headerData = [
  157. 'header' => $outputArray[1],
  158. 'weight' => (int)($outputArray[2] * 1000),
  159. ];
  160. } else {
  161. $headerData = [
  162. 'header' => trim($header),
  163. 'weight' => 1000,
  164. ];
  165. }
  166. return $headerData;
  167. }
  168. /**
  169. * @param array[] $headers
  170. * @param float $currentWeight
  171. * @param bool $hasMoreThan28Headers
  172. * @return string[] array of adjusted "Accept" headers
  173. */
  174. private function adjustWeight(array $headers, float &$currentWeight, bool $hasMoreThan28Headers): array
  175. {
  176. usort($headers, function (array $a, array $b) {
  177. return $b['weight'] - $a['weight'];
  178. });
  179. $acceptHeaders = [];
  180. foreach ($headers as $index => $header) {
  181. if($index > 0 && $headers[$index - 1]['weight'] > $header['weight'])
  182. {
  183. $currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
  184. }
  185. $weight = $currentWeight;
  186. $acceptHeaders[] = $this->buildAcceptHeader($header['header'], $weight);
  187. }
  188. $currentWeight = $this->getNextWeight($currentWeight, $hasMoreThan28Headers);
  189. return $acceptHeaders;
  190. }
  191. /**
  192. * @param string $header
  193. * @param int $weight
  194. * @return string
  195. */
  196. private function buildAcceptHeader(string $header, int $weight): string
  197. {
  198. if($weight === 1000) {
  199. return $header;
  200. }
  201. return trim($header, '; ') . ';q=' . rtrim(sprintf('%0.3f', $weight / 1000), '0');
  202. }
  203. /**
  204. * Calculate the next weight, based on the current one.
  205. *
  206. * If there are less than 28 "Accept" headers, the weights will be decreased by 1 on its highest significant digit, using the
  207. * following formula:
  208. *
  209. * next weight = current weight - 10 ^ (floor(log(current weight - 1)))
  210. *
  211. * ( current weight minus ( 10 raised to the power of ( floor of (log to the base 10 of ( current weight minus 1 ) ) ) ) )
  212. *
  213. * Starting from 1000, this generates the following series:
  214. *
  215. * 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
  216. *
  217. * 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
  218. * 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
  219. * decrement rule, which will result in quality codes like "q=0.999", "q=0.998" etc.
  220. *
  221. * @param int $currentWeight varying from 1 to 1000 (will be divided by 1000 to build the quality value)
  222. * @param bool $hasMoreThan28Headers
  223. * @return int
  224. */
  225. public function getNextWeight(int $currentWeight, bool $hasMoreThan28Headers): int
  226. {
  227. if ($currentWeight <= 1) {
  228. return 1;
  229. }
  230. if ($hasMoreThan28Headers) {
  231. return $currentWeight - 1;
  232. }
  233. return $currentWeight - 10 ** floor( log10($currentWeight - 1) );
  234. }
  235. }