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.
193 lines
5.4 KiB
PHP
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 );
|
|
}
|
|
}
|