diff options
author | Andrey Andreev <narf@devilix.net> | 2014-01-20 14:03:43 +0100 |
---|---|---|
committer | Andrey Andreev <narf@devilix.net> | 2014-01-20 14:03:43 +0100 |
commit | ea801ab4ab80042638ffddc6056483a1ec43fa80 (patch) | |
tree | f75f30c0df6a8f861ca7df22af09fa3240b0bbd6 /system/core/Output.php | |
parent | 1c08d557a21ecb0f79cd1a1de4e06817a26e0537 (diff) | |
parent | 4d0571666d03511ac5b4a1f2a6882ccb1509a209 (diff) |
Merge branch 'develop' into feature/user-guide-cleanup
Diffstat (limited to 'system/core/Output.php')
-rw-r--r-- | system/core/Output.php | 288 |
1 files changed, 156 insertions, 132 deletions
diff --git a/system/core/Output.php b/system/core/Output.php index 06d7a866b..2ad8e90fa 100644 --- a/system/core/Output.php +++ b/system/core/Output.php @@ -58,21 +58,21 @@ class CI_Output { * * @var array */ - public $headers = array(); + public $headers = array(); /** * List of mime types * * @var array */ - public $mimes = array(); + public $mimes = array(); /** * Mime-type for the current page * * @var string */ - protected $mime_type = 'text/html'; + protected $mime_type = 'text/html'; /** * Enable Profiler flag @@ -82,11 +82,18 @@ class CI_Output { public $enable_profiler = FALSE; /** - * zLib output compression flag + * php.ini zlib.output_compression flag * * @var bool */ - protected $_zlib_oc = FALSE; + protected $_zlib_oc = FALSE; + + /** + * CI output compression flag + * + * @var bool + */ + protected $_compress_output = FALSE; /** * List of profiler sections @@ -102,7 +109,7 @@ class CI_Output { * * @var bool */ - public $parse_exec_vars = TRUE; + public $parse_exec_vars = TRUE; /** * Class constructor @@ -114,6 +121,11 @@ class CI_Output { public function __construct() { $this->_zlib_oc = (bool) @ini_get('zlib.output_compression'); + $this->_compress_output = ( + $this->_zlib_oc === FALSE + && config_item('compress_output') === TRUE + && extension_loaded('zlib') + ); // Get mime types for later $this->mimes =& get_mimes(); @@ -436,15 +448,14 @@ class CI_Output { if ($this->parse_exec_vars === TRUE) { $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; - $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); } // -------------------------------------------------------------------- // Is compression requested? - if ($CFG->item('compress_output') === TRUE && $this->_zlib_oc === FALSE - && extension_loaded('zlib') + if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed + && $this->_compress_output === TRUE && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) { ob_start('ob_gzhandler'); @@ -468,6 +479,21 @@ class CI_Output { // simply echo out the data and exit. if ( ! isset($CI)) { + if ($this->_compress_output === TRUE) + { + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) + { + header('Content-Encoding: gzip'); + header('Content-Length: '.strlen($output)); + } + else + { + // User agent doesn't support gzip compression, + // so we'll have to decompress our cache + $output = gzinflate(substr($output, 10, -8)); + } + } + echo $output; log_message('debug', 'Final output sent to browser'); log_message('debug', 'Total execution time: '.$elapsed); @@ -530,9 +556,9 @@ class CI_Output { return; } - $uri = $CI->config->item('base_url'). - $CI->config->item('index_page'). - $CI->uri->uri_string(); + $uri = $CI->config->item('base_url') + .$CI->config->item('index_page') + .$CI->uri->uri_string(); $cache_path .= md5($uri); @@ -542,17 +568,39 @@ class CI_Output { return; } - $expire = time() + ($this->cache_expiration * 60); - - // Put together our serialized info. - $cache_info = serialize(array( - 'expire' => $expire, - 'headers' => $this->headers - )); - if (flock($fp, LOCK_EX)) { - fwrite($fp, $cache_info.'ENDCI--->'.$output); + // If output compression is enabled, compress the cache + // itself, so that we don't have to do that each time + // we're serving it + if ($this->_compress_output === TRUE) + { + $output = gzencode($output); + + if ($this->get_header('content-type') === NULL) + { + $this->set_content_type($this->mime_type); + } + } + + $expire = time() + ($this->cache_expiration * 60); + + // Put together our serialized info. + $cache_info = serialize(array( + 'expire' => $expire, + 'headers' => $this->headers + )); + + $output = $cache_info.'ENDCI--->'.$output; + + for ($written = 0, $length = strlen($output); $written < $length; $written += $result) + { + if (($result = fwrite($fp, substr($output, $written))) === FALSE) + { + break; + } + } + flock($fp, LOCK_UN); } else @@ -560,13 +608,22 @@ class CI_Output { log_message('error', 'Unable to secure a file lock for file at: '.$cache_path); return; } + fclose($fp); - @chmod($cache_path, FILE_WRITE_MODE); - log_message('debug', 'Cache file written: '.$cache_path); + if (is_int($result)) + { + @chmod($cache_path, FILE_WRITE_MODE); + log_message('debug', 'Cache file written: '.$cache_path); - // Send HTTP cache-control headers to browser to match file cache settings. - $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); + // Send HTTP cache-control headers to browser to match file cache settings. + $this->set_cache_header($_SERVER['REQUEST_TIME'], $expire); + } + else + { + @unlink($cache_path); + log_message('error', 'Unable to write the complete cache content at: '.$cache_path); + } } // -------------------------------------------------------------------- @@ -701,7 +758,7 @@ class CI_Output { else { header('Pragma: public'); - header('Cache-Control: max-age=' . $max_age . ', public'); + header('Cache-Control: max-age='.$max_age.', public'); header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT'); header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT'); } @@ -740,13 +797,13 @@ class CI_Output { preg_match_all('{<style.+</style>}msU', $output, $style_clean); foreach ($style_clean[0] as $s) { - $output = str_replace($s, $this->_minify_script_style($s, TRUE), $output); + $output = str_replace($s, $this->_minify_js_css($s, 'css', TRUE), $output); } // Minify the javascript in <script> tags. foreach ($javascript_clean[0] as $s) { - $javascript_mini[] = $this->_minify_script_style($s, TRUE); + $javascript_mini[] = $this->_minify_js_css($s, 'js', TRUE); } // Replace multiple spaces with a single space. @@ -792,13 +849,14 @@ class CI_Output { break; case 'text/css': + + return $this->_minify_js_css($output, 'css'); + case 'text/javascript': case 'application/javascript': case 'application/x-javascript': - $output = $this->_minify_script_style($output); - - break; + return $this->_minify_js_css($output, 'js'); default: break; } @@ -809,134 +867,100 @@ class CI_Output { // -------------------------------------------------------------------- /** - * Minify Style and Script - * - * Reduce excessive size of CSS/JavaScript content. To remove spaces this - * script walks the string as an array and determines if the pointer is inside - * a string created by single quotes or double quotes. spaces inside those - * strings are not stripped. Opening and closing tags are severed from - * the string initially and saved without stripping whitespace to preserve - * the tags and any associated properties if tags are present + * Minify JavaScript and CSS code * - * Minification logic/workflow is similar to methods used by Douglas Crockford - * in JSMIN. http://www.crockford.com/javascript/jsmin.html + * Strips comments and excessive whitespace characters * - * KNOWN ISSUE: ending a line with a closing parenthesis ')' and no semicolon - * where there should be one will break the Javascript. New lines after a - * closing parenthesis are not recognized by the script. For best results - * be sure to terminate lines with a semicolon when appropriate. - * - * @param string $output Output to minify - * @param bool $has_tags Specify if the output has style or script tags - * @return string Minified output + * @param string $output + * @param string $type 'js' or 'css' + * @param bool $tags Whether $output contains the 'script' or 'style' tag + * @return string */ - protected function _minify_script_style($output, $has_tags = FALSE) + protected function _minify_js_css($output, $type, $tags = FALSE) { - // We only need this if there are tags in the file - if ($has_tags === TRUE) + if ($tags === TRUE) { - // Remove opening tag and save for later - $pos = strpos($output, '>') + 1; - $open_tag = substr($output, 0, $pos); - $output = substr_replace($output, '', 0, $pos); + $tags = array('close' => strrchr($output, '<')); - // Remove closing tag and save it for later - $pos = strpos($output, '</'); - $closing_tag = substr($output, $pos, strlen($output)); - $output = substr_replace($output, '', $pos); - } + $open_length = strpos($output, '>') + 1; + $tags['open'] = substr($output, 0, $open_length); - // Remove CSS comments - $output = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!i', '', $output); + $output = substr($output, $open_length, -strlen($tags['close'])); - // Remove spaces around curly brackets, colons, - // semi-colons, parenthesis, commas - $chunks = preg_split('/([\'|"]).+(?![^\\\]\\1)\\1/iU', $output, -1, PREG_SPLIT_OFFSET_CAPTURE); - for ($i = count($chunks) - 1; $i >= 0; $i--) - { - $output = substr_replace( - $output, - preg_replace('/\s*(:|;|,|}|{|\(|\))\s*/i', '$1', $chunks[$i][0]), - $chunks[$i][1], - strlen($chunks[$i][0]) - ); + // Strip spaces from the tags + $tags = preg_replace('#\s{2,}#', ' ', $tags); } - // Replace tabs with spaces - // Replace carriage returns & multiple new lines with single new line - // and trim any leading or trailing whitespace - $output = trim(preg_replace(array('/\t+/', '/\r/', '/\n+/'), array(' ', "\n", "\n"), $output)); + $output = trim($output); - // Remove spaces when safe to do so. - $in_string = $in_dstring = $prev = FALSE; - $array_output = str_split($output); - foreach ($array_output as $key => $value) + if ($type === 'js') { - if ($in_string === FALSE && $in_dstring === FALSE) + // Catch all string literals and comment blocks + if (preg_match_all('#((?:((?<!\\\)\'|")|(/\*)|(//)).*(?(2)(?<!\\\)\2|(?(3)\*/|\n)))#msuUS', $output, $match, PREG_OFFSET_CAPTURE)) { - if ($value === ' ') + $js_literals = $js_code = array(); + for ($match = $match[0], $c = count($match), $i = $pos = $offset = 0; $i < $c; $i++) { - // Get the next element in the array for comparisons - $next = $array_output[$key + 1]; - - // Strip spaces preceded/followed by a non-ASCII character - // or not preceded/followed by an alphanumeric - // or not preceded/followed \ $ and _ - if ((preg_match('/^[\x20-\x7f]*$/D', $next) OR preg_match('/^[\x20-\x7f]*$/D', $prev)) - && ( ! ctype_alnum($next) OR ! ctype_alnum($prev)) - && ! in_array($next, array('\\', '_', '$'), TRUE) - && ! in_array($prev, array('\\', '_', '$'), TRUE) - ) + $js_code[$pos++] = trim(substr($output, $offset, $match[$i][1] - $offset)); + $offset = $match[$i][1] + strlen($match[$i][0]); + + // Save only if we haven't matched a comment block + if ($match[$i][0][0] !== '/') { - unset($array_output[$key]); + $js_literals[$pos++] = array_shift($match[$i]); } } - else - { - // Save this value as previous for the next iteration - // if it is not a blank space - $prev = $value; - } - } + $js_code[$pos] = substr($output, $offset); - if ($value === "'") - { - $in_string = ! $in_string; + // $match might be quite large, so free it up together with other vars that we no longer need + unset($match, $offset, $pos); } - elseif ($value === '"') + else { - $in_dstring = ! $in_dstring; + $js_code = array($output); + $js_literals = array(); } + + $varname = 'js_code'; + } + else + { + $varname = 'output'; } - // Put the string back together after spaces have been stripped - $output = implode($array_output); + // Standartize new lines + $$varname = str_replace(array("\r\n", "\r"), "\n", $$varname); - // Remove new line characters unless previous or next character is - // printable or Non-ASCII - preg_match_all('/[\n]/', $output, $lf, PREG_OFFSET_CAPTURE); - $removed_lf = 0; - foreach ($lf as $feed_position) + if ($type === 'js') { - foreach ($feed_position as $position) - { - $position = $position[1] - $removed_lf; - $next = $output[$position + 1]; - $prev = $output[$position - 1]; - if ( ! ctype_print($next) && ! ctype_print($prev) - && ! preg_match('/^[\x20-\x7f]*$/D', $next) - && ! preg_match('/^[\x20-\x7f]*$/D', $prev) - ) - { - $output = substr_replace($output, '', $position, 1); - $removed_lf++; - } - } + $patterns = array( + '#\s*([!\#%&()*+,\-./:;<=>?@\[\]^`{|}~])\s*#' => '$1', // Remove spaces following and preceeding JS-wise non-special & non-word characters + '#\s{2,}#' => ' ' // Reduce the remaining multiple whitespace characters to a single space + ); + } + else + { + $patterns = array( + '#/\*.*(?=\*/)\*/#s' => '', // Remove /* block comments */ + '#\n?//[^\n]*#' => '', // Remove // line comments + '#\s*([^\w.\#%])\s*#U' => '$1', // Remove spaces following and preceeding non-word characters, excluding dots, hashes and the percent sign + '#\s{2,}#' => ' ' // Reduce the remaining multiple space characters to a single space + ); + } + + $$varname = preg_replace(array_keys($patterns), array_values($patterns), $$varname); + + // Glue back JS quoted strings + if ($type === 'js') + { + $js_code += $js_literals; + ksort($js_code); + $output = implode($js_code); + unset($js_code, $js_literals, $varname, $patterns); } - // Put the opening and closing tags back if applicable - return isset($open_tag) - ? $open_tag.$output.$closing_tag + return is_array($tags) + ? $tags['open'].$output.$tags['close'] : $output; } |