Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\HttpFoundation;
13
 
14
// Help opcache.preload discover always-needed symbols
15
class_exists(ResponseHeaderBag::class);
16
 
17
/**
18
 * Response represents an HTTP response.
19
 *
20
 * @author Fabien Potencier <fabien@symfony.com>
21
 */
22
class Response
23
{
24
    public const HTTP_CONTINUE = 100;
25
    public const HTTP_SWITCHING_PROTOCOLS = 101;
26
    public const HTTP_PROCESSING = 102;            // RFC2518
27
    public const HTTP_EARLY_HINTS = 103;           // RFC8297
28
    public const HTTP_OK = 200;
29
    public const HTTP_CREATED = 201;
30
    public const HTTP_ACCEPTED = 202;
31
    public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
32
    public const HTTP_NO_CONTENT = 204;
33
    public const HTTP_RESET_CONTENT = 205;
34
    public const HTTP_PARTIAL_CONTENT = 206;
35
    public const HTTP_MULTI_STATUS = 207;          // RFC4918
36
    public const HTTP_ALREADY_REPORTED = 208;      // RFC5842
37
    public const HTTP_IM_USED = 226;               // RFC3229
38
    public const HTTP_MULTIPLE_CHOICES = 300;
39
    public const HTTP_MOVED_PERMANENTLY = 301;
40
    public const HTTP_FOUND = 302;
41
    public const HTTP_SEE_OTHER = 303;
42
    public const HTTP_NOT_MODIFIED = 304;
43
    public const HTTP_USE_PROXY = 305;
44
    public const HTTP_RESERVED = 306;
45
    public const HTTP_TEMPORARY_REDIRECT = 307;
46
    public const HTTP_PERMANENTLY_REDIRECT = 308;  // RFC7238
47
    public const HTTP_BAD_REQUEST = 400;
48
    public const HTTP_UNAUTHORIZED = 401;
49
    public const HTTP_PAYMENT_REQUIRED = 402;
50
    public const HTTP_FORBIDDEN = 403;
51
    public const HTTP_NOT_FOUND = 404;
52
    public const HTTP_METHOD_NOT_ALLOWED = 405;
53
    public const HTTP_NOT_ACCEPTABLE = 406;
54
    public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
55
    public const HTTP_REQUEST_TIMEOUT = 408;
56
    public const HTTP_CONFLICT = 409;
57
    public const HTTP_GONE = 410;
58
    public const HTTP_LENGTH_REQUIRED = 411;
59
    public const HTTP_PRECONDITION_FAILED = 412;
60
    public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
61
    public const HTTP_REQUEST_URI_TOO_LONG = 414;
62
    public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
63
    public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
64
    public const HTTP_EXPECTATION_FAILED = 417;
65
    public const HTTP_I_AM_A_TEAPOT = 418;                                               // RFC2324
66
    public const HTTP_MISDIRECTED_REQUEST = 421;                                         // RFC7540
67
    public const HTTP_UNPROCESSABLE_ENTITY = 422;                                        // RFC4918
68
    public const HTTP_LOCKED = 423;                                                      // RFC4918
69
    public const HTTP_FAILED_DEPENDENCY = 424;                                           // RFC4918
70
    public const HTTP_TOO_EARLY = 425;                                                   // RFC-ietf-httpbis-replay-04
71
    public const HTTP_UPGRADE_REQUIRED = 426;                                            // RFC2817
72
    public const HTTP_PRECONDITION_REQUIRED = 428;                                       // RFC6585
73
    public const HTTP_TOO_MANY_REQUESTS = 429;                                           // RFC6585
74
    public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;                             // RFC6585
75
    public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451;                               // RFC7725
76
    public const HTTP_INTERNAL_SERVER_ERROR = 500;
77
    public const HTTP_NOT_IMPLEMENTED = 501;
78
    public const HTTP_BAD_GATEWAY = 502;
79
    public const HTTP_SERVICE_UNAVAILABLE = 503;
80
    public const HTTP_GATEWAY_TIMEOUT = 504;
81
    public const HTTP_VERSION_NOT_SUPPORTED = 505;
82
    public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506;                        // RFC2295
83
    public const HTTP_INSUFFICIENT_STORAGE = 507;                                        // RFC4918
84
    public const HTTP_LOOP_DETECTED = 508;                                               // RFC5842
85
    public const HTTP_NOT_EXTENDED = 510;                                                // RFC2774
86
    public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;                             // RFC6585
87
 
88
    /**
89
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
90
     */
91
    private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = [
92
        'must_revalidate' => false,
93
        'no_cache' => false,
94
        'no_store' => false,
95
        'no_transform' => false,
96
        'public' => false,
97
        'private' => false,
98
        'proxy_revalidate' => false,
99
        'max_age' => true,
100
        's_maxage' => true,
101
        'stale_if_error' => true,         // RFC5861
102
        'stale_while_revalidate' => true, // RFC5861
103
        'immutable' => false,
104
        'last_modified' => true,
105
        'etag' => true,
106
    ];
107
 
108
    /**
109
     * @var ResponseHeaderBag
110
     */
111
    public $headers;
112
 
113
    /**
114
     * @var string
115
     */
116
    protected $content;
117
 
118
    /**
119
     * @var string
120
     */
121
    protected $version;
122
 
123
    /**
124
     * @var int
125
     */
126
    protected $statusCode;
127
 
128
    /**
129
     * @var string
130
     */
131
    protected $statusText;
132
 
133
    /**
134
     * @var string
135
     */
136
    protected $charset;
137
 
138
    /**
139
     * Status codes translation table.
140
     *
141
     * The list of codes is complete according to the
142
     * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
143
     * (last updated 2021-10-01).
144
     *
145
     * Unless otherwise noted, the status code is defined in RFC2616.
146
     *
147
     * @var array
148
     */
149
    public static $statusTexts = [
150
        100 => 'Continue',
151
        101 => 'Switching Protocols',
152
        102 => 'Processing',            // RFC2518
153
        103 => 'Early Hints',
154
        200 => 'OK',
155
        201 => 'Created',
156
        202 => 'Accepted',
157
        203 => 'Non-Authoritative Information',
158
        204 => 'No Content',
159
        205 => 'Reset Content',
160
        206 => 'Partial Content',
161
        207 => 'Multi-Status',          // RFC4918
162
        208 => 'Already Reported',      // RFC5842
163
        226 => 'IM Used',               // RFC3229
164
        300 => 'Multiple Choices',
165
        301 => 'Moved Permanently',
166
        302 => 'Found',
167
        303 => 'See Other',
168
        304 => 'Not Modified',
169
        305 => 'Use Proxy',
170
        307 => 'Temporary Redirect',
171
        308 => 'Permanent Redirect',    // RFC7238
172
        400 => 'Bad Request',
173
        401 => 'Unauthorized',
174
        402 => 'Payment Required',
175
        403 => 'Forbidden',
176
        404 => 'Not Found',
177
        405 => 'Method Not Allowed',
178
        406 => 'Not Acceptable',
179
        407 => 'Proxy Authentication Required',
180
        408 => 'Request Timeout',
181
        409 => 'Conflict',
182
        410 => 'Gone',
183
        411 => 'Length Required',
184
        412 => 'Precondition Failed',
185
        413 => 'Content Too Large',                                           // RFC-ietf-httpbis-semantics
186
        414 => 'URI Too Long',
187
        415 => 'Unsupported Media Type',
188
        416 => 'Range Not Satisfiable',
189
        417 => 'Expectation Failed',
190
        418 => 'I\'m a teapot',                                               // RFC2324
191
        421 => 'Misdirected Request',                                         // RFC7540
192
        422 => 'Unprocessable Content',                                       // RFC-ietf-httpbis-semantics
193
        423 => 'Locked',                                                      // RFC4918
194
        424 => 'Failed Dependency',                                           // RFC4918
195
        425 => 'Too Early',                                                   // RFC-ietf-httpbis-replay-04
196
        426 => 'Upgrade Required',                                            // RFC2817
197
        428 => 'Precondition Required',                                       // RFC6585
198
        429 => 'Too Many Requests',                                           // RFC6585
199
        431 => 'Request Header Fields Too Large',                             // RFC6585
200
        451 => 'Unavailable For Legal Reasons',                               // RFC7725
201
        500 => 'Internal Server Error',
202
        501 => 'Not Implemented',
203
        502 => 'Bad Gateway',
204
        503 => 'Service Unavailable',
205
        504 => 'Gateway Timeout',
206
        505 => 'HTTP Version Not Supported',
207
        506 => 'Variant Also Negotiates',                                     // RFC2295
208
        507 => 'Insufficient Storage',                                        // RFC4918
209
        508 => 'Loop Detected',                                               // RFC5842
210
        510 => 'Not Extended',                                                // RFC2774
211
        511 => 'Network Authentication Required',                             // RFC6585
212
    ];
213
 
214
    /**
215
     * @param int $status The HTTP status code (200 "OK" by default)
216
     *
217
     * @throws \InvalidArgumentException When the HTTP status code is not valid
218
     */
219
    public function __construct(?string $content = '', int $status = 200, array $headers = [])
220
    {
221
        $this->headers = new ResponseHeaderBag($headers);
222
        $this->setContent($content);
223
        $this->setStatusCode($status);
224
        $this->setProtocolVersion('1.0');
225
    }
226
 
227
    /**
228
     * Returns the Response as an HTTP string.
229
     *
230
     * The string representation of the Response is the same as the
231
     * one that will be sent to the client only if the prepare() method
232
     * has been called before.
233
     *
234
     * @see prepare()
235
     */
236
    public function __toString(): string
237
    {
238
        return
239
            sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
240
            $this->headers."\r\n".
241
            $this->getContent();
242
    }
243
 
244
    /**
245
     * Clones the current Response instance.
246
     */
247
    public function __clone()
248
    {
249
        $this->headers = clone $this->headers;
250
    }
251
 
252
    /**
253
     * Prepares the Response before it is sent to the client.
254
     *
255
     * This method tweaks the Response to ensure that it is
256
     * compliant with RFC 2616. Most of the changes are based on
257
     * the Request that is "associated" with this Response.
258
     *
259
     * @return $this
260
     */
261
    public function prepare(Request $request): static
262
    {
263
        $headers = $this->headers;
264
 
265
        if ($this->isInformational() || $this->isEmpty()) {
266
            $this->setContent(null);
267
            $headers->remove('Content-Type');
268
            $headers->remove('Content-Length');
269
            // prevent PHP from sending the Content-Type header based on default_mimetype
270
            ini_set('default_mimetype', '');
271
        } else {
272
            // Content-type based on the Request
273
            if (!$headers->has('Content-Type')) {
274
                $format = $request->getRequestFormat(null);
275
                if (null !== $format && $mimeType = $request->getMimeType($format)) {
276
                    $headers->set('Content-Type', $mimeType);
277
                }
278
            }
279
 
280
            // Fix Content-Type
281
            $charset = $this->charset ?: 'UTF-8';
282
            if (!$headers->has('Content-Type')) {
283
                $headers->set('Content-Type', 'text/html; charset='.$charset);
284
            } elseif (0 === stripos($headers->get('Content-Type'), 'text/') && false === stripos($headers->get('Content-Type'), 'charset')) {
285
                // add the charset
286
                $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
287
            }
288
 
289
            // Fix Content-Length
290
            if ($headers->has('Transfer-Encoding')) {
291
                $headers->remove('Content-Length');
292
            }
293
 
294
            if ($request->isMethod('HEAD')) {
295
                // cf. RFC2616 14.13
296
                $length = $headers->get('Content-Length');
297
                $this->setContent(null);
298
                if ($length) {
299
                    $headers->set('Content-Length', $length);
300
                }
301
            }
302
        }
303
 
304
        // Fix protocol
305
        if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) {
306
            $this->setProtocolVersion('1.1');
307
        }
308
 
309
        // Check if we need to send extra expire info headers
310
        if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) {
311
            $headers->set('pragma', 'no-cache');
312
            $headers->set('expires', -1);
313
        }
