PluginProbe
Plausible Analytics / trunk
Plausible Analytics vtrunk
2.6.1 2.6.0 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 52 releases
plausible-analytics / src / Client / lib / HeaderSelector.php

HeaderSelector.php in Plausible Analytics trunk, at src/Client/lib/HeaderSelector.php

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