<?php
/**
* AlberT-cache
* fast and portable full-page caching system
*
* @copyright Copyleft Emiliano Gabrielli
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @author Emiliano Gabrielli <[email protected]>
* @version $Id: AlberT-cache.inc.php,v 1.7 2004/12/14 17:41:04 albert Exp $
* @package AlberT-cache
*/
/**
* Configuration file
*/
require_once(dirname(__FILE__).'/config.inc.php');
/**
* Automatically handles the entire caching stuffs
*
* It automagically handles everything concerning the caching mechanism:
* gzipping of the contents when the browser supports it, the browser
* cache validation, etc..
*
* @package AlberT-cache
*/
class AlberTcache
{
var $dbg;
var $gzip;
var $post;
var $timeout;
var $expire;
var $also;
var $oneSite;
var $storDir;
var $gcProb;
var $isOn;
var $mask;
var $absfile;
var $data;
var $variables;
var $file;
var $gzcont;
var $size;
var $crc32;
/**
* Class constructor
*/
function AlberTcache()
{
$this->dbg = $GLOBALS['CACHE_DEBUG'];
$this->gzip = $GLOBALS['CACHE_GZIP'];
$this->post = $GLOBALS['CACHE_POST'];
$this->timeout = $GLOBALS['CACHE_TIMEOUT'];
$this->expire = $GLOBALS['CACHE_EXP'];
$this->also = $GLOBALS['CACHE_ALSO'];
$this->oneSite = $GLOBALS['CACHE_1SITE'];
$this->storDir = $GLOBALS['CACHE_DIR'];
$this->gcProb = $GLOBALS['CACHE_GC'];
$this->isOn = $GLOBALS['CACHE_ON'];
$this->mask = $GLOBALS['CACHE_UMASK'];
/**
* We check if gzip functions are avaible
*/
if (!function_exists('gzcompress')) {
$this->gzip = FALSE;
$this->pdebug('GZIP disabled, gzcompress() does not exists. '.
'May be we are on Win...');
}
$this->start();
return TRUE;
}
/**
* Resets the cache state
*/
function doReset()
{
$this->absfile = NULL;
$this->data = array();
$this->variables = array();
return TRUE;
}
/**
* Saves a variable state between caching
*
* @param mixed $vn the name of the variable to save
*/
function storeVar($vn)
{
$this->pdebug('Adding '.$vn.' to the variable store');
$this->variables[] = $vn;
return TRUE;
}
/**
* A simple deguggig handler function
*
* @param string $s The debugging message
*/
function pdebug($s)
{
static $debugline = 0;
if ($this->dbg) {
header('X-Debug-'.++$debugline.': '.$s);
// we can't print any output without generating a warning !!!
//print_r("Line$debugline: $s<br>\n");
}
return TRUE;
}
/**
* Generates the key for the request
*/
function getDefaultKey()
{
return md5('POST='.serialize($_POST).
' GET='.serialize($_GET).
' OTHER='.serialize($this->also));
}
/**
* Returns the default object used by the helper functions
*/
function getDefaultObj()
{
if ($this->oneSite)
$name = $_SERVER['PHP_SELF'];
else
$name = $_SERVER['PATH_TRANSLATED'];
if ($name=='')
$name = 'http://'.$_SERVER['HTTP_HOST'].'/'.$_SERVER['PHP_SELF'];
return $name;
}
/**
* Caches the current page based on the page name and the GET/POST
* variables. All must match or else it will not be fetched
* from the cache!
*/
function cacheAll($cachetime=60)
{
$this->file = $this->getDefaultObj();
return $this->theCache($cachetime, $this->getDefaultKey());
}
/**
* Obtains a lock on the cache storage
*/
function lock_fs($fp, $mode='w')
{
switch ($mode) {
case 'w':
case 'W':
return flock($fp, LOCK_EX);
break;
case 'r':
case 'R':
return flock($fp, LOCK_SH);
break;
default:
die('FATAL: invalid lock mode: '.$mode.' in '.__FILE__.
' for method lock_fs()');
}
}
/**
* Performs the unlock
*/
function unlock_fs($fp)
{
return flock($fp, LOCK_UN);
}
/**
* Writes out the cache
*/
function add_fs($file, $data)
{
$fp = @fopen($file, 'wb');
if (!$fp) {
$this->pdebug('Failed to open for write out to '.$file);
return FALSE;
}
$this->lock_fs($fp, 'w');
fwrite($fp, $data, strlen($data));
$this->unlock_fs($fp);
fclose($fp);
return TRUE;
}
/**
* Reads in the cache
*/
function get_fs($file)
{
$fp = @fopen($file, 'rb');
if (!$fp) {
return NULL;
}
$this->lock_fs($fp, 'r');
$buff = fread($fp, filesize($file));
$this->unlock_fs($fp);
fclose($fp);
return $buff;
}
/**
* Returns the storage for cache
*/
function getStorage($cacheobject)
{
return $this->storDir.'/'.$cacheobject;
}
/**
* Cache garbage collector
*/
function doGC()
{
$de = '';
// Should we garbage collect ?
if ($this->gcProb>0) {
mt_srand(time());
$precision = 100000;
$r = (mt_rand()%$precision)/$precision;
if ($r>($this->gcProb/100)) {
return FALSE;
}
}
$this->pdebug('Running gc');
$dp = @opendir($this->storDir);
if (!$dp) {
$this->pdebug("Error opening '{$this->storDir}' for cleanup");
return FALSE;
}
// walking into the dir and remove expired files
while (FALSE !== ($de=readdir($dp)) ) {
if ( $de != '.' && $de != '..' ) {
// To get around strange php-strpos, add additional char
if (strpos(" $de", 'cache-')==1) {
$absfile = $this->storDir.'/'.$de;
$thecache = unserialize($this->get_fs($absfile));
if (is_array($thecache)) {
if ($thecache['cachetime']!='0' && $thecache['expire']<=time()) {
if (@unlink($absfile))
$this->pdebug('Deleted '.$absfile);
else
$this->pdebug('Failed to delete '.$absfile);
}
else
$this->pdebug($absfile.' expires in '.($thecache['expire']-time()));
}
else
$this->pdebug($absfile.' is empty, being processed in another process?');
}
}
}
@closedir($dp);
return TRUE;
}
/** theCache()
*
* Caches $object based on $key for $cachetime, will return 0 if the
* object has expired or does not exist.
*/
function theCache($cachetime, $key=NULL)
{
if (!$this->isOn) {
$this->pdebug('Not caching, CACHE_ON is 0');
return 0;
}
$curtime = time();
// Make it a valid name
$this->file = eregi_replace('[^A-Z,0-9,=]', '_', $this->file);
$key = eregi_replace('[^A-Z,0-9,=]', '_', $key);
$this->pdebug('Caching based on OBJECT='.$this->file.' KEY='.$key);
$this->file = 'cache-'.$this->file.'-'.$key;
$this->absfile = $this->getStorage($this->file);
// Can we access the cache_file ?
if (($buff = $this->get_fs($this->absfile))) {
$this->pdebug('Opened the cache file');
$cdata = unserialize($buff);
//var_dump($cdata);
if (is_array($cdata)) {
$curco = $cdata['cache_object'];
if ($curco != $this->absfile) {
$this->pdebug('WTF?! That is not my cache file! got='.$curco.
' wanted='.$this->absfile);
}
else {
if ($cdata['cachetime']=='0' || $cdata['expire']>=$curtime) {
// data not yet expired (or never expiring)
$expirein = $cdata['expire']-$curtime+1;
$this->pdebug('Cache expires in '.$expirein);
// restore variables
if (is_array($cdata['variables'])) {
foreach ($cdata['variables'] as $k=>$v) {
$this->pdebug('Restoring variable '.$k.' to value '.$v);
$GLOBALS[$k] = $v;
}
}
// restore gzcontent
$this->pdebug('Restoring gzipped content');
$this->gzcont = $cdata['gzcontent'];
$ret = $expirein;
if ($cdata['cachetime']=='0') {
$ret = 'INFINITE';
}
$this->doReset();
return $ret;
}
}
}
}
else {
// No cache file (yet) or unable to read
$this->pdebug('No previous cache of '.$this->absfile.' or unable to read');
}
// If we came here: start caching!
$umask = (function_exists('umask')) ? TRUE : FALSE;
// Create the file for this page
if ($umask === TRUE) {
$oldum = umask();
umask($this->mask);
}
if (function_exists('readlink') && @readlink($this->absfile)) {
$this->pdebug($this->absfile.' is a symlink! not caching!');
$this->absfile = NULL;
}
else {
$this->pdebug('Created '.$this->absfile.', waiting for callback');
$fp = @fopen($this->absfile, 'wb');
if (!$fp) {
$this->pdebug('Unable to open for write '.$this->absfile);
}
}
if ($umask === TRUE) {
umask($oldum);
}
// Set expire and cachetime
$this->data['expire'] = $curtime + $cachetime;
$this->data['cachetime'] = $cachetime;
return 0;
}
/** doWrite()
*
* Does the actual caching
*/
function doWrite()
{
if (!$this->isOn) {
$this->pdebug('Not caching, CACHE_ON is off');
return 0;
}
if ($this->absfile!=NULL) {
$variables = array();
foreach ($this->variables as $vn) {
if (isset($GLOBALS[$vn])) {
$this->pdebug('Setting variable '.$vn.' to '.$GLOBALS[$vn]);
$variables[$vn] = $GLOBALS[$vn];
}
}
// Fill cache_data
$this->data['gzcontent'] = $this->gzcont;
$this->data['cache_object'] = $this->absfile;
$this->data['variables'] = $this->variables;
$datas = serialize($this->data);
// write data
$this->add_fs($this->absfile, $datas);
}
}
/** getEncoding()
*
* Are we capable of receiving gzipped data ?
* Returns the encoding that is accepted. Maybe additional check for Mac ?
*/
function getEncoding()
{
if ( is_array($_SERVER) && array_key_exists('HTTP_ACCEPT_ENCODING', $_SERVER) ) {
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== FALSE) {
return 'x-gzip';
}
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
return 'gzip';
}
}
return FALSE;
}
/** init()
*
* Checks some global variables and might decide to disable caching
* and calls appropriate initialization-methods
*/
function init()
{
// Override default CACHE_TIME ?
if (isset($this->timeout)) {
$this->expire = $this->timeout;
}
// Force cache off when POST occured when you don't want it cached
if (!$this->post && (count($_POST) > 0)) {
$this->isOn = 0;
$this->expire = -1;
}
// A cachetimeout of -1 disables writing, only ETag and content
// encoding if possible
if ($this->expire == -1) {
$this->isOn = 0;
$this->pdebug('$expire == -1 disabling cache: CACHE_ON is off');
}
// Reset cache
$this->doReset();
}
/** start()
*
* Sets the handler for callback
*/
function start()
{
// Initialize cache
$this->init();
// Check cache
if ($this->cacheAll($this->expire)) {
/** @internal Cache is valid: flush it! */
echo $this->doFlush($this->gzcont, $this->size,
$this->crc32);
exit;
}
else {
/** @internal if we came here, cache is invalid: go generate
* page and wait for 'finalize()' callback which will be
* called automagically
*/
// Check garbage collection
$this->doGC();
ob_start(array(&$this,'finalize'));
ob_implicit_flush(0);
}
}
/** finalize()
*
* This function is called by the callback-funtion of the ob_start
*
* @param string $contents the string representing the page to be flushed out
* to the client
*/
function finalize($contents)
{
$this->size = strlen($contents);
$this->crc32 = crc32($contents);
$this->pdebug('Callback happened');
if ($this->gzip===TRUE) {
$this->gzcont = gzcompress($contents, 9);
}
else {
$this->gzcont = $contents;
}
/**
* @internal cache these variables, as they are about original content
* which is lost after this
*/
$this->storeVar('size');
$this->storeVar('crc32');
// write the cache
$this->doWrite();
// Return flushed data
return $this->doFlush();
}
/** doFlush()
*
* Responsible for final flushing everything.
* Sets ETag-headers and returns "Not modified" when possible
*
* When ETag doesn't match (or is invalid), it is tried to send
* the gzipped data. If that is also not possible, we sadly have to
* uncompress (assuming $CACHE_GZIP is on)
*/
function doFlush()
{
$foundETag = '';
$ret = NULL;
/**
* @internal First check if we can send last-modified
*/
$myETag = '"AlberT-'.$this->crc32.$this->size.'"';
header('ETag: '.$myETag);
if (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) {
$foundETag = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
}
if (strstr($foundETag, $myETag)) {
/**
* @internal Browser has the page in its cache.
* We send only a "Not modified" header and exit!
*/
(php_sapi_name() == 'cgi') ? header('Status: 304') : header('HTTP/1.0 304');
}
else {
// Are we gzipping ?
if ($this->gzip===TRUE) {
$encod = $this->getEncoding();
if (FALSE!==$encod) {
// compressed output: set header
header('Content-Encoding: '.$encod);
$ret = "\x1f\x8b\x08\x00\x00\x00\x00\x00";
$ret .= substr($this->gzcont, 0,
strlen($this->gzcont) - 4);
$ret .= pack('V', $this->crc32);
$ret .= pack('V', $this->size);
}
else {
// We need to uncompress :(
$ret = gzuncompress($this->gzcont);
}
}
else {
$ret = $this->gzcont;
}
}
return $ret;
}
}
new AlberTcache;
?>