From 0d75443c449104ccac4da55882b5f1ea0a55b348 Mon Sep 17 00:00:00 2001 From: Mehul Gohil Date: Thu, 21 May 2026 11:44:03 +0530 Subject: [PATCH] Harden cache runtime writes --- src/Modules/Cache/PageCache.php | 120 +++++++++++++++++++++++++++---- src/Modules/Cache/StatsStore.php | 41 ++++++++++- uninstall.php | 14 ++-- 3 files changed, 152 insertions(+), 23 deletions(-) diff --git a/src/Modules/Cache/PageCache.php b/src/Modules/Cache/PageCache.php index d1e04b6..a71c15e 100644 --- a/src/Modules/Cache/PageCache.php +++ b/src/Modules/Cache/PageCache.php @@ -249,17 +249,10 @@ public function store_cache( $html ) { return $html; } - if ( http_response_code() >= 400 ) { + if ( ! $this->is_cacheable_response( $html ) ) { return $html; } - // Don't cache responses setting cookies. - foreach ( headers_list() as $header_line ) { - if ( 0 === stripos( $header_line, 'Set-Cookie:' ) ) { - return $html; - } - } - $ttl_seconds = (int) Helpers::get_option( 'page_cache_ttl', 'perform_settings', 3600 ); $swr_seconds = (int) Helpers::get_option( 'page_cache_swr_ttl', 'perform_settings', 21600 ); $created = time(); @@ -270,8 +263,13 @@ public function store_cache( $html ) { 'swr_expires' => $created + max( 120, $ttl_seconds + $swr_seconds ), ]; - $this->write_cache_meta( $this->current_cache_key, $meta_payload ); - $this->write_cache_body( $this->current_cache_key, $html ); + if ( ! $this->write_cache_body( $this->current_cache_key, $html ) ) { + return $html; + } + + if ( ! $this->write_cache_meta( $this->current_cache_key, $meta_payload ) ) { + wp_delete_file( $this->get_body_file_path( $this->current_cache_key ) ); + } } finally { $this->release_lock(); } @@ -891,6 +889,62 @@ private function is_cacheable_request() { return true; } + /** + * Whether the rendered response is safe to persist as a page-cache entry. + * + * @param string $html Rendered response body. + * + * @return bool + */ + private function is_cacheable_response( $html ) { + $current_status = http_response_code(); + $status_code = false === $current_status ? 200 : (int) $current_status; + if ( 200 !== $status_code ) { + return false; + } + + $has_content_type = false; + + foreach ( headers_list() as $header_line ) { + if ( 0 === stripos( $header_line, 'Set-Cookie:' ) ) { + return false; + } + + if ( 0 === stripos( $header_line, 'Location:' ) ) { + return false; + } + + if ( 0 === stripos( $header_line, 'Content-Type:' ) ) { + $has_content_type = true; + + if ( false === stripos( $header_line, 'text/html' ) ) { + return false; + } + } + } + + if ( ! $has_content_type && ! $this->looks_like_html_response( $html ) ) { + return false; + } + + return true; + } + + /** + * Lightweight fallback for hosts that have not sent Content-Type yet. + * + * @param string $html Rendered response body. + * + * @return bool + */ + private function looks_like_html_response( $html ) { + $body_start = ltrim( substr( $html, 0, 1024 ) ); + + return 0 === stripos( $body_start, ' $meta Metadata. * - * @return void + * @return bool */ private function write_cache_meta( $key, $meta ) { - $path = $this->get_meta_file_path( $key ); - file_put_contents( $path, wp_json_encode( $meta ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + $path = $this->get_meta_file_path( $key ); + $contents = wp_json_encode( $meta ); + + if ( ! is_string( $contents ) ) { + return false; + } + + return $this->write_file_atomically( $path, $contents ); } /** @@ -1053,11 +1113,41 @@ private function write_cache_meta( $key, $meta ) { * @param string $key Cache key. * @param string $body Body. * - * @return void + * @return bool */ private function write_cache_body( $key, $body ) { $path = $this->get_body_file_path( $key ); - file_put_contents( $path, $body ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + return $this->write_file_atomically( $path, $body ); + } + + /** + * Write cache file through a same-directory temp file and atomic rename. + * + * @param string $path Target file path. + * @param string $contents File contents. + * + * @return bool + */ + private function write_file_atomically( $path, $contents ) { + $directory = dirname( $path ); + if ( ! is_dir( $directory ) || ! is_writable( $directory ) ) { + return false; + } + + $tmp_path = $path . '.' . uniqid( 'tmp-', true ); + $written = file_put_contents( $tmp_path, $contents, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + + if ( false === $written ) { + wp_delete_file( $tmp_path ); + return false; + } + + if ( ! rename( $tmp_path, $path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename + wp_delete_file( $tmp_path ); + return false; + } + + return true; } /** diff --git a/src/Modules/Cache/StatsStore.php b/src/Modules/Cache/StatsStore.php index b4d2721..d1dc015 100644 --- a/src/Modules/Cache/StatsStore.php +++ b/src/Modules/Cache/StatsStore.php @@ -35,6 +35,18 @@ final class StatsStore { */ private $dirty = false; + /** + * High-traffic counters that should be sampled instead of persisted per request. + * + * @var array + */ + private $sampled_counter_keys = [ + 'hits' => true, + 'stale_hits' => true, + 'bypasses' => true, + 'lock_waits' => true, + ]; + /** * Constructor. */ @@ -51,10 +63,37 @@ public function __construct() { * @return void */ public function increment( $key ) { - $this->stats[ $key ] = isset( $this->stats[ $key ] ) ? ( (int) $this->stats[ $key ] + 1 ) : 1; + $increment = $this->get_increment_sample_size( $key ); + if ( 0 === $increment ) { + return; + } + + $this->stats[ $key ] = isset( $this->stats[ $key ] ) ? ( (int) $this->stats[ $key ] + $increment ) : $increment; $this->dirty = true; } + /** + * Get sampled increment size for hot cache counters. + * + * @param string $key Stat key. + * + * @return int + */ + private function get_increment_sample_size( $key ) { + if ( empty( $this->sampled_counter_keys[ $key ] ) ) { + return 1; + } + + $sample_rate = (int) apply_filters( 'perform_cache_stats_sample_rate', 20, $key ); + $sample_rate = max( 1, $sample_rate ); + + if ( 1 === $sample_rate ) { + return 1; + } + + return 1 === wp_rand( 1, $sample_rate ) ? $sample_rate : 0; + } + /** * Set value for stat key. * diff --git a/uninstall.php b/uninstall.php index fe83086..ce03d9b 100644 --- a/uninstall.php +++ b/uninstall.php @@ -18,7 +18,7 @@ */ function perform_handle_plugin_uninstall() { - $setting_types = array( + $option_keys = array( 'perform_settings', 'perform_common', 'perform_ssl', @@ -27,6 +27,9 @@ function perform_handle_plugin_uninstall() { 'perform_advanced', 'perform_import_export', 'perform_support', + 'perform_assets_manager_options', + 'perform_cache_preload_queue', + 'perform_cache_stats', ); $remove_data_on_uninstall = false; @@ -45,18 +48,15 @@ function perform_handle_plugin_uninstall() { if ( ! empty( $sites ) ) { foreach ( $sites as $site ) { - foreach ( $setting_types as $option ) { + foreach ( $option_keys as $option ) { delete_blog_option( (int) $site->blog_id, $option ); } - } } + } } else { - foreach ( $setting_types as $option ) { + foreach ( $option_keys as $option ) { delete_option( $option ); } - delete_option( 'perform_assets_manager_options' ); - delete_option( 'perform_cache_preload_queue' ); - delete_option( 'perform_cache_stats' ); } } }