Response.php 19 KB

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