ResponseEmitter.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.3.5
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. *
  15. * Parts of this file are derived from Zend-Diactoros
  16. *
  17. * @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
  18. * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
  19. */
  20. namespace Cake\Http;
  21. use Cake\Core\Configure;
  22. use Cake\Log\Log;
  23. use Psr\Http\Message\ResponseInterface;
  24. use Zend\Diactoros\RelativeStream;
  25. use Zend\Diactoros\Response\EmitterInterface;
  26. /**
  27. * Emits a Response to the PHP Server API.
  28. *
  29. * This emitter offers a few changes from the emitters offered by
  30. * diactoros:
  31. *
  32. * - It logs headers sent using CakePHP's logging tools.
  33. * - Cookies are emitted using setcookie() to not conflict with ext/session
  34. */
  35. class ResponseEmitter implements EmitterInterface
  36. {
  37. /**
  38. * {@inheritDoc}
  39. */
  40. public function emit(ResponseInterface $response, $maxBufferLength = 8192)
  41. {
  42. $file = $line = null;
  43. if (headers_sent($file, $line)) {
  44. $message = "Unable to emit headers. Headers sent in file=$file line=$line";
  45. if (Configure::read('debug')) {
  46. trigger_error($message, E_USER_WARNING);
  47. } else {
  48. Log::warning($message);
  49. }
  50. }
  51. $this->emitStatusLine($response);
  52. $this->emitHeaders($response);
  53. $this->flush();
  54. $range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
  55. if (is_array($range)) {
  56. $this->emitBodyRange($range, $response, $maxBufferLength);
  57. } else {
  58. $this->emitBody($response, $maxBufferLength);
  59. }
  60. if (function_exists('fastcgi_finish_request')) {
  61. fastcgi_finish_request();
  62. }
  63. }
  64. /**
  65. * Emit the message body.
  66. *
  67. * @param \Psr\Http\Message\ResponseInterface $response The response to emit
  68. * @param int $maxBufferLength The chunk size to emit
  69. * @return void
  70. */
  71. protected function emitBody(ResponseInterface $response, $maxBufferLength)
  72. {
  73. if (in_array($response->getStatusCode(), [204, 304])) {
  74. return;
  75. }
  76. $body = $response->getBody();
  77. if (!$body->isSeekable()) {
  78. echo $body;
  79. return;
  80. }
  81. $body->rewind();
  82. while (!$body->eof()) {
  83. echo $body->read($maxBufferLength);
  84. }
  85. }
  86. /**
  87. * Emit a range of the message body.
  88. *
  89. * @param array $range The range data to emit
  90. * @param \Psr\Http\Message\ResponseInterface $response The response to emit
  91. * @param int $maxBufferLength The chunk size to emit
  92. * @return void
  93. */
  94. protected function emitBodyRange(array $range, ResponseInterface $response, $maxBufferLength)
  95. {
  96. list($unit, $first, $last, $length) = $range;
  97. $body = $response->getBody();
  98. if (!$body->isSeekable()) {
  99. $contents = $body->getContents();
  100. echo substr($contents, $first, $last - $first + 1);
  101. return;
  102. }
  103. $body = new RelativeStream($body, $first);
  104. $body->rewind();
  105. $pos = 0;
  106. $length = $last - $first + 1;
  107. while (!$body->eof() && $pos < $length) {
  108. if (($pos + $maxBufferLength) > $length) {
  109. echo $body->read($length - $pos);
  110. break;
  111. }
  112. echo $body->read($maxBufferLength);
  113. $pos = $body->tell();
  114. }
  115. }
  116. /**
  117. * Emit the status line.
  118. *
  119. * Emits the status line using the protocol version and status code from
  120. * the response; if a reason phrase is availble, it, too, is emitted.
  121. *
  122. * @param \Psr\Http\Message\ResponseInterface $response The response to emit
  123. * @return void
  124. */
  125. protected function emitStatusLine(ResponseInterface $response)
  126. {
  127. $reasonPhrase = $response->getReasonPhrase();
  128. header(sprintf(
  129. 'HTTP/%s %d%s',
  130. $response->getProtocolVersion(),
  131. $response->getStatusCode(),
  132. ($reasonPhrase ? ' ' . $reasonPhrase : '')
  133. ));
  134. }
  135. /**
  136. * Emit response headers.
  137. *
  138. * Loops through each header, emitting each; if the header value
  139. * is an array with multiple values, ensures that each is sent
  140. * in such a way as to create aggregate headers (instead of replace
  141. * the previous).
  142. *
  143. * @param \Psr\Http\Message\ResponseInterface $response The response to emit
  144. * @return void
  145. */
  146. protected function emitHeaders(ResponseInterface $response)
  147. {
  148. $cookies = [];
  149. if (method_exists($response, 'cookie')) {
  150. $cookies = $response->cookie();
  151. }
  152. foreach ($response->getHeaders() as $name => $values) {
  153. if (strtolower($name) === 'set-cookie') {
  154. $cookies = array_merge($cookies, $values);
  155. continue;
  156. }
  157. $first = true;
  158. foreach ($values as $value) {
  159. header(sprintf(
  160. '%s: %s',
  161. $name,
  162. $value
  163. ), $first);
  164. $first = false;
  165. }
  166. }
  167. $this->emitCookies($cookies);
  168. }
  169. /**
  170. * Emit cookies using setcookie()
  171. *
  172. * @param array $cookies An array of Set-Cookie headers.
  173. * @return void
  174. */
  175. protected function emitCookies(array $cookies)
  176. {
  177. foreach ($cookies as $cookie) {
  178. if (is_array($cookie)) {
  179. setcookie(
  180. $cookie['name'],
  181. $cookie['value'],
  182. $cookie['expire'],
  183. $cookie['path'],
  184. $cookie['domain'],
  185. $cookie['secure'],
  186. $cookie['httpOnly']
  187. );
  188. continue;
  189. }
  190. if (strpos($cookie, '";"') !== false) {
  191. $cookie = str_replace('";"', "{__cookie_replace__}", $cookie);
  192. $parts = str_replace("{__cookie_replace__}", '";"', explode(';', $cookie));
  193. } else {
  194. $parts = preg_split('/\;[ \t]*/', $cookie);
  195. }
  196. list($name, $value) = explode('=', array_shift($parts), 2);
  197. $data = [
  198. 'name' => urldecode($name),
  199. 'value' => urldecode($value),
  200. 'expires' => 0,
  201. 'path' => '',
  202. 'domain' => '',
  203. 'secure' => false,
  204. 'httponly' => false
  205. ];
  206. foreach ($parts as $part) {
  207. if (strpos($part, '=') !== false) {
  208. list($key, $value) = explode('=', $part);
  209. } else {
  210. $key = $part;
  211. $value = true;
  212. }
  213. $key = strtolower($key);
  214. $data[$key] = $value;
  215. }
  216. if (!empty($data['expires'])) {
  217. $data['expires'] = strtotime($data['expires']);
  218. }
  219. setcookie(
  220. $data['name'],
  221. $data['value'],
  222. $data['expires'],
  223. $data['path'],
  224. $data['domain'],
  225. $data['secure'],
  226. $data['httponly']
  227. );
  228. }
  229. }
  230. /**
  231. * Loops through the output buffer, flushing each, before emitting
  232. * the response.
  233. *
  234. * @param int|null $maxBufferLevel Flush up to this buffer level.
  235. * @return void
  236. */
  237. protected function flush($maxBufferLevel = null)
  238. {
  239. if (null === $maxBufferLevel) {
  240. $maxBufferLevel = ob_get_level();
  241. }
  242. while (ob_get_level() > $maxBufferLevel) {
  243. ob_end_flush();
  244. }
  245. }
  246. /**
  247. * Parse content-range header
  248. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
  249. *
  250. * @param string $header The Content-Range header to parse.
  251. * @return false|array [unit, first, last, length]; returns false if no
  252. * content range or an invalid content range is provided
  253. */
  254. protected function parseContentRange($header)
  255. {
  256. if (preg_match('/(?P<unit>[\w]+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/', $header, $matches)) {
  257. return [
  258. $matches['unit'],
  259. (int)$matches['first'],
  260. (int)$matches['last'],
  261. $matches['length'] === '*' ? '*' : (int)$matches['length'],
  262. ];
  263. }
  264. return false;
  265. }
  266. }