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:
@@ -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'])): ?>
|
||||
<strong>Click "Send Next Batch" to continue.</strong>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user