Files
InformatiQ-Toolkit/includes/class-itk-honeypot.php
Malin a8d7972ad7 feat: add Central API clients, bot rate limiting, and admin API UI
- Add ITK_HP_API and ITK_Bot_API static classes with queue/flush/cron
- Add WP-Cron (5 min) + shutdown flush for both API queues
- Bot Blocker and Honeypot now queue events to their respective APIs
- Admin: Bot Blocker tab gains Central Bot API settings panel
  (enable, URL, token, test connection, flush queue, historical sync)
- Admin: Honeypot tab gains Central Honeypot API settings panel
- Admin JS: AJAX handlers for Test Connection and Flush Now buttons
- Admin CSS: API card styles (status badge, notices, footer controls)
- Add .gitignore (excludes bot-api/ which lives in CloudHost/bot-api)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 18:32:27 +02:00

280 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 {
$event = [
'ip' => $this->get_ip(),
'form' => $form_type,
'reason' => $reason,
'uri' => $_SERVER['REQUEST_URI'] ?? '',
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
];
ITK_Database::log_honeypot($event);
ITK_HP_API::queue($event);
}
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;
}
}