Passed
Push — main ( cd5116...99c066 )
by Dimitri
12:52
created

Response::notModified()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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

1239
        $this->stream->/** @scrutinizer ignore-call */ 
1240
                       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...
1240
1241
        return $this->stream->getContents();
1242
    }
1243
1244
    /**
1245
     * Créez une nouvelle réponse avec un jeu de cookies.
1246
     *
1247
     * ### Exemple
1248
     *
1249
     * ```
1250
     * // ajouter un objet cookie
1251
     * $response = $response->withCookie(new Cookie('remember_me', 1));
1252
     */
1253
    public function withCookie(CookieInterface $cookie): static
1254
    {
1255 2
        $new           = clone $this;
1256 2
        $new->_cookies = $new->_cookies->add($cookie);
1257
1258 2
        return $new;
1259
    }
1260
1261
    /**
1262
     * Créez une nouvelle réponse avec un jeu de cookies expiré.
1263
     *
1264
     * ### Exemple
1265
     *
1266
     * ```
1267
     * // ajouter un objet cookie
1268
     * $response = $response->withExpiredCookie(new Cookie('remember_me'));
1269
     */
1270
    public function withExpiredCookie(CookieInterface $cookie): static
1271
    {
1272
        $cookie = $cookie->withExpired();
1273
1274
        $new           = clone $this;
1275
        $new->_cookies = $new->_cookies->add($cookie);
1276
1277
        return $new;
1278
    }
1279
1280
    /**
1281
     * Expire un cookie lors de l'envoi de la réponse.
1282
     *
1283
     * @param CookieInterface|string $cookie
1284
     */
1285
    public function withoutCookie($cookie, ?string $path = null, ?string $domain = null)
1286
    {
1287
        if (is_string($cookie) && function_exists('cookie')) {
1288
            $cookie = cookie($cookie, null, -2628000, compact('path', 'domain'));
1289
        }
1290
1291
        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

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