feat: i18n (EN/ES/RO), HP_API_TOKEN constant, EU footer v2.3.0

- SmartHoneypotI18n class with EN/ES/RO string tables and flag switcher
- All WP admin UI strings translated (logs tab, settings tab, notices)
- Language preference stored per-user in user meta (hp_lang)
- Browser language auto-detection with localStorage persistence
- HP_API_TOKEN constant support: define in wp-config.php to keep
  token out of the database; UI shows read-only note when active
- resolve_token() checks constant first, falls back to DB setting
- API dashboard: EN/ES/RO language switcher with flag buttons
- API dashboard: data-i18n attributes on all static UI elements
- API dashboard: EU footer "Made & hosted in the EU by Cloud Host"
  with link to cloudhost.es
- Bump version to 2.3.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 20:34:35 +01:00
parent 92e0522a03
commit f1c32e5060
2 changed files with 584 additions and 132 deletions

View File

@@ -3,7 +3,7 @@
* Plugin Name: Honeypot Fields
* Plugin URI: https://informatiq.services
* Description: Adds invisible honeypot fields to all forms to block spam bots. Works with WordPress core forms, Elementor, Gravity Forms, Contact Form 7, WooCommerce, and more.
* Version: 2.2.0
* Version: 2.3.0
* Author: Malin
* Author URI: https://malin.ro
* License: GPL v2 or later
@@ -184,8 +184,9 @@ class SmartHoneypotAPIClient {
$base = trailingslashit(esc_url_raw($s['api_url']));
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
// Step 1 — reachability
@@ -256,8 +257,9 @@ class SmartHoneypotAPIClient {
], $rows);
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$response = wp_remote_post(
@@ -329,8 +331,9 @@ class SmartHoneypotAPIClient {
$site_hash = hash('sha256', home_url());
$headers = ['Content-Type' => 'application/json'];
if (!empty($s['api_token'])) {
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$response = wp_remote_post(
@@ -358,6 +361,292 @@ class SmartHoneypotAPIClient {
public static function queue_size(): int {
return count((array) get_option(self::OPT_QUEUE, []));
}
/**
* Returns the API token.
* Checks the HP_API_TOKEN constant (defined in wp-config.php) first,
* then falls back to the value stored in the database.
*/
private static function resolve_token(): string {
if (defined('HP_API_TOKEN') && HP_API_TOKEN !== '') {
return (string) HP_API_TOKEN;
}
return self::settings()['api_token'] ?? '';
}
}
/* ======================================================================
* INTERNATIONALISATION
* ====================================================================*/
class SmartHoneypotI18n {
const USER_META = 'hp_lang';
// phpcs:disable Generic.Files.LineLength
const STRINGS = [
'en' => [
'tab_logs' => 'Blocked Logs',
'tab_api' => 'Central API',
'cleared' => 'Logs cleared.',
'saved' => 'API settings saved.',
'flushed' => 'Queue flushed to central API.',
'history_reset_msg' => 'History sync progress has been reset. You can re-send from the beginning.',
'click_next' => 'Click "Send Next Batch" to continue.',
'total_blocked' => 'Total Blocked',
'today' => 'Today',
'unique_ips' => 'Unique IPs',
'form_types_hit' => 'Form Types Hit',
'search_ph' => 'Search IP, UA, reason…',
'filter_ip_ph' => 'Filter by IP',
'all_forms' => 'All form types',
'filter_btn' => 'Filter',
'reset_btn' => 'Reset',
'clear_logs' => 'Clear All Logs',
'clear_confirm' => 'Delete ALL log entries permanently?',
'col_date' => 'Date / Time',
'col_ip' => 'IP Address',
'col_form' => 'Form Type',
'col_reason' => 'Reason',
'col_uri' => 'URI',
'col_ua' => 'User Agent',
'no_results' => 'No blocked attempts recorded yet.',
'filter_link' => 'filter',
'lookup_link' => 'lookup ↗',
'prev' => '← Prev',
'next' => 'Next →',
'showing' => 'Showing',
'results' => 'result(s)',
'page_of' => 'page %d of %d',
'settings_title' => 'Central API Settings',
'settings_desc' => 'Submit blocked attempts to a central dashboard for aggregate threat intelligence. Data sent: full IP address, form type, block reason, UA family, and timestamp. No site URL or usernames are ever sent.',
'enable_label' => 'Enable Submission',
'enable_cb' => 'Send blocked attempts to the central API',
'url_label' => 'API Endpoint URL',
'url_desc' => 'Base URL of your Honeypot API Docker container.',
'token_label' => 'API Token',
'token_desc' => 'Must match the <code>API_TOKEN</code> set in your Docker container\'s environment. Leave empty only if the API runs without a token (not recommended).',
'token_const_note' => 'Token is set via the <code>HP_API_TOKEN</code> constant in <code>wp-config.php</code>.',
'save_settings' => 'Save Settings',
'conn_status' => 'Connection Status',
'conn_label' => 'Connection',
'conn_ok' => 'Verified — connected and token accepted',
'conn_fail' => 'Connection failed',
'conn_untested' => 'Not tested yet',
'last_verified' => 'Last Verified',
'enabled_lbl' => 'Enabled',
'yes' => 'Yes',
'no' => 'No',
'last_sync' => 'Last Sync',
'never' => 'Never',
'total_sent' => 'Total Sent',
'blocks_unit' => 'blocks',
'queue_size' => 'Queue Size',
'pending_unit' => 'pending',
'next_flush' => 'Next Auto-Flush',
'not_scheduled' => 'Not scheduled',
'test_conn' => 'Test Connection',
'flush_queue' => 'Flush Queue Now',
'history_title' => 'Send History to API',
'history_desc' => 'Populate the central dashboard with your existing log so charts and stats are meaningful right away, without waiting for new attacks. Records are sent in batches of 50.',
'local_log' => 'Local Log',
'local_log_val' => '%s records in this site\'s database',
'sent_api' => 'Sent to API',
'remaining_lbl' => 'Remaining',
'remaining_val' => '%s records not yet sent',
'send_history' => 'Send History',
'send_next' => 'Send Next Batch',
'all_sent' => '✓ All history sent',
'reset_progress' => 'Reset Progress',
],
'es' => [
'tab_logs' => 'Registros Bloqueados',
'tab_api' => 'API Central',
'cleared' => 'Registros eliminados.',
'saved' => 'Configuración de API guardada.',
'flushed' => 'Cola enviada a la API central.',
'history_reset_msg' => 'El progreso de sincronización se ha restablecido. Puedes reenviar desde el principio.',
'click_next' => 'Haz clic en "Enviar Siguiente Lote" para continuar.',
'total_blocked' => 'Total Bloqueados',
'today' => 'Hoy',
'unique_ips' => 'IPs Únicas',
'form_types_hit' => 'Tipos de Formulario',
'search_ph' => 'Buscar IP, UA, razón…',
'filter_ip_ph' => 'Filtrar por IP',
'all_forms' => 'Todos los tipos',
'filter_btn' => 'Filtrar',
'reset_btn' => 'Reiniciar',
'clear_logs' => 'Borrar Todos los Registros',
'clear_confirm' => '¿Eliminar TODOS los registros permanentemente?',
'col_date' => 'Fecha / Hora',
'col_ip' => 'Dirección IP',
'col_form' => 'Tipo de Formulario',
'col_reason' => 'Razón',
'col_uri' => 'URI',
'col_ua' => 'User Agent',
'no_results' => 'No hay intentos bloqueados aún.',
'filter_link' => 'filtrar',
'lookup_link' => 'consultar ↗',
'prev' => '← Anterior',
'next' => 'Siguiente →',
'showing' => 'Mostrando',
'results' => 'resultado(s)',
'page_of' => 'página %d de %d',
'settings_title' => 'Configuración de la API Central',
'settings_desc' => 'Envía intentos bloqueados a un panel central de inteligencia de amenazas. Se envía: IP completa, tipo de formulario, razón de bloqueo, familia de UA y marca de tiempo. Nunca se envían URLs del sitio ni nombres de usuario.',
'enable_label' => 'Activar Envío',
'enable_cb' => 'Enviar intentos bloqueados a la API central',
'url_label' => 'URL del Endpoint de la API',
'url_desc' => 'URL base del contenedor Docker de la API Honeypot.',
'token_label' => 'Token de la API',
'token_desc' => 'Debe coincidir con <code>API_TOKEN</code> en el entorno del contenedor Docker.',
'token_const_note' => 'El token está definido mediante la constante <code>HP_API_TOKEN</code> en <code>wp-config.php</code>.',
'save_settings' => 'Guardar Configuración',
'conn_status' => 'Estado de la Conexión',
'conn_label' => 'Conexión',
'conn_ok' => 'Verificada — conectada y token aceptado',
'conn_fail' => 'Error de conexión',
'conn_untested' => 'No probada todavía',
'last_verified' => 'Última Verificación',
'enabled_lbl' => 'Activado',
'yes' => 'Sí',
'no' => 'No',
'last_sync' => 'Última Sincronización',
'never' => 'Nunca',
'total_sent' => 'Total Enviados',
'blocks_unit' => 'bloqueos',
'queue_size' => 'Tamaño de Cola',
'pending_unit' => 'pendientes',
'next_flush' => 'Próximo Envío Automático',
'not_scheduled' => 'No programado',
'test_conn' => 'Probar Conexión',
'flush_queue' => 'Enviar Cola Ahora',
'history_title' => 'Enviar Historial a la API',
'history_desc' => 'Rellena el panel central con tu registro existente para que las estadísticas sean significativas de inmediato, sin esperar nuevos ataques. Los registros se envían en lotes de 50.',
'local_log' => 'Registro Local',
'local_log_val' => '%s registros en la base de datos de este sitio',
'sent_api' => 'Enviados a la API',
'remaining_lbl' => 'Restantes',
'remaining_val' => '%s registros aún no enviados',
'send_history' => 'Enviar Historial',
'send_next' => 'Enviar Siguiente Lote',
'all_sent' => '✓ Todo el historial enviado',
'reset_progress' => 'Reiniciar Progreso',
],
'ro' => [
'tab_logs' => 'Jurnale Blocate',
'tab_api' => 'API Central',
'cleared' => 'Jurnale șterse.',
'saved' => 'Setări API salvate.',
'flushed' => 'Coada trimisă la API-ul central.',
'history_reset_msg' => 'Progresul sincronizării a fost resetat. Poți retrimite de la început.',
'click_next' => 'Apasă „Trimite Lotul Următor" pentru a continua.',
'total_blocked' => 'Total Blocate',
'today' => 'Azi',
'unique_ips' => 'IP-uri Unice',
'form_types_hit' => 'Tipuri de Formulare Atacate',
'search_ph' => 'Caută IP, UA, motiv…',
'filter_ip_ph' => 'Filtrează după IP',
'all_forms' => 'Toate tipurile',
'filter_btn' => 'Filtrează',
'reset_btn' => 'Resetează',
'clear_logs' => 'Șterge Toate Jurnalele',
'clear_confirm' => 'Ștergi TOATE înregistrările permanent?',
'col_date' => 'Dată / Oră',
'col_ip' => 'Adresă IP',
'col_form' => 'Tip Formular',
'col_reason' => 'Motiv',
'col_uri' => 'URI',
'col_ua' => 'User Agent',
'no_results' => 'Nu există tentative blocate încă.',
'filter_link' => 'filtrează',
'lookup_link' => 'caută ↗',
'prev' => '← Anterior',
'next' => 'Următor →',
'showing' => 'Afișând',
'results' => 'rezultat(e)',
'page_of' => 'pagina %d din %d',
'settings_title' => 'Setări API Central',
'settings_desc' => 'Trimite tentativele blocate la un panou central de informații despre amenințări. Date trimise: IP complet, tip formular, motiv blocare, familie UA și marcă temporală. Nu se trimit niciodată URL-ul site-ului sau nume de utilizatori.',
'enable_label' => 'Activează Trimiterea',
'enable_cb' => 'Trimite tentativele blocate la API-ul central',
'url_label' => 'URL Endpoint API',
'url_desc' => 'URL-ul de bază al containerului Docker API Honeypot.',
'token_label' => 'Token API',
'token_desc' => 'Trebuie să coincidă cu <code>API_TOKEN</code> din mediul containerului Docker.',
'token_const_note' => 'Tokenul este setat prin constanta <code>HP_API_TOKEN</code> în <code>wp-config.php</code>.',
'save_settings' => 'Salvează Setările',
'conn_status' => 'Status Conexiune',
'conn_label' => 'Conexiune',
'conn_ok' => 'Verificată — conectată și token acceptat',
'conn_fail' => 'Conexiune eșuată',
'conn_untested' => 'Netestată încă',
'last_verified' => 'Ultima Verificare',
'enabled_lbl' => 'Activat',
'yes' => 'Da',
'no' => 'Nu',
'last_sync' => 'Ultima Sincronizare',
'never' => 'Niciodată',
'total_sent' => 'Total Trimise',
'blocks_unit' => 'blocări',
'queue_size' => 'Dimensiune Coadă',
'pending_unit' => 'în așteptare',
'next_flush' => 'Următoarea Trimitere Automată',
'not_scheduled' => 'Neprogramată',
'test_conn' => 'Testează Conexiunea',
'flush_queue' => 'Trimite Coada Acum',
'history_title' => 'Trimite Istoricul la API',
'history_desc' => 'Populează panoul central cu jurnalul existent pentru ca statisticile să fie relevante imediat, fără a aștepta noi atacuri. Înregistrările se trimit în loturi de 50.',
'local_log' => 'Jurnal Local',
'local_log_val' => '%s înregistrări în baza de date a acestui site',
'sent_api' => 'Trimise la API',
'remaining_lbl' => 'Rămase',
'remaining_val' => '%s înregistrări netrimise',
'send_history' => 'Trimite Istoricul',
'send_next' => 'Trimite Lotul Următor',
'all_sent' => '✓ Tot istoricul a fost trimis',
'reset_progress' => 'Resetează Progresul',
],
];
// phpcs:enable
public static function get_lang(): string {
$user_id = get_current_user_id();
$lang = $user_id ? (string) get_user_meta($user_id, self::USER_META, true) : '';
return in_array($lang, ['en', 'es', 'ro'], true) ? $lang : 'en';
}
public static function t(string $key): string {
$lang = self::get_lang();
$strings = self::STRINGS[$lang] ?? self::STRINGS['en'];
return $strings[$key] ?? (self::STRINGS['en'][$key] ?? $key);
}
public static function flag_switcher(): string {
$lang = self::get_lang();
$nonce = wp_nonce_field('hp_admin_action', '_wpnonce', true, false);
$tab = sanitize_key($_GET['tab'] ?? 'logs');
$html = '<div style="display:inline-flex;gap:4px;align-items:center">';
foreach (['en' => '🇬🇧', 'es' => '🇪🇸', 'ro' => '🇷🇴'] as $code => $flag) {
$style = $lang === $code
? 'background:none;border:none;cursor:pointer;padding:2px;font-size:22px;opacity:1'
: 'background:none;border:none;cursor:pointer;padding:2px;font-size:16px;opacity:0.45';
$html .= sprintf(
'<form method="post" style="margin:0">%s'
. '<input type="hidden" name="tab" value="%s">'
. '<input type="hidden" name="hp_action" value="set_language">'
. '<input type="hidden" name="hp_lang" value="%s">'
. '<button type="submit" title="%s" style="%s">%s</button></form>',
$nonce,
esc_attr($tab),
esc_attr($code),
esc_attr(strtoupper($code)),
esc_attr($style),
$flag
);
}
$html .= '</div>';
return $html;
}
}
/* ======================================================================
@@ -441,7 +730,7 @@ class SmartHoneypotAdmin {
if ($_POST['hp_action'] === 'save_api_settings') {
$current = SmartHoneypotAPIClient::settings();
$new_url = esc_url_raw(trim($_POST['hp_api_url'] ?? ''));
$new_token = sanitize_text_field($_POST['hp_api_token'] ?? '');
$new_token = defined('HP_API_TOKEN') ? $current['api_token'] : sanitize_text_field($_POST['hp_api_token'] ?? '');
$url_changed = $new_url !== $current['api_url'];
$tok_changed = $new_token !== $current['api_token'];
@@ -492,6 +781,16 @@ class SmartHoneypotAdmin {
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'history_reset' => 1], admin_url('admin.php')));
exit;
}
if ($_POST['hp_action'] === 'set_language') {
$lang = sanitize_key($_POST['hp_lang'] ?? 'en');
if (in_array($lang, ['en', 'es', 'ro'], true)) {
update_user_meta(get_current_user_id(), SmartHoneypotI18n::USER_META, $lang);
}
$tab = sanitize_key($_GET['tab'] ?? 'logs');
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => $tab], admin_url('admin.php')));
exit;
}
}
public static function render_page() {
@@ -503,15 +802,16 @@ class SmartHoneypotAdmin {
?>
<div class="wrap" id="hp-wrap">
<h1 class="wp-heading-inline">Honeypot Fields</h1>
<span style="margin-left:14px;vertical-align:middle"><?= SmartHoneypotI18n::flag_switcher() ?></span>
<?php if (!empty($_GET['cleared'])): ?>
<div class="notice notice-success is-dismissible"><p>Logs cleared.</p></div>
<div class="notice notice-success is-dismissible"><p><?= SmartHoneypotI18n::t('cleared') ?></p></div>
<?php endif; ?>
<?php if (!empty($_GET['saved'])): ?>
<div class="notice notice-success is-dismissible"><p>API settings saved.</p></div>
<div class="notice notice-success is-dismissible"><p><?= SmartHoneypotI18n::t('saved') ?></p></div>
<?php endif; ?>
<?php if (!empty($_GET['flushed'])): ?>
<div class="notice notice-success is-dismissible"><p>Queue flushed to central API.</p></div>
<div class="notice notice-success is-dismissible"><p><?= SmartHoneypotI18n::t('flushed') ?></p></div>
<?php endif; ?>
<?php if (!empty($_GET['tested'])):
$res = get_transient('hp_conn_result');
@@ -527,24 +827,24 @@ class SmartHoneypotAdmin {
<div class="notice <?= $cls ?> is-dismissible">
<p><?= esc_html($res['message']) ?>
<?php if (!empty($res['has_more'])): ?>
&nbsp;<strong>Click "Send Next Batch" to continue.</strong>
&nbsp;<strong><?= SmartHoneypotI18n::t('click_next') ?></strong>
<?php endif; ?>
</p>
</div>
<?php endif;
endif; ?>
<?php if (!empty($_GET['history_reset'])): ?>
<div class="notice notice-info is-dismissible"><p>History sync progress has been reset. You can re-send from the beginning.</p></div>
<div class="notice notice-info is-dismissible"><p><?= SmartHoneypotI18n::t('history_reset_msg') ?></p></div>
<?php endif; ?>
<nav class="nav-tab-wrapper hp-tabs">
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=logs')) ?>"
class="nav-tab <?= $tab === 'logs' ? 'nav-tab-active' : '' ?>">
Blocked Logs
<?= SmartHoneypotI18n::t('tab_logs') ?>
</a>
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=settings')) ?>"
class="nav-tab <?= $tab === 'settings' ? 'nav-tab-active' : '' ?>">
Central API
<?= SmartHoneypotI18n::t('tab_api') ?>
</a>
</nav>
@@ -560,6 +860,7 @@ class SmartHoneypotAdmin {
/* ── Logs tab ─────────────────────────────────────────────────── */
private static function render_logs_tab() {
$T = fn(string $k): string => SmartHoneypotI18n::t($k);
$search = sanitize_text_field($_GET['hp_search'] ?? '');
$filter_ip = sanitize_text_field($_GET['hp_ip'] ?? '');
$filter_form = sanitize_text_field($_GET['hp_form'] ?? '');
@@ -586,10 +887,10 @@ class SmartHoneypotAdmin {
?>
<!-- Stats -->
<div class="hp-stats">
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($total_ever) ?></div><div class="hp-stat-lbl">Total Blocked</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($today) ?></div><div class="hp-stat-lbl">Today</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($unique_ips) ?></div><div class="hp-stat-lbl">Unique IPs</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= count($form_types) ?></div><div class="hp-stat-lbl">Form Types Hit</div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($total_ever) ?></div><div class="hp-stat-lbl"><?= $T('total_blocked') ?></div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($today) ?></div><div class="hp-stat-lbl"><?= $T('today') ?></div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= number_format($unique_ips) ?></div><div class="hp-stat-lbl"><?= $T('unique_ips') ?></div></div>
<div class="hp-stat-card"><div class="hp-stat-num"><?= count($form_types) ?></div><div class="hp-stat-lbl"><?= $T('form_types_hit') ?></div></div>
</div>
<!-- Filters + clear -->
@@ -597,44 +898,44 @@ class SmartHoneypotAdmin {
<input type="hidden" name="page" value="<?= esc_attr(self::MENU_SLUG) ?>">
<input type="hidden" name="tab" value="logs">
<div class="hp-filters">
<input type="text" name="hp_search" placeholder="Search IP, UA, reason…" value="<?= esc_attr($search) ?>" size="28">
<input type="text" name="hp_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
<input type="text" name="hp_search" placeholder="<?= esc_attr($T('search_ph')) ?>" value="<?= esc_attr($search) ?>" size="28">
<input type="text" name="hp_ip" placeholder="<?= esc_attr($T('filter_ip_ph')) ?>" value="<?= esc_attr($filter_ip) ?>">
<select name="hp_form">
<option value="">All form types</option>
<option value=""><?= $T('all_forms') ?></option>
<?php foreach ($form_types as $ft): ?>
<option value="<?= esc_attr($ft) ?>" <?= selected($filter_form, $ft, false) ?>><?= esc_html($ft) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="button">Filter</button>
<button type="submit" class="button"><?= $T('filter_btn') ?></button>
<?php if ($search || $filter_ip || $filter_form): ?>
<a href="<?= esc_url($base) ?>" class="button">Reset</a>
<a href="<?= esc_url($base) ?>" class="button"><?= $T('reset_btn') ?></a>
<?php endif; ?>
<span style="flex:1"></span>
<form method="post" style="display:inline" onsubmit="return confirm('Delete ALL log entries permanently?')">
<form method="post" style="display:inline" onsubmit="return confirm('<?= esc_js($T('clear_confirm')) ?>')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="clear_logs">
<button type="submit" class="button hp-red">Clear All Logs</button>
<button type="submit" class="button hp-red"><?= $T('clear_logs') ?></button>
</form>
</div>
</form>
<p>Showing <strong><?= number_format($total) ?></strong> result<?= $total !== 1 ? 's' : '' ?> (page <?= $paged ?> of <?= $total_pages ?>)</p>
<p><?= $T('showing') ?> <strong><?= number_format($total) ?></strong> <?= $T('results') ?> (<?= sprintf($T('page_of'), $paged, $total_pages) ?>)</p>
<table class="hp-log widefat">
<thead>
<tr><th>#</th><th>Date / Time</th><th>IP Address</th><th>Form Type</th><th>Reason</th><th>URI</th><th>User Agent</th></tr>
<tr><th>#</th><th><?= $T('col_date') ?></th><th><?= $T('col_ip') ?></th><th><?= $T('col_form') ?></th><th><?= $T('col_reason') ?></th><th><?= $T('col_uri') ?></th><th><?= $T('col_ua') ?></th></tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" style="text-align:center;padding:24px;color:#646970">No blocked attempts recorded yet.</td></tr>
<tr><td colspan="7" style="text-align:center;padding:24px;color:#646970"><?= $T('no_results') ?></td></tr>
<?php else: foreach ($rows as $row): ?>
<tr>
<td><?= esc_html($row->id) ?></td>
<td style="white-space:nowrap"><?= esc_html($row->blocked_at) ?></td>
<td>
<code><?= esc_html($row->ip_address) ?></code><br>
<a href="<?= esc_url(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'logs', 'hp_ip' => $row->ip_address], admin_url('admin.php'))) ?>" style="font-size:11px">filter</a>
&nbsp;<a href="https://ipinfo.io/<?= esc_attr(urlencode($row->ip_address)) ?>" target="_blank" style="font-size:11px">lookup ↗</a>
<a href="<?= esc_url(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'logs', 'hp_ip' => $row->ip_address], admin_url('admin.php'))) ?>" style="font-size:11px"><?= $T('filter_link') ?></a>
&nbsp;<a href="https://ipinfo.io/<?= esc_attr(urlencode($row->ip_address)) ?>" target="_blank" style="font-size:11px"><?= $T('lookup_link') ?></a>
</td>
<td><span class="hp-badge"><?= esc_html($row->form_type) ?></span></td>
<td><?= esc_html($row->reason) ?></td>
@@ -648,7 +949,7 @@ class SmartHoneypotAdmin {
<?php if ($total_pages > 1): ?>
<div class="hp-pager">
<?php if ($paged > 1): ?>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged - 1]))) ?>">← Prev</a>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged - 1]))) ?>"><?= $T('prev') ?></a>
<?php endif; ?>
<?php for ($p = max(1, $paged-3); $p <= min($total_pages, $paged+3); $p++):
$url = esc_url(add_query_arg(array_merge($_GET, ['paged' => $p]))); ?>
@@ -659,7 +960,7 @@ class SmartHoneypotAdmin {
<?php endif; ?>
<?php endfor; ?>
<?php if ($paged < $total_pages): ?>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged + 1]))) ?>">Next →</a>
<a href="<?= esc_url(add_query_arg(array_merge($_GET, ['paged' => $paged + 1]))) ?>"><?= $T('next') ?></a>
<?php endif; ?>
</div>
<?php endif; ?>
@@ -668,16 +969,15 @@ class SmartHoneypotAdmin {
/* ── Settings / API tab ───────────────────────────────────────── */
private static function render_settings_tab() {
$s = SmartHoneypotAPIClient::settings();
$queue_size = SmartHoneypotAPIClient::queue_size();
$next_run = wp_next_scheduled('hp_api_flush');
$T = fn(string $k): string => SmartHoneypotI18n::t($k);
$s = SmartHoneypotAPIClient::settings();
$queue_size = SmartHoneypotAPIClient::queue_size();
$next_run = wp_next_scheduled('hp_api_flush');
$token_via_const = defined('HP_API_TOKEN') && HP_API_TOKEN !== '';
?>
<div style="max-width:700px;margin-top:20px">
<h2>Central API Settings</h2>
<p style="color:#646970;margin-bottom:16px">
Submit blocked attempts to a central dashboard for aggregate threat intelligence.
Data sent: full IP address, form type, block reason, UA family, and timestamp. No site URL or usernames are ever sent.
</p>
<h2><?= $T('settings_title') ?></h2>
<p style="color:#646970;margin-bottom:16px"><?= $T('settings_desc') ?></p>
<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
@@ -685,61 +985,62 @@ class SmartHoneypotAdmin {
<table class="form-table">
<tr>
<th>Enable Submission</th>
<th><?= $T('enable_label') ?></th>
<td>
<label>
<input type="checkbox" name="hp_api_enabled" value="1" <?= checked($s['enabled']) ?>>
Send blocked attempts to the central API
<?= $T('enable_cb') ?>
</label>
</td>
</tr>
<tr>
<th>API Endpoint URL</th>
<th><?= $T('url_label') ?></th>
<td>
<input type="url" name="hp_api_url" value="<?= esc_attr($s['api_url']) ?>"
class="regular-text" placeholder="https://your-api-host:3000">
<p class="description">Base URL of your Honeypot API Docker container.</p>
<p class="description"><?= $T('url_desc') ?></p>
</td>
</tr>
<tr>
<th>API Token</th>
<th><?= $T('token_label') ?></th>
<td>
<input type="password" name="hp_api_token" value="<?= esc_attr($s['api_token']) ?>"
class="regular-text" placeholder="Bearer token (matches API_TOKEN in docker-compose)">
<p class="description">
Must match the <code>API_TOKEN</code> set in your Docker container's environment.
Leave empty only if the API is running without a token (not recommended).
</p>
<?php if ($token_via_const): ?>
<input type="text" class="regular-text" value="••••••••••••" disabled>
<p class="description"><?= $T('token_const_note') ?></p>
<?php else: ?>
<input type="password" name="hp_api_token" value="<?= esc_attr($s['api_token']) ?>"
class="regular-text" placeholder="Bearer token (matches API_TOKEN in docker-compose)">
<p class="description"><?= $T('token_desc') ?></p>
<?php endif; ?>
</td>
</tr>
</table>
<?php submit_button('Save Settings'); ?>
<?php submit_button($T('save_settings')); ?>
</form>
<hr>
<h3>Connection Status</h3>
<h3><?= $T('conn_status') ?></h3>
<?php
// Resolve dot state: null=untested, true=verified, false=failed
$conn = $s['connection_ok'];
if ($conn === true) {
$dot_class = 'dot-on';
$dot_label = 'Verified — connected and token accepted';
$dot_style = '';
$dot_class = 'dot-on';
$dot_label = $T('conn_ok');
$dot_style = '';
} elseif ($conn === false) {
$dot_class = 'dot-fail';
$dot_label = 'Connection failed';
$dot_style = 'background:#b32d2e;';
$dot_class = 'dot-fail';
$dot_label = $T('conn_fail');
$dot_style = 'background:#b32d2e;';
} else {
$dot_class = 'dot-off';
$dot_label = 'Not tested yet';
$dot_style = '';
$dot_class = 'dot-off';
$dot_label = $T('conn_untested');
$dot_style = '';
}
?>
<table class="form-table">
<tr>
<th>Connection</th>
<th><?= $T('conn_label') ?></th>
<td>
<span class="hp-api-status">
<span class="dot <?= $dot_class ?>" style="<?= $dot_style ?>"></span>
@@ -751,13 +1052,13 @@ class SmartHoneypotAdmin {
</td>
</tr>
<?php if ($s['last_verified']): ?>
<tr><th>Last Verified</th><td><?= esc_html(date('Y-m-d H:i:s', $s['last_verified'])) ?></td></tr>
<tr><th><?= $T('last_verified') ?></th><td><?= esc_html(date('Y-m-d H:i:s', $s['last_verified'])) ?></td></tr>
<?php endif; ?>
<tr><th>Enabled</th><td><?= $s['enabled'] ? 'Yes' : 'No' ?></td></tr>
<tr><th>Last Sync</th><td><?= $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : 'Never' ?></td></tr>
<tr><th>Total Sent</th><td><?= number_format((int) $s['sent_total']) ?> blocks</td></tr>
<tr><th>Queue Size</th><td><?= number_format($queue_size) ?> pending</td></tr>
<tr><th>Next Auto-Flush</th><td><?= $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : 'Not scheduled' ?></td></tr>
<tr><th><?= $T('enabled_lbl') ?></th><td><?= $s['enabled'] ? $T('yes') : $T('no') ?></td></tr>
<tr><th><?= $T('last_sync') ?></th><td><?= $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : $T('never') ?></td></tr>
<tr><th><?= $T('total_sent') ?></th><td><?= number_format((int) $s['sent_total']) ?> <?= $T('blocks_unit') ?></td></tr>
<tr><th><?= $T('queue_size') ?></th><td><?= number_format($queue_size) ?> <?= $T('pending_unit') ?></td></tr>
<tr><th><?= $T('next_flush') ?></th><td><?= $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : $T('not_scheduled') ?></td></tr>
</table>
<div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;align-items:center">
@@ -765,25 +1066,22 @@ class SmartHoneypotAdmin {
<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="test_connection">
<button type="submit" class="button button-primary">Test Connection</button>
<button type="submit" class="button button-primary"><?= $T('test_conn') ?></button>
</form>
<?php endif; ?>
<?php if ($queue_size > 0 && $s['enabled'] && $s['api_url']): ?>
<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="flush_queue">
<button type="submit" class="button button-secondary">Flush Queue Now (<?= $queue_size ?> pending)</button>
<button type="submit" class="button button-secondary"><?= $T('flush_queue') ?> (<?= $queue_size ?> <?= $T('pending_unit') ?>)</button>
</form>
<?php endif; ?>
</div>
<?php if ($s['api_url'] && $s['enabled']): ?>
<hr>
<h3>Send History to API</h3>
<p style="color:#646970;margin-bottom:12px">
Populate the central dashboard with your existing log so the charts and stats are meaningful right away,
without waiting for new attacks. Records are sent in batches of 50.
</p>
<h3><?= $T('history_title') ?></h3>
<p style="color:#646970;margin-bottom:12px"><?= $T('history_desc') ?></p>
<?php
$local_total = SmartHoneypotDB::count();
$history_sent = (int) get_option('hp_history_total_sent', 0);
@@ -792,13 +1090,13 @@ class SmartHoneypotAdmin {
?>
<table class="form-table">
<tr>
<th>Local Log</th>
<td><?= number_format($local_total) ?> records in this site's database</td>
<th><?= $T('local_log') ?></th>
<td><?= sprintf($T('local_log_val'), number_format($local_total)) ?></td>
</tr>
<tr>
<th>Sent to API</th>
<th><?= $T('sent_api') ?></th>
<td>
<?= number_format($history_sent) ?> records (<?= $pct ?>%)
<?= number_format($history_sent) ?> (<?= $pct ?>%)
<?php if ($local_total > 0): ?>
<div style="margin-top:6px;background:#f0f0f1;border-radius:3px;height:8px;max-width:300px">
<div style="background:#2271b1;height:100%;border-radius:3px;width:<?= $pct ?>%;transition:width .4s"></div>
@@ -807,8 +1105,8 @@ class SmartHoneypotAdmin {
</td>
</tr>
<tr>
<th>Remaining</th>
<td><?= number_format($remaining) ?> records not yet sent</td>
<th><?= $T('remaining_lbl') ?></th>
<td><?= sprintf($T('remaining_val'), number_format($remaining)) ?></td>
</tr>
</table>
@@ -818,18 +1116,18 @@ class SmartHoneypotAdmin {
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="send_history">
<button type="submit" class="button button-secondary">
<?= $history_sent === 0 ? 'Send History' : 'Send Next Batch' ?>
(<?= min(50, $remaining) ?> of <?= number_format($remaining) ?> remaining)
<?= $history_sent === 0 ? $T('send_history') : $T('send_next') ?>
(<?= min(50, $remaining) ?> of <?= number_format($remaining) ?> <?= $T('pending_unit') ?>)
</button>
</form>
<?php else: ?>
<span style="color:#00a32a;font-weight:600">✓ All history sent</span>
<span style="color:#00a32a;font-weight:600"><?= $T('all_sent') ?></span>
<?php endif; ?>
<?php if ($history_sent > 0): ?>
<form method="post" onsubmit="return confirm('Reset history sync progress? This allows re-sending all records from the beginning.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="hp_action" value="reset_history">
<button type="submit" class="button">Reset Progress</button>
<button type="submit" class="button"><?= $T('reset_progress') ?></button>
</form>
<?php endif; ?>
</div>