prefix . 'honeypot_log';
}
public static function install() {
global $wpdb;
$table = self::table();
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
blocked_at DATETIME NOT NULL,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
form_type VARCHAR(100) NOT NULL DEFAULT '',
reason VARCHAR(255) NOT NULL DEFAULT '',
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
user_agent TEXT NOT NULL,
PRIMARY KEY (id),
KEY ip_address (ip_address),
KEY blocked_at (blocked_at),
KEY form_type (form_type)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option(self::TABLE_VERSION_OPTION, self::TABLE_VERSION);
}
public static function insert(array $data) {
global $wpdb;
$wpdb->insert(
self::table(),
[
'blocked_at' => current_time('mysql'),
'ip_address' => sanitize_text_field($data['ip'] ?? ''),
'form_type' => sanitize_text_field($data['form'] ?? 'Unknown'),
'reason' => sanitize_text_field($data['reason'] ?? ''),
'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)),
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
],
['%s', '%s', '%s', '%s', '%s', '%s']
);
}
public static function get_rows(array $args = []): array {
global $wpdb;
$table = self::table();
$limit = max(1, intval($args['per_page'] ?? 25));
$offset = max(0, intval($args['offset'] ?? 0));
$where = '1=1';
$params = [];
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = sanitize_text_field($args['ip']);
}
if (!empty($args['form'])) {
$where .= ' AND form_type = %s';
$params[] = sanitize_text_field($args['form']);
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
$params[] = $limit;
$params[] = $offset;
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY blocked_at DESC LIMIT %d OFFSET %d";
return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: [];
}
public static function count(array $args = []): int {
global $wpdb;
$table = self::table();
$where = '1=1';
$params = [];
if (!empty($args['ip'])) {
$where .= ' AND ip_address = %s';
$params[] = sanitize_text_field($args['ip']);
}
if (!empty($args['form'])) {
$where .= ' AND form_type = %s';
$params[] = sanitize_text_field($args['form']);
}
if (!empty($args['search'])) {
$like = '%' . $wpdb->esc_like($args['search']) . '%';
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
return (int) $wpdb->get_var($params ? $wpdb->prepare($sql, $params) : $sql);
}
public static function get_form_types(): array {
global $wpdb;
return $wpdb->get_col("SELECT DISTINCT form_type FROM " . self::table() . " ORDER BY form_type ASC") ?: [];
}
public static function clear(): void {
global $wpdb;
$wpdb->query("TRUNCATE TABLE " . self::table());
}
public static function delete_older_than_days(int $days): void {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM " . self::table() . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
$days
)
);
}
}
/* ======================================================================
* CENTRAL API CLIENT
* Queues blocked submissions and batch-sends to a central dashboard.
* ====================================================================*/
class SmartHoneypotAPIClient {
const OPT_SETTINGS = 'hp_api_settings';
const OPT_QUEUE = 'hp_api_queue';
const QUEUE_MAX = 500;
const BATCH_SIZE = 50;
public static function defaults(): array {
return [
'enabled' => false,
'api_url' => '',
'api_token' => '',
'last_sync' => 0,
'sent_total' => 0,
'connection_ok' => null, // null=untested, true=ok, false=failed
'last_verified' => 0,
'last_error' => '',
];
}
/**
* Tests reachability (health endpoint) and auth (submit with bad payload).
* Returns ['ok' => bool, 'message' => string]
*/
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::resolve_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()];
}
$code = wp_remote_retrieve_response_code($health);
if ($code !== 200) {
return ['ok' => false, 'message' => "API returned HTTP {$code}. Verify the URL is correct."];
}
// Step 2 — token validation: POST with an intentionally invalid payload.
// Auth passes → 400 (bad payload). Wrong/missing token → 403.
$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 request failed: ' . $auth->get_error_message()];
}
$auth_code = wp_remote_retrieve_response_code($auth);
if ($auth_code === 403) {
return ['ok' => false, 'message' => 'API reachable but token rejected (HTTP 403). Check the token matches API_TOKEN in Docker.'];
}
// 400 = auth passed, payload correctly rejected — exactly what we expect
return ['ok' => true, 'message' => 'Connection verified. API is reachable and token is accepted.'];
}
/**
* Sends one batch of existing local log records to the central API.
* Picks up from where it left off using the hp_history_last_id option.
*
* Returns ['ok', 'sent', 'remaining', 'has_more', 'message']
*/
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 = SmartHoneypotDB::table();
$last_id = (int) get_option('hp_history_last_id', 0);
$total = SmartHoneypotDB::count();
$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::resolve_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()];
}
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200) {
return ['ok' => false, 'message' => "API returned HTTP {$code}. Check connection settings."];
}
$new_last_id = (int) end($rows)['id'];
$sent_total = (int) get_option('hp_history_total_sent', 0) + count($rows);
update_option('hp_history_last_id', $new_last_id);
update_option('hp_history_total_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),
];
}
public static function settings(): array {
return wp_parse_args(get_option(self::OPT_SETTINGS, []), self::defaults());
}
/** Called from log_spam() — very fast, just appends to option. */
public static function enqueue(array $data): void {
$s = self::settings();
if (!$s['enabled'] || empty($s['api_url'])) {
return;
}
$queue = (array) get_option(self::OPT_QUEUE, []);
if (count($queue) >= self::QUEUE_MAX) {
array_shift($queue); // drop oldest when full
}
$queue[] = $data;
update_option(self::OPT_QUEUE, $queue, false); // no autoload
}
/** Called by WP-cron every 5 minutes. Sends pending batch to the API. */
public static function flush(): void {
$s = self::settings();
if (!$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);
$site_hash = hash('sha256', home_url());
$headers = ['Content-Type' => 'application/json'];
$token = self::resolve_token();
if ($token !== '') {
$headers['Authorization'] = 'Bearer ' . $token;
}
$response = wp_remote_post(
trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
[
'timeout' => 15,
'blocking' => true,
'headers' => $headers,
'body' => wp_json_encode([
'site_hash' => $site_hash,
'blocks' => $batch,
]),
]
);
if (is_wp_error($response)) {
$s['connection_ok'] = false;
$s['last_error'] = 'Flush failed: ' . $response->get_error_message();
update_option(self::OPT_SETTINGS, $s);
error_log('[Honeypot] flush() wp_error: ' . $response->get_error_message());
return;
}
$code = wp_remote_retrieve_response_code($response);
if ($code === 200) {
update_option(self::OPT_QUEUE, $queue, false);
$s['last_sync'] = time();
$s['sent_total'] = ($s['sent_total'] ?? 0) + count($batch);
$s['last_error'] = '';
$s['connection_ok'] = true;
update_option(self::OPT_SETTINGS, $s);
} else {
$s['connection_ok'] = false;
$s['last_error'] = "Flush failed: API returned HTTP {$code}. Check URL and token.";
update_option(self::OPT_SETTINGS, $s);
error_log("[Honeypot] flush() API returned HTTP {$code}");
}
}
/** Number of items currently waiting to be sent. */
public static function queue_size(): int {
return count((array) get_option(self::OPT_QUEUE, []));
}
/**
* Fallback flush triggered on every PHP shutdown.
* A transient lock ensures we attempt at most once per 5 minutes
* regardless of traffic volume, so WP-Cron is not the sole trigger.
*/
public static function maybe_flush_overdue(): void {
$s = self::settings();
if (!$s['enabled'] || empty($s['api_url'])) return;
if (self::queue_size() === 0) return;
if (get_transient('hp_flush_lock')) return;
set_transient('hp_flush_lock', 1, 300); // 5-min lock
self::flush();
}
/**
* 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 API_TOKEN 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 HP_API_TOKEN constant in wp-config.php.',
'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 API_TOKEN en el entorno del contenedor Docker.',
'token_const_note' => 'El token está definido mediante la constante HP_API_TOKEN en wp-config.php.',
'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 API_TOKEN din mediul containerului Docker.',
'token_const_note' => 'Tokenul este setat prin constanta HP_API_TOKEN în wp-config.php.',
'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 = '
= SmartHoneypotI18n::t('cleared') ?>
= SmartHoneypotI18n::t('saved') ?>
= SmartHoneypotI18n::t('flushed') ?>
= esc_html($res['message']) ?>
= esc_html($res['message']) ?> = SmartHoneypotI18n::t('click_next') ?>
= SmartHoneypotI18n::t('history_reset_msg') ?>
= $T('showing') ?> = number_format($total) ?> = $T('results') ?> (= sprintf($T('page_of'), $paged, $total_pages) ?>)
| # | = $T('col_date') ?> | = $T('col_ip') ?> | = $T('col_form') ?> | = $T('col_reason') ?> | = $T('col_uri') ?> | = $T('col_ua') ?> |
|---|---|---|---|---|---|---|
| = $T('no_results') ?> | ||||||
| = esc_html($row->id) ?> | = esc_html($row->blocked_at) ?> |
= esc_html($row->ip_address) ?>= $T('filter_link') ?> = $T('lookup_link') ?> |
= esc_html($row->form_type) ?> | = esc_html($row->reason) ?> | = esc_html($row->request_uri) ?> | = esc_html($row->user_agent) ?> |
= $T('settings_desc') ?>
| = $T('enable_label') ?> | |
|---|---|
| = $T('url_label') ?> |
= $T('url_desc') ?> |
| = $T('token_label') ?> |
= $T('token_const_note') ?> = $T('token_desc') ?> |
| = $T('conn_label') ?> |
= esc_html($dot_label) ?>
= esc_html($s['last_error']) ?> |
|---|---|
| = $T('last_verified') ?> | = esc_html(date('Y-m-d H:i:s', $s['last_verified'])) ?> |
| = $T('enabled_lbl') ?> | = $s['enabled'] ? $T('yes') : $T('no') ?> |
| = $T('last_sync') ?> | = $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : $T('never') ?> |
| = $T('total_sent') ?> | = number_format((int) $s['sent_total']) ?> = $T('blocks_unit') ?> |
| = $T('queue_size') ?> | = number_format($queue_size) ?> = $T('pending_unit') ?> |
| = $T('next_flush') ?> | = $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : $T('not_scheduled') ?> |
= $T('history_desc') ?>
0 ? min(100, round($history_sent / $local_total * 100)) : 0; ?>| = $T('local_log') ?> | = sprintf($T('local_log_val'), number_format($local_total)) ?> |
|---|---|
| = $T('sent_api') ?> |
= number_format($history_sent) ?> (= $pct ?>%)
0): ?>
|
| = $T('remaining_lbl') ?> | = sprintf($T('remaining_val'), number_format($remaining)) ?> |