Response.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <?php
  2. namespace Symfony\Components\HttpFoundation;
  3. /*
  4. * This file is part of the Symfony package.
  5. *
  6. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. /**
  12. * Response represents an HTTP response.
  13. *
  14. * @package Symfony
  15. * @subpackage Components_HttpFoundation
  16. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  17. */
  18. class Response
  19. {
  20. /**
  21. * @var \Symfony\Components\HttpFoundation\HeaderBag
  22. */
  23. public $headers;
  24. protected $content;
  25. protected $version;
  26. protected $statusCode;
  27. protected $statusText;
  28. static public $statusTexts = array(
  29. 100 => 'Continue',
  30. 101 => 'Switching Protocols',
  31. 200 => 'OK',
  32. 201 => 'Created',
  33. 202 => 'Accepted',
  34. 203 => 'Non-Authoritative Information',
  35. 204 => 'No Content',
  36. 205 => 'Reset Content',
  37. 206 => 'Partial Content',
  38. 300 => 'Multiple Choices',
  39. 301 => 'Moved Permanently',
  40. 302 => 'Found',
  41. 303 => 'See Other',
  42. 304 => 'Not Modified',
  43. 305 => 'Use Proxy',
  44. 307 => 'Temporary Redirect',
  45. 400 => 'Bad Request',
  46. 401 => 'Unauthorized',
  47. 402 => 'Payment Required',
  48. 403 => 'Forbidden',
  49. 404 => 'Not Found',
  50. 405 => 'Method Not Allowed',
  51. 406 => 'Not Acceptable',
  52. 407 => 'Proxy Authentication Required',
  53. 408 => 'Request Timeout',
  54. 409 => 'Conflict',
  55. 410 => 'Gone',
  56. 411 => 'Length Required',
  57. 412 => 'Precondition Failed',
  58. 413 => 'Request Entity Too Large',
  59. 414 => 'Request-URI Too Long',
  60. 415 => 'Unsupported Media Type',
  61. 416 => 'Requested Range Not Satisfiable',
  62. 417 => 'Expectation Failed',
  63. 500 => 'Internal Server Error',
  64. 501 => 'Not Implemented',
  65. 502 => 'Bad Gateway',
  66. 503 => 'Service Unavailable',
  67. 504 => 'Gateway Timeout',
  68. 505 => 'HTTP Version Not Supported',
  69. );
  70. /**
  71. * Constructor.
  72. *
  73. * @param string $content The response content
  74. * @param integer $status The response status code
  75. * @param array $headers An array of response headers
  76. */
  77. public function __construct($content = '', $status = 200, $headers = array())
  78. {
  79. $this->setContent($content);
  80. $this->setStatusCode($status);
  81. $this->setProtocolVersion('1.0');
  82. $this->headers = new HeaderBag($headers, 'response');
  83. }
  84. /**
  85. * Returns the response content as it will be sent (with the headers).
  86. *
  87. * @return string The response content
  88. */
  89. public function __toString()
  90. {
  91. $content = '';
  92. if (!$this->headers->has('Content-Type')) {
  93. $this->headers->set('Content-Type', 'text/html');
  94. }
  95. // status
  96. $content .= sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\n";
  97. // headers
  98. foreach ($this->headers->all() as $name => $values) {
  99. foreach ($values as $value) {
  100. $content .= "$name: $value\n";
  101. }
  102. }
  103. $content .= "\n".$this->getContent();
  104. return $content;
  105. }
  106. /**
  107. * Clones the current Response instance.
  108. */
  109. public function __clone()
  110. {
  111. $this->headers = clone $this->headers;
  112. }
  113. /**
  114. * Sends HTTP headers.
  115. */
  116. public function sendHeaders()
  117. {
  118. if (!$this->headers->has('Content-Type')) {
  119. $this->headers->set('Content-Type', 'text/html');
  120. }
  121. // status
  122. header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
  123. // headers
  124. foreach ($this->headers->all() as $name => $values) {
  125. foreach ($values as $value) {
  126. header($name.': '.$value);
  127. }
  128. }
  129. }
  130. /**
  131. * Sends content for the current web response.
  132. */
  133. public function sendContent()
  134. {
  135. echo $this->content;
  136. }
  137. /**
  138. * Sends HTTP headers and content.
  139. */
  140. public function send()
  141. {
  142. $this->sendHeaders();
  143. $this->sendContent();
  144. }
  145. /**
  146. * Sets the response content
  147. *
  148. * @param string $content
  149. */
  150. public function setContent($content)
  151. {
  152. $this->content = $content;
  153. }
  154. /**
  155. * Gets the current response content
  156. *
  157. * @return string Content
  158. */
  159. public function getContent()
  160. {
  161. return $this->content;
  162. }
  163. /**
  164. * Sets the HTTP protocol version (1.0 or 1.1).
  165. *
  166. * @param string $version The HTTP protocol version
  167. */
  168. public function setProtocolVersion($version)
  169. {
  170. $this->version = $version;
  171. }
  172. /**
  173. * Gets the HTTP protocol version.
  174. *
  175. * @return string The HTTP protocol version
  176. */
  177. public function getProtocolVersion()
  178. {
  179. return $this->version;
  180. }
  181. /**
  182. * Sets response status code.
  183. *
  184. * @param integer $code HTTP status code
  185. * @param string $text HTTP status text
  186. *
  187. * @throws \InvalidArgumentException When the HTTP status code is not valid
  188. */
  189. public function setStatusCode($code, $text = null)
  190. {
  191. $this->statusCode = (int) $code;
  192. if ($this->statusCode < 100 || $this->statusCode > 599) {
  193. throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code));
  194. }
  195. $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
  196. }
  197. /**
  198. * Retrieves status code for the current web response.
  199. *
  200. * @return string Status code
  201. */
  202. public function getStatusCode()
  203. {
  204. return $this->statusCode;
  205. }
  206. /**
  207. * Returns true if the response is worth caching under any circumstance.
  208. *
  209. * Responses marked "private" with an explicit Cache-Control directive are
  210. * considered uncacheable.
  211. *
  212. * Responses with neither a freshness lifetime (Expires, max-age) nor cache
  213. * validator (Last-Modified, ETag) are considered uncacheable.
  214. *
  215. * @return Boolean true if the response is worth caching, false otherwise
  216. */
  217. public function isCacheable()
  218. {
  219. if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) {
  220. return false;
  221. }
  222. if ($this->headers->getCacheControl()->isNoStore() || $this->headers->getCacheControl()->isPrivate()) {
  223. return false;
  224. }
  225. return $this->isValidateable() || $this->isFresh();
  226. }
  227. /**
  228. * Returns true if the response is "fresh".
  229. *
  230. * Fresh responses may be served from cache without any interaction with the
  231. * origin. A response is considered fresh when it includes a Cache-Control/max-age
  232. * indicator or Expiration header and the calculated age is less than the freshness lifetime.
  233. *
  234. * @return Boolean true if the response is fresh, false otherwise
  235. */
  236. public function isFresh()
  237. {
  238. return $this->getTtl() > 0;
  239. }
  240. /**
  241. * Returns true if the response includes headers that can be used to validate
  242. * the response with the origin server using a conditional GET request.
  243. *
  244. * @return Boolean true if the response is validateable, false otherwise
  245. */
  246. public function isValidateable()
  247. {
  248. return $this->headers->has('Last-Modified') || $this->headers->has('ETag');
  249. }
  250. /**
  251. * Marks the response "private".
  252. *
  253. * It makes the response ineligible for serving other clients.
  254. *
  255. * @param Boolean $value Whether to set the response to be private or public.
  256. */
  257. public function setPrivate($value)
  258. {
  259. $value = (Boolean) $value;
  260. $this->headers->getCacheControl()->setPublic(!$value);
  261. $this->headers->getCacheControl()->setPrivate($value);
  262. }
  263. /**
  264. * Returns true if the response must be revalidated by caches.
  265. *
  266. * This method indicates that the response must not be served stale by a
  267. * cache in any circumstance without first revalidating with the origin.
  268. * When present, the TTL of the response should not be overriden to be
  269. * greater than the value provided by the origin.
  270. *
  271. * @return Boolean true if the response must be revalidated by a cache, false otherwise
  272. */
  273. public function mustRevalidate()
  274. {
  275. return $this->headers->getCacheControl()->mustRevalidate() || $this->headers->getCacheControl()->mustProxyRevalidate();
  276. }
  277. /**
  278. * Returns the Date header as a DateTime instance.
  279. *
  280. * When no Date header is present, the current time is returned.
  281. *
  282. * @return \DateTime A \DateTime instance
  283. *
  284. * @throws \RuntimeException when the header is not parseable
  285. */
  286. public function getDate()
  287. {
  288. if (null === $date = $this->headers->getDate('Date')) {
  289. $date = new \DateTime();
  290. $this->headers->set('Date', $date->format(DATE_RFC2822));
  291. }
  292. return $date;
  293. }
  294. /**
  295. * Returns the age of the response.
  296. *
  297. * @return integer The age of the response in seconds
  298. */
  299. public function getAge()
  300. {
  301. if ($age = $this->headers->get('Age')) {
  302. return $age;
  303. }
  304. return max(time() - $this->getDate()->format('U'), 0);
  305. }
  306. /**
  307. * Marks the response stale by setting the Age header to be equal to the maximum age of the response.
  308. */
  309. public function expire()
  310. {
  311. if ($this->isFresh()) {
  312. $this->headers->set('Age', $this->getMaxAge());
  313. }
  314. }
  315. /**
  316. * Returns the value of the Expires header as a DateTime instance.
  317. *
  318. * @return \DateTime A DateTime instance
  319. */
  320. public function getExpires()
  321. {
  322. return $this->headers->getDate('Expires');
  323. }
  324. /**
  325. * Sets the Expires HTTP header with a \DateTime instance.
  326. *
  327. * If passed a null value, it deletes the header.
  328. *
  329. * @param \DateTime $date A \DateTime instance
  330. */
  331. public function setExpires(\DateTime $date = null)
  332. {
  333. if (null === $date) {
  334. $this->headers->delete('Expires');
  335. } else {
  336. $this->headers->set('Expires', $date->format(DATE_RFC2822));
  337. }
  338. }
  339. /**
  340. * Sets the number of seconds after the time specified in the response's Date
  341. * header when the the response should no longer be considered fresh.
  342. *
  343. * First, it checks for a s-maxage directive, then a max-age directive, and then it falls
  344. * back on an expires header. It returns null when no maximum age can be established.
  345. *
  346. * @return integer|null Number of seconds
  347. */
  348. public function getMaxAge()
  349. {
  350. if ($age = $this->headers->getCacheControl()->getSharedMaxAge()) {
  351. return $age;
  352. }
  353. if ($age = $this->headers->getCacheControl()->getMaxAge()) {
  354. return $age;
  355. }
  356. if (null !== $this->getExpires()) {
  357. return $this->getExpires()->format('U') - $this->getDate()->format('U');
  358. }
  359. return null;
  360. }
  361. /**
  362. * Sets the number of seconds after which the response should no longer be considered fresh.
  363. *
  364. * This methods sets the Cache-Control max-age directive.
  365. *
  366. * @param integer $value A number of seconds
  367. */
  368. public function setMaxAge($value)
  369. {
  370. $this->headers->getCacheControl()->setMaxAge($value);
  371. }
  372. /**
  373. * Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
  374. *
  375. * This methods sets the Cache-Control s-maxage directive.
  376. *
  377. * @param integer $value A number of seconds
  378. */
  379. public function setSharedMaxAge($value)
  380. {
  381. $this->headers->getCacheControl()->setSharedMaxAge($value);
  382. }
  383. /**
  384. * Returns the response's time-to-live in seconds.
  385. *
  386. * It returns null when no freshness information is present in the response.
  387. *
  388. * When the responses TTL is <= 0, the response may not be served from cache without first
  389. * revalidating with the origin.
  390. *
  391. * @return integer The TTL in seconds
  392. */
  393. public function getTtl()
  394. {
  395. if ($maxAge = $this->getMaxAge()) {
  396. return $maxAge - $this->getAge();
  397. }
  398. return null;
  399. }
  400. /**
  401. * Sets the response's time-to-live for shared caches.
  402. *
  403. * This method adjusts the Cache-Control/s-maxage directive.
  404. *
  405. * @param integer $seconds The number of seconds
  406. */
  407. public function setTtl($seconds)
  408. {
  409. $this->setSharedMaxAge($this->getAge() + $seconds);
  410. }
  411. /**
  412. * Sets the response's time-to-live for private/client caches.
  413. *
  414. * This method adjusts the Cache-Control/max-age directive.
  415. *
  416. * @param integer $seconds The number of seconds
  417. */
  418. public function setClientTtl($seconds)
  419. {
  420. $this->setMaxAge($this->getAge() + $seconds);
  421. }
  422. /**
  423. * Returns the Last-Modified HTTP header as a DateTime instance.
  424. *
  425. * @return \DateTime A DateTime instance
  426. */
  427. public function getLastModified()
  428. {
  429. return $this->headers->getDate('LastModified');
  430. }
  431. /**
  432. * Sets the Last-Modified HTTP header with a \DateTime instance.
  433. *
  434. * If passed a null value, it deletes the header.
  435. *
  436. * @param \DateTime $date A \DateTime instance
  437. */
  438. public function setLastModified(\DateTime $date = null)
  439. {
  440. if (null === $date) {
  441. $this->headers->delete('Last-Modified');
  442. } else {
  443. $this->headers->set('Last-Modified', $date->format(DATE_RFC2822));
  444. }
  445. }
  446. /**
  447. * Returns the literal value of ETag HTTP header.
  448. *
  449. * @return string The ETag HTTP header
  450. */
  451. public function getEtag()
  452. {
  453. return $this->headers->get('ETag');
  454. }
  455. public function setEtag($etag = null, $weak = false)
  456. {
  457. if (null === $etag) {
  458. $this->headers->delete('Etag');
  459. } else {
  460. if (0 !== strpos($etag, '"')) {
  461. $etag = '"'.$etag.'"';
  462. }
  463. $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag);
  464. }
  465. }
  466. /**
  467. * Modifies the response so that it conforms to the rules defined for a 304 status code.
  468. *
  469. * This sets the status, removes the body, and discards any headers
  470. * that MUST NOT be included in 304 responses.
  471. *
  472. * @see http://tools.ietf.org/html/rfc2616#section-10.3.5
  473. */
  474. public function setNotModified()
  475. {
  476. $this->setStatusCode(304);
  477. $this->setContent(null);
  478. // remove headers that MUST NOT be included with 304 Not Modified responses
  479. foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) {
  480. $this->headers->delete($header);
  481. }
  482. }
  483. /**
  484. * Returns true if the response includes a Vary header.
  485. *
  486. * @return true if the response includes a Vary header, false otherwise
  487. */
  488. public function hasVary()
  489. {
  490. return (Boolean) $this->headers->get('Vary');
  491. }
  492. /**
  493. * Returns an array of header names given in the Vary header.
  494. *
  495. * @return array An array of Vary names
  496. */
  497. public function getVary()
  498. {
  499. if (!$vary = $this->headers->get('Vary')) {
  500. return array();
  501. }
  502. return preg_split('/[\s,]+/', $vary);
  503. }
  504. /**
  505. * Determines if the Response validators (ETag, Last-Modified) matches
  506. * a conditional value specified in the Request.
  507. *
  508. * If the Response is not modified, it sets the status code to 304 and
  509. * remove the actual content by calling the setNotModified() method.
  510. *
  511. * @param Request $request A Request instance
  512. *
  513. * @return Boolean true if the Response validators matches the Request, false otherwise
  514. */
  515. public function isNotModified(Request $request)
  516. {
  517. $lastModified = $request->headers->get('If-Modified-Since');
  518. $notModified = false;
  519. if ($etags = $request->getEtags()) {
  520. $notModified = (in_array($this->getEtag(), $etags) || in_array('*', $etags)) && (!$lastModified || $this->headers->get('Last-Modified') == $lastModified);
  521. } elseif ($lastModified) {
  522. $notModified = $lastModified == $this->headers->get('Last-Modified');
  523. }
  524. if ($notModified) {
  525. $this->setNotModified();
  526. }
  527. return $notModified;
  528. }
  529. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
  530. public function isInvalid()
  531. {
  532. return $this->statusCode < 100 || $this->statusCode >= 600;
  533. }
  534. public function isInformational()
  535. {
  536. return $this->statusCode >= 100 && $this->statusCode < 200;
  537. }
  538. public function isSuccessful()
  539. {
  540. return $this->statusCode >= 200 && $this->statusCode < 300;
  541. }
  542. public function isRedirection()
  543. {
  544. return $this->statusCode >= 300 && $this->statusCode < 400;
  545. }
  546. public function isClientError()
  547. {
  548. return $this->statusCode >= 400 && $this->statusCode < 500;
  549. }
  550. public function isServerError()
  551. {
  552. return $this->statusCode >= 500 && $this->statusCode < 600;
  553. }
  554. public function isOk()
  555. {
  556. return 200 === $this->statusCode;
  557. }
  558. public function isForbidden()
  559. {
  560. return 403 === $this->statusCode;
  561. }
  562. public function isNotFound()
  563. {
  564. return 404 === $this->statusCode;
  565. }
  566. public function isRedirect()
  567. {
  568. return in_array($this->statusCode, array(301, 302, 303, 307));
  569. }
  570. public function isEmpty()
  571. {
  572. return in_array($this->statusCode, array(201, 204, 304));
  573. }
  574. public function isRedirected($location)
  575. {
  576. return $this->isRedirect() && $location == $this->headers->get('Location');
  577. }
  578. }