feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:
- Fixed deactivation bug: all protection methods now guard themselves
with their own option check so toggling off via AJAX takes effect
immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
configurable per-bot limits in goodbots.conf (BotName|req/min);
returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
blocked, today's count, rate-limited hits, top threat sources
(bar chart), top IPs, top honeypot form types, active-module
status panel.
- All optimizations from utils.php merged into Optimization tab as
toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
networks.conf, allowed-ips.conf with AJAX save and transient flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00
< ? 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' ]);
feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:
- Fixed deactivation bug: all protection methods now guard themselves
with their own option check so toggling off via AJAX takes effect
immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
configurable per-bot limits in goodbots.conf (BotName|req/min);
returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
blocked, today's count, rate-limited hits, top threat sources
(bar chart), top IPs, top honeypot form types, active-module
status panel.
- All optimizations from utils.php merged into Optimization tab as
toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
networks.conf, allowed-ips.conf with AJAX save and transient flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00
}
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 ;
feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:
- Fixed deactivation bug: all protection methods now guard themselves
with their own option check so toggling off via AJAX takes effect
immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
configurable per-bot limits in goodbots.conf (BotName|req/min);
returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
blocked, today's count, rate-limited hits, top threat sources
(bar chart), top IPs, top honeypot form types, active-module
status panel.
- All optimizations from utils.php merged into Optimization tab as
toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
networks.conf, allowed-ips.conf with AJAX save and transient flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00
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 );
}
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' ] ? ? '' );
$result = $which === 'bot' ? ITK_Bot_API :: test_connection () : ITK_HP_API :: test_connection ();
wp_send_json ( $result );
}
/* ── AJAX: flush API queue ─────────────────────────────────── */
public function ajax_flush_api_queue () : void {
check_ajax_referer ( self :: NONCE_ACTION , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) wp_send_json_error ( 'unauthorized' );
$which = sanitize_key ( $_POST [ 'api' ] ? ? '' );
if ( $which === 'bot' ) ITK_Bot_API :: flush (); else ITK_HP_API :: flush ();
wp_send_json_success ( 'Queue flushed.' );
}
feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:
- Fixed deactivation bug: all protection methods now guard themselves
with their own option check so toggling off via AJAX takes effect
immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
configurable per-bot limits in goodbots.conf (BotName|req/min);
returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
blocked, today's count, rate-limited hits, top threat sources
(bar chart), top IPs, top honeypot form types, active-module
status panel.
- All optimizations from utils.php merged into Optimization tab as
toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
networks.conf, allowed-ips.conf with AJAX save and transient flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00
private function redirect ( array $args ) : void {
wp_redirect ( add_query_arg ( array_merge ([ 'page' => self :: MENU_SLUG ], $args ), admin_url ( 'options-general.php' )));
exit ;
}
/* ── AJAX: save single toggle setting ─────────────────────── */
public function ajax_save_setting () : void {
check_ajax_referer ( self :: NONCE_ACTION , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) wp_send_json_error ( 'unauthorized' );
$option = sanitize_key ( $_POST [ 'option' ] ? ? '' );
$setting = sanitize_key ( $_POST [ 'setting' ] ? ? '' );
$value = ( int )( $_POST [ 'value' ] ? ? 0 );
$allowed = [ 'itk_security' , 'itk_optimization' , 'itk_honeypot' ];
if ( ! in_array ( $option , $allowed , true ) || empty ( $setting )) {
wp_send_json_error ( 'invalid' );
}
$opts = get_option ( $option , []);
$opts [ $setting ] = $value ;
update_option ( $option , $opts );
wp_send_json_success ();
}
/* ── AJAX: save config file ───────────────────────────────── */
public function ajax_save_config_file () : void {
check_ajax_referer ( self :: NONCE_ACTION , 'nonce' );
if ( ! current_user_can ( 'manage_options' )) wp_send_json_error ( 'unauthorized' );
$file = sanitize_key ( $_POST [ 'file' ] ? ? '' );
$content = wp_unslash ( $_POST [ 'content' ] ? ? '' );
$allowed = [
'badbots' => ITK_PATH . 'config/badbots.conf' ,
'goodbots' => ITK_PATH . 'config/goodbots.conf' ,
'referrers' => ITK_PATH . 'config/referrers.conf' ,
'networks' => ITK_PATH . 'config/networks.conf' ,
'allowed-ips' => ITK_PATH . 'config/allowed-ips.conf' ,
];
if ( ! isset ( $allowed [ $file ])) wp_send_json_error ( 'invalid file' );
$result = file_put_contents ( $allowed [ $file ], sanitize_textarea_field ( $content ));
if ( $result === false ) {
wp_send_json_error ( 'write failed' );
}
// Clear transient caches
delete_transient ( 'itk_bots_list' );
delete_transient ( 'itk_referrers_list' );
delete_transient ( 'itk_networks_list' );
delete_transient ( 'itk_goodbots_list' );
wp_send_json_success ();
}
/* ── Main page render ─────────────────────────────────────── */
public function render_page () : void {
if ( ! current_user_can ( 'manage_options' )) return ;
$tab = sanitize_key ( $_GET [ 'tab' ] ? ? 'dashboard' );
?>
< div class = " wrap itk-wrap " >
< h1 class = " itk-page-title " >
< span class = " itk-logo " > IQ </ span > InformatiQ Toolkit
</ h1 >
< ? php if ( ! empty ( $_GET [ 'cleared' ])) : ?>
< div class = " notice notice-success is-dismissible " >< p > Logs cleared successfully .</ p ></ div >
< ? php endif ; ?>
< ? php if ( ! empty ( $_GET [ 'saved' ])) : ?>
< div class = " notice notice-success is-dismissible " >< p > Settings saved .</ p ></ div >
< ? php endif ; ?>
< nav class = " itk-tabs " >
< ? php
$tabs = [
'dashboard' => 'Dashboard' ,
'bot-blocker' => 'Bot Blocker' ,
'protection' => 'Protection' ,
'optimization' => 'Optimization' ,
'honeypot' => 'Honeypot' ,
'bot-logs' => 'Bot Logs' ,
'honeypot-logs' => 'Honeypot Logs' ,
'config-files' => 'Config Files' ,
];
foreach ( $tabs as $slug => $label ) :
$active = $tab === $slug ? 'itk-tab-active' : '' ;
$url = admin_url ( 'options-general.php?page=' . self :: MENU_SLUG . '&tab=' . $slug );
?>
< a href = " <?= esc_url( $url ) ?> " class = " itk-tab <?= $active ?> " >< ? = esc_html ( $label ) ?> </a>
< ? php endforeach ; ?>
</ nav >
< div class = " itk-tab-content " >
< ? php
match ( $tab ) {
'dashboard' => $this -> tab_dashboard (),
'bot-blocker' => $this -> tab_bot_blocker (),
'protection' => $this -> tab_protection (),
'optimization' => $this -> tab_optimization (),
'honeypot' => $this -> tab_honeypot (),
'bot-logs' => $this -> tab_bot_logs (),
'honeypot-logs' => $this -> tab_honeypot_logs (),
'config-files' => $this -> tab_config_files (),
default => $this -> tab_dashboard (),
};
?>
</ div >
</ div >
< ? php
}
/* ══════════════════════════════════════════════════════════
* TAB : DASHBOARD
* ══════════════════════════════════════════════════════════ */
private function tab_dashboard () : void {
$bot_stats = ITK_Database :: get_bot_stats ();
$hp_stats = ITK_Database :: get_honeypot_stats ();
?>
< div class = " itk-dashboard " >
<!-- ── Bot Activity Panel ── -->
< section class = " itk-monitor-panel " >
< div class = " itk-monitor-header " >
< span class = " itk-monitor-title " >& #9632; BOT ACTIVITY MONITOR</span>
< span class = " itk-monitor-blink " > LIVE </ span >
</ div >
< div class = " itk-stat-row " >
< div class = " itk-stat-card " >
< div class = " itk-stat-num " >< ? = number_format ( $bot_stats [ 'total' ]) ?> </div>
< div class = " itk-stat-lbl " > Total Blocked </ div >
</ div >
< div class = " itk-stat-card " >
< div class = " itk-stat-num itk-green " >< ? = number_format ( $bot_stats [ 'today' ]) ?> </div>
< div class = " itk-stat-lbl " > Today </ div >
</ div >
< div class = " itk-stat-card " >
< div class = " itk-stat-num itk-yellow " >< ? = number_format ( $bot_stats [ 'rate_limited' ]) ?> </div>
< div class = " itk-stat-lbl " > Rate Limited </ div >
</ div >
< div class = " itk-stat-card " >
< div class = " itk-stat-num " >< ? = number_format ( $hp_stats [ 'total' ]) ?> </div>
< div class = " itk-stat-lbl " > Honeypot Catches </ div >
</ div >
< div class = " itk-stat-card " >
< div class = " itk-stat-num itk-green " >< ? = number_format ( $hp_stats [ 'today' ]) ?> </div>
< div class = " itk-stat-lbl " > Honeypot Today </ div >
</ div >
</ div >
<!-- Top bot types bar chart -->
< ? php if ( ! empty ( $bot_stats [ 'top_bot_types' ])) : ?>
< div class = " itk-chart-section " >
< div class = " itk-chart-title " > TOP THREAT SOURCES </ div >
< div class = " itk-bar-chart " >
< ? php
$max = max ( 1 , ( int ) $bot_stats [ 'top_bot_types' ][ 0 ] -> cnt );
foreach ( $bot_stats [ 'top_bot_types' ] as $row ) :
$pct = round (( $row -> cnt / $max ) * 100 );
?>
< div class = " itk-bar-row " >
< span class = " itk-bar-label " >< ? = esc_html ( $row -> bot_type ? : 'Unknown' ) ?> </span>
< div class = " itk-bar-track " >
< div class = " itk-bar-fill " style = " width:<?= $pct ?>% " ></ div >
</ div >
< span class = " itk-bar-count " >< ? = number_format ( $row -> cnt ) ?> </span>
</ div >
< ? php endforeach ; ?>
</ div >
</ div >
< ? php endif ; ?>
<!-- Top honeypot form types -->
< ? php if ( ! empty ( $hp_stats [ 'top_forms' ])) : ?>
< div class = " itk-chart-section " >
< div class = " itk-chart-title " > HONEYPOT – TOP TARGETED FORMS </ div >
< div class = " itk-bar-chart " >
< ? php
$max = max ( 1 , ( int ) $hp_stats [ 'top_forms' ][ 0 ] -> cnt );
foreach ( $hp_stats [ 'top_forms' ] as $row ) :
$pct = round (( $row -> cnt / $max ) * 100 );
?>
< div class = " itk-bar-row " >
< span class = " itk-bar-label " >< ? = esc_html ( $row -> form_type ) ?> </span>
< div class = " itk-bar-track " >
< div class = " itk-bar-fill itk-bar-hp " style = " width:<?= $pct ?>% " ></ div >
</ div >
< span class = " itk-bar-count " >< ? = number_format ( $row -> cnt ) ?> </span>
</ div >
< ? php endforeach ; ?>
</ div >
</ div >
< ? php endif ; ?>
<!-- Top IPs -->
< ? php if ( ! empty ( $bot_stats [ 'top_ips' ])) : ?>
< div class = " itk-chart-section " >
< div class = " itk-chart-title " > TOP OFFENDER IPs </ div >
< table class = " itk-mini-table " >
< tr >< th > IP Address </ th >< th > Hits </ th ></ tr >
< ? php foreach ( $bot_stats [ 'top_ips' ] as $row ) : ?>
< tr >
< td >
< a href = " <?= esc_url(admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=bot-logs&hp_ip=' . urlencode( $row->ip_address ))) ?> " >< ? = esc_html ( $row -> ip_address ) ?> </a>
& nbsp ; < a href = " https://ipinfo.io/<?= urlencode( $row->ip_address ) ?> " target = " _blank " class = " itk-lookup " > [ lookup ] </ a >
</ td >
< td >< ? = number_format ( $row -> cnt ) ?> </td>
</ tr >
< ? php endforeach ; ?>
</ table >
</ div >
< ? php endif ; ?>
</ section >
<!-- ── Quick Settings Status ── -->
< section class = " itk-quick-status " >
< div class = " itk-chart-title " > ACTIVE MODULES </ div >
< ? php
$sec = get_option ( 'itk_security' , []);
$opt = get_option ( 'itk_optimization' , []);
$hp = get_option ( 'itk_honeypot' , []);
$modules = [
'Bot Blocker' => ! empty ( $sec [ 'block_malicious_bots' ]),
'OpenAI Block' => ! empty ( $sec [ 'block_openai_bots' ]),
'Good Bot Rate Limit' => ! empty ( $sec [ 'rate_limit_good_bots' ]),
'Network Block' => ! empty ( $sec [ 'block_bad_networks' ]),
'Login Protection' => ! empty ( $sec [ 'protect_wp_login' ]),
'Security Headers' => ! empty ( $sec [ 'add_security_headers' ]),
'Block XML-RPC' => ! empty ( $sec [ 'block_xmlrpc' ]),
'Honeypot' => ! empty ( $hp [ 'enabled' ]),
'Remove WP Version' => ! empty ( $opt [ 'remove_wp_version' ]),
'Disable Emoji' => ! empty ( $opt [ 'remove_emoji' ]),
'Admin Branding' => ! empty ( $opt [ 'admin_branding' ]),
];
foreach ( $modules as $label => $active ) :
?>
< div class = " itk-module-row " >
< span class = " itk-module-dot <?= $active ? 'itk-dot-on' : 'itk-dot-off' ?> " ></ span >
< span class = " itk-module-label " >< ? = esc_html ( $label ) ?> </span>
< span class = " itk-module-status " >< ? = $active ? 'ACTIVE' : 'off' ?> </span>
</ div >
< ? php endforeach ; ?>
</ section >
</ div >
< ? php
}
/* ══════════════════════════════════════════════════════════
* TAB : BOT BLOCKER
* ══════════════════════════════════════════════════════════ */
private function tab_bot_blocker () : void {
$opts = get_option ( 'itk_security' , []);
?>
< div class = " itk-settings-grid " >
< section class = " itk-card " >
< h2 > Bot Blocking </ h2 >
< ? php
$toggles = [
'block_openai_bots' => [ 'OpenAI / GPT Bots' , 'Block GPTBot, ChatGPT-User, OAI-SearchBot' ],
'block_malicious_bots' => [ 'Malicious Bots' , 'Block bots listed in badbots.conf' ],
'block_bad_referrers' => [ 'Bad Referrers' , 'Block requests from spam referrer domains' ],
'block_bad_networks' => [ 'Bad Networks' , 'Block IP ranges listed in networks.conf' ],
'rate_limit_good_bots' => [ 'Rate-Limit Good Bots' , 'Apply crawl-rate limits to Googlebot, Bingbot, etc. (configurable in goodbots.conf)' ],
];
foreach ( $toggles as $key => [ $label , $desc ]) :
$this -> render_toggle ( 'itk_security' , $key , $label , $desc , $opts );
endforeach ;
?>
</ section >
< section class = " itk-card " >
< h2 > Response Settings </ h2 >
< form method = " post " action = " options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker " >
< ? php wp_nonce_field ( self :: NONCE_ACTION ); ?>
< input type = " hidden " name = " itk_action " value = " save_settings_security " >
< table class = " form-table " >
< tr >
< th > Response Code </ th >
< td >
< select name = " itk_security[response_code] " >
< ? php
$codes = [ '301_custom' => '301 – Redirect to custom URL' , '403' => '403 Forbidden' , '410' => '410 Gone' , '503' => '503 Service Unavailable' ];
$cur = $opts [ 'response_code' ] ? ? '301_custom' ;
foreach ( $codes as $val => $lbl ) :
?>
< option value = " <?= esc_attr( $val ) ?> " < ? = selected ( $cur , $val , false ) ?> ><?= esc_html($lbl) ?></option>
< ? php endforeach ; ?>
</ select >
</ td >
</ tr >
< tr >
< th > Redirect URL </ th >
< td >< input type = " url " name = " itk_security[redirect_url] " value = " <?= esc_attr( $opts['redirect_url'] ?? '') ?> " class = " regular-text " ></ td >
</ tr >
< tr >
< th > Custom Message </ th >
< td >< input type = " text " name = " itk_security[custom_message] " value = " <?= esc_attr( $opts['custom_message'] ?? 'Access denied.') ?> " class = " regular-text " ></ td >
</ tr >
< tr >
< th > Log Blocked Attempts </ th >
< td >
< label >
< input type = " checkbox " name = " itk_security[log_blocked_attempts] " value = " 1 " < ? = checked ( ! empty ( $opts [ 'log_blocked_attempts' ])) ?> >
Log all blocked attempts to the database
</ label >
</ td >
</ tr >
</ table >
< ? php submit_button ( 'Save Response Settings' ); ?>
</ form >
</ section >
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 >
feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:
- Fixed deactivation bug: all protection methods now guard themselves
with their own option check so toggling off via AJAX takes effect
immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
configurable per-bot limits in goodbots.conf (BotName|req/min);
returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
blocked, today's count, rate-limited hits, top threat sources
(bar chart), top IPs, top honeypot form types, active-module
status panel.
- All optimizations from utils.php merged into Optimization tab as
toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
networks.conf, allowed-ips.conf with AJAX save and transient flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00
</ div >
< ? php
}
/* ══════════════════════════════════════════════════════════
* TAB : PROTECTION
* ══════════════════════════════════════════════════════════ */
private function tab_protection () : void {
$opts = get_option ( 'itk_security' , []);
?>
< div class = " itk-settings-grid " >
< section class = " itk-card " >
< h2 > WordPress Protection </ h2 >
< ? php
$toggles = [
'protect_wp_login' => [ 'WP Login IP Whitelist' , 'Restrict wp-login.php to IPs in allowed-ips.conf' ],
'add_security_headers' => [ 'Security Headers' , 'Add X-Content-Type-Options, X-Frame-Options, X-XSS-Protection headers' ],
'protect_wp_includes' => [ 'Protect WP Core Files' , 'Block direct access to wp-includes, wp-admin/includes' ],
'protect_uploads' => [ 'Block PHP in Uploads' , 'Deny PHP file access and uploads in /wp-content/uploads/' ],
'block_xmlrpc' => [ 'Block XML-RPC' , 'Deny all access to xmlrpc.php' ],
'block_malicious_queries' => [ 'Block Malicious Queries' , 'Detect and block SQLi, XSS, and command injection in query strings' ],
'block_author_scans' => [ 'Block Author Scans' , 'Redirect ?author=N requests to prevent username enumeration' ],
];
foreach ( $toggles as $key => [ $label , $desc ]) :
$this -> render_toggle ( 'itk_security' , $key , $label , $desc , $opts );
endforeach ;
?>
</ section >
< section class = " itk-card " >
< h2 > Custom Login URL </ h2 >
< form method = " post " action = " options-general.php?page=<?= self::MENU_SLUG ?>&tab=protection " >
< ? php wp_nonce_field ( self :: NONCE_ACTION ); ?>
< input type = " hidden " name = " itk_action " value = " save_settings_login " >
< ? php $this -> render_toggle ( 'itk_security' , 'enable_custom_login' , 'Enable Custom Login URL' , 'Replace /wp-login.php with a custom slug' , $opts ); ?>
< table class = " form-table " >
< tr >
< th > Login Slug </ th >
< td >
< code >< ? = esc_html ( home_url ( '/' )) ?> </code>
< input type = " text " name = " itk_security[custom_login_slug] " value = " <?= esc_attr( $opts['custom_login_slug'] ?? 'thoushallpass') ?> " style = " width:200px " >
< p class = " description " > Characters : letters , numbers , dashes only .</ p >
</ td >
</ tr >
</ table >
< ? php submit_button ( 'Save Login Settings' ); ?>
</ form >
</ section >
</ div >
< ? php
}
/* ══════════════════════════════════════════════════════════
* TAB : OPTIMIZATION
* ══════════════════════════════════════════════════════════ */
private function tab_optimization () : void {
$opts = get_option ( 'itk_optimization' , []);
?>
< div class = " itk-settings-grid " >
< section class = " itk-card " >
< h2 > Performance & amp ; Cleanup </ h2 >
< ? php
$toggles = [
'remove_wp_version' => [ 'Remove WP Version' , 'Strip WordPress version from <head> and all enqueued assets' ],
'remove_script_versions' => [ 'Remove Asset Versions' , 'Remove ?ver= query string from CSS/JS URLs' ],
'remove_emoji' => [ 'Remove Emojis' , 'Disable WordPress emoji scripts and styles' ],
'deregister_wp_embed' => [ 'Remove WP Embed' , 'Deregister the wp-embed script' ],
'remove_wp_head_noise' => [ 'Clean WP Head' , 'Remove RSD, wlwmanifest, feed links, and adjacent post links from <head>' ],
'limit_revisions' => [ 'Limit Revisions' , 'Keep only 3 post revisions and autosave every 5 minutes' ],
'defer_js' => [ 'Defer JavaScript' , 'Add defer attribute to non-critical scripts' ],
'limit_heartbeat' => [ 'Limit Heartbeat' , 'Restrict WordPress heartbeat to post editor pages only' ],
'stop_empty_search_redirect' => [ 'Fix Empty Search' , 'Prevent redirect loop on empty search queries' ],
'use_google_jquery' => [ 'Use Google jQuery' , 'Load jQuery from Google CDN instead of local' ],
'dns_prefetch' => [ 'DNS Prefetch' , 'Enable DNS prefetching via meta header' ],
];
foreach ( $toggles as $key => [ $label , $desc ]) :
$this -> render_toggle ( 'itk_optimization' , $key , $label , $desc , $opts );
endforeach ;
?>
</ section >
< section class = " itk-card " >
< h2 > Security Tweaks </ h2 >
< ? php
$toggles = [
'hide_login_errors' => [ 'Hide Login Errors' , 'Replace specific login error messages with a generic one' ],
'remove_author_class' => [ 'Remove Author Class' , 'Strip admin username from comment CSS classes' ],
'remove_default_userfields' => [ 'Remove User Fields' , 'Remove AIM, Jabber, YIM from user profiles' ],
'clean_bad_content' => [ 'Clean Bad Content' , 'Remove empty tags, inline styles, and font tags on save' ],
'change_author_base' => [ 'Change Author URL Base' , " Change /author/ to /writer/ in author archive URLs " ],
'disable_xml_rpc' => [ 'Disable XML-RPC (via filter)' , 'Filter-based XML-RPC disable (additional to the blocker)' ],
];
foreach ( $toggles as $key => [ $label , $desc ]) :
$this -> render_toggle ( 'itk_optimization' , $key , $label , $desc , $opts );
endforeach ;
?>
</ section >
< section class = " itk-card " >
< h2 > UI / Branding </ h2 >
< ? php
$toggles = [
'disable_dashboard_widgets' => [ 'Disable Core Widgets' , 'Remove WordPress News and WPEngine news dashboard widgets' ],
'unregister_default_widgets' => [ 'Unregister Sidebar Widgets' , 'Remove Calendar, Archives, Meta, Search, Tag Cloud widgets' ],
'disable_comments_url' => [ 'Remove Comment URL Field' , 'Hide the website URL field from comment forms' ],
'remove_admin_bar_links' => [ 'Clean Admin Bar' , 'Remove WordPress logo, links, and noisy items from the toolbar' ],
'admin_branding' => [ 'InformatiQ Branding' , 'Add InformatiQ logo widget, toolbar link, admin notice, and custom footer' ],
'disable_floc' => [ 'Disable FLoC' , 'Add Permissions-Policy: interest-cohort=() header' ],
'lightbox_images' => [ 'Lightbox Images' , 'Add rel="lightbox" to image links in post content' ],
'featured_image_rss' => [ 'Featured Image in RSS' , 'Include featured image in RSS feed entries' ],
];
foreach ( $toggles as $key => [ $label , $desc ]) :
$this -> render_toggle ( 'itk_optimization' , $key , $label , $desc , $opts );
endforeach ;
?>
</ section >
</ div >
< ? php
}
/* ══════════════════════════════════════════════════════════
* TAB : HONEYPOT
* ══════════════════════════════════════════════════════════ */
private function tab_honeypot () : void {
$opts = get_option ( 'itk_honeypot' , []);
?>
< div class = " itk-settings-grid " >
< section class = " itk-card " >
< h2 > Honeypot Fields </ h2 >
< ? php
$toggles = [
'enabled' => [ 'Enable Honeypot' , 'Inject invisible honeypot fields into all protected forms' ],
'protect_comments' => [ 'Comments' , 'Protect comment forms' ],
'protect_login' => [ 'Login Form' , 'Protect wp-login.php login' ],
'protect_register' => [ 'Registration' , 'Protect user registration' ],
'protect_lost_password' => [ 'Lost Password' , 'Protect lost password form' ],
'protect_woocommerce' => [ 'WooCommerce' , 'Protect WooCommerce checkout and registration' ],
'protect_cf7' => [ 'Contact Form 7' , 'Protect CF7 forms' ],
'protect_elementor' => [ 'Elementor Forms' , 'Protect Elementor Pro form widget' ],
'protect_gravity' => [ 'Gravity Forms' , 'Protect Gravity Forms' ],
'protect_search' => [ 'Search Form' , 'Protect the WordPress search form' ],
];
foreach ( $toggles as $key => [ $label , $desc ]) :
$this -> render_toggle ( 'itk_honeypot' , $key , $label , $desc , $opts );
endforeach ;
?>
</ section >
< section class = " itk-card " >
< h2 > Timing Rules </ h2 >
< form method = " post " action = " options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot " >
< ? php wp_nonce_field ( self :: NONCE_ACTION ); ?>
< input type = " hidden " name = " itk_action " value = " save_settings_honeypot " >
< table class = " form-table " >
< tr >
< th > Minimum Submit Time ( seconds ) </ th >
< td >
< input type = " number " name = " itk_honeypot[min_time] " value = " <?= (int)( $opts['min_time'] ?? 3) ?> " min = " 1 " max = " 60 " >
< p class = " description " > Block submissions faster than this ( bots submit instantly ) .</ p >
</ td >
</ tr >
< tr >
< th > Maximum Submit Time ( seconds ) </ th >
< td >
< input type = " number " name = " itk_honeypot[max_time] " value = " <?= (int)( $opts['max_time'] ?? 7200) ?> " min = " 60 " >
< p class = " description " > Block submissions older than this ( stale / replayed forms ) .</ p >
</ td >
</ tr >
< tr >
< th > Retain Logs ( days ) </ th >
< td >
< input type = " number " name = " itk_honeypot[retain_days] " value = " <?= (int)( $opts['retain_days'] ?? 90) ?> " min = " 1 " >
< p class = " description " > Automatically prune logs older than this many days .</ p >
</ td >
</ tr >
</ table >
< ? php submit_button ( 'Save Honeypot Settings' ); ?>
</ form >
</ section >
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 >
feat: initial InformatiQ Toolkit plugin
Merges informatiq-wp-secure + informatiq-utils + HoneypotFields into
a single unified plugin with the following improvements:
- Fixed deactivation bug: all protection methods now guard themselves
with their own option check so toggling off via AJAX takes effect
immediately without any hook re-registration.
- Added rate-limiting for good/legitimate bots (Googlebot, Bingbot,
DuckDuckBot, Yandex, etc.) via transient sliding-window counters;
configurable per-bot limits in goodbots.conf (BotName|req/min);
returns HTTP 429 with Retry-After: 60 when over limit.
- Unified MySQL-backed logging (itk_bot_log + itk_honeypot_log tables)
replaces the old wp_options-based 100-entry cap.
- New Dashboard tab with terminal-style bot activity monitor: total
blocked, today's count, rate-limited hits, top threat sources
(bar chart), top IPs, top honeypot form types, active-module
status panel.
- All optimizations from utils.php merged into Optimization tab as
toggleable settings (was always-on before).
- Single admin page (Settings → InformatiQ Toolkit) with 8 tabs:
Dashboard | Bot Blocker | Protection | Optimization | Honeypot |
Bot Logs | Honeypot Logs | Config Files.
- Config file editor for badbots.conf, goodbots.conf, referrers.conf,
networks.conf, allowed-ips.conf with AJAX save and transient flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:45:26 +02:00
</ 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>' ;
}
}