Passed
Push — main ( 99c066...305d80 )
by Dimitri
04:43
created

Response::withExpires()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
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\CookieCollection;
20
use DateTime;
21
use DateTimeInterface;
22
use DateTimeZone;
23
use GuzzleHttp\Psr7\MessageTrait;
24
use GuzzleHttp\Psr7\Stream;
25
use GuzzleHttp\Psr7\Utils;
26
use InvalidArgumentException;
27
use Psr\Http\Message\ResponseInterface;
28
use Psr\Http\Message\StreamInterface;
29
use SplFileInfo;
30
31
/**
32
 * Les réponses contiennent le texte de la réponse, l'état et les en-têtes d'une réponse HTTP.
33
 *
34
 * Il existe des packages externes tels que `fig/http-message-util` qui fournissent HTTP
35
 * constantes de code d'état. Ceux-ci peuvent être utilisés avec n'importe quelle méthode qui accepte ou
36
 * renvoie un entier de code d'état. Gardez à l'esprit que ces constantes peuvent
37
 * inclure les codes d'état qui sont maintenant autorisés, ce qui lancera un
38
 * `\InvalidArgumentException`.
39
 *
40
 * @credit CakePHP <a href="https://api.cakephp.org/4.3/class-Cake.Http.Response.html">Cake\Http\Response</a>
41
 */
42
class Response implements ResponseInterface
43
{
44
    use MessageTrait;
45
    use ResponseTrait;
46
47
    /**
48
     * @var int
49
     */
50
    public const STATUS_CODE_MIN = 100;
51
52
    /**
53
     * @var int
54
     */
55
    public const STATUS_CODE_MAX = 599;
56
57
    /**
58
     * Codes d'état HTTP autorisés et leur description par défaut.
59
     *
60
     * @var array<int, string>
61
     */
62
    protected $_statusCodes = [
63
        // 1xx: Informational
64
        100 => 'Continue',
65
        101 => 'Switching Protocols',
66
        102 => 'Processing', // http://www.iana.org/go/rfc2518
67
        103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt
68
        // 2xx: Success
69
        200 => 'OK',
70
        201 => 'Created',
71
        202 => 'Accepted',
72
        203 => 'Non-Authoritative Information', // 1.1
73
        204 => 'No Content',
74
        205 => 'Reset Content',
75
        206 => 'Partial Content',
76
        207 => 'Multi-Status', // http://www.iana.org/go/rfc4918
77
        208 => 'Already Reported', // http://www.iana.org/go/rfc5842
78
        226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt
79
        // 3xx: Redirection
80
        300 => 'Multiple Choices',
81
        301 => 'Moved Permanently',
82
        302 => 'Found', // Formerly 'Moved Temporarily'
83
        303 => 'See Other', // 1.1
84
        304 => 'Not Modified',
85
        305 => 'Use Proxy', // 1.1
86
        306 => 'Switch Proxy', // No longer used
87
        307 => 'Temporary Redirect', // 1.1
88
        308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt
89
        // 4xx: Client error
90
        400 => 'Bad Request',
91
        401 => 'Unauthorized',
92
        402 => 'Payment Required',
93
        403 => 'Forbidden',
94
        404 => 'Not Found',
95
        405 => 'Method Not Allowed',
96
        406 => 'Not Acceptable',
97
        407 => 'Proxy Authentication Required',
98
        408 => 'Request Timeout',
99
        409 => 'Conflict',
100
        410 => 'Gone',
101
        411 => 'Length Required',
102
        412 => 'Precondition Failed',
103
        413 => 'Content Too Large', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
104
        414 => 'URI Too Long', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
105
        415 => 'Unsupported Media Type',
106
        416 => 'Requested Range Not Satisfiable',
107
        417 => 'Expectation Failed',
108
        418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt
109
        // 419 (Authentication Timeout) is a non-standard status code with unknown origin
110
        421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2
111
        422 => 'Unprocessable Content', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
112
        423 => 'Locked', // http://www.iana.org/go/rfc4918
113
        424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918
114
        425 => 'Too Early', // https://datatracker.ietf.org/doc/draft-ietf-httpbis-replay/
115
        426 => 'Upgrade Required',
116
        428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
117
        429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
118
        431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
119
        444 => 'Connection Closed Without Response',
120
        451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725
121
        499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133
122
        // 5xx: Server error
123
        500 => 'Internal Server Error',
124
        501 => 'Not Implemented',
125
        502 => 'Bad Gateway',
126
        503 => 'Service Unavailable',
127
        504 => 'Gateway Timeout',
128
        505 => 'HTTP Version Not Supported',
129
        506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt
130
        507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918
131
        508 => 'Loop Detected', // http://www.iana.org/go/rfc5842
132
        510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt
133
        511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt
134
        599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599
135
    ];
136
137
    /**
138
     * Contient la clé de type pour les mappages de type mime pour les types mime connus.
139
     *
140
     * @var array<string, mixed>
141
     */
142
    protected $_mimeTypes = [
143
        'html'    => ['text/html', '*/*'],
144
        'json'    => 'application/json',
145
        'xml'     => ['application/xml', 'text/xml'],
146
        'xhtml'   => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'],
147
        'webp'    => 'image/webp',
148
        'rss'     => 'application/rss+xml',
149
        'ai'      => 'application/postscript',
150
        'bcpio'   => 'application/x-bcpio',
151
        'bin'     => 'application/octet-stream',
152
        'ccad'    => 'application/clariscad',
153
        'cdf'     => 'application/x-netcdf',
154
        'class'   => 'application/octet-stream',
155
        'cpio'    => 'application/x-cpio',
156
        'cpt'     => 'application/mac-compactpro',
157
        'csh'     => 'application/x-csh',
158
        'csv'     => ['text/csv', 'application/vnd.ms-excel'],
159
        'dcr'     => 'application/x-director',
160
        'dir'     => 'application/x-director',
161
        'dms'     => 'application/octet-stream',
162
        'doc'     => 'application/msword',
163
        'docx'    => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
164
        'drw'     => 'application/drafting',
165
        'dvi'     => 'application/x-dvi',
166
        'dwg'     => 'application/acad',
167
        'dxf'     => 'application/dxf',
168
        'dxr'     => 'application/x-director',
169
        'eot'     => 'application/vnd.ms-fontobject',
170
        'eps'     => 'application/postscript',
171
        'exe'     => 'application/octet-stream',
172
        'ez'      => 'application/andrew-inset',
173
        'flv'     => 'video/x-flv',
174
        'gtar'    => 'application/x-gtar',
175
        'gz'      => 'application/x-gzip',
176
        'bz2'     => 'application/x-bzip',
177
        '7z'      => 'application/x-7z-compressed',
178
        'hal'     => ['application/hal+xml', 'application/vnd.hal+xml'],
179
        'haljson' => ['application/hal+json', 'application/vnd.hal+json'],
180
        'halxml'  => ['application/hal+xml', 'application/vnd.hal+xml'],
181
        'hdf'     => 'application/x-hdf',
182
        'hqx'     => 'application/mac-binhex40',
183
        'ico'     => 'image/x-icon',
184
        'ips'     => 'application/x-ipscript',
185
        'ipx'     => 'application/x-ipix',
186
        'js'      => 'application/javascript',
187
        'jsonapi' => 'application/vnd.api+json',
188
        'latex'   => 'application/x-latex',
189
        'jsonld'  => 'application/ld+json',
190
        'kml'     => 'application/vnd.google-earth.kml+xml',
191
        'kmz'     => 'application/vnd.google-earth.kmz',
192
        'lha'     => 'application/octet-stream',
193
        'lsp'     => 'application/x-lisp',
194
        'lzh'     => 'application/octet-stream',
195
        'man'     => 'application/x-troff-man',
196
        'me'      => 'application/x-troff-me',
197
        'mif'     => 'application/vnd.mif',
198
        'ms'      => 'application/x-troff-ms',
199
        'nc'      => 'application/x-netcdf',
200
        'oda'     => 'application/oda',
201
        'otf'     => 'font/otf',
202
        'pdf'     => 'application/pdf',
203
        'pgn'     => 'application/x-chess-pgn',
204
        'pot'     => 'application/vnd.ms-powerpoint',
205
        'pps'     => 'application/vnd.ms-powerpoint',
206
        'ppt'     => 'application/vnd.ms-powerpoint',
207
        'pptx'    => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
208
        'ppz'     => 'application/vnd.ms-powerpoint',
209
        'pre'     => 'application/x-freelance',
210
        'prt'     => 'application/pro_eng',
211
        'ps'      => 'application/postscript',
212
        'roff'    => 'application/x-troff',
213
        'scm'     => 'application/x-lotusscreencam',
214
        'set'     => 'application/set',
215
        'sh'      => 'application/x-sh',
216
        'shar'    => 'application/x-shar',
217
        'sit'     => 'application/x-stuffit',
218
        'skd'     => 'application/x-koan',
219
        'skm'     => 'application/x-koan',
220
        'skp'     => 'application/x-koan',
221
        'skt'     => 'application/x-koan',
222
        'smi'     => 'application/smil',
223
        'smil'    => 'application/smil',
224
        'sol'     => 'application/solids',
225
        'spl'     => 'application/x-futuresplash',
226
        'src'     => 'application/x-wais-source',
227
        'step'    => 'application/STEP',
228
        'stl'     => 'application/SLA',
229
        'stp'     => 'application/STEP',
230
        'sv4cpio' => 'application/x-sv4cpio',
231
        'sv4crc'  => 'application/x-sv4crc',
232
        'svg'     => 'image/svg+xml',
233
        'svgz'    => 'image/svg+xml',
234
        'swf'     => 'application/x-shockwave-flash',
235
        't'       => 'application/x-troff',
236
        'tar'     => 'application/x-tar',
237
        'tcl'     => 'application/x-tcl',
238
        'tex'     => 'application/x-tex',
239
        'texi'    => 'application/x-texinfo',
240
        'texinfo' => 'application/x-texinfo',
241
        'tr'      => 'application/x-troff',
242
        'tsp'     => 'application/dsptype',
243
        'ttc'     => 'font/ttf',
244
        'ttf'     => 'font/ttf',
245
        'unv'     => 'application/i-deas',
246
        'ustar'   => 'application/x-ustar',
247
        'vcd'     => 'application/x-cdlink',
248
        'vda'     => 'application/vda',
249
        'xlc'     => 'application/vnd.ms-excel',
250
        'xll'     => 'application/vnd.ms-excel',
251
        'xlm'     => 'application/vnd.ms-excel',
252
        'xls'     => 'application/vnd.ms-excel',
253
        'xlsx'    => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
254
        'xlw'     => 'application/vnd.ms-excel',
255
        'zip'     => 'application/zip',
256
        'aif'     => 'audio/x-aiff',
257
        'aifc'    => 'audio/x-aiff',
258
        'aiff'    => 'audio/x-aiff',
259
        'au'      => 'audio/basic',
260
        'kar'     => 'audio/midi',
261
        'mid'     => 'audio/midi',
262
        'midi'    => 'audio/midi',
263
        'mp2'     => 'audio/mpeg',
264
        'mp3'     => 'audio/mpeg',
265
        'mpga'    => 'audio/mpeg',
266
        'ogg'     => 'audio/ogg',
267
        'oga'     => 'audio/ogg',
268
        'spx'     => 'audio/ogg',
269
        'ra'      => 'audio/x-realaudio',
270
        'ram'     => 'audio/x-pn-realaudio',
271
        'rm'      => 'audio/x-pn-realaudio',
272
        'rpm'     => 'audio/x-pn-realaudio-plugin',
273
        'snd'     => 'audio/basic',
274
        'tsi'     => 'audio/TSP-audio',
275
        'wav'     => 'audio/x-wav',
276
        'aac'     => 'audio/aac',
277
        'asc'     => 'text/plain',
278
        'c'       => 'text/plain',
279
        'cc'      => 'text/plain',
280
        'css'     => 'text/css',
281
        'etx'     => 'text/x-setext',
282
        'f'       => 'text/plain',
283
        'f90'     => 'text/plain',
284
        'h'       => 'text/plain',
285
        'hh'      => 'text/plain',
286
        'htm'     => ['text/html', '*/*'],
287
        'ics'     => 'text/calendar',
288
        'm'       => 'text/plain',
289
        'rtf'     => 'text/rtf',
290
        'rtx'     => 'text/richtext',
291
        'sgm'     => 'text/sgml',
292
        'sgml'    => 'text/sgml',
293
        'tsv'     => 'text/tab-separated-values',
294
        'tpl'     => 'text/template',
295
        'txt'     => 'text/plain',
296
        'text'    => 'text/plain',
297
        'avi'     => 'video/x-msvideo',
298
        'fli'     => 'video/x-fli',
299
        'mov'     => 'video/quicktime',
300
        'movie'   => 'video/x-sgi-movie',
301
        'mpe'     => 'video/mpeg',
302
        'mpeg'    => 'video/mpeg',
303
        'mpg'     => 'video/mpeg',
304
        'qt'      => 'video/quicktime',
305
        'viv'     => 'video/vnd.vivo',
306
        'vivo'    => 'video/vnd.vivo',
307
        'ogv'     => 'video/ogg',
308
        'webm'    => 'video/webm',
309
        'mp4'     => 'video/mp4',
310
        'm4v'     => 'video/mp4',
311
        'f4v'     => 'video/mp4',
312
        'f4p'     => 'video/mp4',
313
        'm4a'     => 'audio/mp4',
314
        'f4a'     => 'audio/mp4',
315
        'f4b'     => 'audio/mp4',
316
        'gif'     => 'image/gif',
317
        'ief'     => 'image/ief',
318
        'jpg'     => 'image/jpeg',
319
        'jpeg'    => 'image/jpeg',
320
        'jpe'     => 'image/jpeg',
321
        'pbm'     => 'image/x-portable-bitmap',
322
        'pgm'     => 'image/x-portable-graymap',
323
        'png'     => 'image/png',
324
        'pnm'     => 'image/x-portable-anymap',
325
        'ppm'     => 'image/x-portable-pixmap',
326
        'ras'     => 'image/cmu-raster',
327
        'rgb'     => 'image/x-rgb',
328
        'tif'     => 'image/tiff',
329
        'tiff'    => 'image/tiff',
330
        'xbm'     => 'image/x-xbitmap',
331
        'xpm'     => 'image/x-xpixmap',
332
        'xwd'     => 'image/x-xwindowdump',
333
        'psd'     => [
334
            'application/photoshop',
335
            'application/psd',
336
            'image/psd',
337
            'image/x-photoshop',
338
            'image/photoshop',
339
            'zz-application/zz-winassoc-psd',
340
        ],
341
        'ice'          => 'x-conference/x-cooltalk',
342
        'iges'         => 'model/iges',
343
        'igs'          => 'model/iges',
344
        'mesh'         => 'model/mesh',
345
        'msh'          => 'model/mesh',
346
        'silo'         => 'model/mesh',
347
        'vrml'         => 'model/vrml',
348
        'wrl'          => 'model/vrml',
349
        'mime'         => 'www/mime',
350
        'pdb'          => 'chemical/x-pdb',
351
        'xyz'          => 'chemical/x-pdb',
352
        'javascript'   => 'application/javascript',
353
        'form'         => 'application/x-www-form-urlencoded',
354
        'file'         => 'multipart/form-data',
355
        'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
356
        'atom'         => 'application/atom+xml',
357
        'amf'          => 'application/x-amf',
358
        'wap'          => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'],
359
        'wml'          => 'text/vnd.wap.wml',
360
        'wmlscript'    => 'text/vnd.wap.wmlscript',
361
        'wbmp'         => 'image/vnd.wap.wbmp',
362
        'woff'         => 'application/x-font-woff',
363
        'appcache'     => 'text/cache-manifest',
364
        'manifest'     => 'text/cache-manifest',
365
        'htc'          => 'text/x-component',
366
        'rdf'          => 'application/xml',
367
        'crx'          => 'application/x-chrome-extension',
368
        'oex'          => 'application/x-opera-extension',
369
        'xpi'          => 'application/x-xpinstall',
370
        'safariextz'   => 'application/octet-stream',
371
        'webapp'       => 'application/x-web-app-manifest+json',
372
        'vcf'          => 'text/x-vcard',
373
        'vtt'          => 'text/vtt',
374
        'mkv'          => 'video/x-matroska',
375
        'pkpass'       => 'application/vnd.apple.pkpass',
376
        'ajax'         => 'text/html',
377
        'bmp'          => 'image/bmp',
378
    ];
379
380
    /**
381
     * Code de statut à envoyer au client
382
     *
383
     * @var int
384
     */
385
    protected $_status = 200;
386
387
    /**
388
     * Objet de fichier pour le fichier à lire comme réponse
389
     *
390
     * @var SplFileInfo|null
391
     */
392
    protected $_file;
393
394
    /**
395
     * Gamme de fichiers. Utilisé pour demander des plages de fichiers.
396
     *
397
     * @var array<int>
398
     */
399
    protected $_fileRange = [];
400
401
    /**
402
     * Le jeu de caractères avec lequel le corps de la réponse est encodé
403
     *
404
     * @var string
405
     */
406
    protected $_charset = 'UTF-8';
407
408
    /**
409
     * Contient toutes les directives de cache qui seront converties
410
     * dans les en-têtes lors de l'envoi de la requête
411
     *
412
     * @var array
413
     */
414
    protected $_cacheDirectives = [];
415
416
    /**
417
     * Collecte de cookies à envoyer au client
418
     *
419
     * @var CookieCollection
420
     */
421
    protected $_cookies;
422
423
    /**
424
     * Phrase de raison
425
     *
426
     * @var string
427
     */
428
    protected $_reasonPhrase = 'OK';
429
430
    /**
431
     * Options du mode flux.
432
     *
433
     * @var string
434
     */
435
    protected $_streamMode = 'wb+';
436
437
    /**
438
     * Cible de flux ou objet de ressource.
439
     *
440
     * @var resource|string
441
     */
442
    protected $_streamTarget = 'php://memory';
443
444
    /**
445
     * Constructeur
446
     *
447
     * @param array<string, mixed> $options liste de paramètres pour configurer la réponse. Les valeurs possibles sont :
448
     *
449
     * - body : le texte de réponse qui doit être envoyé au client
450
     * - status : le code d'état HTTP avec lequel répondre
451
     * - type : une chaîne complète de type mime ou une extension mappée dans cette classe
452
     * - charset : le jeu de caractères pour le corps de la réponse
453
     *
454
     * @throws InvalidArgumentException
455
     */
456
    public function __construct(array $options = [])
457
    {
458 22
        $this->_streamTarget = $options['streamTarget'] ?? $this->_streamTarget;
459 22
        $this->_streamMode   = $options['streamMode'] ?? $this->_streamMode;
460
461
        if (isset($options['stream'])) {
462
            if (! $options['stream'] instanceof StreamInterface) {
463
                throw new InvalidArgumentException('Stream option must be an object that implements StreamInterface');
464
            }
465
            $this->stream = $options['stream'];
466
        } else {
467 22
            $this->_createStream();
468
        }
469
470
        if (isset($options['body'])) {
471 22
            $this->stream->write($options['body']);
472
        }
473
474
        if (isset($options['status'])) {
475 22
            $this->_setStatus($options['status']);
476
        }
477
478
        if (! isset($options['charset'])) {
479 22
            $options['charset'] = config('app.charset');
480
        }
481 22
        $this->_charset = $options['charset'];
482
483 22
        $type = 'text/html';
484
        if (isset($options['type'])) {
485 22
            $type = $this->resolveType($options['type']);
486
        }
487 22
        $this->_setContentType($type);
488
489 22
        $this->_cookies = new CookieCollection();
490
    }
491
492
    /**
493
     * Crée l'objet de flux.
494
     */
495
    protected function _createStream(): void
496
    {
497 22
        $this->stream = new Stream(Utils::tryFopen($this->_streamTarget, $this->_streamMode));
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

497
        $this->stream = new Stream(Utils::tryFopen(/** @scrutinizer ignore-type */ $this->_streamTarget, $this->_streamMode));
Loading history...
498
    }
499
500
    /**
501
     * Formate l'en-tête Content-Type en fonction du contentType et du jeu de caractères configurés
502
     * le jeu de caractères ne sera défini dans l'en-tête que si la réponse est de type texte
503
     */
504
    protected function _setContentType(string $type): void
505
    {
506
        if (in_array($this->_status, [304, 204], true)) {
507 22
            $this->_clearHeader('Content-Type');
508
509
            return;
510
        }
511
        $allowed = [
512
            'application/javascript', 'application/xml', 'application/rss+xml',
513 22
        ];
514
515 22
        $charset = false;
516
        if (
517
            $this->_charset
518
            && (
519
                str_starts_with($type, 'text/')
520
                || in_array($type, $allowed, true)
521
            )
522
        ) {
523 22
            $charset = true;
524
        }
525
526
        if ($charset && ! str_contains($type, ';')) {
527 22
            $this->_setHeader('Content-Type', "{$type}; charset={$this->_charset}");
528
        } else {
529 2
            $this->_setHeader('Content-Type', $type);
530
        }
531
    }
532
533
    /**
534
     * Effectuez une redirection vers une nouvelle URL, en deux versions : en-tête ou emplacement.
535
     *
536
     * @param string $uri  L'URI vers laquelle rediriger
537
     * @param int    $code Le type de redirection, par défaut à 302
538
     *
539
     * @throws HttpException Pour un code d'état invalide.
540
     */
541
    public function redirect(string $uri, string $method = 'auto', ?int $code = null): static
542
    {
543
        // Suppose une réponse de code d'état 302 ; remplacer si nécessaire
544
        if (empty($code)) {
545 4
            $code = StatusCode::FOUND;
546
        }
547
548
        // Environnement IIS probable ? Utilisez 'refresh' pour une meilleure compatibilité
549
        if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS')) {
550 10
            $method = 'refresh';
551
        }
552
553
        // remplace le code d'état pour HTTP/1.1 et supérieur
554
        // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get
555
        if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $this->getProtocolVersion() >= 1.1 && $method !== 'refresh') {
556 2
            $code = ($_SERVER['REQUEST_METHOD'] !== 'GET') ? StatusCode::SEE_OTHER : ($code === StatusCode::FOUND ? StatusCode::TEMPORARY_REDIRECT : $code);
557
        }
