index-redis-page-cache.php 12.8 KB
Newer Older
Benjamin Adams's avatar
Benjamin Adams committed
1
<?php
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
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
*/
25
26
27
28

/**
 * GLOBAL CONFIGURATION
 */
29
global $redis_page_cache_config;
30

31
$redis_page_cache_config = array(
32
33
	'debug'          => false,
	'debug_messages' => '',
34
	'stats'          => false,
35
	'cached'         => false,
36
37
38
39
	'server_ip'      => '127.0.0.1',
	'redis_server'   => '127.0.0.1',
	'redis_port'     => 6379,
	'redis_db'       => 0,
40
	'cache_version'  => 1,
41
	'secret_string'  => 'changeme',
42
43
);

44
// Uncomment either option below to fix the values here and disable the admin UI
45
46
// $redis_page_cache_config['cache_duration'] = 43200;
// $redis_page_cache_config['unlimited']      = false;
47

48
// Modify this function to introduce custom handling when exceptions occur
49
function redis_page_cache_exception_handler( $exception ) {
50
51
52
	return;
}

53
/**
54
55
 * END GLOBAL CONFIGURATION
 *
56
57
 * DO NOT EDIT BELOW THIS LINE!
 */
58
$redis_page_cache_config['current_url'] = redis_page_cache_get_clean_url();
59
$redis_page_cache_config['redis_key']   = md5( 'v' . $redis_page_cache_config['cache_version'] . '-' . $redis_page_cache_config['current_url'] );
Benjamin Adams's avatar
Benjamin Adams committed
60

Ulrich Block's avatar
Ulrich Block committed
61
// Start the timer so we can track the page load time
62
if ( $redis_page_cache_config['debug'] || $redis_page_cache_config['stats'] ) {
63
64
	$start = microtime();
}
Ulrich Block's avatar
Ulrich Block committed
65

66
/**
67
 * SET SEPARATE CACHES FOR BROAD DEVICE TYPES
68
 */
69
$redis_page_cache_config['redis_key'] = redis_page_cache_set_device_key( $redis_page_cache_config['redis_key'] );
70

71
72
73
74
75
76
77
78
79
/**
 * UTILITY FUNCTIONS
 */

/**
 * Compute microtime from a timestamp
 *
 * @return float
 */
80
function redis_page_cache_get_micro_time( $time ) {
Erick Hitter's avatar
Erick Hitter committed
81
82
	list( $usec, $sec ) = explode( " ", $time );
	return ( (float) $usec + (float) $sec );
Ulrich Block's avatar
Ulrich Block committed
83
84
}

85
86
87
88
89
90
91
92
/**
 * Count seconds elapsed between two microtime() timestampes
 *
 * @param string $start
 * @param string $end
 * @param int $precision
 * @return float
 */
93
94
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 );
95
96
}

97
98
99
100
101
/**
 * Is the current request a refresh request with the correct secret key?
 *
 * @return bool
 */
102
function redis_page_cache_refresh_has_secret( $secret ) {
Erick Hitter's avatar
Erick Hitter committed
103
	return isset( $_GET['refresh'] ) && $secret == $_GET['refresh'];
Benjamin Adams's avatar
merged    
Benjamin Adams committed
104
}
Ulrich Block's avatar
Ulrich Block committed
105

106
107
108
109
110
/**
 * Does current request include a refresh request?
 *
 * @return bool
 */
111
function redis_page_cache_request_has_secret( $secret ) {
Erick Hitter's avatar
Erick Hitter committed
112
	return false !== strpos( $_SERVER['REQUEST_URI'], "refresh=${secret}" );
Benjamin Adams's avatar
merged    
Benjamin Adams committed
113
}
Hendrik Klemp's avatar
Hendrik Klemp committed
114

115
116
117
118
119
/**
 * Set proper IP address for proxied requests
 *
 * @return null
 */
120
function redis_page_cache_handle_cdn_remote_addressing() {
Erick Hitter's avatar
Erick Hitter committed
121
	// so we don't confuse the cloudflare server
Erick Hitter's avatar
Erick Hitter committed
122
	if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
Erick Hitter's avatar
Erick Hitter committed
123
124
		$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
	}
Benjamin Adams's avatar
merged    
Benjamin Adams committed
125
}
126

127
128
129
/**
 * Prepare a URL for use as a cache key
 *
Erick Hitter's avatar
Erick Hitter committed
130
 * If the URL is too malformed to parse, a one-time cache is set using microtime().
131
132
133
 *
 * @return string
 */
134
function redis_page_cache_get_clean_url() {
Erick Hitter's avatar
Erick Hitter committed
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
	$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 ) {
		$url = $url['scheme'] . '://' . $url['host'] . $url['path'];
	} else {
		$url = microtime();
	}

	return $url;
Benjamin Adams's avatar
Benjamin Adams committed
150
}
Benjamin Adams's avatar
merged    
Benjamin Adams committed
151

152
/**
153
154
155
156
157
 * Prefix cache key if device calls for separate caching
 *
 * @param string $key
 * @return $string
 */
