diff --git a/includes/Activator.php b/includes/Activator.php index 72b0bad..856ead6 100644 --- a/includes/Activator.php +++ b/includes/Activator.php @@ -48,6 +48,13 @@ class WP_Recache_Activator { 'cache_enabled' => true, 'separate_mobile_cache' => false, 'exclude_urls' => ['/cart', '/checkout', '/my-account'], + 'rejected_cookies' => [ + 'woocommerce_items_in_cart', + 'wp_woocommerce_session_*', + 'wp-postpass', + 'comment_author_*', + 'wordpress_*', + ], 'minify_css' => false, 'minify_js' => false, 'lazyload_images' => false, @@ -64,8 +71,20 @@ class WP_Recache_Activator { /** * Write the standalone cache server required by the advanced-cache.php * drop-in. Must run without WordPress loaded. + * + * Public so Admin can regenerate the drop-in when options change. */ - private static function create_cache_server() { + public static function create_cache_server() { + $rejected_cookies = get_option('wp_recache_rejected_cookies', [ + 'woocommerce_items_in_cart', + 'wp_woocommerce_session_*', + 'wp-postpass', + 'comment_author_*', + 'wordpress_*', + ]); + + $cookies_php = var_export($rejected_cookies, true); + $content = ' $value) { - if (strpos($name, \'wordpress_logged_in\') === 0) { - return; +$rejected_cookies = ' . $cookies_php . '; + +foreach ($rejected_cookies as $cookie) { + $cookie = trim($cookie); + if ($cookie === \'\') { + continue; + } + + if (substr($cookie, -1) === \'*\') { + $prefix = substr($cookie, 0, -1); + foreach ($_COOKIE as $name => $value) { + if (strpos($name, $prefix) === 0) { + return; + } + } + } else { + if (isset($_COOKIE[$cookie])) { + return; + } } } diff --git a/includes/Admin/Admin.php b/includes/Admin/Admin.php index 55273ac..f06a94f 100644 --- a/includes/Admin/Admin.php +++ b/includes/Admin/Admin.php @@ -22,6 +22,10 @@ class WP_Recache_Admin { 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'); + add_action('update_option_wp_recache_rejected_cookies', function () { + WP_Recache_Activator::create_cache_server(); + wp_recache_delete_all_cache(); + }); } /** @@ -59,6 +63,18 @@ class WP_Recache_Admin { 'default' => ['/cart', '/checkout', '/my-account'], ]); + register_setting('wp_recache_settings', 'wp_recache_rejected_cookies', [ + 'type' => 'array', + 'sanitize_callback' => [$this, 'sanitize_rejected_cookies'], + 'default' => [ + 'woocommerce_items_in_cart', + 'wp_woocommerce_session_*', + 'wp-postpass', + 'comment_author_*', + 'wordpress_*', + ], + ]); + register_setting('wp_recache_settings', 'wp_recache_minify_css', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', @@ -100,6 +116,22 @@ class WP_Recache_Admin { return array_map('sanitize_text_field', $lines); } + /** + * Sanitize rejected cookies. + * + * @param mixed $input Input value. + * @return array Sanitized cookie names. + */ + public function sanitize_rejected_cookies($input) { + if (is_array($input)) { + $input = implode("\n", $input); + } + + $lines = array_filter(array_map('trim', explode("\n", (string) $input))); + + return array_map('sanitize_text_field', $lines); + } + /** * Render settings page. */ diff --git a/includes/Admin/views/settings.php b/includes/Admin/views/settings.php index 21e2b35..21f45c5 100644 --- a/includes/Admin/views/settings.php +++ b/includes/Admin/views/settings.php @@ -52,6 +52,19 @@ $stats = WP_Recache_Admin::get_cache_stats();

One URL per line. These pages will not be cached.

+ + Rejected Cookies + + +

One cookie name per line. Visitors with these cookies bypass the cache. Use * as a suffix to match a prefix (e.g. comment_author_*).

+ +

Cache Statistics

diff --git a/includes/Common/Helpers.php b/includes/Common/Helpers.php index 0696fa4..f67520a 100644 --- a/includes/Common/Helpers.php +++ b/includes/Common/Helpers.php @@ -58,6 +58,10 @@ function wp_recache_should_exclude() { return true; } + if (wp_recache_has_rejected_cookie()) { + return true; + } + // Dynamic query-string requests are never cached (story 11). if (!empty($_SERVER['QUERY_STRING'])) { return true; @@ -83,6 +87,47 @@ function wp_recache_should_exclude() { return apply_filters('wp_recache_exclude', false); } +/** + * Check if a rejected cookie is present in the request. + * + * @return bool True if any rejected cookie is set. + */ +function wp_recache_has_rejected_cookie() { + if (empty($_COOKIE)) { + return false; + } + + $rejected = wp_recache_get_option('rejected_cookies', [ + 'woocommerce_items_in_cart', + 'wp_woocommerce_session_*', + 'wp-postpass', + 'comment_author_*', + 'wordpress_*', + ]); + + foreach ($rejected as $cookie) { + $cookie = trim($cookie); + if ($cookie === '') { + continue; + } + + if (substr($cookie, -1) === '*') { + $prefix = substr($cookie, 0, -1); + foreach ($_COOKIE as $name => $value) { + if (strpos($name, $prefix) === 0) { + return true; + } + } + } else { + if (isset($_COOKIE[$cookie])) { + return true; + } + } + } + + return false; +} + /** * Check if request is for a robot/crawler. */ diff --git a/tests/test-admin.php b/tests/test-admin.php index 6593600..d640c7d 100644 --- a/tests/test-admin.php +++ b/tests/test-admin.php @@ -52,4 +52,38 @@ class WP_Recache_Admin_Test extends WP_UnitTestCase { $this->assertGreaterThanOrEqual(2, $stats['files']); } + + public function test_sanitize_rejected_cookies_trims_whitespace() { + $admin = new WP_Recache_Admin(); + $result = $admin->sanitize_rejected_cookies(" wp-postpass \n comment_author_* "); + $this->assertEquals(['wp-postpass', 'comment_author_*'], $result); + } + + public function test_sanitize_rejected_cookies_filters_empty_lines() { + $admin = new WP_Recache_Admin(); + $result = $admin->sanitize_rejected_cookies("wp-postpass\n\n\ncomment_author_*\n"); + $this->assertEquals(['wp-postpass', 'comment_author_*'], $result); + } + + public function test_sanitize_rejected_cookies_handles_array_input() { + $admin = new WP_Recache_Admin(); + $result = $admin->sanitize_rejected_cookies(['wp-postpass', 'comment_author_*']); + $this->assertEquals(['wp-postpass', 'comment_author_*'], $result); + } + + public function test_rejected_cookies_option_is_registered() { + $admin = new WP_Recache_Admin(); + $admin->init(); + do_action('admin_init'); + + $default = [ + 'woocommerce_items_in_cart', + 'wp_woocommerce_session_*', + 'wp-postpass', + 'comment_author_*', + 'wordpress_*', + ]; + + $this->assertEquals($default, get_option('wp_recache_rejected_cookies', $default)); + } } diff --git a/tests/test-helpers.php b/tests/test-helpers.php index d3cea89..916d07a 100644 --- a/tests/test-helpers.php +++ b/tests/test-helpers.php @@ -88,6 +88,48 @@ class WP_Recache_Helpers_Test extends WP_UnitTestCase { $this->assertTrue(wp_recache_should_exclude()); } + public function test_wp_recache_has_rejected_cookie_returns_true_for_exact_match() { + wp_set_current_user(0); + update_option('wp_recache_rejected_cookies', ['wp-postpass']); + $_COOKIE = ['wp-postpass' => 'abc123']; + $this->assertTrue(wp_recache_has_rejected_cookie()); + $_COOKIE = []; + } + + public function test_wp_recache_has_rejected_cookie_returns_true_for_prefix_match() { + wp_set_current_user(0); + update_option('wp_recache_rejected_cookies', ['comment_author_*']); + $_COOKIE = ['comment_author_12345' => 'test']; + $this->assertTrue(wp_recache_has_rejected_cookie()); + $_COOKIE = []; + } + + public function test_wp_recache_has_rejected_cookie_returns_false_when_no_match() { + wp_set_current_user(0); + update_option('wp_recache_rejected_cookies', ['wp-postpass']); + $_COOKIE = ['unrelated_cookie' => 'value']; + $this->assertFalse(wp_recache_has_rejected_cookie()); + $_COOKIE = []; + } + + public function test_wp_recache_has_rejected_cookie_returns_false_when_empty_cookies() { + wp_set_current_user(0); + update_option('wp_recache_rejected_cookies', ['wp-postpass']); + $_COOKIE = []; + $this->assertFalse(wp_recache_has_rejected_cookie()); + } + + public function test_wp_recache_should_exclude_returns_true_for_rejected_cookie() { + wp_set_current_user(0); + update_option('wp_recache_rejected_cookies', ['woocommerce_items_in_cart']); + $_COOKIE = ['woocommerce_items_in_cart' => 'yes']; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['REQUEST_URI'] = '/test-page/'; + unset($_SERVER['HTTP_USER_AGENT']); + $this->assertTrue(wp_recache_should_exclude()); + $_COOKIE = []; + } + public function test_wp_recache_delete_cache_removes_file() { $url = 'https://example.com/test-page/'; $path = wp_recache_get_cache_path($url); diff --git a/uninstall.php b/uninstall.php index dbd2148..25893cf 100644 --- a/uninstall.php +++ b/uninstall.php @@ -12,6 +12,7 @@ $options = [ 'wp_recache_cache_enabled', 'wp_recache_separate_mobile_cache', 'wp_recache_exclude_urls', + 'wp_recache_rejected_cookies', 'wp_recache_minify_css', 'wp_recache_minify_js', 'wp_recache_lazyload_images',