Xataface  2.0alpha2
Xataface Application Framework
 All Data Structures Namespaces Files Functions Variables Groups Pages
OutputCache.php
Go to the documentation of this file.
1 <?php
7 
8  var $useGzipCompression = true;
9  var $tableName = '__output_cache';
10  var $ignoredTables=array();
11  var $observedTables=array();
12  var $exemptActions=array();
13  var $stripKeys=array('-l','-lang');
14  var $app;
15  var $threshold = 0.1;
17  var $lifeTime = 360000;
18  var $randomize = 0;
20  var $lastModified=null;
21  var $headers=array();
22  var $userId = null;
23 
27  var $usedTables=array();
28 
29  function Dataface_OutputCache($params=array()){
30  if ( !extension_loaded('zlib') ){
31  $this->useGzipCompression = false;
32  } else if ( isset($params['useGzipCompression']) ){
33  $this->useGzipCompression = $params['useGzipCompression'];
34  }
35 
36  if ( isset($params['threshold']) ) $this->threshold = $params['threshold'];
37  if ( isset($params['lifeTime']) ) $this->lifeTime = $params['lifeTime'];
38  if ( isset($params['tableName']) ) $this->tableName = $params['tableName'];
39  if ( isset($params['ignoredTables']) ) $this->ignoredTables = explode(',', $params['ignoredTables']);
40  if ( isset($params['observedTables']) ) $this->observedTables = explode(',', $params['observedTables']);
41  if ( isset($params['exemptActions']) ) $this->exemptActions = explode(',', $params['exemptActions']);
42  if ( isset($params['stripKeys']) ) $this->stripKeys = explode(',', $params['stripKeys']);
43  $this->app =& Dataface_Application::getInstance();
44 
45  if ( !$this->_cacheTableExists() ) $this->_createCacheTable();
46  }
47 
48  function getUserId(){
49  if ( !isset($this->userId) ){
50  $del = $this->app->getDelegate();
51 
52  if ( $del and method_exists($del, 'getOutputCacheUserId') ){
53  $this->userId = $del->getOutputCacheUserId();
54  }
55  if ( !isset($this->userId) ){
56 
57  if ( class_exists('Dataface_AuthenticationTool') ){
58  $this->userId = Dataface_AuthenticationTool::getInstance()->getLoggedInUsername();
59  }
60 
61  }
62  if ( !isset($this->userId) ) $this->userId = '';
63  }
64  return $this->userId;
65 
66  }
67 
72  function _buildPageSelect($params=array()){
73  import('Dataface/AuthenticationTool.php');
74  $query =& $this->app->getQuery();
75 
76 
77  $PageID = $this->getPageID($params);
78  $Language = ( isset($params['lang']) ? $params['lang'] : $this->app->_conf['lang']);
80  //$UserID = ( isset($params['user']) ? $params['user'] : $auth->getLoggedInUsername());
81  $UserID = $this->getUserId();
82  $TimeStamp = ( isset($params['time']) ? $params['time'] : time()-$this->lifeTime);
83 
84 
85  return "
86  where `PageID` = '".addslashes($PageID)."'
87  and `Language` = '".addslashes($Language)."'
88  and `UserID` = '".addslashes($UserID)."'
89  and `Expires` > NOW()
90  ORDER BY RAND()";
91  }
92 
106  function getPage($params=array()){
107 
109  $query =& $app->getQuery();
110  if ( in_array($query['-action'], $this->exemptActions) ) return null;
111 
112  if ( $this->gzipSupported() and $this->useGzipCompression ){
113  $DataColumn = 'Data_gz';
114  } else {
115  $DataColumn = 'Data';
116  }
117 
118  $res = mysql_query("select `".addslashes($DataColumn)."`, UNIX_TIMESTAMP(`LastModified`) as `TimeStamp`, `Dependencies`, `Headers` from `".addslashes($this->tableName)."`
119  ".$this->_buildPageSelect($params)." LIMIT 1", $this->app->db());
120 
121  if ( !$res ){
122  throw new Exception(mysql_error($this->app->db()), E_USER_ERROR);
123  }
124 
125  if ( mysql_num_rows($res) == 0 ) return null;
126  //echo "here";
127  list($data, $lastModified, $dependencies, $headers) = mysql_fetch_row($res);
128  $this->lastModified=$lastModified;
129  $tables = explode(',',$dependencies);
130  if ( $headers ) $this->headers = unserialize($headers);
131  if ( count($tables) == 0 ) $tables = null;
132  if ( $this->isModified($lastModified, $tables) ){
133  return null;
134  }
135  if ( is_resource($res) ) mysql_free_result($res);
136  return $data;
137  }
138 
151  function numCurrentVersions($params=array()){
152  $res = mysql_query("
153  SELECT COUNT(*) FROM `".addslashes($this->tableName)."` ".
154  $this->_buildPageSelect($params), $this->app->db());
155  if ( !$res ){
156  throw new Exception(mysql_error($this->app->db()), E_USER_ERROR);
157  }
158  list($num) = mysql_fetch_row($res);
159  mysql_free_result($res);
160  return $num;
161 
162  }
163 
190  function ob_start($params=array()){
191 
193  $query =& $app->getQuery();
194  if ( in_array($query['-action'], $this->exemptActions) ){
195  return true;
196  }
197 
198  if ( floatval($this->threshold) * floatval(100) > rand(0,100) ){
199  register_shutdown_function(array(&$this, 'cleanCache'));
200  }
201 
202  if ( isset($params['randomize']) and $params['randomize'] > 1 ){
203  $this->randomize = $params['randomize'];
204  $numVersions = $this->numCurrentVersions($params);
205  if ( $numVersions < $params['randomize'] ){
206  // We don't have enough versions yet to do a proper randomization.
207  if ( floatval(100)*floatval($numVersions)/floatval($params['randomize']) > rand(0,100) ){
208  // We will use the cached version
209  $useCache = true;
210  } else {
211  $useCache = false;
212  }
213  } else {
214  $useCache = true;
215  }
216  } else {
217  $useCache = true;
218  }
219 
220  if ( $useCache ){
221  //echo "Trying to use cached version";
222  $output = $this->getPage($params);
223  } else {
224  //echo "Not using cached version";
225  $output = null;
226  }
227 
228  if ( isset($output) ){
229  //echo "Using cached version";
230  //$last_modified_time = filemtime($file);
231  $etag = md5($output);
232 
233  //echo "Session enabled: ".$this->app->sessionEnabled();exit;
234  if (!$this->app->sessionEnabled() and @$_SERVER['REQUEST_URI'] and strpos($_SERVER['REQUEST_URI'], '?') === false ){
235  session_cache_limiter('public');
236  $expires = 60*60*24;
237  header('Cache-Control: public, max-age='.$expires.', s-maxage='.$expires);
238  header('Connection: close');
239  header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->lastModified)." GMT");
240  header('Pragma: public');
241  header('Content-Length: '.strlen($output));
242  //header('Expires: '.gmdate('D, d M Y H:i:s', time()+$expires) . ' GMT');
243 
244  } else {
245  header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->lastModified)." GMT");
246  header("Etag: $etag");
247  if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->lastModified ||
248  @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
249  header("HTTP/1.1 304 Not Modified");
250  exit;
251  }
252 
253  }
254 
255 
256  // Send the necessary headers
257  if ( function_exists('headers_list')){
258  $hlist = headers_list();
259  $harr = array();
260  foreach ($hlist as $h){
261  if ( preg_match( '/^(?:Content-Type|Content-Language|Content-Location|Content-Disposition|P3P):/i', $h ) ) {
262  list($hname,$hval) = array_map('trim',explode(':',$h));
263  $harr[$hname] = $hval;
264  }
265  }
266 
267  foreach ( $this->headers as $h){
268  list($hname,$hval) = array_map('trim',explode(':',$h));
269  if ( !isset($harr[$hname]) ){
270  header($hname.': '.$hval);
271  }
272  }
273  }
274 
275 
276  if ( $this->gzipSupported() and $this->useGzipCompression ){
277  header("Content-Encoding: gzip");
278  echo $output;
279  } else {
280  echo $output;
281  }
282  exit;
283  }
284 
285  ob_start(array(&$this, 'ob_flush'));
286  ob_implicit_flush(0);
287  return true;
288  }
289 
290  function ob_flush($data){
291  if ( !$data ) return false;
292  $params = array('randomize'=>$this->randomize, 'data'=>$data, 'tables'=>$this->app->tableNamesUsed);
293  $res = $this->cachePage($params);
294 
295  $etag = md5($data);
296  if ( !$this->app->sessionEnabled() and @$_SERVER['REQUEST_URI'] and strpos($_SERVER['REQUEST_URI'], '?') === false ){
297  //echo "here";exit;
298  $expires = 60*60*24;
299  session_cache_limiter('public');
300  header('Cache-Control: public, max-age='.$expires.', s-maxage='.$expires);
301  header('Connection: close');
302  header("Last-Modified: ".gmdate("D, d M Y H:i:s", time())." GMT");
303  header('Pragma: public');
304  header('Content-Length: '.strlen($data));
305  } else {
306  header("Last-Modified: ".gmdate("D, d M Y H:i:s", time())." GMT");
307  header("Etag: $etag");
308  if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == time() ||
309  @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
310  header("HTTP/1.1 304 Not Modified");
311  exit;
312  }
313  }
314 
315 
316  return $data;
317 
318  }
319 
320  function getPageID($params=array()){
321  $page_url = $_SERVER['REQUEST_URI'];
322  foreach ($this->stripKeys as $key){
323  $page_url = preg_replace('/&?'.preg_quote($key, '/').'=[^&]*/','', $page_url);
324  }
325  //mail('steve@weblite.ca', 'Page URL', $page_url);
326  $PageID = ( isset($params['id']) ? $params['id'] : md5($page_url));
327  return $PageID;
328  }
329 
351  function cachePage($params=array()){
352  $PageID = $this->getPageID($params);
353 
354  if ( !isset($params['data']) ) throw new Exception('Missing parameter "data"', E_USER_ERROR);
355  $Data = $params['data'];
356  $Language = ( isset($params['lang']) ? $params['lang'] : $this->app->_conf['lang']);
357  //if ( class_exists('Dataface_AuthenticationTool') ){$auth =& Dataface_AuthenticationTool::getInstance();
358  // $UserID = ( isset($params['user']) ? $params['user'] : $auth->getLoggedInUsername());
359  //} else {
360  // $UserID = null;
361  //}
362  $UserID = $this->getUserId();
363 
364  $Expires = (isset($params['expires']) ? $params['expires'] : time() + $this->lifeTime);
365  $tables = (isset($params['tables']) ? $params['tables'] : '');
366  $Dependencies = (is_array($tables) ? implode(',',$tables) : $tables);
367 
368  if ( $this->useGzipCompression && extension_loaded('zlib') ){
369  // If we are using GZIP compression then we will use zlib library
370  // functions (gzcompress) to compress the data also for storage
371  // in the database.
372  // Apparently we have to play with the headers and footers of the
373  // gzip file for it to work properly with the web browsers.
374  // see http://ca.php.net/gzcompress user comments.
375 
376  $size = strlen($Data);
377  $crc = crc32($Data);
378  /*
379  $Data_gz = "\x1f\x8b\x08\x00\x00\x00\x00\x00".
380  substr(gzcompress($Data,9),0, $size-4).
381  $this->_gzipGetFourChars($crc).
382  $this->_gzipGetFourChars($size);
383  */
384  /* Fix for IE compatibility .. seems to work for mozilla too. */
385  $Data_gz = "\x1f\x8b\x08\x00\x00\x00\x00\x00".
386  substr(gzcompress($Data,9),0, $size);
387 
388  }
389 
390 
391  if ( isset($params['randomize']) and $params['randomize'] ){
392  // We are keeping multiple versions of this page so that we can
393  // show them on a random rotation. This is to simulate dynamicism
394  // while still caching pages.
395 
396  // Basically the following query will delete existing cached versions
397  // of this page except for the most recent X versions - where X
398  // is the number specified in the $randomize parameter. The
399  // $randomize parameter is the number of versions of this page
400  // that should be used on random rotation.
401  $res = mysql_query("
402  DELETE FROM `".addslashes($this->tableName)."`
403  WHERE
404  `PageID`='".addslashes($PageID)."' AND
405  `Language`='".addslashes($Language)."' AND
406  `UserID`='".addslashes($UserID)."' AND
407  `GenID` NOT IN (
408  SELECT `GenID` FROM `".addslashes($this->tableName)."`
409  WHERE
410  `PageID`='".addslashes($PageID)."' AND
411  `Language`='".addslashes($Language)."' AND
412  `UserID`='".addslashes($UserID)."'
413  ORDER BY
414  `LastModified` desc
415  LIMIT ".(intval($params['randomize']) - 1)."
416  )", $this->app->db() );
417 
418  if ( !$res ){
419  throw new Exception(mysql_error($this->app->db()), E_USER_ERROR);
420  }
421  } else {
422  // We are not randomizing. We delete any existing pages.
423  /*
424  $res = mysql_query("
425  DELETE low_priority FROM `".addslashes($this->tableName)."`
426  WHERE
427  `PageID`='".addslashes($PageID)."' AND
428  `Language`='".addslashes($Language)."' AND
429  `UserID`='".addslashes($UserID)."'", $this->app->db());
430  if ( !$res ){
431  throw new Exception(mysql_error($this->app->db()), E_USER_ERROR);
432  }
433  */
434  }
435 
436  // Get the headers so we can reproduce them properly.
437  if ( function_exists('headers_list') ){
438  //$headers = serialize(headers_list());
439  $headers = headers_list();
440  $hout = array();
441  foreach ( $headers as $h){
442  if ( preg_match( '/^(?:Content-Type|Content-Language|Content-Location|Content-Disposition|P3P):/i', $h ) ) {
443  $hout[] = $h;
444  }
445  }
446  $headers = $hout;
447  } else {
448  $headers = array();
449  }
450 
451 
452  // Now we can insert the cached page.
453  $sql = "
454  replace delayed INTO `".addslashes($this->tableName)."`
455  (`PageID`,`Language`,`UserID`,`Dependencies`,`Expires`,`Data`,`Data_gz`, `Headers`)
456  VALUES
457  ('".addslashes($PageID)."',
458  '".addslashes($Language)."',
459  '".addslashes($UserID)."',
460  '".addslashes($Dependencies)."',
461  FROM_UNIXTIME('".addslashes($Expires)."'),
462  '".addslashes($Data)."',
463  '".addslashes($Data_gz)."',
464  '".addslashes(serialize($headers))."'
465  )";
466  //file_put_contents('/tmp/dump.sql',$sql);
467  $res = mysql_query($sql, $this->app->db());
468 
469  if ( !$res ){
470  throw new Exception(mysql_error($this->app->db()), E_USER_ERROR);
471  }
472 
473  if ( @$this->app->_conf['_output_cache']['cachedir'] ){
474  $filename = DATAFACE_SITE_PATH.'/'.$this->app->_conf['_output_cache']['cachedir'];
475  $dir = $PageID{0};
476  $filename = $filename.'/'.$dir;
477  if ( !file_exists($filename)){
478  mkdir($filename, 0777);
479 
480  }
481 
482  $filename .= '/'.$PageID.'-'.md5($Language.'-'.$UserID);
483  if ( file_exists($filename) ){
484  @unlink($filename);
485  }
486  //echo "Opening $filename";
487  $fh = fopen($filename, 'w');
488  if ( $fh ){
489  fwrite($fh, $Data);
490  fclose($fh);
491  }
492 
493  $fh = fopen($filename.'.gz', 'w');
494  if ( $fh ){
495  fwrite($fh, $Data_gz);
496  fclose($fh);
497  }
498 
499  }
500 
501 
502 
503 
504 
505 
506  }
507 
512  function _gzipGetFourChars($Val){
513  $out = '';
514  for ($i = 0; $i < 4; $i ++) {
515  $out .= chr($Val % 256);
516  $Val = floor($Val / 256);
517  }
518  return $out;
519 
520  }
521 
522 
526  function _createCacheTable(){
527  $res = mysql_query("create table IF NOT EXISTS `".addslashes($this->tableName)."`(
528  `GenID` INT(11) auto_increment,
529  `PageID` VARCHAR(64),
530  `Language` CHAR(2),
531  `UserID` VARCHAR(32),
532  `Dependencies` TEXT,
533  `LastModified` TIMESTAMP,
534  `Expires` DateTime,
535  `Data` LONGTEXT,
536  `Data_gz` LONGBLOB,
537  `Headers` TEXT,
538  PRIMARY KEY (`GenID`),
539  INDEX `LookupIndex` (`Language`,`UserID`,`PageID`)
540  )", $this->app->db());
541  if ( !$res ){
542  return PEAR::raiseError('Could not create cache table: '.mysql_error($this->app->db()));
543  }
544 
545  }
546 
552  function _cacheTableExists(){
553  if ( isset($this->_cacheTableExists) ) return $this->_cacheTableExists;
554  $res = mysql_query("SHOW TABLES LIKE '".addslashes($this->tableName)."'", $this->app->db());
555  if ( !$res ){
556  throw new Exception(mysql_error($this->app->db()), E_USER_ERROR);
557  }
558  return (mysql_num_rows($res) > 0);
559  }
560 
564  function cleanCache(){
565  $res = mysql_query("delete low_priority from `".addslashes($this->tableName)."` where `Expires` < NOW()", $this->app->db());
566  }
567 
573  function gzipSupported(){
574  return stristr(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip');
575  }
576 
584  $this->tableModificationTimes =& $mod_times;
585  return $mod_times;
586  }
587 
599  function isModified($time, $tables=null){
600  $this->getTableModificationTimes();
601  if ( !isset($tables) ) $tables = array_keys($this->tableModificationTimes);
602  if ( !is_array($tables) ){
603  $tables = explode(',', $tables);
604  }
605  $tables = array_merge($this->observedTables, $tables);
606  foreach ($tables as $table ){
607  if ( isset( $this->ignoredTables[$table] ) ) continue;
608  if ( !isset($this->tableModificationTimes[$table]) ) continue;
609  if ( $this->tableModificationTimes[$table] > $time ) return true;
610  }
611  return false;
612  }
613 
614 }