Files
InformatiQ-Toolkit/includes/class-itk-admin.php
Malin 6d4349ff7b feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:

- Fixed deactivation bug: all protection methods now guard themselves
  with their own option check so toggling off via AJAX takes effect
  immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
  DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
  configurable per-bot limits in goodbots.conf (BotName|req/min);
  returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
  replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
  blocked, today's count, rate-limited hits, top threat sources
  (bar chart), top IPs, top honeypot form types, active-module
  status panel.
- All optimizations from utils.php merged into Optimization tab as
  toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
  Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
  Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
  networks.conf, allowed-ips.conf with AJAX save and transient flush.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00

863 lines
45 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']);
}
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_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;
}
}
/**
* 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 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'];
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',
];
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');
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',
'bot-logs' => 'Bot Logs',
'honeypot-logs' => 'Honeypot 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(),
'bot-logs' => $this->tab_bot_logs(),
'honeypot-logs' => $this->tab_honeypot_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>
</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>
</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).'],
];
$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>';
}
}