158
159
function redis_page_cache_set_device_key( $key ) {
	switch ( redis_page_cache_get_device_type() ) {
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
		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
178
 *
179
 * @return string
180
 */
181
function redis_page_cache_get_device_type() {
Erick Hitter's avatar
Erick Hitter committed
182
	$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
183
184
185
186
187
188
189
190
191
192
193
194
195

	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'   ) ||
196
		false !== stripos( $ua, 'RIM Tablet' )
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
	) {
		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';
213
214
}

215
/**
216
217
218
219
220
 * Establish a connection to the Redis server
 *
 * Will try the PECL module first, then fall back to PRedis
 *
 * @return object
221
 */
222
223
function redis_page_cache_connect_redis() {
	global $redis_page_cache_config;
224

Erick Hitter's avatar
Erick Hitter committed
225
	// check if PECL Extension is available
Erick Hitter's avatar
Erick Hitter committed
226
	if ( class_exists( 'Redis' ) ) {
227
228
		if ( $redis_page_cache_config['debug'] ) {
			$redis_page_cache_config['debug_messages'] .= "<!-- Redis PECL module found -->\n";
Erick Hitter's avatar
Erick Hitter committed
229
		}
Erick Hitter's avatar
Erick Hitter committed
230

Erick Hitter's avatar
Erick Hitter committed
231
		$redis = new Redis();
232
		$redis->connect( $redis_page_cache_config['redis_server'], $redis_page_cache_config['redis_port'] );
233
234

		// Default DB is 0, so only need to SELECT if other
235
236
		if ( $redis_page_cache_config['redis_db'] ) {
			$redis->select( $redis_page_cache_config['redis_db'] );
237
		}
238
239
	// Fallback to predis5.2.php
	} else {
240
241
		if ( $redis_page_cache_config['debug'] ) {
			$redis_page_cache_config['debug_messages'] .= "<!-- using predis as a backup -->\n";
Erick Hitter's avatar
Erick Hitter committed
242
		}
Erick Hitter's avatar
Erick Hitter committed
243

244
		include_once dirname( __FILE__ ) . '/wp-content/plugins/redis-page-cache/predis5.2.php'; //we need this to use Redis inside of PHP
245
		$redis = array(
246
247
			'host' => $redis_page_cache_config['redis_server'],
			'port' => $redis_page_cache_config['redis_port'],
248
249
250
		);

		// Default DB is 0, so only need to SELECT if other
251
252
		if ( $redis_page_cache_config['redis_db'] ) {
			$redis['database'] = $redis_page_cache_config['redis_db'];
253
254
255
		}

		$redis = new Predis_Client( $redis );
Erick Hitter's avatar
Erick Hitter committed
256
257
	}

258
259
260
261
262
263
264
265
	return $redis;
}

/**
 * BEGIN CACHING LOGIC
 */

// Set proper IP for proxied requests
266
redis_page_cache_handle_cdn_remote_addressing();
267
268
269
270
271
272
273
274

// Ensure WP uses a theme (this is normally set in index.php)
if ( ! defined( 'WP_USE_THEMES' ) ) {
	define( 'WP_USE_THEMES', true );
}

try {
	// Establish connection with Redis server
275
	$redis = redis_page_cache_connect_redis();
276

277
278
279
	// Whether we need to load WP
	$load_wp = true;

280
281
282
283
	// Relevant details on the current request
	$is_post   = (bool) 'POST' === $_SERVER['REQUEST_METHOD'];
	$logged_in = (bool) preg_match( "#(wordpress_(logged|sec)|comment_author)#", var_export( $_COOKIE, true ) );

284
285
286
	if ( $redis_page_cache_config['debug'] ) {
		$redis_page_cache_config['debug_messages'] .= "<!-- POST request: " . ( $is_post ? 'yes' : 'no' ) . "-->\n";
		$redis_page_cache_config['debug_messages'] .= "<!-- Logged in: " . ( $logged_in ? 'yes' : 'no' ) . "-->\n";
287
288
289
	}

	// Refresh request, deletes cache: either manual refresh cache by adding ?refresh=secret_string after the URL or somebody posting a comment
290
291
292
	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 was required -->\n";
Erick Hitter's avatar
Erick Hitter committed
293
		}
Erick Hitter's avatar
Erick Hitter committed
294

295
		$redis->del( $redis_page_cache_config['redis_key'] );
296
	// This page is cached, the user isn't logged in, and it isn't a POST request, so let's use the cache
297
298
299
	} elseif ( ! $is_post && ! $logged_in && $redis->exists( $redis_page_cache_config['redis_key'] ) ) {
		if ( $redis_page_cache_config['debug'] ) {
			$redis_page_cache_config['debug_messages'] .= "<!-- serving page from cache: key: " . $redis_page_cache_config['redis_key'] . " -->\n";
300
301
		}

302
303
		// Page is served from cache, so we don't need WP
		$load_wp = false;
304
		$redis_page_cache_config['cached'] = true;
305

306
		echo trim( $redis->get( $redis_page_cache_config['redis_key'] ) );
307
308

		// Display generation stats if requested
309
310
311
		if ( $redis_page_cache_config['stats'] ) {
			echo "\n<!-- Page cached via Redis using the Redis Page Cache plugin. -->";
			echo "\n<!-- Retrieved from cache in " . redis_page_cache_time_elapsed( $start, microtime() ) . " seconds. -->";
312
		}
Erick Hitter's avatar
Erick Hitter committed
313
	// If the cache does not exist lets display the user the normal page without cache, and then fetch a new cache page
314
315
316
317
	} 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";
318
			}
