feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into a single unified plugin with the following improvements: - Fixed deactivation bug: all protection methods now guard themselves with their own option check so toggling off via AJAX takes effect immediately without any hook re-registration. - Added rate-limiting for good/legitimate bots (Googlebot, Bingbot, DuckDuckBot, Yandex, etc.) via transient sliding-window counters; configurable per-bot limits in goodbots.conf (BotName|req/min); returns HTTP 429 with Retry-After: 60 when over limit. - Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables) replaces the old wp_options-based 100-entry cap. - New Dashboard tab with terminal-style bot activity monitor: total blocked, today's count, rate-limited hits, top threat sources (bar chart), top IPs, top honeypot form types, active-module status panel. - All optimizations from utils.php merged into Optimization tab as toggleable settings (was always-on before). - Single admin page (Settings → InformatiQ Toolkit) with 8 tabs: Dashboard | Bot Blocker | Protection | Optimization | Honeypot | Bot Logs | Honeypot Logs | Config Files. - Config file editor for badbots.conf, goodbots.conf, referrers.conf, networks.conf, allowed-ips.conf with AJAX save and transient flush. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
283
includes/class-itk-database.php
Normal file
283
includes/class-itk-database.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
/**
|
||||
* Database helper for InformatiQ Toolkit.
|
||||
* Manages two log tables: bot_log and honeypot_log.
|
||||
*/
|
||||
class ITK_Database {
|
||||
|
||||
const DB_VERSION = 1;
|
||||
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';
|
||||
}
|
||||
|
||||
/* ── 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};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta($sql_bot);
|
||||
dbDelta($sql_hp);
|
||||
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user