Files
argus-wp-defence/includes/class-argus-static-cache.php
T
root df0f2fccb8 ARGUS WordPress Defence 1.0.0 — first production release
Automatic WordPress security: local firewall, malware and file-integrity
scanning, vulnerability protection, quarantine, scheduled backups, an
optional page cache, and automatic global threat intelligence.

See README.md for installation, update, and uninstall instructions.
2026-08-09 13:40:16 +00:00

496 lines
16 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Argus_Static_Cache {
const STATUS_HIT = 'HIT';
const STATUS_MISS = 'MISS';
const STATUS_BYPASS = 'BYPASS';
const STATUS_EXPIRED = 'EXPIRED';
const STATUS_STALE = 'STALE';
const STATUS_REVALIDATED = 'REVALIDATED';
const STATUS_WARMED = 'WARMED';
const STATUS_PURGED = 'PURGED';
const STATUS_EXCLUDED = 'EXCLUDED';
public static function init() {
if ( class_exists( 'Argus_Settings' ) ) {
$custom_excludes = (array) Argus_Settings::get( 'cache_custom_exclude_paths', array() );
if ( ! empty( $custom_excludes ) ) {
add_filter(
'argus_wpd_cache_protected_paths',
function ( $paths ) use ( $custom_excludes ) {
return array_merge( $paths, $custom_excludes );
}
);
}
}
if ( ! Argus_Settings::get( 'static_cache_enabled', false ) ) {
return;
}
add_action( 'send_headers', array( __CLASS__, 'maybe_serve_cached' ), 0 );
foreach ( array( 'switch_theme', 'customize_save_after', 'activated_plugin', 'deactivated_plugin' ) as $hook ) {
add_action( $hook, array( __CLASS__, 'purge_all' ) );
}
add_action( 'save_post', array( __CLASS__, 'purge_related_to_post' ) );
add_action( 'deleted_post', array( __CLASS__, 'purge_related_to_post' ) );
add_action( 'comment_post', array( __CLASS__, 'purge_related_to_comment' ) );
add_action( 'wp_set_comment_status', array( __CLASS__, 'purge_related_to_comment' ) );
}
public static function maybe_serve_cached() {
$start = microtime( true );
$url = self::current_url();
$classification = Argus_Cache_Eligibility::classify_request( self::request_context() );
if ( self::SAFE_TO_CACHE_STATUS() !== $classification['status'] ) {
$log_status = Argus_Cache_Eligibility::FORCE_EXCLUDED === $classification['status'] ? self::STATUS_EXCLUDED : self::STATUS_BYPASS;
Argus_Cache_Log::record( $log_status, $url, $classification['reason'] );
return;
}
$key = self::cache_key( $url );
$paths = self::cache_paths( $key );
$ttl = (int) Argus_Settings::get( 'static_cache_ttl_secs', 3600 );
$grace = (int) Argus_Settings::get( 'cache_stale_grace_secs', 600 );
if ( file_exists( $paths['html'] ) ) {
$age = time() - filemtime( $paths['html'] );
if ( $age < $ttl ) {
self::serve_hit( $paths, $url, self::STATUS_HIT, $start );
return;
}
if ( $age < ( $ttl + $grace ) ) {
$lock = self::acquire_lock( $key );
if ( ! $lock ) {
self::serve_hit( $paths, $url, self::STATUS_STALE, $start );
return;
}
self::start_capture( $url, $key, $paths, self::STATUS_REVALIDATED, $start, $lock );
return;
}
}
$lock = self::acquire_lock( $key );
if ( ! $lock ) {
Argus_Cache_Log::record( self::STATUS_MISS, $url, 'Cache miss (another request is already regenerating this URL).', null );
return;
}
self::start_capture( $url, $key, $paths, self::STATUS_MISS, $start, $lock );
}
protected static function SAFE_TO_CACHE_STATUS() {
return Argus_Cache_Eligibility::SAFE_TO_CACHE;
}
protected static function start_capture( $url, $key, array $paths, $status, $start, $lock ) {
header( 'X-Argus-Cache: ' . $status );
ob_start(
function ( $html ) use ( $url, $key, $paths, $status, $start, $lock ) {
return self::capture_and_store( $html, $url, $key, $paths, $status, $start, $lock );
}
);
}
public static function capture_and_store( $html, $url, $key, array $paths, $status, $start, $lock ) {
$response_ms = (int) round( ( microtime( true ) - $start ) * 1000 );
$response_context = array(
'status_code' => http_response_code(),
'headers' => self::response_headers(),
'new_cookies_set' => self::response_cookie_names(),
'content_type' => self::response_content_type(),
);
$classification = Argus_Cache_Eligibility::classify_response( $response_context );
if ( Argus_Cache_Eligibility::SAFE_TO_CACHE !== $classification['status'] || '' === $html ) {
$log_status = '' === $html ? self::STATUS_BYPASS : ( Argus_Cache_Eligibility::FORCE_EXCLUDED === $classification['status'] ? self::STATUS_EXCLUDED : self::STATUS_BYPASS );
Argus_Cache_Log::record( $log_status, $url, '' === $html ? 'Empty response body.' : $classification['reason'], $response_ms );
self::release_lock( $lock );
return $html;
}
self::write_cache_entry( $paths, $html, $url );
Argus_Cache_Log::record( $status, $url, 'Stored.', $response_ms );
self::release_lock( $lock );
return $html;
}
protected static function write_cache_entry( array $paths, $html, $url ) {
wp_mkdir_p( dirname( $paths['html'] ) );
file_put_contents( $paths['html'], $html ); // phpcs:ignore WordPress.WP.AlternativeFunctions
$min_bytes = (int) Argus_Settings::get( 'cache_min_compress_bytes', 1024 );
if ( strlen( $html ) >= $min_bytes ) {
if ( Argus_Settings::get( 'cache_gzip_enabled', true ) && function_exists( 'gzencode' ) ) {
$level = (int) Argus_Settings::get( 'cache_gzip_level', 6 );
file_put_contents( $paths['gz'], gzencode( $html, max( 1, min( 9, $level ) ) ) ); // phpcs:ignore
} else {
@unlink( $paths['gz'] ); // phpcs:ignore
}
if ( Argus_Settings::get( 'cache_brotli_enabled', true ) && function_exists( 'brotli_compress' ) ) {
$level = (int) Argus_Settings::get( 'cache_brotli_level', 5 );
file_put_contents( $paths['br'], brotli_compress( $html, max( 0, min( 11, $level ) ) ) ); // phpcs:ignore
} else {
@unlink( $paths['br'] ); // phpcs:ignore
}
} else {
@unlink( $paths['gz'] ); // phpcs:ignore
@unlink( $paths['br'] ); // phpcs:ignore
}
file_put_contents( // phpcs:ignore
$paths['meta'],
wp_json_encode(
array(
'url' => $url,
'bytes' => strlen( $html ),
'created_at' => current_time( 'mysql', true ),
)
)
);
}
protected static function serve_hit( array $paths, $url, $status, $start ) {
$encoding = self::negotiate_encoding();
$source = $paths['html'];
$header_encoding = '';
if ( 'br' === $encoding && file_exists( $paths['br'] ) ) {
$source = $paths['br'];
$header_encoding = 'br';
} elseif ( 'gzip' === $encoding && file_exists( $paths['gz'] ) ) {
$source = $paths['gz'];
$header_encoding = 'gzip';
}
header( 'X-Argus-Cache: ' . $status );
header( 'X-Argus-Cache-Age: ' . ( time() - filemtime( $paths['html'] ) ) );
header( 'Vary: Accept-Encoding' );
if ( $header_encoding ) {
header( 'Content-Encoding: ' . $header_encoding );
}
header( 'Content-Type: text/html; charset=UTF-8' );
readfile( $source ); // phpcs:ignore WordPress.WP.AlternativeFunctions
Argus_Cache_Log::record( $status, $url, 'Served from cache.', (int) round( ( microtime( true ) - $start ) * 1000 ) );
exit;
}
protected static function negotiate_encoding() {
$accept = strtolower( (string) ( $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( false !== strpos( $accept, 'br' ) ) {
return 'br';
}
if ( false !== strpos( $accept, 'gzip' ) ) {
return 'gzip';
}
return '';
}
protected static function request_context() {
$cookies = array();
foreach ( (array) $_COOKIE as $name => $value ) { // phpcs:ignore WordPress.Security.NonceVerification
$cookies[ (string) $name ] = '';
}
return array(
'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
'path' => (string) wp_parse_url( self::current_url(), PHP_URL_PATH ),
'query' => $_GET, // phpcs:ignore WordPress.Security.NonceVerification
'is_admin' => is_admin(),
'is_logged_in' => is_user_logged_in(),
'cookies' => $cookies,
'has_authorization_header' => ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) || ! empty( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ), // phpcs:ignore
'is_ajax' => wp_doing_ajax(),
'is_rest' => defined( 'REST_REQUEST' ) && REST_REQUEST,
'is_cron' => defined( 'DOING_CRON' ) && DOING_CRON,
) + self::conditional_tag_excludes();
}
protected static function conditional_tag_excludes() {
if ( is_feed() || is_trackback() || is_preview() || is_search() || is_404() ) {
return array( 'is_admin' => true );
}
return array();
}
protected static function response_headers() {
$out = array();
foreach ( headers_list() as $header ) {
$parts = explode( ':', $header, 2 );
if ( 2 !== count( $parts ) ) {
continue;
}
$name = strtolower( trim( $parts[0] ) );
if ( isset( $out[ $name ] ) ) {
$out[ $name ] .= ', ' . trim( $parts[1] );
} else {
$out[ $name ] = trim( $parts[1] );
}
}
return $out;
}
protected static function response_cookie_names() {
$names = array();
foreach ( headers_list() as $header ) {
if ( 0 === stripos( $header, 'Set-Cookie:' ) && preg_match( '/^Set-Cookie:\s*([^=]+)=/i', $header, $m ) ) {
$names[] = trim( $m[1] );
}
}
return $names;
}
protected static function response_content_type() {
foreach ( headers_list() as $header ) {
if ( 0 === stripos( $header, 'Content-Type:' ) ) {
return trim( substr( $header, strlen( 'Content-Type:' ) ) );
}
}
return '';
}
protected static function current_url() {
$scheme = is_ssl() ? 'https' : 'http';
$host = (string) ( $_SERVER['HTTP_HOST'] ?? 'default' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$uri = (string) ( $_SERVER['REQUEST_URI'] ?? '/' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
return $scheme . '://' . $host . $uri;
}
public static function cache_key( $url ) {
$parts = wp_parse_url( $url );
$path = $parts['path'] ?? '/';
$query = array();
if ( ! empty( $parts['query'] ) ) {
parse_str( $parts['query'], $query );
$query = array_diff_key( $query, array_flip( Argus_Cache_Eligibility::IGNORABLE_QUERY_PARAMS ) );
ksort( $query );
}
$host = (string) ( $parts['host'] ?? 'default' );
return md5( $host . '|' . $path . '|' . http_build_query( $query ) );
}
protected static function cache_dir() {
$uploads = wp_get_upload_dir();
return trailingslashit( $uploads['basedir'] ) . 'argus-wpd-data/html-cache';
}
public static function cache_paths( $key ) {
$base = trailingslashit( self::cache_dir() ) . $key;
return array(
'html' => $base . '.html',
'gz' => $base . '.html.gz',
'br' => $base . '.html.br',
'meta' => $base . '.meta.json',
);
}
protected static function lock_dir() {
return trailingslashit( self::cache_dir() ) . 'locks';
}
protected static function acquire_lock( $key ) {
$dir = self::lock_dir();
if ( ! is_dir( $dir ) ) {
wp_mkdir_p( $dir );
}
$path = trailingslashit( $dir ) . $key . '.lock';
$fh = @fopen( $path, 'c' ); // phpcs:ignore WordPress.WP.AlternativeFunctions,WordPress.PHP.NoSilencedErrors
if ( ! $fh ) {
return false;
}
if ( ! flock( $fh, LOCK_EX | LOCK_NB ) ) {
fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions
return false;
}
return $fh;
}
protected static function release_lock( $fh ) {
if ( $fh ) {
flock( $fh, LOCK_UN );
fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions
}
}
public static function purge_all() {
$dir = self::cache_dir();
if ( ! is_dir( $dir ) ) {
return 0;
}
$count = 0;
foreach ( array( '*.html', '*.html.gz', '*.html.br', '*.meta.json' ) as $glob ) {
foreach ( glob( trailingslashit( $dir ) . $glob ) ?: array() as $file ) {
@unlink( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
++$count;
}
}
Argus_Cache_Log::record( self::STATUS_PURGED, '*', 'Purge everything.' );
return $count;
}
public static function purge_url( $url ) {
$key = self::cache_key( $url );
foreach ( self::cache_paths( $key ) as $path ) {
@unlink( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
}
Argus_Cache_Log::record( self::STATUS_PURGED, $url, 'Purge single URL.' );
}
public static function purge_pattern( $pattern ) {
$dir = self::cache_dir();
if ( ! is_dir( $dir ) ) {
return 0;
}
$regex = '/^' . str_replace( '\*', '.*', preg_quote( $pattern, '/' ) ) . '$/i';
$count = 0;
foreach ( glob( trailingslashit( $dir ) . '*.meta.json' ) ?: array() as $meta_file ) {
$meta = json_decode( (string) file_get_contents( $meta_file ), true ); // phpcs:ignore
if ( ! is_array( $meta ) || empty( $meta['url'] ) || ! preg_match( $regex, $meta['url'] ) ) {
continue;
}
$key = basename( $meta_file, '.meta.json' );
foreach ( self::cache_paths( $key ) as $path ) {
@unlink( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
}
++$count;
}
Argus_Cache_Log::record( self::STATUS_PURGED, $pattern, 'Purge by pattern (' . $count . ' entries).' );
return $count;
}
public static function purge_related_to_post( $post_id ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
$permalink = get_permalink( $post_id );
if ( $permalink ) {
self::purge_url( $permalink );
}
self::purge_url( home_url( '/' ) );
$page_for_posts = (int) get_option( 'page_for_posts' );
if ( $page_for_posts ) {
self::purge_url( get_permalink( $page_for_posts ) );
}
}
public static function purge_related_to_comment( $comment_id ) {
$comment = get_comment( $comment_id );
if ( $comment && $comment->comment_post_ID ) {
self::purge_related_to_post( (int) $comment->comment_post_ID );
}
}
public static function stats() {
$dir = self::cache_dir();
if ( ! is_dir( $dir ) ) {
return array( 'file_count' => 0, 'total_bytes' => 0 );
}
$files = glob( trailingslashit( $dir ) . '*.html' ) ?: array();
$bytes = 0;
foreach ( $files as $file ) {
$bytes += filesize( $file );
foreach ( array( '.gz', '.br' ) as $ext ) {
$variant = $file . $ext;
if ( file_exists( $variant ) ) {
$bytes += filesize( $variant );
}
}
}
return array( 'file_count' => count( $files ), 'total_bytes' => $bytes );
}
public static function compression_stats() {
$dir = self::cache_dir();
$out = array(
'gzip' => array( 'files' => 0, 'avg_savings_pct' => 0.0, 'bytes_saved' => 0 ),
'brotli' => array( 'files' => 0, 'avg_savings_pct' => 0.0, 'bytes_saved' => 0 ),
'total_bytes_saved' => 0,
);
if ( ! is_dir( $dir ) ) {
return $out;
}
$files = glob( trailingslashit( $dir ) . '*.html' ) ?: array();
$pct_sums = array( 'gzip' => 0.0, 'brotli' => 0.0 );
foreach ( $files as $file ) {
$plain_size = filesize( $file );
if ( ! $plain_size ) {
continue;
}
foreach ( array( 'gzip' => '.gz', 'brotli' => '.br' ) as $format => $ext ) {
$variant = $file . $ext;
if ( ! file_exists( $variant ) ) {
continue;
}
$variant_size = filesize( $variant );
if ( ! $variant_size || $variant_size >= $plain_size ) {
continue;
}
$saved = $plain_size - $variant_size;
++$out[ $format ]['files'];
$out[ $format ]['bytes_saved'] += $saved;
$out['total_bytes_saved'] += $saved;
$pct_sums[ $format ] += ( $saved / $plain_size ) * 100;
}
}
foreach ( array( 'gzip', 'brotli' ) as $format ) {
if ( $out[ $format ]['files'] > 0 ) {
$out[ $format ]['avg_savings_pct'] = round( $pct_sums[ $format ] / $out[ $format ]['files'], 1 );
}
}
return $out;
}
public static function list_cached( $limit = 100 ) {
$dir = self::cache_dir();
if ( ! is_dir( $dir ) ) {
return array();
}
$meta_files = glob( trailingslashit( $dir ) . '*.meta.json' ) ?: array();
usort(
$meta_files,
function ( $a, $b ) {
return filemtime( $b ) <=> filemtime( $a );
}
);
$out = array();
foreach ( array_slice( $meta_files, 0, $limit ) as $meta_file ) {
$meta = json_decode( (string) file_get_contents( $meta_file ), true ); // phpcs:ignore
if ( ! is_array( $meta ) ) {
continue;
}
$key = basename( $meta_file, '.meta.json' );
$paths = self::cache_paths( $key );
$out[] = array(
'url' => $meta['url'] ?? '',
'bytes' => (int) ( $meta['bytes'] ?? 0 ),
'created_at' => $meta['created_at'] ?? '',
'age_secs' => file_exists( $paths['html'] ) ? ( time() - filemtime( $paths['html'] ) ) : null,
'has_gzip' => file_exists( $paths['gz'] ),
'has_brotli' => file_exists( $paths['br'] ),
);
}
return $out;
}
}