[264] | 1 | <?php |
---|
| 2 | /** |
---|
| 3 | * Main caching and output system. |
---|
| 4 | * Modes: |
---|
| 5 | * SKIP = Skips any caching. By enabling this you just disable |
---|
| 6 | * the caching system which can be great for testing. |
---|
| 7 | * use_cache() will always return false and write_cache() |
---|
| 8 | * will just skip any work. |
---|
| 9 | * |
---|
| 10 | * PURGE = Enforce a rewrite of the cache. use_cache() will always |
---|
| 11 | * return false and therefore the cache file will be rewritten |
---|
| 12 | * as if it didn't exist. |
---|
| 13 | * |
---|
| 14 | * DEBUG = Debug cache validation calculation and output calculation. |
---|
| 15 | * This will print verbose data just whenever called. The |
---|
| 16 | * cache system can be run around that, but "NOT CHANGED" calls |
---|
| 17 | * will just abort the scenery. Somewhat weird. |
---|
| 18 | * |
---|
| 19 | * VERBOSE = Print verbose Messages as HTML Comments (print_info) |
---|
| 20 | * or error messages in div spans. This is useful for any |
---|
| 21 | * HTML output but should be disabled for JS/CSS caching. |
---|
| 22 | **/ |
---|
| 23 | |
---|
| 24 | # Lightweight caching system |
---|
| 25 | class t29Cache { |
---|
| 26 | const webroot_cache_dir = '/shared/cache'; # relative to webroot |
---|
| 27 | |
---|
| 28 | public $skip = false; |
---|
| 29 | public $purge = false; |
---|
| 30 | public $debug = false;// debug calculation |
---|
| 31 | public $verbose = false; // print html comments and errors |
---|
| 32 | |
---|
[265] | 33 | // these must be set after constructing! |
---|
| 34 | public $cache_file; // must be set! |
---|
| 35 | public $test_files = array(); // must be set! |
---|
[516] | 36 | public $test_conditions = array(); // can be filled with booleans |
---|
[264] | 37 | |
---|
| 38 | private $mtime_cache_file = null; // needed for cache header output |
---|
| 39 | private $is_valid = null; // cache output value |
---|
| 40 | |
---|
| 41 | function __construct($debug=false, $verbose=false) { |
---|
| 42 | // default values |
---|
| 43 | $this->skip = isset($_GET['skip_cache']); |
---|
| 44 | $this->purge = isset($_GET['purge_cache']); |
---|
| 45 | $this->debug = isset($_GET['debug_cache']) || $debug; |
---|
[275] | 46 | $this->verbose = isset($_GET['verbose_cache']) || $verbose || $this->debug; |
---|
[264] | 47 | } |
---|
| 48 | |
---|
| 49 | /** |
---|
| 50 | * expecting: |
---|
| 51 | * @param $webroot /var/www/foo/bar (no trailing slash!) |
---|
| 52 | * @param $filename /de/bar/baz.htm (starting with slash!) |
---|
| 53 | * @returns absolute filename /var/www/foo/bar/cache/dir/de/bar/baz.htm |
---|
| 54 | **/ |
---|
| 55 | function set_cache_file($webroot, $filename) { |
---|
| 56 | $this->cache_file = $webroot . self::webroot_cache_dir . '/' . $filename; |
---|
| 57 | } |
---|
| 58 | |
---|
| 59 | # helper function |
---|
| 60 | public static function mkdir_recursive($pathname) { |
---|
| 61 | is_dir(dirname($pathname)) || self::mkdir_recursive(dirname($pathname)); |
---|
| 62 | return is_dir($pathname) || @mkdir($pathname); |
---|
| 63 | } |
---|
| 64 | |
---|
[265] | 65 | /** |
---|
| 66 | * Calculates and compares the mtimes of the cache file and testing files. |
---|
| 67 | * Doesn't obey any debug/skip/purge rules, just gives out if the cache file |
---|
| 68 | * is valid or not. |
---|
| 69 | * The result is cached in $is_valid, so you can call this (heavy to calc) |
---|
| 70 | * method frequently. |
---|
| 71 | **/ |
---|
[264] | 72 | function is_valid() { |
---|
| 73 | // no double calculation |
---|
[277] | 74 | if($this->is_valid !== null) return $this->is_valid; |
---|
[264] | 75 | |
---|
| 76 | if($this->debug) { |
---|
| 77 | print '<pre>'; |
---|
| 78 | print 't29Cache: Validity Checking.'.PHP_EOL; |
---|
| 79 | print 'Cache file: '; var_dump($this->cache_file); |
---|
| 80 | print 'Test files: '; var_dump($this->test_files); |
---|
[516] | 81 | print 'Test conditions: '; var_dump($this->test_conditions); |
---|
[264] | 82 | } |
---|
| 83 | |
---|
| 84 | $this->mtime_cache_file = @filemtime($this->cache_file); |
---|
| 85 | $mtime_test_files = array_map( |
---|
| 86 | function($x){return @filemtime($x);}, |
---|
| 87 | $this->test_files); |
---|
| 88 | $mtime_test_max = array_reduce($mtime_test_files, 'max'); |
---|
[516] | 89 | // new feature: Testing boolean conditions. If $this->test_conditions is |
---|
| 90 | // an empty array, the calculation gives true. |
---|
| 91 | $test_conditions = array_reduce($this->test_conditions, function($a,$b){ return $a && $b; }, true); |
---|
[264] | 92 | $this->is_valid = $this->mtime_cache_file |
---|
[516] | 93 | && $mtime_test_max < $this->mtime_cache_file && $test_conditions; |
---|
[264] | 94 | |
---|
| 95 | if($this->debug) { |
---|
| 96 | print 'Cache mtime: '; var_dump($this->mtime_cache_file); |
---|
| 97 | print 'Test files mtimes: '; var_dump($mtime_test_files); |
---|
| 98 | print 'CACHE IS VALID: '; var_dump($this->is_valid); |
---|
| 99 | } |
---|
| 100 | |
---|
| 101 | return $this->is_valid; |
---|
| 102 | } |
---|
| 103 | |
---|
[265] | 104 | /** |
---|
| 105 | * The "front end" to is_valid: Takes skipping and purging rules into |
---|
| 106 | * account to decide whether you shall use the cache or not. |
---|
| 107 | * @returns boolean value if cache is supposed to be valid or not. |
---|
| 108 | **/ |
---|
[264] | 109 | function shall_use() { |
---|
| 110 | $test = $this->is_valid() && !$this->skip && !$this->purge; |
---|
| 111 | if($this->debug) { |
---|
| 112 | print 'Shall use Cache: '; var_dump($test); |
---|
| 113 | } |
---|
| 114 | return $test; |
---|
| 115 | } |
---|
| 116 | |
---|
[265] | 117 | /** |
---|
| 118 | * Prints out cache file with according HTTP headers and HTTP caching |
---|
| 119 | * (HTTP_IF_MODIFIED_SINCE). You must not print out anything after such a http |
---|
| 120 | * header! Therefore consider using the convenience method print_cache_and_exit() |
---|
| 121 | * instead of this one or exit on yourself. |
---|
[273] | 122 | * |
---|
| 123 | * @param $ignore_http_caching Don't check the clients HTTP cache |
---|
[265] | 124 | **/ |
---|
[273] | 125 | function print_cache($ignore_http_caching=false) { |
---|
[264] | 126 | // make sure we already have called is_valid |
---|
| 127 | if($this->mtime_cache_file === null) |
---|
| 128 | $this->is_valid(); // calculate mtime |
---|
| 129 | |
---|
| 130 | if(!$this->debug) { |
---|
| 131 | header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->mtime_cache_file)." GMT"); |
---|
| 132 | //header("Etag: $etag"); |
---|
| 133 | } |
---|
| 134 | |
---|
[273] | 135 | if(!$ignore_http_caching && @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->mtime_cache_file) { |
---|
[264] | 136 | // client already has page cached locally |
---|
| 137 | if($this->debug) { |
---|
| 138 | print 'Would send Client a NOT MODIFIED answer.' . PHP_EOL; |
---|
| 139 | } else { |
---|
| 140 | header("HTTP/1.1 304 Not Modified"); |
---|
| 141 | // important - no more output! |
---|
| 142 | } |
---|
| 143 | } else { |
---|
| 144 | if($this->debug) { |
---|
| 145 | print 'Would send Client output of ' . $this->cache_file . PHP_EOL; |
---|
| 146 | } else { |
---|
| 147 | readfile($this->cache_file); |
---|
| 148 | } |
---|
| 149 | } |
---|
| 150 | } |
---|
| 151 | |
---|
[265] | 152 | /** |
---|
| 153 | * Convenience method which will exit the program after calling print_cache(). |
---|
| 154 | **/ |
---|
[264] | 155 | function print_cache_and_exit() { |
---|
| 156 | $this->print_cache(); |
---|
| 157 | exit; |
---|
| 158 | } |
---|
| 159 | |
---|
[265] | 160 | /** |
---|
| 161 | * Convenience method for calling the typical workflow: Test if the cache file |
---|
| 162 | * shall be used, and if yes, print it out and exit the program. If this method |
---|
| 163 | * returns, you can be sure that you have to create a (new) cache file. So your |
---|
| 164 | * typical code will look like: |
---|
| 165 | * |
---|
| 166 | * $cache = new t29Cache(); |
---|
| 167 | * // initialization stuff $cache->... = ... |
---|
| 168 | * $cache->try_cache_and_exit(); |
---|
| 169 | * // so we are still alive - go making content! |
---|
| 170 | * $cache->start_cache(...); |
---|
| 171 | * echo "be happy"; |
---|
| 172 | * $cache->write_cache(); // at least if you didn't use any registered shutdown function. |
---|
| 173 | * |
---|
| 174 | **/ |
---|
[264] | 175 | function try_cache_and_exit() { |
---|
| 176 | if($this->shall_use()) |
---|
| 177 | $this->print_cache_and_exit(); |
---|
| 178 | } |
---|
| 179 | |
---|
| 180 | /** |
---|
| 181 | * Start Output Buffering and prepare a register shutdown func, |
---|
| 182 | * if wanted. Most likely you will call this method with arguments, |
---|
| 183 | * otherwise it just calls ob_start() and that's it. |
---|
| 184 | * |
---|
[357] | 185 | * TODO FIXME Doku outdated for this method -- |
---|
| 186 | * |
---|
[264] | 187 | * $register_shutdown_func can be: |
---|
| 188 | * - Just 'true': Then it will tell t29Cache to register it's |
---|
| 189 | * own write_cache() method as a shutdown function for PHP so |
---|
| 190 | * the cache file is written on script exit. |
---|
| 191 | * - A callable (method or function callable). This will be |
---|
| 192 | * registered at PHP shutdown, *afterwards* our own write_cache |
---|
| 193 | * method will be called. Thus you can inject printing some |
---|
| 194 | * footer code. |
---|
| 195 | * - A filter function. When $shutdown_func_is_filter is set to |
---|
| 196 | * some true value, your given callable $register_shutdown_func |
---|
| 197 | * will be used as a filter, thus being called with the whole |
---|
| 198 | * output buffer and expected to return some modification of that |
---|
| 199 | * stuff. Example: |
---|
| 200 | * $cache->start_cache(function($text) { |
---|
| 201 | * return strtoupper($text); }, true); |
---|
| 202 | * This will convert all page content to uppercase before saving |
---|
| 203 | * the stuff to the cache file. |
---|
| 204 | **/ |
---|
[357] | 205 | //function start_cache($register_shutdown_func=null, $shutdown_func_is_filter=false) { |
---|
| 206 | function start_cache(array $args) { |
---|
| 207 | $defaults = array( |
---|
| 208 | 'shutdown_func' => null, |
---|
| 209 | 'filter_func' => null, |
---|
| 210 | 'write_cache' => true, |
---|
| 211 | ); |
---|
| 212 | $args = array_merge($defaults, $args); |
---|
| 213 | |
---|
[264] | 214 | if($this->debug) |
---|
[357] | 215 | print "Will start caching with shutdown: " . $args['shutdown_func'] . PHP_EOL; |
---|
[297] | 216 | |
---|
| 217 | // check if output file is writable; for logging and logging output |
---|
| 218 | // purpose. |
---|
| 219 | //if(!is_writable($this->cache_file)) |
---|
| 220 | // print "Cache file not writable: ".$this->cache_file; |
---|
| 221 | // print "\n"; |
---|
| 222 | //exit; |
---|
| 223 | |
---|
[264] | 224 | ob_start(); |
---|
| 225 | |
---|
[357] | 226 | if($args['shutdown_func'] || $args['filter_func']) { |
---|
| 227 | // callback/filter given: Register a shutdown function |
---|
[264] | 228 | // which will call user's callback at first, then |
---|
[357] | 229 | // our own write function. and which handles filters |
---|
[264] | 230 | $t = $this; // PHP stupidity |
---|
[357] | 231 | register_shutdown_function(function() use($args, $t) { |
---|
| 232 | if($args['filter_func']) { |
---|
| 233 | // also collect the shutdown func prints in the $content |
---|
| 234 | if($args['shutdown_func']) |
---|
| 235 | call_user_func($args['shutdown_func']); |
---|
| 236 | |
---|
[270] | 237 | $content = ob_get_clean(); |
---|
[264] | 238 | if($t->debug) |
---|
| 239 | // can print output since OutputBuffering is finished |
---|
| 240 | print 't29Cache: Applying user filter to output' . PHP_EOL; |
---|
[357] | 241 | $content = call_user_func($args['filter_func'], $content); |
---|
[270] | 242 | print $content; |
---|
[357] | 243 | |
---|
| 244 | if($args['write_cache']) |
---|
| 245 | $t->write_cache($content); |
---|
| 246 | return; |
---|
| 247 | } else if($args['shutdown_func']) |
---|
| 248 | call_user_func($args['shutdown_func']); |
---|
| 249 | if($args['write_cache']) |
---|
[264] | 250 | $t->write_cache(); |
---|
| 251 | }); |
---|
[357] | 252 | } elseif($args['write_cache']) { |
---|
| 253 | // Just register our own write function |
---|
[264] | 254 | register_shutdown_function(array($this, 'write_cache')); |
---|
| 255 | } else { |
---|
| 256 | // nothing given: Dont call our write function, |
---|
[357] | 257 | // it must therefore be called by hand. |
---|
[264] | 258 | } |
---|
| 259 | } |
---|
| 260 | |
---|
| 261 | /** |
---|
| 262 | * Write Cache file. If the $content string is given, it will |
---|
| 263 | * be used as the cache content. Otherwise, a running output buffering |
---|
| 264 | * will be assumed (as start_cache fires it) and content will be |
---|
| 265 | * extracted with ob_get_flush. |
---|
[273] | 266 | * @param $content Content to be used as cache content or OB content |
---|
| 267 | * @param $clear_ob_cache Use ob_get_clean instead of flushing it. If given, |
---|
| 268 | * will return $content instead of printing/keeping it. |
---|
[264] | 269 | **/ |
---|
[273] | 270 | function write_cache($content=null, $clear_ob_cache=false) { |
---|
[264] | 271 | if(!$content) |
---|
[273] | 272 | $content = ($clear_ob_cache ? ob_get_clean() : ob_get_flush()); |
---|
[264] | 273 | |
---|
| 274 | if($this->skip) { |
---|
| 275 | $this->print_info('skipped cache and cache saving.'); |
---|
| 276 | return; // do not save anything. |
---|
| 277 | } |
---|
| 278 | |
---|
| 279 | if(!file_exists($this->cache_file)) { |
---|
| 280 | if(!self::mkdir_recursive(dirname($this->cache_file))) |
---|
| 281 | $this->print_error('Could not create recursive caching directories'); |
---|
| 282 | } |
---|
| 283 | |
---|
| 284 | if(@file_put_contents($this->cache_file, $content)) |
---|
| 285 | $this->print_info('Wrote output cache successfully'); |
---|
| 286 | else |
---|
| 287 | $this->print_error('Could not write page output cache to '.$this->cache_file); |
---|
[273] | 288 | |
---|
| 289 | if($clear_ob_cache) |
---|
| 290 | return $content; |
---|
[264] | 291 | } |
---|
| 292 | |
---|
[265] | 293 | |
---|
| 294 | private function print_info($string, $even_if_nonverbose=false) { |
---|
[264] | 295 | if($this->verbose || $even_if_nonverbose) |
---|
| 296 | echo "<!-- t29Cache: $string -->\n"; |
---|
| 297 | } |
---|
| 298 | |
---|
[265] | 299 | private function print_error($string, $even_if_nonverbose=false) { |
---|
[357] | 300 | require_once dirname(__FILE__).'/logging.php'; |
---|
| 301 | $log = t29Log::get(); |
---|
| 302 | |
---|
[264] | 303 | if($this->verbose || $even_if_nonverbose) |
---|
[357] | 304 | $log->WARN("t29Cache: ".$string, t29Log::IMMEDIATELY_PRINT); |
---|
[264] | 305 | } |
---|
| 306 | } |
---|