Files
InformatiQ-Toolkit/includes/class-itk-honeypot.php

280 lines
11 KiB
PHP
Raw Normal View History

<?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;
}
}