Subversion Repositories SmartDukaan

Rev

Blame | Last modification | View Log | RSS feed

<?php
/**
 * CakeResponse
 *
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @package       Cake.Network
 * @since         CakePHP(tm) v 2.0
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */

App::uses('File', 'Utility');

/**
 * CakeResponse is responsible for managing the response text, status and headers of a HTTP response.
 *
 * By default controllers will use this class to render their response. If you are going to use
 * a custom response class it should subclass this object in order to ensure compatibility.
 *
 * @package       Cake.Network
 */
class CakeResponse {

/**
 * Holds HTTP response statuses
 *
 * @var array
 */
        protected $_statusCodes = array(
                100 => 'Continue',
                101 => 'Switching Protocols',
                200 => 'OK',
                201 => 'Created',
                202 => 'Accepted',
                203 => 'Non-Authoritative Information',
                204 => 'No Content',
                205 => 'Reset Content',
                206 => 'Partial Content',
                300 => 'Multiple Choices',
                301 => 'Moved Permanently',
                302 => 'Found',
                303 => 'See Other',
                304 => 'Not Modified',
                305 => 'Use Proxy',
                307 => 'Temporary Redirect',
                400 => 'Bad Request',
                401 => 'Unauthorized',
                402 => 'Payment Required',
                403 => 'Forbidden',
                404 => 'Not Found',
                405 => 'Method Not Allowed',
                406 => 'Not Acceptable',
                407 => 'Proxy Authentication Required',
                408 => 'Request Time-out',
                409 => 'Conflict',
                410 => 'Gone',
                411 => 'Length Required',
                412 => 'Precondition Failed',
                413 => 'Request Entity Too Large',
                414 => 'Request-URI Too Large',
                415 => 'Unsupported Media Type',
                416 => 'Requested range not satisfiable',
                417 => 'Expectation Failed',
                429 => 'Too Many Requests',
                500 => 'Internal Server Error',
                501 => 'Not Implemented',
                502 => 'Bad Gateway',
                503 => 'Service Unavailable',
                504 => 'Gateway Time-out',
                505 => 'Unsupported Version'
        );

/**
 * Holds known mime type mappings
 *
 * @var array
 */
        protected $_mimeTypes = array(
                'html' => array('text/html', '*/*'),
                'json' => 'application/json',
                'xml' => array('application/xml', 'text/xml'),
                'rss' => 'application/rss+xml',
                'ai' => 'application/postscript',
                'bcpio' => 'application/x-bcpio',
                'bin' => 'application/octet-stream',
                'ccad' => 'application/clariscad',
                'cdf' => 'application/x-netcdf',
                'class' => 'application/octet-stream',
                'cpio' => 'application/x-cpio',
                'cpt' => 'application/mac-compactpro',
                'csh' => 'application/x-csh',
                'csv' => array('text/csv', 'application/vnd.ms-excel'),
                'dcr' => 'application/x-director',
                'dir' => 'application/x-director',
                'dms' => 'application/octet-stream',
                'doc' => 'application/msword',
                'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'drw' => 'application/drafting',
                'dvi' => 'application/x-dvi',
                'dwg' => 'application/acad',
                'dxf' => 'application/dxf',
                'dxr' => 'application/x-director',
                'eot' => 'application/vnd.ms-fontobject',
                'eps' => 'application/postscript',
                'exe' => 'application/octet-stream',
                'ez' => 'application/andrew-inset',
                'flv' => 'video/x-flv',
                'gtar' => 'application/x-gtar',
                'gz' => 'application/x-gzip',
                'bz2' => 'application/x-bzip',
                '7z' => 'application/x-7z-compressed',
                'hdf' => 'application/x-hdf',
                'hqx' => 'application/mac-binhex40',
                'ico' => 'image/x-icon',
                'ips' => 'application/x-ipscript',
                'ipx' => 'application/x-ipix',
                'js' => 'application/javascript',
                'latex' => 'application/x-latex',
                'lha' => 'application/octet-stream',
                'lsp' => 'application/x-lisp',
                'lzh' => 'application/octet-stream',
                'man' => 'application/x-troff-man',
                'me' => 'application/x-troff-me',
                'mif' => 'application/vnd.mif',
                'ms' => 'application/x-troff-ms',
                'nc' => 'application/x-netcdf',
                'oda' => 'application/oda',
                'otf' => 'font/otf',
                'pdf' => 'application/pdf',
                'pgn' => 'application/x-chess-pgn',
                'pot' => 'application/vnd.ms-powerpoint',
                'pps' => 'application/vnd.ms-powerpoint',
                'ppt' => 'application/vnd.ms-powerpoint',
                'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
                'ppz' => 'application/vnd.ms-powerpoint',
                'pre' => 'application/x-freelance',
                'prt' => 'application/pro_eng',
                'ps' => 'application/postscript',
                'roff' => 'application/x-troff',
                'scm' => 'application/x-lotusscreencam',
                'set' => 'application/set',
                'sh' => 'application/x-sh',
                'shar' => 'application/x-shar',
                'sit' => 'application/x-stuffit',
                'skd' => 'application/x-koan',
                'skm' => 'application/x-koan',
                'skp' => 'application/x-koan',
                'skt' => 'application/x-koan',
                'smi' => 'application/smil',
                'smil' => 'application/smil',
                'sol' => 'application/solids',
                'spl' => 'application/x-futuresplash',
                'src' => 'application/x-wais-source',
                'step' => 'application/STEP',
                'stl' => 'application/SLA',
                'stp' => 'application/STEP',
                'sv4cpio' => 'application/x-sv4cpio',
                'sv4crc' => 'application/x-sv4crc',
                'svg' => 'image/svg+xml',
                'svgz' => 'image/svg+xml',
                'swf' => 'application/x-shockwave-flash',
                't' => 'application/x-troff',
                'tar' => 'application/x-tar',
                'tcl' => 'application/x-tcl',
                'tex' => 'application/x-tex',
                'texi' => 'application/x-texinfo',
                'texinfo' => 'application/x-texinfo',
                'tr' => 'application/x-troff',
                'tsp' => 'application/dsptype',
                'ttc' => 'font/ttf',
                'ttf' => 'font/ttf',
                'unv' => 'application/i-deas',
                'ustar' => 'application/x-ustar',
                'vcd' => 'application/x-cdlink',
                'vda' => 'application/vda',
                'xlc' => 'application/vnd.ms-excel',
                'xll' => 'application/vnd.ms-excel',
                'xlm' => 'application/vnd.ms-excel',
                'xls' => 'application/vnd.ms-excel',
                'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'xlw' => 'application/vnd.ms-excel',
                'zip' => 'application/zip',
                'aif' => 'audio/x-aiff',
                'aifc' => 'audio/x-aiff',
                'aiff' => 'audio/x-aiff',
                'au' => 'audio/basic',
                'kar' => 'audio/midi',
                'mid' => 'audio/midi',
                'midi' => 'audio/midi',
                'mp2' => 'audio/mpeg',
                'mp3' => 'audio/mpeg',
                'mpga' => 'audio/mpeg',
                'ogg' => 'audio/ogg',
                'oga' => 'audio/ogg',
                'spx' => 'audio/ogg',
                'ra' => 'audio/x-realaudio',
                'ram' => 'audio/x-pn-realaudio',
                'rm' => 'audio/x-pn-realaudio',
                'rpm' => 'audio/x-pn-realaudio-plugin',
                'snd' => 'audio/basic',
                'tsi' => 'audio/TSP-audio',
                'wav' => 'audio/x-wav',
                'aac' => 'audio/aac',
                'asc' => 'text/plain',
                'c' => 'text/plain',
                'cc' => 'text/plain',
                'css' => 'text/css',
                'etx' => 'text/x-setext',
                'f' => 'text/plain',
                'f90' => 'text/plain',
                'h' => 'text/plain',
                'hh' => 'text/plain',
                'htm' => array('text/html', '*/*'),
                'ics' => 'text/calendar',
                'm' => 'text/plain',
                'rtf' => 'text/rtf',
                'rtx' => 'text/richtext',
                'sgm' => 'text/sgml',
                'sgml' => 'text/sgml',
                'tsv' => 'text/tab-separated-values',
                'tpl' => 'text/template',
                'txt' => 'text/plain',
                'text' => 'text/plain',
                'avi' => 'video/x-msvideo',
                'fli' => 'video/x-fli',
                'mov' => 'video/quicktime',
                'movie' => 'video/x-sgi-movie',
                'mpe' => 'video/mpeg',
                'mpeg' => 'video/mpeg',
                'mpg' => 'video/mpeg',
                'qt' => 'video/quicktime',
                'viv' => 'video/vnd.vivo',
                'vivo' => 'video/vnd.vivo',
                'ogv' => 'video/ogg',
                'webm' => 'video/webm',
                'mp4' => 'video/mp4',
                'm4v' => 'video/mp4',
                'f4v' => 'video/mp4',
                'f4p' => 'video/mp4',
                'm4a' => 'audio/mp4',
                'f4a' => 'audio/mp4',
                'f4b' => 'audio/mp4',
                'gif' => 'image/gif',
                'ief' => 'image/ief',
                'jpg' => 'image/jpeg',
                'jpeg' => 'image/jpeg',
                'jpe' => 'image/jpeg',
                'pbm' => 'image/x-portable-bitmap',
                'pgm' => 'image/x-portable-graymap',
                'png' => 'image/png',
                'pnm' => 'image/x-portable-anymap',
                'ppm' => 'image/x-portable-pixmap',
                'ras' => 'image/cmu-raster',
                'rgb' => 'image/x-rgb',
                'tif' => 'image/tiff',
                'tiff' => 'image/tiff',
                'xbm' => 'image/x-xbitmap',
                'xpm' => 'image/x-xpixmap',
                'xwd' => 'image/x-xwindowdump',
                'ice' => 'x-conference/x-cooltalk',
                'iges' => 'model/iges',
                'igs' => 'model/iges',
                'mesh' => 'model/mesh',
                'msh' => 'model/mesh',
                'silo' => 'model/mesh',
                'vrml' => 'model/vrml',
                'wrl' => 'model/vrml',
                'mime' => 'www/mime',
                'pdb' => 'chemical/x-pdb',
                'xyz' => 'chemical/x-pdb',
                'javascript' => 'application/javascript',
                'form' => 'application/x-www-form-urlencoded',
                'file' => 'multipart/form-data',
                'xhtml' => array('application/xhtml+xml', 'application/xhtml', 'text/xhtml'),
                'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
                'atom' => 'application/atom+xml',
                'amf' => 'application/x-amf',
                'wap' => array('text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'),
                'wml' => 'text/vnd.wap.wml',
                'wmlscript' => 'text/vnd.wap.wmlscript',
                'wbmp' => 'image/vnd.wap.wbmp',
                'woff' => 'application/x-font-woff',
                'webp' => 'image/webp',
                'appcache' => 'text/cache-manifest',
                'manifest' => 'text/cache-manifest',
                'htc' => 'text/x-component',
                'rdf' => 'application/xml',
                'crx' => 'application/x-chrome-extension',
                'oex' => 'application/x-opera-extension',
                'xpi' => 'application/x-xpinstall',
                'safariextz' => 'application/octet-stream',
                'webapp' => 'application/x-web-app-manifest+json',
                'vcf' => 'text/x-vcard',
                'vtt' => 'text/vtt',
                'mkv' => 'video/x-matroska',
                'pkpass' => 'application/vnd.apple.pkpass'
        );

/**
 * Protocol header to send to the client
 *
 * @var string
 */
        protected $_protocol = 'HTTP/1.1';

/**
 * Status code to send to the client
 *
 * @var int
 */
        protected $_status = 200;

/**
 * Content type to send. This can be an 'extension' that will be transformed using the $_mimetypes array
 * or a complete mime-type
 *
 * @var int
 */
        protected $_contentType = 'text/html';

/**
 * Buffer list of headers
 *
 * @var array
 */
        protected $_headers = array();

/**
 * Buffer string for response message
 *
 * @var string
 */
        protected $_body = null;

/**
 * File object for file to be read out as response
 *
 * @var File
 */
        protected $_file = null;

/**
 * File range. Used for requesting ranges of files.
 *
 * @var array
 */
        protected $_fileRange = null;

/**
 * The charset the response body is encoded with
 *
 * @var string
 */
        protected $_charset = 'UTF-8';

/**
 * Holds all the cache directives that will be converted
 * into headers when sending the request
 *
 * @var string
 */
        protected $_cacheDirectives = array();

/**
 * Holds cookies to be sent to the client
 *
 * @var array
 */
        protected $_cookies = array();

/**
 * Constructor
 *
 * @param array $options list of parameters to setup the response. Possible values are:
 *      - body: the response text that should be sent to the client
 *      - statusCodes: additional allowable response codes
 *      - status: the HTTP status code to respond with
 *      - type: a complete mime-type string or an extension mapped in this class
 *      - charset: the charset for the response body
 */
        public function __construct(array $options = array()) {
                if (isset($options['body'])) {
                        $this->body($options['body']);
                }
                if (isset($options['statusCodes'])) {
                        $this->httpCodes($options['statusCodes']);
                }
                if (isset($options['status'])) {
                        $this->statusCode($options['status']);
                }
                if (isset($options['type'])) {
                        $this->type($options['type']);
                }
                if (!isset($options['charset'])) {
                        $options['charset'] = Configure::read('App.encoding');
                }
                $this->charset($options['charset']);
        }

/**
 * Sends the complete response to the client including headers and message body.
 * Will echo out the content in the response body.
 *
 * @return void
 */
        public function send() {
                if (isset($this->_headers['Location']) && $this->_status === 200) {
                        $this->statusCode(302);
                }

                $codeMessage = $this->_statusCodes[$this->_status];
                $this->_setCookies();
                $this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}");
                $this->_setContent();
                $this->_setContentLength();
                $this->_setContentType();
                foreach ($this->_headers as $header => $values) {
                        foreach ((array)$values as $value) {
                                $this->_sendHeader($header, $value);
                        }
                }
                if ($this->_file) {
                        $this->_sendFile($this->_file, $this->_fileRange);
                        $this->_file = $this->_fileRange = null;
                } else {
                        $this->_sendContent($this->_body);
                }
        }