314
 
315
        $this->ensureIEOverSSLCompatibility($request);
316
 
317
        if ($request->isSecure()) {
318
            foreach ($headers->getCookies() as $cookie) {
319
                $cookie->setSecureDefault(true);
320
            }
321
        }
322
 
323
        return $this;
324
    }
325
 
326
    /**
327
     * Sends HTTP headers.
328
     *
329
     * @return $this
330
     */
331
    public function sendHeaders(): static
332
    {
333
        // headers have already been sent by the developer
334
        if (headers_sent()) {
335
            return $this;
336
        }
337
 
338
        // headers
339
        foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
340
            $replace = 0 === strcasecmp($name, 'Content-Type');
341
            foreach ($values as $value) {
342
                header($name.': '.$value, $replace, $this->statusCode);
343
            }
344
        }
345
 
346
        // cookies
347
        foreach ($this->headers->getCookies() as $cookie) {
348
            header('Set-Cookie: '.$cookie, false, $this->statusCode);
349
        }
350
 
351
        // status
352
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
353
 
354
        return $this;
355
    }
356
 
357
    /**
358
     * Sends content for the current web response.
359
     *
360
     * @return $this
361
     */
362
    public function sendContent(): static
363
    {
364
        echo $this->content;
365
 
366
        return $this;
367
    }
