1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
18:
19: App::uses('File', 'Utility');
20:
21: 22: 23: 24: 25: 26: 27: 28:
29: class CakeResponse {
30:
31: 32: 33: 34: 35:
36: protected $_statusCodes = array(
37: 100 => 'Continue',
38: 101 => 'Switching Protocols',
39: 200 => 'OK',
40: 201 => 'Created',
41: 202 => 'Accepted',
42: 203 => 'Non-Authoritative Information',
43: 204 => 'No Content',
44: 205 => 'Reset Content',
45: 206 => 'Partial Content',
46: 300 => 'Multiple Choices',
47: 301 => 'Moved Permanently',
48: 302 => 'Found',
49: 303 => 'See Other',
50: 304 => 'Not Modified',
51: 305 => 'Use Proxy',
52: 307 => 'Temporary Redirect',
53: 400 => 'Bad Request',
54: 401 => 'Unauthorized',
55: 402 => 'Payment Required',
56: 403 => 'Forbidden',
57: 404 => 'Not Found',
58: 405 => 'Method Not Allowed',
59: 406 => 'Not Acceptable',
60: 407 => 'Proxy Authentication Required',
61: 408 => 'Request Time-out',
62: 409 => 'Conflict',
63: 410 => 'Gone',
64: 411 => 'Length Required',
65: 412 => 'Precondition Failed',
66: 413 => 'Request Entity Too Large',
67: 414 => 'Request-URI Too Large',
68: 415 => 'Unsupported Media Type',
69: 416 => 'Requested range not satisfiable',
70: 417 => 'Expectation Failed',
71: 500 => 'Internal Server Error',
72: 501 => 'Not Implemented',
73: 502 => 'Bad Gateway',
74: 503 => 'Service Unavailable',
75: 504 => 'Gateway Time-out',
76: 505 => 'Unsupported Version'
77: );
78:
79: 80: 81: 82: 83:
84: protected $_mimeTypes = array(
85: 'html' => array('text/html', '*/*'),
86: 'json' => 'application/json',
87: 'xml' => array('application/xml', 'text/xml'),
88: 'rss' => 'application/rss+xml',
89: 'ai' => 'application/postscript',
90: 'bcpio' => 'application/x-bcpio',
91: 'bin' => 'application/octet-stream',
92: 'ccad' => 'application/clariscad',
93: 'cdf' => 'application/x-netcdf',
94: 'class' => 'application/octet-stream',
95: 'cpio' => 'application/x-cpio',
96: 'cpt' => 'application/mac-compactpro',
97: 'csh' => 'application/x-csh',
98: 'csv' => array('text/csv', 'application/vnd.ms-excel', 'text/plain'),
99: 'dcr' => 'application/x-director',
100: 'dir' => 'application/x-director',
101: 'dms' => 'application/octet-stream',
102: 'doc' => 'application/msword',
103: 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
104: 'drw' => 'application/drafting',
105: 'dvi' => 'application/x-dvi',
106: 'dwg' => 'application/acad',
107: 'dxf' => 'application/dxf',
108: 'dxr' => 'application/x-director',
109: 'eot' => 'application/vnd.ms-fontobject',
110: 'eps' => 'application/postscript',
111: 'exe' => 'application/octet-stream',
112: 'ez' => 'application/andrew-inset',
113: 'flv' => 'video/x-flv',
114: 'gtar' => 'application/x-gtar',
115: 'gz' => 'application/x-gzip',
116: 'bz2' => 'application/x-bzip',
117: '7z' => 'application/x-7z-compressed',
118: 'hdf' => 'application/x-hdf',
119: 'hqx' => 'application/mac-binhex40',
120: 'ico' => 'image/x-icon',
121: 'ips' => 'application/x-ipscript',
122: 'ipx' => 'application/x-ipix',
123: 'js' => 'application/javascript',
124: 'latex' => 'application/x-latex',
125: 'lha' => 'application/octet-stream',
126: 'lsp' => 'application/x-lisp',
127: 'lzh' => 'application/octet-stream',
128: 'man' => 'application/x-troff-man',
129: 'me' => 'application/x-troff-me',
130: 'mif' => 'application/vnd.mif',
131: 'ms' => 'application/x-troff-ms',
132: 'nc' => 'application/x-netcdf',
133: 'oda' => 'application/oda',
134: 'otf' => 'font/otf',
135: 'pdf' => 'application/pdf',
136: 'pgn' => 'application/x-chess-pgn',
137: 'pot' => 'application/vnd.ms-powerpoint',
138: 'pps' => 'application/vnd.ms-powerpoint',
139: 'ppt' => 'application/vnd.ms-powerpoint',
140: 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
141: 'ppz' => 'application/vnd.ms-powerpoint',
142: 'pre' => 'application/x-freelance',
143: 'prt' => 'application/pro_eng',
144: 'ps' => 'application/postscript',
145: 'roff' => 'application/x-troff',
146: 'scm' => 'application/x-lotusscreencam',
147: 'set' => 'application/set',
148: 'sh' => 'application/x-sh',
149: 'shar' => 'application/x-shar',
150: 'sit' => 'application/x-stuffit',
151: 'skd' => 'application/x-koan',
152: 'skm' => 'application/x-koan',
153: 'skp' => 'application/x-koan',
154: 'skt' => 'application/x-koan',
155: 'smi' => 'application/smil',
156: 'smil' => 'application/smil',
157: 'sol' => 'application/solids',
158: 'spl' => 'application/x-futuresplash',
159: 'src' => 'application/x-wais-source',
160: 'step' => 'application/STEP',
161: 'stl' => 'application/SLA',
162: 'stp' => 'application/STEP',
163: 'sv4cpio' => 'application/x-sv4cpio',
164: 'sv4crc' => 'application/x-sv4crc',
165: 'svg' => 'image/svg+xml',
166: 'svgz' => 'image/svg+xml',
167: 'swf' => 'application/x-shockwave-flash',
168: 't' => 'application/x-troff',
169: 'tar' => 'application/x-tar',
170: 'tcl' => 'application/x-tcl',
171: 'tex' => 'application/x-tex',
172: 'texi' => 'application/x-texinfo',
173: 'texinfo' => 'application/x-texinfo',
174: 'tr' => 'application/x-troff',
175: 'tsp' => 'application/dsptype',
176: 'ttc' => 'font/ttf',
177: 'ttf' => 'font/ttf',
178: 'unv' => 'application/i-deas',
179: 'ustar' => 'application/x-ustar',
180: 'vcd' => 'application/x-cdlink',
181: 'vda' => 'application/vda',
182: 'xlc' => 'application/vnd.ms-excel',
183: 'xll' => 'application/vnd.ms-excel',
184: 'xlm' => 'application/vnd.ms-excel',
185: 'xls' => 'application/vnd.ms-excel',
186: 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
187: 'xlw' => 'application/vnd.ms-excel',
188: 'zip' => 'application/zip',
189: 'aif' => 'audio/x-aiff',
190: 'aifc' => 'audio/x-aiff',
191: 'aiff' => 'audio/x-aiff',
192: 'au' => 'audio/basic',
193: 'kar' => 'audio/midi',
194: 'mid' => 'audio/midi',
195: 'midi' => 'audio/midi',
196: 'mp2' => 'audio/mpeg',
197: 'mp3' => 'audio/mpeg',
198: 'mpga' => 'audio/mpeg',
199: 'ogg' => 'audio/ogg',
200: 'oga' => 'audio/ogg',
201: 'spx' => 'audio/ogg',
202: 'ra' => 'audio/x-realaudio',
203: 'ram' => 'audio/x-pn-realaudio',
204: 'rm' => 'audio/x-pn-realaudio',
205: 'rpm' => 'audio/x-pn-realaudio-plugin',
206: 'snd' => 'audio/basic',
207: 'tsi' => 'audio/TSP-audio',
208: 'wav' => 'audio/x-wav',
209: 'aac' => 'audio/aac',
210: 'asc' => 'text/plain',
211: 'c' => 'text/plain',
212: 'cc' => 'text/plain',
213: 'css' => 'text/css',
214: 'etx' => 'text/x-setext',
215: 'f' => 'text/plain',
216: 'f90' => 'text/plain',
217: 'h' => 'text/plain',
218: 'hh' => 'text/plain',
219: 'htm' => array('text/html', '*/*'),
220: 'ics' => 'text/calendar',
221: 'm' => 'text/plain',
222: 'rtf' => 'text/rtf',
223: 'rtx' => 'text/richtext',
224: 'sgm' => 'text/sgml',
225: 'sgml' => 'text/sgml',
226: 'tsv' => 'text/tab-separated-values',
227: 'tpl' => 'text/template',
228: 'txt' => 'text/plain',
229: 'text' => 'text/plain',
230: 'avi' => 'video/x-msvideo',
231: 'fli' => 'video/x-fli',
232: 'mov' => 'video/quicktime',
233: 'movie' => 'video/x-sgi-movie',
234: 'mpe' => 'video/mpeg',
235: 'mpeg' => 'video/mpeg',
236: 'mpg' => 'video/mpeg',
237: 'qt' => 'video/quicktime',
238: 'viv' => 'video/vnd.vivo',
239: 'vivo' => 'video/vnd.vivo',
240: 'ogv' => 'video/ogg',
241: 'webm' => 'video/webm',
242: 'mp4' => 'video/mp4',
243: 'm4v' => 'video/mp4',
244: 'f4v' => 'video/mp4',
245: 'f4p' => 'video/mp4',
246: 'm4a' => 'audio/mp4',
247: 'f4a' => 'audio/mp4',
248: 'f4b' => 'audio/mp4',
249: 'gif' => 'image/gif',
250: 'ief' => 'image/ief',
251: 'jpg' => 'image/jpeg',
252: 'jpeg' => 'image/jpeg',
253: 'jpe' => 'image/jpeg',
254: 'pbm' => 'image/x-portable-bitmap',
255: 'pgm' => 'image/x-portable-graymap',
256: 'png' => 'image/png',
257: 'pnm' => 'image/x-portable-anymap',
258: 'ppm' => 'image/x-portable-pixmap',
259: 'ras' => 'image/cmu-raster',
260: 'rgb' => 'image/x-rgb',
261: 'tif' => 'image/tiff',
262: 'tiff' => 'image/tiff',
263: 'xbm' => 'image/x-xbitmap',
264: 'xpm' => 'image/x-xpixmap',
265: 'xwd' => 'image/x-xwindowdump',
266: 'ice' => 'x-conference/x-cooltalk',
267: 'iges' => 'model/iges',
268: 'igs' => 'model/iges',
269: 'mesh' => 'model/mesh',
270: 'msh' => 'model/mesh',
271: 'silo' => 'model/mesh',
272: 'vrml' => 'model/vrml',
273: 'wrl' => 'model/vrml',
274: 'mime' => 'www/mime',
275: 'pdb' => 'chemical/x-pdb',
276: 'xyz' => 'chemical/x-pdb',
277: 'javascript' => 'application/javascript',
278: 'form' => 'application/x-www-form-urlencoded',
279: 'file' => 'multipart/form-data',
280: 'xhtml' => array('application/xhtml+xml', 'application/xhtml', 'text/xhtml'),
281: 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
282: 'atom' => 'application/atom+xml',
283: 'amf' => 'application/x-amf',
284: 'wap' => array('text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'),
285: 'wml' => 'text/vnd.wap.wml',
286: 'wmlscript' => 'text/vnd.wap.wmlscript',
287: 'wbmp' => 'image/vnd.wap.wbmp',
288: 'woff' => 'application/x-font-woff',
289: 'webp' => 'image/webp',
290: 'appcache' => 'text/cache-manifest',
291: 'manifest' => 'text/cache-manifest',
292: 'htc' => 'text/x-component',
293: 'rdf' => 'application/xml',
294: 'crx' => 'application/x-chrome-extension',
295: 'oex' => 'application/x-opera-extension',
296: 'xpi' => 'application/x-xpinstall',
297: 'safariextz' => 'application/octet-stream',
298: 'webapp' => 'application/x-web-app-manifest+json',
299: 'vcf' => 'text/x-vcard',
300: 'vtt' => 'text/vtt',
301: 'mkv' => 'video/x-matroska',
302: 'pkpass' => 'application/vnd.apple.pkpass'
303: );
304:
305: 306: 307: 308: 309:
310: protected $_protocol = 'HTTP/1.1';
311:
312: 313: 314: 315: 316:
317: protected $_status = 200;
318:
319: 320: 321: 322: 323: 324:
325: protected $_contentType = 'text/html';
326:
327: 328: 329: 330: 331:
332: protected $_headers = array();
333:
334: 335: 336: 337: 338:
339: protected $_body = null;
340:
341: 342: 343: 344: 345:
346: protected $_file = null;
347:
348: 349: 350: 351: 352:
353: protected $_fileRange = null;
354:
355: 356: 357: 358: 359:
360: protected $_charset = 'UTF-8';
361:
362: 363: 364: 365: 366: 367:
368: protected $_cacheDirectives = array();
369:
370: 371: 372: 373: 374:
375: protected $_cookies = array();
376:
377: 378: 379: 380: 381: 382: 383: 384: 385:
386: public function __construct(array $options = array()) {
387: if (isset($options['body'])) {
388: $this->body($options['body']);
389: }
390: if (isset($options['status'])) {
391: $this->statusCode($options['status']);
392: }
393: if (isset($options['type'])) {
394: $this->type($options['type']);
395: }
396: if (!isset($options['charset'])) {
397: $options['charset'] = Configure::read('App.encoding');
398: }
399: $this->charset($options['charset']);
400: }
401:
402: 403: 404: 405: 406: 407:
408: public function send() {
409: if (isset($this->_headers['Location']) && $this->_status === 200) {
410: $this->statusCode(302);
411: }
412:
413: $codeMessage = $this->_statusCodes[$this->_status];
414: $this->_setCookies();
415: $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}");
416: $this->_setContent();
417: $this->_setContentLength();
418: $this->_setContentType();
419: foreach ($this->_headers as $header => $values) {
420: foreach ((array)$values as $value) {
421: $this->_sendHeader($header, $value);
422: }
423: }
424: if ($this->_file) {
425: $this->_sendFile($this->_file, $this->_fileRange);
426: $this->_file = $this->_fileRange = null;
427: } else {
428: $this->_sendContent($this->_body);
429: }
430: }
431:
432: 433: 434: 435: 436: 437: 438:
439: protected function _setCookies() {
440: foreach ($this->_cookies as $name => $c) {
441: setcookie(
442: $name, $c['value'], $c['expire'], $c['path'],
443: $c['domain'], $c['secure'], $c['httpOnly']
444: );
445: }
446: }
447:
448: 449: 450: 451: 452: 453:
454: protected function _setContentType() {
455: if (in_array($this->_status, array(304, 204))) {
456: return;
457: }
458: $whitelist = array(
459: 'application/javascript', 'application/json', 'application/xml', 'application/rss+xml'
460: );
461:
462: $charset = false;
463: if (
464: $this->_charset &&
465: (strpos($this->_contentType, 'text/') === 0 || in_array($this->_contentType, $whitelist))
466: ) {
467: $charset = true;
468: }
469:
470: if ($charset) {
471: $this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}");
472: } else {
473: $this->header('Content-Type', "{$this->_contentType}");
474: }
475: }
476:
477: 478: 479: 480: 481:
482: protected function _setContent() {
483: if (in_array($this->_status, array(304, 204))) {
484: $this->body('');
485: }
486: }
487:
488: 489: 490: 491: 492: 493:
494: protected function _setContentLength() {
495: $shouldSetLength = !isset($this->_headers['Content-Length']) && !in_array($this->_status, range(301, 307));
496: if (isset($this->_headers['Content-Length']) && $this->_headers['Content-Length'] === false) {
497: unset($this->_headers['Content-Length']);
498: return;
499: }
500: if ($shouldSetLength && !$this->outputCompressed()) {
501: $offset = ob_get_level() ? ob_get_length() : 0;
502: if (ini_get('mbstring.func_overload') & 2 && function_exists('mb_strlen')) {
503: $this->length($offset + mb_strlen($this->_body, '8bit'));
504: } else {
505: $this->length($this->_headers['Content-Length'] = $offset + strlen($this->_body));
506: }
507: }
508: }
509:
510: 511: 512: 513: 514: 515: 516:
517: protected function _sendHeader($name, $value = null) {
518: if (!headers_sent()) {
519: if ($value === null) {
520: header($name);
521: } else {
522: header("{$name}: {$value}");
523: }
524: }
525: }
526:
527: 528: 529: 530: 531: 532:
533: protected function _sendContent($content) {
534: echo $content;
535: }
536:
537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563:
564: public function header($header = null, $value = null) {
565: if ($header === null) {
566: return $this->_headers;
567: }
568: $headers = is_array($header) ? $header : array($header => $value);
569: foreach ($headers as $header => $value) {
570: if (is_numeric($header)) {
571: list($header, $value) = array($value, null);
572: }
573: if ($value === null) {
574: list($header, $value) = explode(':', $header, 2);
575: }
576: $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value);
577: }
578: return $this->_headers;
579: }
580:
581: 582: 583: 584: 585: 586: 587: 588:
589: public function location($url = null) {
590: if ($url === null) {
591: $headers = $this->header();
592: return isset($headers['Location']) ? $headers['Location'] : null;
593: }
594: $this->header('Location', $url);
595: }
596:
597: 598: 599: 600: 601: 602: 603:
604: public function body($content = null) {
605: if ($content === null) {
606: return $this->_body;
607: }
608: return $this->_body = $content;
609: }
610:
611: 612: 613: 614: 615: 616: 617: 618:
619: public function statusCode($code = null) {
620: if ($code === null) {
621: return $this->_status;
622: }
623: if (!isset($this->_statusCodes[$code])) {
624: throw new CakeException(__d('cake_dev', 'Unknown status code'));
625: }
626: return $this->_status = $code;
627: }
628:
629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659:
660: public function httpCodes($code = null) {
661: if (empty($code)) {
662: return $this->_statusCodes;
663: }
664: if (is_array($code)) {
665: $codes = array_keys($code);
666: $min = min($codes);
667: if (!is_int($min) || $min < 100 || max($codes) > 999) {
668: throw new CakeException(__d('cake_dev', 'Invalid status code'));
669: }
670: $this->_statusCodes = $code + $this->_statusCodes;
671: return true;
672: }
673: if (!isset($this->_statusCodes[$code])) {
674: return null;
675: }
676: return array($code => $this->_statusCodes[$code]);
677: }
678:
679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703:
704: public function type($contentType = null) {
705: if ($contentType === null) {
706: return $this->_contentType;
707: }
708: if (is_array($contentType)) {
709: foreach ($contentType as $type => $definition) {
710: $this->_mimeTypes[$type] = $definition;
711: }
712: return $this->_contentType;
713: }
714: if (isset($this->_mimeTypes[$contentType])) {
715: $contentType = $this->_mimeTypes[$contentType];
716: $contentType = is_array($contentType) ? current($contentType) : $contentType;
717: }
718: if (strpos($contentType, '/') === false) {
719: return false;
720: }
721: return $this->_contentType = $contentType;
722: }
723:
724: 725: 726: 727: 728: 729: 730: 731:
732: public function getMimeType($alias) {
733: if (isset($this->_mimeTypes[$alias])) {
734: return $this->_mimeTypes[$alias];
735: }
736: return false;
737: }
738:
739: 740: 741: 742: 743: 744: 745: 746:
747: public function mapType($ctype) {
748: if (is_array($ctype)) {
749: return array_map(array($this, 'mapType'), $ctype);
750: }
751:
752: foreach ($this->_mimeTypes as $alias => $types) {
753: if (in_array($ctype, (array)$types)) {
754: return $alias;
755: }
756: }
757: return null;
758: }
759:
760: 761: 762: 763: 764: 765: 766:
767: public function charset($charset = null) {
768: if ($charset === null) {
769: return $this->_charset;
770: }
771: return $this->_charset = $charset;
772: }
773:
774: 775: 776: 777: 778:
779: public function disableCache() {
780: $this->header(array(
781: 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
782: 'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT",
783: 'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
784: ));
785: }
786:
787: 788: 789: 790: 791: 792: 793:
794: public function cache($since, $time = '+1 day') {
795: if (!is_int($time)) {
796: $time = strtotime($time);
797: }
798: $this->header(array(
799: 'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT'
800: ));
801: $this->modified($since);
802: $this->expires($time);
803: $this->sharable(true);
804: $this->maxAge($time - time());
805: }
806:
807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817:
818: public function sharable($public = null, $time = null) {
819: if ($public === null) {
820: $public = array_key_exists('public', $this->_cacheDirectives);
821: $private = array_key_exists('private', $this->_cacheDirectives);
822: $noCache = array_key_exists('no-cache', $this->_cacheDirectives);
823: if (!$public && !$private && !$noCache) {
824: return null;
825: }
826: $sharable = $public || ! ($private || $noCache);
827: return $sharable;
828: }
829: if ($public) {
830: $this->_cacheDirectives['public'] = true;
831: unset($this->_cacheDirectives['private']);
832: $this->sharedMaxAge($time);
833: } else {
834: $this->_cacheDirectives['private'] = true;
835: unset($this->_cacheDirectives['public']);
836: $this->maxAge($time);
837: }
838: if (!$time) {
839: $this->_setCacheControl();
840: }
841: return (bool)$public;
842: }
843:
844: 845: 846: 847: 848: 849: 850: 851: 852:
853: public function sharedMaxAge($seconds = null) {
854: if ($seconds !== null) {
855: $this->_cacheDirectives['s-maxage'] = $seconds;
856: $this->_setCacheControl();
857: }
858: if (isset($this->_cacheDirectives['s-maxage'])) {
859: return $this->_cacheDirectives['s-maxage'];
860: }
861: return null;
862: }
863:
864: 865: 866: 867: 868: 869: 870: 871: 872:
873: public function maxAge($seconds = null) {
874: if ($seconds !== null) {
875: $this->_cacheDirectives['max-age'] = $seconds;
876: $this->_setCacheControl();
877: }
878: if (isset($this->_cacheDirectives['max-age'])) {
879: return $this->_cacheDirectives['max-age'];
880: }
881: return null;
882: }
883:
884: 885: 886: 887: 888: 889: 890: 891: 892: 893: 894:
895: public function mustRevalidate($enable = null) {
896: if ($enable !== null) {
897: if ($enable) {
898: $this->_cacheDirectives['must-revalidate'] = true;
899: } else {
900: unset($this->_cacheDirectives['must-revalidate']);
901: }
902: $this->_setCacheControl();
903: }
904: return array_key_exists('must-revalidate', $this->_cacheDirectives);
905: }
906:
907: 908: 909: 910: 911: 912:
913: protected function _setCacheControl() {
914: $control = '';
915: foreach ($this->_cacheDirectives as $key => $val) {
916: $control .= $val === true ? $key : sprintf('%s=%s', $key, $val);
917: $control .= ', ';
918: }
919: $control = rtrim($control, ', ');
920: $this->header('Cache-Control', $control);
921: }
922:
923: 924: 925: 926: 927: 928: 929: 930: 931: 932: 933: 934: 935:
936: public function expires($time = null) {
937: if ($time !== null) {
938: $date = $this->_getUTCDate($time);
939: $this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT';
940: }
941: if (isset($this->_headers['Expires'])) {
942: return $this->_headers['Expires'];
943: }
944: return null;
945: }
946:
947: 948: 949: 950: 951: 952: 953: 954: 955: 956: 957: 958: 959:
960: public function modified($time = null) {
961: if ($time !== null) {
962: $date = $this->_getUTCDate($time);
963: $this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT';
964: }
965: if (isset($this->_headers['Last-Modified'])) {
966: return $this->_headers['Last-Modified'];
967: }
968: return null;
969: }
970:
971: 972: 973: 974: 975: 976: 977:
978: public function notModified() {
979: $this->statusCode(304);
980: $this->body('');
981: $remove = array(
982: 'Allow',
983: 'Content-Encoding',
984: 'Content-Language',
985: 'Content-Length',
986: 'Content-MD5',
987: 'Content-Type',
988: 'Last-Modified'
989: );
990: foreach ($remove as $header) {
991: unset($this->_headers[$header]);
992: }
993: }
994:
995: 996: 997: 998: 999: 1000: 1001: 1002: 1003: 1004:
1005: public function vary($cacheVariances = null) {
1006: if ($cacheVariances !== null) {
1007: $cacheVariances = (array)$cacheVariances;
1008: $this->_headers['Vary'] = implode(', ', $cacheVariances);
1009: }
1010: if (isset($this->_headers['Vary'])) {
1011: return explode(', ', $this->_headers['Vary']);
1012: }
1013: return null;
1014: }
1015:
1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 1028: 1029: 1030: 1031: 1032: 1033: 1034: 1035: 1036:
1037: public function etag($tag = null, $weak = false) {
1038: if ($tag !== null) {
1039: $this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $tag);
1040: }
1041: if (isset($this->_headers['Etag'])) {
1042: return $this->_headers['Etag'];
1043: }
1044: return null;
1045: }
1046:
1047: 1048: 1049: 1050: 1051: 1052: 1053:
1054: protected function _getUTCDate($time = null) {
1055: if ($time instanceof DateTime) {
1056: $result = clone $time;
1057: } elseif (is_int($time)) {
1058: $result = new DateTime(date('Y-m-d H:i:s', $time));
1059: } else {
1060: $result = new DateTime($time);
1061: }
1062: $result->setTimeZone(new DateTimeZone('UTC'));
1063: return $result;
1064: }
1065:
1066: 1067: 1068: 1069: 1070: 1071:
1072: public function compress() {
1073: $compressionEnabled = ini_get("zlib.output_compression") !== '1' &&
1074: extension_loaded("zlib") &&
1075: (strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false);
1076: return $compressionEnabled && ob_start('ob_gzhandler');
1077: }
1078:
1079: 1080: 1081: 1082: 1083:
1084: public function outputCompressed() {
1085: return strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false
1086: && (ini_get("zlib.output_compression") === '1' || in_array('ob_gzhandler', ob_list_handlers()));
1087: }
1088:
1089: 1090: 1091: 1092: 1093: 1094:
1095: public function download($filename) {
1096: $this->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
1097: }
1098:
1099: 1100: 1101: 1102: 1103: 1104: 1105:
1106: public function protocol($protocol = null) {
1107: if ($protocol !== null) {
1108: $this->_protocol = $protocol;
1109: }
1110: return $this->_protocol;
1111: }
1112:
1113: 1114: 1115: 1116: 1117: 1118: 1119:
1120: public function length($bytes = null) {
1121: if ($bytes !== null) {
1122: $this->_headers['Content-Length'] = $bytes;
1123: }
1124: if (isset($this->_headers['Content-Length'])) {
1125: return $this->_headers['Content-Length'];
1126: }
1127: return null;
1128: }
1129:
1130: 1131: 1132: 1133: 1134: 1135: 1136: 1137: 1138: 1139: 1140: 1141: 1142:
1143: public function checkNotModified(CakeRequest $request) {
1144: $etags = preg_split('/\s*,\s*/', $request->header('If-None-Match'), null, PREG_SPLIT_NO_EMPTY);
1145: $modifiedSince = $request->header('If-Modified-Since');
1146: if ($responseTag = $this->etag()) {
1147: $etagMatches = in_array('*', $etags) || in_array($responseTag, $etags);
1148: }
1149: if ($modifiedSince) {
1150: $timeMatches = strtotime($this->modified()) === strtotime($modifiedSince);
1151: }
1152: $checks = compact('etagMatches', 'timeMatches');
1153: if (empty($checks)) {
1154: return false;
1155: }
1156: $notModified = !in_array(false, $checks, true);
1157: if ($notModified) {
1158: $this->notModified();
1159: }
1160: return $notModified;
1161: }
1162:
1163: 1164: 1165: 1166: 1167: 1168:
1169: public function __toString() {
1170: return (string)$this->_body;
1171: }
1172:
1173: 1174: 1175: 1176: 1177: 1178: 1179: 1180: 1181: 1182: 1183: 1184: 1185: 1186: 1187: 1188: 1189: 1190: 1191: 1192: 1193: 1194: 1195: 1196: 1197: 1198: 1199: 1200: 1201: 1202: 1203: 1204: 1205: 1206: 1207: 1208: 1209: 1210: 1211: 1212:
1213: public function cookie($options = null) {
1214: if ($options === null) {
1215: return $this->_cookies;
1216: }
1217:
1218: if (is_string($options)) {
1219: if (!isset($this->_cookies[$options])) {
1220: return null;
1221: }
1222: return $this->_cookies[$options];
1223: }
1224:
1225: $defaults = array(
1226: 'name' => 'CakeCookie[default]',
1227: 'value' => '',
1228: 'expire' => 0,
1229: 'path' => '/',
1230: 'domain' => '',
1231: 'secure' => false,
1232: 'httpOnly' => false
1233: );
1234: $options += $defaults;
1235:
1236: $this->_cookies[$options['name']] = $options;
1237: }
1238:
1239: 1240: 1241: 1242: 1243: 1244: 1245: 1246: 1247: 1248: 1249: 1250: 1251: 1252: 1253: 1254: 1255:
1256: public function file($path, $options = array()) {
1257: $options += array(
1258: 'name' => null,
1259: 'download' => null
1260: );
1261:
1262: if (strpos($path, '..') !== false) {
1263: throw new NotFoundException(__d(
1264: 'cake_dev',
1265: 'The requested file contains `..` and will not be read.'
1266: ));
1267: }
1268:
1269: if (!is_file($path)) {
1270: $path = APP . $path;
1271: }
1272:
1273: $file = new File($path);
1274: if (!$file->exists() || !$file->readable()) {
1275: if (Configure::read('debug')) {
1276: throw new NotFoundException(__d('cake_dev', 'The requested file %s was not found or not readable', $path));
1277: }
1278: throw new NotFoundException(__d('cake', 'The requested file was not found'));
1279: }
1280:
1281: $extension = strtolower($file->ext());
1282: $download = $options['download'];
1283: if ((!$extension || $this->type($extension) === false) && $download === null) {
1284: $download = true;
1285: }
1286:
1287: $fileSize = $file->size();
1288: if ($download) {
1289: $agent = env('HTTP_USER_AGENT');
1290:
1291: if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) {
1292: $contentType = 'application/octet-stream';
1293: } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
1294: $contentType = 'application/force-download';
1295: }
1296:
1297: if (!empty($contentType)) {
1298: $this->type($contentType);
1299: }
1300: if ($options['name'] === null) {
1301: $name = $file->name;
1302: } else {
1303: $name = $options['name'];
1304: }
1305: $this->download($name);
1306: $this->header('Accept-Ranges', 'bytes');
1307: $this->header('Content-Transfer-Encoding', 'binary');
1308:
1309: $httpRange = env('HTTP_RANGE');
1310: if (isset($httpRange)) {
1311: $this->_fileRange($file, $httpRange);
1312: } else {
1313: $this->header('Content-Length', $fileSize);
1314: }
1315: } else {
1316: $this->header('Content-Length', $fileSize);
1317: }
1318: $this->_clearBuffer();
1319: $this->_file = $file;
1320: }
1321:
1322: 1323: 1324: 1325: 1326: 1327: 1328: 1329: 1330: 1331:
1332: protected function _fileRange($file, $httpRange) {
1333: list(, $range) = explode('=', $httpRange);
1334: list($start, $end) = explode('-', $range);
1335:
1336: $fileSize = $file->size();
1337: $lastByte = $fileSize - 1;
1338:
1339: if ($start === '') {
1340: $start = $fileSize - $end;
1341: $end = $lastByte;
1342: }
1343: if ($end === '') {
1344: $end = $lastByte;
1345: }
1346:
1347: if ($start > $end || $end > $lastByte || $start > $lastByte) {
1348: $this->statusCode(416);
1349: $this->header(array(
1350: 'Content-Range' => 'bytes 0-' . $lastByte . '/' . $fileSize
1351: ));
1352: return;
1353: }
1354:
1355: $this->header(array(
1356: 'Content-Length' => $end - $start + 1,
1357: 'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $fileSize
1358: ));
1359:
1360: $this->statusCode(206);
1361: $this->_fileRange = array($start, $end);
1362: }
1363:
1364: 1365: 1366: 1367: 1368: 1369: 1370:
1371: protected function _sendFile($file, $range) {
1372: $compress = $this->outputCompressed();
1373: $file->open('rb');
1374:
1375: $end = $start = false;
1376: if ($range) {
1377: list($start, $end) = $range;
1378: }
1379: if ($start !== false) {
1380: $file->offset($start);
1381: }
1382:
1383: $bufferSize = 8192;
1384: set_time_limit(0);
1385: session_write_close();
1386: while (!feof($file->handle)) {
1387: if (!$this->_isActive()) {
1388: $file->close();
1389: return false;
1390: }
1391: $offset = $file->offset();
1392: if ($end && $offset >= $end) {
1393: break;
1394: }
1395: if ($end && $offset + $bufferSize >= $end) {
1396: $bufferSize = $end - $offset + 1;
1397: }
1398: echo fread($file->handle, $bufferSize);
1399: if (!$compress) {
1400: $this->_flushBuffer();
1401: }
1402: }
1403: $file->close();
1404: return true;
1405: }
1406:
1407: 1408: 1409: 1410: 1411:
1412: protected function _isActive() {
1413: return connection_status() === CONNECTION_NORMAL && !connection_aborted();
1414: }
1415:
1416: 1417: 1418: 1419: 1420:
1421: protected function _clearBuffer() {
1422:
1423: return @ob_end_clean();
1424:
1425: }
1426:
1427: 1428: 1429: 1430: 1431:
1432: protected function _flushBuffer() {
1433:
1434: @flush();
1435: @ob_flush();
1436:
1437: }
1438:
1439: }
1440: