Response.php 21 KB

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