+ Central Bot API
+ Send blocked-bot events to your self-hosted Bot Intelligence Docker stack (port 3001).
+
+
+ = esc_html($bot_lbl) ?>
+ 0): ?>
+ Last tested = esc_html(human_time_diff((int)$bot_api['last_verified'])) ?> ago
+
+
+ = esc_html($bot_api['last_error']) ?>
+
+
+
+
+ = esc_html($bot_test_r['message']) ?>
+
+
+ = esc_html($bot_hist_r['message']) ?>
+
+
+
+
+
+
+
+
+
+ Central Honeypot API
+ Send honeypot catch events to your self-hosted Honeypot Intelligence Docker stack (port 3000).
+
+
+ = esc_html($hp_lbl) ?>
+ 0): ?>
+ Last tested = esc_html(human_time_diff((int)$hp_api['last_verified'])) ?> ago
+
+
+ = esc_html($hp_api['last_error']) ?>
+
+
+
+
+ = esc_html($hp_test_r['message']) ?>
+
+
+ = esc_html($hp_hist_r['message']) ?>
+
+
+
+
+
+
false,
+ 'api_url' => '',
+ 'api_token' => '',
+ 'last_sync' => 0,
+ 'sent_total' => 0,
+ 'connection_ok' => null,
+ 'last_verified' => 0,
+ 'last_error' => '',
+ ];
+ }
+
+ public static function settings(): array {
+ return array_merge(self::defaults(), (array)get_option(self::OPT_SETTINGS, []));
+ }
+
+ private static function token(): string {
+ if (defined('ITK_BOT_API_TOKEN') && ITK_BOT_API_TOKEN !== '') {
+ return (string)ITK_BOT_API_TOKEN;
+ }
+ return self::settings()['api_token'] ?? '';
+ }
+
+ /* ── Queue ────────────────────────────────────────────────── */
+
+ /**
+ * Queue one bot event.
+ * Called from ITK_Bot_Blocker after logging to the local DB.
+ */
+ public static function queue(array $data): void {
+ $s = self::settings();
+ if (empty($s['enabled']) || empty($s['api_url'])) return;
+
+ $queue = (array)get_option(self::OPT_QUEUE, []);
+ if (count($queue) >= self::QUEUE_MAX) array_shift($queue);
+
+ $queue[] = [
+ 'ip' => sanitize_text_field($data['ip'] ?? ''),
+ 'bot_type' => sanitize_text_field($data['bot_type'] ?? ''),
+ 'action' => sanitize_text_field($data['action'] ?? 'blocked'),
+ 'reason' => sanitize_text_field($data['reason'] ?? ''),
+ 'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
+ 'request_uri' => sanitize_text_field($data['uri'] ?? ''),
+ 'logged_at' => current_time('mysql'),
+ ];
+
+ update_option(self::OPT_QUEUE, $queue);
+
+ if (count($queue) >= self::BATCH_SIZE) {
+ self::flush();
+ }
+ }
+
+ /* ── Flush ────────────────────────────────────────────────── */
+
+ public static function flush(): void {
+ $s = self::settings();
+ if (empty($s['enabled']) || empty($s['api_url'])) return;
+
+ $queue = (array)get_option(self::OPT_QUEUE, []);
+ if (empty($queue)) return;
+
+ $batch = array_splice($queue, 0, self::BATCH_SIZE);
+ update_option(self::OPT_QUEUE, $queue);
+
+ $headers = ['Content-Type' => 'application/json'];
+ $token = self::token();
+ if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
+
+ $response = wp_remote_post(
+ trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
+ [
+ 'timeout' => 15,
+ 'headers' => $headers,
+ 'body' => wp_json_encode([
+ 'site_hash' => hash('sha256', home_url()),
+ 'bots' => $batch,
+ ]),
+ ]
+ );
+
+ if (is_wp_error($response)) {
+ // Re-queue failed batch
+ update_option(self::OPT_QUEUE, array_merge($batch, $queue));
+ return;
+ }
+
+ $s['last_sync'] = time();
+ $s['sent_total'] = ($s['sent_total'] ?? 0) + count($batch);
+ update_option(self::OPT_SETTINGS, $s);
+ }
+
+ public static function flush_shutdown(): void {
+ $queue = (array)get_option(self::OPT_QUEUE, []);
+ if (count($queue) >= 5) self::flush();
+ }
+
+ /* ── Test connection ──────────────────────────────────────── */
+
+ public static function test_connection(): array {
+ $s = self::settings();
+ if (empty($s['api_url'])) {
+ return ['ok' => false, 'message' => 'No API URL configured.'];
+ }
+
+ $base = trailingslashit(esc_url_raw($s['api_url']));
+ $headers = ['Content-Type' => 'application/json'];
+ $token = self::token();
+ if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
+
+ $health = wp_remote_get($base . 'api/v1/health', ['timeout' => 8]);
+ if (is_wp_error($health)) {
+ return ['ok' => false, 'message' => 'Cannot reach API: ' . $health->get_error_message()];
+ }
+ if (wp_remote_retrieve_response_code($health) !== 200) {
+ return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($health)];
+ }
+
+ // Token check with intentionally bad payload
+ $auth = wp_remote_post($base . 'api/v1/submit', [
+ 'timeout' => 8,
+ 'headers' => $headers,
+ 'body' => wp_json_encode(['site_hash' => 'connectivity_test', 'bots' => []]),
+ ]);
+ if (is_wp_error($auth)) {
+ return ['ok' => false, 'message' => 'Token check failed: ' . $auth->get_error_message()];
+ }
+ if (wp_remote_retrieve_response_code($auth) === 403) {
+ return ['ok' => false, 'message' => 'API reachable but token rejected (HTTP 403).'];
+ }
+
+ return ['ok' => true, 'message' => 'Connection verified. API is reachable and token accepted.'];
+ }
+
+ /* ── Send history batch ───────────────────────────────────── */
+
+ public static function send_history_batch(int $batch_size = 50): array {
+ $s = self::settings();
+ if (empty($s['api_url'])) {
+ return ['ok' => false, 'message' => 'No API URL configured.'];
+ }
+
+ global $wpdb;
+ $table = ITK_Database::bot_table();
+ $last_id = (int)get_option('itk_bot_history_last_id', 0);
+ $total = ITK_Database::count_bot_rows();
+
+ $rows = $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT %d",
+ $last_id, $batch_size
+ ), ARRAY_A);
+
+ if (empty($rows)) {
+ return ['ok' => true, 'sent' => 0, 'remaining' => 0, 'has_more' => false,
+ 'message' => 'All records have already been sent.'];
+ }
+
+ $bots = array_map(fn($r) => [
+ 'ip' => $r['ip_address'],
+ 'bot_type' => $r['bot_type'],
+ 'action' => $r['action'],
+ 'reason' => $r['reason'],
+ 'user_agent' => $r['user_agent'],
+ 'request_uri' => $r['request_uri'],
+ 'logged_at' => $r['logged_at'],
+ ], $rows);
+
+ $headers = ['Content-Type' => 'application/json'];
+ $token = self::token();
+ if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
+
+ $response = wp_remote_post(
+ trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
+ [
+ 'timeout' => 30,
+ 'headers' => $headers,
+ 'body' => wp_json_encode([
+ 'site_hash' => hash('sha256', home_url()),
+ 'bots' => $bots,
+ ]),
+ ]
+ );
+
+ if (is_wp_error($response)) {
+ return ['ok' => false, 'message' => 'Request failed: ' . $response->get_error_message()];
+ }
+ if (wp_remote_retrieve_response_code($response) !== 200) {
+ return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($response)];
+ }
+
+ $new_last = (int)end($rows)['id'];
+ $sent_total = (int)get_option('itk_bot_history_sent', 0) + count($rows);
+ update_option('itk_bot_history_last_id', $new_last);
+ update_option('itk_bot_history_sent', $sent_total);
+ $remaining = max(0, $total - $sent_total);
+
+ return [
+ 'ok' => true,
+ 'sent' => count($rows),
+ 'remaining' => $remaining,
+ 'has_more' => $remaining > 0,
+ 'message' => sprintf('Sent %d records. %d remaining.', count($rows), $remaining),
+ ];
+ }
+}
diff --git a/includes/class-itk-bot-blocker.php b/includes/class-itk-bot-blocker.php
index 765df61..2489c73 100644
--- a/includes/class-itk-bot-blocker.php
+++ b/includes/class-itk-bot-blocker.php
@@ -124,7 +124,7 @@ class ITK_Bot_Blocker {
if ($count >= $limit) {
// Over the limit – log and send 429.
if (!empty($options['log_blocked_attempts'])) {
- ITK_Database::log_bot([
+ $event = [
'ip' => $ip,
'ua' => $ua,
'referrer' => '',
@@ -132,7 +132,9 @@ class ITK_Bot_Blocker {
'bot_type' => $name,
'reason' => "Rate limited: {$count}/{$limit} req/min",
'action' => 'rate_limited',
- ]);
+ ];
+ ITK_Database::log_bot($event);
+ ITK_Bot_API::queue($event);
}
status_header(429);
header('Retry-After: 60');
@@ -157,7 +159,7 @@ class ITK_Bot_Blocker {
array $options
): void {
if (!empty($options['log_blocked_attempts'])) {
- ITK_Database::log_bot([
+ $event = [
'ip' => $ip,
'ua' => $ua,
'referrer' => $referrer,
@@ -165,7 +167,9 @@ class ITK_Bot_Blocker {
'bot_type' => $bot_type,
'reason' => $reason,
'action' => 'blocked',
- ]);
+ ];
+ ITK_Database::log_bot($event);
+ ITK_Bot_API::queue($event);
}
$code = $options['response_code'] ?? '403';
diff --git a/includes/class-itk-honeypot.php b/includes/class-itk-honeypot.php
index 45fd11a..5a5c876 100644
--- a/includes/class-itk-honeypot.php
+++ b/includes/class-itk-honeypot.php
@@ -174,13 +174,15 @@ class ITK_Honeypot {
}
private function log_block(string $form_type, string $reason): void {
- ITK_Database::log_honeypot([
+ $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 {
diff --git a/includes/class-itk-hp-api.php b/includes/class-itk-hp-api.php
new file mode 100644
index 0000000..7946b1f
--- /dev/null
+++ b/includes/class-itk-hp-api.php
@@ -0,0 +1,243 @@
+ false,
+ 'api_url' => '',
+ 'api_token' => '',
+ 'last_sync' => 0,
+ 'sent_total' => 0,
+ 'connection_ok' => null,
+ 'last_verified' => 0,
+ 'last_error' => '',
+ ];
+ }
+
+ public static function settings(): array {
+ return array_merge(self::defaults(), (array)get_option(self::OPT_SETTINGS, []));
+ }
+
+ private static function token(): string {
+ if (defined('ITK_HP_API_TOKEN') && ITK_HP_API_TOKEN !== '') {
+ return (string)ITK_HP_API_TOKEN;
+ }
+ return self::settings()['api_token'] ?? '';
+ }
+
+ /* ── Queue ────────────────────────────────────────────────── */
+
+ /**
+ * Queue one honeypot block event.
+ * Called from ITK_Honeypot::log_block() when API is enabled.
+ */
+ public static function queue(array $event): void {
+ $s = self::settings();
+ if (empty($s['enabled']) || empty($s['api_url'])) return;
+
+ $queue = (array)get_option(self::OPT_QUEUE, []);
+ if (count($queue) >= self::QUEUE_MAX) array_shift($queue);
+
+ $queue[] = [
+ 'ip' => sanitize_text_field($event['ip'] ?? ''),
+ 'form_type' => sanitize_text_field($event['form'] ?? 'Unknown'),
+ 'reason' => sanitize_text_field($event['reason'] ?? ''),
+ 'user_agent' => sanitize_textarea_field($event['ua'] ?? ''),
+ 'blocked_at' => current_time('mysql'),
+ ];
+
+ update_option(self::OPT_QUEUE, $queue);
+
+ // Auto-flush when batch is ready
+ if (count($queue) >= self::BATCH_SIZE) {
+ self::flush();
+ }
+ }
+
+ /* ── Flush ────────────────────────────────────────────────── */
+
+ public static function flush(): void {
+ $s = self::settings();
+ if (empty($s['enabled']) || empty($s['api_url'])) return;
+
+ $queue = (array)get_option(self::OPT_QUEUE, []);
+ if (empty($queue)) return;
+
+ $batch = array_splice($queue, 0, self::BATCH_SIZE);
+ update_option(self::OPT_QUEUE, $queue); // save remainder
+
+ $headers = ['Content-Type' => 'application/json'];
+ $token = self::token();
+ if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
+
+ $response = wp_remote_post(
+ trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
+ [
+ 'timeout' => 15,
+ 'headers' => $headers,
+ 'body' => wp_json_encode([
+ 'site_hash' => hash('sha256', home_url()),
+ 'blocks' => $batch,
+ ]),
+ ]
+ );
+
+ if (is_wp_error($response)) {
+ // Re-queue failed batch at the front
+ $queue = array_merge($batch, $queue);
+ update_option(self::OPT_QUEUE, $queue);
+ return;
+ }
+
+ $s['last_sync'] = time();
+ $s['sent_total'] = ($s['sent_total'] ?? 0) + count($batch);
+ update_option(self::OPT_SETTINGS, $s);
+ }
+
+ /**
+ * Emergency flush on PHP shutdown (small queue only).
+ */
+ public static function flush_shutdown(): void {
+ $queue = (array)get_option(self::OPT_QUEUE, []);
+ if (count($queue) >= 5) self::flush();
+ }
+
+ /* ── Test connection ──────────────────────────────────────── */
+
+ public static function test_connection(): array {
+ $s = self::settings();
+ if (empty($s['api_url'])) {
+ return ['ok' => false, 'message' => 'No API URL configured.'];
+ }
+
+ $base = trailingslashit(esc_url_raw($s['api_url']));
+ $headers = ['Content-Type' => 'application/json'];
+ $token = self::token();
+ if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
+
+ // Step 1: reachability
+ $health = wp_remote_get($base . 'api/v1/health', ['timeout' => 8]);
+ if (is_wp_error($health)) {
+ return ['ok' => false, 'message' => 'Cannot reach API: ' . $health->get_error_message()];
+ }
+ if (wp_remote_retrieve_response_code($health) !== 200) {
+ return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($health)];
+ }
+
+ // Step 2: token check (intentionally bad payload → 400 = auth OK, 403 = wrong token)
+ $auth = wp_remote_post($base . 'api/v1/submit', [
+ 'timeout' => 8,
+ 'headers' => $headers,
+ 'body' => wp_json_encode(['site_hash' => 'connectivity_test', 'blocks' => []]),
+ ]);
+ if (is_wp_error($auth)) {
+ return ['ok' => false, 'message' => 'Token check failed: ' . $auth->get_error_message()];
+ }
+ if (wp_remote_retrieve_response_code($auth) === 403) {
+ return ['ok' => false, 'message' => 'API reachable but token rejected (HTTP 403).'];
+ }
+
+ return ['ok' => true, 'message' => 'Connection verified. API is reachable and token accepted.'];
+ }
+
+ /* ── Send history batch ───────────────────────────────────── */
+
+ public static function send_history_batch(int $batch_size = 50): array {
+ $s = self::settings();
+ if (empty($s['api_url'])) {
+ return ['ok' => false, 'message' => 'No API URL configured.'];
+ }
+
+ global $wpdb;
+ $table = ITK_Database::honeypot_table();
+ $last_id = (int)get_option('itk_hp_history_last_id', 0);
+ $total = ITK_Database::count_honeypot_rows();
+
+ $rows = $wpdb->get_results($wpdb->prepare(
+ "SELECT * FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT %d",
+ $last_id, $batch_size
+ ), ARRAY_A);
+
+ if (empty($rows)) {
+ return ['ok' => true, 'sent' => 0, 'remaining' => 0, 'has_more' => false,
+ 'message' => 'All records have already been sent.'];
+ }
+
+ $blocks = array_map(fn($r) => [
+ 'ip' => $r['ip_address'],
+ 'form_type' => $r['form_type'],
+ 'reason' => $r['reason'],
+ 'user_agent' => $r['user_agent'],
+ 'blocked_at' => $r['blocked_at'],
+ ], $rows);
+
+ $headers = ['Content-Type' => 'application/json'];
+ $token = self::token();
+ if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
+
+ $response = wp_remote_post(
+ trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
+ [
+ 'timeout' => 30,
+ 'headers' => $headers,
+ 'body' => wp_json_encode([
+ 'site_hash' => hash('sha256', home_url()),
+ 'blocks' => $blocks,
+ ]),
+ ]
+ );
+
+ if (is_wp_error($response)) {
+ return ['ok' => false, 'message' => 'Request failed: ' . $response->get_error_message()];
+ }
+ if (wp_remote_retrieve_response_code($response) !== 200) {
+ return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($response)];
+ }
+
+ $new_last = (int)end($rows)['id'];
+ $sent_total = (int)get_option('itk_hp_history_sent', 0) + count($rows);
+ update_option('itk_hp_history_last_id', $new_last);
+ update_option('itk_hp_history_sent', $sent_total);
+ $remaining = max(0, $total - $sent_total);
+
+ return [
+ 'ok' => true,
+ 'sent' => count($rows),
+ 'remaining' => $remaining,
+ 'has_more' => $remaining > 0,
+ 'message' => sprintf('Sent %d records. %d remaining.', count($rows), $remaining),
+ ];
+ }
+}
diff --git a/informatiq-toolkit.php b/informatiq-toolkit.php
index 2ecec60..df9d611 100644
--- a/informatiq-toolkit.php
+++ b/informatiq-toolkit.php
@@ -21,6 +21,8 @@ define('ITK_URL', plugin_dir_url(__FILE__));
define('ITK_BASENAME', plugin_basename(__FILE__));
require_once ITK_PATH . 'includes/class-itk-database.php';
+require_once ITK_PATH . 'includes/class-itk-hp-api.php';
+require_once ITK_PATH . 'includes/class-itk-bot-api.php';
require_once ITK_PATH . 'includes/class-itk-bot-blocker.php';
require_once ITK_PATH . 'includes/class-itk-protection.php';
require_once ITK_PATH . 'includes/class-itk-optimization.php';
@@ -39,6 +41,18 @@ class InformatiQ_Toolkit {
}
private function __construct() {
+ // Register custom cron interval (5 minutes)
+ add_filter('cron_schedules', function ($schedules) {
+ if (!isset($schedules['itk_5min'])) {
+ $schedules['itk_5min'] = ['interval' => 300, 'display' => 'Every 5 Minutes'];
+ }
+ return $schedules;
+ });
+
+ // Boot API cron flushers
+ ITK_HP_API::register_cron();
+ ITK_Bot_API::register_cron();
+
new ITK_Bot_Blocker();
new ITK_Protection();
new ITK_Optimization();
@@ -137,6 +151,8 @@ class InformatiQ_Toolkit {
}
public static function deactivate() {
+ ITK_HP_API::clear_cron();
+ ITK_Bot_API::clear_cron();
flush_rewrite_rules();
}
}