index-redis-page-cache.php 12 KB
Newer Older
Benjamin Adams's avatar
Benjamin Adams committed
1
<?php
2
/**
3
 * Redis Page Cache
4
5
6
7
8
 */

/**
 * GLOBAL CONFIGURATION
 */
9
global $redis_page_cache_config;
10

11
$redis_page_cache_config = array(
12
13
	'debug'          => false,
	'debug_messages' => '',
14
	'stats'          => false,
15
16
17
18
19
20
	'cache'          => false,
	'server_ip'      => '127.0.0.1',
	'redis_server'   => '127.0.0.1',
	'redis_port'     => 6379,
	'redis_db'       => 0,
	'secret_string'  => 'changeme',
21
22
);

23
// Uncomment either option below to fix the values here and disable the admin UI
24
25
// $redis_page_cache_config['cache_duration'] = 43200;
// $redis_page_cache_config['unlimited']      = false;
26

27
// Modify this function to introduce custom handling when exceptions occur
28
function redis_page_cache_exception_handler( $exception ) {
29
30
31
	return;
}

32
/**
33
34
 * END GLOBAL CONFIGURATION
 *
35
36
 * DO NOT EDIT BELOW THIS LINE!
 */
37
38
$redis_page_cache_config['current_url'] = redis_page_cache_get_clean_url();
$redis_page_cache_config['redis_key']   = md5( $redis_page_cache_config['current_url'] );
Benjamin Adams's avatar
Benjamin Adams committed
39

Ulrich Block's avatar
Ulrich Block committed
40
// Start the timer so we can track the page load time
41
if ( $redis_page_cache_config['debug'] || $redis_page_cache_config['stats'] ) {
42
43
	$start = microtime();
}
Ulrich Block's avatar
Ulrich Block committed
44

45
/**
46
 * SET SEPARATE CACHES FOR BROAD DEVICE TYPES
47
 */
48
$redis_page_cache_config['redis_key'] = redis_page_cache_set_device_key( $redis_page_cache_config['redis_key'] );
49

50
51
52
53
54
55
56
57
58
/**
 * UTILITY FUNCTIONS
 */

/**
 * Compute microtime from a timestamp
 *
 * @return float
 */
59
function redis_page_cache_get_micro_time( $time ) {
Erick Hitter's avatar
Erick Hitter committed
60
61
	list( $usec, $sec ) = explode( " ", $time );
	return ( (float) $usec + (float) $sec );
Ulrich Block's avatar
Ulrich Block committed
62
63
}

64
65
66
67
68
69
70
71
/**
 * Count seconds elapsed between two microtime() timestampes
 *
 * @param string $start
 * @param string $end
 * @param int $precision
 * @return float
 */
72
73
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 );
74
75
}

76
77
78
79
80
/**
 * Is the current request a refresh request with the correct secret key?
 *
 * @return bool
 */
81
function redis_page_cache_refresh_has_secret( $secret ) {
Erick Hitter's avatar
Erick Hitter committed
82
	return isset( $_GET['refresh'] ) && $secret == $_GET['refresh'];
Benjamin Adams's avatar
merged    
Benjamin Adams committed
83
}
Ulrich Block's avatar
Ulrich Block committed
84

85
86
87
88
89
/**
 * Does current request include a refresh request?
 *
 * @return bool
 */
90
function redis_page_cache_request_has_secret( $secret ) {
Erick Hitter's avatar
Erick Hitter committed
91
	return false !== strpos( $_SERVER['REQUEST_URI'], "refresh=${secret}" );
Benjamin Adams's avatar
merged    
Benjamin Adams committed
92
}
Hendrik Klemp's avatar
Hendrik Klemp committed
93

94
95
96
97
98
/**
 * Set proper IP address for proxied requests
 *
 * @return null
 */
99
function redis_page_cache_handle_cdn_remote_addressing() {
Erick Hitter's avatar
Erick Hitter committed
100
	// so we don't confuse the cloudflare server
Erick Hitter's avatar
Erick Hitter committed
101
	if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
Erick Hitter's avatar
Erick Hitter committed
102
103
		$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
	}
Benjamin Adams's avatar
merged    
Benjamin Adams committed
104
}
105

