Просмотр файла AlberT-cache/AlberT-cache.inc.php

Размер файла: 12.72Kb
<?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;
?>