558
559
        $new = $method === 'refresh'
560
            ? $this->withHeader('Refresh', '0;url=' . $uri)
561 10
            : $this->withLocation($uri);
562
563 10
        return $new->withStatus($code);
564
    }
565
566
    /**
567
     * Renvoie une instance avec un en-tête d'emplacement mis à jour.
568
     *
569
     * Si le code d'état actuel est 200, il sera remplacé
570
     * avec 302.
571
     *
572
     * @param string $url L'emplacement vers lequel rediriger.
573
     *
574
     * @return static Une nouvelle réponse avec l'en-tête Location défini.
575
     */
576
    public function withLocation(string $url): static
577
    {
578 10
        $new = $this->withHeader('Location', $url);
579
        if ($new->_status === StatusCode::OK) {
580 10
            $new->_status = StatusCode::FOUND;
581
        }
582
583 10
        return $new;
584
    }
585
586
    /**
587
     * Définit un en-tête.
588
     *
589
     * @phpstan-param non-empty-string $header
590
     */
591
    protected function _setHeader(string $header, string $value): void
592
    {
593 22
        $normalized                     = strtolower($header);
594 22
        $this->headerNames[$normalized] = $header;
595 22
        $this->headers[$header]         = [$value];
596
    }
597
598
    /**
599
     * Effacer l'en-tête
600
     *
601
     * @phpstan-param non-empty-string $header
602
     */
