Response.php 21 KB

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