diff --git a/honeypot-fields.php b/honeypot-fields.php index 6a1be21..3a367f3 100644 --- a/honeypot-fields.php +++ b/honeypot-fields.php @@ -3,7 +3,7 @@ * Plugin Name: Honeypot Fields * Plugin URI: https://informatiq.services * Description: Adds invisible honeypot fields to all forms to block spam bots. Works with WordPress core forms, Elementor, Gravity Forms, Contact Form 7, WooCommerce, and more. - * Version: 2.0.0 + * Version: 2.1.0 * Author: Malin * Author URI: https://malin.ro * License: GPL v2 or later @@ -14,6 +14,406 @@ if (!defined('ABSPATH')) { exit; } +/* ====================================================================== + * DATABASE HELPER + * ====================================================================*/ +class SmartHoneypotDB { + + const TABLE_VERSION = 1; + const TABLE_VERSION_OPTION = 'hp_db_version'; + + public static function table(): string { + global $wpdb; + return $wpdb->prefix . 'honeypot_log'; + } + + public static function install() { + global $wpdb; + $table = self::table(); + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE {$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_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql); + + update_option(self::TABLE_VERSION_OPTION, self::TABLE_VERSION); + } + + public static function insert(array $data) { + global $wpdb; + $wpdb->insert( + self::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_rows(array $args = []): array { + global $wpdb; + $table = self::table(); + $limit = max(1, intval($args['per_page'] ?? 25)); + $offset = max(0, intval($args['offset'] ?? 0)); + + $where = '1=1'; + $params = []; + + if (!empty($args['ip'])) { + $where .= ' AND ip_address = %s'; + $params[] = sanitize_text_field($args['ip']); + } + if (!empty($args['form'])) { + $where .= ' AND form_type = %s'; + $params[] = sanitize_text_field($args['form']); + } + 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"; + + if ($params) { + $sql = $wpdb->prepare($sql, $params); + } + + return $wpdb->get_results($sql) ?: []; + } + + public static function count(array $args = []): int { + global $wpdb; + $table = self::table(); + $where = '1=1'; + $params = []; + + if (!empty($args['ip'])) { + $where .= ' AND ip_address = %s'; + $params[] = sanitize_text_field($args['ip']); + } + if (!empty($args['form'])) { + $where .= ' AND form_type = %s'; + $params[] = sanitize_text_field($args['form']); + } + 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}"; + if ($params) { + $sql = $wpdb->prepare($sql, $params); + } + return (int) $wpdb->get_var($sql); + } + + public static function get_form_types(): array { + global $wpdb; + return $wpdb->get_col("SELECT DISTINCT form_type FROM " . self::table() . " ORDER BY form_type ASC") ?: []; + } + + public static function clear(): int { + global $wpdb; + return (int) $wpdb->query("TRUNCATE TABLE " . self::table()); + } + + public static function delete_older_than_days(int $days) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM " . self::table() . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)", + $days + ) + ); + } +} + +/* ====================================================================== + * ADMIN PAGE + * ====================================================================*/ +class SmartHoneypotAdmin { + + const MENU_SLUG = 'honeypot-logs'; + const NONCE_ACTION = 'hp_admin_action'; + const PER_PAGE = 25; + + public static function register() { + add_action('admin_menu', [self::class, 'add_menu']); + add_action('admin_init', [self::class, 'handle_actions']); + add_action('admin_enqueue_scripts', [self::class, 'enqueue_styles']); + add_filter('plugin_action_links_' . plugin_basename(HP_PLUGIN_FILE), [self::class, 'plugin_links']); + } + + public static function plugin_links($links) { + $log_link = 'View Logs'; + array_unshift($links, $log_link); + array_push($links, 'Documentation'); + return $links; + } + + public static function add_menu() { + add_menu_page( + 'Honeypot Logs', + 'Honeypot Logs', + 'manage_options', + self::MENU_SLUG, + [self::class, 'render_page'], + 'dashicons-shield-alt', + 81 + ); + } + + public static function enqueue_styles($hook) { + if ($hook !== 'toplevel_page_' . self::MENU_SLUG) { + return; + } + // Inline styles — no external file needed + $css = ' + #hp-log-wrap { max-width: 1400px; } + #hp-log-wrap .hp-stats { display:flex; gap:16px; margin:16px 0; flex-wrap:wrap; } + #hp-log-wrap .hp-stat-card { + background:#fff; border:1px solid #c3c4c7; border-radius:4px; + padding:16px 24px; min-width:140px; text-align:center; + } + #hp-log-wrap .hp-stat-card .hp-stat-num { font-size:2em; font-weight:700; color:#2271b1; line-height:1.2; } + #hp-log-wrap .hp-stat-card .hp-stat-lbl { color:#646970; font-size:12px; } + #hp-log-wrap .hp-filters { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; } + #hp-log-wrap .hp-filters input, #hp-log-wrap .hp-filters select { height:32px; } + #hp-log-wrap table.hp-log-table { width:100%; border-collapse:collapse; background:#fff; } + #hp-log-wrap table.hp-log-table th { + background:#f0f0f1; padding:8px 12px; text-align:left; + border-bottom:2px solid #c3c4c7; white-space:nowrap; + } + #hp-log-wrap table.hp-log-table td { padding:8px 12px; border-bottom:1px solid #f0f0f1; vertical-align:top; } + #hp-log-wrap table.hp-log-table tr:hover td { background:#f6f7f7; } + #hp-log-wrap .hp-ua { font-size:11px; color:#646970; max-width:300px; word-break:break-all; } + #hp-log-wrap .hp-badge { + display:inline-block; padding:2px 8px; border-radius:3px; font-size:11px; font-weight:600; + background:#ffecec; color:#b32d2e; border:1px solid #f7c5c5; + } + #hp-log-wrap .hp-pagination { margin:12px 0; display:flex; align-items:center; gap:8px; } + #hp-log-wrap .hp-pagination a, #hp-log-wrap .hp-pagination span { + display:inline-block; padding:4px 10px; border:1px solid #c3c4c7; + border-radius:3px; background:#fff; text-decoration:none; color:#2271b1; + } + #hp-log-wrap .hp-pagination span.current { background:#2271b1; color:#fff; border-color:#2271b1; } + #hp-log-wrap .hp-clear-btn { color:#b32d2e; } + '; + wp_add_inline_style('common', $css); + } + + public static function handle_actions() { + if (!isset($_POST['hp_action']) || !check_admin_referer(self::NONCE_ACTION)) { + return; + } + if (!current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + if ($_POST['hp_action'] === 'clear_logs') { + SmartHoneypotDB::clear(); + wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'cleared' => 1], admin_url('admin.php'))); + exit; + } + } + + public static function render_page() { + if (!current_user_can('manage_options')) { + return; + } + + // Filters from query string + $search = sanitize_text_field($_GET['hp_search'] ?? ''); + $filter_ip = sanitize_text_field($_GET['hp_ip'] ?? ''); + $filter_form = sanitize_text_field($_GET['hp_form'] ?? ''); + $paged = max(1, intval($_GET['paged'] ?? 1)); + $per_page = self::PER_PAGE; + $offset = ($paged - 1) * $per_page; + + $query_args = array_filter([ + 'ip' => $filter_ip, + 'form' => $filter_form, + 'search' => $search, + 'per_page' => $per_page, + 'offset' => $offset, + ]); + + $rows = SmartHoneypotDB::get_rows($query_args); + $total = SmartHoneypotDB::count($query_args); + $total_ever = SmartHoneypotDB::count(); + $form_types = SmartHoneypotDB::get_form_types(); + $total_pages = max(1, ceil($total / $per_page)); + + // Unique IPs total + global $wpdb; + $unique_ips = (int) $wpdb->get_var("SELECT COUNT(DISTINCT ip_address) FROM " . SmartHoneypotDB::table()); + $today = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM " . SmartHoneypotDB::table() . " WHERE blocked_at >= CURDATE()" + ); + + $base_url = admin_url('admin.php?page=' . self::MENU_SLUG); + ?> +
All logs have been cleared.
Showing = number_format($total) ?> result= $total !== 1 ? 's' : '' ?> + (page = $paged ?> of = $total_pages ?>)
+ + +| # | +Date / Time | +IP Address | +Form Type | +Reason | +URI | +User Agent | +
|---|---|---|---|---|---|---|
| + No blocked attempts recorded yet. + | ||||||
| = esc_html($row->id) ?> | += esc_html($row->blocked_at) ?> | +
+ = esc_html($row->ip_address) ?>+ filter + + lookup ↗ + |
+ = esc_html($row->form_type) ?> | += esc_html($row->reason) ?> | += esc_html($row->request_uri) ?> | += esc_html($row->user_agent) ?> | +