2026-04-09 11:45:26 +02:00
<? php
if ( ! defined ( 'ABSPATH' )) exit ;
/**
* Database helper for InformatiQ Toolkit.
* Manages two log tables: bot_log and honeypot_log.
*/
class ITK_Database {
2026-04-10 09:37:31 +02:00
const DB_VERSION = 2 ;
2026-04-09 11:45:26 +02:00
const DB_VERSION_OPTION = 'itk_db_version' ;
/* ── Table names ──────────────────────────────────────────── */
public static function bot_table () : string {
global $wpdb ;
return $wpdb -> prefix . 'itk_bot_log' ;
}
public static function honeypot_table () : string {
global $wpdb ;
return $wpdb -> prefix . 'itk_honeypot_log' ;
}
2026-04-10 09:37:31 +02:00
public static function attack_table () : string {
global $wpdb ;
return $wpdb -> prefix . 'itk_attack_log' ;
}
2026-04-09 11:45:26 +02:00
/* ── Install / upgrade ────────────────────────────────────── */
public static function install () {
global $wpdb ;
$charset = $wpdb -> get_charset_collate ();
$sql_bot = "CREATE TABLE " . self :: bot_table () . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
logged_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
referrer VARCHAR(1000) NOT NULL DEFAULT '',
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
bot_type VARCHAR(100) NOT NULL DEFAULT '',
reason VARCHAR(255) NOT NULL DEFAULT '',
action VARCHAR(20) NOT NULL DEFAULT 'blocked',
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY logged_at (logged_at),
KEY bot_type (bot_type),
KEY action (action)
) { $charset } ;" ;
$sql_hp = "CREATE TABLE " . self :: honeypot_table () . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
blocked_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
form_type VARCHAR(100) NOT NULL DEFAULT '',
reason VARCHAR(255) NOT NULL DEFAULT '',
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY blocked_at (blocked_at),
KEY form_type (form_type)
) { $charset } ;" ;
2026-04-10 09:37:31 +02:00
$sql_atk = "CREATE TABLE " . self :: attack_table () . " (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
logged_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
attack_type VARCHAR(50) NOT NULL DEFAULT '',
rule_desc VARCHAR(255) NOT NULL DEFAULT '',
source VARCHAR(20) NOT NULL DEFAULT '',
param VARCHAR(200) NOT NULL DEFAULT '',
payload TEXT NOT NULL,
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
method VARCHAR(10) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY logged_at (logged_at),
KEY attack_type (attack_type)
) { $charset } ;" ;
2026-04-09 11:45:26 +02:00
require_once ABSPATH . 'wp-admin/includes/upgrade.php' ;
dbDelta ( $sql_bot );
dbDelta ( $sql_hp );
2026-04-10 09:37:31 +02:00
dbDelta ( $sql_atk );
2026-04-09 11:45:26 +02:00
update_option ( self :: DB_VERSION_OPTION , self :: DB_VERSION );
}
/* ── Bot log ──────────────────────────────────────────────── */
public static function log_bot ( array $data ) : void {
global $wpdb ;
$wpdb -> insert (
self :: bot_table (),
[
'logged_at' => current_time ( 'mysql' ),
'ip_address' => sanitize_text_field ( $data [ 'ip' ] ?? '' ),
'user_agent' => sanitize_textarea_field ( $data [ 'ua' ] ?? '' ),
'referrer' => esc_url_raw ( substr ( $data [ 'referrer' ] ?? '' , 0 , 1000 )),
'request_uri' => esc_url_raw ( substr ( $data [ 'uri' ] ?? '' , 0 , 1000 )),
'bot_type' => sanitize_text_field ( $data [ 'bot_type' ] ?? '' ),
'reason' => sanitize_text_field ( $data [ 'reason' ] ?? '' ),
'action' => sanitize_text_field ( $data [ 'action' ] ?? 'blocked' ),
],
[ '%s' , '%s' , '%s' , '%s' , '%s' , '%s' , '%s' , '%s' ]
);
}
public static function get_bot_rows ( array $args = []) : array {
global $wpdb ;
$table = self :: bot_table ();
$limit = max ( 1 , ( int )( $args [ 'per_page' ] ?? 25 ));
$offset = max ( 0 , ( int )( $args [ 'offset' ] ?? 0 ));
$where = '1=1' ;
$params = [];
if ( ! empty ( $args [ 'action' ])) {
$where .= ' AND action = %s' ;
$params [] = $args [ 'action' ];
}
if ( ! empty ( $args [ 'bot_type' ])) {
$where .= ' AND bot_type = %s' ;
$params [] = $args [ 'bot_type' ];
}
if ( ! empty ( $args [ 'ip' ])) {
$where .= ' AND ip_address = %s' ;
$params [] = $args [ 'ip' ];
}
if ( ! empty ( $args [ 'search' ])) {
$like = '%' . $wpdb -> esc_like ( $args [ 'search' ]) . '%' ;
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)' ;
$params [] = $like ; $params [] = $like ; $params [] = $like ;
}
$params [] = $limit ;
$params [] = $offset ;
$sql = "SELECT * FROM { $table } WHERE { $where } ORDER BY logged_at DESC LIMIT %d OFFSET %d" ;
return $wpdb -> get_results ( $wpdb -> prepare ( $sql , $params )) ?: [];
}
public static function count_bot_rows ( array $args = []) : int {
global $wpdb ;
$table = self :: bot_table ();
$where = '1=1' ;
$params = [];
if ( ! empty ( $args [ 'action' ])) {
$where .= ' AND action = %s' ;
$params [] = $args [ 'action' ];
}
if ( ! empty ( $args [ 'bot_type' ])) {
$where .= ' AND bot_type = %s' ;
$params [] = $args [ 'bot_type' ];
}
if ( ! empty ( $args [ 'ip' ])) {
$where .= ' AND ip_address = %s' ;
$params [] = $args [ 'ip' ];
}
if ( ! empty ( $args [ 'search' ])) {
$like = '%' . $wpdb -> esc_like ( $args [ 'search' ]) . '%' ;
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)' ;
$params [] = $like ; $params [] = $like ; $params [] = $like ;
}
$sql = "SELECT COUNT(*) FROM { $table } WHERE { $where } " ;
return ( int )( $params ? $wpdb -> get_var ( $wpdb -> prepare ( $sql , $params )) : $wpdb -> get_var ( $sql ));
}
public static function get_bot_stats () : array {
global $wpdb ;
$table = self :: bot_table ();
$today = current_time ( 'Y-m-d' );
return [
'total' => ( int ) $wpdb -> get_var ( "SELECT COUNT(*) FROM { $table } " ),
'today' => ( int ) $wpdb -> get_var ( $wpdb -> prepare ( "SELECT COUNT(*) FROM { $table } WHERE DATE(logged_at)=%s" , $today )),
'blocked' => ( int ) $wpdb -> get_var ( "SELECT COUNT(*) FROM { $table } WHERE action='blocked'" ),
'rate_limited' => ( int ) $wpdb -> get_var ( "SELECT COUNT(*) FROM { $table } WHERE action='rate_limited'" ),
'top_bot_types' => $wpdb -> get_results ( "SELECT bot_type, COUNT(*) as cnt FROM { $table } WHERE bot_type != '' GROUP BY bot_type ORDER BY cnt DESC LIMIT 8" ) ?: [],
'top_ips' => $wpdb -> get_results ( "SELECT ip_address, COUNT(*) as cnt FROM { $table } GROUP BY ip_address ORDER BY cnt DESC LIMIT 5" ) ?: [],
'last_24h_counts' => $wpdb -> get_results ( "SELECT DATE_FORMAT(logged_at,'%H:00') as hour, COUNT(*) as cnt FROM { $table } WHERE logged_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY hour ORDER BY hour ASC" ) ?: [],
];
}
public static function get_bot_types () : array {
global $wpdb ;
return $wpdb -> get_col ( "SELECT DISTINCT bot_type FROM " . self :: bot_table () . " WHERE bot_type != '' ORDER BY bot_type ASC" ) ?: [];
}
public static function clear_bot_log () : void {
global $wpdb ;
$wpdb -> query ( "TRUNCATE TABLE " . self :: bot_table ());
}
public static function prune_bot_log ( int $days ) : void {
global $wpdb ;
$wpdb -> query ( $wpdb -> prepare (
"DELETE FROM " . self :: bot_table () . " WHERE logged_at < DATE_SUB(NOW(), INTERVAL %d DAY)" ,
$days
));
}
/* ── Honeypot log ─────────────────────────────────────────── */
public static function log_honeypot ( array $data ) : void {
global $wpdb ;
$wpdb -> insert (
self :: honeypot_table (),
[
'blocked_at' => current_time ( 'mysql' ),
'ip_address' => sanitize_text_field ( $data [ 'ip' ] ?? '' ),
'form_type' => sanitize_text_field ( $data [ 'form' ] ?? 'Unknown' ),
'reason' => sanitize_text_field ( $data [ 'reason' ] ?? '' ),
'request_uri' => esc_url_raw ( substr ( $data [ 'uri' ] ?? '' , 0 , 1000 )),
'user_agent' => sanitize_textarea_field ( $data [ 'ua' ] ?? '' ),
],
[ '%s' , '%s' , '%s' , '%s' , '%s' , '%s' ]
);
}
public static function get_honeypot_rows ( array $args = []) : array {
global $wpdb ;
$table = self :: honeypot_table ();
$limit = max ( 1 , ( int )( $args [ 'per_page' ] ?? 25 ));
$offset = max ( 0 , ( int )( $args [ 'offset' ] ?? 0 ));
$where = '1=1' ;
$params = [];
if ( ! empty ( $args [ 'form' ])) {
$where .= ' AND form_type = %s' ;
$params [] = $args [ 'form' ];
}
if ( ! empty ( $args [ 'ip' ])) {
$where .= ' AND ip_address = %s' ;
$params [] = $args [ 'ip' ];
}
if ( ! empty ( $args [ 'search' ])) {
$like = '%' . $wpdb -> esc_like ( $args [ 'search' ]) . '%' ;
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)' ;
$params [] = $like ; $params [] = $like ; $params [] = $like ;
}
$params [] = $limit ;
$params [] = $offset ;
$sql = "SELECT * FROM { $table } WHERE { $where } ORDER BY blocked_at DESC LIMIT %d OFFSET %d" ;
return $wpdb -> get_results ( $wpdb -> prepare ( $sql , $params )) ?: [];
}
public static function count_honeypot_rows ( array $args = []) : int {
global $wpdb ;
$table = self :: honeypot_table ();
$where = '1=1' ;
$params = [];
if ( ! empty ( $args [ 'form' ])) {
$where .= ' AND form_type = %s' ;
$params [] = $args [ 'form' ];
}
if ( ! empty ( $args [ 'ip' ])) {
$where .= ' AND ip_address = %s' ;
$params [] = $args [ 'ip' ];
}
if ( ! empty ( $args [ 'search' ])) {
$like = '%' . $wpdb -> esc_like ( $args [ 'search' ]) . '%' ;
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)' ;
$params [] = $like ; $params [] = $like ; $params [] = $like ;
}
$sql = "SELECT COUNT(*) FROM { $table } WHERE { $where } " ;
return ( int )( $params ? $wpdb -> get_var ( $wpdb -> prepare ( $sql , $params )) : $wpdb -> get_var ( $sql ));
}
public static function get_honeypot_stats () : array {
global $wpdb ;
$table = self :: honeypot_table ();
$today = current_time ( 'Y-m-d' );
return [
'total' => ( int ) $wpdb -> get_var ( "SELECT COUNT(*) FROM { $table } " ),
'today' => ( int ) $wpdb -> get_var ( $wpdb -> prepare ( "SELECT COUNT(*) FROM { $table } WHERE DATE(blocked_at)=%s" , $today )),
'top_forms' => $wpdb -> get_results ( "SELECT form_type, COUNT(*) as cnt FROM { $table } GROUP BY form_type ORDER BY cnt DESC LIMIT 8" ) ?: [],
'top_ips' => $wpdb -> get_results ( "SELECT ip_address, COUNT(*) as cnt FROM { $table } GROUP BY ip_address ORDER BY cnt DESC LIMIT 5" ) ?: [],
];
}
public static function get_honeypot_form_types () : array {
global $wpdb ;
return $wpdb -> get_col ( "SELECT DISTINCT form_type FROM " . self :: honeypot_table () . " ORDER BY form_type ASC" ) ?: [];
}
public static function clear_honeypot_log () : void {
global $wpdb ;
$wpdb -> query ( "TRUNCATE TABLE " . self :: honeypot_table ());
}
public static function prune_honeypot_log ( int $days ) : void {
global $wpdb ;
$wpdb -> query ( $wpdb -> prepare (
"DELETE FROM " . self :: honeypot_table () . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)" ,
$days
));
}
2026-04-10 09:37:31 +02:00
/* ── Attack log ───────────────────────────────────────────── */
public static function log_attack ( array $data ) : void {
global $wpdb ;
$wpdb -> insert (
self :: attack_table (),
[
'logged_at' => current_time ( 'mysql' ),
'ip_address' => sanitize_text_field ( $data [ 'ip' ] ?? '' ),
'attack_type' => sanitize_text_field ( $data [ 'attack_type' ] ?? '' ),
'rule_desc' => sanitize_text_field ( $data [ 'rule_desc' ] ?? '' ),
'source' => sanitize_text_field ( $data [ 'source' ] ?? '' ),
'param' => sanitize_text_field ( $data [ 'param' ] ?? '' ),
'payload' => sanitize_textarea_field ( substr ( $data [ 'payload' ] ?? '' , 0 , 500 )),
'request_uri' => sanitize_text_field ( substr ( $data [ 'uri' ] ?? '' , 0 , 1000 )),
'method' => sanitize_text_field ( $data [ 'method' ] ?? '' ),
'user_agent' => sanitize_textarea_field ( $data [ 'ua' ] ?? '' ),
],
[ '%s' , '%s' , '%s' , '%s' , '%s' , '%s' , '%s' , '%s' , '%s' , '%s' ]
);
}
public static function get_attack_rows ( array $args = []) : array {
global $wpdb ;
$table = self :: attack_table ();
$limit = max ( 1 , ( int )( $args [ 'per_page' ] ?? 25 ));
$offset = max ( 0 , ( int )( $args [ 'offset' ] ?? 0 ));
$where = '1=1' ;
$params = [];
if ( ! empty ( $args [ 'attack_type' ])) {
$where .= ' AND attack_type = %s' ;
$params [] = $args [ 'attack_type' ];
}
if ( ! empty ( $args [ 'ip' ])) {
$where .= ' AND ip_address = %s' ;
$params [] = $args [ 'ip' ];
}
if ( ! empty ( $args [ 'search' ])) {
$like = '%' . $wpdb -> esc_like ( $args [ 'search' ]) . '%' ;
$where .= ' AND (ip_address LIKE %s OR param LIKE %s OR payload LIKE %s OR rule_desc LIKE %s)' ;
$params [] = $like ; $params [] = $like ; $params [] = $like ; $params [] = $like ;
}
$params [] = $limit ;
$params [] = $offset ;
$sql = "SELECT * FROM { $table } WHERE { $where } ORDER BY logged_at DESC LIMIT %d OFFSET %d" ;
return $wpdb -> get_results ( $wpdb -> prepare ( $sql , $params )) ?: [];
}
public static function count_attack_rows ( array $args = []) : int {
global $wpdb ;
$table = self :: attack_table ();
$where = '1=1' ;
$params = [];
if ( ! empty ( $args [ 'attack_type' ])) {
$where .= ' AND attack_type = %s' ;
$params [] = $args [ 'attack_type' ];
}
if ( ! empty ( $args [ 'ip' ])) {
$where .= ' AND ip_address = %s' ;
$params [] = $args [ 'ip' ];
}
if ( ! empty ( $args [ 'search' ])) {
$like = '%' . $wpdb -> esc_like ( $args [ 'search' ]) . '%' ;
$where .= ' AND (ip_address LIKE %s OR param LIKE %s OR payload LIKE %s OR rule_desc LIKE %s)' ;
$params [] = $like ; $params [] = $like ; $params [] = $like ; $params [] = $like ;
}
$sql = "SELECT COUNT(*) FROM { $table } WHERE { $where } " ;
return ( int )( $params ? $wpdb -> get_var ( $wpdb -> prepare ( $sql , $params )) : $wpdb -> get_var ( $sql ));
}
public static function get_attack_types () : array {
global $wpdb ;
return $wpdb -> get_col ( "SELECT DISTINCT attack_type FROM " . self :: attack_table () . " WHERE attack_type != '' ORDER BY attack_type ASC" ) ?: [];
}
public static function clear_attack_log () : void {
global $wpdb ;
$wpdb -> query ( "TRUNCATE TABLE " . self :: attack_table ());
}
2026-04-09 11:45:26 +02:00
}