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