Response.php 20 KB

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