/**
 * Sets the cookies that have been added via CakeResponse::cookie() before any
 * other output is sent to the client. Will set the cookies in the order they
 * have been set.
 *
 * @return void
 */
        protected function _setCookies() {
                foreach ($this->_cookies as $name => $c) {
                        setcookie(
                                $name, $c['value'], $c['expire'], $c['path'],
                                $c['domain'], $c['secure'], $c['httpOnly']
                        );
                }
        }

/**
 * Formats the Content-Type header based on the configured contentType and charset
 * the charset will only be set in the header if the response is of type text/*
 *
 * @return void
 */
        protected function _setContentType() {
                if (in_array($this->_status, array(304, 204))) {
                        return;
                }
                $whitelist = array(
                        'application/javascript', 'application/json', 'application/xml', 'application/rss+xml'
                );

                $charset = false;
                if ($this->_charset &&
                        (strpos($this->_contentType, 'text/') === 0 || in_array($this->_contentType, $whitelist))
                ) {
                        $charset = true;
                }

                if ($charset) {
                        $this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}");
                } else {
                        $this->header('Content-Type', "{$this->_contentType}");
                }
        }

/**
 * Sets the response body to an empty text if the status code is 204 or 304
 *
 * @return void
 */
        protected function _setContent() {
                if (in_array($this->_status, array(304, 204))) {
                        $this->body('');
                }
        }