603
    protected function _clearHeader(string $header): void
604
    {
605
        $normalized = strtolower($header);
606
        if (! isset($this->headerNames[$normalized])) {
607
            return;
608
        }
609
        $original = $this->headerNames[$normalized];
610
        unset($this->headerNames[$normalized], $this->headers[$original]);
611
    }
612
613
    /**
614
     * Obtient le code d'état de la réponse.
615
     *
616
     * Le code d'état est un code de résultat entier à 3 chiffres de la tentative du serveur
617
     * pour comprendre et satisfaire la demande.
618
     */
619
    public function getStatusCode(): int
620
    {
621 8
        return $this->_status;
622
    }
623
624
    /**
625
     * Renvoie une instance avec le code d'état spécifié et, éventuellement, la phrase de raison.
626
     *
627
     * Si aucune expression de raison n'est spécifiée, les implémentations PEUVENT choisir par défaut
628
     * à la RFC 7231 ou à l'expression de raison recommandée par l'IANA pour la réponse
629
     * code d'état.
630
     *
631
     * Cette méthode DOIT être mise en œuvre de manière à conserver la
632
     * immuabilité du message, et DOIT retourner une instance qui a le
633
     * état mis à jour et expression de raison.
634
     *
635
     * Si le code d'état est 304 ou 204, l'en-tête Content-Type existant
636
     * sera effacé, car ces codes de réponse n'ont pas de corps.
637
     *
638
     * Il existe des packages externes tels que `fig/http-message-util` qui fournissent HTTP
639
     * constantes de code d'état. Ceux-ci peuvent être utilisés avec n'importe quelle méthode qui accepte ou
640
     * renvoie un entier de code d'état. Cependant, gardez à l'esprit que ces constantes
641
     * peut inclure des codes d'état qui sont maintenant autorisés, ce qui lancera un
642
     * `\InvalidArgumentException`.
643
     *
644
     * @see https://tools.ietf.org/html/rfc7231#section-6
645
     * @see https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
646
     *
647
     * @param int    $code         Le code d'état entier à 3 chiffres à définir.
648
     * @param string $reasonPhrase La phrase de raison à utiliser avec le
649
     *                             code d'état fourni ; si aucun n'est fourni, les implémentations PEUVENT
650
     *                             utilisez les valeurs par défaut comme suggéré dans la spécification HTTP.
651
     *
652
     * @throws HttpException Pour les arguments de code d'état non valides.
653
     */
654
    public function withStatus($code, $reasonPhrase = ''): static
655
    {
656 14
        $new = clone $this;
657 14
        $new->_setStatus($code, $reasonPhrase);
658
659 14
        return $new;
660
    }
661
662
    /**
663
     * Modificateur pour l'état de la réponse
664
     *
665
     * @throws HttpException Pour les arguments de code d'état non valides.
666
     */
667
    protected function _setStatus(int $code, string $reasonPhrase = ''): void
668
    {
669
        if ($code < static::STATUS_CODE_MIN || $code > static::STATUS_CODE_MAX) {
670 2
            throw HttpException::invalidStatusCode($code);
671
        }
672
673
        if (! array_key_exists($code, $this->_statusCodes) && empty($reasonPhrase)) {
674 2
            throw HttPException::unkownStatusCode($code);
675
        }
676
677 14
        $this->_status = $code;
678
        if ($reasonPhrase === '' && isset($this->_statusCodes[$code])) {
679 14
            $reasonPhrase = $this->_statusCodes[$code];
680
        }
681 14
        $this->_reasonPhrase = $reasonPhrase;
682
683
        // Ces codes d'état n'ont pas de corps et ne peuvent pas avoir de types de contenu.
684
        if (in_array($code, [304, 204], true)) {
685 14
            $this->_clearHeader('Content-Type');
686
        }
687
    }
688
689
    /**
690
     * Obtient la phrase de motif de réponse associée au code d'état.
691
     *
692
     * Parce qu'une phrase de raison n'est pas un élément obligatoire dans une réponse
693
     * ligne d'état, la valeur de la phrase de raison PEUT être nulle. Implémentations MAI
694
     * choisissez de renvoyer la phrase de raison recommandée par défaut RFC 7231 (ou celles
695
     * répertorié dans le registre des codes d'état HTTP IANA) pour la réponse
696
     * code d'état.
697
     *
698
     * @see https://tools.ietf.org/html/rfc7231#section-6
699
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
700
     */
701
    public function getReasonPhrase(): string
702
    {
703 2
        return $this->_reasonPhrase;
704
    }
705
706
    /**
707
     * Définit une définition de type de contenu dans la collection.
708
     *
709
     * Ex : setTypeMap('xhtml', ['application/xhtml+xml', 'application/xhtml'])
710
     *
711
     * Ceci est nécessaire pour RequestHandlerComponent et la reconnaissance des types.
712
     *
713
     * @param string          $type     Type de contenu.
714
     * @param string|string[] $mimeType Définition du type mime.
715
     */
716
    public function setTypeMap(string $type, $mimeType): void
717
    {
718
        $this->_mimeTypes[$type] = $mimeType;
719
    }
720
721
    /**
722
     * Renvoie le type de contenu actuel.
723
     */
724
    public function getType(): string
725
    {
726
        $header = $this->getHeaderLine('Content-Type');
727
        if (str_contains($header, ';')) {
728
            return explode(';', $header)[0];
729
        }
730
731
        return $header;
732
    }
733
734
    /**
735
     * Obtenez une réponse mise à jour avec le type de contenu défini.
736
     *
737
     * Si vous tentez de définir le type sur une réponse de code d'état 304 ou 204, le
738
     * Le type de contenu ne prendra pas effet car ces codes d'état n'ont pas de types de contenu.
739
     *
740
     * @param string $contentType Soit une extension de fichier qui sera mappée à un type MIME, soit un type MIME concret.
741
     */
742
    public function withType(string $contentType): static
743
    {
744 6
        $mappedType = $this->resolveType($contentType);
745 6
        $new        = clone $this;
746 6
        $new->_setContentType($mappedType);
747
748 6
        return $new;
749
    }
750
751
    /**
752
     * Traduire et valider les types de contenu.
753
     *
754
     * @param string $contentType Type de contenu ou alias de type.
755
     *
756
     * @return string Le type de contenu résolu
757
     *
758
     * @throws InvalidArgumentException Lorsqu'un type de contenu ou un alias non valide est utilisé.
759
     */
