Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | 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
 
688 lars 761
        if (null !== $expires = $this->getExpires()) {
762
            $maxAge = (int) $expires->format('U') - (int) $this->getDate()->format('U');
763
 
764
            return max($maxAge, 0);
148 lars 765
        }
766
 
767
        return null;
768
    }
769
 
770
    /**
771
     * Sets the number of seconds after which the response should no longer be considered fresh.
772
     *
773
     * This methods sets the Cache-Control max-age directive.
774
     *
775
     * @return $this
776
     *
777
     * @final
778
     */
779
    public function setMaxAge(int $value): static
780
    {
781
        $this->headers->addCacheControlDirective('max-age', $value);
782
 
783
        return $this;
784
    }
785
 
786
    /**
787
     * Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down.
788
     *
789
     * This method sets the Cache-Control stale-if-error directive.
790
     *
791
     * @return $this
792
     *
793
     * @final
794
     */
795
    public function setStaleIfError(int $value): static
796
    {
797
        $this->headers->addCacheControlDirective('stale-if-error', $value);
798
 
799
        return $this;
800
    }
801
 
802
    /**
803
     * Sets the number of seconds after which the response should no longer return stale content by shared caches.
804
     *
805
     * This method sets the Cache-Control stale-while-revalidate directive.
806
     *
807
     * @return $this
808
     *
809
     * @final
810
     */
811
    public function setStaleWhileRevalidate(int $value): static
812
    {
813
        $this->headers->addCacheControlDirective('stale-while-revalidate', $value);
814
 
815
        return $this;
816
    }
817
 
818
    /**
819
     * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
820
     *
821
     * This methods sets the Cache-Control s-maxage directive.
822
     *
823
     * @return $this
824
     *
825
     * @final
826
     */
827
    public function setSharedMaxAge(int $value): static
828
    {
829
        $this->setPublic();
830
        $this->headers->addCacheControlDirective('s-maxage', $value);
831
 
832
        return $this;
833
    }
834
 
835
    /**
836
     * Returns the response's time-to-live in seconds.
837
     *
838
     * It returns null when no freshness information is present in the response.
839
     *
688 lars 840
     * When the response's TTL is 0, the response may not be served from cache without first
148 lars 841
     * revalidating with the origin.
842
     *
843
     * @final
844
     */
845
    public function getTtl(): ?int
846
    {
847
        $maxAge = $this->getMaxAge();
848
 
688 lars 849
        return null !== $maxAge ? max($maxAge - $this->getAge(), 0) : null;
148 lars 850
    }
851
 
852
    /**
853
     * Sets the response's time-to-live for shared caches in seconds.
854
     *
855
     * This method adjusts the Cache-Control/s-maxage directive.
856
     *
857
     * @return $this
858
     *
859
     * @final
860
     */
861
    public function setTtl(int $seconds): static
862
    {
863
        $this->setSharedMaxAge($this->getAge() + $seconds);
864
 
865
        return $this;
866
    }
867
 
868
    /**
869
     * Sets the response's time-to-live for private/client caches in seconds.
870
     *
871
     * This method adjusts the Cache-Control/max-age directive.
872
     *
873
     * @return $this
874
     *
875
     * @final
876
     */
877
    public function setClientTtl(int $seconds): static
878
    {
879
        $this->setMaxAge($this->getAge() + $seconds);
880
 
881
        return $this;
882
    }
883
 
884
    /**
885
     * Returns the Last-Modified HTTP header as a DateTime instance.
886
     *
887
     * @throws \RuntimeException When the HTTP header is not parseable
888
     *
889
     * @final
890
     */
891
    public function getLastModified(): ?\DateTimeInterface
892
    {
893
        return $this->headers->getDate('Last-Modified');
894
    }
895
 
896
    /**
897
     * Sets the Last-Modified HTTP header with a DateTime instance.
898
     *
899
     * Passing null as value will remove the header.
900
     *
901
     * @return $this
902
     *
903
     * @final
904
     */
905
    public function setLastModified(\DateTimeInterface $date = null): static
906
    {
907
        if (1 > \func_num_args()) {
908
            trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
909
        }
910
        if (null === $date) {
911
            $this->headers->remove('Last-Modified');
912
 
913
            return $this;
914
        }
915
 
916
        if ($date instanceof \DateTime) {
917
            $date = \DateTimeImmutable::createFromMutable($date);
918
        }
919
 
920
        $date = $date->setTimezone(new \DateTimeZone('UTC'));
921
        $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
922
 
923
        return $this;
924
    }
925
 
926
    /**
927
     * Returns the literal value of the ETag HTTP header.
928
     *
929
     * @final
930
     */
931
    public function getEtag(): ?string
932
    {
933
        return $this->headers->get('ETag');
934
    }
935
 
936
    /**
937
     * Sets the ETag value.
938
     *
939
     * @param string|null $etag The ETag unique identifier or null to remove the header
940
     * @param bool        $weak Whether you want a weak ETag or not
941
     *
942
     * @return $this
943
     *
944
     * @final
945
     */
946
    public function setEtag(string $etag = null, bool $weak = false): static