/**
 * Calculates the correct Content-Length and sets it as a header in the response
 * Will not set the value if already set or if the output is compressed.
 *
 * @return void
 */
        protected function _setContentLength() {
                $shouldSetLength = !isset($this->_headers['Content-Length']) && !in_array($this->_status, range(301, 307));
                if (isset($this->_headers['Content-Length']) && $this->_headers['Content-Length'] === false) {
                        unset($this->_headers['Content-Length']);
                        return;
                }
                if ($shouldSetLength && !$this->outputCompressed()) {
                        $offset = ob_get_level() ? ob_get_length() : 0;
                        if (ini_get('mbstring.func_overload') & 2 && function_exists('mb_strlen')) {
                                $this->length($offset + mb_strlen($this->_body, '8bit'));
                        } else {
                                $this->length($this->_headers['Content-Length'] = $offset + strlen($this->_body));
                        }
                }
        }

/**
 * Sends a header to the client.
 *
 * Will skip sending headers if headers have already been sent.
 *
 * @param string $name the header name
 * @param string $value the header value
 * @return void
 */
        protected function _sendHeader($name, $value = null) {
                if (headers_sent($filename, $linenum)) {
                        return;
                }
                if ($value === null) {
                        header($name);
                } else {
                        header("{$name}: {$value}");
                }
        }

