comparison vendor/symfony/http-foundation/Response.php @ 0:4c8ae668cc8c

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