Response.php 21 KB

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