Files
argus-wp-defence/includes/class-argus-cache-eligibility.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

186 lines
6.5 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Argus_Cache_Eligibility {
const SAFE_TO_CACHE = 'safe_to_cache';
const BYPASS = 'bypass';
const NEEDS_REVIEW = 'needs_review';
const FORCE_EXCLUDED = 'force_excluded';
const IGNORABLE_QUERY_PARAMS = array( 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'msclkid', 'mc_cid', 'mc_eid', 'ref' );
public static function classify_request( array $context ) {
$method = strtoupper( $context['method'] ?? 'GET' );
if ( ! in_array( $method, array( 'GET', 'HEAD' ), true ) ) {
return self::result( self::FORCE_EXCLUDED, 'HTTP method ' . $method . ' is never cached (only GET/HEAD are safe).' );
}
if ( ! empty( $context['has_authorization_header'] ) ) {
return self::result( self::BYPASS, 'Request carries an Authorization header (API/Basic-auth client).' );
}
if ( ! empty( $context['is_admin'] ) ) {
return self::result( self::FORCE_EXCLUDED, 'wp-admin is never cached.' );
}
if ( ! empty( $context['is_ajax'] ) || ! empty( $context['is_rest'] ) || ! empty( $context['is_cron'] ) ) {
return self::result( self::FORCE_EXCLUDED, 'admin-ajax.php / wp-json / wp-cron.php are always excluded from the shared page cache.' );
}
$path = untrailingslashit( (string) ( $context['path'] ?? '' ) );
foreach ( self::protected_paths() as $protected ) {
$protected = untrailingslashit( $protected );
if ( $path === $protected || 0 === strpos( $path, $protected . '/' ) || false !== strpos( $protected, '*' ) && self::wildcard_match( $protected, $path ) ) {
return self::result( self::FORCE_EXCLUDED, 'Matches protected WordPress endpoint: ' . $protected );
}
}
if ( ! empty( $context['is_logged_in'] ) ) {
return self::result( self::BYPASS, 'Authenticated WordPress user.' );
}
foreach ( (array) ( $context['cookies'] ?? array() ) as $cookie_name => $cookie_value ) {
$hit = self::match_cookie_pattern( $cookie_name );
if ( $hit ) {
return self::result( self::BYPASS, 'Session/personalization cookie present: ' . $hit );
}
}
$query = (array) ( $context['query'] ?? array() );
$meaningful_query = array_diff_key( $query, array_flip( self::IGNORABLE_QUERY_PARAMS ) );
if ( ! empty( $meaningful_query ) ) {
return self::result( self::NEEDS_REVIEW, 'Request has query parameters beyond known tracking params: ' . implode( ', ', array_keys( $meaningful_query ) ) );
}
return self::result( self::SAFE_TO_CACHE, 'GET request, anonymous, no protected path, no session cookies, no meaningful query string.' );
}
public static function classify_response( array $context ) {
$status = (int) ( $context['status_code'] ?? 0 );
if ( 200 !== $status ) {
return self::result( self::FORCE_EXCLUDED, 'Response status ' . $status . ' is not 200 (redirects/errors are never cached).' );
}
$content_type = (string) ( $context['content_type'] ?? '' );
if ( '' !== $content_type && false === stripos( $content_type, 'text/html' ) ) {
return self::result( self::BYPASS, 'Response content-type is not text/html: ' . $content_type );
}
$headers = array_change_key_case( (array) ( $context['headers'] ?? array() ), CASE_LOWER );
$cache_control = strtolower( (string) ( $headers['cache-control'] ?? '' ) );
if ( $cache_control ) {
foreach ( array( 'no-store', 'private', 'no-cache' ) as $directive ) {
if ( false !== strpos( $cache_control, $directive ) ) {
return self::result( self::BYPASS, 'Origin response sent Cache-Control: ' . $directive );
}
}
}
if ( ! empty( $headers['pragma'] ) && false !== stripos( (string) $headers['pragma'], 'no-cache' ) ) {
return self::result( self::BYPASS, 'Origin response sent Pragma: no-cache.' );
}
if ( ! empty( $headers['set-cookie'] ) ) {
return self::result( self::BYPASS, 'Origin response set a new cookie during this request -- likely personalized (session/cart/comment identity).' );
}
foreach ( (array) ( $context['new_cookies_set'] ?? array() ) as $cookie_name ) {
if ( self::match_cookie_pattern( $cookie_name ) ) {
return self::result( self::BYPASS, 'Origin response set a session/personalization cookie: ' . $cookie_name );
}
}
return self::result( self::SAFE_TO_CACHE, 'Response is a plain 200 text/html page with no private/session signal.' );
}
protected static function match_cookie_pattern( $cookie_name ) {
foreach ( self::cookie_patterns() as $pattern ) {
if ( self::wildcard_match( $pattern, $cookie_name ) ) {
return $pattern;
}
}
return false;
}
protected static function wildcard_match( $pattern, $subject ) {
$regex = '/^' . str_replace( '\*', '.*', preg_quote( $pattern, '/' ) ) . '$/i';
return (bool) preg_match( $regex, $subject );
}
public static function cookie_patterns() {
$defaults = array(
'wordpress_logged_in_*',
'wordpress_sec_*',
'wordpress_*',
'wp-settings-*',
'wp-settings-time-*',
'comment_author_*',
'comment_author_email_*',
'comment_author_url_*',
'woocommerce_items_in_cart',
'woocommerce_cart_hash',
'wp_woocommerce_session_*',
);
return apply_filters( 'argus_wpd_cache_bypass_cookie_patterns', $defaults );
}
public static function protected_paths() {
$defaults = array(
'/wp-admin',
'/wp-login.php',
'/wp-json',
'/wp-cron.php',
'/xmlrpc.php',
);
if ( class_exists( 'WooCommerce' ) ) {
$defaults = array_merge( $defaults, self::woocommerce_paths() );
}
return apply_filters( 'argus_wpd_cache_protected_paths', $defaults );
}
protected static function woocommerce_paths() {
$paths = array();
$page_options = array(
'woocommerce_cart_page_id' => '/cart',
'woocommerce_checkout_page_id' => '/checkout',
'woocommerce_myaccount_page_id' => '/my-account',
);
foreach ( $page_options as $option => $fallback ) {
$page_id = (int) get_option( $option );
$path = $page_id ? wp_parse_url( get_permalink( $page_id ), PHP_URL_PATH ) : null;
$paths[] = $path ? untrailingslashit( $path ) : $fallback;
}
return $paths;
}
public static function classify_url( $url ) {
$parts = wp_parse_url( $url );
$query = array();
if ( ! empty( $parts['query'] ) ) {
parse_str( $parts['query'], $query );
}
return self::classify_request(
array(
'method' => 'GET',
'path' => $parts['path'] ?? '/',
'query' => $query,
'is_admin' => false,
'is_logged_in' => false,
'cookies' => array(),
)
);
}
protected static function result( $status, $reason ) {
return array( 'status' => $status, 'reason' => $reason );
}
}