863 lines
45 KiB
PHP
863 lines
45 KiB
PHP
|
|
<?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">■ 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>
|
|||
|
|
<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 & 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">« 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 »</a>';
|
|||
|
|
}
|
|||
|
|
echo '</div>';
|
|||
|
|
}
|
|||
|
|
}
|