106
107
108
/**
 * Prepare a URL for use as a cache key
 *
Erick Hitter's avatar
Erick Hitter committed
109
 * If the URL is too malformed to parse, a one-time cache is set using microtime().
110
111
112
 *
 * @return string
 */
113
function redis_page_cache_get_clean_url() {
Erick Hitter's avatar
Erick Hitter committed
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
	$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
129
}
Benjamin Adams's avatar
merged    
Benjamin Adams committed
130

131
/**
132
133
134
135
136
 * Prefix cache key if device calls for separate caching
 *
 * @param string $key
 * @return $string
 */
137
138
function redis_page_cache_set_device_key( $key ) {
	switch ( redis_page_cache_get_device_type() ) {
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
		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
157
 *
158
 * @return string
159
 */
160
function redis_page_cache_get_device_type() {
Erick Hitter's avatar
Erick Hitter committed
161
	$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
162
163
164
165
166
167
168
169
170
171
172
173
174

	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'   ) ||
175
		false !== stripos( $ua, 'RIM Tablet' )
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
	) {
		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';
192
193
}

194
/**
195
196
197
198
199
 * Establish a connection to the Redis server
 *
 * Will try the PECL module first, then fall back to PRedis
 *
 * @return object
200
 */
201
202
function redis_page_cache_connect_redis() {
	global $redis_page_cache_config;
203

Erick Hitter's avatar
Erick Hitter committed
204
	// check if PECL Extension is available
Erick Hitter's avatar
Erick Hitter committed
205
	if ( class_exists( 'Redis' ) ) {
206
207
		if ( $redis_page_cache_config['debug'] ) {
			$redis_page_cache_config['debug_messages'] .= "<!-- Redis PECL module found -->\n";
Erick Hitter's avatar
Erick Hitter committed
208
		}
Erick Hitter's avatar
Erick Hitter committed
209

Erick Hitter's avatar
Erick Hitter committed
210
		$redis = new Redis();
211
		$redis->connect( $redis_page_cache_config['redis_server'], $redis_page_cache_config['redis_port'] );
212
213

		// Default DB is 0, so only need to SELECT if other
214
215
		if ( $redis_page_cache_config['redis_db'] ) {
			$redis->select( $redis_page_cache_config['redis_db'] );
216
		}
217
218
	// Fallback to predis5.2.php
	} else {
219
220
		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
221
		}
Erick Hitter's avatar
Erick Hitter committed
222

223
		include_once dirname( __FILE__ ) . '/wp-content/plugins/wp-redis-cache/predis5.2.php'; //we need this to use Redis inside of PHP
224
		$redis = array(
225
226
			'host' => $redis_page_cache_config['redis_server'],
			'port' => $redis_page_cache_config['redis_port'],
227
228
229
		);

		// Default DB is 0, so only need to SELECT if other
230
231
		if ( $redis_page_cache_config['redis_db'] ) {
			$redis['database'] = $redis_page_cache_config['redis_db'];
232
233
234
		}

		$redis = new Predis_Client( $redis );
Erick Hitter's avatar
Erick Hitter committed
235
236
	}

237
238
239
240
241
242
243
244
	return $redis;
}

/**
 * BEGIN CACHING LOGIC
 */

// Set proper IP for proxied requests
245
redis_page_cache_handle_cdn_remote_addressing();
246
247
248
249
250
251
252
253

// 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
254
	$redis = redis_page_cache_connect_redis();
255

256
257
258
	// Whether we need to load WP
	$load_wp = true;

259
260
261
262
	// 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 ) );

263
264
265
	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";
266
267
268
	}

	// Refresh request, deletes cache: either manual refresh cache by adding ?refresh=secret_string after the URL or somebody posting a comment
269
270
271
	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
272
		}
Erick Hitter's avatar
Erick Hitter committed
273

274
		$redis->del( $redis_page_cache_config['redis_key'] );
275
	// This page is cached, the user isn't logged in, and it isn't a POST request, so let's use the cache
276
277
278
	} 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";
279
280
		}

281
282
		// Page is served from cache, so we don't need WP
		$load_wp = false;
283
		$redis_page_cache_config['cached'] = true;
284

