Code owners
Assign users and groups as approvers for specific file changes. Learn more.
index-redis-page-cache.php 16.84 KiB
<?php
/*
Plugin Name: Redis Page Cache
Plugin URI: http://eth.pw/rpc
Version: 1.0
Description: Manage settings for full-page caching powered by Redis.
Author: Erick Hitter
Author URI: https://ethitter.com/
This software is based on WP Redis Cache by Benjamin Adams, copyright 2013.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2, as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* GLOBAL CONFIGURATION
*/
global $redis_page_cache_config;
$redis_page_cache_config = array(
'debug' => false,
'debug_messages' => '',
'stats' => false,
'cached' => false,
'server_ip' => '127.0.0.1',
'secret_string' => 'changeme',
'redis_server' => '127.0.0.1',
'redis_port' => 6379,
'redis_db' => 0,
'cache_version' => 0,
'cache_headers' => true,
'additional_headers' => array( 'link', 'x-hacker', 'x-pingback' ),
'query_strings_to_ignore' => array(), // common tracking strings are automatically excluded
'minify' => false,
);
// Uncomment either option below to fix the values here and disable the admin UI
// $redis_page_cache_config['cache_duration'] = 43200;
// $redis_page_cache_config['unlimited'] = false;
// Modify this function to introduce custom handling when exceptions occur
function redis_page_cache_exception_handler( $exception ) {
return;
}
/**
* END GLOBAL CONFIGURATION
*
* DO NOT EDIT BELOW THIS LINE!
*/
// Start the timer so we can track the page load time
if ( $redis_page_cache_config['debug'] || $redis_page_cache_config['stats'] ) {
$start = microtime();
}
// Make run-time additions to configuration
$redis_page_cache_config['current_url'] = redis_page_cache_get_clean_url();
$redis_page_cache_config['redis_key'] = md5( 'v' . $redis_page_cache_config['cache_version'] . '-' . $redis_page_cache_config['current_url'] );
$redis_page_cache_config['redis_key'] = redis_page_cache_set_device_key( $redis_page_cache_config['redis_key'] );
/**
* UTILITY FUNCTIONS
*/
/**
* Compute microtime from a timestamp
*
* @return float
*/
function redis_page_cache_get_micro_time( $time ) {
list( $usec, $sec ) = explode( " ", $time );
return ( (float) $usec + (float) $sec );
}
/**
* Count seconds elapsed between two microtime() timestampes
*
* @param string $start
* @param string $end
* @param int $precision
* @return float
*/
function redis_page_cache_time_elapsed( $start, $end ) {
return round( @redis_page_cache_get_micro_time( $end ) - @redis_page_cache_get_micro_time( $start ), 5 );
}
/**
* Is the current request a refresh request with the correct secret key?
*
* @return bool
*/
function redis_page_cache_refresh_has_secret( $secret ) {
return isset( $_GET['refresh'] ) && $secret == $_GET['refresh'];
}
/**
* Does current request include a refresh request?
*
* @return bool
*/
function redis_page_cache_request_has_secret( $secret ) {
return false !== strpos( $_SERVER['REQUEST_URI'], "refresh=${secret}" );
}
/**
* Set proper IP address for proxied requests
*
* @return null
*/
function redis_page_cache_handle_cdn_remote_addressing() {
// so we don't confuse the cloudflare server
if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
}
}
/**
* Prepare a URL for use as a cache key
*
* If the URL is too malformed to parse, a one-time cache is set using microtime().
*
* @return string
*/
function redis_page_cache_get_clean_url() {
static $url;
if ( ! $url ) {
global $redis_page_cache_config;
$proto = 'http';
if ( isset( $_SERVER['HTTPS'] ) && ( 'on' === strtolower( $_SERVER['HTTPS'] ) || '1' === $_SERVER['HTTPS'] ) ) {
$proto .= 's';
} elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
$proto .= 's';
}
$url = parse_url( $proto . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
if ( $url ) {
// Query strings create their own caches, so we reduce proliferation by ignoring certain common strings
$qs = '';
if ( ! empty( $_GET ) ) {
$ignore = array( 'c', 'flush', 'secret', 'redis-page-cache-purge', 'utm_source', 'utm_medium', 'utm_term', 'utm_content', 'utm_campaign', 'fb_action_ids', 'fb_action_types', 'fb_ref', 'fb_source', 'fb_aggregation_id', );
$ignore = array_merge( $ignore, $redis_page_cache_config['query_strings_to_ignore'] );
$ignore = array_flip( $ignore );
$_qs = array_diff_key( $_GET, $ignore );
if ( ! empty( $_qs ) ) {
$qs = '?';
foreach ( $_qs as $key => $value ) {
if ( strlen( $qs ) > 1 ) {
$qs .= '&';
}
$qs .= "{$key}={$value}";
}
$qs = preg_replace( '#[^A-Z0-9=\-\?\&]#i', '', $qs );
}
}
$url = $url['scheme'] . '://' . $url['host'] . $url['path'] . $qs;
} else {
$url = microtime();
}
}
return $url;
}
/**
* Prefix cache key if device calls for separate caching
*
* @param string $key
* @return $string
*/
function redis_page_cache_set_device_key( $key ) {
switch ( redis_page_cache_get_device_type() ) {
case 'tablet' :
$prefix = 'T-';
break;
case 'mobile' :
$prefix = 'M-';
break;
default :
case 'desktop' :
$prefix = '';
break;
}
return $prefix . $key;
}
/**
* Determine the current device type from its user agent
* Allows for separate caches for tablet, mobile, and desktop visitors
*
* @return string
*/
function redis_page_cache_get_device_type() {
$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
if ( empty( $ua ) ) {
return 'desktop';
}
// Tablet user agents
if (
false !== stripos( $ua, 'ipad' ) ||
( false !== stripos( $ua, 'Android' ) && false === stripos( $ua, 'mobile' ) ) ||
false !== stripos( $ua, 'tablet ' ) ||
false !== stripos( $ua, 'Silk/' ) ||
false !== stripos( $ua, 'Kindle' ) ||
false !== stripos( $ua, 'PlayBook' ) ||
false !== stripos( $ua, 'RIM Tablet' )
) {
return 'tablet';
}
// Mobile user agents
if (
false !== stripos( $ua, 'Mobile' ) || // many mobile devices (all iPhone, iPad, etc.)
false !== stripos( $ua, 'Android' ) ||
false !== stripos( $ua, 'BlackBerry' ) ||
false !== stripos( $ua, 'Opera Mini' ) ||
false !== stripos( $ua, 'Opera Mobi' )
) {
return 'mobile';
}
return 'desktop';
}
/**
* Establish a connection to the Redis server
*
* Will try the PECL module first, then fall back to PRedis
*
* @return object
*/
function redis_page_cache_connect_redis() {
global $redis_page_cache_config;
// check if PECL Extension is available
if ( class_exists( 'Redis' ) ) {
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Redis: PECL -->\n";
}
$redis = new Redis();
$redis->connect( $redis_page_cache_config['redis_server'], $redis_page_cache_config['redis_port'] );
// Default DB is 0, so only need to SELECT if other
if ( $redis_page_cache_config['redis_db'] ) {
$redis->select( $redis_page_cache_config['redis_db'] );
}
// Fallback to predis5.2.php
} else {
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Redis: Predis -->\n";
}
include_once dirname( __FILE__ ) . '/wp-content/plugins/redis-page-cache/predis5.2.php'; //we need this to use Redis inside of PHP
$redis = array(
'host' => $redis_page_cache_config['redis_server'],
'port' => $redis_page_cache_config['redis_port'],
);
// Default DB is 0, so only need to SELECT if other
if ( $redis_page_cache_config['redis_db'] ) {
$redis['database'] = $redis_page_cache_config['redis_db'];
}
$redis = new Predis_Client( $redis );
}
return $redis;
}
/**
* BEGIN CACHING LOGIC
*/
// Set proper IP for proxied requests
redis_page_cache_handle_cdn_remote_addressing();
// Ensure WP uses a theme (this is normally set in index.php)
if ( ! defined( 'WP_USE_THEMES' ) ) {
define( 'WP_USE_THEMES', true );
}
// Set a header advertising the cache engine
header( 'X-Redis-Page-Cache: Redis Page Cache for WordPress by Erick Hitter (http://eth.pw/rpc)', true );
try {
// Establish connection with Redis server
$redis = redis_page_cache_connect_redis();
// Whether we need to load WP
$load_wp = true;
// Relevant details on the current request
$is_post_request = ( ! empty( $GLOBALS['HTTP_RAW_POST_DATA'] ) || ! empty( $_POST ) );
$is_cache_exempt = (bool) preg_match( "#(wordpress_(logged|sec)|wp\-postpass|comment_author)#", var_export( $_COOKIE, true ) );
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- POST request: " . ( $is_post_request ? 'yes' : 'no' ) . " -->\n";
$redis_page_cache_config['debug_messages'] .= "<!-- Cache exexmpt (logged in, password-protected post, commenter): " . ( $is_cache_exempt ? 'yes' : 'no' ) . " -->\n";
}
// Refresh request, deletes cache: either manual refresh cache by adding ?refresh=secret_string after the URL or somebody posting a comment
if ( redis_page_cache_refresh_has_secret( $redis_page_cache_config['secret_string'] ) || redis_page_cache_request_has_secret( $redis_page_cache_config['secret_string'] ) ) {
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Manual refresh requested -->\n";
}
$redis->del( $redis_page_cache_config['redis_key'] );
// This page is cached, the user isn't exempted from cache, and it isn't a POST request, so let's use the cache
} elseif ( ! $is_post_request && ! $is_cache_exempt && $redis->exists( $redis_page_cache_config['redis_key'] ) ) {
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Serving page from cache -->\n";
}
// Page is served from cache, so we don't need WP
$load_wp = false;
$redis_page_cache_config['cached'] = true;
// Retrieve cached page, which is an array that includes meta data along with the page output
$cache = unserialize( $redis->get( $redis_page_cache_config['redis_key'] ) );
// Set headers related to content type
header( 'Content-Type: ' . $cache['content_type'] . '; charset=' . $cache['content_encoding'], true );
// Output cached headers from original page
if ( ! empty( $cache['headers'] ) ) {
foreach ( $cache['headers'] as $key => $value ) {
header( "{$key}: {$value}", true );
}
}
// Output cache headers if desired
if ( $redis_page_cache_config['cache_headers'] ) {
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $cache['time'] ) . ' GMT', true );
header( 'Cache-Control: max-age=' . $cache['age'] . ', must-revalidate', false );
}
// Output page content
echo trim( $cache['output'] );
// Display generation stats if requested
if ( 'application/xml' !== $cache['content_type'] && $redis_page_cache_config['stats'] ) {
echo "\n<!-- Page cached via Redis using the Redis Page Cache plugin (http://eth.pw/rpc). -->";
echo "\n<!-- Retrieved from cache in " . redis_page_cache_time_elapsed( $start, microtime() ) . " seconds. -->";
}
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Last Modified: " . gmdate( 'D, d M Y H:i:s', $cache['time'] ) . " GMT . -->\n";
$redis_page_cache_config['debug_messages'] .= "<!-- Max Age: " . $cache['age'] . " -->\n";
}
// If the cache does not exist lets display the user the normal page without cache, and then fetch a new cache page
} elseif ( $_SERVER['REMOTE_ADDR'] != $redis_page_cache_config['server_ip'] ) {
if ( false === strstr( $redis_page_cache_config['current_url'], 'preview=true' ) ) {
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Displaying page without cache -->\n";
}
// If user isn't exempt from caching and this isn't a post request, render the requested page and cache if appropriate.
if ( ! $is_post_request && ! $is_cache_exempt ) {
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Adding page to cache -->\n";
}
// We load WP to generate the cached output, so no need to load again
$load_wp = false;
// Render page into an output buffer and display
ob_start();
require_once dirname( __FILE__ ) . '/wp-blog-header.php';
$output = trim( ob_get_clean() );
echo $output;
// Display generation stats if requested
if ( ! is_feed() && $redis_page_cache_config['stats'] ) {
echo "\n<!-- Page NOT cached via Redis using the Redis Page Cache plugin (http://eth.pw/rpc). -->";
echo "\n<!-- Generated and cached in " . redis_page_cache_time_elapsed( $start, microtime() ) . " seconds. -->";
}
// Cache rendered page if appropriate
if ( ! is_404() && ! is_search() ) {
// Default cache payload
$cache = array(
'output' => $output,
'time' => time(),
'age' => 31536000, // one year in seconds
'content_type' => is_feed() ? 'application/xml' : 'text/html',
'content_encoding' => get_option( 'blog_charset', 'UTF-8' ),
'headers' => array(),
);
// Minify cached content
if ( $redis_page_cache_config['minify'] ) {
$search = array( '#\>[^\S ]+#s', '#[^\S ]+\<#s', '#(\s)+#s' );
$replace = array( '>', '<', '\\1' );
$cache['output'] = preg_replace( $search, $replace, $cache['output'] );
}
// Capture certain headers
// Props to @andy and Batcache (http://wordpress.org/plugins/batcache/) for this code
if ( ! empty( $redis_page_cache_config['additional_headers' ] ) ) {
if ( function_exists( 'headers_list' ) ) {
foreach ( headers_list() as $header ) {
list( $key, $value ) = array_map( 'trim', explode( ':', $header, 2 ) );
$cache['headers'][ $key ] = $value;
}
} elseif ( function_exists( 'apache_response_headers' ) ) {
$cache['headers'] = apache_response_headers();
}
if ( $cache['headers'] ) {
foreach ( $cache['headers'] as $key => $value ) {
if ( ! in_array( strtolower( $key ), $redis_page_cache_config['additional_headers' ] ) )
unset( $cache['headers'][$key] );
}
}
unset( $key );
unset( $value );
}
// Is unlimited cache life requested?
if ( ! isset( $redis_page_cache_config['unlimited'] ) ) {
$redis_page_cache_config['unlimited'] = (bool) get_option( 'redis-page-cache-debug', false );
}
// Cache the page for the chosen duration
if ( $redis_page_cache_config['unlimited'] ) {
$redis->set( $redis_page_cache_config['redis_key'], serialize( $cache ) );
} else {
if ( ! isset( $redis_page_cache_config['cache_duration'] ) ) {
$redis_page_cache_config['cache_duration'] = (int) get_option( 'redis-page-cache-seconds', 43200 );
}
if ( ! is_numeric( $redis_page_cache_config['cache_duration'] ) ) {
$redis_page_cache_config['cache_duration'] = 43200;
}
$cache['age'] = $redis_page_cache_config['cache_duration'];
$redis->setex( $redis_page_cache_config['redis_key'], $redis_page_cache_config['cache_duration'], serialize( $cache ) );
}
}
}
}
}
// The current request wasn't served from cache or isn't cacheable, so we pass off to WP
if ( $load_wp ) {
require_once dirname( __FILE__ ) . '/wp-blog-header.php';
}
} catch ( Exception $e ) {
require_once dirname( __FILE__ ) . '/wp-blog-header.php';
redis_page_cache_exception_handler( $e );
}
/**
* DEBUGGING OUTPUT
*/
if ( $redis_page_cache_config['debug'] ) {
$redis_page_cache_config['debug_messages'] .= "<!-- Redis Page Cache by Erick Hitter (http://eth.pw/rpc). Page generated in " . redis_page_cache_time_elapsed( $start, microtime() ) . " seconds. -->\n";
$redis_page_cache_config['debug_messages'] .= "<!-- Cache key: " . $redis_page_cache_config['redis_key'] . " -->\n";
$redis_page_cache_config['debug_messages'] .= "<!-- Cached URL: " . redis_page_cache_get_clean_url() . " -->\n";
if ( isset( $redis_page_cache_config['unlimited'] ) && $redis_page_cache_config['unlimited'] ) {
$cache_duration = 'infinite';
} elseif ( isset( $redis_page_cache_config['cache_duration'] ) ) {
$cache_duration = $redis_page_cache_config['cache_duration'];
} else {
$cache_duration = 'unknown';
}
$redis_page_cache_config['debug_messages'] .= "<!-- Cache duration in seconds: " . $cache_duration . " -->\n";
$redis_page_cache_config['debug_messages'] .= "<!-- Server IP: " . $redis_page_cache_config['server_ip'] . " -->\n";
echo "\n" . $redis_page_cache_config['debug_messages'];
}