Response.php 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpFoundation;
  11. /**
  12. * Response represents an HTTP response.
  13. *
  14. * @author Fabien Potencier <fabien@symfony.com>
  15. *
  16. * @api
  17. */
  18. class Response
  19. {
  20. /**
  21. * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag
  22. */
  23. public $headers;
  24. /**
  25. * @var string
  26. */
  27. protected $content;
  28. /**
  29. * @var string
  30. */
  31. protected $version;
  32. /**
  33. * @var integer
  34. */
  35. protected $statusCode;
  36. /**
  37. * @var string
  38. */
  39. protected $statusText;
  40. /**
  41. * @var string
  42. */
  43. protected $charset;
  44. /**
  45. * Status codes translation table.
  46. *
  47. * The list of codes is complete according to the
  48. * {@link http://www.iana.org/assignments/http-status-codes/ Hypertext Transfer Protocol (HTTP) Status Code Registry}
  49. * (last updated 2012-02-13).
  50. *
  51. * Unless otherwise noted, the status code is defined in RFC2616.
  52. *
  53. * @var array
  54. */
  55. static public $statusTexts = array(
  56. 100 => 'Continue',
  57. 101 => 'Switching Protocols',
  58. 102 => 'Processing', // RFC2518
  59. 200 => 'OK',
  60. 201 => 'Created',
  61. 202 => 'Accepted',
  62. 203 => 'Non-Authoritative Information',
  63. 204 => 'No Content',
  64. 205 => 'Reset Content',
  65. 206 => 'Partial Content',
  66. 207 => 'Multi-Status', // RFC4918
  67. 208 => 'Already Reported', // RFC5842
  68. 226 => 'IM Used', // RFC3229
  69. 300 => 'Multiple Choices',
  70. 301 => 'Moved Permanently',
  71. 302 => 'Found',
  72. 303 => 'See Other',
  73. 304 => 'Not Modified',
  74. 305 => 'Use Proxy',
  75. 306 => 'Reserved',
  76. 307 => 'Temporary Redirect',
  77. 400 => 'Bad Request',
  78. 401 => 'Unauthorized',
  79. 402 => 'Payment Required',
  80. 403 => 'Forbidden',
  81. 404 => 'Not Found',
  82. 405 => 'Method Not Allowed',
  83. 406 => 'Not Acceptable',
  84. 407 => 'Proxy Authentication Required',
  85. 408 => 'Request Timeout',
  86. 409 => 'Conflict',
  87. 410 => 'Gone',
  88. 411 => 'Length Required',
  89. 412 => 'Precondition Failed',
  90. 413 => 'Request Entity Too Large',
  91. 414 => 'Request-URI Too Long',
  92. 415 => 'Unsupported Media Type',
  93. 416 => 'Requested Range Not Satisfiable',
  94. 417 => 'Expectation Failed',
  95. 418 => 'I\'m a teapot',
  96. 422 => 'Unprocessable Entity', // RFC4918
  97. 423 => 'Locked', // RFC4918
  98. 424 => 'Failed Dependency', // RFC4918
  99. 425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817
  100. 426 => 'Upgrade Required', // RFC2817
  101. 428 => 'Precondition Required', // RFC-nottingham-http-new-status-04
  102. 429 => 'Too Many Requests', // RFC-nottingham-http-new-status-04
  103. 431 => 'Request Header Fields Too Large', // RFC-nottingham-http-new-status-04
  104. 500 => 'Internal Server Error',
  105. 501 => 'Not Implemented',
  106. 502 => 'Bad Gateway',
  107. 503 => 'Service Unavailable',
  108. 504 => 'Gateway Timeout',
  109. 505 => 'HTTP Version Not Supported',
  110. 506 => 'Variant Also Negotiates (Experimental)', // [RFC2295]
  111. 507 => 'Insufficient Storage', // RFC4918
  112. 508 => 'Loop Detected', // RFC5842
  113. 510 => 'Not Extended', // RFC2774
  114. 511 => 'Network Authentication Required', // RFC-nottingham-http-new-status-04
  115. );
  116. /**
  117. * Constructor.
  118. *
  119. * @param string $content The response content
  120. * @param integer $status The response status code
  121. * @param array $headers An array of response headers
  122. *
  123. * @api
  124. */
  125. public function __construct($content = '', $status = 200, $headers = array())
  126. {
  127. $this->headers = new ResponseHeaderBag($headers);
  128. $this->setContent($content);
  129. $this->setStatusCode($status);
  130. $this->setProtocolVersion('1.0');
  131. if (!$this->headers->has('Date')) {
  132. $this->setDate(new \DateTime(null, new \DateTimeZone('UTC')));
  133. }
  134. }
  135. /**
  136. * Factory method for chainability
  137. *
  138. * Example:
  139. *
  140. * return Response::create($body, 200)
  141. * ->setSharedMaxAge(300);
  142. *
  143. * @param string $content The response content
  144. * @param integer $status The response status code
  145. * @param array $headers An array of response headers
  146. *
  147. * @return Response
  148. */
  149. static public function create($content = '', $status = 200, $headers = array())
  150. {
  151. return new static($content, $status, $headers);
  152. }
  153. /**
  154. * Returns the Response as an HTTP string.
  155. *
  156. * The string representation of the Resonse is the same as the
  157. * one that will be sent to the client only if the prepare() method
  158. * has been called before.
  159. *
  160. * @return string The Response as an HTTP string
  161. *
  162. * @see prepare()
  163. */
  164. public function __toString()
  165. {
  166. return
  167. sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
  168. $this->headers."\r\n".
  169. $this->getContent();
  170. }
  171. /**
  172. * Clones the current Response instance.
  173. */
  174. public function __clone()
  175. {
  176. $this->headers = clone $this->headers;
  177. }
  178. /**
  179. * Prepares the Response before it is sent to the client.
  180. *
  181. * This method tweaks the Response to ensure that it is
  182. * compliant with RFC 2616. Most of the changes are based on
  183. * the Request that is "associated" with this Response.
  184. *
  185. * @param Request $request A Request instance
  186. */
  187. public function prepare(Request $request)
  188. {
  189. $headers = $this->headers;
  190. if ($this->isInformational() || in_array($this->statusCode, array(204, 304))) {
  191. $this->setContent('');
  192. }
  193. // Content-type based on the Request
  194. if (!$headers->has('Content-Type')) {
  195. $format = $request->getRequestFormat();
  196. if (null !== $format && $mimeType = $request->getMimeType($format)) {
  197. $headers->set('Content-Type', $mimeType);
  198. }
  199. }
  200. // Fix Content-Type
  201. $charset = $this->charset ?: 'UTF-8';
  202. if (!$headers->has('Content-Type')) {
  203. $headers->set('Content-Type', 'text/html; charset='.$charset);
  204. } elseif (0 === strpos($headers->get('Content-Type'), 'text/') && false === strpos($headers->get('Content-Type'), 'charset')) {
  205. // add the charset
  206. $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset);
  207. }
  208. // Fix Content-Length
  209. if ($headers->has('Transfer-Encoding')) {
  210. $headers->remove('Content-Length');
  211. }
  212. if ('HEAD' === $request->getMethod()) {
  213. // cf. RFC2616 14.13
  214. $length = $headers->get('Content-Length');
  215. $this->setContent('');
  216. if ($length) {
  217. $headers->set('Content-Length', $length);
  218. }
  219. }
  220. }
  221. /**
  222. * Sends HTTP headers.
  223. *
  224. * @return Response
  225. */
  226. public function sendHeaders()
  227. {
  228. // headers have already been sent by the developer
  229. if (headers_sent()) {
  230. return $this;
  231. }
  232. // status
  233. header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
  234. // headers
  235. foreach ($this->headers->all() as $name => $values) {
  236. foreach ($values as $value) {
  237. header($name.': '.$value, false);
  238. }
  239. }
  240. // cookies
  241. foreach ($this->headers->getCookies() as $cookie) {
  242. setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
  243. }
  244. return $this;
  245. }
  246. /**
  247. * Sends content for the current web response.
  248. *
  249. * @return Response
  250. */
  251. public function sendContent()
  252. {
  253. echo $this->content;
  254. return $this;
  255. }
  256. /**
  257. * Sends HTTP headers and content.
  258. *
  259. * @return Response
  260. *
  261. * @api
  262. */
  263. public function send()
  264. {
  265. $this->sendHeaders();
  266. $this->sendContent();
  267. if (function_exists('fastcgi_finish_request')) {
  268. fastcgi_finish_request();
  269. }
  270. return $this;
  271. }
  272. /**
  273. * Sets the response content.
  274. *
  275. * Valid types are strings, numbers, and objects that implement a __toString() method.
  276. *
  277. * @param mixed $content
  278. *
  279. * @return Response
  280. *
  281. * @api
  282. */
  283. public function setContent($content)
  284. {
  285. if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) {
  286. throw new \UnexpectedValueException('The Response content must be a string or object implementing __toString(), "'.gettype($content).'" given.');
  287. }
  288. $this->content = (string) $content;
  289. return $this;
  290. }
  291. /**
  292. * Gets the current response content.
  293. *
  294. * @return string Content
  295. *
  296. * @api
  297. */
  298. public function getContent()
  299. {
  300. return $this->content;
  301. }
  302. /**
  303. * Sets the HTTP protocol version (1.0 or 1.1).
  304. *
  305. * @param string $version The HTTP protocol version
  306. *
  307. * @return Response
  308. *
  309. * @api
  310. */
  311. public function setProtocolVersion($version)
  312. {
  313. $this->version = $version;
  314. return $this;
  315. }
  316. /**
  317. * Gets the HTTP protocol version.
  318. *
  319. * @return string The HTTP protocol version
  320. *
  321. * @api
  322. */
  323. public function getProtocolVersion()
  324. {
  325. return $this->version;
  326. }
  327. /**
  328. * Sets the response status code.
  329. *
  330. * @param integer $code HTTP status code
  331. * @param string $text HTTP status text
  332. *
  333. * @return Response
  334. *
  335. * @throws \InvalidArgumentException When the HTTP status code is not valid
  336. *
  337. * @api
  338. */
  339. public function setStatusCode($code, $text = null)
  340. {
  341. $this->statusCode = (int) $code;
  342. if ($this->isInvalid()) {
  343. throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
  344. }
  345. $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
  346. return $this;
  347. }
  348. /**
  349. * Retrieves the status code for the current web response.
  350. *
  351. * @return string Status code
  352. *
  353. * @api
  354. */
  355. public function getStatusCode()
  356. {
  357. return $this->statusCode;
  358. }
  359. /**
  360. * Sets the response charset.
  361. *
  362. * @param string $charset Character set
  363. *
  364. * @return Response
  365. *
  366. * @api
  367. */
  368. public function setCharset($charset)
  369. {
  370. $this->charset = $charset;
  371. return $this;
  372. }
  373. /**
  374. * Retrieves the response charset.
  375. *
  376. * @return string Character set
  377. *
  378. * @api
  379. */
  380. public function getCharset()
  381. {
  382. return $this->charset;
  383. }
  384. /**
  385. * Returns true if the response is worth caching under any circumstance.
  386. *
  387. * Responses marked "private" with an explicit Cache-Control directive are
  388. * considered uncacheable.
  389. *
  390. * Responses with neither a freshness lifetime (Expires, max-age) nor cache
  391. * validator (Last-Modified, ETag) are considered uncacheable.
  392. *
  393. * @return Boolean true if the response is worth caching, false otherwise
  394. *
  395. * @api
  396. */
  397. public function isCacheable()
  398. {
  399. if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) {
  400. return false;
  401. }
  402. if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) {
  403. return false;
  404. }
  405. return $this->isValidateable() || $this->isFresh();
  406. }
  407. /**
  408. * Returns true if the response is "fresh".
  409. *
  410. * Fresh responses may be served from cache without any interaction with the
  411. * origin. A response is considered fresh when it includes a Cache-Control/max-age
  412. * indicator or Expiration header and the calculated age is less than the freshness lifetime.
  413. *
  414. * @return Boolean true if the response is fresh, false otherwise
  415. *
  416. * @api
  417. */
  418. public function isFresh()
  419. {
  420. return $this->getTtl() > 0;
  421. }
  422. /**
  423. * Returns true if the response includes headers that can be used to validate
  424. * the response with the origin server using a conditional GET request.
  425. *
  426. * @return Boolean true if the response is validateable, false otherwise
  427. *
  428. * @api
  429. */
  430. public function isValidateable()
  431. {
  432. return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
  433. }
  434. /**
  435. * Marks the response as "private".
  436. *
  437. * It makes the response ineligible for serving other clients.
  438. *
  439. * @return Response
  440. *
  441. * @api
  442. */
  443. public function setPrivate()
  444. {
  445. $this->headers->removeCacheControlDirective('public');
  446. $this->headers->addCacheControlDirective('private');
  447. return $this;
  448. }
  449. /**
  450. * Marks the response as "public".
  451. *
  452. * It makes the response eligible for serving other clients.
  453. *
  454. * @return Response
  455. *
  456. * @api
  457. */
  458. public function setPublic()
  459. {
  460. $this->headers->addCacheControlDirective('public');
  461. $this->headers->removeCacheControlDirective('private');
  462. return $this;
  463. }
  464. /**
  465. * Returns true if the response must be revalidated by caches.
  466. *
  467. * This method indicates that the response must not be served stale by a
  468. * cache in any circumstance without first revalidating with the origin.
  469. * When present, the TTL of the response should not be overridden to be
  470. * greater than the value provided by the origin.
  471. *
  472. * @return Boolean true if the response must be revalidated by a cache, false otherwise
  473. *
  474. * @api
  475. */
  476. public function mustRevalidate()
  477. {
  478. return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->has('must-proxy-revalidate');
  479. }
  480. /**
  481. * Returns the Date header as a DateTime instance.
  482. *
  483. * @return \DateTime A \DateTime instance
  484. *
  485. * @throws \RuntimeException When the header is not parseable
  486. *
  487. * @api
  488. */
  489. public function getDate()
  490. {
  491. return $this->headers->getDate('Date');
  492. }
  493. /**
  494. * Sets the Date header.
  495. *
  496. * @param \DateTime $date A \DateTime instance
  497. *
  498. * @return Response
  499. *
  500. * @api
  501. */
  502. public function setDate(\DateTime $date)
  503. {
  504. $date->setTimezone(new \DateTimeZone('UTC'));
  505. $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT');
  506. return $this;
  507. }
  508. /**
  509. * Returns the age of the response.
  510. *
  511. * @return integer The age of the response in seconds
  512. */
  513. public function getAge()
  514. {
  515. if ($age = $this->headers->get('Age')) {
  516. return $age;
  517. }
  518. return max(time() - $this->getDate()->format('U'), 0);
  519. }
  520. /**
  521. * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
  522. *
  523. * @return Response
  524. *
  525. * @api
  526. */
  527. public function expire()
  528. {
  529. if ($this->isFresh()) {
  530. $this->headers->set('Age', $this->getMaxAge());
  531. }
  532. return $this;
  533. }
  534. /**
  535. * Returns the value of the Expires header as a DateTime instance.
  536. *
  537. * @return \DateTime A DateTime instance
  538. *
  539. * @api
  540. */
  541. public function getExpires()
  542. {
  543. return $this->headers->getDate('Expires');
  544. }
  545. /**
  546. * Sets the Expires HTTP header with a DateTime instance.
  547. *
  548. * If passed a null value, it removes the header.
  549. *
  550. * @param \DateTime $date A \DateTime instance
  551. *
  552. * @return Response
  553. *
  554. * @api
  555. */
  556. public function setExpires(\DateTime $date = null)
  557. {
  558. if (null === $date) {
  559. $this->headers->remove('Expires');
  560. } else {
  561. $date = clone $date;
  562. $date->setTimezone(new \DateTimeZone('UTC'));
  563. $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT');
  564. }
  565. return $this;
  566. }
  567. /**
  568. * Sets the number of seconds after the time specified in the response's Date
  569. * header when the the response should no longer be considered fresh.
  570. *
  571. * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
  572. * back on an expires header. It returns null when no maximum age can be established.
  573. *
  574. * @return integer|null Number of seconds
  575. *
  576. * @api
  577. */
  578. public function getMaxAge()
  579. {
  580. if ($age = $this->headers->getCacheControlDirective('s-maxage')) {
  581. return $age;
  582. }
  583. if ($age = $this->headers->getCacheControlDirective('max-age')) {
  584. return $age;
  585. }
  586. if (null !== $this->getExpires()) {
  587. return $this->getExpires()->format('U') - $this->getDate()->format('U');
  588. }
  589. return null;
  590. }
  591. /**
  592. * Sets the number of seconds after which the response should no longer be considered fresh.
  593. *
  594. * This methods sets the Cache-Control max-age directive.
  595. *
  596. * @param integer $value Number of seconds
  597. *
  598. * @return Response
  599. *
  600. * @api
  601. */
  602. public function setMaxAge($value)
  603. {
  604. $this->headers->addCacheControlDirective('max-age', $value);
  605. return $this;
  606. }
  607. /**
  608. * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
  609. *
  610. * This methods sets the Cache-Control s-maxage directive.
  611. *
  612. * @param integer $value Number of seconds
  613. *
  614. * @return Response
  615. *
  616. * @api
  617. */
  618. public function setSharedMaxAge($value)
  619. {
  620. $this->setPublic();
  621. $this->headers->addCacheControlDirective('s-maxage', $value);
  622. return $this;
  623. }
  624. /**
  625. * Returns the response's time-to-live in seconds.
  626. *
  627. * It returns null when no freshness information is present in the response.
  628. *
  629. * When the responses TTL is <= 0, the response may not be served from cache without first
  630. * revalidating with the origin.
  631. *
  632. * @return integer The TTL in seconds
  633. *
  634. * @api
  635. */
  636. public function getTtl()
  637. {
  638. if ($maxAge = $this->getMaxAge()) {
  639. return $maxAge - $this->getAge();
  640. }
  641. return null;
  642. }
  643. /**
  644. * Sets the response's time-to-live for shared caches.
  645. *
  646. * This method adjusts the Cache-Control/s-maxage directive.
  647. *
  648. * @param integer $seconds Number of seconds
  649. *
  650. * @return Response
  651. *
  652. * @api
  653. */
  654. public function setTtl($seconds)
  655. {
  656. $this->setSharedMaxAge($this->getAge() + $seconds);
  657. return $this;
  658. }
  659. /**
  660. * Sets the response's time-to-live for private/client caches.
  661. *
  662. * This method adjusts the Cache-Control/max-age directive.
  663. *
  664. * @param integer $seconds Number of seconds
  665. *
  666. * @return Response
  667. *
  668. * @api
  669. */
  670. public function setClientTtl($seconds)
  671. {
  672. $this->setMaxAge($this->getAge() + $seconds);
  673. return $this;
  674. }
  675. /**
  676. * Returns the Last-Modified HTTP header as a DateTime instance.
  677. *
  678. * @return \DateTime A DateTime instance
  679. *
  680. * @api
  681. */
  682. public function getLastModified()
  683. {
  684. return $this->headers->getDate('Last-Modified');
  685. }
  686. /**
  687. * Sets the Last-Modified HTTP header with a DateTime instance.
  688. *
  689. * If passed a null value, it removes the header.
  690. *
  691. * @param \DateTime $date A \DateTime instance
  692. *
  693. * @return Response
  694. *
  695. * @api
  696. */
  697. public function setLastModified(\DateTime $date = null)
  698. {
  699. if (null === $date) {
  700. $this->headers->remove('Last-Modified');
  701. } else {
  702. $date = clone $date;
  703. $date->setTimezone(new \DateTimeZone('UTC'));
  704. $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT');
  705. }
  706. return $this;
  707. }
  708. /**
  709. * Returns the literal value of the ETag HTTP header.
  710. *
  711. * @return string The ETag HTTP header
  712. *
  713. * @api
  714. */
  715. public function getEtag()
  716. {
  717. return $this->headers->get('ETag');
  718. }
  719. /**
  720. * Sets the ETag value.
  721. *
  722. * @param string $etag The ETag unique identifier
  723. * @param Boolean $weak Whether you want a weak ETag or not
  724. *
  725. * @return Response
  726. *
  727. * @api
  728. */
  729. public function setEtag($etag = null, $weak = false)
  730. {
  731. if (null === $etag) {
  732. $this->headers->remove('Etag');
  733. } else {
  734. if (0 !== strpos($etag, '"')) {
  735. $etag = '"'.$etag.'"';
  736. }
  737. $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
  738. }
  739. return $this;
  740. }
  741. /**
  742. * Sets the response's cache headers (validation and/or expiration).
  743. *
  744. * Available options are: etag, last_modified, max_age, s_maxage, private, and public.
  745. *
  746. * @param array $options An array of cache options
  747. *
  748. * @return Response
  749. *
  750. * @api
  751. */
  752. public function setCache(array $options)
  753. {
  754. if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) {
  755. throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_values($diff))));
  756. }
  757. if (isset($options['etag'])) {
  758. $this->setEtag($options['etag']);
  759. }
  760. if (isset($options['last_modified'])) {
  761. $this->setLastModified($options['last_modified']);
  762. }
  763. if (isset($options['max_age'])) {
  764. $this->setMaxAge($options['max_age']);
  765. }
  766. if (isset($options['s_maxage'])) {
  767. $this->setSharedMaxAge($options['s_maxage']);
  768. }
  769. if (isset($options['public'])) {
  770. if ($options['public']) {
  771. $this->setPublic();
  772. } else {
  773. $this->setPrivate();
  774. }
  775. }
  776. if (isset($options['private'])) {
  777. if ($options['private']) {
  778. $this->setPrivate();
  779. } else {
  780. $this->setPublic();
  781. }
  782. }
  783. return $this;
  784. }
  785. /**
  786. * Modifies the response so that it conforms to the rules defined for a 304 status code.
  787. *
  788. * This sets the status, removes the body, and discards any headers
  789. * that MUST NOT be included in 304 responses.
  790. *
  791. * @return Response
  792. *
  793. * @see http://tools.ietf.org/html/rfc2616#section-10.3.5
  794. *
  795. * @api
  796. */
  797. public function setNotModified()
  798. {
  799. $this->setStatusCode(304);
  800. $this->setContent(null);
  801. // remove headers that MUST NOT be included with 304 Not Modified responses
  802. foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) {
  803. $this->headers->remove($header);
  804. }
  805. return $this;
  806. }
  807. /**
  808. * Returns true if the response includes a Vary header.
  809. *
  810. * @return Boolean true if the response includes a Vary header, false otherwise
  811. *
  812. * @api
  813. */
  814. public function hasVary()
  815. {
  816. return (Boolean) $this->headers->get('Vary');
  817. }
  818. /**
  819. * Returns an array of header names given in the Vary header.
  820. *
  821. * @return array An array of Vary names
  822. *
  823. * @api
  824. */
  825. public function getVary()
  826. {
  827. if (!$vary = $this->headers->get('Vary')) {
  828. return array();
  829. }
  830. return is_array($vary) ? $vary : preg_split('/[\s,]+/', $vary);
  831. }
  832. /**
  833. * Sets the Vary header.
  834. *
  835. * @param string|array $headers
  836. * @param Boolean $replace Whether to replace the actual value of not (true by default)
  837. *
  838. * @return Response
  839. *
  840. * @api
  841. */
  842. public function setVary($headers, $replace = true)
  843. {
  844. $this->headers->set('Vary', $headers, $replace);
  845. return $this;
  846. }
  847. /**
  848. * Determines if the Response validators (ETag, Last-Modified) match
  849. * a conditional value specified in the Request.
  850. *
  851. * If the Response is not modified, it sets the status code to 304 and
  852. * removes the actual content by calling the setNotModified() method.
  853. *
  854. * @param Request $request A Request instance
  855. *
  856. * @return Boolean true if the Response validators match the Request, false otherwise
  857. *
  858. * @api
  859. */
  860. public function isNotModified(Request $request)
  861. {
  862. $lastModified = $request->headers->get('If-Modified-Since');
  863. $notModified = false;
  864. if ($etags = $request->getEtags()) {
  865. $notModified = (in_array($this->getEtag(), $etags) || in_array('*', $etags)) && (!$lastModified || $this->headers->get('Last-Modified') == $lastModified);
  866. } elseif ($lastModified) {
  867. $notModified = $lastModified == $this->headers->get('Last-Modified');
  868. }
  869. if ($notModified) {
  870. $this->setNotModified();
  871. }
  872. return $notModified;
  873. }
  874. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  875. /**
  876. * Is response invalid?
  877. *
  878. * @return Boolean
  879. *
  880. * @api
  881. */
  882. public function isInvalid()
  883. {
  884. return $this->statusCode < 100 || $this->statusCode >= 600;
  885. }
  886. /**
  887. * Is response informative?
  888. *
  889. * @return Boolean
  890. *
  891. * @api
  892. */
  893. public function isInformational()
  894. {
  895. return $this->statusCode >= 100 && $this->statusCode < 200;
  896. }
  897. /**
  898. * Is response successful?
  899. *
  900. * @return Boolean
  901. *
  902. * @api
  903. */
  904. public function isSuccessful()
  905. {
  906. return $this->statusCode >= 200 && $this->statusCode < 300;
  907. }
  908. /**
  909. * Is the response a redirect?
  910. *
  911. * @return Boolean
  912. *
  913. * @api
  914. */
  915. public function isRedirection()
  916. {
  917. return $this->statusCode >= 300 && $this->statusCode < 400;
  918. }
  919. /**
  920. * Is there a client error?
  921. *
  922. * @return Boolean
  923. *
  924. * @api
  925. */
  926. public function isClientError()
  927. {
  928. return $this->statusCode >= 400 && $this->statusCode < 500;
  929. }
  930. /**
  931. * Was there a server side error?
  932. *
  933. * @return Boolean
  934. *
  935. * @api
  936. */
  937. public function isServerError()
  938. {
  939. return $this->statusCode >= 500 && $this->statusCode < 600;
  940. }
  941. /**
  942. * Is the response OK?
  943. *
  944. * @return Boolean
  945. *
  946. * @api
  947. */
  948. public function isOk()
  949. {
  950. return 200 === $this->statusCode;
  951. }
  952. /**
  953. * Is the reponse forbidden?
  954. *
  955. * @return Boolean
  956. *
  957. * @api
  958. */
  959. public function isForbidden()
  960. {
  961. return 403 === $this->statusCode;
  962. }
  963. /**
  964. * Is the response a not found error?
  965. *
  966. * @return Boolean
  967. *
  968. * @api
  969. */
  970. public function isNotFound()
  971. {
  972. return 404 === $this->statusCode;
  973. }
  974. /**
  975. * Is the response a redirect of some form?
  976. *
  977. * @param string $location
  978. *
  979. * @return Boolean
  980. *
  981. * @api
  982. */
  983. public function isRedirect($location = null)
  984. {
  985. return in_array($this->statusCode, array(201, 301, 302, 303, 307)) && (null === $location ?: $location == $this->headers->get('Location'));
  986. }
  987. /**
  988. * Is the response empty?
  989. *
  990. * @return Boolean
  991. *
  992. * @api
  993. */
  994. public function isEmpty()
  995. {
  996. return in_array($this->statusCode, array(201, 204, 304));
  997. }
  998. }