760
    protected function resolveType(string $contentType): string
761
    {
762 6
        $mapped = $this->getMimeType($contentType);
763
        if ($mapped) {
764 2
            return is_array($mapped) ? current($mapped) : $mapped;
765
        }
766
        if (! str_contains($contentType, '/')) {
767 4
            throw new InvalidArgumentException(sprintf('"%s" is an invalid content type.', $contentType));
768
        }
769
770 4
        return $contentType;
771
    }
772
773
    /**
774
     * Renvoie la définition du type mime pour un alias
775
     *
776
     * par exemple `getMimeType('pdf'); // renvoie 'application/pdf'`
777
     *
778
     * @param string $alias l'alias du type de contenu à mapper
779
     *
780
     * @return array|false|string Type mime mappé en chaîne ou false si $alias n'est pas mappé
781
     */
782
    public function getMimeType(string $alias)
783
    {
784 6
        return $this->_mimeTypes[$alias] ?? false;
785
    }
786
787
    /**
788
     * Mappe un type de contenu vers un alias
789
     *
790
     * par exemple `mapType('application/pdf'); // renvoie 'pdf'`
791
     *
792
     * @param array|string $ctype Soit un type de contenu de chaîne à mapper, soit un tableau de types.
793
     *
794
     * @return array|string|null Alias pour les types fournis.
795
     */
796
    public function mapType($ctype)
797
    {
798
        if (is_array($ctype)) {
799
            return array_map([$this, 'mapType'], $ctype);
800
        }
801
802
        foreach ($this->_mimeTypes as $alias => $types) {
803
            if (in_array($ctype, (array) $types, true)) {
804
                return $alias;
805
            }
806
        }
807
808
        return null;
809
    }
810
811
    /**
812
     * Renvoie le jeu de caractères actuel.
813
     */
814
    public function getCharset(): string
815
    {
816
        return $this->_charset;
817
    }
818
819
    /**
820
     * Obtenez une nouvelle instance avec un jeu de caractères mis à jour.
821
     */
822
    public function withCharset(string $charset): static
823
    {
824
        $new           = clone $this;
825
        $new->_charset = $charset;
826
        $new->_setContentType($this->getType());
827
828
        return $new;
829
    }
830
831
    /**
832
     * Créez une nouvelle instance avec des en-têtes pour indiquer au client de ne pas mettre en cache la réponse
833
     */
834
    public function withDisabledCache(): static
835
    {
836
        return $this->withHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
837
            ->withHeader('Last-Modified', gmdate(DATE_RFC7231))
838
            ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
839
    }
840
841
    /**
842
     * Créez une nouvelle instance avec les en-têtes pour activer la mise en cache du client.
843
     *
844
     * @param int|string $since un temps valide depuis que le texte de la réponse n'a pas été modifié
845
     * @param int|string $time  une heure valide pour l'expiration du cache
846
     */
847
    public function withCache($since, $time = '+1 day'): static
848
    {
849
        if (! is_int($time)) {
850 2
            $time = strtotime($time);
851
            if ($time === false) {
852
                throw new InvalidArgumentException(
853
                    'Invalid time parameter. Ensure your time value can be parsed by strtotime'
854 2
                );
855
            }
856
        }
857
858
        return $this
859
            ->withModified($since)
860
            ->withExpires($time)
861
            ->withSharable(true)
862
            ->withMaxAge($time - time())
863 2
            ->withHeader('Date', gmdate(DATE_RFC7231, time()));
864
    }
865
866
    /**
867
     * Créez une nouvelle instance avec le jeu de directives public/privé Cache-Control.
868
     *
869
     * @param bool     $public Si défini sur true, l'en-tête Cache-Control sera défini comme public
870
     *                         si défini sur false, la réponse sera définie sur privé.
871
     * @param int|null $time   temps en secondes après lequel la réponse ne doit plus être considérée comme fraîche.
872
     */
873
    public function withSharable(bool $public, ?int $time = null): static
874
    {
875 2
        $new = clone $this;
876 2
        unset($new->_cacheDirectives['private'], $new->_cacheDirectives['public']);
877
878 2
        $key                         = $public ? 'public' : 'private';
879 2
        $new->_cacheDirectives[$key] = true;
880
881
        if ($time !== null) {
882 2
            $new->_cacheDirectives['max-age'] = $time;
883
        }
884 2
        $new->_setCacheControl();
885
886 2
        return $new;
887
    }
888
889
    /**
890
     * Créez une nouvelle instance avec la directive Cache-Control s-maxage.
891
     *
892
     * Le max-age est le nombre de secondes après lesquelles la réponse ne doit plus être prise en compte
893
     * un bon candidat pour être extrait d'un cache partagé (comme dans un serveur proxy).
894
     *
895
     * @param int $seconds Le nombre de secondes pour max-age partagé
896
     */
897
    public function withSharedMaxAge(int $seconds): static
898
    {
899
        $new                               = clone $this;
900
        $new->_cacheDirectives['s-maxage'] = $seconds;
901
        $new->_setCacheControl();
902
903
        return $new;
904
    }
905
906
    /**
907
     * Créez une instance avec l'ensemble de directives Cache-Control max-age.
908
     *
909
     * Le max-age est le nombre de secondes après lesquelles la réponse ne doit plus être prise en compte
910
     * un bon candidat à récupérer dans le cache local (client).
911
     *
912
     * @param int $seconds Les secondes pendant lesquelles une réponse mise en cache peut être considérée comme valide
913
     */
914
    public function withMaxAge(int $seconds): static
915
    {
916 2
        $new                              = clone $this;
917 2
        $new->_cacheDirectives['max-age'] = $seconds;
918 2
        $new->_setCacheControl();
919
920 2
        return $new;
921
    }
922
923
    /**
924
     * Créez une instance avec le jeu de directives must-revalidate de Cache-Control.
925
     *
926
     * Définit la directive Cache-Control must-revalidate.
927
     * must-revalidate indique que la réponse ne doit pas être servie
928
     * obsolète par un cache en toutes circonstances sans revalidation préalable
929
     * avec l'origine.
930
     *
931
     * @param bool $enable active ou désactive la directive.
932
     */
933
    public function withMustRevalidate(bool $enable): static
934
    {
935
        $new = clone $this;
936
        if ($enable) {
937
            $new->_cacheDirectives['must-revalidate'] = true;
938
        } else {
939
            unset($new->_cacheDirectives['must-revalidate']);
940
        }
941
        $new->_setCacheControl();
942
943
        return $new;
944
    }
945
946
    /**
947
     * Méthode d'assistance pour générer un en-tête Cache-Control valide à partir du jeu d'options
948
     * dans d'autres méthodes
949
     */
950
    protected function _setCacheControl(): void
951
    {
952 2
        $control = '';
953
954
        foreach ($this->_cacheDirectives as $key => $val) {
955 2
            $control .= $val === true ? $key : sprintf('%s=%s', $key, $val);
956 2
            $control .= ', ';
957
        }
958 2
        $control = rtrim($control, ', ');
959 2
        $this->_setHeader('Cache-Control', $control);
960
    }
961
962
    /**
963
     * Créez une nouvelle instance avec l'ensemble d'en-tête Expires.
964
     *
965
     * ### Exemples:
966
     *
967
     * ```
968
     * // Va expirer le cache de réponse maintenant
969
     * $response->withExpires('maintenant')
970
     *
971
     * // Définira l'expiration dans les prochaines 24 heures
972
     * $response->withExpires(new DateTime('+1 jour'))
973
     * ```
974
     *
975
     * @param DateTimeInterface|int|string|null $time Chaîne d'heure valide ou instance de \DateTime.
976
     */