368
 
369
    /**
370
     * Sends HTTP headers and content.
371
     *
372
     * @return $this
373
     */
374
    public function send(): static
375
    {
376
        $this->sendHeaders();
377
        $this->sendContent();
378
 
379
        if (\function_exists('fastcgi_finish_request')) {
380
            fastcgi_finish_request();
381
        } elseif (\function_exists('litespeed_finish_request')) {
382
            litespeed_finish_request();
383
        } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
384
            static::closeOutputBuffers(0, true);
385
            flush();
386
        }
387
 
388
        return $this;
389
    }
390
 
391
    /**
392
     * Sets the response content.
393
     *
394
     * @return $this
395
     */
396
    public function setContent(?string $content): static
397
    {
398
        $this->content = $content ?? '';
399
 
400
        return $this;
401
    }
402
 
403
    /**
404
     * Gets the current response content.
405
     */
406
    public function getContent(): string|false
407
    {
408
        return $this->content;
409
    }
410
 
411
    /**
412
     * Sets the HTTP protocol version (1.0 or 1.1).
413
     *
414
     * @return $this
415
     *
416
     * @final
417
     */
418
    public function setProtocolVersion(string $version): static
419
    {
420
        $this->version = $version;
421
 
422
        return $this;
423
    }
424
 
425
    /**
426
     * Gets the HTTP protocol version.
427
     *
428
     * @final
429
     */