/**
 * Sends a content string to the client.
 *
 * @param string $content string to send as response body
 * @return void
 */
        protected function _sendContent($content) {
                echo $content;
        }

/**
 * Buffers a header string to be sent
 * Returns the complete list of buffered headers
 *
 * ### Single header
 * e.g `header('Location', 'http://example.com');`
 *
 * ### Multiple headers
 * e.g `header(array('Location' => 'http://example.com', 'X-Extra' => 'My header'));`
 *
 * ### String header
 * e.g `header('WWW-Authenticate: Negotiate');`
 *
 * ### Array of string headers
 * e.g `header(array('WWW-Authenticate: Negotiate', 'Content-type: application/pdf'));`
 *
 * Multiple calls for setting the same header name will have the same effect as setting the header once
 * with the last value sent for it
 *  e.g `header('WWW-Authenticate: Negotiate'); header('WWW-Authenticate: Not-Negotiate');`
 * will have the same effect as only doing `header('WWW-Authenticate: Not-Negotiate');`
 *
 * @param string|array $header An array of header strings or a single header string
 *      - an associative array of "header name" => "header value" is also accepted
 *      - an array of string headers is also accepted
 * @param string|array $value The header value(s)
 * @return array list of headers to be sent
 */
        public function header($header = null, $value = null) {
                if ($header === null) {
                        return $this->_headers;
                }
                $headers = is_array($header) ? $header : array($header => $value);
                foreach ($headers as $header => $value) {
                        if (is_numeric($header)) {
                                list($header, $value) = array($value, null);
                        }
                        if ($value === null) {
                                list($header, $value) = explode(':', $header, 2);
                        }
                        $this->_headers[$header] = is_array($value) ? array_map('trim', $value) : trim($value);
                }
                return $this->_headers;
        }

/**
 * Accessor for the location header.
 *
 * Get/Set the Location header value.
 *
 * @param null|string $url Either null to get the current location, or a string to set one.
 * @return string|null When setting the location null will be returned. When reading the location
 *    a string of the current location header value (if any) will be returned.
 */
        public function location($url = null) {
                if ($url === null) {
                        $headers = $this->header();
                        return isset($headers['Location']) ? $headers['Location'] : null;
                }
                $this->header('Location', $url);
                return null;
        }

/**
 * Buffers the response message to be sent
 * if $content is null the current buffer is returned
 *
 * @param string $content the string message to be sent
 * @return string current message buffer if $content param is passed as null
 */
        public function body($content = null) {
                if ($content === null) {
                        return $this->_body;
                }
                return $this->_body = $content;
        }

/**
 * Sets the HTTP status code to be sent
 * if $code is null the current code is returned
 *
 * @param int $code the HTTP status code
 * @return int current status code
 * @throws CakeException When an unknown status code is reached.
 */
        public function statusCode($code = null) {
                if ($code === null) {
                        return $this->_status;
                }
                if (!isset($this->_statusCodes[$code])) {
                        throw new CakeException(__d('cake_dev', 'Unknown status code'));
                }
                return $this->_status = $code;
        }

/**
 * Queries & sets valid HTTP response codes & messages.
 *
 * @param int|array $code If $code is an integer, then the corresponding code/message is
 *        returned if it exists, null if it does not exist. If $code is an array, then the
 *        keys are used as codes and the values as messages to add to the default HTTP
 *        codes. The codes must be integers greater than 99 and less than 1000. Keep in
 *        mind that the HTTP specification outlines that status codes begin with a digit
 *        between 1 and 5, which defines the class of response the client is to expect.
 *        Example:
 *
 *        httpCodes(404); // returns array(404 => 'Not Found')
 *
 *        httpCodes(array(
 *            381 => 'Unicorn Moved',
 *            555 => 'Unexpected Minotaur'
 *        )); // sets these new values, and returns true
 *
 *        httpCodes(array(
 *            0 => 'Nothing Here',
 *            -1 => 'Reverse Infinity',
 *            12345 => 'Universal Password',
 *            'Hello' => 'World'
 *        )); // throws an exception due to invalid codes
 *
 *        For more on HTTP status codes see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
 *
 * @return mixed associative array of the HTTP codes as keys, and the message
 *    strings as values, or null of the given $code does not exist.
 * @throws CakeException If an attempt is made to add an invalid status code
 */
        public function httpCodes($code = null) {
                if (empty($code)) {
                        return $this->_statusCodes;
                }
                if (is_array($code)) {
                        $codes = array_keys($code);
                        $min = min($codes);
                        if (!is_int($min) || $min < 100 || max($codes) > 999) {
                                throw new CakeException(__d('cake_dev', 'Invalid status code'));
                        }
                        $this->_statusCodes = $code + $this->_statusCodes;
                        return true;
                }
                if (!isset($this->_statusCodes[$code])) {
                        return null;
                }
                return array($code => $this->_statusCodes[$code]);
        }

/**
 * Sets the response content type. It can be either a file extension
 * which will be mapped internally to a mime-type or a string representing a mime-type
 * if $contentType is null the current content type is returned
 * if $contentType is an associative array, content type definitions will be stored/replaced
 *
 * ### Setting the content type
 *
 * e.g `type('jpg');`
 *
 * ### Returning the current content type
 *
 * e.g `type();`
 *
 * ### Storing content type definitions
 *
 * e.g `type(array('keynote' => 'application/keynote', 'bat' => 'application/bat'));`
 *
 * ### Replacing a content type definition
 *
 * e.g `type(array('jpg' => 'text/plain'));`
 *
 * @param string $contentType Content type key.
 * @return mixed current content type or false if supplied an invalid content type
 */
        public function type($contentType = null) {
                if ($contentType === null) {
                        return $this->_contentType;
                }
                if (is_array($contentType)) {
                        foreach ($contentType as $type => $definition) {
                                $this->_mimeTypes[$type] = $definition;
                        }
                        return $this->_contentType;
                }
                if (isset($this->_mimeTypes[$contentType])) {
                        $contentType = $this->_mimeTypes[$contentType];
                        $contentType = is_array($contentType) ? current($contentType) : $contentType;
                }
                if (strpos($contentType, '/') === false) {
                        return false;
                }
                return $this->_contentType = $contentType;
        }