977
    public function withExpires($time): static
978
    {
979 2
        $date = $this->_getUTCDate($time);
980
981 2
        return $this->withHeader('Expires', $date->format(DATE_RFC7231));
982
    }
983
984
    /**
985
     * Créez une nouvelle instance avec le jeu d'en-tête Last-Modified.
986
     *
987
     * ### Exemples:
988
     *
989
     * ```
990
     * // Va expirer le cache de réponse maintenant
991
     * $response->withModified('now')
992
     *
993
     * // Définira l'expiration dans les prochaines 24 heures
994
     * $response->withModified(new DateTime('+1 jour'))
995
     * ```
996
     *
997
     * @param DateTimeInterface|int|string $time Chaîne d'heure valide ou instance de \DateTime.
998
     */
999
    public function withModified($time): static
1000
    {
1001 2
        $date = $this->_getUTCDate($time);
1002
1003 2
        return $this->withHeader('Last-Modified', $date->format(DATE_RFC7231));
1004
    }
1005
1006
    /**
1007
     * Définit la réponse comme non modifiée en supprimant tout contenu du corps
1008
     * définir le code d'état sur "304 Non modifié" et supprimer tous
1009
     * en-têtes contradictoires
1010
     *
1011
     * *Avertissement* Cette méthode modifie la réponse sur place et doit être évitée.
1012
     */
1013
    public function notModified(): void
1014
    {
1015
        $this->_createStream();
1016
        $this->_setStatus(StatusCode::NOT_MODIFIED);
1017
1018
        $remove = [
1019
            'Allow',
1020
            'Content-Encoding',
1021
            'Content-Language',
1022
            'Content-Length',
1023
            'Content-MD5',
1024
            'Content-Type',
1025
            'Last-Modified',
1026
        ];
1027
1028
        foreach ($remove as $header) {
1029
            $this->_clearHeader($header);
1030
        }
1031
    }
1032
1033
    /**
1034
     * Créer une nouvelle instance comme "non modifiée"
1035
     *
1036
     * Cela supprimera tout contenu du corps défini le code d'état
1037
     * à "304" et en supprimant les en-têtes qui décrivent
1038
     * un corps de réponse.
1039
     */
1040
    public function withNotModified(): static
1041
    {
1042
        $new = $this->withStatus(StatusCode::NOT_MODIFIED);
1043
        $new->_createStream();
1044
        $remove = [
1045
            'Allow',
1046
            'Content-Encoding',
1047
            'Content-Language',
1048
            'Content-Length',
1049
            'Content-MD5',
1050
            'Content-Type',
1051
            'Last-Modified',
1052
        ];
1053
1054
        foreach ($remove as $header) {
1055
            $new = $new->withoutHeader($header);
1056
        }
1057
1058
        return $new;
1059
    }
1060
1061
    /**
1062
     * Créez une nouvelle instance avec l'ensemble d'en-tête Vary.
1063
     *
1064
     * Si un tableau est passé, les valeurs seront implosées dans une virgule
1065
     * chaîne séparée. Si aucun paramètre n'est passé, alors un
1066
     * le tableau avec la valeur actuelle de l'en-tête Vary est renvoyé
1067
     *
1068
     * @param string|string[] $cacheVariances Une seule chaîne Vary ou un tableau contenant la liste des écarts.
1069
     */
1070
    public function withVary($cacheVariances): static
1071
    {
1072
        return $this->withHeader('Vary', (array) $cacheVariances);
1073
    }
1074
1075
    /**
1076
     * Créez une nouvelle instance avec l'ensemble d'en-tête Etag.
1077
     *
1078
     * Les Etags sont une indication forte qu'une réponse peut être mise en cache par un
1079
     * Client HTTP. Une mauvaise façon de générer des Etags est de créer un hachage de
1080
     * la sortie de la réponse, génère à la place un hachage unique du
1081
     * composants uniques qui identifient une demande, comme un
1082
     * l'heure de modification, un identifiant de ressource et tout ce que vous considérez
1083
     * qui rend la réponse unique.
1084
     *
1085
     * Le deuxième paramètre est utilisé pour informer les clients que le contenu a
1086
     * modifié, mais sémantiquement, il est équivalent aux valeurs mises en cache existantes. Envisager
1087
     * une page avec un compteur de visites, deux pages vues différentes sont équivalentes, mais
1088
     * ils diffèrent de quelques octets. Cela permet au client de décider s'il doit
1089
     * utiliser les données mises en cache.
1090
     *
1091
     * @param string $hash Le hachage unique qui identifie cette réponse
1092
     * @param bool   $weak Si la réponse est sémantiquement la même que
1093
     *                     autre avec le même hash ou non. La valeur par défaut est false
1094
     */
1095
    public function withEtag(string $hash, bool $weak = false): static
1096
    {
1097
        $hash = sprintf('%s"%s"', $weak ? 'W/' : '', $hash);
1098
1099
        return $this->withHeader('Etag', $hash);
1100
    }
1101
1102
    /**
1103
     * Renvoie un objet DateTime initialisé au paramètre $time et utilisant UTC
1104
     * comme fuseau horaire
1105
     *
1106
     * @param DateTimeInterface|int|string|null $time Chaîne d'heure valide ou instance de \DateTimeInterface.
1107
     */
1108
    protected function _getUTCDate($time = null): DateTimeInterface
1109
    {
1110
        if ($time instanceof DateTimeInterface) {
1111
            $result = clone $time;
1112
        } elseif (is_int($time)) {
1113 2
            $result = new DateTime(date('Y-m-d H:i:s', $time));
1114
        } else {
1115 2
            $result = new DateTime($time ?? 'now');
1116
        }
1117
1118
        /** @psalm-suppress UndefinedInterfaceMethod */
1119 2
        return $result->setTimezone(new DateTimeZone('UTC'));
1120
    }
1121
1122
    /**
1123
     * Définit le bon gestionnaire de mise en mémoire tampon de sortie pour envoyer une réponse compressée.
1124
     * Les réponses seront compressé avec zlib, si l'extension est disponible.
1125
     *
1126
     * @return bool false si le client n'accepte pas les réponses compressées ou si aucun gestionnaire n'est disponible, true sinon
1127
     */
1128
    public function compress(): bool
1129
    {
1130
        $compressionEnabled = ini_get('zlib.output_compression') !== '1'
1131
            && extension_loaded('zlib')
1132
            && (str_contains((string) env('HTTP_ACCEPT_ENCODING'), 'gzip'));
1133
1134
        return $compressionEnabled && ob_start('ob_gzhandler');
1135
    }
1136
1137
    /**
1138
     * Retourne VRAI si la sortie résultante sera compressée par PHP
1139
     */
1140
    public function outputCompressed(): bool
1141
    {
1142
        return str_contains((string) env('HTTP_ACCEPT_ENCODING'), 'gzip')
1143
            && (ini_get('zlib.output_compression') === '1' || in_array('ob_gzhandler', ob_list_handlers(), true));
1144
    }
1145
1146
    /**
1147
     * Créez une nouvelle instance avec l'ensemble d'en-tête Content-Disposition.
1148
     *
1149
     * @param string $filename Le nom du fichier car le navigateur téléchargera la réponse
1150
     */
1151
    public function withDownload(string $filename): static
1152
    {
1153 2
        return $this->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
1154
    }
1155
1156
    /**
1157
     * Créez une nouvelle réponse avec l'ensemble d'en-tête Content-Length.
1158
     *
1159
     * @param int|string $bytes Nombre d'octets
1160
     */
1161
    public function withLength(int|string $bytes): static
1162
    {
1163
        return $this->withHeader('Content-Length', (string) $bytes);
1164
    }
