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> |
|
|
|
|
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)); |
|
|
|
|
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(); |
|
|
|
|
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]; |
|
|
|
|
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