feat: add Central API clients, bot rate limiting, and admin API UI

- Add ITK_HP_API and ITK_Bot_API static classes with queue/flush/cron
- Add WP-Cron (5 min) + shutdown flush for both API queues
- Bot Blocker and Honeypot now queue events to their respective APIs
- Admin: Bot Blocker tab gains Central Bot API settings panel
  (enable, URL, token, test connection, flush queue, historical sync)
- Admin: Honeypot tab gains Central Honeypot API settings panel
- Admin JS: AJAX handlers for Test Connection and Flush Now buttons
- Admin CSS: API card styles (status badge, notices, footer controls)
- Add .gitignore (excludes bot-api/ which lives in CloudHost/bot-api)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 18:32:27 +02:00
parent 6d4349ff7b
commit a8d7972ad7
9 changed files with 906 additions and 8 deletions

View File

@@ -17,8 +17,10 @@ class ITK_Admin {
add_action('admin_menu', [$this, 'add_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_init', [$this, 'handle_actions']);
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
add_action('wp_ajax_itk_save_config_file',[$this, 'ajax_save_config_file']);
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
add_action('wp_ajax_itk_save_config_file', [$this, 'ajax_save_config_file']);
add_action('wp_ajax_itk_test_api', [$this, 'ajax_test_api']);
add_action('wp_ajax_itk_flush_api_queue', [$this, 'ajax_flush_api_queue']);
}
public function add_menu(): void {
@@ -65,6 +67,60 @@ class ITK_Admin {
]);
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
break;
case 'save_bot_api':
$this->save_api_settings(ITK_Bot_API::OPT_SETTINGS, 'itk_bot_api_settings');
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
break;
case 'save_hp_api':
$this->save_api_settings(ITK_HP_API::OPT_SETTINGS, 'itk_hp_api_settings');
$this->redirect(['tab' => 'honeypot', 'saved' => 1]);
break;
case 'test_bot_api':
$result = ITK_Bot_API::test_connection();
$s = ITK_Bot_API::settings();
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
$s['last_error'] = $result['ok'] ? '' : $result['message'];
update_option(ITK_Bot_API::OPT_SETTINGS, $s);
set_transient('itk_bot_api_test_result', $result, 60);
$this->redirect(['tab' => 'bot-blocker', 'api_tested' => 1]);
break;
case 'test_hp_api':
$result = ITK_HP_API::test_connection();
$s = ITK_HP_API::settings();
$s['connection_ok'] = $result['ok']; $s['last_verified'] = time();
$s['last_error'] = $result['ok'] ? '' : $result['message'];
update_option(ITK_HP_API::OPT_SETTINGS, $s);
set_transient('itk_hp_api_test_result', $result, 60);
$this->redirect(['tab' => 'honeypot', 'api_tested' => 1]);
break;
case 'flush_bot_api':
ITK_Bot_API::flush();
$this->redirect(['tab' => 'bot-blocker', 'api_flushed' => 1]);
break;
case 'flush_hp_api':
ITK_HP_API::flush();
$this->redirect(['tab' => 'honeypot', 'api_flushed' => 1]);
break;
case 'send_bot_history':
$result = ITK_Bot_API::send_history_batch();
set_transient('itk_bot_history_result', $result, 60);
$this->redirect(['tab' => 'bot-blocker', 'history_sent' => 1]);
break;
case 'send_hp_history':
$result = ITK_HP_API::send_history_batch();
set_transient('itk_hp_history_result', $result, 60);
$this->redirect(['tab' => 'honeypot', 'history_sent' => 1]);
break;
case 'reset_bot_history':
delete_option('itk_bot_history_last_id');
delete_option('itk_bot_history_sent');
$this->redirect(['tab' => 'bot-blocker']);
break;
case 'reset_hp_history':
delete_option('itk_hp_history_last_id');
delete_option('itk_hp_history_sent');
$this->redirect(['tab' => 'honeypot']);
break;
case 'save_settings_login':
$this->save_settings_form('itk_security', [
'custom_login_slug',
@@ -102,6 +158,41 @@ class ITK_Admin {
update_option($option, $opts);
}
private function save_api_settings(string $option_key, string $post_key): void {
$cur = get_option($option_key, []);
$posted = $_POST[$post_key] ?? [];
$new_url = esc_url_raw(trim($posted['api_url'] ?? ''));
$changed = $new_url !== ($cur['api_url'] ?? '') || (!empty($posted['api_token']) && $posted['api_token'] !== ($cur['api_token'] ?? ''));
update_option($option_key, array_merge($cur, [
'enabled' => !empty($posted['enabled']),
'api_url' => $new_url,
'api_token' => sanitize_text_field($posted['api_token'] ?? ($cur['api_token'] ?? '')),
'connection_ok' => $changed ? null : ($cur['connection_ok'] ?? null),
'last_verified' => $changed ? 0 : ($cur['last_verified'] ?? 0),
'last_error' => $changed ? '' : ($cur['last_error'] ?? ''),
]));
}
/* ── AJAX: test API connection ─────────────────────────────── */
public function ajax_test_api(): void {
check_ajax_referer(self::NONCE_ACTION, 'nonce');
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
$which = sanitize_key($_POST['api'] ?? '');
$result = $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.');
}
private function redirect(array $args): void {
wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php')));
exit;
@@ -423,6 +514,98 @@ class ITK_Admin {
<?php submit_button('Save Response Settings'); ?>
</form>
</section>
<?php
/* ── Central Bot API card ─────────────────────── */
$bot_api = ITK_Bot_API::settings();
$bot_queue = count((array) get_option(ITK_Bot_API::OPT_QUEUE, []));
$bot_total = ITK_Database::count_bot_rows();
$bot_sent = (int) get_option('itk_bot_history_sent', 0);
$bot_rem = max(0, $bot_total - $bot_sent);
$bot_ok = $bot_api['connection_ok'];
$bot_cls = is_null($bot_ok) ? 'itk-api-unknown' : ($bot_ok ? 'itk-api-ok' : 'itk-api-err');
$bot_lbl = is_null($bot_ok) ? 'Not tested' : ($bot_ok ? 'Connected' : 'Connection failed');
$bot_test_r = get_transient('itk_bot_api_test_result'); if ($bot_test_r) delete_transient('itk_bot_api_test_result');
$bot_hist_r = get_transient('itk_bot_history_result'); if ($bot_hist_r) delete_transient('itk_bot_history_result');
?>
<section class="itk-card itk-api-card">
<h2>Central Bot API</h2>
<p class="description itk-api-desc">Send blocked-bot events to your self-hosted Bot Intelligence Docker stack (port 3001).</p>
<div class="itk-api-status-bar">
<span class="itk-api-badge <?= esc_attr($bot_cls) ?>"><?= esc_html($bot_lbl) ?></span>
<?php if ($bot_api['last_verified'] > 0): ?>
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$bot_api['last_verified'])) ?> ago</span>
<?php endif; ?>
<?php if (!$bot_ok && !is_null($bot_ok) && !empty($bot_api['last_error'])): ?>
<span class="itk-api-err-msg"><?= esc_html($bot_api['last_error']) ?></span>
<?php endif; ?>
</div>
<?php if ($bot_test_r): ?>
<div class="itk-api-notice <?= $bot_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($bot_test_r['message']) ?></div>
<?php endif; ?>
<?php if ($bot_hist_r): ?>
<div class="itk-api-notice <?= $bot_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($bot_hist_r['message']) ?></div>
<?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_bot_api">
<table class="form-table itk-api-table">
<tr>
<th>Enable</th>
<td><label><input type="checkbox" name="itk_bot_api_settings[enabled]" value="1" <?= checked(!empty($bot_api['enabled'])) ?>> Send events to Central API</label></td>
</tr>
<tr>
<th>API URL</th>
<td>
<input type="url" name="itk_bot_api_settings[api_url]" value="<?= esc_attr($bot_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3001">
<p class="description">Base URL of your Bot API stack (e.g. <code>http://localhost:3001</code>)</p>
</td>
</tr>
<tr>
<th>API Token</th>
<td>
<input type="password" name="itk_bot_api_settings[api_token]" value="" class="regular-text"
placeholder="<?= !empty($bot_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
autocomplete="new-password">
</td>
</tr>
</table>
<div class="itk-api-form-actions">
<?php submit_button('Save Bot API Settings', 'primary', 'submit', false); ?>
<button type="button" class="button itk-btn-test-api" data-api="bot" style="margin-left:8px">Test Connection</button>
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
</div>
</form>
<div class="itk-api-footer">
<div class="itk-api-queue-row">
<strong><?= (int) $bot_queue ?></strong> event(s) pending in queue
<button type="button" class="button button-small itk-btn-flush-api" data-api="bot" style="margin-left:8px">Flush Now</button>
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
</div>
<div class="itk-api-history-row">
<strong>Historical sync:</strong>
<?= number_format($bot_sent) ?> / <?= number_format($bot_total) ?> records sent
<?php if ($bot_rem > 0): ?><em class="itk-api-rem">(<?= number_format($bot_rem) ?> remaining)</em><?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker" style="display:inline;margin-left:10px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="send_bot_history">
<input type="submit" class="button button-small" value="Send Next 50">
</form>
<?php if ($bot_sent > 0): ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="reset_bot_history">
<input type="submit" class="button button-small" value="Reset Progress">
</form>
<?php endif; ?>
</div>
</div>
</section>
</div>
<?php
}
@@ -607,6 +790,98 @@ class ITK_Admin {
<?php submit_button('Save Honeypot Settings'); ?>
</form>
</section>
<?php
/* ── Central Honeypot API card ────────────────── */
$hp_api = ITK_HP_API::settings();
$hp_queue = count((array) get_option(ITK_HP_API::OPT_QUEUE, []));
$hp_total = ITK_Database::count_honeypot_rows();
$hp_sent = (int) get_option('itk_hp_history_sent', 0);
$hp_rem = max(0, $hp_total - $hp_sent);
$hp_ok = $hp_api['connection_ok'];
$hp_cls = is_null($hp_ok) ? 'itk-api-unknown' : ($hp_ok ? 'itk-api-ok' : 'itk-api-err');
$hp_lbl = is_null($hp_ok) ? 'Not tested' : ($hp_ok ? 'Connected' : 'Connection failed');
$hp_test_r = get_transient('itk_hp_api_test_result'); if ($hp_test_r) delete_transient('itk_hp_api_test_result');
$hp_hist_r = get_transient('itk_hp_history_result'); if ($hp_hist_r) delete_transient('itk_hp_history_result');
?>
<section class="itk-card itk-api-card">
<h2>Central Honeypot API</h2>
<p class="description itk-api-desc">Send honeypot catch events to your self-hosted Honeypot Intelligence Docker stack (port 3000).</p>
<div class="itk-api-status-bar">
<span class="itk-api-badge <?= esc_attr($hp_cls) ?>"><?= esc_html($hp_lbl) ?></span>
<?php if ($hp_api['last_verified'] > 0): ?>
<span class="itk-api-time">Last tested <?= esc_html(human_time_diff((int)$hp_api['last_verified'])) ?> ago</span>
<?php endif; ?>
<?php if (!$hp_ok && !is_null($hp_ok) && !empty($hp_api['last_error'])): ?>
<span class="itk-api-err-msg"><?= esc_html($hp_api['last_error']) ?></span>
<?php endif; ?>
</div>
<?php if ($hp_test_r): ?>
<div class="itk-api-notice <?= $hp_test_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($hp_test_r['message']) ?></div>
<?php endif; ?>
<?php if ($hp_hist_r): ?>
<div class="itk-api-notice <?= $hp_hist_r['ok'] ? 'itk-api-notice-ok' : 'itk-api-notice-err' ?>"><?= esc_html($hp_hist_r['message']) ?></div>
<?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="save_hp_api">
<table class="form-table itk-api-table">
<tr>
<th>Enable</th>
<td><label><input type="checkbox" name="itk_hp_api_settings[enabled]" value="1" <?= checked(!empty($hp_api['enabled'])) ?>> Send events to Central API</label></td>
</tr>
<tr>
<th>API URL</th>
<td>
<input type="url" name="itk_hp_api_settings[api_url]" value="<?= esc_attr($hp_api['api_url'] ?? '') ?>" class="regular-text" placeholder="http://your-server:3000">
<p class="description">Base URL of your Honeypot API stack (e.g. <code>http://localhost:3000</code>)</p>
</td>
</tr>
<tr>
<th>API Token</th>
<td>
<input type="password" name="itk_hp_api_settings[api_token]" value="" class="regular-text"
placeholder="<?= !empty($hp_api['api_token']) ? '●●●●●●●● (set — leave blank to keep)' : 'Enter bearer token' ?>"
autocomplete="new-password">
</td>
</tr>
</table>
<div class="itk-api-form-actions">
<?php submit_button('Save Honeypot API Settings', 'primary', 'submit', false); ?>
<button type="button" class="button itk-btn-test-api" data-api="hp" style="margin-left:8px">Test Connection</button>
<span class="itk-api-ajax-result" style="margin-left:10px;display:none"></span>
</div>
</form>
<div class="itk-api-footer">
<div class="itk-api-queue-row">
<strong><?= (int) $hp_queue ?></strong> event(s) pending in queue
<button type="button" class="button button-small itk-btn-flush-api" data-api="hp" style="margin-left:8px">Flush Now</button>
<span class="itk-api-flush-result" style="margin-left:8px;display:none"></span>
</div>
<div class="itk-api-history-row">
<strong>Historical sync:</strong>
<?= number_format($hp_sent) ?> / <?= number_format($hp_total) ?> records sent
<?php if ($hp_rem > 0): ?><em class="itk-api-rem">(<?= number_format($hp_rem) ?> remaining)</em><?php endif; ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="display:inline;margin-left:10px">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="send_hp_history">
<input type="submit" class="button button-small" value="Send Next 50">
</form>
<?php if ($hp_sent > 0): ?>
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot" style="display:inline;margin-left:6px" onsubmit="return confirm('Reset sync progress? No data is deleted.')">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<input type="hidden" name="itk_action" value="reset_hp_history">
<input type="submit" class="button button-small" value="Reset Progress">
</form>
<?php endif; ?>
</div>
</div>
</section>
</div>
<?php
}