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), ]; } }