430
    public function getProtocolVersion(): string
431
    {
432
        return $this->version;
433
    }
434
 
435
    /**
436
     * Sets the response status code.
437
     *
438
     * If the status text is null it will be automatically populated for the known
439
     * status codes and left empty otherwise.
440
     *
441
     * @return $this
442
     *
443
     * @throws \InvalidArgumentException When the HTTP status code is not valid
444
     *
445
     * @final
446
     */
447
    public function setStatusCode(int $code, string $text = null): static
448
    {
449
        $this->statusCode = $code;
450
        if ($this->isInvalid()) {
451
            throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
452
        }
453
 
454
        if (null === $text) {
455
            $this->statusText = self::$statusTexts[$code] ?? 'unknown status';
456
 
457
            return $this;
458
        }
459
 
460
        if (false === $text) {
461
            $this->statusText = '';
462
 
463
            return $this;
464
        }
465
 
466
        $this->statusText = $text;
467
 
468
        return $this;
469
    }
470
 
471
    /**
472
     * Retrieves the status code for the current web response.
473
     *
474
     * @final
475
     */
476
    public function getStatusCode(): int
477
    {
478
        return $this->statusCode;
479
    }
480
 
481
    /**
482
     * Sets the response charset.
483
     *
484
     * @return $this
485
     *
486
     * @final
487
     */
488
    public function setCharset(string $charset): static
489
    {
490
        $this->charset = $charset;
491
 
492
        return $this;
493
    }
494
 
495
    /**
496
     * Retrieves the response charset.
497
     *
498
     * @final
499
     */
500
    public function getCharset(): ?string
501
    {
502
        return $this->charset;
503
    }
504
 
505
    /**
506
     * Returns true if the response may safely be kept in a shared (surrogate) cache.
507
     *
508
     * Responses marked "private" with an explicit Cache-Control directive are
509
     * considered uncacheable.
510
     *
511
     * Responses with neither a freshness lifetime (Expires, max-age) nor cache
512
     * validator (Last-Modified, ETag) are considered uncacheable because there is
513
     * no way to tell when or how to remove them from the cache.
514
     *
515
     * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
516
     * for example "status codes that are defined as cacheable by default [...]
517
     * can be reused by a cache with heuristic expiration unless otherwise indicated"
518
     * (https://tools.ietf.org/html/rfc7231#section-6.1)
519
     *
520
     * @final
521
     */
522
    public function isCacheable(): bool
523
    {
524
        if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) {
525
            return false;
526
        }
527
 
528
        if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
529
            return false;
530
        }
531
 
532
        return $this->isValidateable() || $this->isFresh();
533
    }
534
 
535
    /**
536
     * Returns true if the response is "fresh".
537
     *
538
     * Fresh responses may be served from cache without any interaction with the
539
     * origin. A response is considered fresh when it includes a Cache-Control/max-age
540
     * indicator or Expires header and the calculated age is less than the freshness lifetime.
541
     *
542
     * @final
543
     */
544
    public function isFresh(): bool
545
    {
546
        return $this->getTtl() > 0;
547
    }
548
 
549
    /**
550
     * Returns true if the response includes headers that can be used to validate
551
     * the response with the origin server using a conditional GET request.
552
     *
553
     * @final
554
     */
555
    public function isValidateable(): bool
556
    {
557
        return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
558
    }
559
 
560
    /**
561
     * Marks the response as "private".
562
     *
563
     * It makes the response ineligible for serving other clients.
564
     *
565
     * @return $this
566
     *
567
     * @final
568
     */
569
    public function setPrivate(): static
570
    {
571
        $this->headers->removeCacheControlDirective('public');
572
        $this->headers->addCacheControlDirective('private');
573
 
574
        return $this;
575
    }
576
 
577
    /**
578
     * Marks the response as "public".
579
     *
580
     * It makes the response eligible for serving other clients.
581
     *
582
     * @return $this
583
     *
584
     * @final
585
     */
586
    public function setPublic(): static
587
    {
588
        $this->headers->addCacheControlDirective('public');
589
        $this->headers->removeCacheControlDirective('private');
590
 
591
        return $this;
592
    }
593
 
594
    /**
595
     * Marks the response as "immutable".
596
     *
597
     * @return $this
598
     *
599
     * @final
600
     */
601
    public function setImmutable(bool $immutable = true): static
