<?php
/*
CSStacker
***************************************************************************
Current version:
1.3
***************************************************************************
css.class.php, a CSS optimizer and compressor
http://unreal4u.com/
Coded by unreal4u (Camilo Sperberg)
Copyleft (c) 2010 Camilo Sperberg
Released under the BSD License
*/
if (!isset($proc)) die('Sorry, direct access is not allowed!');
class CSStacker {
private $files = array();
private $qCSS = 0;
private $filename = '';
private $lastmod = '';
public $resetCSS = FALSE;
// PUBLIC function that adds CSS files to the stack
public function add($file) {
if (is_array($file)) {
foreach($file AS $a)
if ($this->verify_original_css($a)) $this->qCSS++;
}
else if ($this->verify_original_css($file)) $this->qCSS++;
}
// PUBLIC function that does all the hard work
public function printme($method = 'file',$force='') {
if($this->qCSS > 0 AND ($method == 'file' OR $method == 'inline' OR $method == 'filename')) {
if ($force == 'force') {
$force = TRUE;
$this->error('NOTICE','Cache creation is being forced!');
}
else $force = FALSE;
$this->filename = $this->get_cache_filename();
$this->lastmod = time();
$status = $this->status($method,$force);
switch($status['act']) {
CASE 1 : header($_SERVER['SERVER_PROTOCOL'].' 304 Not Modified', TRUE, 304);
$this->output_headers();
break;
CASE 2 : ob_start();
header('Content-Type: text/css; charset='.CHARSET);
$this->output_headers();
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) AND GZIP_CONTENTS) {
$enc = explode(',',str_replace(' ','',$_SERVER['HTTP_ACCEPT_ENCODING']));
if (in_array('gzip',$enc)) {
header('Content-Encoding: gzip');
echo gzencode($status['css'],GZIP_LEVEL);
return TRUE;
}
}
echo $status['css'];
header('Content-Length: '.ob_get_length());
ob_end_flush();
break;
CASE 3 : echo $status['css'];
break;
CASE 4 : return $status['css'];
break;
}
return 1;
}
return 0;
}
/*
PRIVATE function that verifies if original CSS's can be readable or not
*/
private function verify_original_css($file) {
$is_valid = TRUE;
if (is_readable($file) AND !in_array($file,$this->files)) $this->files[] = $file;
else {
$is_valid = FALSE;
if (!file_exists($file)) $this->error('WARNING','File "'.$file.'" doesn\'t exist');
else $this->error('FATAL','File "'.$file.'" exists but I cannot read it. Permission problems?');
}
return $is_valid;
}
/*
PRIVATE function that does several checks, and based on that returns a status code and the corresponding CSS
*/
private function status($method,$force) {
$out = array('act' => '1', 'css' => FALSE);
$cache_created = FALSE;
$is_cache_valid = FALSE;
if (USE_CSS_CACHE AND $force == FALSE) $is_cache_valid = $this->valid_cache();
else $force = TRUE;
if ($is_cache_valid AND $force == FALSE) {
if ($method == 'file' AND USE_CSS_CACHE AND USE_BROWSER_CACHE AND isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->lastmod) return $out;
elseif ($method == 'filename') return array('act' => '4', 'css' => $this->filename);
elseif ($method == 'inline') return array('act' => '3', 'css' => $this->get_cache());
else $out['act'] = 2;
}
elseif ($force == TRUE OR !$is_cache_valid) {
$out['css'] = $this->compress();
if ($method == 'file') $out['act'] = 2;
elseif ($method == 'inline') $out['act'] = 3;
}
if ($out['css'] !== FALSE) $cache_created = $this->create_cache($out['css']);
else $out['css'] = $this->get_cache();
if ($method == 'filename') {
$out['act'] = 4;
$out['css'] = $cache_created;
}
return $out;
}
/*
PRIVATE function that outputs the necesary browser cache headers
*/
private function output_headers() {
header('Last-Modified: '.gmdate('D, d M Y H:i:s',$this->lastmod).' GMT');
header('Cache-Control: public, must-revalidate, max-age='.TIME_BROWSER_CACHE);
header('Expires: '.gmdate('D, d M Y H:i:s',time() + TIME_BROWSER_CACHE).' GMT');
return TRUE;
}
/*
PRIVATE function that checks if cache is still valid
*/
private function valid_cache($check = NULL) {
if (!USE_CSS_CACHE) return FALSE;
$is_valid = TRUE;
if (!is_readable($this->filename)) {
$is_valid = FALSE;
if (!file_exists($this->filename)) $this->error('NOTICE','Cache file doesn\'t exist.');
else $this->error('FATAL','Couldn\'t read the cache file. Please check permissions!');
}
if ($is_valid)
foreach($this->files AS $a)
if (filemtime($a) > filemtime($this->filename) AND $is_valid)
$is_valid = FALSE;
if (!empty($check) OR !$is_valid) return $is_valid;
else {
$this->lastmod = filemtime($this->filename);
return $is_valid;
}
}
/*
PRIVATE function that creates or get an unique name for the cache based on original CSS filenames
*/
private function get_cache_filename() {
$filename = '';
foreach($this->files AS $a) $filename .= $a;
return CACHE_LOCATION.md5($filename).'.css';
}
/*
PRIVATE function that actually creates the cache file
*/
private function create_cache($contents) {
$created = FALSE;
if (is_writable(CACHE_LOCATION)) {
if (USE_CSS_CACHE) {
file_put_contents($this->filename,$contents);
$created = $this->filename;
}
}
else $this->error('WARNING','Cache location isn\'t writable');
return $created;
}
/*
PRIVATE function that rescues the cache content
*/
private function get_cache() {
$cache = FALSE;
$recreate = FALSE;
if (is_readable($this->filename)) {
if (!$this->valid_cache(TRUE)) $recreate = TRUE;
if ($recreate === FALSE) $cache = file_get_contents($this->filename);
else {
unlink($this->filename);
$this->error('NOTICE','Cache file out-dated. Deleted so it can be recreated');
}
}
else {
if(!file_exists($this->filename)) $this->error('NOTICE','Cache file doesn\'t exist');
else $this->error('WARNING','Cache file exists, but I could\'t read it. Permission or disk problems?');
}
return $cache;
}
/*
PRIVATE function that does several replaces in order to strip comments and remove some common unnecesary spaces
*/
private function compress() {
if ($this->resetCSS) $out = 'html{color:#000;background:#FFF}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}li{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}q:before,q:after{content:\'\'}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}input,textarea,select{*font-size:100%}legend{color:#000}';
// CSS reset is taken from Yahoo developer and optimized/compressed:
// http://developer.yahoo.com/yui/3/cssreset/
// Original file is the Global one, its size is 501 bytes, compressed to 343 bytes.
else $out = '';
foreach($this->files AS $a) {
$css_content = file_get_contents($a);
if(strlen($css_content) > 94325) $tmp_buffer = str_split($css_content,94300);
else $tmp_buffer[] = $css_content;
// Why this? See: http://www.php.net/manual/en/function.preg-replace.php#93840
// Note that around that area it is impossible to do a good optimization!
// If you happen to have such big CSS files, it would be better to separate it into smaller ones
unset($css_content);
$tmp_buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!','',$tmp_buffer); // Strip out all comments
$tmp_buffer = str_replace (array("\r\n", "\r", "\n", "\t", ' ', ' '),'',$tmp_buffer); // All enter types + additional white spaces
if(OPTIMIZE_CSS === TRUE) $out .= $this->optimize($tmp_buffer);
else {
if(is_array($tmp_buffer)) {
foreach($tmp_buffer AS $b) $out .= $b;
unset($b);
}
else $out .= $tmp_buffer;
}
unset($tmp_buffer);
}
return $out;
}
/*
PRIVATE function that goes a little further replacing color codes and some common colors with their color codes
*/
private function optimize($the_css) {
$original = array(', ',' , ',';}','; }',' ; }',' :',': ',' {','; ', // Some common typos and unnecesary characters
':black',':dark grey',':fuchsia',':light grey',':orange',':white',':yellow'); // Come common colors which are shorter in hex
$replace = array(',' ,',' ,'}' ,'}' ,'}' ,':' ,':' ,'{' ,';', // their replacements
':#000' ,':#666' ,':#F0F' ,':#CCC' ,':#F60' ,':#FFF' ,':#FF0');
$the_css = str_replace (array(' 0px',' 0em',' 0%',' 0ex',' 0cm',' 0mm',' 0in',' 0pt',' 0pc'),' 0',$the_css);
$the_css = str_replace (array(':0px',':0em',':0%',':0ex',':0cm',':0mm',':0in',':0pt',':0pc'),':0',$the_css); // 0 needs no unit
foreach($the_css AS $a) { // Color code optimization. Ex: #334455 -> #345
$how_many = substr_count($a,'#');
$offset = 0;
for($i = 0; $i < $how_many; $i++) {
$from = strpos($a,'#',$offset) + 1;
$tmp = substr($a,$from,6);
$k = strlen($tmp);
for ($j = 0; $j < $k + 1; $j++) {
if ($j % 2 != 0)
if ($pre[$j - 1] == $tmp{$j}) $pre[] = TRUE;
else $pre[] = FALSE;
elseif ($j != $k) $pre[] = $tmp{$j};
}
if ($pre[1] AND $pre[3] AND $pre[5] AND !in_array('#'.$tmp,$original)) {
$original[] = '#'.$tmp;
$replace[] = '#'.$pre[0].$pre[2].$pre[4];
}
unset($pre,$tmp,$j,$k);
$offset = $from;
}
}
$the_css = str_ireplace($original,$replace,$the_css);
$out = '';
foreach($the_css AS $a) $out .= $a;
unset($original,$replace,$how_many,$offset,$from,$i,$a,$the_css);
$out = implode('}',array_reverse(array_unique(array_reverse(explode('}',$out)))));
// NOTE: This is beta! If you happen to have problems with the CSS, comment the above line
// What it does is stripping out repeated declarations, which means that:
// body{color:#000}p{background:#EEE}body{color:#000}
// will result in:
// p{background:#EEE}body{color:#000}
return $out;
}
/*
PRIVATE function that keep a log on all errors or notices
*/
private function error($errtype,$errmsg) {
global $CSSErrors;
$CSSErrors[] = array('type' => $errtype, 'errm' => $errmsg);
}
}
?>
|