2026-04-09 11:45:26 +02:00
<? 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' ]);
2026-04-09 18:32:27 +02:00
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' ]);
2026-04-09 11:45:26 +02:00
}
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 ;
2026-04-09 18:32:27 +02:00
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 ;
2026-04-09 11:45:26 +02:00
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' , [
2026-04-17 07:18:49 +02:00
'min_time' , 'max_time' , 'retain_days' , 'max_links' ,
], [
'block_url_in_email' , 'block_links_new_commenters' ,
]);
2026-04-09 11:45:26 +02:00
$this -> redirect ([ 'tab' => 'honeypot' , 'saved' => 1 ]);
break ;
2026-04-10 09:37:31 +02:00
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 ;
2026-04-09 11:45:26 +02:00
}
}
/**
* 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 );
}
2026-04-09 18:32:27 +02:00
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' ] ?? '' );
2026-04-10 09:37:31 +02:00
$result = match ( $which ) {
'bot' => ITK_Bot_API :: test_connection (),
'attacks' => ITK_Attacks_API :: test_connection (),
default => ITK_HP_API :: test_connection (),
};
2026-04-09 18:32:27 +02:00
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' ] ?? '' );
2026-04-10 09:37:31 +02:00
match ( $which ) {
'bot' => ITK_Bot_API :: flush (),
'attacks' => ITK_Attacks_API :: flush (),
default => ITK_HP_API :: flush (),
};
2026-04-09 18:32:27 +02:00
wp_send_json_success ( 'Queue flushed.' );
}
2026-04-09 11:45:26 +02:00
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 );
2026-04-10 09:37:31 +02:00
$allowed = [ 'itk_security' , 'itk_optimization' , 'itk_honeypot' , 'itk_waf' ];
2026-04-09 11:45:26 +02:00
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' ,
2026-04-13 10:00:16 +02:00
'whitelist' => ITK_PATH . 'config/whitelist.conf' ,
2026-04-09 11:45:26 +02:00
];
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' );
2026-04-13 10:00:16 +02:00
ITK_Whitelist :: invalidate_cache ();
2026-04-09 11:45:26 +02:00
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',
2026-04-10 09:37:31 +02:00
'waf' => 'WAF',
2026-04-09 11:45:26 +02:00
'bot-logs' => 'Bot Logs',
'honeypot-logs' => 'Honeypot Logs',
2026-04-10 09:37:31 +02:00
'attack-logs' => 'Attack Logs',
2026-04-09 11:45:26 +02:00
'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(),
2026-04-10 09:37:31 +02:00
'waf' => $this->tab_waf(),
2026-04-09 11:45:26 +02:00
'bot-logs' => $this->tab_bot_logs(),
'honeypot-logs' => $this->tab_honeypot_logs(),
2026-04-10 09:37:31 +02:00
'attack-logs' => $this->tab_attack_logs(),
2026-04-09 11:45:26 +02:00
'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>
2026-04-09 18:32:27 +02:00
<?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>
2026-04-09 11:45:26 +02:00
</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>
2026-04-09 18:32:27 +02:00
2026-04-17 07:18:49 +02:00
<section class="itk-card">
<h2>Comment Spam Content</h2>
<p class="description" style="margin-bottom:16px">Content-based rules that catch spam comments that pass the honeypot and timing checks.</p>
<?php
$content_toggles = [
'block_url_in_email' => ['Block URL in Email Field', 'Block comments where the email field contains a URL instead of a real address'],
'block_links_new_commenters' => ['Block Links from New Commenters', 'Block any comment with a URL or website from an author with zero approved comments'],
];
foreach ($content_toggles as $key => [$label, $desc]):
$this->render_toggle('itk_honeypot', $key, $label, $desc, $opts);
endforeach;
?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="margin-top:16px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_settings_honeypot">
<table class="form-table">
<tr>
<th>Max Links per Comment</th>
<td>
<input type="number" name="itk_honeypot[max_links]" value="<?= (int)($opts['max_links'] ?? 2) ?>" min="0" max="20">
<p class="description">Block comments with more than this many URLs. Set 0 to disable.</p>
</td>
</tr>
</table>
<?php submit_button('Save Content Rules'); ?>
</form>
</section>
2026-04-09 18:32:27 +02:00
<?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>
2026-04-09 11:45:26 +02:00
</div>
<?php
}
2026-04-10 09:37:31 +02:00
/* ══════════════════════════════════════════════════════════
* 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 & 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
}
2026-04-09 11:45:26 +02:00
/* ══════════════════════════════════════════════════════════
* 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).'],
2026-04-13 10:00:16 +02:00
'whitelist' => ['Whitelist', 'config/whitelist.conf', 'IPs, CIDRs, or UA substrings (ua:...) that bypass all restrictions. One per line.'],
2026-04-09 11:45:26 +02:00
];
$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>';
}
}