![]() |
Xataface 2.0
Xataface Application Framework
|
00001 <?php 00006 class Dataface_OutputCache { 00007 00008 var $useGzipCompression = true; 00009 var $tableName = '__output_cache'; 00010 var $ignoredTables=array(); 00011 var $observedTables=array(); 00012 var $exemptActions=array(); 00013 var $stripKeys=array('-l','-lang'); 00014 var $app; 00015 var $threshold = 0.1; 00016 var $tableModificationTimes; 00017 var $lifeTime = 360000; 00018 var $randomize = 0; 00019 var $_cacheTableExists=null; 00020 var $lastModified=null; 00021 var $headers=array(); 00022 var $userId = null; 00023 00027 var $usedTables=array(); 00028 00029 function Dataface_OutputCache($params=array()){ 00030 if ( !extension_loaded('zlib') ){ 00031 $this->useGzipCompression = false; 00032 } else if ( isset($params['useGzipCompression']) ){ 00033 $this->useGzipCompression = $params['useGzipCompression']; 00034 } 00035 00036 if ( isset($params['threshold']) ) $this->threshold = $params['threshold']; 00037 if ( isset($params['lifeTime']) ) $this->lifeTime = $params['lifeTime']; 00038 if ( isset($params['tableName']) ) $this->tableName = $params['tableName']; 00039 if ( isset($params['ignoredTables']) ) $this->ignoredTables = explode(',', $params['ignoredTables']); 00040 if ( isset($params['observedTables']) ) $this->observedTables = explode(',', $params['observedTables']); 00041 if ( isset($params['exemptActions']) ) $this->exemptActions = explode(',', $params['exemptActions']); 00042 if ( isset($params['stripKeys']) ) $this->stripKeys = explode(',', $params['stripKeys']); 00043 $this->app =& Dataface_Application::getInstance(); 00044 00045 if ( !$this->_cacheTableExists() ) $this->_createCacheTable(); 00046 } 00047 00048 function getUserId(){ 00049 if ( !isset($this->userId) ){ 00050 $del = $this->app->getDelegate(); 00051 00052 if ( $del and method_exists($del, 'getOutputCacheUserId') ){ 00053 $this->userId = $del->getOutputCacheUserId(); 00054 } 00055 if ( !isset($this->userId) ){ 00056 00057 if ( class_exists('Dataface_AuthenticationTool') ){ 00058 $this->userId = Dataface_AuthenticationTool::getInstance()->getLoggedInUsername(); 00059 } 00060 00061 } 00062 if ( !isset($this->userId) ) $this->userId = ''; 00063 } 00064 return $this->userId; 00065 00066 } 00067 00072 function _buildPageSelect($params=array()){ 00073 import('Dataface/AuthenticationTool.php'); 00074 $query =& $this->app->getQuery(); 00075 00076 00077 $PageID = $this->getPageID($params); 00078 $Language = ( isset($params['lang']) ? $params['lang'] : $this->app->_conf['lang']); 00079 $auth =& Dataface_AuthenticationTool::getInstance(); 00080 //$UserID = ( isset($params['user']) ? $params['user'] : $auth->getLoggedInUsername()); 00081 $UserID = $this->getUserId(); 00082 $TimeStamp = ( isset($params['time']) ? $params['time'] : time()-$this->lifeTime); 00083 00084 00085 return " 00086 where `PageID` = '".addslashes($PageID)."' 00087 and `Language` = '".addslashes($Language)."' 00088 and `UserID` = '".addslashes($UserID)."' 00089 and `Expires` > NOW() 00090 ORDER BY RAND()"; 00091 } 00092 00106 function getPage($params=array()){ 00107 00108 $app =& Dataface_Application::getInstance(); 00109 $query =& $app->getQuery(); 00110 if ( in_array($query['-action'], $this->exemptActions) ) return null; 00111 00112 if ( $this->gzipSupported() and $this->useGzipCompression ){ 00113 $DataColumn = 'Data_gz'; 00114 } else { 00115 $DataColumn = 'Data'; 00116 } 00117 00118 $res = mysql_query("select `".addslashes($DataColumn)."`, UNIX_TIMESTAMP(`LastModified`) as `TimeStamp`, `Dependencies`, `Headers` from `".addslashes($this->tableName)."` 00119 ".$this->_buildPageSelect($params)." LIMIT 1", $this->app->db()); 00120 00121 if ( !$res ){ 00122 throw new Exception(mysql_error($this->app->db()), E_USER_ERROR); 00123 } 00124 00125 if ( mysql_num_rows($res) == 0 ) return null; 00126 //echo "here"; 00127 list($data, $lastModified, $dependencies, $headers) = mysql_fetch_row($res); 00128 $this->lastModified=$lastModified; 00129 $tables = explode(',',$dependencies); 00130 if ( $headers ) $this->headers = unserialize($headers); 00131 if ( count($tables) == 0 ) $tables = null; 00132 if ( $this->isModified($lastModified, $tables) ){ 00133 return null; 00134 } 00135 if ( is_resource($res) ) mysql_free_result($res); 00136 return $data; 00137 } 00138 00151 function numCurrentVersions($params=array()){ 00152 $res = mysql_query(" 00153 SELECT COUNT(*) FROM `".addslashes($this->tableName)."` ". 00154 $this->_buildPageSelect($params), $this->app->db()); 00155 if ( !$res ){ 00156 throw new Exception(mysql_error($this->app->db()), E_USER_ERROR); 00157 } 00158 list($num) = mysql_fetch_row($res); 00159 mysql_free_result($res); 00160 return $num; 00161 00162 } 00163 00190 function ob_start($params=array()){ 00191 00192 $app =& Dataface_Application::getInstance(); 00193 $query =& $app->getQuery(); 00194 if ( in_array($query['-action'], $this->exemptActions) ){ 00195 return true; 00196 } 00197 00198 if ( floatval($this->threshold) * floatval(100) > rand(0,100) ){ 00199 register_shutdown_function(array(&$this, 'cleanCache')); 00200 } 00201 00202 if ( isset($params['randomize']) and $params['randomize'] > 1 ){ 00203 $this->randomize = $params['randomize']; 00204 $numVersions = $this->numCurrentVersions($params); 00205 if ( $numVersions < $params['randomize'] ){ 00206 // We don't have enough versions yet to do a proper randomization. 00207 if ( floatval(100)*floatval($numVersions)/floatval($params['randomize']) > rand(0,100) ){ 00208 // We will use the cached version 00209 $useCache = true; 00210 } else { 00211 $useCache = false; 00212 } 00213 } else { 00214 $useCache = true; 00215 } 00216 } else { 00217 $useCache = true; 00218 } 00219 00220 if ( $useCache ){ 00221 //echo "Trying to use cached version"; 00222 $output = $this->getPage($params); 00223 } else { 00224 //echo "Not using cached version"; 00225 $output = null; 00226 } 00227 00228 if ( isset($output) ){ 00229 //echo "Using cached version"; 00230 //$last_modified_time = filemtime($file); 00231 $etag = md5($output); 00232 00233 //echo "Session enabled: ".$this->app->sessionEnabled();exit; 00234 if (!$this->app->sessionEnabled() and @$_SERVER['REQUEST_URI'] and strpos($_SERVER['REQUEST_URI'], '?') === false ){ 00235 session_cache_limiter('public'); 00236 $expires = 60*60*24; 00237 header('Cache-Control: public, max-age='.$expires.', s-maxage='.$expires); 00238 header('Connection: close'); 00239 header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->lastModified)." GMT"); 00240 header('Pragma: public'); 00241 header('Content-Length: '.strlen($output)); 00242 //header('Expires: '.gmdate('D, d M Y H:i:s', time()+$expires) . ' GMT'); 00243 00244 } else { 00245 header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->lastModified)." GMT"); 00246 header("Etag: $etag"); 00247 if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->lastModified || 00248 @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) { 00249 header("HTTP/1.1 304 Not Modified"); 00250 exit; 00251 } 00252 00253 } 00254 00255 00256 // Send the necessary headers 00257 if ( function_exists('headers_list')){ 00258 $hlist = headers_list(); 00259 $harr = array(); 00260 foreach ($hlist as $h){ 00261 if ( preg_match( '/^(?:Content-Type|Content-Language|Content-Location|Content-Disposition|P3P):/i', $h ) ) { 00262 list($hname,$hval) = array_map('trim',explode(':',$h)); 00263 $harr[$hname] = $hval; 00264 } 00265 } 00266 00267 foreach ( $this->headers as $h){ 00268 list($hname,$hval) = array_map('trim',explode(':',$h)); 00269 if ( !isset($harr[$hname]) ){ 00270 header($hname.': '.$hval); 00271 } 00272 } 00273 } 00274 00275 00276 if ( $this->gzipSupported() and $this->useGzipCompression ){ 00277 header("Content-Encoding: gzip"); 00278 echo $output; 00279 } else { 00280 echo $output; 00281 } 00282 exit; 00283 } 00284 00285 ob_start(array(&$this, 'ob_flush')); 00286 ob_implicit_flush(0); 00287 return true; 00288 } 00289 00290 function ob_flush($data){ 00291 if ( !$data ) return false; 00292 $params = array('randomize'=>$this->randomize, 'data'=>$data, 'tables'=>$this->app->tableNamesUsed); 00293 $res = $this->cachePage($params); 00294 00295 $etag = md5($data); 00296 if ( !$this->app->sessionEnabled() and @$_SERVER['REQUEST_URI'] and strpos($_SERVER['REQUEST_URI'], '?') === false ){ 00297 //echo "here";exit; 00298 $expires = 60*60*24; 00299 session_cache_limiter('public'); 00300 header('Cache-Control: public, max-age='.$expires.', s-maxage='.$expires); 00301 header('Connection: close'); 00302 header("Last-Modified: ".gmdate("D, d M Y H:i:s", time())." GMT"); 00303 header('Pragma: public'); 00304 header('Content-Length: '.strlen($data)); 00305 } else { 00306 header("Last-Modified: ".gmdate("D, d M Y H:i:s", time())." GMT"); 00307 header("Etag: $etag"); 00308 if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == time() || 00309 @trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) { 00310 header("HTTP/1.1 304 Not Modified"); 00311 exit; 00312 } 00313 } 00314 00315 00316 return $data; 00317 00318 } 00319 00320 function getPageID($params=array()){ 00321 $page_url = $_SERVER['REQUEST_URI']; 00322 foreach ($this->stripKeys as $key){ 00323 $page_url = preg_replace('/&?'.preg_quote($key, '/').'=[^&]*/','', $page_url); 00324 } 00325 //mail('steve@weblite.ca', 'Page URL', $page_url); 00326 $PageID = ( isset($params['id']) ? $params['id'] : md5($page_url)); 00327 return $PageID; 00328 } 00329 00351 function cachePage($params=array()){ 00352 $PageID = $this->getPageID($params); 00353 00354 if ( !isset($params['data']) ) throw new Exception('Missing parameter "data"', E_USER_ERROR); 00355 $Data = $params['data']; 00356 $Language = ( isset($params['lang']) ? $params['lang'] : $this->app->_conf['lang']); 00357 //if ( class_exists('Dataface_AuthenticationTool') ){$auth =& Dataface_AuthenticationTool::getInstance(); 00358 // $UserID = ( isset($params['user']) ? $params['user'] : $auth->getLoggedInUsername()); 00359 //} else { 00360 // $UserID = null; 00361 //} 00362 $UserID = $this->getUserId(); 00363 00364 $Expires = (isset($params['expires']) ? $params['expires'] : time() + $this->lifeTime); 00365 $tables = (isset($params['tables']) ? $params['tables'] : ''); 00366 $Dependencies = (is_array($tables) ? implode(',',$tables) : $tables); 00367 00368 if ( $this->useGzipCompression && extension_loaded('zlib') ){ 00369 // If we are using GZIP compression then we will use zlib library 00370 // functions (gzcompress) to compress the data also for storage 00371 // in the database. 00372 // Apparently we have to play with the headers and footers of the 00373 // gzip file for it to work properly with the web browsers. 00374 // see http://ca.php.net/gzcompress user comments. 00375 00376 $size = strlen($Data); 00377 $crc = crc32($Data); 00378 /* 00379 $Data_gz = "\x1f\x8b\x08\x00\x00\x00\x00\x00". 00380 substr(gzcompress($Data,9),0, $size-4). 00381 $this->_gzipGetFourChars($crc). 00382 $this->_gzipGetFourChars($size); 00383 */ 00384 /* Fix for IE compatibility .. seems to work for mozilla too. */ 00385 $Data_gz = "\x1f\x8b\x08\x00\x00\x00\x00\x00". 00386 substr(gzcompress($Data,9),0, $size); 00387 00388 } 00389 00390 00391 if ( isset($params['randomize']) and $params['randomize'] ){ 00392 // We are keeping multiple versions of this page so that we can 00393 // show them on a random rotation. This is to simulate dynamicism 00394 // while still caching pages. 00395 00396 // Basically the following query will delete existing cached versions 00397 // of this page except for the most recent X versions - where X 00398 // is the number specified in the $randomize parameter. The 00399 // $randomize parameter is the number of versions of this page 00400 // that should be used on random rotation. 00401 $res = mysql_query(" 00402 DELETE FROM `".addslashes($this->tableName)."` 00403 WHERE 00404 `PageID`='".addslashes($PageID)."' AND 00405 `Language`='".addslashes($Language)."' AND 00406 `UserID`='".addslashes($UserID)."' AND 00407 `GenID` NOT IN ( 00408 SELECT `GenID` FROM `".addslashes($this->tableName)."` 00409 WHERE 00410 `PageID`='".addslashes($PageID)."' AND 00411 `Language`='".addslashes($Language)."' AND 00412 `UserID`='".addslashes($UserID)."' 00413 ORDER BY 00414 `LastModified` desc 00415 LIMIT ".(intval($params['randomize']) - 1)." 00416 )", $this->app->db() ); 00417 00418 if ( !$res ){ 00419 throw new Exception(mysql_error($this->app->db()), E_USER_ERROR); 00420 } 00421 } else { 00422 // We are not randomizing. We delete any existing pages. 00423 /* 00424 $res = mysql_query(" 00425 DELETE low_priority FROM `".addslashes($this->tableName)."` 00426 WHERE 00427 `PageID`='".addslashes($PageID)."' AND 00428 `Language`='".addslashes($Language)."' AND 00429 `UserID`='".addslashes($UserID)."'", $this->app->db()); 00430 if ( !$res ){ 00431 throw new Exception(mysql_error($this->app->db()), E_USER_ERROR); 00432 } 00433 */ 00434 } 00435 00436 // Get the headers so we can reproduce them properly. 00437 if ( function_exists('headers_list') ){ 00438 //$headers = serialize(headers_list()); 00439 $headers = headers_list(); 00440 $hout = array(); 00441 foreach ( $headers as $h){ 00442 if ( preg_match( '/^(?:Content-Type|Content-Language|Content-Location|Content-Disposition|P3P):/i', $h ) ) { 00443 $hout[] = $h; 00444 } 00445 } 00446 $headers = $hout; 00447 } else { 00448 $headers = array(); 00449 } 00450 00451 00452 // Now we can insert the cached page. 00453 $sql = " 00454 replace delayed INTO `".addslashes($this->tableName)."` 00455 (`PageID`,`Language`,`UserID`,`Dependencies`,`Expires`,`Data`,`Data_gz`, `Headers`) 00456 VALUES 00457 ('".addslashes($PageID)."', 00458 '".addslashes($Language)."', 00459 '".addslashes($UserID)."', 00460 '".addslashes($Dependencies)."', 00461 FROM_UNIXTIME('".addslashes($Expires)."'), 00462 '".addslashes($Data)."', 00463 '".addslashes($Data_gz)."', 00464 '".addslashes(serialize($headers))."' 00465 )"; 00466 //file_put_contents('/tmp/dump.sql',$sql); 00467 $res = mysql_query($sql, $this->app->db()); 00468 00469 if ( !$res ){ 00470 throw new Exception(mysql_error($this->app->db()), E_USER_ERROR); 00471 } 00472 00473 if ( @$this->app->_conf['_output_cache']['cachedir'] ){ 00474 $filename = DATAFACE_SITE_PATH.'/'.$this->app->_conf['_output_cache']['cachedir']; 00475 $dir = $PageID{0}; 00476 $filename = $filename.'/'.$dir; 00477 if ( !file_exists($filename)){ 00478 mkdir($filename, 0777); 00479 00480 } 00481 00482 $filename .= '/'.$PageID.'-'.md5($Language.'-'.$UserID); 00483 if ( file_exists($filename) ){ 00484 @unlink($filename); 00485 } 00486 //echo "Opening $filename"; 00487 $fh = fopen($filename, 'w'); 00488 if ( $fh ){ 00489 fwrite($fh, $Data); 00490 fclose($fh); 00491 } 00492 00493 $fh = fopen($filename.'.gz', 'w'); 00494 if ( $fh ){ 00495 fwrite($fh, $Data_gz); 00496 fclose($fh); 00497 } 00498 00499 } 00500 00501 00502 00503 00504 00505 00506 } 00507 00512 function _gzipGetFourChars($Val){ 00513 $out = ''; 00514 for ($i = 0; $i < 4; $i ++) { 00515 $out .= chr($Val % 256); 00516 $Val = floor($Val / 256); 00517 } 00518 return $out; 00519 00520 } 00521 00522 00526 function _createCacheTable(){ 00527 $res = mysql_query("create table IF NOT EXISTS `".addslashes($this->tableName)."`( 00528 `GenID` INT(11) auto_increment, 00529 `PageID` VARCHAR(64), 00530 `Language` CHAR(2), 00531 `UserID` VARCHAR(32), 00532 `Dependencies` TEXT, 00533 `LastModified` TIMESTAMP, 00534 `Expires` DateTime, 00535 `Data` LONGTEXT, 00536 `Data_gz` LONGBLOB, 00537 `Headers` TEXT, 00538 PRIMARY KEY (`GenID`), 00539 INDEX `LookupIndex` (`Language`,`UserID`,`PageID`) 00540 )", $this->app->db()); 00541 if ( !$res ){ 00542 return PEAR::raiseError('Could not create cache table: '.mysql_error($this->app->db())); 00543 } 00544 00545 } 00546 00552 function _cacheTableExists(){ 00553 if ( isset($this->_cacheTableExists) ) return $this->_cacheTableExists; 00554 $res = mysql_query("SHOW TABLES LIKE '".addslashes($this->tableName)."'", $this->app->db()); 00555 if ( !$res ){ 00556 throw new Exception(mysql_error($this->app->db()), E_USER_ERROR); 00557 } 00558 return (mysql_num_rows($res) > 0); 00559 } 00560 00564 function cleanCache(){ 00565 $res = mysql_query("delete low_priority from `".addslashes($this->tableName)."` where `Expires` < NOW()", $this->app->db()); 00566 } 00567 00573 function gzipSupported(){ 00574 return stristr(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip'); 00575 } 00576 00582 function &getTableModificationTimes(){ 00583 $mod_times =& Dataface_Table::getTableModificationTimes(); 00584 $this->tableModificationTimes =& $mod_times; 00585 return $mod_times; 00586 } 00587 00599 function isModified($time, $tables=null){ 00600 $this->getTableModificationTimes(); 00601 if ( !isset($tables) ) $tables = array_keys($this->tableModificationTimes); 00602 if ( !is_array($tables) ){ 00603 $tables = explode(',', $tables); 00604 } 00605 $tables = array_merge($this->observedTables, $tables); 00606 foreach ($tables as $table ){ 00607 if ( isset( $this->ignoredTables[$table] ) ) continue; 00608 if ( !isset($this->tableModificationTimes[$table]) ) continue; 00609 if ( $this->tableModificationTimes[$table] > $time ) return true; 00610 } 00611 return false; 00612 } 00613 00614 }