Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 105 additions & 15 deletions src/Modules/Cache/PageCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
Expand Down Expand Up @@ -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, '<!doctype html' )
|| 0 === stripos( $body_start, '<html' )
|| false !== stripos( $body_start, '<html' );
}

/**
* Normalize current request URL.
*
Expand Down Expand Up @@ -1040,11 +1094,17 @@ private function read_cache_body( $key ) {
* @param string $key Cache key.
* @param array<string, int|string> $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 );
}

/**
Expand All @@ -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;
}

/**
Expand Down
41 changes: 40 additions & 1 deletion src/Modules/Cache/StatsStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ final class StatsStore {
*/
private $dirty = false;

/**
* High-traffic counters that should be sampled instead of persisted per request.
*
* @var array<string, bool>
*/
private $sampled_counter_keys = [
'hits' => true,
'stale_hits' => true,
'bypasses' => true,
'lock_waits' => true,
];

/**
* Constructor.
*/
Expand All @@ -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.
*
Expand Down
14 changes: 7 additions & 7 deletions uninstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
function perform_handle_plugin_uninstall() {

$setting_types = array(
$option_keys = array(
'perform_settings',
'perform_common',
'perform_ssl',
Expand All @@ -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;
Expand All @@ -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' );
}
}
}
Expand Down
Loading