/**
 * Returns the mime type definition for an alias
 *
 * e.g `getMimeType('pdf'); // returns 'application/pdf'`
 *
 * @param string $alias the content type alias to map
 * @return mixed string mapped mime type or false if $alias is not mapped
 */
        public function getMimeType($alias) {
                if (isset($this->_mimeTypes[$alias])) {
                        return $this->_mimeTypes[$alias];
                }
                return false;
        }

/**
 * Maps a content-type back to an alias
 *
 * e.g `mapType('application/pdf'); // returns 'pdf'`
 *
 * @param string|array $ctype Either a string content type to map, or an array of types.
 * @return mixed Aliases for the types provided.
 */
        public function mapType($ctype) {
                if (is_array($ctype)) {
                        return array_map(array($this, 'mapType'), $ctype);
                }

                foreach ($this->_mimeTypes as $alias => $types) {
                        if (in_array($ctype, (array)$types)) {
                                return $alias;
                        }
                }
                return null;
        }

/**
 * Sets the response charset
 * if $charset is null the current charset is returned
 *
 * @param string $charset Character set string.
 * @return string current charset
 */
        public function charset($charset = null) {
                if ($charset === null) {
                        return $this->_charset;
                }
                return $this->_charset = $charset;
        }

/**
 * Sets the correct headers to instruct the client to not cache the response
 *
 * @return void
 */
        public function disableCache() {
                $this->header(array(
                        'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
                        'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT",
                        'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
                ));
        }

/**
 * Sets the correct headers to instruct the client to cache the response.
 *
 * @param string $since a valid time since the response text has not been modified
 * @param string $time a valid time for cache expiry
 * @return void
 */
        public function cache($since, $time = '+1 day') {
                if (!is_int($time)) {
                        $time = strtotime($time);
                }
                $this->header(array(
                        'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT'
                ));
                $this->modified($since);
                $this->expires($time);
                $this->sharable(true);
                $this->maxAge($time - time());
        }

/**
 * Sets whether a response is eligible to be cached by intermediate proxies
 * This method controls the `public` or `private` directive in the Cache-Control
 * header
 *
 * @param bool $public If set to true, the Cache-Control header will be set as public
 *   if set to false, the response will be set to private
 *   if no value is provided, it will return whether the response is sharable or not
 * @param int $time time in seconds after which the response should no longer be considered fresh
 * @return bool
 */
        public function sharable($public = null, $time = null) {
                if ($public === null) {
                        $public = array_key_exists('public', $this->_cacheDirectives);
                        $private = array_key_exists('private', $this->_cacheDirectives);
                        $noCache = array_key_exists('no-cache', $this->_cacheDirectives);
                        if (!$public && !$private && !$noCache) {
                                return null;
                        }
                        $sharable = $public || ! ($private || $noCache);
                        return $sharable;
                }
                if ($public) {
                        $this->_cacheDirectives['public'] = true;
                        unset($this->_cacheDirectives['private']);
                } else {
                        $this->_cacheDirectives['private'] = true;
                        unset($this->_cacheDirectives['public']);
                }

                $this->maxAge($time);
                if (!$time) {
                        $this->_setCacheControl();
                }
                return (bool)$public;
        }

/**
 * Sets the Cache-Control s-maxage directive.
 * The max-age is the number of seconds after which the response should no longer be considered
 * a good candidate to be fetched from a shared cache (like in a proxy server).
 * If called with no parameters, this function will return the current max-age value if any
 *
 * @param int $seconds if null, the method will return the current s-maxage value
 * @return int
 */
        public function sharedMaxAge($seconds = null) {
                if ($seconds !== null) {
                        $this->_cacheDirectives['s-maxage'] = $seconds;
                        $this->_setCacheControl();
                }
                if (isset($this->_cacheDirectives['s-maxage'])) {
                        return $this->_cacheDirectives['s-maxage'];
                }
                return null;
        }

/**
 * Sets the Cache-Control max-age directive.
 * The max-age is the number of seconds after which the response should no longer be considered
 * a good candidate to be fetched from the local (client) cache.
 * If called with no parameters, this function will return the current max-age value if any
 *
 * @param int $seconds if null, the method will return the current max-age value
 * @return int
 */
        public function maxAge($seconds = null) {
                if ($seconds !== null) {
                        $this->_cacheDirectives['max-age'] = $seconds;
                        $this->_setCacheControl();
                }
                if (isset($this->_cacheDirectives['max-age'])) {
                        return $this->_cacheDirectives['max-age'];
                }
                return null;
        }

/**
 * Sets the Cache-Control must-revalidate directive.
 * must-revalidate indicates that the response should not be served
 * stale by a cache under any circumstance without first revalidating
 * with the origin.
 * If called with no parameters, this function will return whether must-revalidate is present.
 *
 * @param bool $enable If null returns whether directive is set, if boolean
 *   sets or unsets directive.
 * @return bool
 */
        public function mustRevalidate($enable = null) {
                if ($enable !== null) {
                        if ($enable) {
                                $this->_cacheDirectives['must-revalidate'] = true;
                        } else {
                                unset($this->_cacheDirectives['must-revalidate']);
                        }
                        $this->_setCacheControl();
                }
                return array_key_exists('must-revalidate', $this->_cacheDirectives);
        }

