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>
278 lines
11 KiB
PHP
278 lines
11 KiB
PHP
<?php
|
||
if (!defined('ABSPATH')) exit;
|
||
|
||
/**
|
||
* ITK Honeypot
|
||
*
|
||
* Ported from HoneypotFields v2.4.0.
|
||
* Injects invisible honeypot fields into all major form types and blocks
|
||
* submissions that fill them (bots) or arrive too fast/too slow.
|
||
* Uses ITK_Database for logging instead of a separate table.
|
||
*/
|
||
class ITK_Honeypot {
|
||
|
||
const FIELD_PREFIX = '_hp_trap_';
|
||
const TOKEN_FIELD = '_hp_token';
|
||
const TIME_FIELD = '_hp_ts';
|
||
|
||
private static array $opts = [];
|
||
|
||
public function __construct() {
|
||
self::$opts = get_option('itk_honeypot', []);
|
||
|
||
if (empty(self::$opts['enabled'])) return;
|
||
|
||
// Inject honeypot into forms
|
||
add_action('comment_form', [$this, 'inject_comment']);
|
||
add_action('login_form', [$this, 'inject_generic']);
|
||
add_action('register_form', [$this, 'inject_generic']);
|
||
add_action('lostpassword_form', [$this, 'inject_generic']);
|
||
add_action('wp_head', [$this, 'inject_honeypot_style']);
|
||
|
||
// Validate on submission
|
||
add_filter('preprocess_comment', [$this, 'validate_comment'], 1);
|
||
add_action('authenticate', [$this, 'validate_login'], 1, 3);
|
||
add_action('register_post', [$this, 'validate_register'], 1, 3);
|
||
add_action('lostpassword_post', [$this, 'validate_lost_password'], 1);
|
||
|
||
// WooCommerce
|
||
if (!empty(self::$opts['protect_woocommerce']) && class_exists('WooCommerce')) {
|
||
add_action('woocommerce_checkout_before_customer_details', [$this, 'inject_generic']);
|
||
add_action('woocommerce_checkout_process', [$this, 'validate_woo_checkout']);
|
||
add_action('woocommerce_register_form', [$this, 'inject_generic']);
|
||
add_action('woocommerce_process_registration_errors', [$this, 'validate_woo_registration'], 10, 3);
|
||
add_action('woocommerce_login_form', [$this, 'inject_generic']);
|
||
}
|
||
|
||
// Contact Form 7
|
||
if (!empty(self::$opts['protect_cf7']) && class_exists('WPCF7')) {
|
||
add_action('wpcf7_form_elements', [$this, 'inject_cf7']);
|
||
add_filter('wpcf7_before_send_mail', [$this, 'validate_cf7'], 10, 3);
|
||
}
|
||
|
||
// Elementor forms
|
||
if (!empty(self::$opts['protect_elementor']) && defined('ELEMENTOR_VERSION')) {
|
||
add_action('elementor/frontend/after_enqueue_scripts', [$this, 'elementor_enqueue']);
|
||
add_action('elementor_pro/forms/validation', [$this, 'validate_elementor'], 10, 2);
|
||
}
|
||
|
||
// Gravity Forms
|
||
if (!empty(self::$opts['protect_gravity']) && class_exists('GFForms')) {
|
||
add_filter('gform_form_tag', [$this, 'inject_gravity'], 10, 2);
|
||
add_filter('gform_validation', [$this, 'validate_gravity']);
|
||
}
|
||
|
||
// Search form
|
||
if (!empty(self::$opts['protect_search'])) {
|
||
add_action('get_search_form', [$this, 'inject_search']);
|
||
}
|
||
|
||
// Enqueue JS token generator
|
||
add_action('wp_enqueue_scripts', [$this, 'enqueue_token_script']);
|
||
}
|
||
|
||
/* ── Style (hide honeypot fields) ─────────────────────────── */
|
||
|
||
public function inject_honeypot_style(): void {
|
||
echo '<style>.itk-hp-field{display:none!important;visibility:hidden!important;opacity:0!important;position:absolute!important;left:-9999px!important;top:-9999px!important;}</style>' . "\n";
|
||
}
|
||
|
||
/* ── JS token (HMAC-based anti-CSRF) ─────────────────────── */
|
||
|
||
public function enqueue_token_script(): void {
|
||
$secret = $this->get_page_secret();
|
||
wp_add_inline_script('jquery-core', "
|
||
(function(){
|
||
var s='" . esc_js($secret) . "',n='" . esc_js(wp_create_nonce('itk_hp')) . "';
|
||
document.querySelectorAll('." . self::TOKEN_FIELD . "').forEach(function(f){
|
||
f.value=btoa(s+'|'+Date.now()+'|'+n);
|
||
});
|
||
document.querySelectorAll('." . self::TIME_FIELD . "').forEach(function(f){
|
||
f.value=Math.floor(Date.now()/1000);
|
||
});
|
||
})();
|
||
", 'after');
|
||
}
|
||
|
||
/* ── Field HTML generator ─────────────────────────────────── */
|
||
|
||
private function honeypot_html(string $form_type = ''): string {
|
||
$field = self::FIELD_PREFIX . substr(md5(uniqid()), 0, 8);
|
||
$ts = time();
|
||
$label = ['Your email address', 'Website URL', 'Full name'][array_rand(['a','b','c'])];
|
||
return sprintf(
|
||
'<div class="itk-hp-field" aria-hidden="true" tabindex="-1" style="display:none!important">
|
||
<label>%s <input type="text" name="%s" value="" autocomplete="off" tabindex="-1"></label>
|
||
<input type="hidden" name="%s" class="%s" value="">
|
||
<input type="hidden" name="%s" class="%s" value="%d">
|
||
</div>',
|
||
esc_html($label),
|
||
esc_attr($field),
|
||
self::TOKEN_FIELD, self::TOKEN_FIELD,
|
||
self::TIME_FIELD, self::TIME_FIELD,
|
||
$ts
|
||
);
|
||
}
|
||
|
||
/* ── Injectors ────────────────────────────────────────────── */
|
||
|
||
public function inject_generic(): void {
|
||
if (empty(self::$opts['enabled'])) return;
|
||
echo $this->honeypot_html(); // phpcs:ignore
|
||
}
|
||
|
||
public function inject_comment(): void {
|
||
if (empty(self::$opts['protect_comments'])) return;
|
||
echo $this->honeypot_html('comment'); // phpcs:ignore
|
||
}
|
||
|
||
public function inject_search(string $form): string {
|
||
return $form . $this->honeypot_html('search');
|
||
}
|
||
|
||
public function inject_cf7(string $content): string {
|
||
return $content . $this->honeypot_html('cf7');
|
||
}
|
||
|
||
public function inject_gravity(string $tag, array $form): string {
|
||
return $tag . $this->honeypot_html('gravity');
|
||
}
|
||
|
||
public function elementor_enqueue(): void {
|
||
// Elementor injects via JS – add hidden fields via wp_footer
|
||
add_action('wp_footer', [$this, 'inject_generic']);
|
||
}
|
||
|
||
/* ── Validators ───────────────────────────────────────────── */
|
||
|
||
private function check_honeypot(string $form_type): bool {
|
||
// 1. Honeypot field must be empty
|
||
foreach ($_POST as $key => $val) {
|
||
if (strpos($key, self::FIELD_PREFIX) === 0 && !empty($val)) {
|
||
$this->log_block($form_type, 'Honeypot field filled');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 2. Timing check
|
||
$opts = get_option('itk_honeypot', []);
|
||
$min_t = max(1, (int)($opts['min_time'] ?? 3));
|
||
$max_t = max(60, (int)($opts['max_time'] ?? 7200));
|
||
$ts = (int)($_POST[self::TIME_FIELD] ?? 0);
|
||
$elapsed = time() - $ts;
|
||
|
||
if ($ts > 0 && $elapsed < $min_t) {
|
||
$this->log_block($form_type, "Submitted too fast ({$elapsed}s)");
|
||
return false;
|
||
}
|
||
if ($ts > 0 && $elapsed > $max_t) {
|
||
$this->log_block($form_type, "Submitted too slow ({$elapsed}s > {$max_t}s)");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private function log_block(string $form_type, string $reason): void {
|
||
ITK_Database::log_honeypot([
|
||
'ip' => $this->get_ip(),
|
||
'form' => $form_type,
|
||
'reason' => $reason,
|
||
'uri' => $_SERVER['REQUEST_URI'] ?? '',
|
||
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||
]);
|
||
}
|
||
|
||
public function validate_comment(array $comment_data): array {
|
||
if (empty(self::$opts['protect_comments'])) return $comment_data;
|
||
if (!$this->check_honeypot('comment')) {
|
||
wp_die('Spam detected. Please go back and try again.', 'Spam Blocked', ['response' => 403]);
|
||
}
|
||
return $comment_data;
|
||
}
|
||
|
||
public function validate_login($user, string $username, string $password) {
|
||
if (empty(self::$opts['protect_login'])) return $user;
|
||
if (!empty($username) && !$this->check_honeypot('login')) {
|
||
return new WP_Error('honeypot_blocked', 'Access denied.');
|
||
}
|
||
return $user;
|
||
}
|
||
|
||
public function validate_register($sanitized_user_login, $user_email, \WP_Error $errors): void {
|
||
if (empty(self::$opts['protect_register'])) return;
|
||
if (!$this->check_honeypot('register')) {
|
||
$errors->add('honeypot_blocked', 'Spam registration detected.');
|
||
}
|
||
}
|
||
|
||
public function validate_lost_password(\WP_Error $errors): void {
|
||
if (empty(self::$opts['protect_lost_password'])) return;
|
||
if (!$this->check_honeypot('lostpassword')) {
|
||
$errors->add('honeypot_blocked', 'Access denied.');
|
||
}
|
||
}
|
||
|
||
public function validate_woo_checkout(): void {
|
||
if (!$this->check_honeypot('woo_checkout')) {
|
||
wc_add_notice('Spam submission detected. Please refresh and try again.', 'error');
|
||
}
|
||
}
|
||
|
||
public function validate_woo_registration(\WP_Error $errors, $username, $email): \WP_Error {
|
||
if (!$this->check_honeypot('woo_register')) {
|
||
$errors->add('honeypot_blocked', 'Spam registration detected.');
|
||
}
|
||
return $errors;
|
||
}
|
||
|
||
public function validate_cf7($contact_form, &$abort, $submission): void {
|
||
if (!$this->check_honeypot('cf7')) {
|
||
$abort = true;
|
||
$contact_form->set_status('spam');
|
||
}
|
||
}
|
||
|
||
public function validate_elementor($record, $ajax_handler): void {
|
||
if (!$this->check_honeypot('elementor')) {
|
||
$ajax_handler->add_error_message('Spam detected. Please try again.');
|
||
}
|
||
}
|
||
|
||
public function validate_gravity(array $validation_result): array {
|
||
if (!$this->check_honeypot('gravity')) {
|
||
$validation_result['is_valid'] = false;
|
||
foreach ($validation_result['form']['fields'] as &$field) {
|
||
$field->failed_validation = true;
|
||
$field->validation_message = 'Spam detected.';
|
||
}
|
||
}
|
||
return $validation_result;
|
||
}
|
||
|
||
/* ── Helpers ──────────────────────────────────────────────── */
|
||
|
||
private function get_ip(): string {
|
||
$keys = [
|
||
'HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED',
|
||
'HTTP_X_CLUSTER_CLIENT_IP','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR',
|
||
];
|
||
foreach ($keys as $k) {
|
||
if (!empty($_SERVER[$k])) {
|
||
$ip = trim(explode(',', $_SERVER[$k])[0]);
|
||
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
|
||
}
|
||
}
|
||
return 'UNKNOWN';
|
||
}
|
||
|
||
private function get_page_secret(): string {
|
||
$secret = get_option('itk_hp_secret');
|
||
if (!$secret) {
|
||
$secret = wp_generate_password(32, false);
|
||
update_option('itk_hp_secret', $secret);
|
||
}
|
||
return $secret;
|
||
}
|
||
}
|