285
		echo trim( $redis->get( $redis_page_cache_config['redis_key'] ) );
286
287

		// Display generation stats if requested
288
289
290
		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. -->";
291
		}
Erick Hitter's avatar
Erick Hitter committed
292
	// If the cache does not exist lets display the user the normal page without cache, and then fetch a new cache page
293
294
295
296
	} 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";
297
			}
298

299
300
301
302
303
304
305
			// 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();
306
				require_once dirname( __FILE__ ) . '/wp-blog-header.php';
307
308
309
				$markup_to_cache = trim( ob_get_clean() );
				echo $markup_to_cache;

310
				// Display generation stats if requested
311
312
313
				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. -->";
314
315
				}

316
317
318
				// Cache rendered page if appropriate
				if ( ! is_404() && ! is_search() ) {
					// Is unlimited cache life requested?
319
320
					if ( isset( $redis_page_cache_config['unlimited'] ) ) {
						$unlimited = $redis_page_cache_config['unlimited'];
321
					} else {
322
						$unlimited = (bool) get_option( 'wp-redis-cache-debug', false );
323
						$redis_page_cache_config['unlimited'] = $unlimited;
324
325
					}

326
327
					// Cache the page for the chosen duration
					if ( $unlimited ) {
328
						$redis->set( $redis_page_cache_config['redis_key'], $markup_to_cache );
329
					} else {
330
331
						if ( isset( $redis_page_cache_config['cache_duration'] ) ) {
							$cache_duration = $redis_page_cache_config['cache_duration'];
332
333
						} else {
							$cache_duration = (int) get_option( 'wp-redis-cache-seconds', 43200 );
334
							$redis_page_cache_config['cache_duration'] = $cache_duration;
335
336
337
						}

						if ( ! is_numeric( $cache_duration ) ) {
338
							$cache_duration = $redis_page_cache_config['cache_duration'] = 43200;
339
340
						}

341
						$redis->setex( $redis_page_cache_config['redis_key'], $cache_duration, $markup_to_cache );
342
					}
Erick Hitter's avatar
Erick Hitter committed
343
				}
344
345
346
			}
		}
	}
347
348
349

	// The current request wasn't served from cache or isn't cacheable, so we pass off to WP
	if ( $load_wp ) {
350
		require_once dirname( __FILE__ ) . '/wp-blog-header.php';
Erick Hitter's avatar
Erick Hitter committed
351
	}
Erick Hitter's avatar
Erick Hitter committed
352
} catch ( Exception $e ) {
353
	require_once dirname( __FILE__ ) . '/wp-blog-header.php';
354
	redis_page_cache_exception_handler( $e );
Benjamin Adams's avatar
Benjamin Adams committed
355
356
}

357
358
359
/**
 * DEBUGGING OUTPUT
 */
360
if ( $redis_page_cache_config['debug'] ) {
361
	$end  = microtime();
362
363
364
365
366
367
	$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";
	$redis_page_cache_config['debug_messages'] .= "<!-- wp-redis-cache-key = " . $redis_page_cache_config['redis_key'] . "-->\n";
	if ( isset( $redis_page_cache_config['cache_duration'] ) ) {
		$redis_page_cache_config['debug_messages'] .= "<!-- wp-redis-cache-seconds = " . $redis_page_cache_config['cache_duration'] . " -->\n";
Erick Hitter's avatar
Erick Hitter committed
368
	}
369
370
371
	$redis_page_cache_config['debug_messages'] .= "<!-- wp-redis-cache-ip = " . $redis_page_cache_config['server_ip'] . "-->\n";
	if ( isset( $redis_page_cache_config['unlimited'] ) ) {
		$redis_page_cache_config['debug_messages'] .= "<!-- wp-redis-cache-unlimited = " . $redis_page_cache_config['unlimited'] . "-->\n";
Erick Hitter's avatar
Erick Hitter committed
372
	}
373
	$redis_page_cache_config['debug_messages'] .= "<!-- wp-redis-cache-debug = " . $redis_page_cache_config['debug'] . "-->\n";
374

375
	echo $redis_page_cache_config['debug_messages'];
Benjamin Adams's avatar
Benjamin Adams committed
376
}