602
    {
603
        if ($immutable) {
604
            $this->headers->addCacheControlDirective('immutable');
605
        } else {
606
            $this->headers->removeCacheControlDirective('immutable');
607
        }
608
 
609
        return $this;
610
    }
611
 
612
    /**
613
     * Returns true if the response is marked as "immutable".
614
     *
615
     * @final
616
     */
617
    public function isImmutable(): bool
618
    {
619
        return $this->headers->hasCacheControlDirective('immutable');
620
    }
621
 
622
    /**
623
     * Returns true if the response must be revalidated by shared caches once it has become stale.
624
     *
625
     * This method indicates that the response must not be served stale by a
626
     * cache in any circumstance without first revalidating with the origin.
627
     * When present, the TTL of the response should not be overridden to be
628
     * greater than the value provided by the origin.
629
     *
630
     * @final
631
     */
632
    public function mustRevalidate(): bool
633
    {
634
        return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate');
635
    }
636
 
637
    /**
638
     * Returns the Date header as a DateTime instance.
639
     *
640
     * @throws \RuntimeException When the header is not parseable
641
     *
642
     * @final
643
     */
644
    public function getDate(): ?\DateTimeInterface
645
    {
646
        return $this->headers->getDate('Date');
647
    }
648
 
649
    /**
650
     * Sets the Date header.
651
     *
652
     * @return $this
653
     *
654
     * @final
655
     */
656
    public function setDate(\DateTimeInterface $date): static
657
    {
658
        if ($date instanceof \DateTime) {
659
            $date = \DateTimeImmutable::createFromMutable($date);
660
        }
661
 
662
        $date = $date->setTimezone(new \DateTimeZone('UTC'));
663
        $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
664
 
665
        return $this;
666
    }
667
 
668
    /**
669
     * Returns the age of the response in seconds.
670
     *
671
     * @final
672
     */
673
    public function getAge(): int
674
    {
675
        if (null !== $age = $this->headers->get('Age')) {
676
            return (int) $age;
677
        }
678
 
679
        return max(time() - (int) $this->getDate()->format('U'), 0);
680
    }
681
 
682
    /**
683
     * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
684
     *
685
     * @return $this
686
     */
687
    public function expire(): static
688
    {
689
        if ($this->isFresh()) {
690
            $this->headers->set('Age', $this->getMaxAge());
691
            $this->headers->remove('Expires');
692
        }
693
 
694
        return $this;
695
    }
696
 
697
    /**
698
     * Returns the value of the Expires header as a DateTime instance.
699
     *
700
     * @final
701
     */
702
    public function getExpires(): ?\DateTimeInterface
703
    {
704
        try {
705
            return $this->headers->getDate('Expires');
706
        } catch (\RuntimeException) {
707
            // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past
708
            return \DateTime::createFromFormat('U', time() - 172800);
709
        }
710
    }
711
 
712
    /**
713
     * Sets the Expires HTTP header with a DateTime instance.
714
     *
715
     * Passing null as value will remove the header.
716
     *
717
     * @return $this
718
     *
719
     * @final
720
     */
721
    public function setExpires(\DateTimeInterface $date = null): static
722
    {
723
        if (1 > \func_num_args()) {
724
            trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
725
        }
726
        if (null === $date) {
727
            $this->headers->remove('Expires');
728
 
729
            return $this;
730
        }
731
 
732
        if ($date instanceof \DateTime) {
733
            $date = \DateTimeImmutable::createFromMutable($date);
734
        }
735
 
736
        $date = $date->setTimezone(new \DateTimeZone('UTC'));
737
        $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
738
 
739
        return $this;
740
    }
741
 
742
    /**
743
     * Returns the number of seconds after the time specified in the response's Date
744
     * header when the response should no longer be considered fresh.
745
     *
746
     * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
747
     * back on an expires header. It returns null when no maximum age can be established.
748
     *
749
     * @final
750
     */
751
    public function getMaxAge(): ?int
752
    {
753
        if ($this->headers->hasCacheControlDirective('s-maxage')) {
754
            return (int) $this->headers->getCacheControlDirective('s-maxage');
755
        }
756
 
757
        if ($this->headers->hasCacheControlDirective('max-age')) {
758
            return (int) $this->headers->getCacheControlDirective('max-age');
759
        }
760
 
761
        if (null !== $this->getExpires()) {
762
            return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U');
763
        }
764
 
765
        return null;
766
    }
767
 
768
    /**
769
     * Sets the number of seconds after which the response should no longer be considered fresh.
770
     *
771
     * This methods sets the Cache-Control max-age directive.
772
     *
773
     * @return $this
774
     *
775
     * @final
776
     */
777
    public function setMaxAge(int $value): static
778
    {
779
        $this->headers->addCacheControlDirective('max-age', $value);
780
 
781
        return $this;
782
    }
