├── advanced-cache.php ├── batcache-stats-example.php ├── batcache.php ├── composer.json └── readme.txt /advanced-cache.php: -------------------------------------------------------------------------------- 1 | cancel = true; 16 | } 17 | 18 | // Variants can be set by functions which use early-set globals like $_SERVER to run simple tests. 19 | // Functions defined in WordPress, plugins, and themes are not available and MUST NOT be used. 20 | // Example: vary_cache_on_function('return preg_match("/feedburner/i", $_SERVER["HTTP_USER_AGENT"]);'); 21 | // This will cause batcache to cache a variant for requests from Feedburner. 22 | // Tips for writing $function: 23 | // X_X DO NOT use any functions from your theme or plugins. Those files have not been included. Fatal error. 24 | // X_X DO NOT use any WordPress functions except is_admin() and is_multisite(). Fatal error. 25 | // X_X DO NOT include or require files from anywhere without consulting expensive professionals first. Fatal error. 26 | // X_X DO NOT use $wpdb, $blog_id, $current_user, etc. These have not been initialized. 27 | // ^_^ DO understand how anonymous functions and eval work. This is how your code is used: eval( '$fun = function() { ' . $function . '; };' ); 28 | // ^_^ DO remember to return something. The return value determines the cache variant. 29 | function vary_cache_on_function($function) { 30 | global $batcache; 31 | 32 | if ( preg_match('/include|require|echo|(?add_variant($function); 39 | } 40 | 41 | class batcache { 42 | // This is the base configuration. You can edit these variables or move them into your wp-config.php file. 43 | var $max_age = 300; // Expire batcache items aged this many seconds (zero to disable batcache) 44 | 45 | var $remote = 0; // Zero disables sending buffers to remote datacenters (req/sec is never sent) 46 | 47 | var $times = 2; // Only batcache a page after it is accessed this many times... (two or more) 48 | var $seconds = 120; // ...in this many seconds (zero to ignore this and use batcache immediately) 49 | 50 | var $group = 'batcache'; // Name of memcached group. You can simulate a cache flush by changing this. 51 | 52 | var $unique = array(); // If you conditionally serve different content, put the variable values here. 53 | 54 | var $vary = array(); // Array of functions for anonymous function eval. The return value is added to $unique above. 55 | 56 | var $headers = array(); // Add headers here as name=>value or name=>array(values). These will be sent with every response from the cache. 57 | 58 | var $status_header = false; 59 | var $cache_redirects = false; // Set true to enable redirect caching. 60 | var $redirect_status = false; // This is set to the response code during a redirect. 61 | var $redirect_location = false; // This is set to the redirect location. 62 | 63 | var $use_stale = true; // Is it ok to return stale cached response when updating the cache? 64 | var $uncached_headers = array('transfer-encoding'); // These headers will never be cached. Apply strtolower. 65 | 66 | var $debug = true; // Set false to hide the batcache info 67 | 68 | var $cache_control = true; // Set false to disable Last-Modified and Cache-Control headers 69 | 70 | var $cancel = false; // Change this to cancel the output buffer. Use batcache_cancel(); 71 | 72 | var $cookie = ''; 73 | var $noskip_cookies = array( 'wordpress_test_cookie' ); // Names of cookies - if they exist and the cache would normally be bypassed, don't bypass it 74 | var $cacheable_origin_hostnames = array(); // A whitelist of HTTP origin `:` (or just ``) names that are allowed as cache variations. 75 | 76 | var $origin = null; // Current Origin header. 77 | var $query = array(); 78 | var $ignored_query_args = array(); 79 | var $genlock = false; 80 | var $do = false; 81 | 82 | //Declare used variables for PHP 8.2+ 83 | var $cache = array(); 84 | var $key = ''; 85 | var $keys = array(); 86 | var $permalink = ''; 87 | var $pos = 0; 88 | var $req_key = ''; 89 | var $requests = 0; 90 | var $status_code = null; 91 | var $url_key = ''; 92 | var $url_version = null; 93 | 94 | function __construct( $settings ) { 95 | if ( is_array( $settings ) ) foreach ( $settings as $k => $v ) 96 | $this->$k = $v; 97 | } 98 | 99 | function is_ssl() { 100 | if ( isset($_SERVER['HTTPS']) ) { 101 | if ( 'on' == strtolower($_SERVER['HTTPS']) ) 102 | return true; 103 | if ( '1' == $_SERVER['HTTPS'] ) 104 | return true; 105 | } elseif ( isset($_SERVER['SERVER_PORT']) && ( '443' == $_SERVER['SERVER_PORT'] ) ) { 106 | return true; 107 | } 108 | return false; 109 | } 110 | 111 | function client_accepts_only_json() { 112 | if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) 113 | return false; 114 | 115 | $is_json_only = false; 116 | 117 | foreach ( explode( ',', $_SERVER['HTTP_ACCEPT'] ) as $mime_type ) { 118 | if ( false !== $pos = strpos( $mime_type, ';' ) ) 119 | $mime_type = substr( $mime_type, 0, $pos ); 120 | 121 | $mime_type = trim( $mime_type ); 122 | 123 | if ( '/json' === substr( $mime_type, -5 ) || '+json' === substr( $mime_type, -5 ) ) { 124 | $is_json_only = true; 125 | continue; 126 | } 127 | 128 | return false; 129 | } 130 | 131 | return $is_json_only; 132 | } 133 | 134 | function is_cacheable_origin( $origin ) { 135 | $parsed_origin = parse_url( $origin ); 136 | 137 | if ( false === $parsed_origin ) { 138 | return false; 139 | } 140 | 141 | $origin_host = ! empty( $parsed_origin['host'] ) ? strtolower( $parsed_origin['host'] ) : null; 142 | $origin_scheme = ! empty( $parsed_origin['scheme'] ) ? strtolower( $parsed_origin['scheme'] ) : null; 143 | $origin_port = ! empty( $parsed_origin['port'] ) ? $parsed_origin['port'] : null; 144 | 145 | return $origin 146 | && $origin_host 147 | && ( 'http' === $origin_scheme || 'https' === $origin_scheme ) 148 | && ( null === $origin_port || 80 === $origin_port || 443 === $origin_port ) 149 | && in_array( $origin_host, $this->cacheable_origin_hostnames, true ); 150 | } 151 | 152 | function status_header( $status_header, $status_code ) { 153 | $this->status_header = $status_header; 154 | $this->status_code = $status_code; 155 | 156 | return $status_header; 157 | } 158 | 159 | function redirect_status( $status, $location ) { 160 | if ( $this->cache_redirects ) { 161 | $this->redirect_status = $status; 162 | $this->redirect_location = $location; 163 | } 164 | 165 | return $status; 166 | } 167 | 168 | function do_headers( $headers1, $headers2 = array() ) { 169 | // Merge the arrays of headers into one 170 | $headers = array(); 171 | $keys = array_unique( array_merge( array_keys( $headers1 ), array_keys( $headers2 ) ) ); 172 | foreach ( $keys as $k ) { 173 | $headers[$k] = array(); 174 | if ( isset( $headers1[$k] ) && isset( $headers2[$k] ) ) 175 | $headers[$k] = array_merge( (array) $headers2[$k], (array) $headers1[$k] ); 176 | elseif ( isset( $headers2[$k] ) ) 177 | $headers[$k] = (array) $headers2[$k]; 178 | else 179 | $headers[$k] = (array) $headers1[$k]; 180 | $headers[$k] = array_unique( $headers[$k] ); 181 | } 182 | // These headers take precedence over any previously sent with the same names 183 | foreach ( $headers as $k => $values ) { 184 | $clobber = true; 185 | foreach ( $values as $v ) { 186 | header( "$k: $v", $clobber ); 187 | $clobber = false; 188 | } 189 | } 190 | } 191 | 192 | function configure_groups() { 193 | // Configure the memcached client 194 | if ( ! $this->remote ) 195 | if ( function_exists('wp_cache_add_no_remote_groups') ) 196 | wp_cache_add_no_remote_groups(array($this->group)); 197 | if ( function_exists('wp_cache_add_global_groups') ) 198 | wp_cache_add_global_groups(array($this->group)); 199 | } 200 | 201 | // Defined here because timer_stop() calls number_format_i18n() 202 | function timer_stop($display = 0, $precision = 3) { 203 | global $timestart, $timeend; 204 | $mtime = microtime(); 205 | $mtime = explode(' ',$mtime); 206 | $mtime = $mtime[1] + $mtime[0]; 207 | $timeend = $mtime; 208 | $timetotal = $timeend-$timestart; 209 | $r = number_format($timetotal, $precision); 210 | if ( $display ) 211 | echo $r; 212 | return $r; 213 | } 214 | 215 | function ob($output) { 216 | // PHP5 and objects disappearing before output buffers? 217 | wp_cache_init(); 218 | 219 | // Remember, $wp_object_cache was clobbered in wp-settings.php so we have to repeat this. 220 | $this->configure_groups(); 221 | 222 | if ( $this->cancel !== false ) { 223 | wp_cache_delete( "{$this->url_key}_genlock", $this->group ); 224 | return $output; 225 | } 226 | 227 | // Do not batcache blank pages unless they are HTTP redirects 228 | $output = trim($output); 229 | if ( $output === '' && (!$this->redirect_status || !$this->redirect_location) ) { 230 | wp_cache_delete( "{$this->url_key}_genlock", $this->group ); 231 | return; 232 | } 233 | 234 | // Do not cache 5xx responses 235 | if ( isset( $this->status_code ) && intval($this->status_code / 100) == 5 ) { 236 | wp_cache_delete( "{$this->url_key}_genlock", $this->group ); 237 | return $output; 238 | } 239 | 240 | $this->do_variants($this->vary); 241 | $this->generate_keys(); 242 | 243 | // Construct and save the batcache 244 | $this->cache = array( 245 | 'output' => $output, 246 | 'time' => isset( $_SERVER['REQUEST_TIME'] ) ? $_SERVER['REQUEST_TIME'] : time(), 247 | 'timer' => $this->timer_stop(false, 3), 248 | 'headers' => array(), 249 | 'status_header' => $this->status_header, 250 | 'redirect_status' => $this->redirect_status, 251 | 'redirect_location' => $this->redirect_location, 252 | 'version' => $this->url_version 253 | ); 254 | 255 | foreach ( headers_list() as $header ) { 256 | list($k, $v) = array_map('trim', explode(':', $header, 2)); 257 | $this->cache['headers'][$k][] = $v; 258 | } 259 | 260 | if ( !empty( $this->cache['headers'] ) && !empty( $this->uncached_headers ) ) { 261 | foreach ( $this->uncached_headers as $header ) 262 | unset( $this->cache['headers'][$header] ); 263 | } 264 | 265 | foreach ( $this->cache['headers'] as $header => $values ) { 266 | // Do not cache if cookies were set 267 | if ( strtolower( $header ) === 'set-cookie' ) { 268 | wp_cache_delete( "{$this->url_key}_genlock", $this->group ); 269 | return $output; 270 | } 271 | 272 | foreach ( (array) $values as $value ) 273 | if ( preg_match('/^Cache-Control:.*max-?age=(\d+)/i', "$header: $value", $matches) ) 274 | $this->max_age = intval($matches[1]); 275 | } 276 | 277 | $this->cache['max_age'] = $this->max_age; 278 | 279 | wp_cache_set($this->key, $this->cache, $this->group, $this->max_age + $this->seconds + 30); 280 | 281 | // Unlock regeneration 282 | wp_cache_delete("{$this->url_key}_genlock", $this->group); 283 | 284 | if ( $this->cache_control ) { 285 | // Don't clobber Last-Modified header if already set, e.g. by WP::send_headers() 286 | if ( !isset($this->cache['headers']['Last-Modified']) ) 287 | header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $this->cache['time'] ) . ' GMT', true ); 288 | if ( !isset($this->cache['headers']['Cache-Control']) ) 289 | header("Cache-Control: max-age=$this->max_age, must-revalidate", false); 290 | } 291 | 292 | $this->do_headers( $this->headers ); 293 | 294 | // Add some debug info just before debug ) { 296 | $this->add_debug_just_cached(); 297 | } 298 | 299 | // Pass output to next ob handler 300 | batcache_stats( 'batcache', 'total_page_views' ); 301 | return $this->cache['output']; 302 | } 303 | 304 | function add_variant($function) { 305 | $key = md5($function); 306 | $this->vary[$key] = $function; 307 | } 308 | 309 | function do_variants($dimensions = false) { 310 | // This function is called without arguments early in the page load, then with arguments during the OB handler. 311 | if ( $dimensions === false ) 312 | $dimensions = wp_cache_get("{$this->url_key}_vary", $this->group); 313 | else 314 | wp_cache_set("{$this->url_key}_vary", $dimensions, $this->group, $this->max_age + 10); 315 | 316 | if ( is_array($dimensions) ) { 317 | ksort($dimensions); 318 | foreach ( $dimensions as $key => $function ) { 319 | eval( '$fun = function() { ' . $function . '; };' ); 320 | $value = call_user_func( $fun ); 321 | $this->keys[$key] = $value; 322 | } 323 | } 324 | } 325 | 326 | function generate_keys() { 327 | // ksort($this->keys); // uncomment this when traffic is slow 328 | $this->key = md5(serialize($this->keys)); 329 | $this->req_key = $this->key . '_reqs'; 330 | } 331 | 332 | function add_debug_just_cached() { 333 | $generation = $this->cache['timer']; 334 | $bytes = strlen( serialize( $this->cache ) ); 335 | $html = <<max_age} seconds 339 | --> 340 | 341 | HTML; 342 | $this->add_debug_html_to_output( $html ); 343 | } 344 | 345 | function add_debug_from_cache() { 346 | $seconds_ago = time() - $this->cache['time']; 347 | $generation = $this->cache['timer']; 348 | $serving = $this->timer_stop( false, 3 ); 349 | $expires = $this->cache['max_age'] - time() + $this->cache['time']; 350 | $html = << 357 | 358 | HTML; 359 | $this->add_debug_html_to_output( $html ); 360 | } 361 | 362 | function add_debug_html_to_output( $debug_html ) { 363 | // Casing on the Content-Type header is inconsistent 364 | foreach ( array( 'Content-Type', 'Content-type' ) as $key ) { 365 | if ( isset( $this->cache['headers'][ $key ][0] ) && 0 !== strpos( $this->cache['headers'][ $key ][0], 'text/html' ) ) 366 | return; 367 | } 368 | 369 | $head_position = strpos( $this->cache['output'], 'cache['output'] .= "\n$debug_html"; 374 | } 375 | 376 | function set_query( $query_string ) { 377 | parse_str( $query_string, $this->query ); 378 | 379 | foreach ( $this->ignored_query_args as $arg ) { 380 | unset( $this->query[ $arg ] ); 381 | } 382 | 383 | // Normalize query parameters for better cache hits. 384 | ksort( $this->query ); 385 | } 386 | } 387 | 388 | global $batcache; 389 | // Pass in the global variable which may be an array of settings to override defaults. 390 | $batcache = new batcache($batcache); 391 | 392 | if ( ! defined( 'WP_CONTENT_DIR' ) ) 393 | return; 394 | 395 | // Never batcache interactive scripts or API endpoints. 396 | if ( in_array( 397 | basename( $_SERVER['SCRIPT_FILENAME'] ), 398 | array( 399 | 'wp-app.php', 400 | 'xmlrpc.php', 401 | 'wp-cron.php', 402 | ) ) ) 403 | return; 404 | 405 | // Never batcache WP javascript generators 406 | if ( strstr( $_SERVER['SCRIPT_FILENAME'], 'wp-includes/js' ) ) 407 | return; 408 | 409 | // Only cache HEAD and GET requests. 410 | if ((isset($_SERVER['REQUEST_METHOD']) && !in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD')))) { 411 | return; 412 | } 413 | 414 | // Never batcache a request with X-WP-Nonce header. 415 | if ( ! empty( $_SERVER['HTTP_X_WP_NONCE'] ) ) { 416 | batcache_stats( 'batcache', 'x_wp_nonce_skip' ); 417 | return; 418 | } 419 | 420 | // Never batcache when cookies indicate a cache-exempt visitor. 421 | if ( is_array( $_COOKIE) && ! empty( $_COOKIE ) ) { 422 | foreach ( array_keys( $_COOKIE ) as $batcache->cookie ) { 423 | if ( ! in_array( $batcache->cookie, $batcache->noskip_cookies ) && ( substr( $batcache->cookie, 0, 2 ) == 'wp' || substr( $batcache->cookie, 0, 9 ) == 'wordpress' || substr( $batcache->cookie, 0, 14 ) == 'comment_author' ) ) { 424 | batcache_stats( 'batcache', 'cookie_skip' ); 425 | return; 426 | } 427 | } 428 | } 429 | 430 | // Never batcache a response for a request with an Origin request header. 431 | // *Unless* that Origin header is in the configured whitelist of allowed origins with restricted schemes and ports. 432 | if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) { 433 | if ( ! $batcache->is_cacheable_origin( $_SERVER['HTTP_ORIGIN'] ) ) { 434 | batcache_stats( 'batcache', 'origin_skip' ); 435 | return; 436 | } 437 | 438 | $batcache->origin = $_SERVER['HTTP_ORIGIN']; 439 | } 440 | 441 | if ( ! include_once( WP_CONTENT_DIR . '/object-cache.php' ) ) 442 | return; 443 | 444 | wp_cache_init(); // Note: wp-settings.php calls wp_cache_init() which clobbers the object made here. 445 | 446 | if ( empty( $wp_object_cache ) || ! is_object( $wp_object_cache ) ) { 447 | return; 448 | } 449 | 450 | // Now that the defaults are set, you might want to use different settings under certain conditions. 451 | 452 | /* Example: if your documents have a mobile variant (a different document served by the same URL) you must tell batcache about the variance. Otherwise you might accidentally cache the mobile version and serve it to desktop users, or vice versa. 453 | $batcache->unique['mobile'] = is_mobile_user_agent(); 454 | */ 455 | 456 | /* Example: never batcache for this host 457 | if ( $_SERVER['HTTP_HOST'] == 'do-not-batcache-me.com' ) 458 | return; 459 | */ 460 | 461 | /* Example: batcache everything on this host regardless of traffic level 462 | if ( $_SERVER['HTTP_HOST'] == 'always-batcache-me.com' ) 463 | return; 464 | */ 465 | 466 | /* Example: If you sometimes serve variants dynamically (e.g. referrer search term highlighting) you probably don't want to batcache those variants. Remember this code is run very early in wp-settings.php so plugins are not yet loaded. You will get a fatal error if you try to call an undefined function. Either include your plugin now or define a test function in this file. 467 | if ( include_once( 'plugins/searchterm-highlighter.php') && referrer_has_search_terms() ) 468 | return; 469 | */ 470 | 471 | // Disabled 472 | if ( $batcache->max_age < 1 ) 473 | return; 474 | 475 | // Make sure we can increment. If not, turn off the traffic sensor. 476 | if ( ! method_exists( $GLOBALS['wp_object_cache'], 'incr' ) ) 477 | $batcache->times = 0; 478 | 479 | // Necessary to prevent clients using cached version after login cookies set. If this is a problem, comment it out and remove all Last-Modified headers. 480 | header('Vary: Cookie', false); 481 | 482 | // Things that define a unique page. 483 | if ( isset( $_SERVER['QUERY_STRING'] ) ) { 484 | $batcache->set_query( $_SERVER['QUERY_STRING'] ); 485 | } 486 | 487 | $batcache->keys = array( 488 | 'host' => $_SERVER['HTTP_HOST'], 489 | 'method' => $_SERVER['REQUEST_METHOD'], 490 | 'path' => ( $batcache->pos = strpos($_SERVER['REQUEST_URI'], '?') ) ? substr($_SERVER['REQUEST_URI'], 0, $batcache->pos) : $_SERVER['REQUEST_URI'], 491 | 'query' => $batcache->query, 492 | 'extra' => $batcache->unique 493 | ); 494 | if ( isset( $batcache->origin ) ) { 495 | $batcache->keys['origin'] = $batcache->origin; 496 | } 497 | 498 | if ( $batcache->is_ssl() ) 499 | $batcache->keys['ssl'] = true; 500 | 501 | # Some plugins return html or json based on the Accept value for the same URL. 502 | if ( $batcache->client_accepts_only_json() ) 503 | $batcache->keys['json'] = true; 504 | 505 | // Recreate the permalink from the URL 506 | $batcache->permalink = 'http://' . $batcache->keys['host'] . $batcache->keys['path'] . ( isset($batcache->keys['query']['p']) ? "?p=" . $batcache->keys['query']['p'] : '' ); 507 | $batcache->url_key = md5($batcache->permalink); 508 | $batcache->configure_groups(); 509 | $batcache->url_version = (int) wp_cache_get("{$batcache->url_key}_version", $batcache->group); 510 | $batcache->do_variants(); 511 | $batcache->generate_keys(); 512 | 513 | // Get the batcache 514 | $batcache->cache = wp_cache_get($batcache->key, $batcache->group); 515 | $is_cached = is_array( $batcache->cache ) && isset( $batcache->cache['time'] ); 516 | $has_expired = $is_cached && time() > $batcache->cache['time'] + $batcache->cache['max_age']; 517 | 518 | if ( isset( $batcache->cache['version'] ) && $batcache->cache['version'] != $batcache->url_version ) { 519 | // Always refresh the cache if a newer version is available. 520 | $batcache->do = true; 521 | } else if ( $batcache->seconds < 1 || $batcache->times < 2 ) { 522 | // Cache is empty or has expired and we're caching all requests. 523 | $batcache->do = ! $is_cached || $has_expired; 524 | } else { 525 | // No batcache item found, or ready to sample traffic again at the end of the batcache life? 526 | if ( ! $is_cached || time() >= $batcache->cache['time'] + $batcache->max_age - $batcache->seconds ) { 527 | wp_cache_add($batcache->req_key, 0, $batcache->group); 528 | $batcache->requests = wp_cache_incr($batcache->req_key, 1, $batcache->group); 529 | 530 | if ( 531 | $batcache->requests >= $batcache->times && // visited enough times 532 | ( 533 | ! $is_cached || // no cache 534 | time() >= $batcache->cache['time'] + $batcache->cache['max_age'] // or cache expired 535 | ) 536 | ) { 537 | wp_cache_delete( $batcache->req_key, $batcache->group ); 538 | $batcache->do = true; 539 | } else { 540 | $batcache->do = false; 541 | } 542 | } 543 | } 544 | 545 | // Obtain cache generation lock 546 | if ( $batcache->do ) 547 | $batcache->genlock = wp_cache_add("{$batcache->url_key}_genlock", 1, $batcache->group, 10); 548 | 549 | if ( 550 | $is_cached && // We have cache 551 | ! $batcache->genlock && // We have not obtained cache regeneration lock 552 | ( 553 | ! $has_expired || // Batcached page that hasn't expired 554 | ( $batcache->do && $batcache->use_stale ) // Regenerating it in another request and can use stale cache 555 | ) 556 | ) { 557 | // Issue redirect if cached and enabled 558 | if ( $batcache->cache['redirect_status'] && $batcache->cache['redirect_location'] && $batcache->cache_redirects ) { 559 | $status = $batcache->cache['redirect_status']; 560 | $location = $batcache->cache['redirect_location']; 561 | // From vars.php 562 | $is_IIS = (strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false || strpos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false); 563 | 564 | $batcache->do_headers( $batcache->headers ); 565 | if ( $is_IIS ) { 566 | header("Refresh: 0;url=$location"); 567 | } else { 568 | if ( php_sapi_name() != 'cgi-fcgi' ) { 569 | $texts = array( 570 | 300 => 'Multiple Choices', 571 | 301 => 'Moved Permanently', 572 | 302 => 'Found', 573 | 303 => 'See Other', 574 | 304 => 'Not Modified', 575 | 305 => 'Use Proxy', 576 | 306 => 'Reserved', 577 | 307 => 'Temporary Redirect', 578 | ); 579 | $protocol = $_SERVER["SERVER_PROTOCOL"]; 580 | if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) 581 | $protocol = 'HTTP/1.0'; 582 | if ( isset($texts[$status]) ) 583 | header("$protocol $status " . $texts[$status]); 584 | else 585 | header("$protocol 302 Found"); 586 | } 587 | header("Location: $location"); 588 | } 589 | exit; 590 | } 591 | 592 | // Respect ETags served with feeds. 593 | $three04 = false; 594 | if ( isset( $SERVER['HTTP_IF_NONE_MATCH'] ) && isset( $batcache->cache['headers']['ETag'][0] ) && $_SERVER['HTTP_IF_NONE_MATCH'] == $batcache->cache['headers']['ETag'][0] ) 595 | $three04 = true; 596 | 597 | // Respect If-Modified-Since. 598 | elseif ( $batcache->cache_control && isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) { 599 | $client_time = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); 600 | if ( isset($batcache->cache['headers']['Last-Modified'][0]) ) 601 | $cache_time = strtotime($batcache->cache['headers']['Last-Modified'][0]); 602 | else 603 | $cache_time = $batcache->cache['time']; 604 | 605 | if ( $client_time >= $cache_time ) 606 | $three04 = true; 607 | } 608 | 609 | // Use the batcache save time for Last-Modified so we can issue "304 Not Modified" but don't clobber a cached Last-Modified header. 610 | if ( $batcache->cache_control && !isset($batcache->cache['headers']['Last-Modified'][0]) ) { 611 | header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $batcache->cache['time'] ) . ' GMT', true ); 612 | header('Cache-Control: max-age=' . ($batcache->cache['max_age'] - time() + $batcache->cache['time']) . ', must-revalidate', true); 613 | } 614 | 615 | // Add some debug info just before 616 | if ( $batcache->debug ) { 617 | $batcache->add_debug_from_cache(); 618 | } 619 | 620 | $batcache->do_headers( $batcache->headers, $batcache->cache['headers'] ); 621 | 622 | if ( $three04 ) { 623 | header("HTTP/1.1 304 Not Modified", true, 304); 624 | die; 625 | } 626 | 627 | if ( !empty($batcache->cache['status_header']) ) 628 | header($batcache->cache['status_header'], true); 629 | 630 | batcache_stats( 'batcache', 'total_cached_views' ); 631 | 632 | // Have you ever heard a death rattle before? 633 | die($batcache->cache['output']); 634 | } 635 | 636 | // Didn't meet the minimum condition? 637 | if ( ! $batcache->do || ! $batcache->genlock ) 638 | return; 639 | 640 | //WordPress 4.7 changes how filters are hooked. Since WordPress 4.6 add_filter can be used in advanced-cache.php. Previous behaviour is kept for backwards compatability with WP < 4.6 641 | if ( function_exists( 'add_filter' ) ) { 642 | add_filter( 'status_header', array( &$batcache, 'status_header' ), 10, 2 ); 643 | add_filter( 'wp_redirect_status', array( &$batcache, 'redirect_status' ), 10, 2 ); 644 | } else { 645 | $wp_filter['status_header'][10]['batcache'] = array( 'function' => array(&$batcache, 'status_header'), 'accepted_args' => 2 ); 646 | $wp_filter['wp_redirect_status'][10]['batcache'] = array( 'function' => array(&$batcache, 'redirect_status'), 'accepted_args' => 2 ); 647 | } 648 | 649 | ob_start(array(&$batcache, 'ob')); 650 | 651 | // It is safer to omit the final PHP closing tag. 652 | -------------------------------------------------------------------------------- /batcache-stats-example.php: -------------------------------------------------------------------------------- 1 | configure_groups(); 16 | 17 | // Regen home and permalink on posts and pages 18 | add_action('clean_post_cache', 'batcache_post', 10, 2); 19 | 20 | // Regen permalink on comments (TODO) 21 | //add_action('comment_post', 'batcache_comment'); 22 | //add_action('wp_set_comment_status', 'batcache_comment'); 23 | //add_action('edit_comment', 'batcache_comment'); 24 | 25 | function batcache_post($post_id, $post = null) { 26 | global $batcache; 27 | 28 | // Get the post for backwards compatibility with earlier versions of WordPress 29 | if ( ! $post ) { 30 | $post = get_post( $post_id ); 31 | } 32 | 33 | if ( ! $post || $post->post_type == 'revision' || ! in_array( get_post_status($post_id), array( 'publish', 'trash' ) ) ) 34 | return; 35 | 36 | $home = trailingslashit( get_option('home') ); 37 | batcache_clear_url( $home ); 38 | batcache_clear_url( $home . 'feed/' ); 39 | batcache_clear_url( get_permalink($post_id) ); 40 | } 41 | 42 | function batcache_clear_url($url) { 43 | global $batcache, $wp_object_cache; 44 | 45 | if ( empty($url) ) 46 | return false; 47 | 48 | if ( 0 === strpos( $url, 'https://' ) ) 49 | $url = str_replace( 'https://', 'http://', $url ); 50 | if ( 0 !== strpos( $url, 'http://' ) ) 51 | $url = 'http://' . $url; 52 | 53 | $url_key = md5( $url ); 54 | wp_cache_add("{$url_key}_version", 0, $batcache->group); 55 | $retval = wp_cache_incr("{$url_key}_version", 1, $batcache->group); 56 | 57 | $batcache_no_remote_group_key = array_search( $batcache->group, (array) $wp_object_cache->no_remote_groups ); 58 | if ( false !== $batcache_no_remote_group_key ) { 59 | // The *_version key needs to be replicated remotely, otherwise invalidation won't work. 60 | // The race condition here should be acceptable. 61 | unset( $wp_object_cache->no_remote_groups[ $batcache_no_remote_group_key ] ); 62 | $retval = wp_cache_set( "{$url_key}_version", $retval, $batcache->group ); 63 | $wp_object_cache->no_remote_groups[ $batcache_no_remote_group_key ] = $batcache->group; 64 | } 65 | 66 | return $retval; 67 | } 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "automattic/batcache", 3 | "description": "A memcached HTML page cache for WordPress.", 4 | "homepage" : "https://github.com/Automattic/batcache", 5 | "type" : "wordpress-muplugin", 6 | "license" : "GPL-2.0+", 7 | "support" : { 8 | "issues": "https://github.com/Automattic/batcache/issues", 9 | "forum": "https://wordpress.org/support/plugin/batcache", 10 | "source": "https://github.com/Automattic/batcache" 11 | }, 12 | "require" : { 13 | "composer/installers": "~1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Batcache === 2 | Contributors: automattic, andy, orensol, markjaquith, vnsavage, batmoo, yoavf 3 | Tags: cache, memcache, memcached, speed, performance, load, server 4 | Requires at least: 3.2 5 | Tested up to: 5.3.2 6 | Stable tag: 1.5 7 | 8 | Batcache uses Memcached to store and serve rendered pages. 9 | 10 | == Description == 11 | 12 | Batcache uses Memcached to store and serve rendered pages. It can also optionally cache redirects. It's not as fast as Donncha's WP-Super-Cache but it can be used where file-based caching is not practical or not desired. For instance, any site that is run on more than one server should use Batcache because it allows all servers to use the same storage. 13 | 14 | Development testing showed a 40x reduction in page generation times: pages generated in 200ms were served from the cache in 5ms. Traffic simulations with Siege demonstrate that WordPress can handle up to twenty times more traffic with Batcache installed. 15 | 16 | Batcache is aimed at preventing a flood of traffic from breaking your site. It does this by serving old pages to new users. This reduces the demand on the web server CPU and the database. It also means some people may see a page that is a few minutes old. However this only applies to people who have not interacted with your web site before. Once they have logged in or left a comment they will always get fresh pages. 17 | 18 | Possible future features: 19 | 20 | * Comments, edits, and new posts will trigger cache regeneration 21 | * Online installation assistance 22 | * Configuration page 23 | * Stats 24 | 25 | == Installation == 26 | 27 | 1. Get the Memcached backend working. See below. 28 | 29 | 1. Upload `advanced-cache.php` to the `/wp-content/` directory 30 | 31 | 1. Add this line the top of `wp-config.php` to activate Batcache: 32 | 33 | `define('WP_CACHE', true);` 34 | 35 | 1. Test by reloading a page in your browser several times and then viewing the source. Just above the `` closing tag you should see some Batcache stats. 36 | 37 | 1. Tweak the options near the top of `advanced-cache.php` 38 | 39 | 1. *Optional* Upload `batcache.php` to the `/wp-content/plugins/` directory. 40 | 41 | = Memcached backend = 42 | 43 | 1. Install [memcached](https://memcached.org/) on at least one server. Note the connection info. The default is `127.0.0.1:11211`. 44 | 45 | 1. Install the [PECL memcached extension](http://pecl.php.net/package/memcache) and [Memcached Object Cache](https://wordpress.org/plugins/memcached/). 46 | 47 | == Frequently Asked Questions == 48 | 49 | = Should I use this? = 50 | 51 | Batcache can be used anywhere Memcached is available. WP-Super-Cache is preferred for most blogs. If you have more than one web server, try Batcache. 52 | 53 | = Why was this written? = 54 | 55 | Batcache was written to help WordPress.com cope with the massive and prolonged traffic spike on Gizmodo's live blog during Apple events. Live blogs were famous for failing under the load of traffic. Gizmodo's live blog stays up because of Batcache. 56 | 57 | Actually all of WordPress.com stays up during Apple events because of Batcache. The traffic is twice the average during Apple events. But the web servers and databases barely feel the difference. 58 | 59 | = What does it have to do with bats? = 60 | 61 | Batcache was named "supercache" when it was written. (It's still called that on WordPress.com.) A few months later, while "supercache" was still private, Donncha released the WP-Super-Cache plugin. It wouldn't be fun to dispute the name or create confusion for users so a name change seemed best. The move from "Super" to "Bat" was inspired by comic book heroes. It has nothing to do with the fact that the author's city is home to the [world's largest urban bat colony](http://www.batcon.org/our-work/regions/usa-canada/protect-mega-populations/cab-intro). 62 | 63 | == Changelog == 64 | 65 | = 1.5 = 66 | 67 | * Add stats for cache hits 68 | * PHP 4 constructors are deprecated in PHP7 69 | * Removed "HTTP_RAW_POST_DATA" variable replaced with input stream check 70 | * Use Plugins API rather than the global variable 71 | * Set page gen time to request start if possible 72 | * Don't use get_post() when cleaning post cache, use already passed $post object 73 | * Only cache GET or HEAD 74 | * Add Opt-in CORS GET request cache. 75 | = 1.4 = 76 | * Misc updates 77 | 78 | = 1.3 = 79 | * Code cleanup, multi-dc support improvements 80 | 81 | = 1.2 = 82 | * Add REQUEST_METHOD to the cache keys. Prevents GET requests receiving bodyless HEAD responses. This change invalidates the entire cache at upgrade time. 83 | 84 | = 1.1 = 85 | * Many bugfixes and updates from trunk 86 | --------------------------------------------------------------------------------