Files
WP-Security-Pack/security-pack/includes/class-wpsp-activity-log.php
T
2026-02-01 21:12:06 +01:00

283 lines
8.6 KiB
PHP

<?php
/**
* Activity logging for Security Pack.
*
* @package Security_Pack
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Activity log class.
*/
class WPSP_Activity_Log {
/**
* Event types.
*/
const EVENT_LOGIN_SUCCESS = 'login_success';
const EVENT_LOGIN_FAILED = 'login_failed';
const EVENT_LOCKOUT = 'lockout';
const EVENT_IP_BLOCKED = 'ip_blocked';
const EVENT_GEO_BLOCKED = 'geo_blocked';
const EVENT_LOCKOUT_LIFTED = 'lockout_lifted';
/**
* Constructor.
*/
public function __construct() {
// Hooks are set up by Login Protection class.
}
/**
* Log an event.
*
* @param string $event_type Event type.
* @param string|null $ip_address IP address (auto-detected if null).
* @param string|null $username Username.
* @param string|null $details Additional details.
* @return int|false Insert ID or false on failure.
*/
public static function log( $event_type, $ip_address = null, $username = null, $details = null ) {
global $wpdb;
if ( null === $ip_address ) {
$ip_address = WPSP_Helper::get_client_ip();
}
// Get user agent.
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 255 )
: '';
// Get country code if geo-blocking is available.
$country_code = null;
$geo_blocking = wpsp()->get_component( 'geo_blocking' );
if ( $geo_blocking && $ip_address ) {
$country_code = $geo_blocking->get_country_code( $ip_address );
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$result = $wpdb->insert(
$wpdb->prefix . 'wpsp_activity_log',
array(
'event_type' => $event_type,
'ip_address' => $ip_address ? $ip_address : '',
'username' => $username,
'user_agent' => $user_agent,
'country_code' => $country_code,
'details' => $details,
'created_at' => current_time( 'mysql' ),
),
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
);
return $result ? $wpdb->insert_id : false;
}
/**
* Get recent logs.
*
* @param array $args Query arguments.
* @return array
*/
public static function get_logs( $args = array() ) {
global $wpdb;
$defaults = array(
'limit' => 50,
'offset' => 0,
'event_type' => '',
'ip_address' => '',
);
$args = wp_parse_args( $args, $defaults );
$limit = absint( $args['limit'] );
$offset = absint( $args['offset'] );
// Build query based on filters. Always order by created_at DESC for security logs.
if ( ! empty( $args['event_type'] ) && ! empty( $args['ip_address'] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s AND ip_address = %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
$args['event_type'],
$args['ip_address'],
$limit,
$offset
)
);
} elseif ( ! empty( $args['event_type'] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
$args['event_type'],
$limit,
$offset
)
);
} elseif ( ! empty( $args['ip_address'] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log WHERE ip_address = %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
$args['ip_address'],
$limit,
$offset
)
);
}
// No filters.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log ORDER BY created_at DESC LIMIT %d OFFSET %d",
$limit,
$offset
)
);
}
/**
* Get total log count.
*
* @param array $args Query arguments.
* @return int
*/
public static function get_log_count( $args = array() ) {
global $wpdb;
// Build query based on filters.
if ( ! empty( $args['event_type'] ) && ! empty( $args['ip_address'] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s AND ip_address = %s",
$args['event_type'],
$args['ip_address']
)
);
} elseif ( ! empty( $args['event_type'] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s",
$args['event_type']
)
);
} elseif ( ! empty( $args['ip_address'] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE ip_address = %s",
$args['ip_address']
)
);
}
// No filters.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log" );
}
/**
* Get log statistics.
*
* @param int $days Number of days to look back.
* @return array
*/
public static function get_stats( $days = 30 ) {
global $wpdb;
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security stats must be real-time.
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT event_type, COUNT(*) as count FROM {$wpdb->prefix}wpsp_activity_log WHERE created_at >= %s GROUP BY event_type",
$cutoff
)
);
$stats = array(
'login_success' => 0,
'login_failed' => 0,
'lockout' => 0,
'ip_blocked' => 0,
'geo_blocked' => 0,
'total' => 0,
);
foreach ( $results as $row ) {
$stats[ $row->event_type ] = (int) $row->count;
$stats['total'] += (int) $row->count;
}
return $stats;
}
/**
* Cleanup old logs (cron job).
*/
public static function cleanup_old_logs() {
global $wpdb;
$retention_days = Security_Pack::get_setting( 'log_retention_days', 30 );
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
// Delete old logs.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup operation, caching not applicable.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}wpsp_activity_log WHERE created_at < %s",
$cutoff
)
);
// Delete expired lockouts (lockout_until stores Unix timestamp as integer).
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup operation, caching not applicable.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}wpsp_lockouts WHERE lockout_until IS NOT NULL AND lockout_until > 0 AND lockout_until < %d",
time()
)
);
}
/**
* Clear all logs.
*
* @return bool
*/
public static function clear_all_logs() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Truncate for admin action.
return false !== $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wpsp_activity_log" );
}
/**
* Get event type label.
*
* @param string $event_type Event type.
* @return string
*/
public static function get_event_label( $event_type ) {
$labels = array(
self::EVENT_LOGIN_SUCCESS => __( 'Login Success', 'security-pack' ),
self::EVENT_LOGIN_FAILED => __( 'Login Failed', 'security-pack' ),
self::EVENT_LOCKOUT => __( 'Lockout', 'security-pack' ),
self::EVENT_IP_BLOCKED => __( 'IP Blocked', 'security-pack' ),
self::EVENT_GEO_BLOCKED => __( 'Geo Blocked', 'security-pack' ),
self::EVENT_LOCKOUT_LIFTED => __( 'Lockout Lifted', 'security-pack' ),
);
return isset( $labels[ $event_type ] ) ? $labels[ $event_type ] : $event_type;
}
}