783
 
784
    /**
785
     * Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down.
786
     *
787
     * This method sets the Cache-Control stale-if-error directive.
788
     *
789
     * @return $this
790
     *
791
     * @final
792
     */
793
    public function setStaleIfError(int $value): static
794
    {
795
        $this->headers->addCacheControlDirective('stale-if-error', $value);
796
 
797
        return $this;
798
    }
799
 
800
    /**
801
     * Sets the number of seconds after which the response should no longer return stale content by shared caches.
802
     *
803
     * This method sets the Cache-Control stale-while-revalidate directive.
804
     *
805
     * @return $this
806
     *
807
     * @final
808
     */
809
    public function setStaleWhileRevalidate(int $value): static
810
    {
811
        $this->headers->addCacheControlDirective('stale-while-revalidate', $value);
812
 
813
        return $this;
814
    }
815
 
816
    /**
817
     * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
818
     *
819
     * This methods sets the Cache-Control s-maxage directive.
820
     *
821
     * @return $this
822
     *
823
     * @final
824
     */
825
    public function setSharedMaxAge(int $value): static
826
    {
827
        $this->setPublic();
828
        $this->headers->addCacheControlDirective('s-maxage', $value);
829
 
830
        return $this;
831
    }
832
 
833
    /**
834
     * Returns the response's time-to-live in seconds.
835
     *
836
     * It returns null when no freshness information is present in the response.
837
     *
838
     * When the responses TTL is <= 0, the response may not be served from cache without first
839
     * revalidating with the origin.
840
     *
841
     * @final
842
     */
843
    public function getTtl(): ?int
844
    {
845
        $maxAge = $this->getMaxAge();
846
 
847
        return null !== $maxAge ? $maxAge - $this->getAge() : null;
848
    }
849
 
850
    /**
851
     * Sets the response's time-to-live for shared caches in seconds.
852
     *
853
     * This method adjusts the Cache-Control/s-maxage directive.
854
     *
855
     * @return $this
856
     *
857
     * @final
858
     */
859
    public function setTtl(int $seconds): static
860
    {
861
        $this->setSharedMaxAge($this->getAge() + $seconds);
862
 
863
        return $this;
864
    }
865
 
866
    /**
867
     * Sets the response's time-to-live for private/client caches in seconds.
868
     *
869
     * This method adjusts the Cache-Control/max-age directive.
870
     *
871
     * @return $this
872
     *
873
     * @final
874
     */
875
    public function setClientTtl(int $seconds): static
876
    {
877
        $this->setMaxAge($this->getAge() + $seconds);
878
 
879
        return $this;
880
    }
881
 
882
    /**
883
     * Returns the Last-Modified HTTP header as a DateTime instance.
884
     *
885
     * @throws \RuntimeException When the HTTP header is not parseable
886
     *
887
     * @final
888
     */
889
    public function getLastModified(): ?\DateTimeInterface
890
    {
891
        return $this->headers->getDate('Last-Modified');
892
    }
893
 
894
    /**
895
     * Sets the Last-Modified HTTP header with a DateTime instance.
896
     *
897
     * Passing null as value will remove the header.
898
     *
899
     * @return $this
900
     *
901
     * @final
902
     */
903
    public function setLastModified(\DateTimeInterface $date = null): static
904
    {
905
        if (1 > \func_num_args()) {
906
            trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
907
        }
908
        if (null === $date) {
909
            $this->headers->remove('Last-Modified');
910
 
911
            return $this;
912
        }
913
 
914
        if ($date instanceof \DateTime) {
915
            $date = \DateTimeImmutable::createFromMutable($date);
916
        }
917
 
918
        $date = $date->setTimezone(new \DateTimeZone('UTC'));
919
        $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
920
 
921
        return $this;
922
    }
923
 
924
    /**
925
     * Returns the literal value of the ETag HTTP header.
926
     *
927
     * @final
928
     */
929
    public function getEtag(): ?string
930
    {
931
        return $this->headers->get('ETag');
932
    }
933
 
934
    /**
935
     * Sets the ETag value.
936
     *
937
     * @param string|null $etag The ETag unique identifier or null to remove the header
938
     * @param bool        $weak Whether you want a weak ETag or not
939
     *
940
     * @return $this
941
     *
942
     * @final
943
     */
944
    public function setEtag(string $etag = null, bool $weak = false): static
945
    {
946
        if (1 > \func_num_args()) {
947
            trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
948
        }
949
        if (null === $etag) {
950
            $this->headers->remove('Etag');
951
        } else {
952
            if (!str_starts_with($etag, '"')) {
953
                $etag = '"'.$etag.'"';
954
            }
955
 
956
            $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
957
        }
958
 
959
        return $this;
960
    }
961
 
