secret = wp_hash('honeypot_plugin_secret_v2'); // Field names look like legitimate form fields to trick bots $this->hp_name = 'website_url_confirm'; $this->token_name = 'form_session_id'; $this->time_name = 'form_render_ts'; add_action('init', [$this, 'init']); } /* ------------------------------------------------------------------ * INIT — register all hooks * ----------------------------------------------------------------*/ public function init() { if (is_admin()) { add_action('admin_notices', [$this, 'activation_notice']); return; } // --- Inject honeypot into forms --- // WordPress core 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) 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 --- add_action('wp_head', [$this, 'print_css']); add_action('wp_footer', [$this, 'print_js'], 99); } /* ------------------------------------------------------------------ * HONEYPOT FIELD HTML * ----------------------------------------------------------------*/ private function get_honeypot_html() { $ts = time(); $token_data = $ts . '|' . wp_create_nonce('hp_form_' . $ts); // The wrapper uses a generic class name return sprintf( '', esc_attr($this->hp_name), esc_attr($this->token_name), esc_attr($this->time_name), esc_attr($ts) ); } /** Echo the honeypot (for action hooks) */ public function echo_honeypot() { echo $this->get_honeypot_html(); } /* ------------------------------------------------------------------ * INJECTION HELPERS * ----------------------------------------------------------------*/ public function add_to_content_forms($content) { if (is_admin() || is_feed()) { return $content; } return preg_replace_callback( '/(]*>)(.*?)(<\/form>)/is', function ($m) { return $m[1] . $m[2] . $this->get_honeypot_html() . $m[3]; }, $content ); } public function add_to_comment_form_defaults($defaults) { if (isset($defaults['fields']['url'])) { // Honeypot already has a URL-looking field; inject after existing fields } return $defaults; } public function add_to_search_form($form) { return preg_replace( '/(<\/form>)/i', $this->get_honeypot_html() . '$1', $form, 1 ); } public function add_to_elementor_form($field, $instance) { static $done = false; if (!$done && $field['type'] === 'submit') { $done = true; echo $this->get_honeypot_html(); } } public function filter_elementor_widget($content, $widget) { if ($widget->get_name() === 'form') { $content = preg_replace( '/(<\/form>)/i', $this->get_honeypot_html() . '$1', $content, 1 ); } return $content; } public function add_to_gravity_forms($tag, $form) { return preg_replace( '/(
validate_js_token($_POST[$this->token_name])) { $this->log_spam('JS token invalid'); return false; } } // 3. Timestamp check if ($require_fields && !isset($_POST[$this->time_name])) { $this->log_spam('Timestamp field missing'); return false; } if (isset($_POST[$this->time_name])) { $ts = intval($_POST[$this->time_name]); $now = time(); $diff = $now - $ts; if ($diff < self::MIN_SUBMIT_TIME) { $this->log_spam("Submitted too fast ({$diff}s)"); return false; } if ($diff > self::MAX_SUBMIT_TIME) { $this->log_spam("Timestamp too old ({$diff}s)"); return false; } } // Clean up honeypot fields so downstream code never sees them $this->clean_post_data(); return true; } /** Validate the JS-generated token */ private function validate_js_token(string $token): bool { // Token format: "nonce_value" set by JS using the hidden field // JS computes: HMAC of (timestamp + site-specific value) // We verify it matches what we'd expect $parts = explode('|', $token); if (count($parts) !== 2) { return false; } $ts = intval($parts[0]); $hash = $parts[1]; $expected = hash_hmac('sha256', $ts . '|honeypot_js_proof', $this->secret); return hash_equals(substr($expected, 0, 16), $hash); } /** Remove honeypot fields from $_POST / $_REQUEST */ private function clean_post_data() { foreach ([$this->hp_name, $this->token_name, $this->time_name] as $key) { unset($_POST[$key], $_REQUEST[$key]); } } private function log_spam(string $reason) { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $uri = $_SERVER['REQUEST_URI'] ?? 'unknown'; $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; error_log("[Honeypot] SPAM blocked: {$reason} | IP: {$ip} | URI: {$uri} | UA: {$ua}"); } /* ------------------------------------------------------------------ * RATE LIMITING * ----------------------------------------------------------------*/ private function check_rate_limit(): bool { $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (!$ip) { return false; } $key = 'hp_rate_' . md5($ip); $count = (int) get_transient($key); if ($count >= self::RATE_LIMIT) { $this->log_spam("Rate limit exceeded ({$count} attempts) for IP {$ip}"); return false; } set_transient($key, $count + 1, HOUR_IN_SECONDS); return true; } /* ------------------------------------------------------------------ * WOOCOMMERCE VALIDATION * ----------------------------------------------------------------*/ public function validate_wc_registration($errors, $username, $password, $email) { if (!$this->check_submission(true)) { $errors->add( 'honeypot_spam', __('Error: Registration blocked. If you are not a bot, please enable JavaScript and try again.', 'smart-honeypot') ); return $errors; } if (!$this->check_rate_limit()) { $errors->add( 'honeypot_rate', __('Error: Too many registration attempts. Please try again later.', 'smart-honeypot') ); } return $errors; } public function validate_wc_login($errors, $username, $password) { if (!$this->check_submission(true)) { $errors->add( 'honeypot_spam', __('Error: Login blocked. Please enable JavaScript and try again.', 'smart-honeypot') ); } return $errors; } public function validate_wc_checkout($data, $errors) { if (!$this->check_submission(false)) { $errors->add( 'honeypot_spam', __('Order could not be processed. Please try again.', 'smart-honeypot') ); } } /* ------------------------------------------------------------------ * WORDPRESS CORE VALIDATION * ----------------------------------------------------------------*/ public function validate_wp_registration($errors, $sanitized_user_login, $user_email) { if (!$this->check_submission(true)) { $errors->add( 'honeypot_spam', __('Error: Registration blocked. Please enable JavaScript and try again.', 'smart-honeypot') ); } if (!$this->check_rate_limit()) { $errors->add( 'honeypot_rate', __('Error: Too many attempts. Please try again later.', 'smart-honeypot') ); } return $errors; } public function validate_comment($commentdata) { // Don't check admins/editors if (is_user_logged_in() && current_user_can('moderate_comments')) { $this->clean_post_data(); return $commentdata; } if (!$this->check_submission(true)) { wp_die( __('Comment blocked as spam. Please enable JavaScript and try again.', 'smart-honeypot'), __('Spam Detected', 'smart-honeypot'), ['response' => 403, 'back_link' => true] ); } return $commentdata; } /* ------------------------------------------------------------------ * ELEMENTOR VALIDATION * ----------------------------------------------------------------*/ public function validate_elementor_form($record, $ajax_handler) { if (!$this->check_submission(true)) { $ajax_handler->add_error('honeypot', __('Spam detected. Please try again.', 'smart-honeypot')); } } /* ------------------------------------------------------------------ * GENERIC POST VALIDATION (catch-all for other forms) * ----------------------------------------------------------------*/ public function validate_generic_post() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { return; } // Only validate if our honeypot fields are present (don't interfere // with forms we didn't inject into, admin-ajax, REST API, etc.) if (!isset($_POST[$this->hp_name]) && !isset($_POST[$this->token_name])) { return; } // Skip if already handled by a specific hook above if ( isset($_POST['woocommerce-register-nonce']) || isset($_POST['woocommerce-login-nonce']) || isset($_POST['woocommerce-process-checkout-nonce']) || isset($_POST['comment_post_ID']) || (isset($_POST['action']) && $_POST['action'] === 'elementor_pro_forms_send_form') ) { return; } if (!$this->check_submission(false)) { if (wp_doing_ajax()) { wp_send_json_error(['message' => __('Spam detected.', 'smart-honeypot')]); } $ref = wp_get_referer(); wp_safe_redirect($ref ?: home_url()); exit; } } /* ------------------------------------------------------------------ * CSS * ----------------------------------------------------------------*/ public function print_css() { // Use a nondescript class name echo ''; } /* ------------------------------------------------------------------ * JS — generates the proof-of-work token * ----------------------------------------------------------------*/ public function print_js() { $secret = esc_js($this->secret); $token_name = esc_js($this->token_name); $time_name = esc_js($this->time_name); echo << (function(){ function computeToken(ts, secret) { // Simple HMAC-like hash using SubtleCrypto var msg = ts + '|honeypot_js_proof'; var encoder = new TextEncoder(); return crypto.subtle.importKey( 'raw', encoder.encode(secret), {name:'HMAC',hash:'SHA-256'}, false, ['sign'] ).then(function(key){ return crypto.subtle.sign('HMAC', key, encoder.encode(msg)); }).then(function(sig){ var arr = Array.from(new Uint8Array(sig)); var hex = arr.map(function(b){return b.toString(16).padStart(2,'0')}).join(''); return hex.substring(0, 16); }); } function fillTokens() { var forms = document.querySelectorAll('input[name="{$token_name}"]'); forms.forEach(function(input){ var tsInput = input.parentElement.querySelector('input[name="{$time_name}"]'); var ts = tsInput ? tsInput.value : Math.floor(Date.now()/1000).toString(); computeToken(ts, '{$secret}').then(function(hash){ input.value = ts + '|' + hash; }); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fillTokens); } else { fillTokens(); } // Re-fill for dynamically added forms var obs = new MutationObserver(function(mutations){ var found = false; mutations.forEach(function(m){ m.addedNodes.forEach(function(n){ if (n.nodeType === 1 && (n.querySelector && n.querySelector('input[name="{$token_name}"]'))) { found = true; } }); }); if (found) fillTokens(); }); obs.observe(document.body, {childList:true, subtree:true}); })(); JSBLOCK; } /* ------------------------------------------------------------------ * ADMIN NOTICE * ----------------------------------------------------------------*/ public function activation_notice() { if (get_transient('smart_honeypot_activated')) { echo '

Honeypot Fields is now active. All forms are protected automatically.

'; delete_transient('smart_honeypot_activated'); } } } // Boot add_action('plugins_loaded', function () { new SmartHoneypotAntiSpam(); }); // Activation register_activation_hook(__FILE__, function () { set_transient('smart_honeypot_activated', true, 30); }); // Deactivation register_deactivation_hook(__FILE__, function () { delete_transient('smart_honeypot_activated'); }); // Settings link add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { array_unshift($links, 'Documentation'); return $links; });