PageRenderTime 84ms CodeModel.GetById 5ms RepoModel.GetById 0ms app.codeStats 0ms

/Classes/TYPO3/FLOW3/Http/Response.php

https://github.com/christianjul/FLOW3-Composer
PHP | 592 lines | 251 code | 50 blank | 291 comment | 27 complexity | cec4c604efdbcf2b69b336043e3271bf MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-3.0
  1. <?php
  2. namespace TYPO3\FLOW3\Http;
  3. /* *
  4. * This script belongs to the FLOW3 framework. *
  5. * *
  6. * It is free software; you can redistribute it and/or modify it under *
  7. * the terms of the GNU Lesser General Public License, either version 3 *
  8. * of the License, or (at your option) any later version. *
  9. * *
  10. * The TYPO3 project - inspiring people to share! *
  11. * */
  12. use TYPO3\FLOW3\Mvc\ResponseInterface;
  13. use TYPO3\FLOW3\Annotations as FLOW3;
  14. /**
  15. * Represents a HTTP Response
  16. *
  17. * @api
  18. * @FLOW3\Proxy(false)
  19. */
  20. class Response extends Message implements ResponseInterface {
  21. /**
  22. * @var \TYPO3\FLOW3\Http\Response
  23. */
  24. protected $parentResponse;
  25. /**
  26. * The HTTP status code
  27. * @var integer
  28. */
  29. protected $statusCode = 200;
  30. /**
  31. * The HTTP status message
  32. * @var string
  33. */
  34. protected $statusMessage = 'OK';
  35. /**
  36. * The current point in time, used for comparisons
  37. * @var \DateTime
  38. */
  39. protected $now;
  40. /**
  41. * The standardized and other important HTTP Status messages
  42. *
  43. * @var array
  44. */
  45. static protected $statusMessages = array(
  46. 100 => 'Continue',
  47. 101 => 'Switching Protocols',
  48. 102 => 'Processing', # RFC 2518
  49. 200 => 'OK',
  50. 201 => 'Created',
  51. 202 => 'Accepted',
  52. 203 => 'Non-Authoritative Information',
  53. 204 => 'No Content',
  54. 205 => 'Reset Content',
  55. 206 => 'Partial Content',
  56. 207 => 'Multi-Status',
  57. 300 => 'Multiple Choices',
  58. 301 => 'Moved Permanently',
  59. 302 => 'Found',
  60. 303 => 'See Other',
  61. 304 => 'Not Modified',
  62. 305 => 'Use Proxy',
  63. 307 => 'Temporary Redirect',
  64. 400 => 'Bad Request',
  65. 401 => 'Unauthorized',
  66. 402 => 'Payment Required',
  67. 403 => 'Forbidden',
  68. 404 => 'Not Found',
  69. 405 => 'Method Not Allowed',
  70. 406 => 'Not Acceptable',
  71. 407 => 'Proxy Authentication Required',
  72. 408 => 'Request Timeout',
  73. 409 => 'Conflict',
  74. 410 => 'Gone',
  75. 411 => 'Length Required',
  76. 412 => 'Precondition Failed',
  77. 413 => 'Request Entity Too Large',
  78. 414 => 'Request-URI Too Long',
  79. 415 => 'Unsupported Media Type',
  80. 416 => 'Requested Range Not Satisfiable',
  81. 417 => 'Expectation Failed',
  82. 418 => 'Sono Vibiemme',
  83. 500 => 'Internal Server Error',
  84. 501 => 'Not Implemented',
  85. 502 => 'Bad Gateway',
  86. 503 => 'Service Unavailable',
  87. 504 => 'Gateway Timeout',
  88. 505 => 'HTTP Version Not Supported',
  89. 507 => 'Insufficient Storage',
  90. 509 => 'Bandwidth Limit Exceeded',
  91. );
  92. /**
  93. * Returns the human-readable message for the given status code.
  94. *
  95. * @param integer $statusCode
  96. * @return string
  97. */
  98. static public function getStatusMessageByCode($statusCode) {
  99. return isset(self::$statusMessages[$statusCode]) ? self::$statusMessages[$statusCode] : 'Unknown Status';
  100. }
  101. /**
  102. * Construct this Response
  103. *
  104. * @param \TYPO3\FLOW3\Http\Response $parentResponse
  105. */
  106. public function __construct(Response $parentResponse = NULL) {
  107. $this->headers = new Headers();
  108. $this->headers->set('X-FLOW3-Powered', 'FLOW3/' . FLOW3_VERSION_BRANCH);
  109. $this->headers->set('Content-Type', 'text/html; charset=' . $this->charset);
  110. $this->parentResponse = $parentResponse;
  111. }
  112. /**
  113. * Creates a response from the given raw, that is plain text, HTTP response.
  114. *
  115. * @param string $rawResponse
  116. * @param \TYPO3\FLOW3\Http\Response $parentResponse Parent response, if called recursively
  117. *
  118. * @throws \InvalidArgumentException
  119. * @return \TYPO3\FLOW3\Http\Response
  120. */
  121. static public function createFromRaw($rawResponse, Response $parentResponse = NULL) {
  122. $response = new static($parentResponse);
  123. $lines = explode(chr(10), $rawResponse);
  124. $firstLine = array_shift($lines);
  125. if (substr($firstLine, 0, 5) !== 'HTTP/') {
  126. throw new \InvalidArgumentException('The given raw HTTP message is not a valid response.', 1335175601);
  127. }
  128. list(, $statusCode, $statusMessage) = explode(' ', $firstLine, 3);
  129. $response->setStatus((integer)$statusCode, trim($statusMessage));
  130. $parsingHeader = TRUE;
  131. $contentLines = array();
  132. $headers = new Headers();
  133. foreach ($lines as $line) {
  134. if ($parsingHeader) {
  135. if (trim($line) === '') {
  136. $parsingHeader = FALSE;
  137. continue;
  138. }
  139. $fieldName = trim(substr($line, 0, strpos($line, ':')));
  140. $fieldValue = trim(substr($line, strlen($fieldName) + 1));
  141. $headers->set($fieldName, $fieldValue, FALSE);
  142. } else {
  143. $contentLines[] = $line;
  144. }
  145. }
  146. $content = implode(chr(10), $contentLines);
  147. $response->setHeaders($headers);
  148. $response->setContent($content);
  149. return $response;
  150. }
  151. /**
  152. * Return the parent response or NULL if none exists.
  153. *
  154. * @return \TYPO3\FLOW3\Http\Response the parent response, or NULL if none
  155. */
  156. public function getParentResponse() {
  157. return $this->parentResponse;
  158. }
  159. /**
  160. * Appends content to the already existing content.
  161. *
  162. * @param string $content More response content
  163. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  164. * @api
  165. */
  166. public function appendContent($content) {
  167. $this->content .= $content;
  168. return $this;
  169. }
  170. /**
  171. * Returns the response content without sending it.
  172. *
  173. * @return string The response content
  174. * @api
  175. */
  176. public function getContent() {
  177. return $this->content;
  178. }
  179. /**
  180. * Sets the HTTP status code and (optionally) a customized message.
  181. *
  182. * @param integer $code The status code
  183. * @param string $message If specified, this message is sent instead of the standard message
  184. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  185. * @throws \InvalidArgumentException if the specified status code is not valid
  186. * @api
  187. */
  188. public function setStatus($code, $message = NULL) {
  189. if (!is_int($code)) {
  190. throw new \InvalidArgumentException('The HTTP status code must be of type integer, ' . gettype($code) . ' given.', 1220526013);
  191. }
  192. if ($message === NULL && !isset(self::$statusMessages[$code])) {
  193. throw new \InvalidArgumentException('No message found for HTTP status code "' . $code . '".', 1220526014);
  194. }
  195. $this->statusCode = $code;
  196. $this->statusMessage = ($message === NULL) ? self::$statusMessages[$code] : $message;
  197. return $this;
  198. }
  199. /**
  200. * Returns status code and status message.
  201. *
  202. * @return string The status code and status message, eg. "404 Not Found"
  203. * @api
  204. */
  205. public function getStatus() {
  206. return $this->statusCode . ' ' . $this->statusMessage;
  207. }
  208. /**
  209. * Returns the status code.
  210. *
  211. * @return integer The status code, eg. 404
  212. * @api
  213. */
  214. public function getStatusCode() {
  215. return $this->statusCode;
  216. }
  217. /**
  218. * Replaces all possibly existing HTTP headers with the ones specified
  219. *
  220. * @param \TYPO3\FLOW3\Http\Headers
  221. * @return void
  222. * @api
  223. */
  224. public function setHeaders(Headers $headers) {
  225. $this->headers = $headers;
  226. }
  227. /**
  228. * Sets the current point in time.
  229. *
  230. * This date / time is used internally for comparisons in order to determine the
  231. * freshness of this response. By default this DateTime object is set automatically
  232. * through dependency injection, configured in the Objects.yaml of the FLOW3 package.
  233. *
  234. * Unless you are mocking the current time in a test, there is probably no need
  235. * to use this function. Also note that this method must be called before any
  236. * of the Response methods are used and it must not be called a second time.
  237. *
  238. * @param \DateTime $now The current point in time
  239. * @return void
  240. * @api
  241. */
  242. public function setNow(\DateTime $now) {
  243. $this->now = clone $now;
  244. $this->now->setTimezone(new \DateTimeZone('UTC'));
  245. $this->headers->set('Date', $this->now);
  246. }
  247. /**
  248. * Sets the Date header.
  249. *
  250. * The given date must either be an RFC2822 parseable date string or a DateTime
  251. * object. The timezone will be converted to GMT internally, but the point in
  252. * time remains the same.
  253. *
  254. * @param string|\DateTime $date
  255. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  256. * @api
  257. */
  258. public function setDate($date) {
  259. $this->headers->set('Date', $date);
  260. return $this;
  261. }
  262. /**
  263. * Returns the date from the Date header.
  264. *
  265. * The returned date is configured to be in the GMT timezone.
  266. *
  267. * @return \DateTime The date of this response
  268. * @api
  269. */
  270. public function getDate() {
  271. return $this->headers->get('Date');
  272. }
  273. /**
  274. * Sets the Last-Modified header.
  275. *
  276. * The given date must either be an RFC2822 parseable date string or a DateTime
  277. * object. The timezone will be converted to GMT internally, but the point in
  278. * time remains the same.
  279. *
  280. * @param string|\DateTime $date
  281. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  282. * @api
  283. */
  284. public function setLastModified($date) {
  285. $this->headers->set('Last-Modified', $date);
  286. return $this;
  287. }
  288. /**
  289. * Returns the date from the Last-Modified header or NULL if no such header
  290. * is present.
  291. *
  292. * The returned date is configured to be in the GMT timezone.
  293. *
  294. * @return \DateTime The last modification date or NULL
  295. * @api
  296. */
  297. public function getLastModified() {
  298. return $this->headers->get('Last-Modified');
  299. }
  300. /**
  301. * Sets the Expires header.
  302. *
  303. * The given date must either be an RFC2822 parseable date string or a DateTime
  304. * object. The timezone will be converted to GMT internally, but the point in
  305. * time remains the same.
  306. *
  307. * In order to signal that the response has already expired, the date should
  308. * be set to the same date as the Date header (that is, $now). To communicate
  309. * an infinite expiration time, the date should be set to one year in the future.
  310. *
  311. * Expiration times should not be more than one year in the future, according
  312. * to RFC 2616 / 14.21
  313. *
  314. * @param string|\DateTime $date
  315. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  316. * @api
  317. */
  318. public function setExpires($date) {
  319. $this->headers->set('Expires', $date);
  320. return $this;
  321. }
  322. /**
  323. * Returns the date from the Expires header or NULL if no such header
  324. * is present.
  325. *
  326. * The returned date is configured to be in the GMT timezone.
  327. *
  328. * @return \DateTime The expiration date or NULL
  329. * @api
  330. */
  331. public function getExpires() {
  332. return $this->headers->get('Expires');
  333. }
  334. /**
  335. * Returns the age of this responds in seconds.
  336. *
  337. * The age is determined either by an explicitly set Age header or by the
  338. * difference between Date and "now".
  339. *
  340. * Note that, according to RFC 2616 / 13.2.3, the presence of an Age header implies
  341. * that the response is not first-hand. You should therefore only explicitly set
  342. * an Age header if this is the case.
  343. *
  344. * @return integer The age in seconds
  345. * @api
  346. */
  347. public function getAge() {
  348. if ($this->headers->has('Age')) {
  349. return $this->headers->get('Age');
  350. } else {
  351. $dateTimestamp = $this->headers->get('Date')->getTimestamp();
  352. $nowTimestamp = $this->now->getTimestamp();
  353. return ($nowTimestamp > $dateTimestamp) ? ($nowTimestamp - $dateTimestamp) : 0;
  354. }
  355. }
  356. /**
  357. * Sets the maximum age in seconds before this response becomes stale.
  358. *
  359. * This method sets the "max-age" directive in the Cache-Control header.
  360. *
  361. * @param integer $age The maximum age in seconds
  362. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  363. * @api
  364. */
  365. public function setMaximumAge($age) {
  366. $this->headers->setCacheControlDirective('max-age', $age);
  367. return $this;
  368. }
  369. /**
  370. * Returns the maximum age in seconds before this response becomes stale.
  371. *
  372. * This method returns the value from the "max-age" directive in the
  373. * Cache-Control header.
  374. *
  375. * @return integer The maximum age in seconds, or NULL if none has been defined
  376. * @api
  377. */
  378. public function getMaximumAge() {
  379. return $this->headers->getCacheControlDirective('max-age');
  380. }
  381. /**
  382. * Sets the maximum age in seconds before this response becomes stale in shared
  383. * caches, such as proxies.
  384. *
  385. * This method sets the "s-maxage" directive in the Cache-Control header.
  386. *
  387. * @param integer $maximumAge The maximum age in seconds
  388. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  389. * @api
  390. */
  391. public function setSharedMaximumAge($maximumAge) {
  392. $this->headers->setCacheControlDirective('s-maxage', $maximumAge);
  393. return $this;
  394. }
  395. /**
  396. * Returns the maximum age in seconds before this response becomes stale in shared
  397. * caches, such as proxies.
  398. *
  399. * This method returns the value from the "s-maxage" directive in the
  400. * Cache-Control header.
  401. *
  402. * @return integer The maximum age in seconds, or NULL if none has been defined
  403. * @api
  404. */
  405. public function getSharedMaximumAge() {
  406. return $this->headers->getCacheControlDirective('s-maxage');
  407. }
  408. /**
  409. * Renders the HTTP headers - including the status header - of this response
  410. *
  411. * @return array The HTTP headers
  412. * @api
  413. */
  414. public function renderHeaders() {
  415. $preparedHeaders = array();
  416. $statusHeader = 'HTTP/1.1 ' . $this->statusCode . ' ' . $this->statusMessage;
  417. $preparedHeaders[] = $statusHeader;
  418. foreach ($this->headers->getAll() as $name => $values) {
  419. foreach ($values as $value) {
  420. $preparedHeaders[] = $name . ': ' . $value;
  421. }
  422. }
  423. return $preparedHeaders;
  424. }
  425. /**
  426. * Sets the respective directive in the Cache-Control header.
  427. *
  428. * A response flagged as "public" may be cached by any cache, even if it normally
  429. * wouldn't be cacheable in a shared cache.
  430. *
  431. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  432. * @api
  433. */
  434. public function setPublic() {
  435. $this->headers->setCacheControlDirective('public');
  436. return $this;
  437. }
  438. /**
  439. * Sets the respective directive in the Cache-Control header.
  440. *
  441. * A response flagged as "private" tells that it is intended for a specific
  442. * user and must not be cached by a shared cache.
  443. *
  444. * @return \TYPO3\FLOW3\Http\Response This response, for method chaining
  445. * @api
  446. */
  447. public function setPrivate() {
  448. $this->headers->setCacheControlDirective('private');
  449. return $this;
  450. }
  451. /**
  452. * Analyzes this response, considering the given request and makes additions
  453. * or removes certain headers in order to make the response compliant to
  454. * RFC 2616 and related standards.
  455. *
  456. * It is recommended to call this method before the response is sent and FLOW3
  457. * does so by default in its built-in HTTP request handler.
  458. *
  459. * @param \TYPO3\FLOW3\Http\Request $request The corresponding request
  460. * @return void
  461. * @api
  462. */
  463. public function makeStandardsCompliant(Request $request) {
  464. if ($request->hasHeader('If-Modified-Since') && $this->headers->has('Last-Modified') && $this->statusCode === 200) {
  465. $ifModifiedSinceDate = $request->getHeader('If-Modified-Since');
  466. $lastModifiedDate = $this->headers->get('Last-Modified');
  467. if ($lastModifiedDate <= $ifModifiedSinceDate) {
  468. $this->setStatus(304);
  469. $this->content = '';
  470. }
  471. } elseif ($request->hasHeader('If-Unmodified-Since') && $this->headers->has('Last-Modified')
  472. && (($this->statusCode >= 200 && $this->statusCode <= 299) || $this->statusCode === 412)) {
  473. $unmodifiedSinceDate = $request->getHeader('If-Unmodified-Since');
  474. $lastModifiedDate = $this->headers->get('Last-Modified');
  475. if ($lastModifiedDate > $unmodifiedSinceDate) {
  476. $this->setStatus(412);
  477. }
  478. }
  479. if (in_array($this->statusCode, array(100, 101, 204, 304))) {
  480. $this->content = '';
  481. }
  482. if ($this->headers->getCacheControlDirective('no-cache') !== NULL
  483. || $this->headers->has('Expires')) {
  484. $this->headers->removeCacheControlDirective('max-age');
  485. }
  486. if ($request->getMethod() === 'HEAD') {
  487. if (!$this->headers->has('Content-Length')) {
  488. $this->headers->set('Content-Length', strlen($this->content));
  489. }
  490. $this->content = '';
  491. }
  492. if (!$this->headers->has('Content-Length')) {
  493. $this->headers->set('Content-Length', strlen($this->content));
  494. }
  495. if ($this->headers->has('Transfer-Encoding')) {
  496. $this->headers->remove('Content-Length');
  497. }
  498. }
  499. /**
  500. * Sends the HTTP headers.
  501. *
  502. * If headers have been sent previously, this method fails silently.
  503. *
  504. * @return void
  505. * @codeCoverageIgnore
  506. * @api
  507. */
  508. public function sendHeaders() {
  509. if (headers_sent() === TRUE) {
  510. return;
  511. }
  512. foreach ($this->renderHeaders() as $header) {
  513. header($header);
  514. }
  515. foreach ($this->headers->getCookies() as $cookie) {
  516. header('Set-Cookie: ' . $cookie, FALSE);
  517. }
  518. }
  519. /**
  520. * Renders and sends the whole web response
  521. *
  522. * @return void
  523. * @codeCoverageIgnore
  524. * @api
  525. */
  526. public function send() {
  527. $this->sendHeaders();
  528. if ($this->content !== NULL) {
  529. echo $this->getContent();
  530. }
  531. }
  532. /**
  533. * Cast the response to a string: return the content part of this response
  534. *
  535. * @return string The same as getContent()
  536. * @api
  537. */
  538. public function __toString() {
  539. return $this->getContent();
  540. }
  541. }
  542. ?>