fix: address Phase 1 code review findings
- Serve cache via template_redirect so feeds/404/REST are excluded and query conditionals work - Bypass cache on query strings; key files by md5(host+path) to avoid collisions and support multisite - Write wp-content/wp-recache-cache.php drop-in server and set WP_CACHE in wp-config.php so advanced-cache.php actually runs - Only remove advanced-cache.php on deactivation when it is ours - Respect DONOTCACHEPAGE, non-200 responses; bots read but never warm cache - Fix comment purge (comment_post_ID) and heartbeat purge ($data arg) - Fix exclude URLs textarea parsing; purge all when exclusions change - Enqueue admin purge script on every admin-bar page, not just settings - Sync tests, add TESTING.md manual test guide
This commit is contained in:
+81
-1
@@ -15,7 +15,9 @@ class WP_Recache_Activator {
|
||||
public static function activate() {
|
||||
self::create_cache_directory();
|
||||
self::set_default_options();
|
||||
self::create_cache_server();
|
||||
self::create_advanced_cache();
|
||||
self::set_wp_cache_constant(true);
|
||||
self::flush_rewrite_rules();
|
||||
}
|
||||
|
||||
@@ -29,7 +31,7 @@ class WP_Recache_Activator {
|
||||
|
||||
$htaccess = WP_RECACHE_CACHE_PATH . '.htaccess';
|
||||
if (!file_exists($htaccess)) {
|
||||
file_put_contents($htaccess, "Deny from all\n");
|
||||
file_put_contents($htaccess, "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n");
|
||||
}
|
||||
|
||||
$index = WP_RECACHE_CACHE_PATH . 'index.php';
|
||||
@@ -59,6 +61,50 @@ class WP_Recache_Activator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the standalone cache server required by the advanced-cache.php
|
||||
* drop-in. Must run without WordPress loaded.
|
||||
*/
|
||||
private static function create_cache_server() {
|
||||
$content = '<?php
|
||||
/**
|
||||
* WP Recache early cache serving.
|
||||
*
|
||||
* This file is automatically generated by WP Recache.
|
||||
* Do not edit this file manually.
|
||||
*/
|
||||
|
||||
if (($_SERVER[\'REQUEST_METHOD\'] ?? \'GET\') !== \'GET\') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($_SERVER[\'QUERY_STRING\'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($_COOKIE as $name => $value) {
|
||||
if (strpos($name, \'wordpress_logged_in\') === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$uri = isset($_SERVER[\'REQUEST_URI\']) ? (string) parse_url($_SERVER[\'REQUEST_URI\'], PHP_URL_PATH) : \'/\';
|
||||
$host = $_SERVER[\'HTTP_HOST\'] ?? \'\';
|
||||
$file = WP_CONTENT_DIR . \'/cache/wp-recache/\' . md5($host . $uri) . \'.html\';
|
||||
|
||||
// With separate mobile cache enabled, files carry a -desktop/-mobile suffix,
|
||||
// so the plain file never exists and serving falls through to WordPress.
|
||||
if (file_exists($file)) {
|
||||
header(\'X-WP-Recache: HIT\');
|
||||
header(\'X-WP-Recache-Dropin: 1\');
|
||||
readfile($file);
|
||||
exit;
|
||||
}
|
||||
';
|
||||
|
||||
file_put_contents(WP_CONTENT_DIR . '/wp-recache-cache.php', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create advanced-cache.php drop-in.
|
||||
*/
|
||||
@@ -87,6 +133,40 @@ if (file_exists(WP_CONTENT_DIR . \'/wp-recache-cache.php\')) {
|
||||
file_put_contents($advanced_cache, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove the WP_CACHE constant in wp-config.php.
|
||||
*
|
||||
* Without it, WordPress never loads advanced-cache.php.
|
||||
*
|
||||
* @param bool $enable True to add the constant, false to remove it.
|
||||
*/
|
||||
public static function set_wp_cache_constant($enable) {
|
||||
$marker = "define('WP_CACHE', true); // Added by WP Recache.\n";
|
||||
$config_path = ABSPATH . 'wp-config.php';
|
||||
|
||||
if (!file_exists($config_path)) {
|
||||
$config_path = dirname(ABSPATH) . '/wp-config.php';
|
||||
}
|
||||
|
||||
if (!file_exists($config_path) || !is_writable($config_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($config_path);
|
||||
|
||||
if ($enable) {
|
||||
if (strpos($content, $marker) !== false || preg_match("/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*true\s*\)/", $content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = preg_replace('/^<\?php/', "<?php\n" . $marker, $content, 1);
|
||||
} else {
|
||||
$content = str_replace($marker, '', $content);
|
||||
}
|
||||
|
||||
file_put_contents($config_path, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules.
|
||||
*/
|
||||
|
||||
@@ -19,11 +19,9 @@ class WP_Recache_Admin {
|
||||
add_action('wp_ajax_wp_recache_purge_all', [$this, 'ajax_purge_all']);
|
||||
add_action('wp_ajax_wp_recache_purge_url', [$this, 'ajax_purge_url']);
|
||||
add_action('admin_notices', [$this, 'admin_notices']);
|
||||
|
||||
wp_localize_script('jquery', 'wpRecache', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wp_recache_nonce'),
|
||||
]);
|
||||
add_action('admin_enqueue_scripts', 'wp_recache_enqueue_admin_bar_script');
|
||||
// New exclusions must not leave stale files that the drop-in would still serve.
|
||||
add_action('update_option_wp_recache_exclude_urls', 'wp_recache_delete_all_cache');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,11 +91,13 @@ class WP_Recache_Admin {
|
||||
* @return array Sanitized URLs.
|
||||
*/
|
||||
public function sanitize_exclude_urls($input) {
|
||||
if (!is_array($input)) {
|
||||
return [];
|
||||
if (is_array($input)) {
|
||||
$input = implode("\n", $input);
|
||||
}
|
||||
|
||||
return array_map('sanitize_text_field', $input);
|
||||
$lines = array_filter(array_map('trim', explode("\n", (string) $input)));
|
||||
|
||||
return array_map('sanitize_text_field', $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,6 +110,10 @@ class WP_Recache_Admin {
|
||||
|
||||
$active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'cache';
|
||||
|
||||
if (!in_array($active_tab, ['cache', 'optimize', 'advanced'], true)) {
|
||||
$active_tab = 'cache';
|
||||
}
|
||||
|
||||
include WP_RECACHE_INC_PATH . 'Admin/views/settings.php';
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ $stats = WP_Recache_Admin::get_cache_stats();
|
||||
<tr>
|
||||
<th scope="row">Exclude URLs</th>
|
||||
<td>
|
||||
<textarea name="wp_recache_exclude_urls[]" rows="5" cols="50" class="large-text"><?php echo esc_textarea(implode("\n", get_option('wp_recache_exclude_urls', ['/cart', '/checkout', '/my-account']))); ?></textarea>
|
||||
<textarea name="wp_recache_exclude_urls" rows="5" cols="50" class="large-text"><?php echo esc_textarea(implode("\n", get_option('wp_recache_exclude_urls', ['/cart', '/checkout', '/my-account']))); ?></textarea>
|
||||
<p class="description">One URL per line. These pages will not be cached.</p>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -131,23 +131,3 @@ $stats = WP_Recache_Admin::get_cache_stats();
|
||||
<?php submit_button('Save Settings'); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function wpRecachePurgeAll() {
|
||||
if (!confirm('Are you sure you want to purge all cache?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
jQuery.post(wpRecache.ajaxUrl, {
|
||||
action: 'wp_recache_purge_all',
|
||||
nonce: wpRecache.nonce
|
||||
}, function(response) {
|
||||
if (response.success) {
|
||||
alert('Cache purged successfully');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + response.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,17 +9,26 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class WP_Recache_WPCache {
|
||||
|
||||
/**
|
||||
* Whether this request started our output buffer.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $buffer_started = false;
|
||||
|
||||
/**
|
||||
* Initialize cache hooks.
|
||||
*/
|
||||
public function init() {
|
||||
add_action('init', [$this, 'start_output_buffering']);
|
||||
// template_redirect: query conditionals (is_feed/is_404/is_preview) are
|
||||
// available here, and REST/AJAX requests never reach it.
|
||||
add_action('template_redirect', [$this, 'start_output_buffering'], 0);
|
||||
add_action('shutdown', [$this, 'end_output_buffering']);
|
||||
|
||||
add_action('save_post', [$this, 'purge_post_cache']);
|
||||
add_action('wp_insert_comment', [$this, 'purge_post_cache']);
|
||||
add_action('wp_insert_comment', [$this, 'purge_comment_cache']);
|
||||
|
||||
add_filter('heartbeat_received', [$this, 'purge_cache_onheartbeat']);
|
||||
add_filter('heartbeat_received', [$this, 'purge_cache_onheartbeat'], 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,8 +63,14 @@ class WP_Recache_WPCache {
|
||||
}
|
||||
}
|
||||
|
||||
// Bots get served warm cache but never warm it themselves.
|
||||
if (wp_recache_is_robots()) {
|
||||
return;
|
||||
}
|
||||
|
||||
header('X-WP-Recache: MISS');
|
||||
ob_start([$this, 'cache_output']);
|
||||
$this->buffer_started = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +84,20 @@ class WP_Recache_WPCache {
|
||||
return $output;
|
||||
}
|
||||
|
||||
// Respect opt-outs from WordPress and third-party plugins.
|
||||
if (defined('DONOTCACHEPAGE') && DONOTCACHEPAGE) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
$status = http_response_code();
|
||||
if ($status !== false && $status !== 200) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
if (wp_recache_is_robots()) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
$url = $this->get_current_url();
|
||||
$cache_path = wp_recache_get_cache_path_with_device($url);
|
||||
$cache_dir = dirname($cache_path);
|
||||
@@ -86,7 +115,7 @@ class WP_Recache_WPCache {
|
||||
* End output buffering.
|
||||
*/
|
||||
public function end_output_buffering() {
|
||||
if (ob_get_level()) {
|
||||
if ($this->buffer_started && ob_get_level()) {
|
||||
ob_end_flush();
|
||||
}
|
||||
}
|
||||
@@ -106,15 +135,30 @@ class WP_Recache_WPCache {
|
||||
wp_recache_delete_cache(home_url('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge cache for the post a comment belongs to.
|
||||
*
|
||||
* @param int $comment_id Comment ID.
|
||||
*/
|
||||
public function purge_comment_cache($comment_id) {
|
||||
$comment = get_comment($comment_id);
|
||||
|
||||
if ($comment) {
|
||||
$this->purge_post_cache($comment->comment_post_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge cache on heartbeat.
|
||||
*
|
||||
* @param array $response Heartbeat response.
|
||||
* @param array $data Data sent with the heartbeat request.
|
||||
* @return array Modified response.
|
||||
*/
|
||||
public function purge_cache_onheartbeat($response) {
|
||||
if (isset($response['wp_recache_purge'])) {
|
||||
public function purge_cache_onheartbeat($response, $data) {
|
||||
if (isset($data['wp_recache_purge'])) {
|
||||
wp_recache_delete_all_cache();
|
||||
$response['wp_recache_purged'] = true;
|
||||
}
|
||||
|
||||
return $response;
|
||||
@@ -127,8 +171,8 @@ class WP_Recache_WPCache {
|
||||
*/
|
||||
private function get_current_url() {
|
||||
$protocol = is_ssl() ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
|
||||
return $protocol . '://' . $host . $uri;
|
||||
}
|
||||
|
||||
+25
-27
@@ -58,11 +58,12 @@ function wp_recache_should_exclude() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (wp_recache_is_robots()) {
|
||||
// Dynamic query-string requests are never cached (story 11).
|
||||
if (!empty($_SERVER['QUERY_STRING'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_preview()) {
|
||||
if (is_preview() || is_feed() || is_404()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -93,40 +94,22 @@ function wp_recache_is_robots() {
|
||||
* Check if request is a POST action.
|
||||
*/
|
||||
function wp_recache_is_post_action() {
|
||||
return $_SERVER['REQUEST_METHOD'] === 'POST';
|
||||
return ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache file path for URL.
|
||||
*
|
||||
* @param string $url URL to cache.
|
||||
*/
|
||||
function wp_recache_get_cache_path($url) {
|
||||
$path = wp_parse_url($url, PHP_URL_PATH);
|
||||
$path = trim($path, '/');
|
||||
|
||||
if (empty($path)) {
|
||||
$path = 'index';
|
||||
}
|
||||
|
||||
$path = sanitize_file_name($path);
|
||||
|
||||
return WP_RECACHE_CACHE_PATH . $path . '.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for URL.
|
||||
* Keyed by md5(host + path) so nested paths, hosts and multisite
|
||||
* sub-directories never collide. Query strings are bypassed upstream.
|
||||
*
|
||||
* @param string $url URL to cache.
|
||||
*/
|
||||
function wp_recache_get_cache_key($url) {
|
||||
$key = wp_parse_url($url, PHP_URL_PATH);
|
||||
function wp_recache_get_cache_path($url) {
|
||||
$host = (string) wp_parse_url($url, PHP_URL_HOST);
|
||||
$path = (string) wp_parse_url($url, PHP_URL_PATH);
|
||||
|
||||
if (empty($key)) {
|
||||
$key = '/';
|
||||
}
|
||||
|
||||
return md5($key);
|
||||
return WP_RECACHE_CACHE_PATH . md5($host . $path) . '.html';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,3 +197,18 @@ function wp_recache_get_cache_path_with_device($url) {
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the admin-bar purge script (admin and front-end admin bar).
|
||||
*/
|
||||
function wp_recache_enqueue_admin_bar_script() {
|
||||
if (!is_admin_bar_showing() || !current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script('wp-recache-admin', WP_RECACHE_URL . 'assets/admin.js', ['jquery'], WP_RECACHE_VERSION, true);
|
||||
wp_localize_script('wp-recache-admin', 'wpRecache', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wp_recache_nonce'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -14,20 +14,38 @@ class WP_Recache_Deactivator {
|
||||
*/
|
||||
public static function deactivate() {
|
||||
self::remove_advanced_cache();
|
||||
self::remove_cache_server();
|
||||
WP_Recache_Activator::set_wp_cache_constant(false);
|
||||
self::flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove advanced-cache.php drop-in.
|
||||
* Remove advanced-cache.php drop-in, but only if it is ours.
|
||||
*/
|
||||
private static function remove_advanced_cache() {
|
||||
$advanced_cache = WP_CONTENT_DIR . '/advanced-cache.php';
|
||||
|
||||
if (file_exists($advanced_cache)) {
|
||||
if (!file_exists($advanced_cache)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($advanced_cache);
|
||||
if (strpos($content, 'WP Recache') !== false) {
|
||||
unlink($advanced_cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the standalone cache server.
|
||||
*/
|
||||
private static function remove_cache_server() {
|
||||
$server = WP_CONTENT_DIR . '/wp-recache-cache.php';
|
||||
|
||||
if (file_exists($server)) {
|
||||
unlink($server);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user