Xataface 2.0
Xataface Application Framework
Dataface/OutputCache.php
Go to the documentation of this file.
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations