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