PageRenderTime 55ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/src/Network/Response.php

https://github.com/binondord/cakephp
PHP | 1613 lines | 909 code | 92 blank | 612 comment | 122 complexity | 5a74fca3ceb62be398c187aa13daa9fb MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  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 2.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Network;
  16. use Cake\Core\Configure;
  17. use Cake\Filesystem\File;
  18. use Cake\Network\Exception\NotFoundException;
  19. use InvalidArgumentException;
  20. /**
  21. * Cake Response is responsible for managing the response text, status and headers of a HTTP response.
  22. *
  23. * By default controllers will use this class to render their response. If you are going to use
  24. * a custom response class it should subclass this object in order to ensure compatibility.
  25. *
  26. */
  27. class Response
  28. {
  29. /**
  30. * Holds HTTP response statuses
  31. *
  32. * @var array
  33. */
  34. protected $_statusCodes = [
  35. 100 => 'Continue',
  36. 101 => 'Switching Protocols',
  37. 200 => 'OK',
  38. 201 => 'Created',
  39. 202 => 'Accepted',
  40. 203 => 'Non-Authoritative Information',
  41. 204 => 'No Content',
  42. 205 => 'Reset Content',
  43. 206 => 'Partial Content',
  44. 300 => 'Multiple Choices',
  45. 301 => 'Moved Permanently',
  46. 302 => 'Found',
  47. 303 => 'See Other',
  48. 304 => 'Not Modified',
  49. 305 => 'Use Proxy',
  50. 307 => 'Temporary Redirect',
  51. 400 => 'Bad Request',
  52. 401 => 'Unauthorized',
  53. 402 => 'Payment Required',
  54. 403 => 'Forbidden',
  55. 404 => 'Not Found',
  56. 405 => 'Method Not Allowed',
  57. 406 => 'Not Acceptable',
  58. 407 => 'Proxy Authentication Required',
  59. 408 => 'Request Time-out',
  60. 409 => 'Conflict',
  61. 410 => 'Gone',
  62. 411 => 'Length Required',
  63. 412 => 'Precondition Failed',
  64. 413 => 'Request Entity Too Large',
  65. 414 => 'Request-URI Too Large',
  66. 415 => 'Unsupported Media Type',
  67. 416 => 'Requested range not satisfiable',
  68. 417 => 'Expectation Failed',
  69. 429 => 'Too Many Requests',
  70. 500 => 'Internal Server Error',
  71. 501 => 'Not Implemented',
  72. 502 => 'Bad Gateway',
  73. 503 => 'Service Unavailable',
  74. 504 => 'Gateway Time-out',
  75. 505 => 'Unsupported Version'
  76. ];
  77. /**
  78. * Holds type key to mime type mappings for known mime types.
  79. *
  80. * @var array
  81. */
  82. protected $_mimeTypes = [
  83. 'html' => ['text/html', '*/*'],
  84. 'json' => 'application/json',
  85. 'xml' => ['application/xml', 'text/xml'],
  86. 'rss' => 'application/rss+xml',
  87. 'ai' => 'application/postscript',
  88. 'bcpio' => 'application/x-bcpio',
  89. 'bin' => 'application/octet-stream',
  90. 'ccad' => 'application/clariscad',
  91. 'cdf' => 'application/x-netcdf',
  92. 'class' => 'application/octet-stream',
  93. 'cpio' => 'application/x-cpio',
  94. 'cpt' => 'application/mac-compactpro',
  95. 'csh' => 'application/x-csh',
  96. 'csv' => ['text/csv', 'application/vnd.ms-excel'],
  97. 'dcr' => 'application/x-director',
  98. 'dir' => 'application/x-director',
  99. 'dms' => 'application/octet-stream',
  100. 'doc' => 'application/msword',
  101. 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  102. 'drw' => 'application/drafting',
  103. 'dvi' => 'application/x-dvi',
  104. 'dwg' => 'application/acad',
  105. 'dxf' => 'application/dxf',
  106. 'dxr' => 'application/x-director',
  107. 'eot' => 'application/vnd.ms-fontobject',
  108. 'eps' => 'application/postscript',
  109. 'exe' => 'application/octet-stream',
  110. 'ez' => 'application/andrew-inset',
  111. 'flv' => 'video/x-flv',
  112. 'gtar' => 'application/x-gtar',
  113. 'gz' => 'application/x-gzip',
  114. 'bz2' => 'application/x-bzip',
  115. '7z' => 'application/x-7z-compressed',
  116. 'hdf' => 'application/x-hdf',
  117. 'hqx' => 'application/mac-binhex40',
  118. 'ico' => 'image/x-icon',
  119. 'ips' => 'application/x-ipscript',
  120. 'ipx' => 'application/x-ipix',
  121. 'js' => 'application/javascript',
  122. 'latex' => 'application/x-latex',
  123. 'lha' => 'application/octet-stream',
  124. 'lsp' => 'application/x-lisp',
  125. 'lzh' => 'application/octet-stream',
  126. 'man' => 'application/x-troff-man',
  127. 'me' => 'application/x-troff-me',
  128. 'mif' => 'application/vnd.mif',
  129. 'ms' => 'application/x-troff-ms',
  130. 'nc' => 'application/x-netcdf',
  131. 'oda' => 'application/oda',
  132. 'otf' => 'font/otf',
  133. 'pdf' => 'application/pdf',
  134. 'pgn' => 'application/x-chess-pgn',
  135. 'pot' => 'application/vnd.ms-powerpoint',
  136. 'pps' => 'application/vnd.ms-powerpoint',
  137. 'ppt' => 'application/vnd.ms-powerpoint',
  138. 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  139. 'ppz' => 'application/vnd.ms-powerpoint',
  140. 'pre' => 'application/x-freelance',
  141. 'prt' => 'application/pro_eng',
  142. 'ps' => 'application/postscript',
  143. 'roff' => 'application/x-troff',
  144. 'scm' => 'application/x-lotusscreencam',
  145. 'set' => 'application/set',
  146. 'sh' => 'application/x-sh',
  147. 'shar' => 'application/x-shar',
  148. 'sit' => 'application/x-stuffit',
  149. 'skd' => 'application/x-koan',
  150. 'skm' => 'application/x-koan',
  151. 'skp' => 'application/x-koan',
  152. 'skt' => 'application/x-koan',
  153. 'smi' => 'application/smil',
  154. 'smil' => 'application/smil',
  155. 'sol' => 'application/solids',
  156. 'spl' => 'application/x-futuresplash',
  157. 'src' => 'application/x-wais-source',
  158. 'step' => 'application/STEP',
  159. 'stl' => 'application/SLA',
  160. 'stp' => 'application/STEP',
  161. 'sv4cpio' => 'application/x-sv4cpio',
  162. 'sv4crc' => 'application/x-sv4crc',
  163. 'svg' => 'image/svg+xml',
  164. 'svgz' => 'image/svg+xml',
  165. 'swf' => 'application/x-shockwave-flash',
  166. 't' => 'application/x-troff',
  167. 'tar' => 'application/x-tar',
  168. 'tcl' => 'application/x-tcl',
  169. 'tex' => 'application/x-tex',
  170. 'texi' => 'application/x-texinfo',
  171. 'texinfo' => 'application/x-texinfo',
  172. 'tr' => 'application/x-troff',
  173. 'tsp' => 'application/dsptype',
  174. 'ttc' => 'font/ttf',
  175. 'ttf' => 'font/ttf',
  176. 'unv' => 'application/i-deas',
  177. 'ustar' => 'application/x-ustar',
  178. 'vcd' => 'application/x-cdlink',
  179. 'vda' => 'application/vda',
  180. 'xlc' => 'application/vnd.ms-excel',
  181. 'xll' => 'application/vnd.ms-excel',
  182. 'xlm' => 'application/vnd.ms-excel',
  183. 'xls' => 'application/vnd.ms-excel',
  184. 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  185. 'xlw' => 'application/vnd.ms-excel',
  186. 'zip' => 'application/zip',
  187. 'aif' => 'audio/x-aiff',
  188. 'aifc' => 'audio/x-aiff',
  189. 'aiff' => 'audio/x-aiff',
  190. 'au' => 'audio/basic',
  191. 'kar' => 'audio/midi',
  192. 'mid' => 'audio/midi',
  193. 'midi' => 'audio/midi',
  194. 'mp2' => 'audio/mpeg',
  195. 'mp3' => 'audio/mpeg',
  196. 'mpga' => 'audio/mpeg',
  197. 'ogg' => 'audio/ogg',
  198. 'oga' => 'audio/ogg',
  199. 'spx' => 'audio/ogg',
  200. 'ra' => 'audio/x-realaudio',
  201. 'ram' => 'audio/x-pn-realaudio',
  202. 'rm' => 'audio/x-pn-realaudio',
  203. 'rpm' => 'audio/x-pn-realaudio-plugin',
  204. 'snd' => 'audio/basic',
  205. 'tsi' => 'audio/TSP-audio',
  206. 'wav' => 'audio/x-wav',
  207. 'aac' => 'audio/aac',
  208. 'asc' => 'text/plain',
  209. 'c' => 'text/plain',
  210. 'cc' => 'text/plain',
  211. 'css' => 'text/css',
  212. 'etx' => 'text/x-setext',
  213. 'f' => 'text/plain',
  214. 'f90' => 'text/plain',
  215. 'h' => 'text/plain',
  216. 'hh' => 'text/plain',
  217. 'htm' => ['text/html', '*/*'],
  218. 'ics' => 'text/calendar',
  219. 'm' => 'text/plain',
  220. 'rtf' => 'text/rtf',
  221. 'rtx' => 'text/richtext',
  222. 'sgm' => 'text/sgml',
  223. 'sgml' => 'text/sgml',
  224. 'tsv' => 'text/tab-separated-values',
  225. 'tpl' => 'text/template',
  226. 'txt' => 'text/plain',
  227. 'text' => 'text/plain',
  228. 'avi' => 'video/x-msvideo',
  229. 'fli' => 'video/x-fli',
  230. 'mov' => 'video/quicktime',
  231. 'movie' => 'video/x-sgi-movie',
  232. 'mpe' => 'video/mpeg',
  233. 'mpeg' => 'video/mpeg',
  234. 'mpg' => 'video/mpeg',
  235. 'qt' => 'video/quicktime',
  236. 'viv' => 'video/vnd.vivo',
  237. 'vivo' => 'video/vnd.vivo',
  238. 'ogv' => 'video/ogg',
  239. 'webm' => 'video/webm',
  240. 'mp4' => 'video/mp4',
  241. 'm4v' => 'video/mp4',
  242. 'f4v' => 'video/mp4',
  243. 'f4p' => 'video/mp4',
  244. 'm4a' => 'audio/mp4',
  245. 'f4a' => 'audio/mp4',
  246. 'f4b' => 'audio/mp4',
  247. 'gif' => 'image/gif',
  248. 'ief' => 'image/ief',
  249. 'jpg' => 'image/jpeg',
  250. 'jpeg' => 'image/jpeg',
  251. 'jpe' => 'image/jpeg',
  252. 'pbm' => 'image/x-portable-bitmap',
  253. 'pgm' => 'image/x-portable-graymap',
  254. 'png' => 'image/png',
  255. 'pnm' => 'image/x-portable-anymap',
  256. 'ppm' => 'image/x-portable-pixmap',
  257. 'ras' => 'image/cmu-raster',
  258. 'rgb' => 'image/x-rgb',
  259. 'tif' => 'image/tiff',
  260. 'tiff' => 'image/tiff',
  261. 'xbm' => 'image/x-xbitmap',
  262. 'xpm' => 'image/x-xpixmap',
  263. 'xwd' => 'image/x-xwindowdump',
  264. 'ice' => 'x-conference/x-cooltalk',
  265. 'iges' => 'model/iges',
  266. 'igs' => 'model/iges',
  267. 'mesh' => 'model/mesh',
  268. 'msh' => 'model/mesh',
  269. 'silo' => 'model/mesh',
  270. 'vrml' => 'model/vrml',
  271. 'wrl' => 'model/vrml',
  272. 'mime' => 'www/mime',
  273. 'pdb' => 'chemical/x-pdb',
  274. 'xyz' => 'chemical/x-pdb',
  275. 'javascript' => 'application/javascript',
  276. 'form' => 'application/x-www-form-urlencoded',
  277. 'file' => 'multipart/form-data',
  278. 'xhtml' => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'],
  279. 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
  280. 'atom' => 'application/atom+xml',
  281. 'amf' => 'application/x-amf',
  282. 'wap' => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'],
  283. 'wml' => 'text/vnd.wap.wml',
  284. 'wmlscript' => 'text/vnd.wap.wmlscript',
  285. 'wbmp' => 'image/vnd.wap.wbmp',
  286. 'woff' => 'application/x-font-woff',
  287. 'webp' => 'image/webp',
  288. 'appcache' => 'text/cache-manifest',
  289. 'manifest' => 'text/cache-manifest',
  290. 'htc' => 'text/x-component',
  291. 'rdf' => 'application/xml',
  292. 'crx' => 'application/x-chrome-extension',
  293. 'oex' => 'application/x-opera-extension',
  294. 'xpi' => 'application/x-xpinstall',
  295. 'safariextz' => 'application/octet-stream',
  296. 'webapp' => 'application/x-web-app-manifest+json',
  297. 'vcf' => 'text/x-vcard',
  298. 'vtt' => 'text/vtt',
  299. 'mkv' => 'video/x-matroska',
  300. 'pkpass' => 'application/vnd.apple.pkpass',
  301. 'ajax' => 'text/html'
  302. ];
  303. /**
  304. * Protocol header to send to the client
  305. *
  306. * @var string
  307. */
  308. protected $_protocol = 'HTTP/1.1';
  309. /**
  310. * Status code to send to the client
  311. *
  312. * @var int
  313. */
  314. protected $_status = 200;
  315. /**
  316. * Content type to send. This can be an 'extension' that will be transformed using the $_mimetypes array
  317. * or a complete mime-type
  318. *
  319. * @var int
  320. */
  321. protected $_contentType = 'text/html';
  322. /**
  323. * Buffer list of headers
  324. *
  325. * @var array
  326. */
  327. protected $_headers = [];
  328. /**
  329. * Buffer string or callable for response message
  330. *
  331. * @var string
  332. */
  333. protected $_body = null;
  334. /**
  335. * File object for file to be read out as response
  336. *
  337. * @var File
  338. */
  339. protected $_file = null;
  340. /**
  341. * File range. Used for requesting ranges of files.
  342. *
  343. * @var array
  344. */
  345. protected $_fileRange = [];
  346. /**
  347. * The charset the response body is encoded with
  348. *
  349. * @var string
  350. */
  351. protected $_charset = 'UTF-8';
  352. /**
  353. * Holds all the cache directives that will be converted
  354. * into headers when sending the request
  355. *
  356. * @var array
  357. */
  358. protected $_cacheDirectives = [];
  359. /**
  360. * Holds cookies to be sent to the client
  361. *
  362. * @var array
  363. */
  364. protected $_cookies = [];
  365. /**
  366. * Constructor
  367. *
  368. * @param array $options list of parameters to setup the response. Possible values are:
  369. * - body: the response text that should be sent to the client
  370. * - statusCodes: additional allowable response codes
  371. * - status: the HTTP status code to respond with
  372. * - type: a complete mime-type string or an extension mapped in this class
  373. * - charset: the charset for the response body
  374. */
  375. public function __construct(array $options = [])
  376. {
  377. if (isset($options['body'])) {
  378. $this->body($options['body']);
  379. }
  380. if (isset($options['statusCodes'])) {
  381. $this->httpCodes($options['statusCodes']);
  382. }
  383. if (isset($options['status'])) {
  384. $this->statusCode($options['status']);
  385. }
  386. if (isset($options['type'])) {
  387. $this->type($options['type']);
  388. }
  389. if (!isset($options['charset'])) {
  390. $options['charset'] = Configure::read('App.encoding');
  391. }
  392. $this->charset($options['charset']);
  393. }
  394. /**
  395. * Sends the complete response to the client including headers and message body.
  396. * Will echo out the content in the response body.
  397. *
  398. * @return void
  399. */
  400. public function send()
  401. {
  402. if (isset($this->_headers['Location']) && $this->_status === 200) {
  403. $this->statusCode(302);
  404. }
  405. $this->_setContent();
  406. $this->sendHeaders();
  407. if ($this->_file) {
  408. $this->_sendFile($this->_file, $this->_fileRange);
  409. $this->_file = $this->_fileRange = null;
  410. } else {
  411. $this->_sendContent($this->_body);
  412. }
  413. if (function_exists('fastcgi_finish_request')) {
  414. fastcgi_finish_request();
  415. }
  416. }
  417. /**
  418. * Sends the HTTP headers and cookies.
  419. *
  420. * @return void
  421. */
  422. public function sendHeaders()
  423. {
  424. if (headers_sent()) {
  425. return;
  426. }
  427. $codeMessage = $this->_statusCodes[$this->_status];
  428. $this->_setCookies();
  429. $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}");
  430. $this->_setContentType();
  431. foreach ($this->_headers as $header => $values) {
  432. foreach ((array)$values as $value) {
  433. $this->_sendHeader($header, $value);
  434. }
  435. }
  436. }
  437. /**
  438. * Sets the cookies that have been added via Cake\Network\Response::cookie() before any
  439. * other output is sent to the client. Will set the cookies in the order they
  440. * have been set.
  441. *
  442. * @return void
  443. */
  444. protected function _setCookies()
  445. {
  446. foreach ($this->_cookies as $name => $c) {
  447. setcookie(
  448. $name,
  449. $c['value'],
  450. $c['expire'],
  451. $c['path'],
  452. $c['domain'],
  453. $c['secure'],
  454. $c['httpOnly']
  455. );
  456. }
  457. }
  458. /**
  459. * Formats the Content-Type header based on the configured contentType and charset
  460. * the charset will only be set in the header if the response is of type text/*
  461. *
  462. * @return void
  463. */
  464. protected function _setContentType()
  465. {
  466. if (in_array($this->_status, [304, 204])) {
  467. return;
  468. }
  469. $whitelist = [
  470. 'application/javascript', 'application/json', 'application/xml', 'application/rss+xml'
  471. ];
  472. $charset = false;
  473. if ($this->_charset &&
  474. (strpos($this->_contentType, 'text/') === 0 || in_array($this->_contentType, $whitelist))
  475. ) {
  476. $charset = true;
  477. }
  478. if ($charset) {
  479. $this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}");
  480. } else {
  481. $this->header('Content-Type', "{$this->_contentType}");
  482. }
  483. }
  484. /**
  485. * Sets the response body to an empty text if the status code is 204 or 304
  486. *
  487. * @return void
  488. */
  489. protected function _setContent()
  490. {
  491. if (in_array($this->_status, [304, 204])) {
  492. $this->body('');
  493. }
  494. }
  495. /**
  496. * Sends a header to the client.
  497. *
  498. * @param string $name the header name
  499. * @param string|null $value the header value
  500. * @return void
  501. */
  502. protected function _sendHeader($name, $value = null)
  503. {
  504. if ($value === null) {
  505. header($name);
  506. } else {
  507. header("{$name}: {$value}");
  508. }
  509. }
  510. /**
  511. * Sends a content string to the client.
  512. *
  513. * If the content is a callable, it is invoked. The callable should either
  514. * return a string or output content directly and have no return value.
  515. *
  516. * @param string|callable $content String to send as response body or callable
  517. * which returns/outputs content.
  518. * @return void
  519. */
  520. protected function _sendContent($content)
  521. {
  522. if (!is_string($content) && is_callable($content)) {
  523. $content = $content();
  524. }
  525. echo $content;
  526. }
  527. /**
  528. * Buffers a header string to be sent
  529. * Returns the complete list of buffered headers
  530. *
  531. * ### Single header
  532. * ```
  533. * header('Location', 'http://example.com');
  534. * ```
  535. *
  536. * ### Multiple headers
  537. * ```
  538. * header(['Location' => 'http://example.com', 'X-Extra' => 'My header']);
  539. * ```
  540. *
  541. * ### String header
  542. * ```
  543. * header('WWW-Authenticate: Negotiate');
  544. * ```
  545. *
  546. * ### Array of string headers
  547. * ```
  548. * header(['WWW-Authenticate: Negotiate', 'Content-type: application/pdf']);
  549. * ```
  550. *
  551. * Multiple calls for setting the same header name will have the same effect as setting the header once
  552. * with the last value sent for it
  553. * ```
  554. * header('WWW-Authenticate: Negotiate');
  555. * header('WWW-Authenticate: Not-Negotiate');
  556. * ```
  557. * will have the same effect as only doing
  558. * ```
  559. * header('WWW-Authenticate: Not-Negotiate');
  560. * ```
  561. *
  562. * @param string|array|null $header An array of header strings or a single header string
  563. * - an associative array of "header name" => "header value" is also accepted
  564. * - an array of string headers is also accepted
  565. * @param string|array|null $value The header value(s)
  566. * @return array List of headers to be sent
  567. */
  568. public function header($header = null, $value = null)
  569. {
  570. if ($header === null) {
  571. return $this->_headers;
  572. }
  573. $headers = is_array($header) ? $header : [$header => $value];
  574. foreach ($headers as $header => $value) {
  575. if (is_numeric($header)) {
  576. list($header, $value) = [$value, null];
  577. }
  578. if ($value === null) {
  579. list($header, $value) = explode(':', $header, 2);
  580. }
  581. $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value);
  582. }
  583. return $this->_headers;
  584. }
  585. /**
  586. * Accessor for the location header.
  587. *
  588. * Get/Set the Location header value.
  589. *
  590. * @param null|string $url Either null to get the current location, or a string to set one.
  591. * @return string|null When setting the location null will be returned. When reading the location
  592. * a string of the current location header value (if any) will be returned.
  593. */
  594. public function location($url = null)
  595. {
  596. if ($url === null) {
  597. $headers = $this->header();
  598. return isset($headers['Location']) ? $headers['Location'] : null;
  599. }
  600. $this->header('Location', $url);
  601. return null;
  602. }
  603. /**
  604. * Buffers the response message to be sent
  605. * if $content is null the current buffer is returned
  606. *
  607. * @param string|callable|null $content the string or callable message to be sent
  608. * @return string Current message buffer if $content param is passed as null
  609. */
  610. public function body($content = null)
  611. {
  612. if ($content === null) {
  613. return $this->_body;
  614. }
  615. return $this->_body = $content;
  616. }
  617. /**
  618. * Sets the HTTP status code to be sent
  619. * if $code is null the current code is returned
  620. *
  621. * @param int|null $code the HTTP status code
  622. * @return int Current status code
  623. * @throws \InvalidArgumentException When an unknown status code is reached.
  624. */
  625. public function statusCode($code = null)
  626. {
  627. if ($code === null) {
  628. return $this->_status;
  629. }
  630. if (!isset($this->_statusCodes[$code])) {
  631. throw new InvalidArgumentException('Unknown status code');
  632. }
  633. return $this->_status = $code;
  634. }
  635. /**
  636. * Queries & sets valid HTTP response codes & messages.
  637. *
  638. * @param int|array|null $code If $code is an integer, then the corresponding code/message is
  639. * returned if it exists, null if it does not exist. If $code is an array, then the
  640. * keys are used as codes and the values as messages to add to the default HTTP
  641. * codes. The codes must be integers greater than 99 and less than 1000. Keep in
  642. * mind that the HTTP specification outlines that status codes begin with a digit
  643. * between 1 and 5, which defines the class of response the client is to expect.
  644. * Example:
  645. *
  646. * httpCodes(404); // returns [404 => 'Not Found']
  647. *
  648. * httpCodes([
  649. * 381 => 'Unicorn Moved',
  650. * 555 => 'Unexpected Minotaur'
  651. * ]); // sets these new values, and returns true
  652. *
  653. * httpCodes([
  654. * 0 => 'Nothing Here',
  655. * -1 => 'Reverse Infinity',
  656. * 12345 => 'Universal Password',
  657. * 'Hello' => 'World'
  658. * ]); // throws an exception due to invalid codes
  659. *
  660. * For more on HTTP status codes see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
  661. *
  662. * @return mixed Associative array of the HTTP codes as keys, and the message
  663. * strings as values, or null of the given $code does not exist.
  664. * @throws \InvalidArgumentException If an attempt is made to add an invalid status code
  665. */
  666. public function httpCodes($code = null)
  667. {
  668. if (empty($code)) {
  669. return $this->_statusCodes;
  670. }
  671. if (is_array($code)) {
  672. $codes = array_keys($code);
  673. $min = min($codes);
  674. if (!is_int($min) || $min < 100 || max($codes) > 999) {
  675. throw new InvalidArgumentException('Invalid status code');
  676. }
  677. $this->_statusCodes = $code + $this->_statusCodes;
  678. return true;
  679. }
  680. if (!isset($this->_statusCodes[$code])) {
  681. return null;
  682. }
  683. return [$code => $this->_statusCodes[$code]];
  684. }
  685. /**
  686. * Sets the response content type. It can be either a file extension
  687. * which will be mapped internally to a mime-type or a string representing a mime-type
  688. * if $contentType is null the current content type is returned
  689. * if $contentType is an associative array, content type definitions will be stored/replaced
  690. *
  691. * ### Setting the content type
  692. *
  693. * ```
  694. * type('jpg');
  695. * ```
  696. *
  697. * ### Returning the current content type
  698. *
  699. * ```
  700. * type();
  701. * ```
  702. *
  703. * ### Storing content type definitions
  704. *
  705. * ```
  706. * type(['keynote' => 'application/keynote', 'bat' => 'application/bat']);
  707. * ```
  708. *
  709. * ### Replacing a content type definition
  710. *
  711. * ```
  712. * type(['jpg' => 'text/plain']);
  713. * ```
  714. *
  715. * @param string|null $contentType Content type key.
  716. * @return mixed Current content type or false if supplied an invalid content type
  717. */
  718. public function type($contentType = null)
  719. {
  720. if ($contentType === null) {
  721. return $this->_contentType;
  722. }
  723. if (is_array($contentType)) {
  724. foreach ($contentType as $type => $definition) {
  725. $this->_mimeTypes[$type] = $definition;
  726. }
  727. return $this->_contentType;
  728. }
  729. if (isset($this->_mimeTypes[$contentType])) {
  730. $contentType = $this->_mimeTypes[$contentType];
  731. $contentType = is_array($contentType) ? current($contentType) : $contentType;
  732. }
  733. if (strpos($contentType, '/') === false) {
  734. return false;
  735. }
  736. return $this->_contentType = $contentType;
  737. }
  738. /**
  739. * Returns the mime type definition for an alias
  740. *
  741. * e.g `getMimeType('pdf'); // returns 'application/pdf'`
  742. *
  743. * @param string $alias the content type alias to map
  744. * @return mixed String mapped mime type or false if $alias is not mapped
  745. */
  746. public function getMimeType($alias)
  747. {
  748. if (isset($this->_mimeTypes[$alias])) {
  749. return $this->_mimeTypes[$alias];
  750. }
  751. return false;
  752. }
  753. /**
  754. * Maps a content-type back to an alias
  755. *
  756. * e.g `mapType('application/pdf'); // returns 'pdf'`
  757. *
  758. * @param string|array $ctype Either a string content type to map, or an array of types.
  759. * @return mixed Aliases for the types provided.
  760. */
  761. public function mapType($ctype)
  762. {
  763. if (is_array($ctype)) {
  764. return array_map([$this, 'mapType'], $ctype);
  765. }
  766. foreach ($this->_mimeTypes as $alias => $types) {
  767. if (in_array($ctype, (array)$types)) {
  768. return $alias;
  769. }
  770. }
  771. return null;
  772. }
  773. /**
  774. * Sets the response charset
  775. * if $charset is null the current charset is returned
  776. *
  777. * @param string|null $charset Character set string.
  778. * @return string Current charset
  779. */
  780. public function charset($charset = null)
  781. {
  782. if ($charset === null) {
  783. return $this->_charset;
  784. }
  785. return $this->_charset = $charset;
  786. }
  787. /**
  788. * Sets the correct headers to instruct the client to not cache the response
  789. *
  790. * @return void
  791. */
  792. public function disableCache()
  793. {
  794. $this->header([
  795. 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
  796. 'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT",
  797. 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
  798. ]);
  799. }
  800. /**
  801. * Sets the correct headers to instruct the client to cache the response.
  802. *
  803. * @param string $since a valid time since the response text has not been modified
  804. * @param string $time a valid time for cache expiry
  805. * @return void
  806. */
  807. public function cache($since, $time = '+1 day')
  808. {
  809. if (!is_int($time)) {
  810. $time = strtotime($time);
  811. }
  812. $this->header([
  813. 'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT'
  814. ]);
  815. $this->modified($since);
  816. $this->expires($time);
  817. $this->sharable(true);
  818. $this->maxAge($time - time());
  819. }
  820. /**
  821. * Sets whether a response is eligible to be cached by intermediate proxies
  822. * This method controls the `public` or `private` directive in the Cache-Control
  823. * header
  824. *
  825. * @param bool|null $public If set to true, the Cache-Control header will be set as public
  826. * if set to false, the response will be set to private
  827. * if no value is provided, it will return whether the response is sharable or not
  828. * @param int|null $time time in seconds after which the response should no longer be considered fresh
  829. * @return bool|null
  830. */
  831. public function sharable($public = null, $time = null)
  832. {
  833. if ($public === null) {
  834. $public = array_key_exists('public', $this->_cacheDirectives);
  835. $private = array_key_exists('private', $this->_cacheDirectives);
  836. $noCache = array_key_exists('no-cache', $this->_cacheDirectives);
  837. if (!$public && !$private && !$noCache) {
  838. return null;
  839. }
  840. $sharable = $public || ! ($private || $noCache);
  841. return $sharable;
  842. }
  843. if ($public) {
  844. $this->_cacheDirectives['public'] = true;
  845. unset($this->_cacheDirectives['private']);
  846. } else {
  847. $this->_cacheDirectives['private'] = true;
  848. unset($this->_cacheDirectives['public']);
  849. }
  850. $this->maxAge($time);
  851. if (!$time) {
  852. $this->_setCacheControl();
  853. }
  854. return (bool)$public;
  855. }
  856. /**
  857. * Sets the Cache-Control s-maxage directive.
  858. * The max-age is the number of seconds after which the response should no longer be considered
  859. * a good candidate to be fetched from a shared cache (like in a proxy server).
  860. * If called with no parameters, this function will return the current max-age value if any
  861. *
  862. * @param int|null $seconds if null, the method will return the current s-maxage value
  863. * @return int|null
  864. */
  865. public function sharedMaxAge($seconds = null)
  866. {
  867. if ($seconds !== null) {
  868. $this->_cacheDirectives['s-maxage'] = $seconds;
  869. $this->_setCacheControl();
  870. }
  871. if (isset($this->_cacheDirectives['s-maxage'])) {
  872. return $this->_cacheDirectives['s-maxage'];
  873. }
  874. return null;
  875. }
  876. /**
  877. * Sets the Cache-Control max-age directive.
  878. * The max-age is the number of seconds after which the response should no longer be considered
  879. * a good candidate to be fetched from the local (client) cache.
  880. * If called with no parameters, this function will return the current max-age value if any
  881. *
  882. * @param int|null $seconds if null, the method will return the current max-age value
  883. * @return int|null
  884. */
  885. public function maxAge($seconds = null)
  886. {
  887. if ($seconds !== null) {
  888. $this->_cacheDirectives['max-age'] = $seconds;
  889. $this->_setCacheControl();
  890. }
  891. if (isset($this->_cacheDirectives['max-age'])) {
  892. return $this->_cacheDirectives['max-age'];
  893. }
  894. return null;
  895. }
  896. /**
  897. * Sets the Cache-Control must-revalidate directive.
  898. * must-revalidate indicates that the response should not be served
  899. * stale by a cache under any circumstance without first revalidating
  900. * with the origin.
  901. * If called with no parameters, this function will return whether must-revalidate is present.
  902. *
  903. * @param bool|null $enable if null, the method will return the current
  904. * must-revalidate value. If boolean sets or unsets the directive.
  905. * @return bool
  906. */
  907. public function mustRevalidate($enable = null)
  908. {
  909. if ($enable !== null) {
  910. if ($enable) {
  911. $this->_cacheDirectives['must-revalidate'] = true;
  912. } else {
  913. unset($this->_cacheDirectives['must-revalidate']);
  914. }
  915. $this->_setCacheControl();
  916. }
  917. return array_key_exists('must-revalidate', $this->_cacheDirectives);
  918. }
  919. /**
  920. * Helper method to generate a valid Cache-Control header from the options set
  921. * in other methods
  922. *
  923. * @return void
  924. */
  925. protected function _setCacheControl()
  926. {
  927. $control = '';
  928. foreach ($this->_cacheDirectives as $key => $val) {
  929. $control .= $val === true ? $key : sprintf('%s=%s', $key, $val);
  930. $control .= ', ';
  931. }
  932. $control = rtrim($control, ', ');
  933. $this->header('Cache-Control', $control);
  934. }
  935. /**
  936. * Sets the Expires header for the response by taking an expiration time
  937. * If called with no parameters it will return the current Expires value
  938. *
  939. * ### Examples:
  940. *
  941. * `$response->expires('now')` Will Expire the response cache now
  942. * `$response->expires(new DateTime('+1 day'))` Will set the expiration in next 24 hours
  943. * `$response->expires()` Will return the current expiration header value
  944. *
  945. * @param string|\DateTime|null $time Valid time string or \DateTime instance.
  946. * @return string|null
  947. */
  948. public function expires($time = null)
  949. {
  950. if ($time !== null) {
  951. $date = $this->_getUTCDate($time);
  952. $this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT';
  953. }
  954. if (isset($this->_headers['Expires'])) {
  955. return $this->_headers['Expires'];
  956. }
  957. return null;
  958. }
  959. /**
  960. * Sets the Last-Modified header for the response by taking a modification time
  961. * If called with no parameters it will return the current Last-Modified value
  962. *
  963. * ### Examples:
  964. *
  965. * `$response->modified('now')` Will set the Last-Modified to the current time
  966. * `$response->modified(new DateTime('+1 day'))` Will set the modification date in the past 24 hours
  967. * `$response->modified()` Will return the current Last-Modified header value
  968. *
  969. * @param string|\DateTime|null $time Valid time string or \DateTime instance.
  970. * @return string|null
  971. */
  972. public function modified($time = null)
  973. {
  974. if ($time !== null) {
  975. $date = $this->_getUTCDate($time);
  976. $this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT';
  977. }
  978. if (isset($this->_headers['Last-Modified'])) {
  979. return $this->_headers['Last-Modified'];
  980. }
  981. return null;
  982. }
  983. /**
  984. * Sets the response as Not Modified by removing any body contents
  985. * setting the status code to "304 Not Modified" and removing all
  986. * conflicting headers
  987. *
  988. * @return void
  989. */
  990. public function notModified()
  991. {
  992. $this->statusCode(304);
  993. $this->body('');
  994. $remove = [
  995. 'Allow',
  996. 'Content-Encoding',
  997. 'Content-Language',
  998. 'Content-Length',
  999. 'Content-MD5',
  1000. 'Content-Type',
  1001. 'Last-Modified'
  1002. ];
  1003. foreach ($remove as $header) {
  1004. unset($this->_headers[$header]);
  1005. }
  1006. }
  1007. /**
  1008. * Sets the Vary header for the response, if an array is passed,
  1009. * values will be imploded into a comma separated string. If no
  1010. * parameters are passed, then an array with the current Vary header
  1011. * value is returned
  1012. *
  1013. * @param string|array|null $cacheVariances A single Vary string or an array
  1014. * containing the list for variances.
  1015. * @return array|null
  1016. */
  1017. public function vary($cacheVariances = null)
  1018. {
  1019. if ($cacheVariances !== null) {
  1020. $cacheVariances = (array)$cacheVariances;
  1021. $this->_headers['Vary'] = implode(', ', $cacheVariances);
  1022. }
  1023. if (isset($this->_headers['Vary'])) {
  1024. return explode(', ', $this->_headers['Vary']);
  1025. }
  1026. return null;
  1027. }
  1028. /**
  1029. * Sets the response Etag, Etags are a strong indicative that a response
  1030. * can be cached by a HTTP client. A bad way of generating Etags is
  1031. * creating a hash of the response output, instead generate a unique
  1032. * hash of the unique components that identifies a request, such as a
  1033. * modification time, a resource Id, and anything else you consider it
  1034. * makes it unique.
  1035. *
  1036. * Second parameter is used to instruct clients that the content has
  1037. * changed, but semantically, it can be used as the same thing. Think
  1038. * for instance of a page with a hit counter, two different page views
  1039. * are equivalent, but they differ by a few bytes. This leaves off to
  1040. * the Client the decision of using or not the cached page.
  1041. *
  1042. * If no parameters are passed, current Etag header is returned.
  1043. *
  1044. * @param string|null $hash The unique hash that identifies this response
  1045. * @param bool $weak Whether the response is semantically the same as
  1046. * other with the same hash or not
  1047. * @return string|null
  1048. */
  1049. public function etag($hash = null, $weak = false)
  1050. {
  1051. if ($hash !== null) {
  1052. $this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $hash);
  1053. }
  1054. if (isset($this->_headers['Etag'])) {
  1055. return $this->_headers['Etag'];
  1056. }
  1057. return null;
  1058. }
  1059. /**
  1060. * Returns a DateTime object initialized at the $time param and using UTC
  1061. * as timezone
  1062. *
  1063. * @param string|int|\DateTime|null $time Valid time string or \DateTime instance.
  1064. * @return \DateTime
  1065. */
  1066. protected function _getUTCDate($time = null)
  1067. {
  1068. if ($time instanceof \DateTime) {
  1069. $result = clone $time;
  1070. } elseif (is_int($time)) {
  1071. $result = new \DateTime(date('Y-m-d H:i:s', $time));
  1072. } else {
  1073. $result = new \DateTime($time);
  1074. }
  1075. $result->setTimeZone(new \DateTimeZone('UTC'));
  1076. return $result;
  1077. }
  1078. /**
  1079. * Sets the correct output buffering handler to send a compressed response. Responses will
  1080. * be compressed with zlib, if the extension is available.
  1081. *
  1082. * @return bool false if client does not accept compressed responses or no handler is available, true otherwise
  1083. */
  1084. public function compress()
  1085. {
  1086. $compressionEnabled = ini_get("zlib.output_compression") !== '1' &&
  1087. extension_loaded("zlib") &&
  1088. (strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false);
  1089. return $compressionEnabled && ob_start('ob_gzhandler');
  1090. }
  1091. /**
  1092. * Returns whether the resulting output will be compressed by PHP
  1093. *
  1094. * @return bool
  1095. */
  1096. public function outputCompressed()
  1097. {
  1098. return strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false
  1099. && (ini_get("zlib.output_compression") === '1' || in_array('ob_gzhandler', ob_list_handlers()));
  1100. }
  1101. /**
  1102. * Sets the correct headers to instruct the browser to download the response as a file.
  1103. *
  1104. * @param string $filename The name of the file as the browser will download the response
  1105. * @return void
  1106. */
  1107. public function download($filename)
  1108. {
  1109. $this->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
  1110. }
  1111. /**
  1112. * Sets the protocol to be used when sending the response. Defaults to HTTP/1.1
  1113. * If called with no arguments, it will return the current configured protocol
  1114. *
  1115. * @param string|null $protocol Protocol to be used for sending response.
  1116. * @return string Protocol currently set
  1117. */
  1118. public function protocol($protocol = null)
  1119. {
  1120. if ($protocol !== null) {
  1121. $this->_protocol = $protocol;
  1122. }
  1123. return $this->_protocol;
  1124. }
  1125. /**
  1126. * Sets the Content-Length header for the response
  1127. * If called with no arguments returns the last Content-Length set
  1128. *
  1129. * @param int|null $bytes Number of bytes
  1130. * @return int|null
  1131. */
  1132. public function length($bytes = null)
  1133. {
  1134. if ($bytes !== null) {
  1135. $this->_headers['Content-Length'] = $bytes;
  1136. }
  1137. if (isset($this->_headers['Content-Length'])) {
  1138. return $this->_headers['Content-Length'];
  1139. }
  1140. return null;
  1141. }
  1142. /**
  1143. * Checks whether a response has not been modified according to the 'If-None-Match'
  1144. * (Etags) and 'If-Modified-Since' (last modification date) request
  1145. * headers. If the response is detected to be not modified, it
  1146. * is marked as so accordingly so the client can be informed of that.
  1147. *
  1148. * In order to mark a response as not modified, you need to set at least
  1149. * the Last-Modified etag response header before calling this method. Otherwise
  1150. * a comparison will not be possible.
  1151. *
  1152. * @param \Cake\Network\Request $request Request object
  1153. * @return bool Whether the response was marked as not modified or not.
  1154. */
  1155. public function checkNotModified(Request $request)
  1156. {
  1157. $etags = preg_split('/\s*,\s*/', $request->header('If-None-Match'), null, PREG_SPLIT_NO_EMPTY);
  1158. $modifiedSince = $request->header('If-Modified-Since');
  1159. if ($responseTag = $this->etag()) {
  1160. $etagMatches = in_array('*', $etags) || in_array($responseTag, $etags);
  1161. }
  1162. if ($modifiedSince) {
  1163. $timeMatches = strtotime($this->modified()) === strtotime($modifiedSince);
  1164. }
  1165. $checks = compact('etagMatches', 'timeMatches');
  1166. if (empty($checks)) {
  1167. return false;
  1168. }
  1169. $notModified = !in_array(false, $checks, true);
  1170. if ($notModified) {
  1171. $this->notModified();
  1172. }
  1173. return $notModified;
  1174. }
  1175. /**
  1176. * String conversion. Fetches the response body as a string.
  1177. * Does *not* send headers.
  1178. * If body is a callable, a blank string is returned.
  1179. *
  1180. * @return string
  1181. */
  1182. public function __toString()
  1183. {
  1184. if (!is_string($this->_body) && is_callable($this->_body)) {
  1185. return '';
  1186. }
  1187. return (string)$this->_body;
  1188. }
  1189. /**
  1190. * Getter/Setter for cookie configs
  1191. *
  1192. * This method acts as a setter/getter depending on the type of the argument.
  1193. * If the method is called with no arguments, it returns all configurations.
  1194. *
  1195. * If the method is called with a string as argument, it returns either the
  1196. * given configuration if it is set, or null, if it's not set.
  1197. *
  1198. * If the method is called with an array as argument, it will set the cookie
  1199. * configuration to the cookie container.
  1200. *
  1201. * ### Options (when setting a configuration)
  1202. * - name: The Cookie name
  1203. * - value: Value of the cookie
  1204. * - expire: Time the cookie expires in
  1205. * - path: Path the cookie applies to
  1206. * - domain: Domain the cookie is for.
  1207. * - secure: Is the cookie https?
  1208. * - httpOnly: Is the cookie available in the client?
  1209. *
  1210. * ### Examples
  1211. *
  1212. * ### Getting all cookies
  1213. *
  1214. * `$this->cookie()`
  1215. *
  1216. * ### Getting a certain cookie configuration
  1217. *
  1218. * `$this->cookie('MyCookie')`
  1219. *
  1220. * ### Setting a cookie configuration
  1221. *
  1222. * `$this->cookie((array) $options)`
  1223. *
  1224. * @param array|null $options Either null to get all cookies, string for a specific cookie
  1225. * or array to set cookie.
  1226. * @return mixed
  1227. */
  1228. public function cookie($options = null)
  1229. {
  1230. if ($options === null) {
  1231. return $this->_cookies;
  1232. }
  1233. if (is_string($options)) {
  1234. if (!isset($this->_cookies[$options])) {
  1235. return null;
  1236. }
  1237. return $this->_cookies[$options];
  1238. }
  1239. $defaults = [
  1240. 'name' => 'CakeCookie[default]',
  1241. 'value' => '',
  1242. 'expire' => 0,
  1243. 'path' => '/',
  1244. 'domain' => '',
  1245. 'secure' => false,
  1246. 'httpOnly' => false
  1247. ];
  1248. $options += $defaults;
  1249. $this->_cookies[$options['name']] = $options;
  1250. }
  1251. /**
  1252. * Setup access for origin and methods on cross origin requests
  1253. *
  1254. * This method allow multiple ways to setup the domains, see the examples
  1255. *
  1256. * ### Full URI
  1257. * ```
  1258. * cors($request, 'http://www.cakephp.org');
  1259. * ```
  1260. *
  1261. * ### URI with wildcard
  1262. * ```
  1263. * cors($request, 'http://*.cakephp.org');
  1264. * ```
  1265. *
  1266. * ### Ignoring the requested protocol
  1267. * ```
  1268. * cors($request, 'www.cakephp.org');
  1269. * ```
  1270. *
  1271. * ### Any URI
  1272. * ```
  1273. * cors($request, '*');
  1274. * ```
  1275. *
  1276. * ### Whitelist of URIs
  1277. * ```
  1278. * cors($request, ['http://www.cakephp.org', '*.google.com', 'https://myproject.github.io']);
  1279. * ```
  1280. *
  1281. * @param \Cake\Network\Request $request Request object
  1282. * @param string|array $allowedDomains List of allowed domains, see method description for more details
  1283. * @param string|array $allowedMethods List of HTTP verbs allowed
  1284. * @param string|array $allowedHeaders List of HTTP headers allowed
  1285. * @return void
  1286. */
  1287. public function cors(Request $request, $allowedDomains, $allowedMethods = [], $allowedHeaders = [])
  1288. {
  1289. $origin = $request->header('Origin');
  1290. if (!$origin) {
  1291. return;
  1292. }
  1293. $allowedDomains = $this->_normalizeCorsDomains((array)$allowedDomains, $request->is('ssl'));
  1294. foreach ($allowedDomains as $domain) {
  1295. if (!preg_match($domain['preg'], $origin)) {
  1296. continue;
  1297. }
  1298. $this->header('Access-Control-Allow-Origin', $domain['original'] === '*' ? '*' : $origin);
  1299. $allowedMethods && $this->header('Access-Control-Allow-Methods', implode(', ', (array)$allowedMethods));
  1300. $allowedHeaders && $this->header('Access-Control-Allow-Headers', implode(', ', (array)$allowedHeaders));
  1301. break;
  1302. }
  1303. }
  1304. /**
  1305. * Normalize the origin to regular expressions and put in an array format
  1306. *
  1307. * @param array $domains Domain names to normalize.
  1308. * @param bool $requestIsSSL Whether it's a SSL request.
  1309. * @return array
  1310. */
  1311. protected function _normalizeCorsDomains($domains, $requestIsSSL = false)
  1312. {
  1313. $result = [];
  1314. foreach ($domains as $domain) {
  1315. if ($domain === '*') {
  1316. $result[] = ['preg' => '@.@', 'original' => '*'];
  1317. continue;
  1318. }
  1319. $original = $preg = $domain;
  1320. if (strpos($domain, '://') === false) {
  1321. $preg = ($requestIsSSL ? 'https://' : 'http://') . $domain;
  1322. }
  1323. $preg = '@' . str_replace('*', '.*', $domain) . '@';
  1324. $result[] = compact('original', 'preg');
  1325. }
  1326. return $result;
  1327. }
  1328. /**
  1329. * Setup for display or download the given file.
  1330. *
  1331. * If $_SERVER['HTTP_RANGE'] is set a slice of the file will be
  1332. * returned instead of the entire file.
  1333. *
  1334. * ### Options keys
  1335. *
  1336. * - name: Alternate download name
  1337. * - download: If `true` sets download header and forces file to be downloaded rather than displayed in browser
  1338. *
  1339. * @param string $path Path to file. If the path is not an absolute path that resolves
  1340. * to a file, `APP` will be prepended to the path.
  1341. * @param array $options Options See above.
  1342. * @return void
  1343. * @throws \Cake\Network\Exception\NotFoundException
  1344. */
  1345. public function file($path, array $options = [])
  1346. {
  1347. $options += [
  1348. 'name' => null,
  1349. 'download' => null
  1350. ];
  1351. if (strpos($path, '..') !== false) {
  1352. throw new NotFoundException('The requested file contains `..` and will not be read.');
  1353. }
  1354. if (!is_file($path)) {
  1355. $path = APP . $path;
  1356. }
  1357. $file = new File($path);
  1358. if (!$file->exists() || !$file->readable()) {
  1359. if (Configure::read('debug')) {
  1360. throw new NotFoundException(sprintf('The requested file %s was not found or not readable', $path));
  1361. }
  1362. throw new NotFoundException(__d('cake', 'The requested file was not found'));
  1363. }
  1364. $extension = strtolower($file->ext());
  1365. $download = $options['download'];
  1366. if ((!$extension || $this->type($extension) === false) && $download === null) {
  1367. $download = true;
  1368. }
  1369. $fileSize = $file->size();
  1370. if ($download) {
  1371. $agent = env('HTTP_USER_AGENT');
  1372. if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) {
  1373. $contentType = 'application/octet-stream';
  1374. } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
  1375. $contentType = 'application/force-download';
  1376. }
  1377. if (!empty($contentType)) {
  1378. $this->type($contentType);
  1379. }
  1380. if ($options['name'] === null) {
  1381. $name = $file->name;
  1382. } else {
  1383. $name = $options['name'];
  1384. }
  1385. $this->download($name);
  1386. $this->header('Content-Transfer-Encoding', 'binary');
  1387. }
  1388. $this->header('Accept-Ranges', 'bytes');
  1389. $httpRange = env('HTTP_RANGE');
  1390. if (isset($httpRange)) {
  1391. $this->_fileRange($file, $httpRange);
  1392. } else {
  1393. $this->header('Content-Length', $fileSize);
  1394. }
  1395. $this->_clearBuffer();
  1396. $this->_file = $file;
  1397. }
  1398. /**
  1399. * Apply a file range to a file and set the end offset.
  1400. *
  1401. * If an invalid range is requested a 416 Status code will be used
  1402. * in the response.
  1403. *
  1404. * @param File $file The file to set a range on.
  1405. * @param string $httpRange The range to use.
  1406. * @return void
  1407. */
  1408. protected function _fileRange($file, $httpRange)
  1409. {
  1410. list(, $range) = explode('=', $httpRange);
  1411. list($start, $end) = explode('-', $range);
  1412. $fileSize = $file->size();
  1413. $lastByte = $fileSize - 1;
  1414. if ($start === '') {
  1415. $start = $fileSize - $end;
  1416. $end =

Large files files are truncated, but you can click here to view the full file