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); + ?> +
+

Honeypot Logs

+ + +

All logs have been cleared.

+ + + +
+
+
+
Total Blocked
+
+
+
+
Blocked Today
+
+
+
+
Unique IPs
+
+
+
+
Form Types Hit
+
+
+ + +
+ +
+ + + + + + Reset + + + + + + + + +
+ + +

Showing result + (page of )

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#Date / TimeIP AddressForm TypeReasonURIUser Agent
+ No blocked attempts recorded yet. +
id) ?>blocked_at) ?> + ip_address) ?>
+ filter +   + lookup ↗ +
form_type) ?>reason) ?>request_uri) ?>user_agent) ?>
+ + + 1): ?> +
+ 1): ?> + ← Prev + + + $p]))); + if ($p === $paged): ?> + + + + + + + Next → + +
+ + +
+ secret = wp_hash('honeypot_plugin_secret_v2'); - - // Field names look like legitimate form fields to trick bots - $this->hp_name = 'website_url_confirm'; + $this->secret = wp_hash('honeypot_plugin_secret_v2'); + $this->hp_name = 'website_url_confirm'; $this->token_name = 'form_session_id'; $this->time_name = 'form_render_ts'; @@ -50,7 +444,7 @@ class SmartHoneypotAntiSpam { } /* ------------------------------------------------------------------ - * INIT — register all hooks + * INIT * ----------------------------------------------------------------*/ public function init() { if (is_admin()) { @@ -58,72 +452,40 @@ class SmartHoneypotAntiSpam { return; } - // --- Inject honeypot into forms --- - - // WordPress core + // Inject honeypot add_filter('the_content', [$this, 'add_to_content_forms'], 99); - add_filter('comment_form_defaults', [$this, 'add_to_comment_form_defaults']); add_action('comment_form_after_fields', [$this, 'echo_honeypot']); add_action('comment_form_logged_in_after', [$this, 'echo_honeypot']); - - // WooCommerce add_action('woocommerce_register_form', [$this, 'echo_honeypot']); add_action('woocommerce_login_form', [$this, 'echo_honeypot']); add_action('woocommerce_after_order_notes', [$this, 'echo_honeypot']); - - // WordPress registration add_action('register_form', [$this, 'echo_honeypot']); add_action('login_form', [$this, 'echo_honeypot']); - - // Elementor Pro forms add_action('elementor_pro/forms/render_field', [$this, 'add_to_elementor_form'], 10, 2); add_action('elementor/widget/render_content', [$this, 'filter_elementor_widget'], 10, 2); - - // Gravity Forms add_filter('gform_form_tag', [$this, 'add_to_gravity_forms'], 10, 2); - - // Contact Form 7 add_filter('wpcf7_form_elements', [$this, 'add_to_cf7']); - - // Generic form search add_filter('get_search_form', [$this, 'add_to_search_form'], 99); - // --- Validate on POST --- - - // WooCommerce registration (proper hook) + // Validate add_filter('woocommerce_process_registration_errors', [$this, 'validate_wc_registration'], 10, 4); - - // WooCommerce login add_filter('woocommerce_process_login_errors', [$this, 'validate_wc_login'], 10, 3); - - // WooCommerce checkout add_action('woocommerce_after_checkout_validation', [$this, 'validate_wc_checkout'], 10, 2); - - // WordPress core registration add_filter('registration_errors', [$this, 'validate_wp_registration'], 10, 3); - - // Comments add_filter('preprocess_comment', [$this, 'validate_comment']); - - // Elementor Pro forms add_action('elementor_pro/forms/validation', [$this, 'validate_elementor_form'], 10, 2); - - // Generic early POST check for other forms add_action('template_redirect', [$this, 'validate_generic_post']); - // --- CSS & JS --- + // CSS & JS add_action('wp_head', [$this, 'print_css']); add_action('wp_footer', [$this, 'print_js'], 99); } /* ------------------------------------------------------------------ - * HONEYPOT FIELD HTML + * HONEYPOT HTML * ----------------------------------------------------------------*/ - private function get_honeypot_html() { + private function get_honeypot_html(): string { $ts = time(); - $token_data = $ts . '|' . wp_create_nonce('hp_form_' . $ts); - - // The wrapper uses a generic class name return sprintf( '