Issues (536)

src/Http/Response.php (4 issues)

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Http;
13
14
use BlitzPHP\Contracts\Http\StatusCode;
15
use BlitzPHP\Contracts\Session\CookieInterface;
16
use BlitzPHP\Exceptions\HttpException;
17
use BlitzPHP\Exceptions\LoadException;
18
use BlitzPHP\Http\Concerns\ResponseTrait;
19
use BlitzPHP\Session\Cookie\Cookie;
20
use BlitzPHP\Session\Cookie\CookieCollection;
21
use DateTime;
22
use DateTimeInterface;
23
use DateTimeZone;
24
use GuzzleHttp\Psr7\MessageTrait;
25
use GuzzleHttp\Psr7\Stream;
26
use GuzzleHttp\Psr7\Utils;
27
use InvalidArgumentException;
28
use Psr\Http\Message\ResponseInterface;
29
use Psr\Http\Message\StreamInterface;
30
use SplFileInfo;
31
use Stringable;
32
33
/**
34
 * Les réponses contiennent le texte de la réponse, l'état et les en-têtes d'une réponse HTTP.
35
 *
36
 * Il existe des packages externes tels que `fig/http-message-util` qui fournissent HTTP
37
 * constantes de code d'état. Ceux-ci peuvent être utilisés avec n'importe quelle méthode qui accepte ou
38
 * renvoie un entier de code d'état. Gardez à l'esprit que ces constantes peuvent
39
 * inclure les codes d'état qui sont maintenant autorisés, ce qui lancera un
40
 * `\InvalidArgumentException`.
41
 *
42
 * @credit CakePHP <a href="https://api.cakephp.org/4.3/class-Cake.Http.Response.html">Cake\Http\Response</a>
43
 */
