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