/**
 * Helper method to generate a valid Cache-Control header from the options set
 * in other methods
 *
 * @return void
 */
        protected function _setCacheControl() {
                $control = '';
                foreach ($this->_cacheDirectives as $key => $val) {
                        $control .= $val === true ? $key : sprintf('%s=%s', $key, $val);
                        $control .= ', ';
                }
                $control = rtrim($control, ', ');
                $this->header('Cache-Control', $control);
        }

/**
 * Sets the Expires header for the response by taking an expiration time
 * If called with no parameters it will return the current Expires value
 *
 * ## Examples:
 *
 * `$response->expires('now')` Will Expire the response cache now
 * `$response->expires(new DateTime('+1 day'))` Will set the expiration in next 24 hours
 * `$response->expires()` Will return the current expiration header value
 *
 * @param string|DateTime $time Valid time string or DateTime object.
 * @return string
 */
        public function expires($time = null) {
                if ($time !== null) {
                        $date = $this->_getUTCDate($time);
                        $this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT';
                }
                if (isset($this->_headers['Expires'])) {
                        return $this->_headers['Expires'];
                }
                return null;
        }

/**
 * Sets the Last-Modified header for the response by taking a modification time
 * If called with no parameters it will return the current Last-Modified value
 *
 * ## Examples:
 *
 * `$response->modified('now')` Will set the Last-Modified to the current time
 * `$response->modified(new DateTime('+1 day'))` Will set the modification date in the past 24 hours
 * `$response->modified()` Will return the current Last-Modified header value
 *
 * @param string|DateTime $time Valid time string or DateTime object.
 * @return string
 */
        public function modified($time = null) {
                if ($time !== null) {
                        $date = $this->_getUTCDate($time);
                        $this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT';
                }
                if (isset($this->_headers['Last-Modified'])) {
                        return $this->_headers['Last-Modified'];
                }
                return null;
        }

/**
 * Sets the response as Not Modified by removing any body contents
 * setting the status code to "304 Not Modified" and removing all
 * conflicting headers
 *
 * @return void
 */
        public function notModified() {
                $this->statusCode(304);
                $this->body('');
                $remove = array(
                        'Allow',
                        'Content-Encoding',
                        'Content-Language',
                        'Content-Length',
                        'Content-MD5',
                        'Content-Type',
                        'Last-Modified'
                );
                foreach ($remove as $header) {
                        unset($this->_headers[$header]);
                }
        }

/**
 * Sets the Vary header for the response, if an array is passed,
 * values will be imploded into a comma separated string. If no
 * parameters are passed, then an array with the current Vary header
 * value is returned
 *
 * @param string|array $cacheVariances a single Vary string or an array
 *   containing the list for variances.
 * @return array
 */
        public function vary($cacheVariances = null) {
                if ($cacheVariances !== null) {
                        $cacheVariances = (array)$cacheVariances;
                        $this->_headers['Vary'] = implode(', ', $cacheVariances);
                }
                if (isset($this->_headers['Vary'])) {
                        return explode(', ', $this->_headers['Vary']);
                }
                return null;
        }

/**
 * Sets the response Etag, Etags are a strong indicative that a response
 * can be cached by a HTTP client. A bad way of generating Etags is
 * creating a hash of the response output, instead generate a unique
 * hash of the unique components that identifies a request, such as a
 * modification time, a resource Id, and anything else you consider it
 * makes it unique.
 *
 * Second parameter is used to instruct clients that the content has
 * changed, but sematicallly, it can be used as the same thing. Think
 * for instance of a page with a hit counter, two different page views
 * are equivalent, but they differ by a few bytes. This leaves off to
 * the Client the decision of using or not the cached page.
 *
 * If no parameters are passed, current Etag header is returned.
 *
 * @param string $tag Tag to set.
 * @param bool $weak whether the response is semantically the same as
 *   other with the same hash or not
 * @return string
 */
        public function etag($tag = null, $weak = false) {
                if ($tag !== null) {
                        $this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $tag);
                }
                if (isset($this->_headers['Etag'])) {
                        return $this->_headers['Etag'];
                }
                return null;
        }

/**
 * Returns a DateTime object initialized at the $time param and using UTC
 * as timezone
 *
 * @param string|DateTime $time Valid time string or unix timestamp or DateTime object.
 * @return DateTime
 */
        protected function _getUTCDate($time = null) {
                if ($time instanceof DateTime) {
                        $result = clone $time;
                } elseif (is_int($time)) {
                        $result = new DateTime(date('Y-m-d H:i:s', $time));
                } else {
                        $result = new DateTime($time);
                }
                $result->setTimeZone(new DateTimeZone('UTC'));
                return $result;
        }

/**
 * Sets the correct output buffering handler to send a compressed response. Responses will
 * be compressed with zlib, if the extension is available.
 *
 * @return bool false if client does not accept compressed responses or no handler is available, true otherwise
 */
        public function compress() {
                $compressionEnabled = ini_get("zlib.output_compression") !== '1' &&
                        extension_loaded("zlib") &&
                        (strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false);
                return $compressionEnabled && ob_start('ob_gzhandler');
        }

