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

193 lines
5.4 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Argus_Cache_Log {
const RAW_RETENTION_DAYS = 7;
public static function log_dir() {
$uploads = wp_get_upload_dir();
return trailingslashit( $uploads['basedir'] ) . 'argus-wpd-data/cache-log';
}
public static function record( $status, $url, $reason = '', $response_time_ms = null, $rule = '' ) {
$dir = self::log_dir();
if ( ! is_dir( $dir ) ) {
wp_mkdir_p( $dir );
$htaccess = trailingslashit( $dir ) . '.htaccess';
if ( ! file_exists( $htaccess ) ) {
@file_put_contents( $htaccess, "Require all denied\n" ); // phpcs:ignore
}
}
$line = wp_json_encode(
array(
't' => microtime( true ),
's' => $status,
'u' => mb_substr( (string) $url, 0, 500 ),
'r' => mb_substr( (string) $reason, 0, 255 ),
'ms' => null === $response_time_ms ? null : (int) $response_time_ms,
'rule' => $rule,
)
);
$file = trailingslashit( $dir ) . gmdate( 'Y-m-d' ) . '.log';
@file_put_contents( $file, $line . "\n", FILE_APPEND | LOCK_EX ); // phpcs:ignore
}
public static function recent( $limit = 200, $status_filter = null ) {
$dir = self::log_dir();
if ( ! is_dir( $dir ) ) {
return array();
}
$files = glob( trailingslashit( $dir ) . '*.log' ) ?: array();
rsort( $files );
$rows = array();
foreach ( array_slice( $files, 0, self::RAW_RETENTION_DAYS ) as $file ) {
$lines = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ) ?: array();
for ( $i = count( $lines ) - 1; $i >= 0 && count( $rows ) < $limit * 4; $i-- ) {
$row = json_decode( $lines[ $i ], true );
if ( ! is_array( $row ) ) {
continue;
}
if ( $status_filter && ( $row['s'] ?? '' ) !== $status_filter ) {
continue;
}
$rows[] = $row;
if ( count( $rows ) >= $limit ) {
break 2;
}
}
}
return $rows;
}
public static function paginated( $page = 1, $per_page = 10, $status_filter = null ) {
$page = max( 1, (int) $page );
$all = self::recent( ( $page * $per_page ) + 1, $status_filter );
$has_next = count( $all ) > ( $page - 1 ) * $per_page + $per_page;
$rows = array_slice( $all, ( $page - 1 ) * $per_page, $per_page );
return array(
'rows' => $rows,
'page' => $page,
'total_pages' => $page + ( $has_next ? 1 : 0 ),
);
}
public static function rollup_and_prune() {
global $wpdb;
$dir = self::log_dir();
if ( ! is_dir( $dir ) ) {
return;
}
$table = Argus_DB::table( 'cache_stats_daily' );
$files = glob( trailingslashit( $dir ) . '*.log' ) ?: array();
foreach ( $files as $file ) {
$basename = basename( $file, '.log' );
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $basename ) ) {
continue;
}
$lines = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ) ?: array();
$by_status = array();
foreach ( $lines as $line ) {
$row = json_decode( $line, true );
if ( ! is_array( $row ) || empty( $row['s'] ) ) {
continue;
}
$status = $row['s'];
if ( ! isset( $by_status[ $status ] ) ) {
$by_status[ $status ] = array( 'count' => 0, 'ms' => 0 );
}
++$by_status[ $status ]['count'];
$by_status[ $status ]['ms'] += (int) ( $row['ms'] ?? 0 );
}
foreach ( $by_status as $status => $agg ) {
$wpdb->query(
$wpdb->prepare(
"INSERT INTO {$table} (stat_date, status, request_count, total_response_time_ms, total_bytes)
VALUES (%s, %s, %d, %d, 0)
ON DUPLICATE KEY UPDATE request_count = VALUES(request_count), total_response_time_ms = VALUES(total_response_time_ms)", // phpcs:ignore
$basename,
$status,
$agg['count'],
$agg['ms']
)
);
}
}
$cutoff = gmdate( 'Y-m-d', strtotime( '-' . self::RAW_RETENTION_DAYS . ' days' ) );
foreach ( $files as $file ) {
$basename = basename( $file, '.log' );
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $basename ) && $basename < $cutoff ) {
@unlink( $file ); // phpcs:ignore
}
}
}
public static function summary( $days = 7 ) {
global $wpdb;
$table = Argus_DB::table( 'cache_stats_daily' );
$since = gmdate( 'Y-m-d', strtotime( '-' . (int) $days . ' days' ) );
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT status, SUM(request_count) AS cnt, SUM(total_response_time_ms) AS ms
FROM {$table} WHERE stat_date >= %s GROUP BY status", // phpcs:ignore
$since
)
);
$out = array();
foreach ( $rows as $row ) {
$cnt = (int) $row->cnt;
$out[ $row->status ] = array(
'count' => $cnt,
'avg_ms' => $cnt > 0 ? (int) round( $row->ms / $cnt ) : null,
);
}
return $out;
}
public static function daily_series( $days = 14 ) {
global $wpdb;
$table = Argus_DB::table( 'cache_stats_daily' );
$since = gmdate( 'Y-m-d', strtotime( '-' . (int) $days . ' days' ) );
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT stat_date, status, request_count FROM {$table} WHERE stat_date >= %s", $since ) // phpcs:ignore
);
$by_date = array();
for ( $i = $days - 1; $i >= 0; $i-- ) {
$d = gmdate( 'Y-m-d', strtotime( "-{$i} days" ) );
$by_date[ $d ] = array( 'date' => $d, 'hit' => 0, 'miss' => 0, 'bypass' => 0 );
}
foreach ( $rows as $row ) {
if ( ! isset( $by_date[ $row->stat_date ] ) ) {
continue;
}
$key = strtolower( $row->status );
if ( isset( $by_date[ $row->stat_date ][ $key ] ) ) {
$by_date[ $row->stat_date ][ $key ] += (int) $row->request_count;
}
}
return array_values( $by_date );
}
}