947
    {
948
        if (1 > \func_num_args()) {
949
            trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
950
        }
951
        if (null === $etag) {
952
            $this->headers->remove('Etag');
953
        } else {
954
            if (!str_starts_with($etag, '"')) {
955
                $etag = '"'.$etag.'"';
956
            }
957
 
958
            $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
959
        }
960
 
961
        return $this;
962
    }
963
 
964
    /**
965
     * Sets the response's cache headers (validation and/or expiration).
966
     *
967
     * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
968
     *
969
     * @return $this
970
     *
971
     * @throws \InvalidArgumentException
972
     *
973
     * @final
974
     */
975
    public function setCache(array $options): static
976
    {
977
        if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) {
978
            throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', $diff)));
979
        }
980
 
981
        if (isset($options['etag'])) {
982
            $this->setEtag($options['etag']);
983
        }
984
 
985
        if (isset($options['last_modified'])) {
986
            $this->setLastModified($options['last_modified']);
987
        }
988
 
989
        if (isset($options['max_age'])) {
990
            $this->setMaxAge($options['max_age']);
991
        }
992
 
993
        if (isset($options['s_maxage'])) {
994
            $this->setSharedMaxAge($options['s_maxage']);
995
        }
996
 
997
        if (isset($options['stale_while_revalidate'])) {
998
            $this->setStaleWhileRevalidate($options['stale_while_revalidate']);
999
        }
1000
 
1001
        if (isset($options['stale_if_error'])) {
1002
            $this->setStaleIfError($options['stale_if_error']);
1003
        }
1004
 
1005
        foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) {
1006
            if (!$hasValue && isset($options[$directive])) {
1007
                if ($options[$directive]) {
1008
                    $this->headers->addCacheControlDirective(str_replace('_', '-', $directive));
1009
                } else {
1010
                    $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive));
1011
                }
1012
            }
1013
        }
1014
 
1015
        if (isset($options['public'])) {
1016
            if ($options['public']) {
1017
                $this->setPublic();
1018
            } else {
1019
                $this->setPrivate();
1020
            }
1021
        }
1022
 
1023
        if (isset($options['private'])) {
1024
            if ($options['private']) {
1025
                $this->setPrivate();
1026
            } else {
1027
                $this->setPublic();
1028
            }
1029
        }
1030
 
1031
        return $this;
1032
    }
1033
 
1034
    /**
1035
     * Modifies the response so that it conforms to the rules defined for a 304 status code.
1036
     *
1037
     * This sets the status, removes the body, and discards any headers
1038
     * that MUST NOT be included in 304 responses.
1039
     *
1040
     * @return $this
1041
     *
1042
     * @see https://tools.ietf.org/html/rfc2616#section-10.3.5
1043
     *
1044
     * @final
1045
     */
1046
    public function setNotModified(): static
1047
    {
1048
        $this->setStatusCode(304);
1049
        $this->setContent(null);
1050
 
1051
        // remove headers that MUST NOT be included with 304 Not Modified responses
1052
        foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) {
1053
            $this->headers->remove($header);
1054
        }
1055
 
1056
        return $this;
1057
    }
1058
 
1059
    /**
1060
     * Returns true if the response includes a Vary header.
1061
     *
1062
     * @final
1063
     */
1064
    public function hasVary(): bool
1065
    {
1066
        return null !== $this->headers->get('Vary');
1067
    }
1068
 
1069
    /**
1070
     * Returns an array of header names given in the Vary header.
1071
     *
1072
     * @final
1073
     */
1074
    public function getVary(): array
1075
    {
1076
        if (!$vary = $this->headers->all('Vary')) {
1077
            return [];
1078
        }
1079
 
1080
        $ret = [];
1081
        foreach ($vary as $item) {
1082
            $ret[] = preg_split('/[\s,]+/', $item);
1083
        }
1084
 
1085
        return array_merge([], ...$ret);
1086
    }
1087
 
1088
    /**
1089
     * Sets the Vary header.
1090
     *
1091
     * @param bool $replace Whether to replace the actual value or not (true by default)
1092
     *
1093
     * @return $this
1094
     *
1095
     * @final
1096
     */
1097
    public function setVary(string|array $headers, bool $replace = true): static
1098
    {
1099
        $this->headers->set('Vary', $headers, $replace);
1100
 
1101
        return $this;
1102
    }
1103
 
1104
    /**
1105
     * Determines if the Response validators (ETag, Last-Modified) match
1106
     * a conditional value specified in the Request.
1107
     *
1108
     * If the Response is not modified, it sets the status code to 304 and
1109
     * removes the actual content by calling the setNotModified() method.
1110
     *
1111
     * @final
1112
     */
1113
    public function isNotModified(Request $request): bool
1114
    {
1115
        if (!$request->isMethodCacheable()) {
1116
            return false;
1117
        }
1118
 
1119
        $notModified = false;
1120
        $lastModified = $this->headers->get('Last-Modified');
1121
        $modifiedSince = $request->headers->get('If-Modified-Since');
1122
 
1123
        if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) {
1124
            if (0 == strncmp($etag, 'W/', 2)) {
1125
                $etag = substr($etag, 2);
1126
            }
1127
 
1128
            // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2.
1129
            foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) {
1130
                if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) {
1131
                    $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2);
1132
                }
1133
 
1134
                if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) {
1135
                    $notModified = true;
1136
                    break;
1137
                }
1138
            }
1139
        }
1140
        // 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.
1141
        elseif ($modifiedSince && $lastModified) {
1142
            $notModified = strtotime($modifiedSince) >= strtotime($lastModified);
1143
        }
1144
 
1145
        if ($notModified) {
1146
            $this->setNotModified();
1147
        }
1148
 
1149
        return $notModified;
1150
    }
1151
 
1152
    /**
1153
     * Is response invalid?
1154
     *
1155
     * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1156
     *
1157
     * @final
1158
     */
1159
    public function isInvalid(): bool
1160
    {
1161
        return $this->statusCode < 100 || $this->statusCode >= 600;
1162
    }
1163
 
1164
    /**
1165
     * Is response informative?
1166
     *
1167
     * @final
1168
     */
1169
    public function isInformational(): bool
1170
    {
1171
        return $this->statusCode >= 100 && $this->statusCode < 200;
1172
    }
1173
 
1174
    /**
1175
     * Is response successful?
1176
     *
1177
     * @final
1178
     */
1179
    public function isSuccessful(): bool
1180
    {
1181
        return $this->statusCode >= 200 && $this->statusCode < 300;
1182
    }
1183
 
1184
    /**
1185
     * Is the response a redirect?
1186
     *
1187
     * @final
1188
     */
1189
    public function isRedirection(): bool
1190
    {
1191
        return $this->statusCode >= 300 && $this->statusCode < 400;
1192
    }
1193
 
1194
    /**
1195
     * Is there a client error?
1196
     *
1197
     * @final
1198
     */
1199
    public function isClientError(): bool
1200
    {
1201
        return $this->statusCode >= 400 && $this->statusCode < 500;
1202
    }
1203
 
1204
    /**
1205
     * Was there a server side error?
1206
     *
1207
     * @final
1208
     */
1209
    public function isServerError(): bool
1210
    {
1211
        return $this->statusCode >= 500 && $this->statusCode < 600;
1212
    }
1213
 
1214
    /**
1215
     * Is the response OK?
1216
     *
1217
     * @final
1218
     */
1219
    public function isOk(): bool
1220
    {
1221
        return 200 === $this->statusCode;
1222
    }
1223
 
1224
    /**
1225
     * Is the response forbidden?
1226
     *
1227
     * @final
1228
     */
1229
    public function isForbidden(): bool
1230
    {
1231
        return 403 === $this->statusCode;
1232
    }
1233
 
1234
    /**
1235
     * Is the response a not found error?
1236
     *
1237
     * @final
1238
     */
1239
    public function isNotFound(): bool
1240
    {
1241
        return 404 === $this->statusCode;
1242
    }
1243
 
1244
    /**
1245
     * Is the response a redirect of some form?
1246
     *
1247
     * @final
1248
     */
1249
    public function isRedirect(string $location = null): bool
1250
    {
1251
        return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
1252
    }
1253
 
1254
    /**
1255
     * Is the response empty?
1256
     *
1257
     * @final
1258
     */
1259
    public function isEmpty(): bool
1260
    {
1261
        return \in_array($this->statusCode, [204, 304]);
1262
    }
1263
 
1264
    /**
1265
     * Cleans or flushes output buffers up to target level.
1266
     *
1267
     * Resulting level can be greater than target level if a non-removable buffer has been encountered.
1268
     *
1269
     * @final
1270
     */
1271
    public static function closeOutputBuffers(int $targetLevel, bool $flush): void
1272
    {
1273
        $status = ob_get_status(true);
1274
        $level = \count($status);
1275
        $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE);
1276
 
1277
        while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) {
1278
            if ($flush) {
1279
                ob_end_flush();
1280
            } else {
1281
                ob_end_clean();
1282
            }
1283
        }
1284
    }
1285
 
1286
    /**
1287
     * Marks a response as safe according to RFC8674.
1288
     *
1289
     * @see https://tools.ietf.org/html/rfc8674
1290
     */
1291
    public function setContentSafe(bool $safe = true): void
1292
    {
1293
        if ($safe) {
1294
            $this->headers->set('Preference-Applied', 'safe');
1295
        } elseif ('safe' === $this->headers->get('Preference-Applied')) {
1296
            $this->headers->remove('Preference-Applied');
1297
        }
1298
 
1299
        $this->setVary('Prefer', false);
1300
    }
1301
 
1302
    /**
1303
     * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
1304
     *
1305
     * @see http://support.microsoft.com/kb/323308
1306
     *
1307
     * @final
1308
     */
1309
    protected function ensureIEOverSSLCompatibility(Request $request): void
1310
    {
1311
        if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) {
1312
            if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) {
1313
                $this->headers->remove('Cache-Control');
1314
            }
1315
        }
1316
    }
1317
}