/**
 * Returns whether the resulting output will be compressed by PHP
 *
 * @return bool
 */
        public function outputCompressed() {
                return strpos(env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false
                        && (ini_get("zlib.output_compression") === '1' || in_array('ob_gzhandler', ob_list_handlers()));
        }

/**
 * Sets the correct headers to instruct the browser to download the response as a file.
 *
 * @param string $filename the name of the file as the browser will download the response
 * @return void
 */
        public function download($filename) {
                $this->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
        }

/**
 * Sets the protocol to be used when sending the response. Defaults to HTTP/1.1
 * If called with no arguments, it will return the current configured protocol
 *
 * @param string $protocol Protocol to be used for sending response.
 * @return string protocol currently set
 */
        public function protocol($protocol = null) {
                if ($protocol !== null) {
                        $this->_protocol = $protocol;
                }
                return $this->_protocol;
        }

/**
 * Sets the Content-Length header for the response
 * If called with no arguments returns the last Content-Length set
 *
 * @param int $bytes Number of bytes
 * @return int|null
 */
        public function length($bytes = null) {
                if ($bytes !== null) {
                        $this->_headers['Content-Length'] = $bytes;
                }
                if (isset($this->_headers['Content-Length'])) {
                        return $this->_headers['Content-Length'];
                }
                return null;
        }

/**
 * Checks whether a response has not been modified according to the 'If-None-Match'
 * (Etags) and 'If-Modified-Since' (last modification date) request
 * headers. If the response is detected to be not modified, it
 * is marked as so accordingly so the client can be informed of that.
 *
 * In order to mark a response as not modified, you need to set at least
 * the Last-Modified etag response header before calling this method. Otherwise
 * a comparison will not be possible.
 *
 * @param CakeRequest $request Request object
 * @return bool whether the response was marked as not modified or not.
 */
        public function checkNotModified(CakeRequest $request) {
                $etags = preg_split('/\s*,\s*/', $request->header('If-None-Match'), null, PREG_SPLIT_NO_EMPTY);
                $modifiedSince = $request->header('If-Modified-Since');
                if ($responseTag = $this->etag()) {
                        $etagMatches = in_array('*', $etags) || in_array($responseTag, $etags);
                }
                if ($modifiedSince) {
                        $timeMatches = strtotime($this->modified()) === strtotime($modifiedSince);
                }
                $checks = compact('etagMatches', 'timeMatches');
                if (empty($checks)) {
                        return false;
                }
                $notModified = !in_array(false, $checks, true);
                if ($notModified) {
                        $this->notModified();
                }
                return $notModified;
        }

/**
 * String conversion. Fetches the response body as a string.
 * Does *not* send headers.
 *
 * @return string
 */
        public function __toString() {
                return (string)$this->_body;
        }

/**
 * Getter/Setter for cookie configs
 *
 * This method acts as a setter/getter depending on the type of the argument.
 * If the method is called with no arguments, it returns all configurations.
 *
 * If the method is called with a string as argument, it returns either the
 * given configuration if it is set, or null, if it's not set.
 *
 * If the method is called with an array as argument, it will set the cookie
 * configuration to the cookie container.
 *
 * @param array $options Either null to get all cookies, string for a specific cookie
 *  or array to set cookie.
 *
 * ### Options (when setting a configuration)
 *  - name: The Cookie name
 *  - value: Value of the cookie
 *  - expire: Time the cookie expires in
 *  - path: Path the cookie applies to
 *  - domain: Domain the cookie is for.
 *  - secure: Is the cookie https?
 *  - httpOnly: Is the cookie available in the client?
 *
 * ## Examples
 *
 * ### Getting all cookies
 *
 * `$this->cookie()`
 *
 * ### Getting a certain cookie configuration
 *
 * `$this->cookie('MyCookie')`
 *
 * ### Setting a cookie configuration
 *
 * `$this->cookie((array) $options)`
 *
 * @return mixed
 */
        public function cookie($options = null) {
                if ($options === null) {
                        return $this->_cookies;
                }

                if (is_string($options)) {
                        if (!isset($this->_cookies[$options])) {
                                return null;
                        }
                        return $this->_cookies[$options];
                }

                $defaults = array(
                        'name' => 'CakeCookie[default]',
                        'value' => '',
                        'expire' => 0,
                        'path' => '/',
                        'domain' => '',
                        'secure' => false,
                        'httpOnly' => false
                );
                $options += $defaults;

                $this->_cookies[$options['name']] = $options;
        }

/**
 * Setup access for origin and methods on cross origin requests
 *
 * This method allow multiple ways to setup the domains, see the examples
 *
 * ### Full URI
 * e.g `cors($request, 'http://www.cakephp.org');`
 *
 * ### URI with wildcard
 * e.g `cors($request, 'http://*.cakephp.org');`
 *
 * ### Ignoring the requested protocol
 * e.g `cors($request, 'www.cakephp.org');`
 *
 * ### Any URI
 * e.g `cors($request, '*');`
 *
 * ### Whitelist of URIs
 * e.g `cors($request, array('http://www.cakephp.org', '*.google.com', 'https://myproject.github.io'));`
 *
 * @param CakeRequest $request Request object
 * @param string|array $allowedDomains List of allowed domains, see method description for more details
 * @param string|array $allowedMethods List of HTTP verbs allowed
 * @param string|array $allowedHeaders List of HTTP headers allowed
 * @return void
 */
        public function cors(CakeRequest $request, $allowedDomains, $allowedMethods = array(), $allowedHeaders = array()) {
                $origin = $request->header('Origin');
                if (!$origin) {
                        return;
                }

                $allowedDomains = $this->_normalizeCorsDomains((array)$allowedDomains, $request->is('ssl'));
                foreach ($allowedDomains as $domain) {
                        if (!preg_match($domain['preg'], $origin)) {
                                continue;
                        }
                        $this->header('Access-Control-Allow-Origin', $domain['original'] === '*' ? '*' : $origin);
                        $allowedMethods && $this->header('Access-Control-Allow-Methods', implode(', ', (array)$allowedMethods));
                        $allowedHeaders && $this->header('Access-Control-Allow-Headers', implode(', ', (array)$allowedHeaders));
                        break;
                }
        }

/**
 * Normalize the origin to regular expressions and put in an array format
 *
 * @param array $domains Domains to normalize
 * @param bool $requestIsSSL Whether it's a SSL request.
 * @return array
 */
        protected function _normalizeCorsDomains($domains, $requestIsSSL = false) {
                $result = array();
                foreach ($domains as $domain) {
                        if ($domain === '*') {
                                $result[] = array('preg' => '@.@', 'original' => '*');
                                continue;
                        }

                        $original = $preg = $domain;
                        if (strpos($domain, '://') === false) {
                                $preg = ($requestIsSSL ? 'https://' : 'http://') . $domain;
                        }
                        $preg = '@' . str_replace('*', '.*', $domain) . '@';
                        $result[] = compact('original', 'preg');
                }
                return $result;
        }

/**
 * Setup for display or download the given file.
 *
 * If $_SERVER['HTTP_RANGE'] is set a slice of the file will be
 * returned instead of the entire file.
 *
 * ### Options keys
 *
 * - name: Alternate download name
 * - download: If `true` sets download header and forces file to be downloaded rather than displayed in browser
 *
 * @param string $path Path to file. If the path is not an absolute path that resolves
 *   to a file, `APP` will be prepended to the path.
 * @param array $options Options See above.
 * @return void
 * @throws NotFoundException
 */
        public function file($path, $options = array()) {
                $options += array(
                        'name' => null,
                        'download' => null
                );

                if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) {
                        throw new NotFoundException(__d(
                                'cake_dev',
                                'The requested file contains `..` and will not be read.'
                        ));
                }

                if (!is_file($path)) {
                        $path = APP . $path;
                }

                $file = new File($path);
                if (!$file->exists() || !$file->readable()) {
                        if (Configure::read('debug')) {
                                throw new NotFoundException(__d('cake_dev', 'The requested file %s was not found or not readable', $path));
                        }
                        throw new NotFoundException(__d('cake', 'The requested file was not found'));
                }

                $extension = strtolower($file->ext());
                $download = $options['download'];
                if ((!$extension || $this->type($extension) === false) && $download === null) {
                        $download = true;
                }

                $fileSize = $file->size();
                if ($download) {
                        $agent = env('HTTP_USER_AGENT');

                        if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) {
                                $contentType = 'application/octet-stream';
                        } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
                                $contentType = 'application/force-download';
                        }

                        if (!empty($contentType)) {
                                $this->type($contentType);
                        }
                        if ($options['name'] === null) {
                                $name = $file->name;
                        } else {
                                $name = $options['name'];
                        }
                        $this->download($name);
                        $this->header('Content-Transfer-Encoding', 'binary');
                }

                $this->header('Accept-Ranges', 'bytes');
                $httpRange = env('HTTP_RANGE');
                if (isset($httpRange)) {
                        $this->_fileRange($file, $httpRange);
                } else {
                        $this->header('Content-Length', $fileSize);
                }

                $this->_clearBuffer();
                $this->_file = $file;
        }