962
    /**
963
     * Sets the response's cache headers (validation and/or expiration).
964
     *
965
     * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
966
     *
967
     * @return $this
968
     *
969
     * @throws \InvalidArgumentException
970
     *
971
     * @final
972
     */
973
    public function setCache(array $options): static
974
    {
975
        if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) {
976
            throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff)));
977
        }
978
 
979
        if (isset($options['etag'])) {
980
            $this->setEtag($options['etag']);
981
        }
982
 
983
        if (isset($options['last_modified'])) {
984
            $this->setLastModified($options['last_modified']);
985
        }
986
 
987
        if (isset($options['max_age'])) {
988
            $this->setMaxAge($options['max_age']);
989
        }
990
 
991
        if (isset($options['s_maxage'])) {
992
            $this->setSharedMaxAge($options['s_maxage']);
993
        }
994
 
995
        if (isset($options['stale_while_revalidate'])) {
996
            $this->setStaleWhileRevalidate($options['stale_while_revalidate']);
997
        }
998
 
999
        if (isset($options['stale_if_error'])) {
1000
            $this->setStaleIfError($options['stale_if_error']);
1001
        }
1002
 
1003
        foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) {
1004
            if (!$hasValue && isset($options[$directive])) {
1005
                if ($options[$directive]) {
1006
                    $this->headers->addCacheControlDirective(str_replace('_', '-', $directive));
1007
                } else {
1008
                    $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive));
1009
                }
1010
            }
1011
        }
1012
 
1013
        if (isset($options['public'])) {
1014
            if ($options['public']) {
1015
                $this->setPublic();
1016
            } else {
1017
                $this->setPrivate();
1018
            }
1019
        }
1020
 
1021
        if (isset($options['private'])) {
1022
            if ($options['private']) {
1023
                $this->setPrivate();
1024
            } else {
1025
                $this->setPublic();
1026
            }
1027
        }
1028
 
1029
        return $this;
1030
    }
1031
 
1032
    /**
1033
     * Modifies the response so that it conforms to the rules defined for a 304 status code.
1034
     *
1035
     * This sets the status, removes the body, and discards any headers
1036
     * that MUST NOT be included in 304 responses.
1037
     *
1038
     * @return $this
1039
     *
1040
     * @see https://tools.ietf.org/html/rfc2616#section-10.3.5
1041
     *
1042
     * @final
1043
     */
1044
    public function setNotModified(): static
1045
    {
1046
        $this->setStatusCode(304);
1047
        $this->setContent(null);
1048
 
1049
        // remove headers that MUST NOT be included with 304 Not Modified responses
1050
        foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) {
1051
            $this->headers->remove($header);
1052
        }
1053
 
1054
        return $this;
1055
    }
1056
 
1057
    /**
1058
     * Returns true if the response includes a Vary header.
1059
     *
1060
     * @final
1061
     */
1062
    public function hasVary(): bool
1063
    {
1064
        return null !== $this->headers->get('Vary');
1065
    }
1066
 
1067
    /**
1068
     * Returns an array of header names given in the Vary header.
1069
     *
1070
     * @final
1071
     */
1072
    public function getVary(): array
1073
    {
1074
        if (!$vary = $this->headers->all('Vary')) {
1075
            return [];
1076
        }
1077
 
1078
        $ret = [];
1079
        foreach ($vary as $item) {
1080
            $ret[] = preg_split('/[\s,]+/', $item);
1081
        }
1082
 
1083
        return array_merge([], ...$ret);
1084
    }
1085
 
1086
    /**
1087
     * Sets the Vary header.
1088
     *
1089
     * @param bool $replace Whether to replace the actual value or not (true by default)
1090
     *
1091
     * @return $this
1092
     *
1093
     * @final
1094
     */
1095
    public function setVary(string|array $headers, bool $replace = true): static
1096
    {
1097
        $this->headers->set('Vary', $headers, $replace);
1098
 
1099
        return $this;
1100
    }
1101
 
1102
    /**
1103
     * Determines if the Response validators (ETag, Last-Modified) match
1104
     * a conditional value specified in the Request.
1105
     *
1106
     * If the Response is not modified, it sets the status code to 304 and
1107
     * removes the actual content by calling the setNotModified() method.
1108
     *
1109
     * @final
1110
     */
1111
    public function isNotModified(Request $request): bool
1112
    {
1113
        if (!$request->isMethodCacheable()) {
1114
            return false;
1115
        }
1116
 
1117
        $notModified = false;
1118
        $lastModified = $this->headers->get('Last-Modified');
1119
        $modifiedSince = $request->headers->get('If-Modified-Since');
1120
 
1121
        if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) {
1122
            if (0 == strncmp($etag, 'W/', 2)) {
1123
                $etag = substr($etag, 2);
1124
            }
1125
 
1126
            // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2.
1127
            foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) {
1128
                if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) {
1129
                    $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2);
