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

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