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: