Subversion Repositories php-qbpwcf

Rev

Details | Last modification | View Log | RSS feed

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