1165
1166
    /**
1167
     * Créez une nouvelle réponse avec l'ensemble d'en-tête de lien.
1168
     *
1169
     * ### Exemples
1170
     *
1171
     * ```
1172
     * $response = $response->withAddedLink('http://example.com?page=1', ['rel' => 'prev'])
1173
     * ->withAddedLink('http://example.com?page=3', ['rel' => 'next']);
1174
     * ```
1175
     *
1176
     * Générera :
1177
     *
1178
     * ```
1179
     * Link : <http://example.com?page=1> ; rel="prev"
1180
     * Link : <http://example.com?page=3> ; rel="suivant"
1181
     * ```
1182
     *
1183
     * @param string               $url     L'URL LinkHeader.
1184
     * @param array<string, mixed> $options Les paramètres LinkHeader.
1185
     */
1186
    public function withAddedLink(string $url, array $options = []): static
1187
    {
1188 2
        $params = [];
1189
1190
        foreach ($options as $key => $option) {
1191 2
            $params[] = $key . '="' . $option . '"';
1192
        }
1193
1194 2
        $param = '';
1195
        if ($params) {
1196 2
            $param = '; ' . implode('; ', $params);
1197
        }
1198
1199 2
        return $this->withAddedHeader('Link', '<' . $url . '>' . $param);
1200
    }
1201
1202
    /**
1203
     * Vérifie si une réponse n'a pas été modifiée selon le 'If-None-Match'
1204
     * (Etags) et requête 'If-Modified-Since' (dernière modification)
1205
     * en-têtes. Si la réponse est détectée comme n'étant pas modifiée, elle
1206
     * est marqué comme tel afin que le client puisse en être informé.
1207
     *
1208
     * Pour marquer une réponse comme non modifiée, vous devez définir au moins
1209
     * l'en-tête de réponse etag Last-Modified avant d'appeler cette méthode. Autrement
1210
     * une comparaison ne sera pas possible.
1211
     *
1212
     * *Avertissement* Cette méthode modifie la réponse sur place et doit être évitée.
1213
     *
1214
     * @param ServerRequest $request Objet de requête
1215
     *
1216
     * @return bool Indique si la réponse a été marquée comme non modifiée ou non.
1217
     */
1218
    public function checkNotModified(ServerRequest $request): bool
1219
    {
1220
        $etags       = preg_split('/\s*,\s*/', $request->getHeaderLine('If-None-Match'), 0, PREG_SPLIT_NO_EMPTY);
1221
        $responseTag = $this->getHeaderLine('Etag');
1222
        $etagMatches = null;
1223
        if ($responseTag) {
1224
            $etagMatches = in_array('*', $etags, true) || in_array($responseTag, $etags, true);
1225
        }
1226
1227
        $modifiedSince = $request->getHeaderLine('If-Modified-Since');
1228
        $timeMatches   = null;
1229
        if ($modifiedSince && $this->hasHeader('Last-Modified')) {
1230
            $timeMatches = strtotime($this->getHeaderLine('Last-Modified')) === strtotime($modifiedSince);
1231
        }
1232
        if ($etagMatches === null && $timeMatches === null) {
1233
            return false;
1234
        }
1235
        $notModified = $etagMatches !== false && $timeMatches !== false;
1236
        if ($notModified) {
1237
            $this->notModified();
1238
        }
1239
1240
        return $notModified;
1241
    }
1242
1243
    /**
1244
     * Conversion de chaînes. Récupère le corps de la réponse sous forme de chaîne.
1245
     * N'envoie *pas* d'en-têtes.
1246
     * Si body est un appelable, une chaîne vide est renvoyée.
1247
     */
1248
    public function __toString(): string
1249
    {
1250
        $this->stream->rewind();
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

1250
        $this->stream->/** @scrutinizer ignore-call */ 
1251
                       rewind();

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.

Loading history...
1251
1252
        return $this->stream->getContents();
1253
    }
1254
1255
    /**
1256
     * Créez une nouvelle réponse avec un jeu de cookies.
1257
     *
1258
     * ### Exemple
1259
     *
1260
     * ```
1261
     * // ajouter un objet cookie
1262
     * $response = $response->withCookie(new Cookie('remember_me', 1));
1263
     */
1264
    public function withCookie(CookieInterface $cookie): static
1265
    {
1266 4
        $new           = clone $this;
1267 4
        $new->_cookies = $new->_cookies->add($cookie);
1268
1269 4
        return $new;
1270
    }
1271
1272
    /**
1273
     * Créez une nouvelle réponse avec un jeu de cookies expiré.
1274
     *
1275
     * ### Exemple
1276
     *
1277
     * ```
1278
     * // ajouter un objet cookie
1279
     * $response = $response->withExpiredCookie(new Cookie('remember_me'));
1280
     */
1281
    public function withExpiredCookie(CookieInterface $cookie): static
1282
    {
1283
        $cookie = $cookie->withExpired();
1284
1285
        $new           = clone $this;
1286
        $new->_cookies = $new->_cookies->add($cookie);
1287
1288
        return $new;
1289
    }
1290
1291
    /**
1292
     * Expire un cookie lors de l'envoi de la réponse.
1293
     *
1294
     * @param CookieInterface|string $cookie
1295
     */
1296
    public function withoutCookie($cookie, ?string $path = null, ?string $domain = null)
1297
    {
1298
        if (is_string($cookie) && function_exists('cookie')) {
1299
            $cookie = cookie($cookie, null, -2628000, compact('path', 'domain'));
1300
        }
1301
1302
        return $this->withExpiredCookie($cookie);
0 ignored issues
show
Bug introduced by
It seems like $cookie can also be of type string; however, parameter $cookie of BlitzPHP\Http\Response::withExpiredCookie() does only seem to accept BlitzPHP\Contracts\Session\CookieInterface, 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 ignore-type  annotation

1302
        return $this->withExpiredCookie(/** @scrutinizer ignore-type */ $cookie);
Loading history...
1303
    }
1304
1305
    /**
1306
     * Lire un seul cookie à partir de la réponse.
1307
     *
1308
     * Cette méthode fournit un accès en lecture aux cookies en attente. Ce sera
1309
     * ne lit pas l'en-tête `Set-Cookie` s'il est défini.
1310
     *
1311
     * @param string $name Le nom du cookie que vous souhaitez lire.
1312
     *
1313
     * @return array|null Soit les données du cookie, soit null
1314
     */
1315
    public function getCookie(string $name): ?array
1316
    {
1317
        if (! $this->hasCookie($name)) {
1318
            return null;
1319
        }
1320
1321 2
        return $this->_cookies->get($name)->toArray();
1322
    }
1323
1324
    /**
1325
     * Vérifier si la reponse contient un cookie avec le nom donné
1326
     */
1327
    public function hasCookie(string $name, ?string $value = null): bool
1328
    {
1329
        if (! $this->_cookies->has($name)) {
1330 4
            return false;
1331
        }
1332
1333
        if ($value !== null) {
1334 2
            return $this->_cookies->get($name)->getValue() === $value;
1335
        }
1336
1337 4
        return true;
1338
    }
1339
1340
    /**
1341
     * Obtenez tous les cookies dans la réponse.
1342
     *
1343
     * Renvoie un tableau associatif de nom de cookie => données de cookie.
1344
     */
1345
    public function getCookies(): array
1346
    {
1347 2
        $out = [];
1348
        /** @var array<\BlitzPHP\Session\Cookie\Cookie> $cookies */
1349 2
        $cookies = $this->_cookies;
1350
1351
        foreach ($cookies as $cookie) {
1352 2
            $out[$cookie->getName()] = $cookie->toArray();
1353
        }
1354
1355 2
        return $out;
1356
    }