44
class Response implements ResponseInterface, Stringable
45
{
46
    use MessageTrait;
47
    use ResponseTrait;
48
49
    /**
50
     * @var int
51
     */
52
    public const STATUS_CODE_MIN = 100;
53
54
    /**
55
     * @var int
56
     */
57
    public const STATUS_CODE_MAX = 599;
58
59
    /**
60
     * Codes d'état HTTP autorisés et leur description par défaut.
61
     *
62
     * @var array<int, string>
63
     */
64
    protected array $_statusCodes = StatusCode::VALID_CODES;
65
66
    /**
67
     * Contient la clé de type pour les mappages de type mime pour les types mime connus.
68
     *
69
     * @var array<string, mixed>
70
     */
71
    protected array $_mimeTypes = [
72
        'html'    => ['text/html', '*/*'],
73
        'json'    => 'application/json',
74
        'xml'     => ['application/xml', 'text/xml'],
75
        'xhtml'   => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'],
76
        'webp'    => 'image/webp',
77
        'rss'     => 'application/rss+xml',
78
        'ai'      => 'application/postscript',
79
        'bcpio'   => 'application/x-bcpio',
80
        'bin'     => 'application/octet-stream',
81
        'ccad'    => 'application/clariscad',
82
        'cdf'     => 'application/x-netcdf',
83
        'class'   => 'application/octet-stream',
84
        'cpio'    => 'application/x-cpio',
85
        'cpt'     => 'application/mac-compactpro',
86
        'csh'     => 'application/x-csh',
87
        'csv'     => ['text/csv', 'application/vnd.ms-excel'],
88
        'dcr'     => 'application/x-director',
89
        'dir'     => 'application/x-director',
90
        'dms'     => 'application/octet-stream',
91
        'doc'     => 'application/msword',
92
        'docx'    => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
93
        'drw'     => 'application/drafting',
94
        'dvi'     => 'application/x-dvi',
95
        'dwg'     => 'application/acad',
96
        'dxf'     => 'application/dxf',
97
        'dxr'     => 'application/x-director',
98
        'eot'     => 'application/vnd.ms-fontobject',
99
        'eps'     => 'application/postscript',
100
        'exe'     => 'application/octet-stream',
101
        'ez'      => 'application/andrew-inset',
102
        'flv'     => 'video/x-flv',
103
        'gtar'    => 'application/x-gtar',
104
        'gz'      => 'application/x-gzip',
105
        'bz2'     => 'application/x-bzip',
106
        '7z'      => 'application/x-7z-compressed',
107
        'hal'     => ['application/hal+xml', 'application/vnd.hal+xml'],
108
        'haljson' => ['application/hal+json', 'application/vnd.hal+json'],
109
        'halxml'  => ['application/hal+xml', 'application/vnd.hal+xml'],
110
        'hdf'     => 'application/x-hdf',
111
        'hqx'     => 'application/mac-binhex40',
112
        'ico'     => 'image/x-icon',
113
        'ips'     => 'application/x-ipscript',
114
        'ipx'     => 'application/x-ipix',
115
        'js'      => 'application/javascript',
116
        'jsonapi' => 'application/vnd.api+json',
117
        'latex'   => 'application/x-latex',
118
        'jsonld'  => 'application/ld+json',
119
        'kml'     => 'application/vnd.google-earth.kml+xml',
120
        'kmz'     => 'application/vnd.google-earth.kmz',
121
        'lha'     => 'application/octet-stream',
122
        'lsp'     => 'application/x-lisp',
123
        'lzh'     => 'application/octet-stream',
124
        'man'     => 'application/x-troff-man',
125
        'me'      => 'application/x-troff-me',
126
        'mif'     => 'application/vnd.mif',
127
        'ms'      => 'application/x-troff-ms',
128
        'nc'      => 'application/x-netcdf',
129
        'oda'     => 'application/oda',
130
        'otf'     => 'font/otf',
131
        'pdf'     => 'application/pdf',
132
        'pgn'     => 'application/x-chess-pgn',
133
        'pot'     => 'application/vnd.ms-powerpoint',
134
        'pps'     => 'application/vnd.ms-powerpoint',
135
        'ppt'     => 'application/vnd.ms-powerpoint',
136
        'pptx'    => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
137
        'ppz'     => 'application/vnd.ms-powerpoint',
138
        'pre'     => 'application/x-freelance',
139
        'prt'     => 'application/pro_eng',
140
        'ps'      => 'application/postscript',
141
        'roff'    => 'application/x-troff',
142
        'scm'     => 'application/x-lotusscreencam',
143
        'set'     => 'application/set',
144
        'sh'      => 'application/x-sh',
145
        'shar'    => 'application/x-shar',
146
        'sit'     => 'application/x-stuffit',
147
        'skd'     => 'application/x-koan',
148
        'skm'     => 'application/x-koan',
149
        'skp'     => 'application/x-koan',
150
        'skt'     => 'application/x-koan',
151
        'smi'     => 'application/smil',
152
        'smil'    => 'application/smil',
153
        'sol'     => 'application/solids',
154
        'spl'     => 'application/x-futuresplash',
155
        'src'     => 'application/x-wais-source',
156
        'step'    => 'application/STEP',
157
        'stl'     => 'application/SLA',
158
        'stp'     => 'application/STEP',
159
        'sv4cpio' => 'application/x-sv4cpio',
160
        'sv4crc'  => 'application/x-sv4crc',
161
        'svg'     => 'image/svg+xml',
162
        'svgz'    => 'image/svg+xml',
163
        'swf'     => 'application/x-shockwave-flash',
164
        't'       => 'application/x-troff',
165
        'tar'     => 'application/x-tar',
166
        'tcl'     => 'application/x-tcl',
167
        'tex'     => 'application/x-tex',
168
        'texi'    => 'application/x-texinfo',
169
        'texinfo' => 'application/x-texinfo',
170
        'tr'      => 'application/x-troff',
171
        'tsp'     => 'application/dsptype',
172
        'ttc'     => 'font/ttf',
173
        'ttf'     => 'font/ttf',
174
        'unv'     => 'application/i-deas',
175
        'ustar'   => 'application/x-ustar',
176
        'vcd'     => 'application/x-cdlink',
177
        'vda'     => 'application/vda',
178
        'xlc'     => 'application/vnd.ms-excel',
179
        'xll'     => 'application/vnd.ms-excel',
180
        'xlm'     => 'application/vnd.ms-excel',
181
        'xls'     => 'application/vnd.ms-excel',
182
        'xlsx'    => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
183
        'xlw'     => 'application/vnd.ms-excel',
184
        'zip'     => 'application/zip',
185
        'aif'     => 'audio/x-aiff',
186
        'aifc'    => 'audio/x-aiff',
187
        'aiff'    => 'audio/x-aiff',
188
        'au'      => 'audio/basic',
189
        'kar'     => 'audio/midi',
190
        'mid'     => 'audio/midi',
191
        'midi'    => 'audio/midi',
192
        'mp2'     => 'audio/mpeg',
193
        'mp3'     => 'audio/mpeg',
194
        'mpga'    => 'audio/mpeg',
195
        'ogg'     => 'audio/ogg',
196
        'oga'     => 'audio/ogg',
197
        'spx'     => 'audio/ogg',
198
        'ra'      => 'audio/x-realaudio',
199
        'ram'     => 'audio/x-pn-realaudio',
200
        'rm'      => 'audio/x-pn-realaudio',
201
        'rpm'     => 'audio/x-pn-realaudio-plugin',
202
        'snd'     => 'audio/basic',
203
        'tsi'     => 'audio/TSP-audio',
204
        'wav'     => 'audio/x-wav',
205
        'aac'     => 'audio/aac',
206
        'asc'     => 'text/plain',
207
        'c'       => 'text/plain',
208
        'cc'      => 'text/plain',
209
        'css'     => 'text/css',
210
        'etx'     => 'text/x-setext',
211
        'f'       => 'text/plain',
212
        'f90'     => 'text/plain',
213
        'h'       => 'text/plain',
214
        'hh'      => 'text/plain',
215
        'htm'     => ['text/html', '*/*'],
216
        'ics'     => 'text/calendar',
217
        'm'       => 'text/plain',
218
        'rtf'     => 'text/rtf',
219
        'rtx'     => 'text/richtext',
220
        'sgm'     => 'text/sgml',
221
        'sgml'    => 'text/sgml',
222
        'tsv'     => 'text/tab-separated-values',
223
        'tpl'     => 'text/template',
224
        'txt'     => 'text/plain',
225
        'text'    => 'text/plain',
226
        'avi'     => 'video/x-msvideo',
227
        'fli'     => 'video/x-fli',
228
        'mov'     => 'video/quicktime',
229
        'movie'   => 'video/x-sgi-movie',
230
        'mpe'     => 'video/mpeg',
231
        'mpeg'    => 'video/mpeg',
232
        'mpg'     => 'video/mpeg',
233
        'qt'      => 'video/quicktime',
234
        'viv'     => 'video/vnd.vivo',
235
        'vivo'    => 'video/vnd.vivo',
236
        'ogv'     => 'video/ogg',
237
        'webm'    => 'video/webm',
238
        'mp4'     => 'video/mp4',
239
        'm4v'     => 'video/mp4',
240
        'f4v'     => 'video/mp4',
241
        'f4p'     => 'video/mp4',
242
        'm4a'     => 'audio/mp4',
243
        'f4a'     => 'audio/mp4',
244
        'f4b'     => 'audio/mp4',
245
        'gif'     => 'image/gif',
246
        'ief'     => 'image/ief',
247
        'jpg'     => 'image/jpeg',
248
        'jpeg'    => 'image/jpeg',
249
        'jpe'     => 'image/jpeg',
250
        'pbm'     => 'image/x-portable-bitmap',
251
        'pgm'     => 'image/x-portable-graymap',
252
        'png'     => 'image/png',
253
        'pnm'     => 'image/x-portable-anymap',
254
        'ppm'     => 'image/x-portable-pixmap',
255
        'ras'     => 'image/cmu-raster',
256
        'rgb'     => 'image/x-rgb',
257
        'tif'     => 'image/tiff',
258
        'tiff'    => 'image/tiff',
259
        'xbm'     => 'image/x-xbitmap',
260
        'xpm'     => 'image/x-xpixmap',
261
        'xwd'     => 'image/x-xwindowdump',
262
        'psd'     => [
263
            'application/photoshop',
264
            'application/psd',
265
            'image/psd',
266
            'image/x-photoshop',
267
            'image/photoshop',
268
            'zz-application/zz-winassoc-psd',
269
        ],
270
        'ice'          => 'x-conference/x-cooltalk',
271
        'iges'         => 'model/iges',
272
        'igs'          => 'model/iges',
273
        'mesh'         => 'model/mesh',
274
        'msh'          => 'model/mesh',
275
        'silo'         => 'model/mesh',
276
        'vrml'         => 'model/vrml',
277
        'wrl'          => 'model/vrml',
278
        'mime'         => 'www/mime',
279
        'pdb'          => 'chemical/x-pdb',
280
        'xyz'          => 'chemical/x-pdb',
281
        'javascript'   => 'application/javascript',
282
        'form'         => 'application/x-www-form-urlencoded',
283
        'file'         => 'multipart/form-data',
284
        'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
285
        'atom'         => 'application/atom+xml',
286
        'amf'          => 'application/x-amf',
287
        'wap'          => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'],
288
        'wml'          => 'text/vnd.wap.wml',
289
        'wmlscript'    => 'text/vnd.wap.wmlscript',
290
        'wbmp'         => 'image/vnd.wap.wbmp',
291
        'woff'         => 'application/x-font-woff',
292
        'appcache'     => 'text/cache-manifest',
293
        'manifest'     => 'text/cache-manifest',
294
        'htc'          => 'text/x-component',
295
        'rdf'          => 'application/xml',
296
        'crx'          => 'application/x-chrome-extension',
297
        'oex'          => 'application/x-opera-extension',
298
        'xpi'          => 'application/x-xpinstall',
299
        'safariextz'   => 'application/octet-stream',
300
        'webapp'       => 'application/x-web-app-manifest+json',
301
        'vcf'          => 'text/x-vcard',
302
        'vtt'          => 'text/vtt',
303
        'mkv'          => 'video/x-matroska',
304
        'pkpass'       => 'application/vnd.apple.pkpass',
305
        'ajax'         => 'text/html',
306
        'bmp'          => 'image/bmp',
307
    ];
308
309
    /**
310
     * Code de statut à envoyer au client
311
     */
312
    protected int $_status = StatusCode::OK;
313
314
    /**
315
     * Objet de fichier pour le fichier à lire comme réponse
316
     *
317
     * @var SplFileInfo|null
318
     */
319
    protected $_file;
320
321
    /**
322
     * Gamme de fichiers. Utilisé pour demander des plages de fichiers.
323
     *
324
     * @var list<int>
0 ignored issues
show
The type BlitzPHP\Http\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
325
     */
326
    protected array $_fileRange = [];
327
328
    /**
329
     * Le jeu de caractères avec lequel le corps de la réponse est encodé
330
     */
331
    protected string $_charset = 'UTF-8';
332
333
    /**
334
     * Contient toutes les directives de cache qui seront converties
335
     * dans les en-têtes lors de l'envoi de la requête
336
     */
337
    protected array $_cacheDirectives = [];
338
339
    /**
340
     * Collecte de cookies à envoyer au client
341
     *
342
     * @var CookieCollection
343
     */
344
    protected $_cookies;
345
346
    /**
347
     * Phrase de raison
348
     */
349
    protected string $_reasonPhrase = 'OK';
350
351
    /**
352
     * Options du mode flux.
353
     */
354
    protected string $_streamMode = 'wb+';
355
356
    /**
357
     * Cible de flux ou objet de ressource.
358
     *
359
     * @var resource|string
360
     */
361
    protected $_streamTarget = 'php://memory';
362
363
    /**
364
     * Constructeur
365
     *
366
     * @param array<string, mixed> $options liste de paramètres pour configurer la réponse. Les valeurs possibles sont :
367
     *
368
     * - body : le texte de réponse qui doit être envoyé au client
369
     * - status : le code d'état HTTP avec lequel répondre
370
     * - type : une chaîne complète de type mime ou une extension mappée dans cette classe
371
     * - charset : le jeu de caractères pour le corps de la réponse
372
     *
373
     * @throws InvalidArgumentException
374
     */
375
    public function __construct(array $options = [])
376
    {
377 35
        $this->_streamTarget = $options['streamTarget'] ?? $this->_streamTarget;
378 35
        $this->_streamMode   = $options['streamMode'] ?? $this->_streamMode;
379
380
        if (isset($options['stream'])) {
381
            if (! $options['stream'] instanceof StreamInterface) {
382
                throw new InvalidArgumentException('Stream option must be an object that implements StreamInterface');
383
            }
384
            $this->stream = $options['stream'];
385
        } else {
386 35
            $this->_createStream();
387
        }
388
389
        if (isset($options['body'])) {
390 4
            $this->stream->write($options['body']);
391
        }
392
393
        if (isset($options['status'])) {
394 2
            $this->_setStatus($options['status']);
395
        }
396
397
        if (! isset($options['charset'])) {
398 35
            $options['charset'] = config('app.charset');
399
        }
400 35
        $this->_charset = $options['charset'];
401
402 35
        $type = 'text/html';
403
        if (isset($options['type'])) {
404 2
            $type = $this->resolveType($options['type']);
405
        }
406 35
        $this->_setContentType($type);
407
408 35
        $this->_cookies = new CookieCollection();
409
    }
410
411
    /**
412
     * Crée l'objet de flux.
413
     */
414
    protected function _createStream(): void
415
    {
416 35
        $this->stream = new Stream(Utils::tryFopen($this->_streamTarget, $this->_streamMode));
0 ignored issues
show
It seems like $this->_streamTarget can also be of type resource; however, parameter $filename of GuzzleHttp\Psr7\Utils::tryFopen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1169
        $this->stream->/** @scrutinizer ignore-call */ 
1170
                       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...
1170
1171
        return $this->stream->getContents();
1172
    }
1173
1174
    /**
1175
     * Créez une nouvelle réponse avec un jeu de cookies.
1176
     *
1177
     * ### Exemple
1178
     *
1179
     * ```
1180
     * // ajouter un objet cookie
1181
     * $response = $response->withCookie(new Cookie('remember_me', 1));
1182
     */
1183
    public function withCookie(CookieInterface $cookie): static
1184
    {
1185 4
        $new           = clone $this;
1186 4
        $new->_cookies = $new->_cookies->add($cookie);
1187
1188 4
        return $new;
1189
    }
1190
1191
    /**
1192
     * Créez une nouvelle réponse avec un jeu de cookies expiré.
1193
     *
1194
     * ### Exemple
1195
     *
1196
     * ```
1197
     * // ajouter un objet cookie
1198
     * $response = $response->withExpiredCookie(new Cookie('remember_me'));
1199
     */
1200
    public function withExpiredCookie(CookieInterface $cookie): static
1201
    {
1202
        $cookie = $cookie->withExpired();
1203
1204
        $new           = clone $this;
1205
        $new->_cookies = $new->_cookies->add($cookie);
1206
1207
        return $new;
1208
    }
1209
1210
    /**
1211
     * Expire un cookie lors de l'envoi de la réponse.
1212
     *
1213
     * @param CookieInterface|string $cookie
1214
     */
1215
    public function withoutCookie($cookie, ?string $path = null, ?string $domain = null)
1216
    {
1217
        if (is_string($cookie) && function_exists('cookie')) {
1218
            $cookie = cookie($cookie, null, -2628000, compact('path', 'domain'));
1219
        }
1220
1221
        return $this->withExpiredCookie($cookie);
1222
    }
1223
1224
    /**
1225
     * Lire un seul cookie à partir de la réponse.
1226
     *
1227
     * Cette méthode fournit un accès en lecture aux cookies en attente. Ce sera
1228
     * ne lit pas l'en-tête `Set-Cookie` s'il est défini.
1229
     *
1230
     * @param string $name Le nom du cookie que vous souhaitez lire.
1231
     *
1232
     * @return array|null Soit les données du cookie, soit null
1233
     */
1234
    public function getCookie(string $name): ?array
1235
    {
1236
        if (! $this->hasCookie($name)) {
1237
            return null;
1238
        }
1239
1240 2
        return $this->_cookies->get($name)->toArray();
1241
    }
1242
1243
    /**
1244
     * Vérifier si la reponse contient un cookie avec le nom donné
1245
     */
1246
    public function hasCookie(string $name, ?string $value = null): bool
1247
    {
1248
        if (! $this->_cookies->has($name)) {
1249 4
            return false;
1250
        }
1251
1252
        if ($value !== null) {
1253 2
            return $this->_cookies->get($name)->getValue() === $value;
1254
        }
1255
1256 4
        return true;
1257
    }
1258
1259
    /**
1260
     * Obtenez tous les cookies dans la réponse.
1261
     *
1262
     * Renvoie un tableau associatif de nom de cookie => données de cookie.
1263
     */
1264
    public function getCookies(): array
1265
    {
1266 2
        $out = [];
1267
        /** @var list<Cookie> $cookies */
1268 2
        $cookies = $this->_cookies;
1269
1270
        foreach ($cookies as $cookie) {
1271 2
            $out[$cookie->getName()] = $cookie->toArray();
1272
        }
1273
1274 2
        return $out;
1275
    }
1276
1277
    /**
1278
     * Obtenez la CookieCollection à partir de la réponse
1279
     */
1280
    public function getCookieCollection(): CookieCollection
1281
    {
1282 2
        return $this->_cookies;
1283
    }
1284
1285
    /**
1286
     * Obtenez une nouvelle instance avec la collection de cookies fournie.
1287
     */
1288
    public function withCookieCollection(CookieCollection $cookieCollection): static
1289
    {
1290 2
        $new           = clone $this;
1291 2
        $new->_cookies = $cookieCollection;
1292
1293 2
        return $new;
1294
    }
1295
1296
    /**
1297
     * Créez une nouvelle instance basée sur un fichier.
1298
     *
1299
     * Cette méthode augmentera à la fois le corps et un certain nombre d'en-têtes associés.
1300
     *
1301
     * Si `$_SERVER['HTTP_RANGE']` est défini, une tranche du fichier sera
1302
     * retourné au lieu du fichier entier.
1303
     *
1304
     * ### Touches d'options
1305
     *
1306
     * - nom : autre nom de téléchargement
1307
     * - download : si `true` définit l'en-tête de téléchargement et force le fichier à
1308
     * être téléchargé plutôt qu'affiché en ligne.
1309
     *
1310
     * @param SplFileInfo|string   $file    Chemin d'accès absolu au fichier ou instance de \SplFileInfo.
1311
     * @param array<string, mixed> $options Options Voir ci-dessus.
1312
     *
1313
     * @throws LoadException
1314
     */
1315
    public function withFile(SplFileInfo|string $file, array $options = []): static
1316
    {
1317 2
        $file = is_string($file) ? $this->validateFile($file) : $file;
1318
        $options += [
1319
            'name'     => null,
1320
            'download' => null,
1321 2
        ];
1322
1323 2
        $extension = strtolower($file->getExtension());
1324 2
        $mapped    = $this->getMimeType($extension);
1325
        if ((! $extension || ! $mapped) && $options['download'] === null) {
1326 2
            $options['download'] = true;
1327
        }
1328
1329 2
        $new = clone $this;
1330
        if ($mapped) {
1331 2
            $new = $new->withType($extension);
1332
        }
1333
1334 2
        $fileSize = $file->getSize();
1335
        if ($options['download']) {
1336 2
            $agent = (string) env('HTTP_USER_AGENT');
1337
1338
            if ($agent && preg_match('%Opera([/ ])([0-9].[0-9]{1,2})%', $agent)) {
1339 2
                $contentType = 'application/octet-stream';
1340
            } elseif ($agent && preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
1341 2
                $contentType = 'application/force-download';
1342
            }
1343
1344
            if (isset($contentType)) {
1345 2
                $new = $new->withType($contentType);
1346
            }
1347 2
            $name = $options['name'] ?: $file->getFilename();
1348
            $new  = $new->withDownload($name)
1349 2
                ->withHeader('Content-Transfer-Encoding', 'binary');
1350
        }
1351
1352 2
        $new       = $new->withHeader('Accept-Ranges', 'bytes');
1353 2
        $httpRange = (string) env('HTTP_RANGE');
1354
        if ($httpRange !== '' && $httpRange !== '0') {
1355 2
            $new->_fileRange($file, $httpRange);
1356
        } else {
1357 2
            $new = $new->withHeader('Content-Length', (string) $fileSize);
1358
        }
1359 2
        $new->_file  = $file;
1360 2
        $new->stream = new Stream(Utils::tryFopen($file->getPathname(), 'rb'));
1361
1362 2
        return $new;
1363
    }
1364
1365
    /**
1366
     * Méthode pratique pour définir une chaîne dans le corps de la réponse
1367
     *
1368
     * @param string|null $string La chaîne à envoyer
1369
     */
1370
    public function withStringBody(?string $string): static
1371
    {
1372 2
        $new = clone $this;
1373
1374 2
        return $new->withBody(Utils::streamFor($string));
1375
    }
1376
1377
    /**
1378
     * Valider qu'un chemin de fichier est un corps de réponse valide.
1379
     *
1380
     * @throws LoadException
1381
     */
1382
    protected function validateFile(string $path): SplFileInfo
1383
    {
1384
        if (str_contains($path, '../') || str_contains($path, '..\\')) {
1385 2
            throw new LoadException('The requested file contains `..` and will not be read.');
1386
        }
1387
        if (! is_file($path)) {
1388
            $path = APP_PATH . $path;
1389
        }
1390
1391 2
        $file = new SplFileInfo($path);
1392
        if (! $file->isFile() || ! $file->isReadable()) {
1393
            if (on_dev()) {
1394
                throw new LoadException(sprintf('The requested file %s was not found or not readable', $path));
1395
            }
1396
1397
            throw new LoadException('The requested file was not found');
1398
        }
1399
1400 2
        return $file;
1401
    }
1402
1403
    /**
1404
     * Obtenir le fichier actuel s'il en existe un.
1405
     *
1406
     * @return SplFileInfo|null Le fichier à utiliser dans la réponse ou null
1407
     */
1408
    public function getFile(): ?SplFileInfo
1409
    {
1410
        return $this->_file;
1411
    }
1412
1413
    /**
1414
     * Appliquez une plage de fichiers à un fichier et définissez le décalage de fin.
1415
     *
1416
     * Si une plage non valide est demandée, un code d'état 416 sera utilisé
1417
     * dans la réponse.
1418
     *
1419
     * @param SplFileInfo $file      Le fichier sur lequel définir une plage.
1420
     * @param string      $httpRange La plage à utiliser.
1421
     */
1422
    protected function _fileRange(SplFileInfo $file, string $httpRange): void
1423
    {
1424
        $fileSize = $file->getSize();
1425
        $lastByte = $fileSize - 1;
1426
        $start    = 0;
1427
        $end      = $lastByte;
1428
1429
        preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches);
1430
        if ($matches !== []) {
1431
            $start = $matches[1];
1432
            $end   = $matches[2] ?? '';
1433
        }
1434
1435
        if ($start === '') {
1436
            $start = $fileSize - (int) $end;
1437
            $end   = $lastByte;
1438
        }
1439
        if ($end === '') {
1440
            $end = $lastByte;
1441
        }
1442
1443
        if ($start > $end || $end > $lastByte || $start > $lastByte) {
1444
            $this->_setStatus(416);
1445
            $this->_setHeader('Content-Range', 'bytes 0-' . $lastByte . '/' . $fileSize);
1446
1447
            return;
1448
        }
1449
1450
        /** @psalm-suppress PossiblyInvalidOperand */
1451
        $this->_setHeader('Content-Length', (string) ($end - $start + 1));
1452
        $this->_setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $fileSize);
1453
        $this->_setStatus(206);
1454
        /**
1455
         * @var int $start
1456
         * @var int $end
1457
         */
1458
        $this->_fileRange = [$start, $end];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($start, $end) of type array<integer,integer> is incompatible with the declared type BlitzPHP\Http\list of property $_fileRange.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1459
    }
1460
1461
    /**
1462
     * Retourne un tableau qui peut être utilisé pour décrire l'état interne de cet objet.
1463
     *
1464
     * @return array<string, mixed>
1465
     */
1466
    public function __debugInfo(): array
1467
    {
1468
        return [
1469
            'status'          => $this->_status,
1470
            'contentType'     => $this->getType(),
1471
            'headers'         => $this->headers,
1472
            'file'            => $this->_file,
1473
            'fileRange'       => $this->_fileRange,
1474
            'cookies'         => $this->_cookies,
1475
            'cacheDirectives' => $this->_cacheDirectives,
1476
            'body'            => (string) $this->getBody(),
1477
        ];
1478
    }
1479
}
1480