<?php
/**
* GzOutput class,
* simple class with some static method to create gz output without ob_gzhandler
* ______________________________________________________________
* @example
* GzOutput::create('body{padding:0;}', 'text/css'); // css file
* GzOutput::createNew('var something = "somevalue";', 'text/javascript'); // JS file without caching
* GzOutput::createFromList(array('file.css', 'file2.css'), 'text/css'); // css file
* GzOutput::createNewFromList(array('file.css', 'file2.css'), 'text/css'); // css file without caching
* --------------------------------------------------------------
* @example using external "bootstrap.php" file (css example)
*
* <link rel="stylesheet" href="css/bootstrap.css.php?base|tables|extra" media="screen" type="text/css" />
*
* <?php // css/bootstrap.css.php
* require '../classes/GzOutput.class.php';
* GzOutput::createFromFile(__FILE__, 'text/css');
* // single css file with: [css/base.css, css/tables.css, css/extra.css]
* ?>
* Please read over createFromFile method to know more
* --------------------------------------------------------------
* @example using external configuration file (for createFromConfig static method)
* <?php
* // You can add or remove every "type" configuration but please don't change GzOutputClassFile and GlobalCacheFolder key names
* $config = array(
*
* // * relative path from this file
* // (i.e. 'javascript' if this file folder has a "javascript" folder, '../javascript/' if folder is before than this one)
*
* 'GzOutputClassFile' => 'php/classes/GzOutput.class.php', // relative GzOutput class file folder
* 'GlobalCacheFolder' => 'cache', // relative cache folder*
*
* 'JavaScriptInformations'=> array( // JavaScript configuration
* 'Folder' => 'javascript', // relative JavaScript folder*
* 'Extension' => 'js', // JavaScript extension
* 'Type' => 'text/javascript', // JavaScript Content-Typ,
* 'Comments' => '// (C) Andrea Giammarchi', // JavaScript Comments
* 'Compression' => 9, // JavaScript Compression level (0 - 9)
* 'Crunch' => 2 // JavaScript Crunch level (0 - 3)
* ),
*
* 'CSSInformations' => array( // CSS configuration
* 'Folder' => 'css', // relative CSS folder*
* 'Extension' => 'css', // CSS extension
* 'Type' => 'text/css', // CSS Content-Type
* 'Comments' => '// (C) Andrea Giammarchi', // CSS Comments
* 'Compression'=> 9, // CSS Compression level (0 - 9)
* 'Crunch' => 3 // CSS Crunch level (0 - 3)
* )
* );
*
* // please don't modify these two lines
* require $config['GzOutputClassFile'];
* GzOutput::createFromConfig($config);
* ?>
* --------------------------------------------------------------
* @Compatibility PHP >= 4 [requires zlib extension and gzencode function to work correctly or produced output will not be compressed]
* @Dependencies Crunch.class.php
* @Author Andrea Giammarchi
* @Site http://www.devpro.it/
* @Date 2007/01/15
* @Version 0.5 [new createFromCache and createFromConfig static methods, please view bootstrap.php example file to know more]
*/
define('GzOutput_ACCEPT_ENCODING', isset($_SERVER['HTTP_ACCEPT_ENCODING']));
define('GzOutput_GZIP', GzOutput_ACCEPT_ENCODING && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false);
define('GzOutput_DEFLATE', GzOutput_ACCEPT_ENCODING && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== false);
class GzOutput {
/**
* public static method,
* exit showing created output using a compression level
*
* GzOutput::create(output:String, type:String [, level:UIntRange(0,9)]):Void(exit)
*
* @param String the content to show
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @param UInt compression level from 0 to 9, default: 9
*/
function create($output, $type, $level = 9) {
$cache = !defined('GzOutput_NO_CACHE');
$hash = GzOutput::__getGzHeader($output, $level);
if(!empty($hash))
header($hash);
$hash = '"'.sha1($output).'"';
if($cache) {
header('ETag: '.$hash);
header('Cache-Control: public');
}
if($cache && isset($_SERVER['HTTP_IF_NONE_MATCH']) && $hash === $_SERVER['HTTP_IF_NONE_MATCH']) {
header('HTTP/1.1 304 Not Modified');
$output = '';
}
else
header('Content-Length: '.strlen($output));
header('Content-Type: '.$type);
exit($output);
}
/**
* public static method,
* exit showing created output using a cache list of directories, a list of files, used type, compression level and crunch level.
*
* GzOutput::createFromCache(dirs:Array, list:Array, type:String [, level:Uint [, crunch:UInt]]):Void(exit)
*
* @param Array associative array with relative paths. The cache key with its path is required. Other keys should be setted or not.
* Exmple: array('cache'=>'cache', 'javascript'=>'js') // if You use this method to parse a list of javascript files
* array('cache'=>'cache', 'css'=>'css') // if You use this method to parse a list of css files
* @param Array one or more file to cache
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @param UInt gz compression level from 0 to 9, default: 9
* @param UInt Cruncher compression level from 0 to 3, default: 0 (no crunch)
*/
function createFromCache($dir, $list, $type, $comments = '', $level = 9, $crunch = 0){
$etag = $ext = $hash = 'a';
$fp = null;
$lastmod = 0;
$output = GzOutput::getRealType($type);
if(!isset($dir[$output])) {
header('HTTP/1.0 503 Not Implemented');
$output = '';
}
else {
foreach($list as $key => $value) {
if(!file_exists($list[$key] = $dir[$output].'/'.$value)) {
header('HTTP/1.0 404 Not Found');
$output = '';
$lastmod = 0;
break;
}
else
$lastmod = max(filemtime($list[$key]), $lastmod);
}
if($lastmod) {
switch($ext = GzOutput::__getGzHeader($hash, $level)) {
case 'Content-Encoding: gzip':
header($ext);
$ext = 'gzip';
break;
case 'Content-Encoding: deflate':
header($ext);
$ext = 'deflate';
break;
default:
$ext = 'text';
break;
}
$hash = $output.sha1(implode('|', $list)).$ext;
$etag = '"'.$lastmod.$hash.'"';
header('ETag: '.$etag);
header('Cache-Control: public');
if(isset($_SERVER['HTTP_IF_NONE_MATCH']) && $etag === $_SERVER['HTTP_IF_NONE_MATCH']) {
header('HTTP/1.1 304 Not Modified');
$output = '';
}
else {
$dir['cache'] = $dir['cache'].'/'.$hash;
switch(file_exists($dir['cache']) && filemtime($dir['cache']) === $lastmod){
case true:
$output = file_get_contents($dir['cache']);
break;
default:
$output = '';
foreach($list as $value)
$output .= trim(file_get_contents($value))."\r\n";
$output = rtrim($output);
if($crunch) {
$fp = new Cruncher;
$output = $fp->crunch($output, $crunch);
}
if(!empty($comments))
$output = $comments."\r\n".$output;
GzOutput::__getGzHeader($output, $level);
if(@$fp = fopen($dir['cache'], 'wb')){
@flock($fp, LOCK_EX );
fwrite($fp, $output);
@flock($fp, LOCK_UN);
fclose($fp);
@touch($dir['cache'], $lastmod, $lastmod);
$fp = fopen($dir['cache'].'.txt', 'wb');
@fwrite($fp, implode("\r\n\r\n", $list = array(
"[CACHE FILE INFORMATIONS]\r\n\tcreated by GzOutput createFromCache public static method\r\n".
"Date: ".gmdate('D, d M Y H:i:s')."\r\n".
"File: ".__FILE__."\r\n".
"___________________________________________",
"[host]\r\n".$_SERVER['HTTP_HOST'],
"[self]\r\n".$_SERVER['PHP_SELF'],
"[query string]\r\n".$_SERVER['QUERY_STRING'],
"[request]\r\n".$_SERVER['REQUEST_URI'],
"[user agent]\r\n".$_SERVER['HTTP_USER_AGENT'],
"[remote address]\r\n".$_SERVER['REMOTE_ADDR'],
"[included files]\r\n".implode("\r\n", $list),
"[file type]\t[compression]\t[crunch]\r\n{$ext}\t\t{$level}\t\t{$crunch}",
"[last modification date]\r\n".gmdate('D, d M Y H:i:s', $lastmod),
"[created hash]\r\n".$hash,
"[returned etag]\r\n".$etag,
"[user comments]\r\n".$comments
)));
fclose($fp);
}
GzOutput::forceNewContent();
break;
}
header('Content-Length: '.strlen($output));
}
}
}
header('Content-Type: '.$type);
exit($output);
}
/**
* public static method,
* exit showing created output using a compression level
*
* GzOutput::createFromConfig(*config:Array):Void(exit)
*
* @param Array configuration associative array
*/
function createFromConfig(&$config){
$pos = strpos($_SERVER['REQUEST_URI'], '?');
$ext = '';
$list = array();
if(
$pos !== false &&
isset($config['GlobalCacheFolder']) &&
is_dir($config['GlobalCacheFolder'])
) {
$list = explode('|', substr($_SERVER['REQUEST_URI'], ++$pos));
$ext = array_pop($list);
foreach($config as $key => $value) {
if($key !== 'GlobalCacheFolder' && $value['Extension'] === $ext) {
GzOutput::createFromCache(
array(
'cache' => $config['GlobalCacheFolder'],
GzOutput::getRealType($value['Type']) => $value['Folder']
),
array_map(create_function('$f', 'return str_replace(array("..", "./"), "", $f).".'.$ext.'";'), $list),
$value['Type'],
$value['Comments'],
$value['Compression'],
$value['Crunch']
);
}
}
}
}
/**
* public static method,
* exit showing created output using an external "bootstrap.php" file
*
* GzOutput::createFromFile(__FILE__:String [, type:String [, extension:String[, level:UIntRange(0,9) [, new:Boolean]]]]):Void(exit)
* @example
* [bootstrap.php inside folder js]
* <?php require '../classes/GzOutput.class.php'; GzOutput::createFromFile(__FILE__); ?>
*
* [index.html]
* <script type="text/javascript" src="js/bootstrap.php?jsfile1|jsfile2|otherjsFile|folder/jsFile3"></script>
*
* bootstrap.php file will read content of these files: js/jsfile1, js/jsfile2, js/otherjsFile.js, js/folder/jsFile3.js
*
* @param String external __FILE__ constant
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8"), default: 'text/javascript'
* @param String valid file extension, default: 'js'
* @param UInt compression level from 0 to 9, default: 9
* @param Boolean doesn't use cache or use it if present, default: false
*/
function createFromFile($__FILE__, $type = 'text/javascript', $ext = 'js', $level = 9, $new = false){
if(($list = strpos($_SERVER['REQUEST_URI'], '?')) !== false)
GzOutput::__listToOutput($list = array_map(
create_function('$f', 'return "'.dirname($__FILE__).'/".$f.".'.$ext.'";'),
explode('|', substr($_SERVER['REQUEST_URI'], ++$list))
), $type, $level, $new);
}
/**
* public static method,
* exit showing created output using an array with file names to read
*
* GzOutput::createFromList(list:Array, type:String [, level:UIntRange(0,9)]):Void(exit)
*
* @param Array an array with one or more files to get contents to create the output
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @param UInt compression level from 0 to 9, default: 9
*/
function createFromList($list, $type, $level = 9) {
GzOutput::__listToOutput($list, $type, $level, false);
}
/**
* public static method,
* exit showing created output using a compression level and removing cache problems
*
* GzOutput::createNew(output:String, type:String [, level:UIntRange(0,9)]):Void(exit)
*
* @param String the content to show
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @param UInt compression level from 0 to 9, default: 9
*/
function createNew($output, $type, $level = 9) {
define('GzOutput_NO_CACHE', true);
GzOutput::forceNewContent();
call_user_func(array('GzOutput', 'create'), $output, $type, $level);
}
/**
* public static method,
* exit showing created output using an array with file names to read and removing cache problems
*
* GzOutput::createNewFromList(list:Array, type:String [, level:UIntRange(0,9)]):Void(exit)
*
* @param Array an array with one or more files to get contents to create the output
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @param UInt compression level from 0 to 9, default: 9
*/
function createNewFromList($list, $type, $level = 9) {
GzOutput::__listToOutput($list, $type, $level, true);
}
/**
* public static method,
* set headers to force download of new content
*
* GzOutput::forceNewContent(Void):Void()
*/
function forceNewContent() {
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
}
/**
* public static method,
* parses a content-type string and returns the type ('css', 'javascript' ... others)
*
* GzOutput::getRealType(*type:String):String
*
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @return String content type witout text , charset and other informations
*/
function getRealType(&$type){
$realtype = substr($type, strpos($type, '/') + 1);
$i = strpos($realtype, ';');
if($i !== false)
$realtype = substr($realtype, 0, $i);
return trim($realtype);
}
/**
* private static method,
* checks extensions loaded, gz capabilities, compress output and returns header value
*
* GzOutput::__getGzHeader(*output:String, *level:UInt):String
*
* @param String the content to show
* @param UInt compression level from 0 to 9, default: 9
* @return String empty string or used gz compression header
*/
function __getGzHeader(&$output, &$level){
$header = '';
if(
extension_loaded('zlib') &&
function_exists('gzencode') &&
(GzOutput_GZIP || GzOutput_DEFLATE)
) {
switch(GzOutput_GZIP) {
case true:
$output = gzencode($output, $level, FORCE_GZIP);
$header = 'Content-Encoding: gzip';
break;
case false:
$output = gzencode($output, $level, FORCE_DEFLATE);
$header = 'Content-Encoding: deflate';
break;
}
}
return $header;
}
/**
* private static method,
* creates an output looping over list array and reading each file content.
*
* GzOutput::__listToOutput(*list:Array, *type:String, *level:UIntRange(0,9) [, nocache:Boolean]):Void(exit)
*
* @param Array an array with one or more files to get contents to create the output
* @param String the Content-Type (i.e. "text/css", "text/javascript", "text/html; charset:UTF-8")
* @param UInt compression level from 0 to 9, default: 9
* @param Boolean doesn't use cache or use it if present
*/
function __listToOutput(&$list, &$type, &$level, $new) {
$create = $new ? 'createNew' : 'create';
for($i = 0, $j = count($list); $i < $j; $i++)
$list[$i] = file_exists($list[$i]) && is_readable($list[$i]) ? file_get_contents($list[$i]) : '';
GzOutput::$create(implode("\r\n", $list), $type, $level);
}
}
?>