Files
InformatiQ-Toolkit/includes/class-itk-admin.php
Malin 52af2d9931 feat: global IP/CIDR/UA whitelist bypassing all restrictions
- class-itk-whitelist.php: static class with 5min transient cache,
  supports exact IP, CIDR notation, and ua: prefix for UA substrings
- config/whitelist.conf: editable config file (template with examples)
- whitelist check added to bot-blocker, WAF, protection (4 methods),
  and honeypot validator — matched requests skip all ITK enforcement
- admin: whitelist.conf added to Config Files editor tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 10:00:16 +02:00

1465 lines
82 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
if (!defined('ABSPATH')) exit;
/**
* ITK Admin
*
* Single admin page with tabs:
* Dashboard | Bot Blocker | Protection | Optimization | Honeypot | Bot Logs | Honeypot Logs | Config Files
*/
class ITK_Admin {
const MENU_SLUG = 'informatiq-toolkit';
const NONCE_ACTION = 'itk_admin';
const PER_PAGE = 25;
public function __construct() {
add_action('admin_menu', [$this, 'add_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_init', [$this, 'handle_actions']);
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
add_action('wp_ajax_itk_save_config_file', [$this, 'ajax_save_config_file']);
add_action('wp_ajax_itk_test_api', [$this, 'ajax_test_api']);
add_action('wp_ajax_itk_flush_api_queue', [$this, 'ajax_flush_api_queue']);
}
public function add_menu(): void {
add_options_page(
'InformatiQ Toolkit',
'InformatiQ Toolkit',
'manage_options',
self::MENU_SLUG,
[$this, 'render_page']
);
}
public function enqueue_assets(string $hook): void {
if ($hook !== 'settings_page_' . self::MENU_SLUG) return;
wp_enqueue_style('itk-admin', ITK_URL . 'assets/css/admin.css', [], ITK_VERSION);
wp_enqueue_script('itk-admin', ITK_URL . 'assets/js/admin.js', ['jquery'], ITK_VERSION, true);
wp_localize_script('itk-admin', 'itkAdmin', [
'nonce' => wp_create_nonce(self::NONCE_ACTION),
'ajaxUrl' => admin_url('admin-ajax.php'),
]);
}
/* ── Actions (form submissions) ───────────────────────────── */
public function handle_actions(): void {
if (!isset($_POST['itk_action']) || !check_admin_referer(self::NONCE_ACTION)) return;
if (!current_user_can('manage_options')) wp_die('Unauthorized');
switch ($_POST['itk_action']) {
case 'clear_bot_log':
ITK_Database::clear_bot_log();
$this->redirect(['tab' => 'bot-logs', 'cleared' => 1]);
break;
case 'clear_honeypot_log':
ITK_Database::clear_honeypot_log();
$this->redirect(['tab' => 'honeypot-logs', 'cleared' => 1]);
break;
case 'save_settings_security':
$this->save_settings_form('itk_security', [
'response_code', 'redirect_url', 'custom_message',
], [
'log_blocked_attempts',
]);
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
break;
case 'save_bot_api':
$this->save_api_settings(ITK_Bot_API::OPT_SETTINGS, 'itk_bot_api_settings');
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
break;
case 'save_hp_api':
$this->save_api_settings(ITK_HP_API::OPT_SETTINGS, 'itk_hp_api_settings');
$this->redirect(['tab' => 'honeypot', 'saved' => 1]);
break;
case 'test_bot_api':
$result = ITK_Bot_API::test_connection();
$s = ITK_Bot_API::settings();
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
$s['last_error'] = $result['ok'] ? '' : $result['message'];
update_option(ITK_Bot_API::OPT_SETTINGS, $s);
set_transient('itk_bot_api_test_result', $result, 60);
$this->redirect(['tab' => 'bot-blocker', 'api_tested' => 1]);
break;
case 'test_hp_api':
$result = ITK_HP_API::test_connection();
$s = ITK_HP_API::settings();
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
$s['last_error'] = $result['ok'] ? '' : $result['message'];
update_option(ITK_HP_API::OPT_SETTINGS, $s);
set_transient('itk_hp_api_test_result', $result, 60);
$this->redirect(['tab' => 'honeypot', 'api_tested' => 1]);
break;
case 'flush_bot_api':
ITK_Bot_API::flush();
$this->redirect(['tab' => 'bot-blocker', 'api_flushed' => 1]);
break;
case 'flush_hp_api':
ITK_HP_API::flush();
$this->redirect(['tab' => 'honeypot', 'api_flushed' => 1]);
break;
case 'send_bot_history':
$result = ITK_Bot_API::send_history_batch();
set_transient('itk_bot_history_result', $result, 60);
$this->redirect(['tab' => 'bot-blocker', 'history_sent' => 1]);
break;
case 'send_hp_history':
$result = ITK_HP_API::send_history_batch();
set_transient('itk_hp_history_result', $result, 60);
$this->redirect(['tab' => 'honeypot', 'history_sent' => 1]);
break;
case 'reset_bot_history':
delete_option('itk_bot_history_last_id');
delete_option('itk_bot_history_sent');
$this->redirect(['tab' => 'bot-blocker']);
break;
case 'reset_hp_history':
delete_option('itk_hp_history_last_id');
delete_option('itk_hp_history_sent');
$this->redirect(['tab' => 'honeypot']);
break;
case 'save_settings_login':
$this->save_settings_form('itk_security', [
'custom_login_slug',
], [
'enable_custom_login',
]);
$this->redirect(['tab' => 'protection', 'saved' => 1]);
break;
case 'save_settings_honeypot':
$this->save_settings_form('itk_honeypot', [
'min_time', 'max_time', 'retain_days',
], []);
$this->redirect(['tab' => 'honeypot', 'saved' => 1]);
break;
case 'save_settings_waf':
$this->save_settings_form('itk_waf', [
'action', 'response_code', 'redirect_url', 'custom_message',
], [
'enabled', 'log_attacks', 'scan_post', 'scan_cookies', 'scan_ua',
'block_sqli', 'block_xss', 'block_lfi', 'block_rfi', 'block_cmdi',
'block_xxe', 'block_php_inject', 'block_ssrf', 'block_wp_specific',
]);
(new ITK_WAF())->invalidate_cache();
$this->redirect(['tab' => 'waf', 'saved' => 1]);
break;
case 'clear_attack_log':
ITK_Database::clear_attack_log();
$this->redirect(['tab' => 'attack-logs', 'cleared' => 1]);
break;
case 'save_attacks_api':
$this->save_api_settings(ITK_Attacks_API::OPT_SETTINGS, 'itk_attacks_api_settings');
$this->redirect(['tab' => 'waf', 'saved' => 1]);
break;
case 'test_attacks_api':
$result = ITK_Attacks_API::test_connection();
$s = ITK_Attacks_API::settings();
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
$s['last_error'] = $result['ok'] ? '' : $result['message'];
update_option(ITK_Attacks_API::OPT_SETTINGS, $s);
set_transient('itk_attacks_api_test_result', $result, 60);
$this->redirect(['tab' => 'waf', 'api_tested' => 1]);
break;
case 'flush_attacks_api':
ITK_Attacks_API::flush();
$this->redirect(['tab' => 'waf', 'api_flushed' => 1]);
break;
case 'send_attacks_history':
$result = ITK_Attacks_API::send_history_batch();
set_transient('itk_attacks_history_result', $result, 60);
$this->redirect(['tab' => 'waf', 'history_sent' => 1]);
break;
case 'reset_attacks_history':
delete_option('itk_attacks_history_last_id');
delete_option('itk_attacks_history_sent');
$this->redirect(['tab' => 'waf']);
break;
}
}
/**
* Save a subset of fields from $_POST['itk_*'] into a WP option.
* $text_fields = scalar fields (sanitize_text_field)
* $toggle_fields = checkbox fields (0 or 1)
*/
private function save_settings_form(string $option, array $text_fields, array $toggle_fields): void {
$opts = get_option($option, []);
$posted = $_POST[$option] ?? [];
foreach ($text_fields as $key) {
if (isset($posted[$key])) {
$opts[$key] = sanitize_text_field(wp_unslash($posted[$key]));
}
}
foreach ($toggle_fields as $key) {
$opts[$key] = !empty($posted[$key]) ? 1 : 0;
}
update_option($option, $opts);
}
private function save_api_settings(string $option_key, string $post_key): void {
$cur = get_option($option_key, []);
$posted = $_POST[$post_key] ?? [];
$new_url = esc_url_raw(trim($posted['api_url'] ?? ''));
$changed = $new_url !== ($cur['api_url'] ?? '') || (!empty($posted['api_token']) && $posted['api_token'] !== ($cur['api_token'] ?? ''));
update_option($option_key, array_merge($cur, [
'enabled' => !empty($posted['enabled']),
'api_url' => $new_url,
'api_token' => sanitize_text_field($posted['api_token'] ?? ($cur['api_token'] ?? '')),
'connection_ok' => $changed ? null : ($cur['connection_ok'] ?? null),
'last_verified' => $changed ? 0 : ($cur['last_verified'] ?? 0),
'last_error' => $changed ? '' : ($cur['last_error'] ?? ''),
]));
}
/* ── AJAX: test API connection ─────────────────────────────── */
public function ajax_test_api(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
$which = sanitize_key($_POST['api'] ?? '');
$result = match ($which) {
'bot' => ITK_Bot_API::test_connection(),
'attacks' => ITK_Attacks_API::test_connection(),
default => ITK_HP_API::test_connection(),
};
wp_send_json($result);
}
/* ── AJAX: flush API queue ─────────────────────────────────── */
public function ajax_flush_api_queue(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
$which = sanitize_key($_POST['api'] ?? '');
match ($which) {
'bot' => ITK_Bot_API::flush(),
'attacks' => ITK_Attacks_API::flush(),
default => ITK_HP_API::flush(),
};
wp_send_json_success('Queue flushed.');
}
private function redirect(array $args): void {
wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php')));
exit;
}
/* ── AJAX: save single toggle setting ─────────────────────── */
public function ajax_save_setting(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
$option = sanitize_key($_POST['option'] ?? '');
$setting = sanitize_key($_POST['setting'] ?? '');
$value = (int)($_POST['value'] ?? 0);
$allowed = ['itk_security','itk_optimization','itk_honeypot','itk_waf'];
if (!in_array($option, $allowed, true) || empty($setting)) {
wp_send_json_error('invalid');
}
$opts = get_option($option, []);
$opts[$setting] = $value;
update_option($option, $opts);
wp_send_json_success();
}
/* ── AJAX: save config file ───────────────────────────────── */
public function ajax_save_config_file(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
$file = sanitize_key($_POST['file'] ?? '');
$content = wp_unslash($_POST['content'] ?? '');
$allowed = [
'badbots' => ITK_PATH . 'config/badbots.conf',
'goodbots' => ITK_PATH . 'config/goodbots.conf',
'referrers' => ITK_PATH . 'config/referrers.conf',
'networks' => ITK_PATH . 'config/networks.conf',
'allowed-ips'=> ITK_PATH . 'config/allowed-ips.conf',
'whitelist' => ITK_PATH . 'config/whitelist.conf',
];
if (!isset($allowed[$file])) wp_send_json_error('invalid file');
$result = file_put_contents($allowed[$file], sanitize_textarea_field($content));
if ($result === false) {
wp_send_json_error('write failed');
}
// Clear transient caches
delete_transient('itk_bots_list');
delete_transient('itk_referrers_list');
delete_transient('itk_networks_list');
delete_transient('itk_goodbots_list');
ITK_Whitelist::invalidate_cache();
wp_send_json_success();
}
/* ── Main page render ─────────────────────────────────────── */
public function render_page(): void {
if (!current_user_can('manage_options')) return;
$tab = sanitize_key($_GET['tab'] ?? 'dashboard');
?>
<div class="wrap itk-wrap">
<h1 class="itk-page-title">
<span class="itk-logo">IQ</span> InformatiQ Toolkit
</h1>
<?php if (!empty($_GET['cleared'])): ?>
<div class="notice notice-success is-dismissible"><p>Logs cleared successfully.</p></div>
<?php endif; ?>
<?php if (!empty($_GET['saved'])): ?>
<div class="notice notice-success is-dismissible"><p>Settings saved.</p></div>
<?php endif; ?>
<nav class="itk-tabs">
<?php
$tabs = [
'dashboard' => 'Dashboard',
'bot-blocker' => 'Bot Blocker',
'protection' => 'Protection',
'optimization' => 'Optimization',
'honeypot' => 'Honeypot',
'waf' => 'WAF',
'bot-logs' => 'Bot Logs',
'honeypot-logs' => 'Honeypot Logs',
'attack-logs' => 'Attack Logs',
'config-files' => 'Config Files',
];
foreach ($tabs as $slug => $label):
$active = $tab === $slug ? 'itk-tab-active' : '';
$url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=' . $slug);
?>
<a href="<?= esc_url($url) ?>" class="itk-tab <?= $active ?>"><?= esc_html($label) ?></a>
<?php endforeach; ?>
</nav>
<div class="itk-tab-content">
<?php
match ($tab) {
'dashboard' => $this->tab_dashboard(),
'bot-blocker' => $this->tab_bot_blocker(),
'protection' => $this->tab_protection(),
'optimization' => $this->tab_optimization(),
'honeypot' => $this->tab_honeypot(),
'waf' => $this->tab_waf(),
'bot-logs' => $this->tab_bot_logs(),
'honeypot-logs' => $this->tab_honeypot_logs(),
'attack-logs' => $this->tab_attack_logs(),
'config-files' => $this->tab_config_files(),
default => $this->tab_dashboard(),
};
?>
</div>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: DASHBOARD
* ══════════════════════════════════════════════════════════ */
private function tab_dashboard(): void {
$bot_stats = ITK_Database::get_bot_stats();
$hp_stats = ITK_Database::get_honeypot_stats();
?>
<div class="itk-dashboard">
<!-- ── Bot Activity Panel ── -->
<section class="itk-monitor-panel">
<div class="itk-monitor-header">
<span class="itk-monitor-title">&#9632; BOT ACTIVITY MONITOR</span>
<span class="itk-monitor-blink">LIVE</span>
</div>
<div class="itk-stat-row">
<div class="itk-stat-card">
<div class="itk-stat-num"><?= number_format($bot_stats['total']) ?></div>
<div class="itk-stat-lbl">Total Blocked</div>
</div>
<div class="itk-stat-card">
<div class="itk-stat-num itk-green"><?= number_format($bot_stats['today']) ?></div>
<div class="itk-stat-lbl">Today</div>
</div>
<div class="itk-stat-card">
<div class="itk-stat-num itk-yellow"><?= number_format($bot_stats['rate_limited']) ?></div>
<div class="itk-stat-lbl">Rate Limited</div>
</div>
<div class="itk-stat-card">
<div class="itk-stat-num"><?= number_format($hp_stats['total']) ?></div>
<div class="itk-stat-lbl">Honeypot Catches</div>
</div>
<div class="itk-stat-card">
<div class="itk-stat-num itk-green"><?= number_format($hp_stats['today']) ?></div>
<div class="itk-stat-lbl">Honeypot Today</div>
</div>
</div>
<!-- Top bot types bar chart -->
<?php if (!empty($bot_stats['top_bot_types'])): ?>
<div class="itk-chart-section">
<div class="itk-chart-title">TOP THREAT SOURCES</div>
<div class="itk-bar-chart">
<?php
$max = max(1, (int)$bot_stats['top_bot_types'][0]->cnt);
foreach ($bot_stats['top_bot_types'] as $row):
$pct = round(($row->cnt / $max) * 100);
?>
<div class="itk-bar-row">
<span class="itk-bar-label"><?= esc_html($row->bot_type ?: 'Unknown') ?></span>
<div class="itk-bar-track">
<div class="itk-bar-fill" style="width:<?= $pct ?>%"></div>
</div>
<span class="itk-bar-count"><?= number_format($row->cnt) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Top honeypot form types -->
<?php if (!empty($hp_stats['top_forms'])): ?>
<div class="itk-chart-section">
<div class="itk-chart-title">HONEYPOT TOP TARGETED FORMS</div>
<div class="itk-bar-chart">
<?php
$max = max(1, (int)$hp_stats['top_forms'][0]->cnt);
foreach ($hp_stats['top_forms'] as $row):
$pct = round(($row->cnt / $max) * 100);
?>
<div class="itk-bar-row">
<span class="itk-bar-label"><?= esc_html($row->form_type) ?></span>
<div class="itk-bar-track">
<div class="itk-bar-fill itk-bar-hp" style="width:<?= $pct ?>%"></div>
</div>
<span class="itk-bar-count"><?= number_format($row->cnt) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Top IPs -->
<?php if (!empty($bot_stats['top_ips'])): ?>
<div class="itk-chart-section">
<div class="itk-chart-title">TOP OFFENDER IPs</div>
<table class="itk-mini-table">
<tr><th>IP Address</th><th>Hits</th></tr>
<?php foreach ($bot_stats['top_ips'] as $row): ?>
<tr>
<td>
<a href="<?= esc_url(admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=bot-logs&hp_ip=' . urlencode($row->ip_address))) ?>"><?= esc_html($row->ip_address) ?></a>
&nbsp;<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-lookup">[lookup]</a>
</td>
<td><?= number_format($row->cnt) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endif; ?>
</section>
<!-- ── Quick Settings Status ── -->
<section class="itk-quick-status">
<div class="itk-chart-title">ACTIVE MODULES</div>
<?php
$sec = get_option('itk_security', []);
$opt = get_option('itk_optimization', []);
$hp = get_option('itk_honeypot', []);
$modules = [
'Bot Blocker' => !empty($sec['block_malicious_bots']),
'OpenAI Block' => !empty($sec['block_openai_bots']),
'Good Bot Rate Limit'=> !empty($sec['rate_limit_good_bots']),
'Network Block' => !empty($sec['block_bad_networks']),
'Login Protection' => !empty($sec['protect_wp_login']),
'Security Headers' => !empty($sec['add_security_headers']),
'Block XML-RPC' => !empty($sec['block_xmlrpc']),
'Honeypot' => !empty($hp['enabled']),
'Remove WP Version' => !empty($opt['remove_wp_version']),
'Disable Emoji' => !empty($opt['remove_emoji']),
'Admin Branding' => !empty($opt['admin_branding']),
];
foreach ($modules as $label => $active):
?>
<div class="itk-module-row">
<span class="itk-module-dot <?= $active ? 'itk-dot-on' : 'itk-dot-off' ?>"></span>
<span class="itk-module-label"><?= esc_html($label) ?></span>
<span class="itk-module-status"><?= $active ? 'ACTIVE' : 'off' ?></span>
</div>
<?php endforeach; ?>
</section>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: BOT BLOCKER
* ══════════════════════════════════════════════════════════ */
private function tab_bot_blocker(): void {
$opts = get_option('itk_security', []);
?>
<div class="itk-settings-grid">
<section class="itk-card">
<h2>Bot Blocking</h2>
<?php
$toggles = [
'block_openai_bots' => ['OpenAI / GPT Bots', 'Block GPTBot, ChatGPT-User, OAI-SearchBot'],
'block_malicious_bots' => ['Malicious Bots', 'Block bots listed in badbots.conf'],
'block_bad_referrers' => ['Bad Referrers', 'Block requests from spam referrer domains'],
'block_bad_networks' => ['Bad Networks', 'Block IP ranges listed in networks.conf'],
'rate_limit_good_bots' => ['Rate-Limit Good Bots', 'Apply crawl-rate limits to Googlebot, Bingbot, etc. (configurable in goodbots.conf)'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_security', $key, $label, $desc, $opts);
endforeach;
?>
</section>
<section class="itk-card">
<h2>Response Settings</h2>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_settings_security">
<table class="form-table">
<tr>
<th>Response Code</th>
<td>
<select name="itk_security[response_code]">
<?php
$codes = ['301_custom' => '301 Redirect to custom URL', '403' => '403 Forbidden', '410' => '410 Gone', '503' => '503 Service Unavailable'];
$cur = $opts['response_code'] ?? '301_custom';
foreach ($codes as $val => $lbl):
?>
<option value="<?= esc_attr($val) ?>" <?= selected($cur, $val, false) ?>><?= esc_html($lbl) ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th>Redirect URL</th>
<td><input type="url" name="itk_security[redirect_url]" value="<?= esc_attr($opts['redirect_url'] ?? '') ?>" class="regular-text"></td>
</tr>
<tr>
<th>Custom Message</th>
<td><input type="text" name="itk_security[custom_message]" value="<?= esc_attr($opts['custom_message'] ?? 'Access denied.') ?>" class="regular-text"></td>
</tr>
<tr>
<th>Log Blocked Attempts</th>
<td>
<label>
<input type="checkbox" name="itk_security[log_blocked_attempts]" value="1" <?= checked(!empty($opts['log_blocked_attempts'])) ?>>
Log all blocked attempts to the database
</label>
</td>
</tr>
</table>
<?php submit_button('Save Response Settings'); ?>
</form>
</section>
<?php
/* ── Central Bot API card ─────────────────────── */
$bot_api = ITK_Bot_API::settings();
$bot_queue = count((array) get_option(ITK_Bot_API::OPT_QUEUE, []));
$bot_total = ITK_Database::count_bot_rows();
$bot_sent = (int) get_option('itk_bot_history_sent', 0);
$bot_rem = max(0, $bot_total - $bot_sent);
$bot_ok = $bot_api['connection_ok'];
$bot_cls = is_null($bot_ok) ? 'itk-api-unknown' : ($bot_ok ? 'itk-api-ok' : 'itk-api-err');
$bot_lbl = is_null($bot_ok) ? 'Not tested' : ($bot_ok ? 'Connected' : 'Connection failed');
$bot_test_r = get_transient('itk_bot_api_test_result'); if ($bot_test_r) delete_transient('itk_bot_api_test_result');
$bot_hist_r = get_transient('itk_bot_history_result'); if ($bot_hist_r) delete_transient('itk_bot_history_result');
?>
<section class="itk-card itk-api-card">
<h2>Central Bot API</h2>
<p class="description itk-api-desc">Send blocked-bot events to your self-hosted Bot Intelligence Docker stack (port 3001).</p>
<div class="itk-api-status-bar">
<span class="itk-api-badge <?= esc_attr($bot_cls) ?>"><?= esc_html($bot_lbl) ?></span>
<?php if ($bot_api['last_verified'] > 0): ?>
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$bot_api['last_verified'])) ?> ago</span>
<?php endif; ?>
<?php if (!$bot_ok && !is_null($bot_ok) && !empty($bot_api['last_error'])): ?>
<span class="itk-api-err-msg"><?= esc_html($bot_api['last_error']) ?></span>
<?php endif; ?>
</div>
<?php if ($bot_test_r): ?>
<div class="itk-api-notice <?= $bot_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($bot_test_r['message']) ?></div>
<?php endif; ?>
<?php if ($bot_hist_r): ?>
<div class="itk-api-notice <?= $bot_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($bot_hist_r['message']) ?></div>
<?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_bot_api">
<table class="form-table itk-api-table">
<tr>
<th>Enable</th>
<td><label><input type="checkbox" name="itk_bot_api_settings[enabled]" value="1" <?= checked(!empty($bot_api['enabled'])) ?>> Send events to Central API</label></td>
</tr>
<tr>
<th>API URL</th>
<td>
<input type="url" name="itk_bot_api_settings[api_url]" value="<?= esc_attr($bot_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3001">
<p class="description">Base URL of your Bot API stack (e.g. <code>http://localhost:3001</code>)</p>
</td>
</tr>
<tr>
<th>API Token</th>
<td>
<input type="password" name="itk_bot_api_settings[api_token]" value="" class="regular-text"
placeholder="<?= !empty($bot_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
autocomplete="new-password">
</td>
</tr>
</table>
<div class="itk-api-form-actions">
<?php submit_button('Save Bot API Settings', 'primary', 'submit', false); ?>
<button type="button" class="button itk-btn-test-api" data-api="bot" style="margin-left:8px">Test Connection</button>
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
</div>
</form>
<div class="itk-api-footer">
<div class="itk-api-queue-row">
<strong><?= (int) $bot_queue ?></strong> event(s) pending in queue
<button type="button" class="button button-small itk-btn-flush-api" data-api="bot" style="margin-left:8px">Flush Now</button>
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
</div>
<div class="itk-api-history-row">
<strong>Historical sync:</strong>
<?= number_format($bot_sent) ?> / <?= number_format($bot_total) ?> records sent
<?php if ($bot_rem > 0): ?><em class="itk-api-rem">(<?= number_format($bot_rem) ?> remaining)</em><?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker" style="display:inline;margin-left:10px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="send_bot_history">
<input type="submit" class="button button-small" value="Send Next 50">
</form>
<?php if ($bot_sent > 0): ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="reset_bot_history">
<input type="submit" class="button button-small" value="Reset Progress">
</form>
<?php endif; ?>
</div>
</div>
</section>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: PROTECTION
* ══════════════════════════════════════════════════════════ */
private function tab_protection(): void {
$opts = get_option('itk_security', []);
?>
<div class="itk-settings-grid">
<section class="itk-card">
<h2>WordPress Protection</h2>
<?php
$toggles = [
'protect_wp_login' => ['WP Login IP Whitelist', 'Restrict wp-login.php to IPs in allowed-ips.conf'],
'add_security_headers' => ['Security Headers', 'Add X-Content-Type-Options, X-Frame-Options, X-XSS-Protection headers'],
'protect_wp_includes' => ['Protect WP Core Files', 'Block direct access to wp-includes, wp-admin/includes'],
'protect_uploads' => ['Block PHP in Uploads', 'Deny PHP file access and uploads in /wp-content/uploads/'],
'block_xmlrpc' => ['Block XML-RPC', 'Deny all access to xmlrpc.php'],
'block_malicious_queries' => ['Block Malicious Queries', 'Detect and block SQLi, XSS, and command injection in query strings'],
'block_author_scans' => ['Block Author Scans', 'Redirect ?author=N requests to prevent username enumeration'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_security', $key, $label, $desc, $opts);
endforeach;
?>
</section>
<section class="itk-card">
<h2>Custom Login URL</h2>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=protection">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_settings_login">
<?php $this->render_toggle('itk_security', 'enable_custom_login', 'Enable Custom Login URL', 'Replace /wp-login.php with a custom slug', $opts); ?>
<table class="form-table">
<tr>
<th>Login Slug</th>
<td>
<code><?= esc_html(home_url('/')) ?></code>
<input type="text" name="itk_security[custom_login_slug]" value="<?= esc_attr($opts['custom_login_slug'] ?? 'thoushallpass') ?>" style="width:200px">
<p class="description">Characters: letters, numbers, dashes only.</p>
</td>
</tr>
</table>
<?php submit_button('Save Login Settings'); ?>
</form>
</section>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: OPTIMIZATION
* ══════════════════════════════════════════════════════════ */
private function tab_optimization(): void {
$opts = get_option('itk_optimization', []);
?>
<div class="itk-settings-grid">
<section class="itk-card">
<h2>Performance &amp; Cleanup</h2>
<?php
$toggles = [
'remove_wp_version' => ['Remove WP Version', 'Strip WordPress version from <head> and all enqueued assets'],
'remove_script_versions' => ['Remove Asset Versions', 'Remove ?ver= query string from CSS/JS URLs'],
'remove_emoji' => ['Remove Emojis', 'Disable WordPress emoji scripts and styles'],
'deregister_wp_embed' => ['Remove WP Embed', 'Deregister the wp-embed script'],
'remove_wp_head_noise' => ['Clean WP Head', 'Remove RSD, wlwmanifest, feed links, and adjacent post links from <head>'],
'limit_revisions' => ['Limit Revisions', 'Keep only 3 post revisions and autosave every 5 minutes'],
'defer_js' => ['Defer JavaScript', 'Add defer attribute to non-critical scripts'],
'limit_heartbeat' => ['Limit Heartbeat', 'Restrict WordPress heartbeat to post editor pages only'],
'stop_empty_search_redirect'=> ['Fix Empty Search', 'Prevent redirect loop on empty search queries'],
'use_google_jquery' => ['Use Google jQuery', 'Load jQuery from Google CDN instead of local'],
'dns_prefetch' => ['DNS Prefetch', 'Enable DNS prefetching via meta header'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_optimization', $key, $label, $desc, $opts);
endforeach;
?>
</section>
<section class="itk-card">
<h2>Security Tweaks</h2>
<?php
$toggles = [
'hide_login_errors' => ['Hide Login Errors', 'Replace specific login error messages with a generic one'],
'remove_author_class' => ['Remove Author Class', 'Strip admin username from comment CSS classes'],
'remove_default_userfields'=> ['Remove User Fields', 'Remove AIM, Jabber, YIM from user profiles'],
'clean_bad_content' => ['Clean Bad Content', 'Remove empty tags, inline styles, and font tags on save'],
'change_author_base' => ['Change Author URL Base', "Change /author/ to /writer/ in author archive URLs"],
'disable_xml_rpc' => ['Disable XML-RPC (via filter)', 'Filter-based XML-RPC disable (additional to the blocker)'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_optimization', $key, $label, $desc, $opts);
endforeach;
?>
</section>
<section class="itk-card">
<h2>UI / Branding</h2>
<?php
$toggles = [
'disable_dashboard_widgets' => ['Disable Core Widgets', 'Remove WordPress News and WPEngine news dashboard widgets'],
'unregister_default_widgets'=> ['Unregister Sidebar Widgets', 'Remove Calendar, Archives, Meta, Search, Tag Cloud widgets'],
'disable_comments_url' => ['Remove Comment URL Field', 'Hide the website URL field from comment forms'],
'remove_admin_bar_links' => ['Clean Admin Bar', 'Remove WordPress logo, links, and noisy items from the toolbar'],
'admin_branding' => ['InformatiQ Branding', 'Add InformatiQ logo widget, toolbar link, admin notice, and custom footer'],
'disable_floc' => ['Disable FLoC', 'Add Permissions-Policy: interest-cohort=() header'],
'lightbox_images' => ['Lightbox Images', 'Add rel="lightbox" to image links in post content'],
'featured_image_rss' => ['Featured Image in RSS', 'Include featured image in RSS feed entries'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_optimization', $key, $label, $desc, $opts);
endforeach;
?>
</section>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: HONEYPOT
* ══════════════════════════════════════════════════════════ */
private function tab_honeypot(): void {
$opts = get_option('itk_honeypot', []);
?>
<div class="itk-settings-grid">
<section class="itk-card">
<h2>Honeypot Fields</h2>
<?php
$toggles = [
'enabled' => ['Enable Honeypot', 'Inject invisible honeypot fields into all protected forms'],
'protect_comments' => ['Comments', 'Protect comment forms'],
'protect_login' => ['Login Form', 'Protect wp-login.php login'],
'protect_register' => ['Registration', 'Protect user registration'],
'protect_lost_password' => ['Lost Password', 'Protect lost password form'],
'protect_woocommerce' => ['WooCommerce', 'Protect WooCommerce checkout and registration'],
'protect_cf7' => ['Contact Form 7', 'Protect CF7 forms'],
'protect_elementor' => ['Elementor Forms', 'Protect Elementor Pro form widget'],
'protect_gravity' => ['Gravity Forms', 'Protect Gravity Forms'],
'protect_search' => ['Search Form', 'Protect the WordPress search form'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_honeypot', $key, $label, $desc, $opts);
endforeach;
?>
</section>
<section class="itk-card">
<h2>Timing Rules</h2>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_settings_honeypot">
<table class="form-table">
<tr>
<th>Minimum Submit Time (seconds)</th>
<td>
<input type="number" name="itk_honeypot[min_time]" value="<?= (int)($opts['min_time'] ?? 3) ?>" min="1" max="60">
<p class="description">Block submissions faster than this (bots submit instantly).</p>
</td>
</tr>
<tr>
<th>Maximum Submit Time (seconds)</th>
<td>
<input type="number" name="itk_honeypot[max_time]" value="<?= (int)($opts['max_time'] ?? 7200) ?>" min="60">
<p class="description">Block submissions older than this (stale/replayed forms).</p>
</td>
</tr>
<tr>
<th>Retain Logs (days)</th>
<td>
<input type="number" name="itk_honeypot[retain_days]" value="<?= (int)($opts['retain_days'] ?? 90) ?>" min="1">
<p class="description">Automatically prune logs older than this many days.</p>
</td>
</tr>
</table>
<?php submit_button('Save Honeypot Settings'); ?>
</form>
</section>
<?php
/* ── Central Honeypot API card ────────────────── */
$hp_api = ITK_HP_API::settings();
$hp_queue = count((array) get_option(ITK_HP_API::OPT_QUEUE, []));
$hp_total = ITK_Database::count_honeypot_rows();
$hp_sent = (int) get_option('itk_hp_history_sent', 0);
$hp_rem = max(0, $hp_total - $hp_sent);
$hp_ok = $hp_api['connection_ok'];
$hp_cls = is_null($hp_ok) ? 'itk-api-unknown' : ($hp_ok ? 'itk-api-ok' : 'itk-api-err');
$hp_lbl = is_null($hp_ok) ? 'Not tested' : ($hp_ok ? 'Connected' : 'Connection failed');
$hp_test_r = get_transient('itk_hp_api_test_result'); if ($hp_test_r) delete_transient('itk_hp_api_test_result');
$hp_hist_r = get_transient('itk_hp_history_result'); if ($hp_hist_r) delete_transient('itk_hp_history_result');
?>
<section class="itk-card itk-api-card">
<h2>Central Honeypot API</h2>
<p class="description itk-api-desc">Send honeypot catch events to your self-hosted Honeypot Intelligence Docker stack (port 3000).</p>
<div class="itk-api-status-bar">
<span class="itk-api-badge <?= esc_attr($hp_cls) ?>"><?= esc_html($hp_lbl) ?></span>
<?php if ($hp_api['last_verified'] > 0): ?>
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$hp_api['last_verified'])) ?> ago</span>
<?php endif; ?>
<?php if (!$hp_ok && !is_null($hp_ok) && !empty($hp_api['last_error'])): ?>
<span class="itk-api-err-msg"><?= esc_html($hp_api['last_error']) ?></span>
<?php endif; ?>
</div>
<?php if ($hp_test_r): ?>
<div class="itk-api-notice <?= $hp_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($hp_test_r['message']) ?></div>
<?php endif; ?>
<?php if ($hp_hist_r): ?>
<div class="itk-api-notice <?= $hp_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($hp_hist_r['message']) ?></div>
<?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_hp_api">
<table class="form-table itk-api-table">
<tr>
<th>Enable</th>
<td><label><input type="checkbox" name="itk_hp_api_settings[enabled]" value="1" <?= checked(!empty($hp_api['enabled'])) ?>> Send events to Central API</label></td>
</tr>
<tr>
<th>API URL</th>
<td>
<input type="url" name="itk_hp_api_settings[api_url]" value="<?= esc_attr($hp_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3000">
<p class="description">Base URL of your Honeypot API stack (e.g. <code>http://localhost:3000</code>)</p>
</td>
</tr>
<tr>
<th>API Token</th>
<td>
<input type="password" name="itk_hp_api_settings[api_token]" value="" class="regular-text"
placeholder="<?= !empty($hp_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
autocomplete="new-password">
</td>
</tr>
</table>
<div class="itk-api-form-actions">
<?php submit_button('Save Honeypot API Settings', 'primary', 'submit', false); ?>
<button type="button" class="button itk-btn-test-api" data-api="hp" style="margin-left:8px">Test Connection</button>
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
</div>
</form>
<div class="itk-api-footer">
<div class="itk-api-queue-row">
<strong><?= (int) $hp_queue ?></strong> event(s) pending in queue
<button type="button" class="button button-small itk-btn-flush-api" data-api="hp" style="margin-left:8px">Flush Now</button>
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
</div>
<div class="itk-api-history-row">
<strong>Historical sync:</strong>
<?= number_format($hp_sent) ?> / <?= number_format($hp_total) ?> records sent
<?php if ($hp_rem > 0): ?><em class="itk-api-rem">(<?= number_format($hp_rem) ?> remaining)</em><?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="display:inline;margin-left:10px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="send_hp_history">
<input type="submit" class="button button-small" value="Send Next 50">
</form>
<?php if ($hp_sent > 0): ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="reset_hp_history">
<input type="submit" class="button button-small" value="Reset Progress">
</form>
<?php endif; ?>
</div>
</div>
</section>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: WAF
* ══════════════════════════════════════════════════════════ */
private function tab_waf(): void {
$opts = get_option('itk_waf', []);
?>
<div class="itk-settings-grid">
<!-- ── WAF Enable + Category Toggles ── -->
<section class="itk-card">
<h2>Web Application Firewall</h2>
<?php
$toggles = [
'enabled' => ['Enable WAF', 'Inspect incoming requests against all active rule categories'],
'block_sqli' => ['SQL Injection', 'Block SQLi patterns in query parameters, POST fields, and URIs'],
'block_xss' => ['Cross-Site Scripting', 'Block XSS payloads including script tags, event handlers, and JS URIs'],
'block_lfi' => ['Local File Inclusion', 'Block path traversal and LFI patterns (../../etc/passwd etc.)'],
'block_rfi' => ['Remote File Inclusion', 'Block attempts to include remote URLs as file paths'],
'block_cmdi' => ['Command Injection', 'Block OS command injection patterns (;, |, backticks, etc.)'],
'block_xxe' => ['XML External Entity', 'Block XXE payloads in request bodies and parameters'],
'block_php_inject' => ['PHP Code Injection', 'Block PHP code execution attempts (eval, base64_decode, etc.)'],
'block_ssrf' => ['SSRF', 'Block Server-Side Request Forgery patterns targeting internal hosts'],
'block_wp_specific' => ['WordPress-Specific','Block WP-targeted probes (xmlrpc abuse, admin enumeration, etc.)'],
];
foreach ($toggles as $key => [$label, $desc]):
$this->render_toggle('itk_waf', $key, $label, $desc, $opts);
endforeach;
?>
</section>
<!-- ── Scan Scope + Action Settings ── -->
<section class="itk-card">
<h2>Scan Scope &amp; Response</h2>
<?php
$scope_toggles = [
'scan_post' => ['Scan POST Data', 'Inspect POST body fields for attack patterns'],
'scan_cookies' => ['Scan Cookies', 'Inspect cookie values (adds overhead; off by default)'],
'scan_ua' => ['Scan User-Agent', 'Inspect the User-Agent header'],
'log_attacks' => ['Log Attacks', 'Record matched attacks to the local attack log table'],
];
foreach ($scope_toggles as $key => [$label, $desc]):
$this->render_toggle('itk_waf', $key, $label, $desc, $opts);
endforeach;
?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=waf" style="margin-top:20px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_settings_waf">
<table class="form-table">
<tr>
<th>Action</th>
<td>
<select name="itk_waf[action]">
<option value="block" <?= selected($opts['action'] ?? 'block', 'block', false) ?>>Block request</option>
<option value="log_only" <?= selected($opts['action'] ?? 'block', 'log_only', false) ?>>Log only (no block)</option>
</select>
<p class="description">Choose whether to block the request or only log the attack.</p>
</td>
</tr>
<tr>
<th>Response Code</th>
<td>
<select name="itk_waf[response_code]">
<option value="403" <?= selected($opts['response_code'] ?? '403', '403', false) ?>>403 Forbidden</option>
<option value="400" <?= selected($opts['response_code'] ?? '403', '400', false) ?>>400 Bad Request</option>
<option value="301_custom" <?= selected($opts['response_code'] ?? '403', '301_custom', false) ?>>301 Redirect to URL</option>
</select>
</td>
</tr>
<tr>
<th>Redirect URL</th>
<td>
<input type="url" name="itk_waf[redirect_url]" value="<?= esc_attr($opts['redirect_url'] ?? '') ?>" class="regular-text" placeholder="https://example.com/blocked">
<p class="description">Only used when response code is 301 redirect.</p>
</td>
</tr>
<tr>
<th>Block Message</th>
<td>
<input type="text" name="itk_waf[custom_message]" value="<?= esc_attr($opts['custom_message'] ?? 'Access denied.') ?>" class="regular-text">
<p class="description">Message shown on 403/400 responses.</p>
</td>
</tr>
</table>
<?php submit_button('Save WAF Settings'); ?>
</form>
</section>
<!-- ── Central Attacks API card ── -->
<?php
$atk_api = ITK_Attacks_API::settings();
$atk_queue = count((array) get_option(ITK_Attacks_API::OPT_QUEUE, []));
$atk_total = ITK_Database::count_attack_rows();
$atk_sent = (int) get_option('itk_attacks_history_sent', 0);
$atk_rem = max(0, $atk_total - $atk_sent);
$atk_ok = $atk_api['connection_ok'];
$atk_cls = is_null($atk_ok) ? 'itk-api-unknown' : ($atk_ok ? 'itk-api-ok' : 'itk-api-err');
$atk_lbl = is_null($atk_ok) ? 'Not tested' : ($atk_ok ? 'Connected' : 'Connection failed');
$atk_test_r = get_transient('itk_attacks_api_test_result'); if ($atk_test_r) delete_transient('itk_attacks_api_test_result');
$atk_hist_r = get_transient('itk_attacks_history_result'); if ($atk_hist_r) delete_transient('itk_attacks_history_result');
?>
<section class="itk-card itk-api-card">
<h2>Central Attacks API</h2>
<p class="description itk-api-desc">Send WAF attack events to your self-hosted Attack Intelligence Docker stack (port 3083).</p>
<div class="itk-api-status-bar">
<span class="itk-api-badge <?= esc_attr($atk_cls) ?>"><?= esc_html($atk_lbl) ?></span>
<?php if ($atk_api['last_verified'] > 0): ?>
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$atk_api['last_verified'])) ?> ago</span>
<?php endif; ?>
<?php if (!$atk_ok && !is_null($atk_ok) && !empty($atk_api['last_error'])): ?>
<span class="itk-api-err-msg"><?= esc_html($atk_api['last_error']) ?></span>
<?php endif; ?>
</div>
<?php if ($atk_test_r): ?>
<div class="itk-api-notice <?= $atk_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($atk_test_r['message']) ?></div>
<?php endif; ?>
<?php if ($atk_hist_r): ?>
<div class="itk-api-notice <?= $atk_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($atk_hist_r['message']) ?></div>
<?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=waf">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_attacks_api">
<table class="form-table itk-api-table">
<tr>
<th>Enable</th>
<td><label><input type="checkbox" name="itk_attacks_api_settings[enabled]" value="1" <?= checked(!empty($atk_api['enabled'])) ?>> Send attacks to Central API</label></td>
</tr>
<tr>
<th>API URL</th>
<td>
<input type="url" name="itk_attacks_api_settings[api_url]" value="<?= esc_attr($atk_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3083">
<p class="description">Base URL of your Attack API stack (e.g. <code>http://localhost:3083</code>)</p>
</td>
</tr>
<tr>
<th>API Token</th>
<td>
<input type="password" name="itk_attacks_api_settings[api_token]" value="" class="regular-text"
placeholder="<?= !empty($atk_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
autocomplete="new-password">
</td>
</tr>
</table>
<div class="itk-api-form-actions">
<?php submit_button('Save Attacks API Settings', 'primary', 'submit', false); ?>
<button type="button" class="button itk-btn-test-api" data-api="attacks" style="margin-left:8px">Test Connection</button>
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
</div>
</form>
<div class="itk-api-footer">
<div class="itk-api-queue-row">
<strong><?= (int) $atk_queue ?></strong> event(s) pending in queue
<button type="button" class="button button-small itk-btn-flush-api" data-api="attacks" style="margin-left:8px">Flush Now</button>
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
</div>
<div class="itk-api-history-row">
<strong>Historical sync:</strong>
<?= number_format($atk_sent) ?> / <?= number_format($atk_total) ?> records sent
<?php if ($atk_rem > 0): ?><em class="itk-api-rem">(<?= number_format($atk_rem) ?> remaining)</em><?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=waf" style="display:inline;margin-left:10px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="send_attacks_history">
<input type="submit" class="button button-small" value="Send Next 50">
</form>
<?php if ($atk_sent > 0): ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=waf" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="reset_attacks_history">
<input type="submit" class="button button-small" value="Reset Progress">
</form>
<?php endif; ?>
</div>
</div>
</section>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: ATTACK LOGS
* ══════════════════════════════════════════════════════════ */
private function tab_attack_logs(): void {
$search = sanitize_text_field($_GET['atk_search'] ?? '');
$filter_ip = sanitize_text_field($_GET['atk_ip'] ?? '');
$filter_type = sanitize_key($_GET['atk_type'] ?? '');
$paged = max(1, (int)($_GET['paged'] ?? 1));
$offset = ($paged - 1) * self::PER_PAGE;
$args = ['per_page' => self::PER_PAGE, 'offset' => $offset];
if ($search) $args['search'] = $search;
if ($filter_ip) $args['ip'] = $filter_ip;
if ($filter_type) $args['attack_type'] = $filter_type;
$rows = ITK_Database::get_attack_rows($args);
$total = ITK_Database::count_attack_rows($args);
$attack_types = ITK_Database::get_attack_types();
$total_pages = max(1, (int)ceil($total / self::PER_PAGE));
$base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=attack-logs');
?>
<div class="itk-log-page">
<!-- Filters -->
<form method="get" class="itk-filters">
<input type="hidden" name="page" value="<?= self::MENU_SLUG ?>">
<input type="hidden" name="tab" value="attack-logs">
<input type="text" name="atk_search" placeholder="Search IP, param, payload…" value="<?= esc_attr($search) ?>">
<input type="text" name="atk_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
<select name="atk_type">
<option value="">All attack types</option>
<?php foreach ($attack_types as $at): ?>
<option value="<?= esc_attr($at) ?>" <?= selected($filter_type, $at, false) ?>><?= esc_html($at) ?></option>
<?php endforeach; ?>
</select>
<input type="submit" class="button" value="Filter">
<a href="<?= esc_url($base_url) ?>" class="button">Reset</a>
</form>
<p class="itk-count">Showing <?= count($rows) ?> of <?= number_format($total) ?> result(s)</p>
<table class="itk-log-table widefat striped">
<thead>
<tr>
<th>Date / Time</th><th>IP</th><th>Type</th>
<th>Source</th><th>Param</th><th>Payload</th><th>URI</th><th>Method</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="8" class="itk-no-results">No attacks logged yet.</td></tr>
<?php else: ?>
<?php foreach ($rows as $row): ?>
<tr>
<td class="itk-nowrap"><?= esc_html($row->logged_at) ?></td>
<td>
<?= esc_html($row->ip_address) ?>
<a href="<?= esc_url($base_url . '&atk_ip=' . urlencode($row->ip_address)) ?>" class="itk-filter-link">[filter]</a>
<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-filter-link">[lookup]</a>
</td>
<td><span class="itk-badge itk-badge-block"><?= esc_html($row->attack_type) ?></span></td>
<td><?= esc_html($row->source) ?></td>
<td class="itk-uri"><?= esc_html(substr($row->param, 0, 60)) ?></td>
<td class="itk-uri" title="<?= esc_attr($row->payload) ?>"><?= esc_html(substr($row->payload, 0, 80)) ?></td>
<td class="itk-uri"><?= esc_html(substr($row->request_uri, 0, 80)) ?></td>
<td><?= esc_html($row->method) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php $this->render_pager($paged, $total_pages, $base_url); ?>
<!-- Clear logs -->
<form method="post" style="margin-top:16px" onsubmit="return confirm('Delete ALL attack log entries?')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="clear_attack_log">
<input type="submit" class="button button-secondary itk-btn-danger" value="Clear All Attack Logs">
</form>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: BOT LOGS
* ══════════════════════════════════════════════════════════ */
private function tab_bot_logs(): void {
$search = sanitize_text_field($_GET['hp_search'] ?? '');
$filter_ip= sanitize_text_field($_GET['hp_ip'] ?? '');
$filter_bt= sanitize_text_field($_GET['hp_bot'] ?? '');
$filter_ac= sanitize_key($_GET['hp_action'] ?? '');
$paged = max(1, (int)($_GET['paged'] ?? 1));
$offset = ($paged - 1) * self::PER_PAGE;
$args = ['per_page' => self::PER_PAGE, 'offset' => $offset];
if ($search) $args['search'] = $search;
if ($filter_ip) $args['ip'] = $filter_ip;
if ($filter_bt) $args['bot_type'] = $filter_bt;
if ($filter_ac) $args['action'] = $filter_ac;
$rows = ITK_Database::get_bot_rows($args);
$total = ITK_Database::count_bot_rows($args);
$bot_types = ITK_Database::get_bot_types();
$total_pages= max(1, (int)ceil($total / self::PER_PAGE));
$base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=bot-logs');
?>
<div class="itk-log-page">
<!-- Filters -->
<form method="get" class="itk-filters">
<input type="hidden" name="page" value="<?= self::MENU_SLUG ?>">
<input type="hidden" name="tab" value="bot-logs">
<input type="text" name="hp_search" placeholder="Search IP, UA, reason…" value="<?= esc_attr($search) ?>">
<input type="text" name="hp_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
<select name="hp_bot">
<option value="">All bot types</option>
<?php foreach ($bot_types as $bt): ?>
<option value="<?= esc_attr($bt) ?>" <?= selected($filter_bt, $bt, false) ?>><?= esc_html($bt) ?></option>
<?php endforeach; ?>
</select>
<select name="hp_action">
<option value="">All actions</option>
<option value="blocked" <?= selected($filter_ac, 'blocked', false) ?>>Blocked</option>
<option value="rate_limited" <?= selected($filter_ac, 'rate_limited', false) ?>>Rate Limited</option>
</select>
<input type="submit" class="button" value="Filter">
<a href="<?= esc_url($base_url) ?>" class="button">Reset</a>
</form>
<p class="itk-count">Showing <?= count($rows) ?> of <?= number_format($total) ?> result(s)</p>
<table class="itk-log-table widefat striped">
<thead>
<tr>
<th>Date / Time</th><th>IP</th><th>Bot Type</th>
<th>Action</th><th>Reason</th><th>URI</th><th>User Agent</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" class="itk-no-results">No bot activity logged yet.</td></tr>
<?php else: ?>
<?php foreach ($rows as $row):
$action_class = $row->action === 'rate_limited' ? 'itk-badge-warn' : 'itk-badge-block';
?>
<tr>
<td class="itk-nowrap"><?= esc_html($row->logged_at) ?></td>
<td>
<?= esc_html($row->ip_address) ?>
<a href="<?= esc_url($base_url . '&hp_ip=' . urlencode($row->ip_address)) ?>" class="itk-filter-link">[filter]</a>
<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-filter-link">[lookup]</a>
</td>
<td><?= esc_html($row->bot_type) ?></td>
<td><span class="itk-badge <?= $action_class ?>"><?= esc_html($row->action) ?></span></td>
<td><?= esc_html($row->reason) ?></td>
<td class="itk-uri"><?= esc_html(substr($row->request_uri, 0, 80)) ?></td>
<td class="itk-ua"><?= esc_html(substr($row->user_agent, 0, 100)) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php $this->render_pager($paged, $total_pages, $base_url); ?>
<!-- Clear logs -->
<form method="post" style="margin-top:16px" onsubmit="return confirm('Delete ALL bot log entries?')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="clear_bot_log">
<input type="submit" class="button button-secondary itk-btn-danger" value="Clear All Bot Logs">
</form>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: HONEYPOT LOGS
* ══════════════════════════════════════════════════════════ */
private function tab_honeypot_logs(): void {
$search = sanitize_text_field($_GET['hp_search'] ?? '');
$filter_ip = sanitize_text_field($_GET['hp_ip'] ?? '');
$filter_form = sanitize_text_field($_GET['hp_form'] ?? '');
$paged = max(1, (int)($_GET['paged'] ?? 1));
$offset = ($paged - 1) * self::PER_PAGE;
$args = ['per_page' => self::PER_PAGE, 'offset' => $offset];
if ($search) $args['search'] = $search;
if ($filter_ip) $args['ip'] = $filter_ip;
if ($filter_form) $args['form'] = $filter_form;
$rows = ITK_Database::get_honeypot_rows($args);
$total = ITK_Database::count_honeypot_rows($args);
$form_types = ITK_Database::get_honeypot_form_types();
$total_pages = max(1, (int)ceil($total / self::PER_PAGE));
$base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=honeypot-logs');
?>
<div class="itk-log-page">
<form method="get" class="itk-filters">
<input type="hidden" name="page" value="<?= self::MENU_SLUG ?>">
<input type="hidden" name="tab" value="honeypot-logs">
<input type="text" name="hp_search" placeholder="Search IP, UA, reason…" value="<?= esc_attr($search) ?>">
<input type="text" name="hp_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
<select name="hp_form">
<option value="">All form types</option>
<?php foreach ($form_types as $ft): ?>
<option value="<?= esc_attr($ft) ?>" <?= selected($filter_form, $ft, false) ?>><?= esc_html($ft) ?></option>
<?php endforeach; ?>
</select>
<input type="submit" class="button" value="Filter">
<a href="<?= esc_url($base_url) ?>" class="button">Reset</a>
</form>
<p class="itk-count">Showing <?= count($rows) ?> of <?= number_format($total) ?> result(s)</p>
<table class="itk-log-table widefat striped">
<thead>
<tr>
<th>Date / Time</th><th>IP</th><th>Form Type</th>
<th>Reason</th><th>URI</th><th>User Agent</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="6" class="itk-no-results">No honeypot catches yet.</td></tr>
<?php else: ?>
<?php foreach ($rows as $row): ?>
<tr>
<td class="itk-nowrap"><?= esc_html($row->blocked_at) ?></td>
<td>
<?= esc_html($row->ip_address) ?>
<a href="<?= esc_url($base_url . '&hp_ip=' . urlencode($row->ip_address)) ?>" class="itk-filter-link">[filter]</a>
<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-filter-link">[lookup]</a>
</td>
<td><span class="itk-badge itk-badge-hp"><?= esc_html($row->form_type) ?></span></td>
<td><?= esc_html($row->reason) ?></td>
<td class="itk-uri"><?= esc_html(substr($row->request_uri, 0, 80)) ?></td>
<td class="itk-ua"><?= esc_html(substr($row->user_agent, 0, 100)) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php $this->render_pager($paged, $total_pages, $base_url); ?>
<form method="post" style="margin-top:16px" onsubmit="return confirm('Delete ALL honeypot log entries?')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="clear_honeypot_log">
<input type="submit" class="button button-secondary itk-btn-danger" value="Clear All Honeypot Logs">
</form>
</div>
<?php
}
/* ══════════════════════════════════════════════════════════
* TAB: CONFIG FILES
* ══════════════════════════════════════════════════════════ */
private function tab_config_files(): void {
$files = [
'badbots' => ['Bad Bots', 'config/badbots.conf', 'One bot user-agent substring per line. Lines starting with # are comments.'],
'goodbots' => ['Good Bots', 'config/goodbots.conf', 'Format: BotName|rate_per_minute (0 = always block)'],
'referrers' => ['Bad Referrers', 'config/referrers.conf', 'One domain substring per line.'],
'networks' => ['Bad Networks', 'config/networks.conf', 'One IP or CIDR range per line (e.g. 1.2.3.0/24).'],
'allowed-ips' => ['Allowed IPs', 'config/allowed-ips.conf','IPs/CIDRs allowed to access wp-login.php (one per line).'],
'whitelist' => ['Whitelist', 'config/whitelist.conf', 'IPs, CIDRs, or UA substrings (ua:...) that bypass all restrictions. One per line.'],
];
$active_file = sanitize_key($_GET['file'] ?? 'badbots');
if (!isset($files[$active_file])) $active_file = 'badbots';
[$title, $path, $desc] = $files[$active_file];
$full_path = ITK_PATH . $path;
$content = file_exists($full_path) ? file_get_contents($full_path) : '';
?>
<div class="itk-config-editor">
<div class="itk-config-tabs">
<?php foreach ($files as $slug => [$label]): ?>
<a href="<?= esc_url(admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=config-files&file=' . $slug)) ?>"
class="itk-config-tab <?= $slug === $active_file ? 'active' : '' ?>"><?= esc_html($label) ?></a>
<?php endforeach; ?>
</div>
<div class="itk-config-editor-area">
<h3><?= esc_html($title) ?> <code><?= esc_html($path) ?></code></h3>
<p class="description"><?= esc_html($desc) ?></p>
<textarea id="itk-config-content" rows="25" class="itk-config-textarea"><?= esc_textarea($content) ?></textarea>
<p>
<button id="itk-save-config" class="button button-primary" data-file="<?= esc_attr($active_file) ?>">Save File</button>
<span id="itk-config-status" style="margin-left:10px;color:green;display:none">Saved!</span>
</p>
</div>
</div>
<?php
}
/* ── Shared helpers ───────────────────────────────────────── */
private function render_toggle(string $option, string $key, string $label, string $desc, array $opts): void {
$checked = !empty($opts[$key]);
?>
<div class="itk-toggle-row">
<div class="itk-toggle-info">
<span class="itk-toggle-label"><?= esc_html($label) ?></span>
<span class="itk-toggle-desc"><?= esc_html($desc) ?></span>
</div>
<label class="itk-switch">
<input type="checkbox"
class="itk-toggle-input"
data-option="<?= esc_attr($option) ?>"
data-setting="<?= esc_attr($key) ?>"
<?= $checked ? 'checked' : '' ?>>
<span class="itk-slider"></span>
</label>
</div>
<?php
}
private function render_pager(int $paged, int $total_pages, string $base_url): void {
if ($total_pages <= 1) return;
echo '<div class="itk-pager">';
if ($paged > 1) {
echo '<a href="' . esc_url(add_query_arg('paged', $paged - 1, $base_url)) . '" class="button">&laquo; Prev</a>';
}
echo '<span>' . sprintf('Page %d of %d', $paged, $total_pages) . '</span>';
if ($paged < $total_pages) {
echo '<a href="' . esc_url(add_query_arg('paged', $paged + 1, $base_url)) . '" class="button">Next &raquo;</a>';
}
echo '</div>';
}
}