1130
                }
1131
 
1132
                if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) {
1133
                    $notModified = true;
1134
                    break;
1135
                }
1136
            }
1137
        }
1138
        // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3.
1139
        elseif ($modifiedSince && $lastModified) {
1140
            $notModified = strtotime($modifiedSince) >= strtotime($lastModified);
1141
        }
1142
 
1143
        if ($notModified) {
1144
            $this->setNotModified();
1145
        }
1146
 
1147
        return $notModified;
1148
    }
1149
 
1150
    /**
1151
     * Is response invalid?
1152
     *
1153
     * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1154
     *
1155
     * @final
1156
     */
1157
    public function isInvalid(): bool
1158
    {
1159
        return $this->statusCode < 100 || $this->statusCode >= 600;
1160
    }
1161
 
1162
    /**
1163
     * Is response informative?
1164
     *
1165
     * @final
1166
     */
1167
    public function isInformational(): bool
1168
    {
1169
        return $this->statusCode >= 100 && $this->statusCode < 200;
1170
    }
1171
 
1172
    /**
1173
     * Is response successful?
1174
     *
1175
     * @final
1176
     */
1177
    public function isSuccessful(): bool
1178
    {
1179
        return $this->statusCode >= 200 && $this->statusCode < 300;
1180
    }
1181
 
1182
    /**
1183
     * Is the response a redirect?
1184
     *
1185
     * @final
1186
     */
1187
    public function isRedirection(): bool
1188
    {
1189
        return $this->statusCode >= 300 && $this->statusCode < 400;
1190
    }
1191
 
1192
    /**
1193
     * Is there a client error?
1194
     *
1195
     * @final
1196
     */
1197
    public function isClientError(): bool
1198
    {
1199
        return $this->statusCode >= 400 && $this->statusCode < 500;
1200
    }
1201
 
1202
    /**
1203
     * Was there a server side error?
1204
     *
1205
     * @final
1206
     */
1207
    public function isServerError(): bool
1208
    {
1209
        return $this->statusCode >= 500 && $this->statusCode < 600;
1210
    }
1211
 
1212
    /**
1213
     * Is the response OK?
1214
     *
1215
     * @final
1216
     */
1217
    public function isOk(): bool
1218
    {
1219
        return 200 === $this->statusCode;
1220
    }
1221
 
1222
    /**
1223
     * Is the response forbidden?
1224
     *
1225
     * @final
1226
     */
1227
    public function isForbidden(): bool
1228
    {
1229
        return 403 === $this->statusCode;
1230
    }
1231
 
1232
    /**
1233
     * Is the response a not found error?
1234
     *
1235
     * @final
1236
     */
1237
    public function isNotFound(): bool
1238
    {
1239
        return 404 === $this->statusCode;
1240
    }
1241
 
1242
    /**
1243
     * Is the response a redirect of some form?
1244
     *
1245
     * @final
1246
     */
1247
    public function isRedirect(string $location = null): bool
1248
    {
1249
        return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
1250
    }
1251
 
1252
    /**
1253
     * Is the response empty?
1254
     *
1255
     * @final
1256
     */
1257
    public function isEmpty(): bool
1258
    {
1259
        return \in_array($this->statusCode, [204, 304]);
1260
    }
1261
 
1262
    /**
1263
     * Cleans or flushes output buffers up to target level.
1264
     *
1265
     * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1266
     *
1267
     * @final
1268
     */
1269
    public static function closeOutputBuffers(int $targetLevel, bool $flush): void
1270
    {
1271
        $status = ob_get_status(true);
1272
        $level = \count($status);
1273
        $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE);
1274
 
1275
        while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
1276
            if ($flush) {
1277
                ob_end_flush();
1278
            } else {
1279
                ob_end_clean();
1280
            }
1281
        }
1282
    }
1283
 
1284
    /**
1285
     * Marks a response as safe according to RFC8674.
1286
     *
1287
     * @see https://tools.ietf.org/html/rfc8674
1288
     */
1289
    public function setContentSafe(bool $safe = true): void
1290
    {
1291
        if ($safe) {
1292
            $this->headers->set('Preference-Applied', 'safe');
1293
        } elseif ('safe' === $this->headers->get('Preference-Applied')) {
1294
            $this->headers->remove('Preference-Applied');
1295
        }
1296
 
1297
        $this->setVary('Prefer', false);
1298
    }
1299
 
1300
    /**
1301
     * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1302
     *
1303
     * @see http://support.microsoft.com/kb/323308
1304
     *
1305
     * @final
1306
     */
1307
    protected function ensureIEOverSSLCompatibility(Request $request): void
1308
    {
1309
        if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) {
1310
            if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1311
                $this->headers->remove('Cache-Control');
1312
            }
1313
        }
1314
    }
1315
}