1 | <?php |
||||||
2 | |||||||
3 | /** |
||||||
4 | * This file is part of Blitz PHP framework. |
||||||
5 | * |
||||||
6 | * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]> |
||||||
7 | * |
||||||
8 | * For the full copyright and license information, please view |
||||||
9 | * the LICENSE file that was distributed with this source code. |
||||||
10 | */ |
||||||
11 | |||||||
12 | namespace BlitzPHP\Http; |
||||||
13 | |||||||
14 | use BlitzPHP\Contracts\Http\StatusCode; |
||||||
15 | use BlitzPHP\Contracts\Session\CookieInterface; |
||||||
16 | use BlitzPHP\Exceptions\HttpException; |
||||||
17 | use BlitzPHP\Exceptions\LoadException; |
||||||
18 | use BlitzPHP\Http\Concerns\ResponseTrait; |
||||||
19 | use BlitzPHP\Session\Cookie\Cookie; |
||||||
20 | use BlitzPHP\Session\Cookie\CookieCollection; |
||||||
21 | use DateTime; |
||||||
22 | use DateTimeInterface; |
||||||
23 | use DateTimeZone; |
||||||
24 | use GuzzleHttp\Psr7\MessageTrait; |
||||||
25 | use GuzzleHttp\Psr7\Stream; |
||||||
26 | use GuzzleHttp\Psr7\Utils; |
||||||
27 | use InvalidArgumentException; |
||||||
28 | use Psr\Http\Message\ResponseInterface; |
||||||
29 | use Psr\Http\Message\StreamInterface; |
||||||
30 | use SplFileInfo; |
||||||
31 | use Stringable; |
||||||
32 | |||||||
33 | /** |
||||||
34 | * Les réponses contiennent le texte de la réponse, l'état et les en-têtes d'une réponse HTTP. |
||||||
35 | * |
||||||
36 | * Il existe des packages externes tels que `fig/http-message-util` qui fournissent HTTP |
||||||
37 | * constantes de code d'état. Ceux-ci peuvent être utilisés avec n'importe quelle méthode qui accepte ou |
||||||
38 | * renvoie un entier de code d'état. Gardez à l'esprit que ces constantes peuvent |
||||||
39 | * inclure les codes d'état qui sont maintenant autorisés, ce qui lancera un |
||||||
40 | * `\InvalidArgumentException`. |
||||||
41 | * |
||||||
42 | * @credit CakePHP <a href="https://api.cakephp.org/4.3/class-Cake.Http.Response.html">Cake\Http\Response</a> |
||||||
43 | */ |
||||||
44 | class Response implements ResponseInterface, Stringable |
||||||
45 | { |
||||||
46 | use MessageTrait; |
||||||
47 | use ResponseTrait; |
||||||
48 | |||||||
49 | /** |
||||||
50 | * @var int |
||||||
51 | */ |
||||||
52 | public const STATUS_CODE_MIN = 100; |
||||||
53 | |||||||
54 | /** |
||||||
55 | * @var int |
||||||
56 | */ |
||||||
57 | public const STATUS_CODE_MAX = 599; |
||||||
58 | |||||||
59 | /** |
||||||
60 | * Codes d'état HTTP autorisés et leur description par défaut. |
||||||
61 | * |
||||||
62 | * @var array<int, string> |
||||||
63 | */ |
||||||
64 | protected array $_statusCodes = StatusCode::VALID_CODES; |
||||||
65 | |||||||
66 | /** |
||||||
67 | * Contient la clé de type pour les mappages de type mime pour les types mime connus. |
||||||
68 | * |
||||||
69 | * @var array<string, mixed> |
||||||
70 | */ |
||||||
71 | protected array $_mimeTypes = [ |
||||||
72 | 'html' => ['text/html', '*/*'], |
||||||
73 | 'json' => 'application/json', |
||||||
74 | 'xml' => ['application/xml', 'text/xml'], |
||||||
75 | 'xhtml' => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'], |
||||||
76 | 'webp' => 'image/webp', |
||||||
77 | 'rss' => 'application/rss+xml', |
||||||
78 | 'ai' => 'application/postscript', |
||||||
79 | 'bcpio' => 'application/x-bcpio', |
||||||
80 | 'bin' => 'application/octet-stream', |
||||||
81 | 'ccad' => 'application/clariscad', |
||||||
82 | 'cdf' => 'application/x-netcdf', |
||||||
83 | 'class' => 'application/octet-stream', |
||||||
84 | 'cpio' => 'application/x-cpio', |
||||||
85 | 'cpt' => 'application/mac-compactpro', |
||||||
86 | 'csh' => 'application/x-csh', |
||||||
87 | 'csv' => ['text/csv', 'application/vnd.ms-excel'], |
||||||
88 | 'dcr' => 'application/x-director', |
||||||
89 | 'dir' => 'application/x-director', |
||||||
90 | 'dms' => 'application/octet-stream', |
||||||
91 | 'doc' => 'application/msword', |
||||||
92 | 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
||||||
93 | 'drw' => 'application/drafting', |
||||||
94 | 'dvi' => 'application/x-dvi', |
||||||
95 | 'dwg' => 'application/acad', |
||||||
96 | 'dxf' => 'application/dxf', |
||||||
97 | 'dxr' => 'application/x-director', |
||||||
98 | 'eot' => 'application/vnd.ms-fontobject', |
||||||
99 | 'eps' => 'application/postscript', |
||||||
100 | 'exe' => 'application/octet-stream', |
||||||
101 | 'ez' => 'application/andrew-inset', |
||||||
102 | 'flv' => 'video/x-flv', |
||||||
103 | 'gtar' => 'application/x-gtar', |
||||||
104 | 'gz' => 'application/x-gzip', |
||||||
105 | 'bz2' => 'application/x-bzip', |
||||||
106 | '7z' => 'application/x-7z-compressed', |
||||||
107 | 'hal' => ['application/hal+xml', 'application/vnd.hal+xml'], |
||||||
108 | 'haljson' => ['application/hal+json', 'application/vnd.hal+json'], |
||||||
109 | 'halxml' => ['application/hal+xml', 'application/vnd.hal+xml'], |
||||||
110 | 'hdf' => 'application/x-hdf', |
||||||
111 | 'hqx' => 'application/mac-binhex40', |
||||||
112 | 'ico' => 'image/x-icon', |
||||||
113 | 'ips' => 'application/x-ipscript', |
||||||
114 | 'ipx' => 'application/x-ipix', |
||||||
115 | 'js' => 'application/javascript', |
||||||
116 | 'jsonapi' => 'application/vnd.api+json', |
||||||
117 | 'latex' => 'application/x-latex', |
||||||
118 | 'jsonld' => 'application/ld+json', |
||||||
119 | 'kml' => 'application/vnd.google-earth.kml+xml', |
||||||
120 | 'kmz' => 'application/vnd.google-earth.kmz', |
||||||
121 | 'lha' => 'application/octet-stream', |
||||||
122 | 'lsp' => 'application/x-lisp', |
||||||
123 | 'lzh' => 'application/octet-stream', |
||||||
124 | 'man' => 'application/x-troff-man', |
||||||
125 | 'me' => 'application/x-troff-me', |
||||||
126 | 'mif' => 'application/vnd.mif', |
||||||
127 | 'ms' => 'application/x-troff-ms', |
||||||
128 | 'nc' => 'application/x-netcdf', |
||||||
129 | 'oda' => 'application/oda', |
||||||
130 | 'otf' => 'font/otf', |
||||||
131 | 'pdf' => 'application/pdf', |
||||||
132 | 'pgn' => 'application/x-chess-pgn', |
||||||
133 | 'pot' => 'application/vnd.ms-powerpoint', |
||||||
134 | 'pps' => 'application/vnd.ms-powerpoint', |
||||||
135 | 'ppt' => 'application/vnd.ms-powerpoint', |
||||||
136 | 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', |
||||||
137 | 'ppz' => 'application/vnd.ms-powerpoint', |
||||||
138 | 'pre' => 'application/x-freelance', |
||||||
139 | 'prt' => 'application/pro_eng', |
||||||
140 | 'ps' => 'application/postscript', |
||||||
141 | 'roff' => 'application/x-troff', |
||||||
142 | 'scm' => 'application/x-lotusscreencam', |
||||||
143 | 'set' => 'application/set', |
||||||
144 | 'sh' => 'application/x-sh', |
||||||
145 | 'shar' => 'application/x-shar', |
||||||
146 | 'sit' => 'application/x-stuffit', |
||||||
147 | 'skd' => 'application/x-koan', |
||||||
148 | 'skm' => 'application/x-koan', |
||||||
149 | 'skp' => 'application/x-koan', |
||||||
150 | 'skt' => 'application/x-koan', |
||||||
151 | 'smi' => 'application/smil', |
||||||
152 | 'smil' => 'application/smil', |
||||||
153 | 'sol' => 'application/solids', |
||||||
154 | 'spl' => 'application/x-futuresplash', |
||||||
155 | 'src' => 'application/x-wais-source', |
||||||
156 | 'step' => 'application/STEP', |
||||||
157 | 'stl' => 'application/SLA', |
||||||
158 | 'stp' => 'application/STEP', |
||||||
159 | 'sv4cpio' => 'application/x-sv4cpio', |
||||||
160 | 'sv4crc' => 'application/x-sv4crc', |
||||||
161 | 'svg' => 'image/svg+xml', |
||||||
162 | 'svgz' => 'image/svg+xml', |
||||||
163 | 'swf' => 'application/x-shockwave-flash', |
||||||
164 | 't' => 'application/x-troff', |
||||||
165 | 'tar' => 'application/x-tar', |
||||||
166 | 'tcl' => 'application/x-tcl', |
||||||
167 | 'tex' => 'application/x-tex', |
||||||
168 | 'texi' => 'application/x-texinfo', |
||||||
169 | 'texinfo' => 'application/x-texinfo', |
||||||
170 | 'tr' => 'application/x-troff', |
||||||
171 | 'tsp' => 'application/dsptype', |
||||||
172 | 'ttc' => 'font/ttf', |
||||||
173 | 'ttf' => 'font/ttf', |
||||||
174 | 'unv' => 'application/i-deas', |
||||||
175 | 'ustar' => 'application/x-ustar', |
||||||
176 | 'vcd' => 'application/x-cdlink', |
||||||
177 | 'vda' => 'application/vda', |
||||||
178 | 'xlc' => 'application/vnd.ms-excel', |
||||||
179 | 'xll' => 'application/vnd.ms-excel', |
||||||
180 | 'xlm' => 'application/vnd.ms-excel', |
||||||
181 | 'xls' => 'application/vnd.ms-excel', |
||||||
182 | 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', |
||||||
183 | 'xlw' => 'application/vnd.ms-excel', |
||||||
184 | 'zip' => 'application/zip', |
||||||
185 | 'aif' => 'audio/x-aiff', |
||||||
186 | 'aifc' => 'audio/x-aiff', |
||||||
187 | 'aiff' => 'audio/x-aiff', |
||||||
188 | 'au' => 'audio/basic', |
||||||
189 | 'kar' => 'audio/midi', |
||||||
190 | 'mid' => 'audio/midi', |
||||||
191 | 'midi' => 'audio/midi', |
||||||
192 | 'mp2' => 'audio/mpeg', |
||||||
193 | 'mp3' => 'audio/mpeg', |
||||||
194 | 'mpga' => 'audio/mpeg', |
||||||
195 | 'ogg' => 'audio/ogg', |
||||||
196 | 'oga' => 'audio/ogg', |
||||||
197 | 'spx' => 'audio/ogg', |
||||||
198 | 'ra' => 'audio/x-realaudio', |
||||||
199 | 'ram' => 'audio/x-pn-realaudio', |
||||||
200 | 'rm' => 'audio/x-pn-realaudio', |
||||||
201 | 'rpm' => 'audio/x-pn-realaudio-plugin', |
||||||
202 | 'snd' => 'audio/basic', |
||||||
203 | 'tsi' => 'audio/TSP-audio', |
||||||
204 | 'wav' => 'audio/x-wav', |
||||||
205 | 'aac' => 'audio/aac', |
||||||
206 | 'asc' => 'text/plain', |
||||||
207 | 'c' => 'text/plain', |
||||||
208 | 'cc' => 'text/plain', |
||||||
209 | 'css' => 'text/css', |
||||||
210 | 'etx' => 'text/x-setext', |
||||||
211 | 'f' => 'text/plain', |
||||||
212 | 'f90' => 'text/plain', |
||||||
213 | 'h' => 'text/plain', |
||||||
214 | 'hh' => 'text/plain', |
||||||
215 | 'htm' => ['text/html', '*/*'], |
||||||
216 | 'ics' => 'text/calendar', |
||||||
217 | 'm' => 'text/plain', |
||||||
218 | 'rtf' => 'text/rtf', |
||||||
219 | 'rtx' => 'text/richtext', |
||||||
220 | 'sgm' => 'text/sgml', |
||||||
221 | 'sgml' => 'text/sgml', |
||||||
222 | 'tsv' => 'text/tab-separated-values', |
||||||
223 | 'tpl' => 'text/template', |
||||||
224 | 'txt' => 'text/plain', |
||||||
225 | 'text' => 'text/plain', |
||||||
226 | 'avi' => 'video/x-msvideo', |
||||||
227 | 'fli' => 'video/x-fli', |
||||||
228 | 'mov' => 'video/quicktime', |
||||||
229 | 'movie' => 'video/x-sgi-movie', |
||||||
230 | 'mpe' => 'video/mpeg', |
||||||
231 | 'mpeg' => 'video/mpeg', |
||||||
232 | 'mpg' => 'video/mpeg', |
||||||
233 | 'qt' => 'video/quicktime', |
||||||
234 | 'viv' => 'video/vnd.vivo', |
||||||
235 | 'vivo' => 'video/vnd.vivo', |
||||||
236 | 'ogv' => 'video/ogg', |
||||||
237 | 'webm' => 'video/webm', |
||||||
238 | 'mp4' => 'video/mp4', |
||||||
239 | 'm4v' => 'video/mp4', |
||||||
240 | 'f4v' => 'video/mp4', |
||||||
241 | 'f4p' => 'video/mp4', |
||||||
242 | 'm4a' => 'audio/mp4', |
||||||
243 | 'f4a' => 'audio/mp4', |
||||||
244 | 'f4b' => 'audio/mp4', |
||||||
245 | 'gif' => 'image/gif', |
||||||
246 | 'ief' => 'image/ief', |
||||||
247 | 'jpg' => 'image/jpeg', |
||||||
248 | 'jpeg' => 'image/jpeg', |
||||||
249 | 'jpe' => 'image/jpeg', |
||||||
250 | 'pbm' => 'image/x-portable-bitmap', |
||||||
251 | 'pgm' => 'image/x-portable-graymap', |
||||||
252 | 'png' => 'image/png', |
||||||
253 | 'pnm' => 'image/x-portable-anymap', |
||||||
254 | 'ppm' => 'image/x-portable-pixmap', |
||||||
255 | 'ras' => 'image/cmu-raster', |
||||||
256 | 'rgb' => 'image/x-rgb', |
||||||
257 | 'tif' => 'image/tiff', |
||||||
258 | 'tiff' => 'image/tiff', |
||||||
259 | 'xbm' => 'image/x-xbitmap', |
||||||
260 | 'xpm' => 'image/x-xpixmap', |
||||||
261 | 'xwd' => 'image/x-xwindowdump', |
||||||
262 | 'psd' => [ |
||||||
263 | 'application/photoshop', |
||||||
264 | 'application/psd', |
||||||
265 | 'image/psd', |
||||||
266 | 'image/x-photoshop', |
||||||
267 | 'image/photoshop', |
||||||
268 | 'zz-application/zz-winassoc-psd', |
||||||
269 | ], |
||||||
270 | 'ice' => 'x-conference/x-cooltalk', |
||||||
271 | 'iges' => 'model/iges', |
||||||
272 | 'igs' => 'model/iges', |
||||||
273 | 'mesh' => 'model/mesh', |
||||||
274 | 'msh' => 'model/mesh', |
||||||
275 | 'silo' => 'model/mesh', |
||||||
276 | 'vrml' => 'model/vrml', |
||||||
277 | 'wrl' => 'model/vrml', |
||||||
278 | 'mime' => 'www/mime', |
||||||
279 | 'pdb' => 'chemical/x-pdb', |
||||||
280 | 'xyz' => 'chemical/x-pdb', |
||||||
281 | 'javascript' => 'application/javascript', |
||||||
282 | 'form' => 'application/x-www-form-urlencoded', |
||||||
283 | 'file' => 'multipart/form-data', |
||||||
284 | 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml', |
||||||
285 | 'atom' => 'application/atom+xml', |
||||||
286 | 'amf' => 'application/x-amf', |
||||||
287 | 'wap' => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'], |
||||||
288 | 'wml' => 'text/vnd.wap.wml', |
||||||
289 | 'wmlscript' => 'text/vnd.wap.wmlscript', |
||||||
290 | 'wbmp' => 'image/vnd.wap.wbmp', |
||||||
291 | 'woff' => 'application/x-font-woff', |
||||||
292 | 'appcache' => 'text/cache-manifest', |
||||||
293 | 'manifest' => 'text/cache-manifest', |
||||||
294 | 'htc' => 'text/x-component', |
||||||
295 | 'rdf' => 'application/xml', |
||||||
296 | 'crx' => 'application/x-chrome-extension', |
||||||
297 | 'oex' => 'application/x-opera-extension', |
||||||
298 | 'xpi' => 'application/x-xpinstall', |
||||||
299 | 'safariextz' => 'application/octet-stream', |
||||||
300 | 'webapp' => 'application/x-web-app-manifest+json', |
||||||
301 | 'vcf' => 'text/x-vcard', |
||||||
302 | 'vtt' => 'text/vtt', |
||||||
303 | 'mkv' => 'video/x-matroska', |
||||||
304 | 'pkpass' => 'application/vnd.apple.pkpass', |
||||||
305 | 'ajax' => 'text/html', |
||||||
306 | 'bmp' => 'image/bmp', |
||||||
307 | ]; |
||||||
308 | |||||||
309 | /** |
||||||
310 | * Code de statut à envoyer au client |
||||||
311 | */ |
||||||
312 | protected int $_status = StatusCode::OK; |
||||||
313 | |||||||
314 | /** |
||||||
315 | * Objet de fichier pour le fichier à lire comme réponse |
||||||
316 | * |
||||||
317 | * @var SplFileInfo|null |
||||||
318 | */ |
||||||
319 | protected $_file; |
||||||
320 | |||||||
321 | /** |
||||||
322 | * Gamme de fichiers. Utilisé pour demander des plages de fichiers. |
||||||
323 | * |
||||||
324 | * @var list<int> |
||||||
0 ignored issues
–
show
|
|||||||
325 | */ |
||||||
326 | protected array $_fileRange = []; |
||||||
327 | |||||||
328 | /** |
||||||
329 | * Le jeu de caractères avec lequel le corps de la réponse est encodé |
||||||
330 | */ |
||||||
331 | protected string $_charset = 'UTF-8'; |
||||||
332 | |||||||
333 | /** |
||||||
334 | * Contient toutes les directives de cache qui seront converties |
||||||
335 | * dans les en-têtes lors de l'envoi de la requête |
||||||
336 | */ |
||||||
337 | protected array $_cacheDirectives = []; |
||||||
338 | |||||||
339 | /** |
||||||
340 | * Collecte de cookies à envoyer au client |
||||||
341 | * |
||||||
342 | * @var CookieCollection |
||||||
343 | */ |
||||||
344 | protected $_cookies; |
||||||
345 | |||||||
346 | /** |
||||||
347 | * Phrase de raison |
||||||
348 | */ |
||||||
349 | protected string $_reasonPhrase = 'OK'; |
||||||
350 | |||||||
351 | /** |
||||||
352 | * Options du mode flux. |
||||||
353 | */ |
||||||
354 | protected string $_streamMode = 'wb+'; |
||||||
355 | |||||||
356 | /** |
||||||
357 | * Cible de flux ou objet de ressource. |
||||||
358 | * |
||||||
359 | * @var resource|string |
||||||
360 | */ |
||||||
361 | protected $_streamTarget = 'php://memory'; |
||||||
362 | |||||||
363 | /** |
||||||
364 | * Constructeur |
||||||
365 | * |
||||||
366 | * @param array<string, mixed> $options liste de paramètres pour configurer la réponse. Les valeurs possibles sont : |
||||||
367 | * |
||||||
368 | * - body : le texte de réponse qui doit être envoyé au client |
||||||
369 | * - status : le code d'état HTTP avec lequel répondre |
||||||
370 | * - type : une chaîne complète de type mime ou une extension mappée dans cette classe |
||||||
371 | * - charset : le jeu de caractères pour le corps de la réponse |
||||||
372 | * |
||||||
373 | * @throws InvalidArgumentException |
||||||
374 | */ |
||||||
375 | public function __construct(array $options = []) |
||||||
376 | { |
||||||
377 | 35 | $this->_streamTarget = $options['streamTarget'] ?? $this->_streamTarget; |
|||||
378 | 35 | $this->_streamMode = $options['streamMode'] ?? $this->_streamMode; |
|||||
379 | |||||||
380 | if (isset($options['stream'])) { |
||||||
381 | if (! $options['stream'] instanceof StreamInterface) { |
||||||
382 | throw new InvalidArgumentException('Stream option must be an object that implements StreamInterface'); |
||||||
383 | } |
||||||
384 | $this->stream = $options['stream']; |
||||||
385 | } else { |
||||||
386 | 35 | $this->_createStream(); |
|||||
387 | } |
||||||
388 | |||||||
389 | if (isset($options['body'])) { |
||||||
390 | 4 | $this->stream->write($options['body']); |
|||||
391 | } |
||||||
392 | |||||||
393 | if (isset($options['status'])) { |
||||||
394 | 2 | $this->_setStatus($options['status']); |
|||||
395 | } |
||||||
396 | |||||||
397 | if (! isset($options['charset'])) { |
||||||
398 | 35 | $options['charset'] = config('app.charset'); |
|||||
399 | } |
||||||
400 | 35 | $this->_charset = $options['charset']; |
|||||
401 | |||||||
402 | 35 | $type = 'text/html'; |
|||||
403 | if (isset($options['type'])) { |
||||||
404 | 2 | $type = $this->resolveType($options['type']); |
|||||
405 | } |
||||||
406 | 35 | $this->_setContentType($type); |
|||||
407 | |||||||
408 | 35 | $this->_cookies = new CookieCollection(); |
|||||
409 | } |
||||||
410 | |||||||
411 | /** |
||||||
412 | * Crée l'objet de flux. |
||||||
413 | */ |
||||||
414 | protected function _createStream(): void |
||||||
415 | { |
||||||
416 | 35 | $this->stream = new Stream(Utils::tryFopen($this->_streamTarget, $this->_streamMode)); |
|||||
0 ignored issues
–
show
It seems like
$this->_streamTarget can also be of type resource ; however, parameter $filename of GuzzleHttp\Psr7\Utils::tryFopen() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
417 | } |
||||||
418 | |||||||
419 | /** |
||||||
420 | * Formate l'en-tête Content-Type en fonction du contentType et du jeu de caractères configurés |
||||||
421 | * le jeu de caractères ne sera défini dans l'en-tête que si la réponse est de type texte |
||||||
422 | */ |
||||||
423 | protected function _setContentType(string $type): void |
||||||
424 | { |
||||||
425 | if (in_array($this->_status, [304, 204], true)) { |
||||||
426 | 2 | $this->_clearHeader('Content-Type'); |
|||||
427 | |||||||
428 | 2 | return; |
|||||
429 | } |
||||||
430 | $allowed = [ |
||||||
431 | 'application/javascript', 'application/xml', 'application/rss+xml', |
||||||
432 | 35 | ]; |
|||||
433 | |||||||
434 | 35 | $charset = false; |
|||||
435 | if ( |
||||||
436 | $this->_charset |
||||||
437 | && ( |
||||||
438 | str_starts_with($type, 'text/') |
||||||
439 | || in_array($type, $allowed, true) |
||||||
440 | ) |
||||||
441 | ) { |
||||||
442 | 35 | $charset = true; |
|||||
443 | } |
||||||
444 | |||||||
445 | if ($charset && ! str_contains($type, ';')) { |
||||||
446 | 35 | $this->_setHeader('Content-Type', "{$type}; charset={$this->_charset}"); |
|||||
447 | } else { |
||||||
448 | 10 | $this->_setHeader('Content-Type', $type); |
|||||
449 | } |
||||||
450 | } |
||||||
451 | |||||||
452 | /** |
||||||
453 | * Effectuez une redirection vers une nouvelle URL, en deux versions : en-tête ou emplacement. |
||||||
454 | * |
||||||
455 | * @param string $uri L'URI vers laquelle rediriger |
||||||
456 | * @param int|null $code Le type de redirection, par défaut à 302 |
||||||
457 | * |
||||||
458 | * @throws HttpException Pour un code d'état invalide. |
||||||
459 | */ |
||||||
460 | public function redirect(string $uri, string $method = 'auto', ?int $code = null): static |
||||||
461 | { |
||||||
462 | // Suppose une réponse de code d'état 302 ; remplacer si nécessaire |
||||||
463 | if ($code === null || $code === 0) { |
||||||
464 | 4 | $code = StatusCode::FOUND; |
|||||
465 | } |
||||||
466 | |||||||
467 | // Environnement IIS probable ? Utilisez 'refresh' pour une meilleure compatibilité |
||||||
468 | if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS')) { |
||||||
469 | 10 | $method = 'refresh'; |
|||||
470 | } |
||||||
471 | |||||||
472 | // remplace le code d'état pour HTTP/1.1 et supérieur |
||||||
473 | // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get |
||||||
474 | if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1 && $method !== 'refresh') { |
||||||
475 | 2 | $code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? StatusCode::SEE_OTHER : ($code === StatusCode::FOUND ? StatusCode::TEMPORARY_REDIRECT : $code); |
|||||
476 | } |
||||||
477 | |||||||
478 | $new = $method === 'refresh' |
||||||
479 | ? $this->withHeader('Refresh', '0;url=' . $uri) |
||||||
480 | 10 | : $this->withLocation($uri); |
|||||
481 | |||||||
482 | 10 | return $new->withStatus($code); |
|||||
483 | } |
||||||
484 | |||||||
485 | /** |
||||||
486 | * Renvoie une instance avec un en-tête d'emplacement mis à jour. |
||||||
487 | * |
||||||
488 | * Si le code d'état actuel est 200, il sera remplacé |
||||||
489 | * avec 302. |
||||||
490 | * |
||||||
491 | * @param string $url L'emplacement vers lequel rediriger. |
||||||
492 | * |
||||||
493 | * @return static Une nouvelle réponse avec l'en-tête Location défini. |
||||||
494 | */ |
||||||
495 | public function withLocation(string $url): static |
||||||
496 | { |
||||||
497 | 10 | $new = $this->withHeader('Location', $url); |
|||||
498 | if ($new->_status === StatusCode::OK) { |
||||||
499 | 10 | $new->_status = StatusCode::FOUND; |
|||||
500 | } |
||||||
501 | |||||||
502 | 10 | return $new; |
|||||
503 | } |
||||||
504 | |||||||
505 | /** |
||||||
506 | * Définit un en-tête. |
||||||
507 | * |
||||||
508 | * @phpstan-param non-empty-string $header |
||||||
509 | */ |
||||||
510 | protected function _setHeader(string $header, string $value): void |
||||||
511 | { |
||||||
512 | 35 | $normalized = strtolower($header); |
|||||
513 | 35 | $this->headerNames[$normalized] = $header; |
|||||
514 | 35 | $this->headers[$header] = [$value]; |
|||||
515 | } |
||||||
516 | |||||||
517 | /** |
||||||
518 | * Effacer l'en-tête |
||||||
519 | * |
||||||
520 | * @phpstan-param non-empty-string $header |
||||||
521 | */ |
||||||
522 | protected function _clearHeader(string $header): void |
||||||
523 | { |
||||||
524 | 8 | $normalized = strtolower($header); |
|||||
525 | if (! isset($this->headerNames[$normalized])) { |
||||||
526 | 2 | return; |
|||||
527 | } |
||||||
528 | 8 | $original = $this->headerNames[$normalized]; |
|||||
529 | 8 | unset($this->headerNames[$normalized], $this->headers[$original]); |
|||||
530 | } |
||||||
531 | |||||||
532 | /** |
||||||
533 | * Obtient le code d'état de la réponse. |
||||||
534 | * |
||||||
535 | * Le code d'état est un code de résultat entier à 3 chiffres de la tentative du serveur |
||||||
536 | * pour comprendre et satisfaire la demande. |
||||||
537 | */ |
||||||
538 | public function getStatusCode(): int |
||||||
539 | { |
||||||
540 | 16 | return $this->_status; |
|||||
541 | } |
||||||
542 | |||||||
543 | /** |
||||||
544 | * Renvoie une instance avec le code d'état spécifié et, éventuellement, la phrase de raison. |
||||||
545 | * |
||||||
546 | * Si aucune expression de raison n'est spécifiée, les implémentations PEUVENT choisir par défaut |
||||||
547 | * à la RFC 7231 ou à l'expression de raison recommandée par l'IANA pour la réponse |
||||||
548 | * code d'état. |
||||||
549 | * |
||||||
550 | * Cette méthode DOIT être mise en œuvre de manière à conserver la |
||||||
551 | * immuabilité du message, et DOIT retourner une instance qui a le |
||||||
552 | * état mis à jour et expression de raison. |
||||||
553 | * |
||||||
554 | * Si le code d'état est 304 ou 204, l'en-tête Content-Type existant |
||||||
555 | * sera effacé, car ces codes de réponse n'ont pas de corps. |
||||||
556 | * |
||||||
557 | * Il existe des packages externes tels que `fig/http-message-util` qui fournissent HTTP |
||||||
558 | * constantes de code d'état. Ceux-ci peuvent être utilisés avec n'importe quelle méthode qui accepte ou |
||||||
559 | * renvoie un entier de code d'état. Cependant, gardez à l'esprit que ces constantes |
||||||
560 | * peut inclure des codes d'état qui sont maintenant autorisés, ce qui lancera un |
||||||
561 | * `\InvalidArgumentException`. |
||||||
562 | * |
||||||
563 | * @see https://tools.ietf.org/html/rfc7231#section-6 |
||||||
564 | * @see https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml |
||||||
565 | * |
||||||
566 | * @param int $code Le code d'état entier à 3 chiffres à définir. |
||||||
567 | * @param string $reasonPhrase La phrase de raison à utiliser avec le |
||||||
568 | * code d'état fourni ; si aucun n'est fourni, les implémentations PEUVENT |
||||||
569 | * utilisez les valeurs par défaut comme suggéré dans la spécification HTTP. |
||||||
570 | * |
||||||
571 | * @throws HttpException Pour les arguments de code d'état non valides. |
||||||
572 | */ |
||||||
573 | public function withStatus($code, $reasonPhrase = ''): static |
||||||
574 | { |
||||||
575 | 20 | $new = clone $this; |
|||||
576 | 20 | $new->_setStatus($code, $reasonPhrase); |
|||||
577 | |||||||
578 | 20 | return $new; |
|||||
579 | } |
||||||
580 | |||||||
581 | /** |
||||||
582 | * Modificateur pour l'état de la réponse |
||||||
583 | * |
||||||
584 | * @throws HttpException Pour les arguments de code d'état non valides. |
||||||
585 | */ |
||||||
586 | protected function _setStatus(int $code, string $reasonPhrase = ''): void |
||||||
587 | { |
||||||
588 | if ($code < static::STATUS_CODE_MIN || $code > static::STATUS_CODE_MAX) { |
||||||
589 | 2 | throw HttpException::invalidStatusCode($code); |
|||||
590 | } |
||||||
591 | |||||||
592 | if (! array_key_exists($code, $this->_statusCodes) && ($reasonPhrase === '' || $reasonPhrase === '0')) { |
||||||
593 | 2 | throw HttpException::unkownStatusCode($code); |
|||||
594 | } |
||||||
595 | |||||||
596 | 22 | $this->_status = $code; |
|||||
597 | if ($reasonPhrase === '' && isset($this->_statusCodes[$code])) { |
||||||
598 | 22 | $reasonPhrase = $this->_statusCodes[$code]; |
|||||
599 | } |
||||||
600 | 22 | $this->_reasonPhrase = $reasonPhrase; |
|||||
601 | |||||||
602 | // Ces codes d'état n'ont pas de corps et ne peuvent pas avoir de types de contenu. |
||||||
603 | if (in_array($code, [304, 204], true)) { |
||||||
604 | 8 | $this->_clearHeader('Content-Type'); |
|||||
605 | } |
||||||
606 | } |
||||||
607 | |||||||
608 | /** |
||||||
609 | * Obtient la phrase de motif de réponse associée au code d'état. |
||||||
610 | * |
||||||
611 | * Parce qu'une phrase de raison n'est pas un élément obligatoire dans une réponse |
||||||
612 | * ligne d'état, la valeur de la phrase de raison PEUT être nulle. Implémentations MAI |
||||||
613 | * choisissez de renvoyer la phrase de raison recommandée par défaut RFC 7231 (ou celles |
||||||
614 | * répertorié dans le registre des codes d'état HTTP IANA) pour la réponse |
||||||
615 | * code d'état. |
||||||
616 | * |
||||||
617 | * @see https://tools.ietf.org/html/rfc7231#section-6 |
||||||
618 | * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml |
||||||
619 | */ |
||||||
620 | public function getReasonPhrase(): string |
||||||
621 | { |
||||||
622 | 2 | return $this->_reasonPhrase; |
|||||
623 | } |
||||||
624 | |||||||
625 | /** |
||||||
626 | * Définit une définition de type de contenu dans la collection. |
||||||
627 | * |
||||||
628 | * Ex : setTypeMap('xhtml', ['application/xhtml+xml', 'application/xhtml']) |
||||||
629 | * |
||||||
630 | * Ceci est nécessaire pour RequestHandlerComponent et la reconnaissance des types. |
||||||
631 | * |
||||||
632 | * @param string $type Type de contenu. |
||||||
633 | * @param list<string>|string $mimeType Définition du type mime. |
||||||
634 | */ |
||||||
635 | public function setTypeMap(string $type, $mimeType): void |
||||||
636 | { |
||||||
637 | 2 | $this->_mimeTypes[$type] = $mimeType; |
|||||
638 | } |
||||||
639 | |||||||
640 | /** |
||||||
641 | * Renvoie le type de contenu actuel. |
||||||
642 | */ |
||||||
643 | public function getType(): string |
||||||
644 | { |
||||||
645 | 8 | $header = $this->getHeaderLine('Content-Type'); |
|||||
646 | if (str_contains($header, ';')) { |
||||||
647 | 6 | return explode(';', $header)[0]; |
|||||
648 | } |
||||||
649 | |||||||
650 | 6 | return $header; |
|||||
651 | } |
||||||
652 | |||||||
653 | /** |
||||||
654 | * Obtenez une réponse mise à jour avec le type de contenu défini. |
||||||
655 | * |
||||||
656 | * Si vous tentez de définir le type sur une réponse de code d'état 304 ou 204, le |
||||||
657 | * Le type de contenu ne prendra pas effet car ces codes d'état n'ont pas de types de contenu. |
||||||
658 | * |
||||||
659 | * @param string $contentType Soit une extension de fichier qui sera mappée à un type MIME, soit un type MIME concret. |
||||||
660 | */ |
||||||
661 | public function withType(string $contentType): static |
||||||
662 | { |
||||||
663 | 10 | $mappedType = $this->resolveType($contentType); |
|||||
664 | 10 | $new = clone $this; |
|||||
665 | 10 | $new->_setContentType($mappedType); |
|||||
666 | |||||||
667 | 10 | return $new; |
|||||
668 | } |
||||||
669 | |||||||
670 | /** |
||||||
671 | * Traduire et valider les types de contenu. |
||||||
672 | * |
||||||
673 | * @param string $contentType Type de contenu ou alias de type. |
||||||
674 | * |
||||||
675 | * @return string Le type de contenu résolu |
||||||
676 | * |
||||||
677 | * @throws InvalidArgumentException Lorsqu'un type de contenu ou un alias non valide est utilisé. |
||||||
678 | */ |
||||||
679 | protected function resolveType(string $contentType): string |
||||||
680 | { |
||||||
681 | 12 | $mapped = $this->getMimeType($contentType); |
|||||
682 | if ($mapped) { |
||||||
683 | 8 | return is_array($mapped) ? current($mapped) : $mapped; |
|||||
684 | } |
||||||
685 | if (! str_contains($contentType, '/')) { |
||||||
686 | 2 | throw new InvalidArgumentException(sprintf('`%s` est un content type invalide.', $contentType)); |
|||||
687 | } |
||||||
688 | |||||||
689 | 6 | return $contentType; |
|||||
690 | } |
||||||
691 | |||||||
692 | /** |
||||||
693 | * Renvoie la définition du type mime pour un alias |
||||||
694 | * |
||||||
695 | * par exemple `getMimeType('pdf'); // renvoie 'application/pdf'` |
||||||
696 | * |
||||||
697 | * @param string $alias l'alias du type de contenu à mapper |
||||||
698 | * |
||||||
699 | * @return array|false|string Type mime mappé en chaîne ou false si $alias n'est pas mappé |
||||||
700 | */ |
||||||
701 | public function getMimeType(string $alias) |
||||||
702 | { |
||||||
703 | 12 | return $this->_mimeTypes[$alias] ?? false; |
|||||
704 | } |
||||||
705 | |||||||
706 | /** |
||||||
707 | * Mappe un type de contenu vers un alias |
||||||
708 | * |
||||||
709 | * par exemple `mapType('application/pdf'); // renvoie 'pdf'` |
||||||
710 | * |
||||||
711 | * @param array|string $ctype Soit un type de contenu de chaîne à mapper, soit un tableau de types. |
||||||
712 | * |
||||||
713 | * @return array|string|null Alias pour les types fournis. |
||||||
714 | */ |
||||||
715 | public function mapType($ctype) |
||||||
716 | { |
||||||
717 | if (is_array($ctype)) { |
||||||
718 | 2 | return array_map($this->mapType(...), $ctype); |
|||||
719 | } |
||||||
720 | |||||||
721 | foreach ($this->_mimeTypes as $alias => $types) { |
||||||
722 | if (in_array($ctype, (array) $types, true)) { |
||||||
723 | 2 | return $alias; |
|||||
724 | } |
||||||
725 | } |
||||||
726 | |||||||
727 | return null; |
||||||
728 | } |
||||||
729 | |||||||
730 | /** |
||||||
731 | * Renvoie le jeu de caractères actuel. |
||||||
732 | */ |
||||||
733 | public function getCharset(): string |
||||||
734 | { |
||||||
735 | 4 | return $this->_charset; |
|||||
736 | } |
||||||
737 | |||||||
738 | /** |
||||||
739 | * Obtenez une nouvelle instance avec un jeu de caractères mis à jour. |
||||||
740 | */ |
||||||
741 | public function withCharset(string $charset): static |
||||||
742 | { |
||||||
743 | 2 | $new = clone $this; |
|||||
744 | 2 | $new->_charset = $charset; |
|||||
745 | 2 | $new->_setContentType($this->getType()); |
|||||
746 | |||||||
747 | 2 | return $new; |
|||||
748 | } |
||||||
749 | |||||||
750 | /** |
||||||
751 | * Créez une nouvelle instance avec des en-têtes pour indiquer au client de ne pas mettre en cache la réponse |
||||||
752 | */ |
||||||
753 | public function withDisabledCache(): static |
||||||
754 | { |
||||||
755 | return $this->withHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT') |
||||||
756 | ->withHeader('Last-Modified', gmdate(DATE_RFC7231)) |
||||||
757 | 2 | ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); |
|||||
758 | } |
||||||
759 | |||||||
760 | /** |
||||||
761 | * Créez une nouvelle instance avec les en-têtes pour activer la mise en cache du client. |
||||||
762 | * |
||||||
763 | * @param int|string $since un temps valide depuis que le texte de la réponse n'a pas été modifié |
||||||
764 | * @param int|string $time une heure valide pour l'expiration du cache |
||||||
765 | */ |
||||||
766 | public function withCache($since, $time = '+1 day'): static |
||||||
767 | { |
||||||
768 | if (! is_int($time)) { |
||||||
769 | 2 | $time = strtotime($time); |
|||||
770 | if ($time === false) { |
||||||
771 | throw new InvalidArgumentException( |
||||||
772 | 'Invalid time parameter. Ensure your time value can be parsed by strtotime' |
||||||
773 | 2 | ); |
|||||
774 | } |
||||||
775 | } |
||||||
776 | |||||||
777 | return $this |
||||||
778 | ->withModified($since) |
||||||
779 | ->withExpires($time) |
||||||
780 | ->withSharable(true) |
||||||
781 | ->withMaxAge($time - time()) |
||||||
782 | 2 | ->withHeader('Date', gmdate(DATE_RFC7231, time())); |
|||||
783 | } |
||||||
784 | |||||||
785 | /** |
||||||
786 | * Créez une nouvelle instance avec le jeu de directives public/privé Cache-Control. |
||||||
787 | * |
||||||
788 | * @param bool $public Si défini sur true, l'en-tête Cache-Control sera défini comme public |
||||||
789 | * si défini sur false, la réponse sera définie sur privé. |
||||||
790 | * @param int|null $time temps en secondes après lequel la réponse ne doit plus être considérée comme fraîche. |
||||||
791 | */ |
||||||
792 | public function withSharable(bool $public, ?int $time = null): static |
||||||
793 | { |
||||||
794 | 2 | $new = clone $this; |
|||||
795 | 2 | unset($new->_cacheDirectives['private'], $new->_cacheDirectives['public']); |
|||||
796 | |||||||
797 | 2 | $key = $public ? 'public' : 'private'; |
|||||
798 | 2 | $new->_cacheDirectives[$key] = true; |
|||||
799 | |||||||
800 | if ($time !== null) { |
||||||
801 | 2 | $new->_cacheDirectives['max-age'] = $time; |
|||||
802 | } |
||||||
803 | 2 | $new->_setCacheControl(); |
|||||
804 | |||||||
805 | 2 | return $new; |
|||||
806 | } |
||||||
807 | |||||||
808 | /** |
||||||
809 | * Créez une nouvelle instance avec la directive Cache-Control s-maxage. |
||||||
810 | * |
||||||
811 | * Le max-age est le nombre de secondes après lesquelles la réponse ne doit plus être prise en compte |
||||||
812 | * un bon candidat pour être extrait d'un cache partagé (comme dans un serveur proxy). |
||||||
813 | * |
||||||
814 | * @param int $seconds Le nombre de secondes pour max-age partagé |
||||||
815 | */ |
||||||
816 | public function withSharedMaxAge(int $seconds): static |
||||||
817 | { |
||||||
818 | 2 | $new = clone $this; |
|||||
819 | 2 | $new->_cacheDirectives['s-maxage'] = $seconds; |
|||||
820 | 2 | $new->_setCacheControl(); |
|||||
821 | |||||||
822 | 2 | return $new; |
|||||
823 | } |
||||||
824 | |||||||
825 | /** |
||||||
826 | * Créez une instance avec l'ensemble de directives Cache-Control max-age. |
||||||
827 | * |
||||||
828 | * Le max-age est le nombre de secondes après lesquelles la réponse ne doit plus être prise en compte |
||||||
829 | * un bon candidat à récupérer dans le cache local (client). |
||||||
830 | * |
||||||
831 | * @param int $seconds Les secondes pendant lesquelles une réponse mise en cache peut être considérée comme valide |
||||||
832 | */ |
||||||
833 | public function withMaxAge(int $seconds): static |
||||||
834 | { |
||||||
835 | 2 | $new = clone $this; |
|||||
836 | 2 | $new->_cacheDirectives['max-age'] = $seconds; |
|||||
837 | 2 | $new->_setCacheControl(); |
|||||
838 | |||||||
839 | 2 | return $new; |
|||||
840 | } |
||||||
841 | |||||||
842 | /** |
||||||
843 | * Créez une instance avec le jeu de directives must-revalidate de Cache-Control. |
||||||
844 | * |
||||||
845 | * Définit la directive Cache-Control must-revalidate. |
||||||
846 | * must-revalidate indique que la réponse ne doit pas être servie |
||||||
847 | * obsolète par un cache en toutes circonstances sans revalidation préalable |
||||||
848 | * avec l'origine. |
||||||
849 | * |
||||||
850 | * @param bool $enable active ou désactive la directive. |
||||||
851 | */ |
||||||
852 | public function withMustRevalidate(bool $enable): static |
||||||
853 | { |
||||||
854 | 2 | $new = clone $this; |
|||||
855 | if ($enable) { |
||||||
856 | 2 | $new->_cacheDirectives['must-revalidate'] = true; |
|||||
857 | } else { |
||||||
858 | 2 | unset($new->_cacheDirectives['must-revalidate']); |
|||||
859 | } |
||||||
860 | 2 | $new->_setCacheControl(); |
|||||
861 | |||||||
862 | 2 | return $new; |
|||||
863 | } |
||||||
864 | |||||||
865 | /** |
||||||
866 | * Méthode d'assistance pour générer un en-tête Cache-Control valide à partir du jeu d'options |
||||||
867 | * dans d'autres méthodes |
||||||
868 | */ |
||||||
869 | protected function _setCacheControl(): void |
||||||
870 | { |
||||||
871 | 2 | $control = ''; |
|||||
872 | |||||||
873 | foreach ($this->_cacheDirectives as $key => $val) { |
||||||
874 | 2 | $control .= $val === true ? $key : sprintf('%s=%s', $key, $val); |
|||||
875 | 2 | $control .= ', '; |
|||||
876 | } |
||||||
877 | 2 | $control = rtrim($control, ', '); |
|||||
878 | 2 | $this->_setHeader('Cache-Control', $control); |
|||||
879 | } |
||||||
880 | |||||||
881 | /** |
||||||
882 | * Créez une nouvelle instance avec l'ensemble d'en-tête Expires. |
||||||
883 | * |
||||||
884 | * ### Exemples: |
||||||
885 | * |
||||||
886 | * ``` |
||||||
887 | * // Va expirer le cache de réponse maintenant |
||||||
888 | * $response->withExpires('maintenant') |
||||||
889 | * |
||||||
890 | * // Définira l'expiration dans les prochaines 24 heures |
||||||
891 | * $response->withExpires(new DateTime('+1 jour')) |
||||||
892 | * ``` |
||||||
893 | * |
||||||
894 | * @param DateTimeInterface|int|string|null $time Chaîne d'heure valide ou instance de \DateTime. |
||||||
895 | */ |
||||||
896 | public function withExpires($time): static |
||||||
897 | { |
||||||
898 | 2 | $date = $this->_getUTCDate($time); |
|||||
899 | |||||||
900 | 2 | return $this->withHeader('Expires', $date->format(DATE_RFC7231)); |
|||||
901 | } |
||||||
902 | |||||||
903 | /** |
||||||
904 | * Créez une nouvelle instance avec le jeu d'en-tête Last-Modified. |
||||||
905 | * |
||||||
906 | * ### Exemples: |
||||||
907 | * |
||||||
908 | * ``` |
||||||
909 | * // Va expirer le cache de réponse maintenant |
||||||
910 | * $response->withModified('now') |
||||||
911 | * |
||||||
912 | * // Définira l'expiration dans les prochaines 24 heures |
||||||
913 | * $response->withModified(new DateTime('+1 jour')) |
||||||
914 | * ``` |
||||||
915 | * |
||||||
916 | * @param DateTimeInterface|int|string $time Chaîne d'heure valide ou instance de \DateTime. |
||||||
917 | */ |
||||||
918 | public function withModified($time): static |
||||||
919 | { |
||||||
920 | 2 | $date = $this->_getUTCDate($time); |
|||||
921 | |||||||
922 | 2 | return $this->withHeader('Last-Modified', $date->format(DATE_RFC7231)); |
|||||
923 | } |
||||||
924 | |||||||
925 | /** |
||||||
926 | * Définit la réponse comme non modifiée en supprimant tout contenu du corps |
||||||
927 | * définir le code d'état sur "304 Non modifié" et supprimer tous |
||||||
928 | * en-têtes contradictoires |
||||||
929 | * |
||||||
930 | * *Avertissement* Cette méthode modifie la réponse sur place et doit être évitée. |
||||||
931 | */ |
||||||
932 | public function notModified(): void |
||||||
933 | { |
||||||
934 | $this->_createStream(); |
||||||
935 | $this->_setStatus(StatusCode::NOT_MODIFIED); |
||||||
936 | |||||||
937 | $remove = [ |
||||||
938 | 'Allow', |
||||||
939 | 'Content-Encoding', |
||||||
940 | 'Content-Language', |
||||||
941 | 'Content-Length', |
||||||
942 | 'Content-MD5', |
||||||
943 | 'Content-Type', |
||||||
944 | 'Last-Modified', |
||||||
945 | ]; |
||||||
946 | |||||||
947 | foreach ($remove as $header) { |
||||||
948 | $this->_clearHeader($header); |
||||||
949 | } |
||||||
950 | } |
||||||
951 | |||||||
952 | /** |
||||||
953 | * Créer une nouvelle instance comme "non modifiée" |
||||||
954 | * |
||||||
955 | * Cela supprimera tout contenu du corps défini le code d'état |
||||||
956 | * à "304" et en supprimant les en-têtes qui décrivent |
||||||
957 | * un corps de réponse. |
||||||
958 | */ |
||||||
959 | public function withNotModified(): static |
||||||
960 | { |
||||||
961 | 2 | $new = $this->withStatus(StatusCode::NOT_MODIFIED); |
|||||
962 | 2 | $new->_createStream(); |
|||||
963 | $remove = [ |
||||||
964 | 'Allow', |
||||||
965 | 'Content-Encoding', |
||||||
966 | 'Content-Language', |
||||||
967 | 'Content-Length', |
||||||
968 | 'Content-MD5', |
||||||
969 | 'Content-Type', |
||||||
970 | 'Last-Modified', |
||||||
971 | 2 | ]; |
|||||
972 | |||||||
973 | foreach ($remove as $header) { |
||||||
974 | 2 | $new = $new->withoutHeader($header); |
|||||
975 | } |
||||||
976 | |||||||
977 | 2 | return $new; |
|||||
978 | } |
||||||
979 | |||||||
980 | /** |
||||||
981 | * Créez une nouvelle instance avec l'ensemble d'en-tête Vary. |
||||||
982 | * |
||||||
983 | * Si un tableau est passé, les valeurs seront implosées dans une virgule |
||||||
984 | * chaîne séparée. Si aucun paramètre n'est passé, alors un |
||||||
985 | * le tableau avec la valeur actuelle de l'en-tête Vary est renvoyé |
||||||
986 | * |
||||||
987 | * @param list<string>|string $cacheVariances Une seule chaîne Vary ou un tableau contenant la liste des écarts. |
||||||
988 | */ |
||||||
989 | public function withVary($cacheVariances): static |
||||||
990 | { |
||||||
991 | 2 | return $this->withHeader('Vary', (array) $cacheVariances); |
|||||
992 | } |
||||||
993 | |||||||
994 | /** |
||||||
995 | * Créez une nouvelle instance avec l'ensemble d'en-tête Etag. |
||||||
996 | * |
||||||
997 | * Les Etags sont une indication forte qu'une réponse peut être mise en cache par un |
||||||
998 | * Client HTTP. Une mauvaise façon de générer des Etags est de créer un hachage de |
||||||
999 | * la sortie de la réponse, génère à la place un hachage unique du |
||||||
1000 | * composants uniques qui identifient une demande, comme un |
||||||
1001 | * l'heure de modification, un identifiant de ressource et tout ce que vous considérez |
||||||
1002 | * qui rend la réponse unique. |
||||||
1003 | * |
||||||
1004 | * Le deuxième paramètre est utilisé pour informer les clients que le contenu a |
||||||
1005 | * modifié, mais sémantiquement, il est équivalent aux valeurs mises en cache existantes. Envisager |
||||||
1006 | * une page avec un compteur de visites, deux pages vues différentes sont équivalentes, mais |
||||||
1007 | * ils diffèrent de quelques octets. Cela permet au client de décider s'il doit |
||||||
1008 | * utiliser les données mises en cache. |
||||||
1009 | * |
||||||
1010 | * @param string $hash Le hachage unique qui identifie cette réponse |
||||||
1011 | * @param bool $weak Si la réponse est sémantiquement la même que |
||||||
1012 | * autre avec le même hash ou non. La valeur par défaut est false |
||||||
1013 | */ |
||||||
1014 | public function withEtag(string $hash, bool $weak = false): static |
||||||
1015 | { |
||||||
1016 | 2 | $hash = sprintf('%s"%s"', $weak ? 'W/' : '', $hash); |
|||||
1017 | |||||||
1018 | 2 | return $this->withHeader('Etag', $hash); |
|||||
1019 | } |
||||||
1020 | |||||||
1021 | /** |
||||||
1022 | * Renvoie un objet DateTime initialisé au paramètre $time et utilisant UTC |
||||||
1023 | * comme fuseau horaire |
||||||
1024 | * |
||||||
1025 | * @param DateTimeInterface|int|string|null $time Chaîne d'heure valide ou instance de \DateTimeInterface. |
||||||
1026 | */ |
||||||
1027 | protected function _getUTCDate($time = null): DateTimeInterface |
||||||
1028 | { |
||||||
1029 | if ($time instanceof DateTimeInterface) { |
||||||
1030 | 2 | $result = clone $time; |
|||||
1031 | } elseif (is_int($time)) { |
||||||
1032 | 2 | $result = new DateTime(date('Y-m-d H:i:s', $time)); |
|||||
1033 | } else { |
||||||
1034 | 2 | $result = new DateTime($time ?? 'now'); |
|||||
1035 | } |
||||||
1036 | |||||||
1037 | /** @psalm-suppress UndefinedInterfaceMethod */ |
||||||
1038 | 2 | return $result->setTimezone(new DateTimeZone('UTC')); |
|||||
1039 | } |
||||||
1040 | |||||||
1041 | /** |
||||||
1042 | * Définit le bon gestionnaire de mise en mémoire tampon de sortie pour envoyer une réponse compressée. |
||||||
1043 | * Les réponses seront compressé avec zlib, si l'extension est disponible. |
||||||
1044 | * |
||||||
1045 | * @return bool false si le client n'accepte pas les réponses compressées ou si aucun gestionnaire n'est disponible, true sinon |
||||||
1046 | */ |
||||||
1047 | public function compress(): bool |
||||||
1048 | { |
||||||
1049 | $compressionEnabled = ini_get('zlib.output_compression') !== '1' |
||||||
1050 | && extension_loaded('zlib') |
||||||
1051 | 2 | && (str_contains((string) env('HTTP_ACCEPT_ENCODING'), 'gzip')); |
|||||
1052 | |||||||
1053 | 2 | return $compressionEnabled && ob_start('ob_gzhandler'); |
|||||
1054 | } |
||||||
1055 | |||||||
1056 | /** |
||||||
1057 | * Retourne VRAI si la sortie résultante sera compressée par PHP |
||||||
1058 | */ |
||||||
1059 | public function outputCompressed(): bool |
||||||
1060 | { |
||||||
1061 | return str_contains((string) env('HTTP_ACCEPT_ENCODING'), 'gzip') |
||||||
1062 | 2 | && (ini_get('zlib.output_compression') === '1' || in_array('ob_gzhandler', ob_list_handlers(), true)); |
|||||
1063 | } |
||||||
1064 | |||||||
1065 | /** |
||||||
1066 | * Créez une nouvelle instance avec l'ensemble d'en-tête Content-Disposition. |
||||||
1067 | * |
||||||
1068 | * @param string $filename Le nom du fichier car le navigateur téléchargera la réponse |
||||||
1069 | */ |
||||||
1070 | public function withDownload(string $filename): static |
||||||
1071 | { |
||||||
1072 | 2 | return $this->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); |
|||||
1073 | } |
||||||
1074 | |||||||
1075 | /** |
||||||
1076 | * Créez une nouvelle réponse avec l'ensemble d'en-tête Content-Length. |
||||||
1077 | * |
||||||
1078 | * @param int|string $bytes Nombre d'octets |
||||||
1079 | */ |
||||||
1080 | public function withLength(int|string $bytes): static |
||||||
1081 | { |
||||||
1082 | 2 | return $this->withHeader('Content-Length', (string) $bytes); |
|||||
1083 | } |
||||||
1084 | |||||||
1085 | /** |
||||||
1086 | * Créez une nouvelle réponse avec l'ensemble d'en-tête de lien. |
||||||
1087 | * |
||||||
1088 | * ### Exemples |
||||||
1089 | * |
||||||
1090 | * ``` |
||||||
1091 | * $response = $response->withAddedLink('http://example.com?page=1', ['rel' => 'prev']) |
||||||
1092 | * ->withAddedLink('http://example.com?page=3', ['rel' => 'next']); |
||||||
1093 | * ``` |
||||||
1094 | * |
||||||
1095 | * Générera : |
||||||
1096 | * |
||||||
1097 | * ``` |
||||||
1098 | * Link : <http://example.com?page=1> ; rel="prev" |
||||||
1099 | * Link : <http://example.com?page=3> ; rel="suivant" |
||||||
1100 | * ``` |
||||||
1101 | * |
||||||
1102 | * @param string $url L'URL LinkHeader. |
||||||
1103 | * @param array<string, mixed> $options Les paramètres LinkHeader. |
||||||
1104 | */ |
||||||
1105 | public function withAddedLink(string $url, array $options = []): static |
||||||
1106 | { |
||||||
1107 | 2 | $params = []; |
|||||
1108 | |||||||
1109 | foreach ($options as $key => $option) { |
||||||
1110 | 2 | $params[] = $key . '="' . $option . '"'; |
|||||
1111 | } |
||||||
1112 | |||||||
1113 | 2 | $param = ''; |
|||||
1114 | if ($params !== []) { |
||||||
1115 | 2 | $param = '; ' . implode('; ', $params); |
|||||
1116 | } |
||||||
1117 | |||||||
1118 | 2 | return $this->withAddedHeader('Link', '<' . $url . '>' . $param); |
|||||
1119 | } |
||||||
1120 | |||||||
1121 | /** |
||||||
1122 | * Vérifie si une réponse n'a pas été modifiée selon le 'If-None-Match' |
||||||
1123 | * (Etags) et requête 'If-Modified-Since' (dernière modification) |
||||||
1124 | * en-têtes. Si la réponse est détectée comme n'étant pas modifiée, elle |
||||||
1125 | * est marqué comme tel afin que le client puisse en être informé. |
||||||
1126 | * |
||||||
1127 | * Pour marquer une réponse comme non modifiée, vous devez définir au moins |
||||||
1128 | * l'en-tête de réponse etag Last-Modified avant d'appeler cette méthode. Autrement |
||||||
1129 | * une comparaison ne sera pas possible. |
||||||
1130 | * |
||||||
1131 | * *Avertissement* Cette méthode modifie la réponse sur place et doit être évitée. |
||||||
1132 | * |
||||||
1133 | * @param ServerRequest $request Objet de requête |
||||||
1134 | * |
||||||
1135 | * @return bool Indique si la réponse a été marquée comme non modifiée ou non. |
||||||
1136 | */ |
||||||
1137 | public function checkNotModified(ServerRequest $request): bool |
||||||
1138 | { |
||||||
1139 | $etags = preg_split('/\s*,\s*/', $request->getHeaderLine('If-None-Match'), 0, PREG_SPLIT_NO_EMPTY); |
||||||
1140 | $responseTag = $this->getHeaderLine('Etag'); |
||||||
1141 | $etagMatches = null; |
||||||
1142 | if ($responseTag !== '' && $responseTag !== '0') { |
||||||
1143 | $etagMatches = in_array('*', $etags, true) || in_array($responseTag, $etags, true); |
||||||
1144 | } |
||||||
1145 | |||||||
1146 | $modifiedSince = $request->getHeaderLine('If-Modified-Since'); |
||||||
1147 | $timeMatches = null; |
||||||
1148 | if ($modifiedSince && $this->hasHeader('Last-Modified')) { |
||||||
1149 | $timeMatches = strtotime($this->getHeaderLine('Last-Modified')) === strtotime($modifiedSince); |
||||||
1150 | } |
||||||
1151 | if ($etagMatches === null && $timeMatches === null) { |
||||||
1152 | return false; |
||||||
1153 | } |
||||||
1154 | $notModified = $etagMatches !== false && $timeMatches !== false; |
||||||
1155 | if ($notModified) { |
||||||
1156 | $this->notModified(); |
||||||
1157 | } |
||||||
1158 | |||||||
1159 | return $notModified; |
||||||
1160 | } |
||||||
1161 | |||||||
1162 | /** |
||||||
1163 | * Conversion de chaînes. Récupère le corps de la réponse sous forme de chaîne. |
||||||
1164 | * N'envoie *pas* d'en-têtes. |
||||||
1165 | * Si body est un appelable, une chaîne vide est renvoyée. |
||||||
1166 | */ |
||||||
1167 | public function __toString(): string |
||||||
1168 | { |
||||||
1169 | $this->stream->rewind(); |
||||||
0 ignored issues
–
show
The method
rewind() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
1170 | |||||||
1171 | return $this->stream->getContents(); |
||||||
1172 | } |
||||||
1173 | |||||||
1174 | /** |
||||||
1175 | * Créez une nouvelle réponse avec un jeu de cookies. |
||||||
1176 | * |
||||||
1177 | * ### Exemple |
||||||
1178 | * |
||||||
1179 | * ``` |
||||||
1180 | * // ajouter un objet cookie |
||||||
1181 | * $response = $response->withCookie(new Cookie('remember_me', 1)); |
||||||
1182 | */ |
||||||
1183 | public function withCookie(CookieInterface $cookie): static |
||||||
1184 | { |
||||||
1185 | 4 | $new = clone $this; |
|||||
1186 | 4 | $new->_cookies = $new->_cookies->add($cookie); |
|||||
1187 | |||||||
1188 | 4 | return $new; |
|||||
1189 | } |
||||||
1190 | |||||||
1191 | /** |
||||||
1192 | * Créez une nouvelle réponse avec un jeu de cookies expiré. |
||||||
1193 | * |
||||||
1194 | * ### Exemple |
||||||
1195 | * |
||||||
1196 | * ``` |
||||||
1197 | * // ajouter un objet cookie |
||||||
1198 | * $response = $response->withExpiredCookie(new Cookie('remember_me')); |
||||||
1199 | */ |
||||||
1200 | public function withExpiredCookie(CookieInterface $cookie): static |
||||||
1201 | { |
||||||
1202 | $cookie = $cookie->withExpired(); |
||||||
1203 | |||||||
1204 | $new = clone $this; |
||||||
1205 | $new->_cookies = $new->_cookies->add($cookie); |
||||||
1206 | |||||||
1207 | return $new; |
||||||
1208 | } |
||||||
1209 | |||||||
1210 | /** |
||||||
1211 | * Expire un cookie lors de l'envoi de la réponse. |
||||||
1212 | * |
||||||
1213 | * @param CookieInterface|string $cookie |
||||||
1214 | */ |
||||||
1215 | public function withoutCookie($cookie, ?string $path = null, ?string $domain = null) |
||||||
1216 | { |
||||||
1217 | if (is_string($cookie) && function_exists('cookie')) { |
||||||
1218 | $cookie = cookie($cookie, null, -2628000, compact('path', 'domain')); |
||||||
1219 | } |
||||||
1220 | |||||||
1221 | return $this->withExpiredCookie($cookie); |
||||||
1222 | } |
||||||
1223 | |||||||
1224 | /** |
||||||
1225 | * Lire un seul cookie à partir de la réponse. |
||||||
1226 | * |
||||||
1227 | * Cette méthode fournit un accès en lecture aux cookies en attente. Ce sera |
||||||
1228 | * ne lit pas l'en-tête `Set-Cookie` s'il est défini. |
||||||
1229 | * |
||||||
1230 | * @param string $name Le nom du cookie que vous souhaitez lire. |
||||||
1231 | * |
||||||
1232 | * @return array|null Soit les données du cookie, soit null |
||||||
1233 | */ |
||||||
1234 | public function getCookie(string $name): ?array |
||||||
1235 | { |
||||||
1236 | if (! $this->hasCookie($name)) { |
||||||
1237 | return null; |
||||||
1238 | } |
||||||
1239 | |||||||
1240 | 2 | return $this->_cookies->get($name)->toArray(); |
|||||
1241 | } |
||||||
1242 | |||||||
1243 | /** |
||||||
1244 | * Vérifier si la reponse contient un cookie avec le nom donné |
||||||
1245 | */ |
||||||
1246 | public function hasCookie(string $name, ?string $value = null): bool |
||||||
1247 | { |
||||||
1248 | if (! $this->_cookies->has($name)) { |
||||||
1249 | 4 | return false; |
|||||
1250 | } |
||||||
1251 | |||||||
1252 | if ($value !== null) { |
||||||
1253 | 2 | return $this->_cookies->get($name)->getValue() === $value; |
|||||
1254 | } |
||||||
1255 | |||||||
1256 | 4 | return true; |
|||||
1257 | } |
||||||
1258 | |||||||
1259 | /** |
||||||
1260 | * Obtenez tous les cookies dans la réponse. |
||||||
1261 | * |
||||||
1262 | * Renvoie un tableau associatif de nom de cookie => données de cookie. |
||||||
1263 | */ |
||||||
1264 | public function getCookies(): array |
||||||
1265 | { |
||||||
1266 | 2 | $out = []; |
|||||
1267 | /** @var list<Cookie> $cookies */ |
||||||
1268 | 2 | $cookies = $this->_cookies; |
|||||
1269 | |||||||
1270 | foreach ($cookies as $cookie) { |
||||||
1271 | 2 | $out[$cookie->getName()] = $cookie->toArray(); |
|||||
1272 | } |
||||||
1273 | |||||||
1274 | 2 | return $out; |
|||||
1275 | } |
||||||
1276 | |||||||
1277 | /** |
||||||
1278 | * Obtenez la CookieCollection à partir de la réponse |
||||||
1279 | */ |
||||||
1280 | public function getCookieCollection(): CookieCollection |
||||||
1281 | { |
||||||
1282 | 2 | return $this->_cookies; |
|||||
1283 | } |
||||||
1284 | |||||||
1285 | /** |
||||||
1286 | * Obtenez une nouvelle instance avec la collection de cookies fournie. |
||||||
1287 | */ |
||||||
1288 | public function withCookieCollection(CookieCollection $cookieCollection): static |
||||||
1289 | { |
||||||
1290 | 2 | $new = clone $this; |
|||||
1291 | 2 | $new->_cookies = $cookieCollection; |
|||||
1292 | |||||||
1293 | 2 | return $new; |
|||||
1294 | } |
||||||
1295 | |||||||
1296 | /** |
||||||
1297 | * Créez une nouvelle instance basée sur un fichier. |
||||||
1298 | * |
||||||
1299 | * Cette méthode augmentera à la fois le corps et un certain nombre d'en-têtes associés. |
||||||
1300 | * |
||||||
1301 | * Si `$_SERVER['HTTP_RANGE']` est défini, une tranche du fichier sera |
||||||
1302 | * retourné au lieu du fichier entier. |
||||||
1303 | * |
||||||
1304 | * ### Touches d'options |
||||||
1305 | * |
||||||
1306 | * - nom : autre nom de téléchargement |
||||||
1307 | * - download : si `true` définit l'en-tête de téléchargement et force le fichier à |
||||||
1308 | * être téléchargé plutôt qu'affiché en ligne. |
||||||
1309 | * |
||||||
1310 | * @param SplFileInfo|string $file Chemin d'accès absolu au fichier ou instance de \SplFileInfo. |
||||||
1311 | * @param array<string, mixed> $options Options Voir ci-dessus. |
||||||
1312 | * |
||||||
1313 | * @throws LoadException |
||||||
1314 | */ |
||||||
1315 | public function withFile(SplFileInfo|string $file, array $options = []): static |
||||||
1316 | { |
||||||
1317 | 2 | $file = is_string($file) ? $this->validateFile($file) : $file; |
|||||
1318 | $options += [ |
||||||
1319 | 'name' => null, |
||||||
1320 | 'download' => null, |
||||||
1321 | 2 | ]; |
|||||
1322 | |||||||
1323 | 2 | $extension = strtolower($file->getExtension()); |
|||||
1324 | 2 | $mapped = $this->getMimeType($extension); |
|||||
1325 | if ((! $extension || ! $mapped) && $options['download'] === null) { |
||||||
1326 | 2 | $options['download'] = true; |
|||||
1327 | } |
||||||
1328 | |||||||
1329 | 2 | $new = clone $this; |
|||||
1330 | if ($mapped) { |
||||||
1331 | 2 | $new = $new->withType($extension); |
|||||
1332 | } |
||||||
1333 | |||||||
1334 | 2 | $fileSize = $file->getSize(); |
|||||
1335 | if ($options['download']) { |
||||||
1336 | 2 | $agent = (string) env('HTTP_USER_AGENT'); |
|||||
1337 | |||||||
1338 | if ($agent && preg_match('%Opera([/ ])([0-9].[0-9]{1,2})%', $agent)) { |
||||||
1339 | 2 | $contentType = 'application/octet-stream'; |
|||||
1340 | } elseif ($agent && preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) { |
||||||
1341 | 2 | $contentType = 'application/force-download'; |
|||||
1342 | } |
||||||
1343 | |||||||
1344 | if (isset($contentType)) { |
||||||
1345 | 2 | $new = $new->withType($contentType); |
|||||
1346 | } |
||||||
1347 | 2 | $name = $options['name'] ?: $file->getFilename(); |
|||||
1348 | $new = $new->withDownload($name) |
||||||
1349 | 2 | ->withHeader('Content-Transfer-Encoding', 'binary'); |
|||||
1350 | } |
||||||
1351 | |||||||
1352 | 2 | $new = $new->withHeader('Accept-Ranges', 'bytes'); |
|||||
1353 | 2 | $httpRange = (string) env('HTTP_RANGE'); |
|||||
1354 | if ($httpRange !== '' && $httpRange !== '0') { |
||||||
1355 | 2 | $new->_fileRange($file, $httpRange); |
|||||
1356 | } else { |
||||||
1357 | 2 | $new = $new->withHeader('Content-Length', (string) $fileSize); |
|||||
1358 | } |
||||||
1359 | 2 | $new->_file = $file; |
|||||
1360 | 2 | $new->stream = new Stream(Utils::tryFopen($file->getPathname(), 'rb')); |
|||||
1361 | |||||||
1362 | 2 | return $new; |
|||||
1363 | } |
||||||
1364 | |||||||
1365 | /** |
||||||
1366 | * Méthode pratique pour définir une chaîne dans le corps de la réponse |
||||||
1367 | * |
||||||
1368 | * @param string|null $string La chaîne à envoyer |
||||||
1369 | */ |
||||||
1370 | public function withStringBody(?string $string): static |
||||||
1371 | { |
||||||
1372 | 2 | $new = clone $this; |
|||||
1373 | |||||||
1374 | 2 | return $new->withBody(Utils::streamFor($string)); |
|||||
1375 | } |
||||||
1376 | |||||||
1377 | /** |
||||||
1378 | * Valider qu'un chemin de fichier est un corps de réponse valide. |
||||||
1379 | * |
||||||
1380 | * @throws LoadException |
||||||
1381 | */ |
||||||
1382 | protected function validateFile(string $path): SplFileInfo |
||||||
1383 | { |
||||||
1384 | if (str_contains($path, '../') || str_contains($path, '..\\')) { |
||||||
1385 | 2 | throw new LoadException('The requested file contains `..` and will not be read.'); |
|||||
1386 | } |
||||||
1387 | if (! is_file($path)) { |
||||||
1388 | $path = APP_PATH . $path; |
||||||
1389 | } |
||||||
1390 | |||||||
1391 | 2 | $file = new SplFileInfo($path); |
|||||
1392 | if (! $file->isFile() || ! $file->isReadable()) { |
||||||
1393 | if (on_dev()) { |
||||||
1394 | throw new LoadException(sprintf('The requested file %s was not found or not readable', $path)); |
||||||
1395 | } |
||||||
1396 | |||||||
1397 | throw new LoadException('The requested file was not found'); |
||||||
1398 | } |
||||||
1399 | |||||||
1400 | 2 | return $file; |
|||||
1401 | } |
||||||
1402 | |||||||
1403 | /** |
||||||
1404 | * Obtenir le fichier actuel s'il en existe un. |
||||||
1405 | * |
||||||
1406 | * @return SplFileInfo|null Le fichier à utiliser dans la réponse ou null |
||||||
1407 | */ |
||||||
1408 | public function getFile(): ?SplFileInfo |
||||||
1409 | { |
||||||
1410 | return $this->_file; |
||||||
1411 | } |
||||||
1412 | |||||||
1413 | /** |
||||||
1414 | * Appliquez une plage de fichiers à un fichier et définissez le décalage de fin. |
||||||
1415 | * |
||||||
1416 | * Si une plage non valide est demandée, un code d'état 416 sera utilisé |
||||||
1417 | * dans la réponse. |
||||||
1418 | * |
||||||
1419 | * @param SplFileInfo $file Le fichier sur lequel définir une plage. |
||||||
1420 | * @param string $httpRange La plage à utiliser. |
||||||
1421 | */ |
||||||
1422 | protected function _fileRange(SplFileInfo $file, string $httpRange): void |
||||||
1423 | { |
||||||
1424 | $fileSize = $file->getSize(); |
||||||
1425 | $lastByte = $fileSize - 1; |
||||||
1426 | $start = 0; |
||||||
1427 | $end = $lastByte; |
||||||
1428 | |||||||
1429 | preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches); |
||||||
1430 | if ($matches !== []) { |
||||||
1431 | $start = $matches[1]; |
||||||
1432 | $end = $matches[2] ?? ''; |
||||||
1433 | } |
||||||
1434 | |||||||
1435 | if ($start === '') { |
||||||
1436 | $start = $fileSize - (int) $end; |
||||||
1437 | $end = $lastByte; |
||||||
1438 | } |
||||||
1439 | if ($end === '') { |
||||||
1440 | $end = $lastByte; |
||||||
1441 | } |
||||||
1442 | |||||||
1443 | if ($start > $end || $end > $lastByte || $start > $lastByte) { |
||||||
1444 | $this->_setStatus(416); |
||||||
1445 | $this->_setHeader('Content-Range', 'bytes 0-' . $lastByte . '/' . $fileSize); |
||||||
1446 | |||||||
1447 | return; |
||||||
1448 | } |
||||||
1449 | |||||||
1450 | /** @psalm-suppress PossiblyInvalidOperand */ |
||||||
1451 | $this->_setHeader('Content-Length', (string) ($end - $start + 1)); |
||||||
1452 | $this->_setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $fileSize); |
||||||
1453 | $this->_setStatus(206); |
||||||
1454 | /** |
||||||
1455 | * @var int $start |
||||||
1456 | * @var int $end |
||||||
1457 | */ |
||||||
1458 | $this->_fileRange = [$start, $end]; |
||||||
0 ignored issues
–
show
It seems like
array($start, $end) of type array<integer,integer> is incompatible with the declared type BlitzPHP\Http\list of property $_fileRange .
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property. Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.. ![]() |
|||||||
1459 | } |
||||||
1460 | |||||||
1461 | /** |
||||||
1462 | * Retourne un tableau qui peut être utilisé pour décrire l'état interne de cet objet. |
||||||
1463 | * |
||||||
1464 | * @return array<string, mixed> |
||||||
1465 | */ |
||||||
1466 | public function __debugInfo(): array |
||||||
1467 | { |
||||||
1468 | return [ |
||||||
1469 | 'status' => $this->_status, |
||||||
1470 | 'contentType' => $this->getType(), |
||||||
1471 | 'headers' => $this->headers, |
||||||
1472 | 'file' => $this->_file, |
||||||
1473 | 'fileRange' => $this->_fileRange, |
||||||
1474 | 'cookies' => $this->_cookies, |
||||||
1475 | 'cacheDirectives' => $this->_cacheDirectives, |
||||||
1476 | 'body' => (string) $this->getBody(), |
||||||
1477 | ]; |
||||||
1478 | } |
||||||
1479 | } |
||||||
1480 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths