vendor/symfony/http-foundation/Response.php line 1240

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