<?php


/**
	Returns a caching class. The class must implement
	
	<pre>
	$fname = $o->generateID($id);
	$contents = $o->get($fname);
	$o->save($fname,$contents);
	</pre>
*/
function &PHPLens_Cache($secs = 60)
{
static $cache;
global $ADODB_CACHE_DIR;

	if (is_object($cache)) return $cache;
	
	$cache = new PhpLensCache($ADODB_CACHE_DIR,$secs);
	return $cache;
}

/*
function TestCache()
{
global $ADODB_CACHE_DIR;

	$c = new PHPLensCache($ADODB_CACHE_DIR,50);
	print_r($c);
	
	for ($i=0; $i < 10; $i++) {
		$data = $c->Get($i);
		if ($data === false) {
			$data = "TeSt$i";
			
			print "Cache miss for $i: ".$c->lastError."<br>";
			$c->Save($i,$data);
			
		}
		$d = $c->Get($i); 
		if ($d !== "TeSt$i") print "Error for $i, expected=TeSt$i data=$d<br>";
	}
}

TestCache();
*/

/**
	File cache for objects and variables.
	Requires some other process to clean up expired values.
	
	File cache directories created as combinations of: ca[0-F][0-F]
	Note: ADODB cache directories use: [0-F][0-F] 
	
	Usage:
	
	<pre>
	$c = new LensCache(LENS_CACHE_DIR,1000);
	$data = $c->Get($unique_id);
	if (! $data) {
		# generate $data
		$c->Save($unique_id,$data);
	}
	print $data;
	</pre>
*/
class PhpLensCache
{
	var $lastError = false;
	var $cacheSecs;
	var $path;				/// path to store cache data, eg /tmp
	var $dirPrefix = '/ca'; /// note - must have / prefix!
	var $enabled = true;
	var $debug = false;
	
	/**
		@param path			The cache directory (should be of the format '/tmp').
		@param cacheSecs	Number of secs to cache data.
	*/
	function PHPLensCache($path,$cacheSecs=3600)
	{
		if ($cacheSecs < 30) $cacheSecs = 30;
		$this->cacheSecs = $cacheSecs;
		$this->path = $path;
	}
	
	/**
		Get data given a unique $name that will be used as a hash to a real file name. 
		The file name is a hash is generated by LensCache.GetFileName. 
		A good id should be a combination of page name, page element, and session id.
	*/
	function Get($name)
	{
		if (!$this->enabled) return false;
		
		$path = $this->_GetFileName($name,false);
		
		$t = @filemtime($path);
		$expired = time() - $this->cacheSecs;
		if ($t < $expired) {
			if ($this->debug) print "Expired $path. Cache secs: $this->cacheSecs s. File : ".
				date('Y-m-d H:i:s',$t)." Expired on: ".date('Y-m-d H:i:s',$expired)."<br>";
			return false;
		}
		if ($t === false)  {
			
			$this->lastError = "File $path ($name): file/URL not found";
			if ($this->debug) print "Getting file $path ".$this->lastError;
			return false;
		}
		$fp = @fopen($path,'r');
		$this->err = false;
		if (!$fp) {
			$this->lastError = "$name: path file/URL not found";
			return false;
		}
		flock($fp, LOCK_SH);
		// slurp in the data
		$MAXSIZE = 128000;
		$data = fread($fp,$MAXSIZE);
		if (strlen($data) == $MAXSIZE) {
			$data .= fread($fp,filesize($path)-$MAXSIZE);
		}
		fclose($fp);
		$len = strlen($data);
		
		if (substr($data,$len-4,4) == '~LCA') return substr($data,0,$len-4);
		
		$this->lastError = 'Corrupt flag';
		return false;
	}
	
	/**
		Save the $contents given a unique $name that will be used as a hash to a real file name. 
		The file name is a hash is generated by LensCache.GetFileName. 
		A good id should be a combination of page name, page element, and session id.
	*/
	function Save($name,$contents,$debug=false)
	{
		$filename = $this->_GetFileName($name,true);
		if ($this->debug) {
			$debug = true;
			print "Saving file $filename ";
		}
			
	# http://www.php.net/bugs.php?id=9203 Bug that flock fails on Windows
	# So to simulate locking, we assume that rename is an atomic operation.
	# First we delete $filename, then we create a $tempfile write to it and 
	# rename to the desired $filename. If the rename works, then we successfully 
	# modified the file exclusively.
	# What a stupid need - having to simulate locking.
	# Risks:
	# 1. $tempfile name is not unique -- very very low
	# 2. unlink($filename) fails -- ok, rename will fail
	# 3. adodb reads stale file because unlink fails -- ok, $rs timeout occurs
	# 4. another process creates $filename between unlink() and rename() -- ok, rename() fails and  cache updated
		if (strncmp(PHP_OS,'WIN',3) === 0) {
			// skip the decimal place
			$mtime = substr(str_replace(' ','_',microtime()),2); 
			// unlink will let some latencies develop, so uniqid() is more random
			@unlink($filename);
			// getmypid() actually returns 0 on Win98 - never mind!
			$tmpname = $filename.uniqid($mtime).getmypid();
			if (!($fd = fopen($tmpname,'a'))) return false;
			$ok = ftruncate($fd,0);			
			if (!fwrite($fd,$contents)) $ok = false;
			else if (!fwrite($fd,'~LCA')) $ok = false;
			fclose($fd);
			chmod($tmpname,0644);
			if (!@rename($tmpname,$filename)) {
				unlink($tmpname);
				$ok = false;
			}
			if ($debug && !$ok) print " Rename $tmpname ".($ok? 'ok' : 'failed')." <br>";
			return $ok;
		}
		if (!($fd = fopen($filename, 'a'))) return false;
		if (flock($fd, LOCK_EX) && ftruncate($fd, 0)) {
			$ok = true;
			if (!fwrite($fd,$contents)) $ok = false;
			else if (!fwrite($fd,'~LCA')) $ok = false;
			fclose($fd);
			chmod($filename,0644);
		}else {
			fclose($fd);
			if ($debug)print " Failed acquiring lock for $filename<br>";
			$ok = false;
		}
	
	    return $ok;
	}

	/**
		Generate an md5 hash from $name, and then use it to generate a file name.
		
		@returns The full path to the file, based on the base directory ($this->path).
	*/
	function _GetFileName($name,$createdir=true)
	{
		
		$m = md5($name);
		$dir = $this->path.$this->dirPrefix.substr($m,0,2);
		if ($createdir)
			if(!file_exists($dir) && !mkdir($dir,0771)) {
				error_log("LensCache:: Unable to mkdir $dir for $name<br>");
			}
		return $dir.'/'.$m;
	}
}
?>