319

320
321
322
323
324
325
326
			// If user isn't logged in and this isn't a post request, render the requested page and cache if appropriate.
			if ( ! $is_post && ! $logged_in ) {
				// 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();
327
				require_once dirname( __FILE__ ) . '/wp-blog-header.php';
328
329
330
				$markup_to_cache = trim( ob_get_clean() );
				echo $markup_to_cache;

331
				// Display generation stats if requested
332
333
334
				if ( $redis_page_cache_config['stats'] ) {
					echo "\n<!-- Page NOT cached via Redis using the Redis Page Cache plugin. -->";
					echo "\n<!-- Generated and cached in " . redis_page_cache_time_elapsed( $start, microtime() ) . " seconds. -->";
335
336
				}

337
338
339
				// Cache rendered page if appropriate
				if ( ! is_404() && ! is_search() ) {
					// Is unlimited cache life requested?
340
341
					if ( ! isset( $redis_page_cache_config['unlimited'] ) ) {
						$redis_page_cache_config['unlimited'] = (bool) get_option( 'redis-page-cache-debug', false );
342
343
					}

344
					// Cache the page for the chosen duration
345
					if ( $redis_page_cache_config['unlimited'] ) {
346
						$redis->set( $redis_page_cache_config['redis_key'], $markup_to_cache );
347
					} else {
348
349
						if ( ! isset( $redis_page_cache_config['cache_duration'] ) ) {
							$redis_page_cache_config['cache_duration'] = (int) get_option( 'redis-page-cache-seconds', 43200 );
350
351
						}

352
353
						if ( ! is_numeric( $redis_page_cache_config['cache_duration'] ) ) {
							$redis_page_cache_config['cache_duration'] = 43200;
354
355
						}

356
						$redis->setex( $redis_page_cache_config['redis_key'], $redis_page_cache_config['cache_duration'], $markup_to_cache );
357
					}
Erick Hitter's avatar
Erick Hitter committed
358
				}
359
360
361
			}
		}
	}
362
363
364

	// The current request wasn't served from cache or isn't cacheable, so we pass off to WP
	if ( $load_wp ) {
365
		require_once dirname( __FILE__ ) . '/wp-blog-header.php';
Erick Hitter's avatar
Erick Hitter committed
366
	}
Erick Hitter's avatar
Erick Hitter committed
367
} catch ( Exception $e ) {
368
	require_once dirname( __FILE__ ) . '/wp-blog-header.php';
369
	redis_page_cache_exception_handler( $e );
Benjamin Adams's avatar
Benjamin Adams committed
370
371
}

372
373
374
/**
 * DEBUGGING OUTPUT
 */
375
if ( $redis_page_cache_config['debug'] ) {
376
	$end  = microtime();
377
378
379
	$time = redis_page_cache_time_elapsed( $start, $end );
	$redis_page_cache_config['debug_messages'] .= "<!-- Redis Page Cache by Erick Hitter. Page generated in " . $time . " seconds. -->\n";
	$redis_page_cache_config['debug_messages'] .= "<!-- Site was cached = " . $redis_page_cache_config['cached'] . " -->\n";
380
	$redis_page_cache_config['debug_messages'] .= "<!-- redis-page-cache-key = " . $redis_page_cache_config['redis_key'] . "-->\n";
381
	if ( isset( $redis_page_cache_config['cache_duration'] ) ) {
382
		$redis_page_cache_config['debug_messages'] .= "<!-- redis-page-cache-seconds = " . $redis_page_cache_config['cache_duration'] . " -->\n";
Erick Hitter's avatar
Erick Hitter committed
383
	}
384
	$redis_page_cache_config['debug_messages'] .= "<!-- redis-page-cache-ip = " . $redis_page_cache_config['server_ip'] . "-->\n";
385
	if ( isset( $redis_page_cache_config['unlimited'] ) ) {
386
		$redis_page_cache_config['debug_messages'] .= "<!-- redis-page-cache-unlimited = " . $redis_page_cache_config['unlimited'] . "-->\n";
Erick Hitter's avatar
Erick Hitter committed
387
	}
388
	$redis_page_cache_config['debug_messages'] .= "<!-- redis-page-cache-debug = " . $redis_page_cache_config['debug'] . "-->\n";
389

390
	echo $redis_page_cache_config['debug_messages'];
Benjamin Adams's avatar
Benjamin Adams committed
391
}