1357
1358
    /**
1359
     * Obtenez la CookieCollection à partir de la réponse
1360
     */
1361
    public function getCookieCollection(): CookieCollection
1362
    {
1363 2
        return $this->_cookies;
1364
    }
1365
1366
    /**
1367
     * Obtenez une nouvelle instance avec la collection de cookies fournie.
1368
     */
1369
    public function withCookieCollection(CookieCollection $cookieCollection): static
1370
    {
1371 2
        $new           = clone $this;
1372 2
        $new->_cookies = $cookieCollection;
1373
1374 2
        return $new;
1375
    }
1376
1377
    /**
1378
     * Créez une nouvelle instance basée sur un fichier.
1379
     *
1380
     * Cette méthode augmentera à la fois le corps et un certain nombre d'en-têtes associés.
1381
     *
1382
     * Si `$_SERVER['HTTP_RANGE']` est défini, une tranche du fichier sera
1383
     * retourné au lieu du fichier entier.
1384
     *
1385
     * ### Touches d'options
1386
     *
1387
     * - nom : autre nom de téléchargement
1388
     * - download : si `true` définit l'en-tête de téléchargement et force le fichier à
1389
     * être téléchargé plutôt qu'affiché en ligne.
1390
     *
1391
     * @param SplFileInfo|string   $file    Chemin d'accès absolu au fichier ou instance de \SplFileInfo.
1392
     * @param array<string, mixed> $options Options Voir ci-dessus.
1393
     *
1394
     * @throws LoadException
1395
     */
1396
    public function withFile(SplFileInfo|string $file, array $options = []): static
1397
    {
1398 2
        $file = is_string($file) ? $this->validateFile($file) : $file;
1399
        $options += [
1400
            'name'     => null,
1401
            'download' => null,
1402 2
        ];
1403
1404 2
        $extension = strtolower($file->getExtension());
1405 2
        $mapped    = $this->getMimeType($extension);
1406
        if ((! $extension || ! $mapped) && $options['download'] === null) {
1407 2
            $options['download'] = true;
1408
        }
1409
1410 2
        $new = clone $this;
1411
        if ($mapped) {
1412 2
            $new = $new->withType($extension);
1413
        }
1414
1415 2
        $fileSize = $file->getSize();
1416
        if ($options['download']) {
1417 2
            $agent = (string) env('HTTP_USER_AGENT');
1418
1419
            if ($agent && preg_match('%Opera([/ ])([0-9].[0-9]{1,2})%', $agent)) {
1420 2
                $contentType = 'application/octet-stream';
1421
            } elseif ($agent && preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
1422 2
                $contentType = 'application/force-download';
1423
            }
1424
1425
            if (isset($contentType)) {
1426 2
                $new = $new->withType($contentType);
1427
            }
1428 2
            $name = $options['name'] ?: $file->getFileName();
1429
            $new  = $new->withDownload($name)
1430 2
                ->withHeader('Content-Transfer-Encoding', 'binary');
1431
        }
1432
1433 2
        $new       = $new->withHeader('Accept-Ranges', 'bytes');
1434 2
        $httpRange = (string) env('HTTP_RANGE');
1435
        if ($httpRange) {
1436 2
            $new->_fileRange($file, $httpRange);
1437
        } else {
1438 2
            $new = $new->withHeader('Content-Length', (string) $fileSize);
1439
        }
1440 2
        $new->_file  = $file;
1441 2
        $new->stream = new Stream(Utils::tryFopen($file->getPathname(), 'rb'));
1442
1443 2
        return $new;
1444
    }
1445
1446
    /**
1447
     * Méthode pratique pour définir une chaîne dans le corps de la réponse
1448
     *
1449
     * @param string|null $string La chaîne à envoyer
1450
     */
1451
    public function withStringBody(?string $string): static
1452
    {
1453 2
        $new = clone $this;
1454
1455 2
        return $new->withBody(Utils::streamFor($string));
1456
    }
1457
1458
    /**
1459
     * Valider qu'un chemin de fichier est un corps de réponse valide.
1460
     *
1461
     * @throws LoadException
1462
     */
1463
    protected function validateFile(string $path): SplFileInfo
1464
    {
1465
        if (str_contains($path, '../') || str_contains($path, '..\\')) {
1466 2
            throw new LoadException('The requested file contains `..` and will not be read.');
1467
        }
1468
        if (! is_file($path)) {
1469
            $path = APP_PATH . $path;
1470
        }
1471
1472 2
        $file = new SplFileInfo($path);
1473
        if (! $file->isFile() || ! $file->isReadable()) {
1474
            if (on_dev()) {
1475
                throw new LoadException(sprintf('The requested file %s was not found or not readable', $path));
1476
            }
1477
1478
            throw new LoadException('The requested file was not found');
1479
        }
1480
1481 2
        return $file;
1482
    }
1483
1484
    /**
1485
     * Obtenir le fichier actuel s'il en existe un.
1486
     *
1487
     * @return SplFileInfo|null Le fichier à utiliser dans la réponse ou null
1488
     */
1489
    public function getFile(): ?SplFileInfo
1490
    {
1491
        return $this->_file;
1492
    }
1493
1494
    /**
1495
     * Appliquez une plage de fichiers à un fichier et définissez le décalage de fin.
1496
     *
1497
     * Si une plage non valide est demandée, un code d'état 416 sera utilisé
1498
     * dans la réponse.
1499
     *
1500
     * @param SplFileInfo $file      Le fichier sur lequel définir une plage.
1501
     * @param string      $httpRange La plage à utiliser.
1502
     */
1503
    protected function _fileRange(SplFileInfo $file, string $httpRange): void
1504
    {
1505
        $fileSize = $file->getSize();
1506
        $lastByte = $fileSize - 1;
1507
        $start    = 0;
1508
        $end      = $lastByte;
1509
1510
        preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches);
1511
        if ($matches) {
1512
            $start = $matches[1];
1513
            $end   = $matches[2] ?? '';
1514
        }
1515
1516
        if ($start === '') {
1517
            $start = $fileSize - (int) $end;
1518
            $end   = $lastByte;
1519
        }
1520
        if ($end === '') {
1521
            $end = $lastByte;
1522
        }
1523
1524
        if ($start > $end || $end > $lastByte || $start > $lastByte) {
1525
            $this->_setStatus(416);
1526
            $this->_setHeader('Content-Range', 'bytes 0-' . $lastByte . '/' . $fileSize);
1527
1528
            return;
1529
        }
1530
1531
        /** @psalm-suppress PossiblyInvalidOperand */
1532
        $this->_setHeader('Content-Length', (string) ($end - $start + 1));
1533
        $this->_setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $fileSize);
1534
        $this->_setStatus(206);
1535
        /**
1536
         * @var int $start
1537
         * @var int $end
1538
         */
1539
        $this->_fileRange = [$start, $end];
1540
    }
1541
1542
    /**
1543
     * Retourne un tableau qui peut être utilisé pour décrire l'état interne de cet objet.
1544
     *
1545
     * @return array<string, mixed>
1546
     */
1547
    public function __debugInfo(): array
1548
    {
1549
        return [
1550
            'status'          => $this->_status,
1551
            'contentType'     => $this->getType(),
1552
            'headers'         => $this->headers,
1553
            'file'            => $this->_file,
1554
            'fileRange'       => $this->_fileRange,
1555
            'cookies'         => $this->_cookies,
1556
            'cacheDirectives' => $this->_cacheDirectives,
1557
            'body'            => (string) $this->getBody(),
1558
        ];
1559
    }
1560
}
1561