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