Documentation is available at AlberT-cache.inc.php
1 <?php
2 /**
3 * AlberT-cache
4 * fast and portable full-page caching system
5 *
6 * @copyright Copyleft Emiliano Gabrielli
7 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
8 * @author Emiliano Gabrielli <AlberT@SuperAlberT.it>
9 * @version $Id: AlberT-cache.inc.php,v 1.7 2004/12/14 17:41:04 albert Exp $
10 * @package AlberT-cache
11 */
12
13 /**
14 * Configuration file
15 */
16 require_once(dirname(__FILE__).'/config.inc.php');
17
18 /**
19 * Automatically handles the entire caching stuffs
20 *
21 * It automagically handles everything concerning the caching mechanism:
22 * gzipping of the contents when the browser supports it, the browser
23 * cache validation, etc..
24 *
25 * @package AlberT-cache
26 */
27 class AlberTcache
28 {
29 var $dbg;
30 var $gzip;
31 var $post;
32 var $timeout;
33 var $expire;
34 var $also;
35 var $oneSite;
36 var $storDir;
37 var $gcProb;
38 var $isOn;
39 var $mask;
40
41 var $absfile;
42 var $data;
43 var $variables;
44 var $file;
45 var $gzcont;
46 var $size;
47 var $crc32;
48
49 /**
50 * Class constructor
51 */
52 function AlberTcache()
53 {
54 $this->dbg = $GLOBALS['CACHE_DEBUG'];
55 $this->gzip = $GLOBALS['CACHE_GZIP'];
56 $this->post = $GLOBALS['CACHE_POST'];
57 $this->timeout = $GLOBALS['CACHE_TIMEOUT'];
58 $this->expire = $GLOBALS['CACHE_EXP'];
59 $this->also = $GLOBALS['CACHE_ALSO'];
60 $this->oneSite = $GLOBALS['CACHE_1SITE'];
61 $this->storDir = $GLOBALS['CACHE_DIR'];
62 $this->gcProb = $GLOBALS['CACHE_GC'];
63 $this->isOn = $GLOBALS['CACHE_ON'];
64 $this->mask = $GLOBALS['CACHE_UMASK'];
65
66 /**
67 * We check if gzip functions are avaible
68 */
69 if (!function_exists('gzcompress')) {
70 $this->gzip = FALSE;
71 $this->pdebug('GZIP disabled, gzcompress() does not exists. '.
72 'May be we are on Win...');
73 }
74 $this->start();
75 return TRUE;
76 }
77
78 /**
79 * Resets the cache state
80 */
81 function doReset()
82 {
83 $this->absfile = NULL;
84 $this->data = array();
85 $this->variables = array();
86
87 return TRUE;
88 }
89
90 /**
91 * Saves a variable state between caching
92 *
93 * @param mixed $vn the name of the variable to save
94 */
95 function storeVar($vn)
96 {
97 $this->pdebug('Adding '.$vn.' to the variable store');
98 $this->variables[] = $vn;
99
100 return TRUE;
101 }
102
103 /**
104 * A simple deguggig handler function
105 *
106 * @param string $s The debugging message
107 */
108 function pdebug($s)
109 {
110 static $debugline = 0;
111
112 if ($this->dbg) {
113 header('X-Debug-'.++$debugline.': '.$s);
114
115 // we can't print any output without generating a warning !!!
116 //print_r("Line$debugline: $s<br>\n");
117 }
118
119 return TRUE;
120 }
121
122 /**
123 * Generates the key for the request
124 */
125 function getDefaultKey()
126 {
127 return md5('POST='.serialize($_POST).
128 ' GET='.serialize($_GET).
129 ' OTHER='.serialize($this->also));
130 }
131
132 /**
133 * Returns the default object used by the helper functions
134 */
135 function getDefaultObj()
136 {
137 if ($this->oneSite)
138 $name = $_SERVER['PHP_SELF'];
139 else
140 $name = $_SERVER['PATH_TRANSLATED'];
141
142 if ($name=='')
143 $name = 'http://'.$_SERVER['HTTP_HOST'].'/'.$_SERVER['PHP_SELF'];
144
145 return $name;
146 }
147
148 /**
149 * Caches the current page based on the page name and the GET/POST
150 * variables. All must match or else it will not be fetched
151 * from the cache!
152 */
153 function cacheAll($cachetime=60)
154 {
155 $this->file = $this->getDefaultObj();
156 return $this->theCache($cachetime, $this->getDefaultKey());
157 }
158
159 /**
160 * Obtains a lock on the cache storage
161 */
162 function lock_fs($fp, $mode='w')
163 {
164 switch ($mode) {
165 case 'w':
166 case 'W':
167 return flock($fp, LOCK_EX);
168 break;
169 case 'r':
170 case 'R':
171 return flock($fp, LOCK_SH);
172 break;
173 default:
174 die('FATAL: invalid lock mode: '.$mode.' in '.__FILE__.
175 ' for method lock_fs()');
176 }
177 }
178
179 /**
180 * Performs the unlock
181 */
182 function unlock_fs($fp)
183 {
184 return flock($fp, LOCK_UN);
185 }
186
187 /**
188 * Writes out the cache
189 */
190 function add_fs($file, $data)
191 {
192 $fp = @fopen($file, 'wb');
193 if (!$fp) {
194 $this->pdebug('Failed to open for write out to '.$file);
195 return FALSE;
196 }
197 $this->lock_fs($fp, 'w');
198 fwrite($fp, $data, strlen($data));
199 $this->unlock_fs($fp);
200 fclose($fp);
201 return TRUE;
202 }
203
204 /**
205 * Reads in the cache
206 */
207 function get_fs($file)
208 {
209 $fp = @fopen($file, 'rb');
210 if (!$fp) {
211 return NULL;
212 }
213 $this->lock_fs($fp, 'r');
214 $buff = fread($fp, filesize($file));
215 $this->unlock_fs($fp);
216 fclose($fp);
217 return $buff;
218 }
219
220 /**
221 * Returns the storage for cache
222 */
223 function getStorage($cacheobject)
224 {
225 return $this->storDir.'/'.$cacheobject;
226 }
227
228 /**
229 * Cache garbage collector
230 */
231 function doGC()
232 {
233 $de = '';
234
235 // Should we garbage collect ?
236 if ($this->gcProb>0) {
237 mt_srand(time());
238 $precision = 100000;
239 $r = (mt_rand()%$precision)/$precision;
240 if ($r>($this->gcProb/100)) {
241 return FALSE;
242 }
243 }
244 $this->pdebug('Running gc');
245 $dp = @opendir($this->storDir);
246 if (!$dp) {
247 $this->pdebug("Error opening '{$this->storDir}' for cleanup");
248 return FALSE;
249 }
250 // walking into the dir and remove expired files
251 while (FALSE !== ($de=readdir($dp)) ) {
252 if ( $de != '.' && $de != '..' ) {
253 // To get around strange php-strpos, add additional char
254 if (strpos(" $de", 'cache-')==1) {
255 $absfile = $this->storDir.'/'.$de;
256 $thecache = unserialize($this->get_fs($absfile));
257 if (is_array($thecache)) {
258 if ($thecache['cachetime']!='0' && $thecache['expire']<=time()) {
259 if (@unlink($absfile))
260 $this->pdebug('Deleted '.$absfile);
261 else
262 $this->pdebug('Failed to delete '.$absfile);
263 }
264 else
265 $this->pdebug($absfile.' expires in '.($thecache['expire']-time()));
266 }
267 else
268 $this->pdebug($absfile.' is empty, being processed in another process?');
269 }
270 }
271 }
272 @closedir($dp);
273 return TRUE;
274 }
275
276 /** theCache()
277 *
278 * Caches $object based on $key for $cachetime, will return 0 if the
279 * object has expired or does not exist.
280 */
281 function theCache($cachetime, $key=NULL)
282 {
283 if (!$this->isOn) {
284 $this->pdebug('Not caching, CACHE_ON is 0');
285 return 0;
286 }
287 $curtime = time();
288 // Make it a valid name
289 $this->file = eregi_replace('[^A-Z,0-9,=]', '_', $this->file);
290 $key = eregi_replace('[^A-Z,0-9,=]', '_', $key);
291 $this->pdebug('Caching based on OBJECT='.$this->file.' KEY='.$key);
292 $this->file = 'cache-'.$this->file.'-'.$key;
293 $this->absfile = $this->getStorage($this->file);
294 // Can we access the cache_file ?
295 if (($buff = $this->get_fs($this->absfile))) {
296 $this->pdebug('Opened the cache file');
297 $cdata = unserialize($buff);
298 //var_dump($cdata);
299 if (is_array($cdata)) {
300 $curco = $cdata['cache_object'];
301 if ($curco != $this->absfile) {
302 $this->pdebug('WTF?! That is not my cache file! got='.$curco.
303 ' wanted='.$this->absfile);
304 }
305 else {
306 if ($cdata['cachetime']=='0' || $cdata['expire']>=$curtime) {
307 // data not yet expired (or never expiring)
308 $expirein = $cdata['expire']-$curtime+1;
309 $this->pdebug('Cache expires in '.$expirein);
310
311 // restore variables
312 if (is_array($cdata['variables'])) {
313 foreach ($cdata['variables'] as $k=>$v) {
314 $this->pdebug('Restoring variable '.$k.' to value '.$v);
315 $GLOBALS[$k] = $v;
316 }
317 }
318 // restore gzcontent
319 $this->pdebug('Restoring gzipped content');
320 $this->gzcont = $cdata['gzcontent'];
321
322 $ret = $expirein;
323 if ($cdata['cachetime']=='0') {
324 $ret = 'INFINITE';
325 }
326 $this->doReset();
327 return $ret;
328 }
329 }
330 }
331 }
332 else {
333 // No cache file (yet) or unable to read
334 $this->pdebug('No previous cache of '.$this->absfile.' or unable to read');
335 }
336
337 // If we came here: start caching!
338 $umask = (function_exists('umask')) ? TRUE : FALSE;
339 // Create the file for this page
340 if ($umask === TRUE) {
341 $oldum = umask();
342 umask($this->mask);
343 }
344 if (function_exists('readlink') && @readlink($this->absfile)) {
345 $this->pdebug($this->absfile.' is a symlink! not caching!');
346 $this->absfile = NULL;
347 }
348 else {
349 $this->pdebug('Created '.$this->absfile.', waiting for callback');
350 $fp = @fopen($this->absfile, 'wb');
351 if (!$fp) {
352 $this->pdebug('Unable to open for write '.$this->absfile);
353 }
354 }
355 if ($umask === TRUE) {
356 umask($oldum);
357 }
358 // Set expire and cachetime
359 $this->data['expire'] = $curtime + $cachetime;
360 $this->data['cachetime'] = $cachetime;
361
362 return 0;
363 }
364
365 /** doWrite()
366 *
367 * Does the actual caching
368 */
369 function doWrite()
370 {
371 if (!$this->isOn) {
372 $this->pdebug('Not caching, CACHE_ON is off');
373 return 0;
374 }
375 if ($this->absfile!=NULL) {
376 $variables = array();
377 foreach ($this->variables as $vn) {
378 if (isset($GLOBALS[$vn])) {
379 $this->pdebug('Setting variable '.$vn.' to '.$GLOBALS[$vn]);
380 $variables[$vn] = $GLOBALS[$vn];
381 }
382 }
383 // Fill cache_data
384 $this->data['gzcontent'] = $this->gzcont;
385 $this->data['cache_object'] = $this->absfile;
386 $this->data['variables'] = $this->variables;
387 $datas = serialize($this->data);
388 // write data
389 $this->add_fs($this->absfile, $datas);
390 }
391 }
392
393 /** getEncoding()
394 *
395 * Are we capable of receiving gzipped data ?
396 * Returns the encoding that is accepted. Maybe additional check for Mac ?
397 */
398 function getEncoding()
399 {
400 if ( is_array($_SERVER) && array_key_exists('HTTP_ACCEPT_ENCODING', $_SERVER) ) {
401 if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== FALSE) {
402 return 'x-gzip';
403 }
404 if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
405 return 'gzip';
406 }
407 }
408 return FALSE;
409 }
410
411 /** init()
412 *
413 * Checks some global variables and might decide to disable caching
414 * and calls appropriate initialization-methods
415 */
416 function init()
417 {
418 // Override default CACHE_TIME ?
419 if (isset($this->timeout)) {
420 $this->expire = $this->timeout;
421 }
422 // Force cache off when POST occured when you don't want it cached
423 if (!$this->post && (count($_POST) > 0)) {
424 $this->isOn = 0;
425 $this->expire = -1;
426 }
427 // A cachetimeout of -1 disables writing, only ETag and content
428 // encoding if possible
429 if ($this->expire == -1) {
430 $this->isOn = 0;
431 $this->pdebug('$expire == -1 disabling cache: CACHE_ON is off');
432 }
433 // Reset cache
434 $this->doReset();
435 }
436
437 /** start()
438 *
439 * Sets the handler for callback
440 */
441 function start()
442 {
443 // Initialize cache
444 $this->init();
445
446 // Check cache
447 if ($this->cacheAll($this->expire)) {
448 /** @internal Cache is valid: flush it! */
449 echo $this->doFlush($this->gzcont, $this->size,
450 $this->crc32);
451 exit;
452 }
453 else {
454 /** @internal if we came here, cache is invalid: go generate
455 * page and wait for 'finalize()' callback which will be
456 * called automagically
457 */
458
459 // Check garbage collection
460 $this->doGC();
461
462 ob_start(array(&$this,'finalize'));
463 ob_implicit_flush(0);
464 }
465 }
466
467 /** finalize()
468 *
469 * This function is called by the callback-funtion of the ob_start
470 *
471 * @param string $contents the string representing the page to be flushed out
472 * to the client
473 */
474 function finalize($contents)
475 {
476 $this->size = strlen($contents);
477 $this->crc32 = crc32($contents);
478 $this->pdebug('Callback happened');
479 if ($this->gzip===TRUE) {
480 $this->gzcont = gzcompress($contents, 9);
481 }
482 else {
483 $this->gzcont = $contents;
484 }
485 /**
486 * @internal cache these variables, as they are about original content
487 * which is lost after this
488 */
489 $this->storeVar('size');
490 $this->storeVar('crc32');
491 // write the cache
492 $this->doWrite();
493
494 // Return flushed data
495 return $this->doFlush();
496 }
497
498 /** doFlush()
499 *
500 * Responsible for final flushing everything.
501 * Sets ETag-headers and returns "Not modified" when possible
502 *
503 * When ETag doesn't match (or is invalid), it is tried to send
504 * the gzipped data. If that is also not possible, we sadly have to
505 * uncompress (assuming $CACHE_GZIP is on)
506 */
507 function doFlush()
508 {
509 $foundETag = '';
510 $ret = NULL;
511
512 /**
513 * @internal First check if we can send last-modified
514 */
515 $myETag = '"AlberT-'.$this->crc32.$this->size.'"';
516 header('ETag: '.$myETag);
517 if (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) {
518 $foundETag = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
519 }
520 if (strstr($foundETag, $myETag)) {
521 /**
522 * @internal Browser has the page in its cache.
523 * We send only a "Not modified" header and exit!
524 */
525 (php_sapi_name() == 'cgi') ? header('Status: 304') : header('HTTP/1.0 304');
526 }
527 else {
528 // Are we gzipping ?
529 if ($this->gzip===TRUE) {
530 $encod = $this->getEncoding();
531 if (FALSE!==$encod) {
532 // compressed output: set header
533 header('Content-Encoding: '.$encod);
534 $ret = "\x1f\x8b\x08\x00\x00\x00\x00\x00";
535 $ret .= substr($this->gzcont, 0,
536 strlen($this->gzcont) - 4);
537 $ret .= pack('V', $this->crc32);
538 $ret .= pack('V', $this->size);
539 }
540 else {
541 // We need to uncompress :(
542 $ret = gzuncompress($this->gzcont);
543 }
544 }
545 else {
546 $ret = $this->gzcont;
547 }
548 }
549 return $ret;
550 }
551 }
552
553 new AlberTcache;
554 ?>