/**
 * Apply a file range to a file and set the end offset.
 *
 * If an invalid range is requested a 416 Status code will be used
 * in the response.
 *
 * @param File $file The file to set a range on.
 * @param string $httpRange The range to use.
 * @return void
 */
        protected function _fileRange($file, $httpRange) {
                list(, $range) = explode('=', $httpRange);
                list($start, $end) = explode('-', $range);

                $fileSize = $file->size();
                $lastByte = $fileSize - 1;

                if ($start === '') {
                        $start = $fileSize - $end;
                        $end = $lastByte;
                }
                if ($end === '') {
                        $end = $lastByte;
                }

                if ($start > $end || $end > $lastByte || $start > $lastByte) {
                        $this->statusCode(416);
                        $this->header(array(
                                'Content-Range' => 'bytes 0-' . $lastByte . '/' . $fileSize
                        ));
                        return;
                }

                $this->header(array(
                        'Content-Length' => $end - $start + 1,
                        'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $fileSize
                ));

                $this->statusCode(206);
                $this->_fileRange = array($start, $end);
        }

/**
 * Reads out a file, and echos the content to the client.
 *
 * @param File $file File object
 * @param array $range The range to read out of the file.
 * @return bool True is whole file is echoed successfully or false if client connection is lost in between
 */
        protected function _sendFile($file, $range) {
                $compress = $this->outputCompressed();
                $file->open('rb');

                $end = $start = false;
                if ($range) {
                        list($start, $end) = $range;
                }
                if ($start !== false) {
                        $file->offset($start);
                }

                $bufferSize = 8192;
                set_time_limit(0);
                session_write_close();
                while (!feof($file->handle)) {
                        if (!$this->_isActive()) {
                                $file->close();
                                return false;
                        }
                        $offset = $file->offset();
                        if ($end && $offset >= $end) {
                                break;
                        }
                        if ($end && $offset + $bufferSize >= $end) {
                                $bufferSize = $end - $offset + 1;
                        }
                        echo fread($file->handle, $bufferSize);
                        if (!$compress) {
                                $this->_flushBuffer();
                        }
                }
                $file->close();
                return true;
        }

/**
 * Returns true if connection is still active
 *
 * @return bool
 */
        protected function _isActive() {
                return connection_status() === CONNECTION_NORMAL && !connection_aborted();
        }

/**
 * Clears the contents of the topmost output buffer and discards them
 *
 * @return bool
 */
        protected function _clearBuffer() {
                if (ob_get_length()) {
                        return ob_end_clean();
                }
                return true;
        }

/**
 * Flushes the contents of the output buffer
 *
 * @return void
 */
        protected function _flushBuffer() {
                //@codingStandardsIgnoreStart
                @flush();
                if (ob_get_level()) {
                        @ob_flush();
                }
                //@codingStandardsIgnoreEnd
        }

}