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