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:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bot-api/
|
||||||
@@ -308,3 +308,60 @@ input:checked + .itk-slider:before { transform: translateX(20px); }
|
|||||||
.itk-toggle-saving { opacity: .6; pointer-events: none; }
|
.itk-toggle-saving { opacity: .6; pointer-events: none; }
|
||||||
.itk-toggle-saved { color: #00a32a; font-size: 11px; }
|
.itk-toggle-saved { color: #00a32a; font-size: 11px; }
|
||||||
.itk-toggle-error { color: #b32d2e; font-size: 11px; }
|
.itk-toggle-error { color: #b32d2e; font-size: 11px; }
|
||||||
|
|
||||||
|
/* ── Central API card ─────────────────────────────────────── */
|
||||||
|
.itk-api-card { grid-column: 1 / -1; }
|
||||||
|
.itk-api-desc { margin: -8px 0 14px; color: #646970; }
|
||||||
|
|
||||||
|
.itk-api-status-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.itk-api-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.itk-api-ok { background: #d7f7e0; color: #1a7a3a; border: 1px solid #9de0b4; }
|
||||||
|
.itk-api-err { background: #ffecec; color: #b32d2e; border: 1px solid #f7c5c5; }
|
||||||
|
.itk-api-unknown { background: #f0f0f1; color: #646970; border: 1px solid #c3c4c7; }
|
||||||
|
.itk-api-time { font-size: 11px; color: #646970; }
|
||||||
|
.itk-api-err-msg { font-size: 11px; color: #b32d2e; font-style: italic; }
|
||||||
|
|
||||||
|
.itk-api-notice {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.itk-api-notice-ok { background: #d7f7e0; border: 1px solid #9de0b4; color: #1a7a3a; }
|
||||||
|
.itk-api-notice-err { background: #ffecec; border: 1px solid #f7c5c5; color: #b32d2e; }
|
||||||
|
|
||||||
|
.itk-api-table { margin-top: 0 !important; }
|
||||||
|
.itk-api-table th { width: 130px; }
|
||||||
|
|
||||||
|
.itk-api-form-actions { margin-top: 4px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||||
|
|
||||||
|
.itk-api-footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.itk-api-queue-row,
|
||||||
|
.itk-api-history-row { font-size: 13px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.itk-api-rem { color: #646970; font-size: 12px; }
|
||||||
|
|||||||
@@ -43,6 +43,64 @@
|
|||||||
setTimeout(function () { $fb.fadeOut(400, function () { $(this).remove(); }); }, 2000);
|
setTimeout(function () { $fb.fadeOut(400, function () { $(this).remove(); }); }, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── API: Test connection ─────────────────────────────────── */
|
||||||
|
$(document).on('click', '.itk-btn-test-api', function () {
|
||||||
|
var $btn = $(this);
|
||||||
|
var api = $btn.data('api');
|
||||||
|
var $result = $btn.closest('form').find('.itk-api-ajax-result');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('Testing…');
|
||||||
|
$result.hide();
|
||||||
|
|
||||||
|
$.post(itkAdmin.ajaxUrl, {
|
||||||
|
action: 'itk_test_api',
|
||||||
|
nonce: itkAdmin.nonce,
|
||||||
|
api: api
|
||||||
|
})
|
||||||
|
.done(function (res) {
|
||||||
|
var ok = res && res.ok;
|
||||||
|
var msg = (res && res.message) ? res.message : (ok ? 'Connected.' : 'Test failed.');
|
||||||
|
$result.text(msg).css('color', ok ? '#00a32a' : '#b32d2e').show();
|
||||||
|
setTimeout(function () { $result.fadeOut(); }, 5000);
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
$result.text('Request failed.').css('color', '#b32d2e').show();
|
||||||
|
setTimeout(function () { $result.fadeOut(); }, 4000);
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
$btn.prop('disabled', false).text('Test Connection');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── API: Flush queue ─────────────────────────────────────── */
|
||||||
|
$(document).on('click', '.itk-btn-flush-api', function () {
|
||||||
|
var $btn = $(this);
|
||||||
|
var api = $btn.data('api');
|
||||||
|
var $result = $btn.siblings('.itk-api-flush-result');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('Flushing…');
|
||||||
|
$result.hide();
|
||||||
|
|
||||||
|
$.post(itkAdmin.ajaxUrl, {
|
||||||
|
action: 'itk_flush_api_queue',
|
||||||
|
nonce: itkAdmin.nonce,
|
||||||
|
api: api
|
||||||
|
})
|
||||||
|
.done(function (res) {
|
||||||
|
var ok = res && res.success;
|
||||||
|
var msg = ok ? 'Queue flushed.' : 'Flush failed.';
|
||||||
|
$result.text(msg).css('color', ok ? '#00a32a' : '#b32d2e').show();
|
||||||
|
setTimeout(function () { $result.fadeOut(); }, 3000);
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
$result.text('Request failed.').css('color', '#b32d2e').show();
|
||||||
|
setTimeout(function () { $result.fadeOut(); }, 3000);
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
$btn.prop('disabled', false).text('Flush Now');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* ── Config file editor (AJAX) ────────────────────────────── */
|
/* ── Config file editor (AJAX) ────────────────────────────── */
|
||||||
$('#itk-save-config').on('click', function (e) {
|
$('#itk-save-config').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ class ITK_Admin {
|
|||||||
add_action('admin_menu', [$this, 'add_menu']);
|
add_action('admin_menu', [$this, 'add_menu']);
|
||||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||||
add_action('admin_init', [$this, 'handle_actions']);
|
add_action('admin_init', [$this, 'handle_actions']);
|
||||||
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
|
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_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 {
|
public function add_menu(): void {
|
||||||
@@ -65,6 +67,60 @@ class ITK_Admin {
|
|||||||
]);
|
]);
|
||||||
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
|
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
|
||||||
break;
|
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':
|
case 'save_settings_login':
|
||||||
$this->save_settings_form('itk_security', [
|
$this->save_settings_form('itk_security', [
|
||||||
'custom_login_slug',
|
'custom_login_slug',
|
||||||
@@ -102,6 +158,41 @@ class ITK_Admin {
|
|||||||
update_option($option, $opts);
|
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 {
|
private function redirect(array $args): void {
|
||||||
wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php')));
|
wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php')));
|
||||||
exit;
|
exit;
|
||||||
@@ -423,6 +514,98 @@ class ITK_Admin {
|
|||||||
<?php submit_button('Save Response Settings'); ?>
|
<?php submit_button('Save Response Settings'); ?>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -607,6 +790,98 @@ class ITK_Admin {
|
|||||||
<?php submit_button('Save Honeypot Settings'); ?>
|
<?php submit_button('Save Honeypot Settings'); ?>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|||||||
242
includes/class-itk-bot-api.php
Normal file
242
includes/class-itk-bot-api.php
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Bot Central API Client
|
||||||
|
*
|
||||||
|
* Queues bot-block events locally and batch-submits them to the
|
||||||
|
* central Bot Intelligence API Docker stack.
|
||||||
|
*
|
||||||
|
* Mirrors ITK_HP_API but uses the bot-api endpoint and schema:
|
||||||
|
* { site_hash, bots: [ { ip, bot_type, action, reason, user_agent, request_uri, logged_at } ] }
|
||||||
|
*/
|
||||||
|
class ITK_Bot_API {
|
||||||
|
|
||||||
|
const OPT_SETTINGS = 'itk_bot_api_settings';
|
||||||
|
const OPT_QUEUE = 'itk_bot_api_queue';
|
||||||
|
const CRON_HOOK = 'itk_bot_api_flush';
|
||||||
|
const QUEUE_MAX = 500;
|
||||||
|
const BATCH_SIZE = 50;
|
||||||
|
|
||||||
|
/* ── Bootstrap ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function register_cron(): void {
|
||||||
|
if (!wp_next_scheduled(self::CRON_HOOK)) {
|
||||||
|
wp_schedule_event(time(), 'itk_5min', self::CRON_HOOK);
|
||||||
|
}
|
||||||
|
add_action(self::CRON_HOOK, [self::class, 'flush']);
|
||||||
|
add_action('shutdown', [self::class, 'flush_shutdown']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clear_cron(): void {
|
||||||
|
wp_clear_scheduled_hook(self::CRON_HOOK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Settings ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function defaults(): array {
|
||||||
|
return [
|
||||||
|
'enabled' => false,
|
||||||
|
'api_url' => '',
|
||||||
|
'api_token' => '',
|
||||||
|
'last_sync' => 0,
|
||||||
|
'sent_total' => 0,
|
||||||
|
'connection_ok' => null,
|
||||||
|
'last_verified' => 0,
|
||||||
|
'last_error' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function settings(): array {
|
||||||
|
return array_merge(self::defaults(), (array)get_option(self::OPT_SETTINGS, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function token(): string {
|
||||||
|
if (defined('ITK_BOT_API_TOKEN') && ITK_BOT_API_TOKEN !== '') {
|
||||||
|
return (string)ITK_BOT_API_TOKEN;
|
||||||
|
}
|
||||||
|
return self::settings()['api_token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Queue ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue one bot event.
|
||||||
|
* Called from ITK_Bot_Blocker after logging to the local DB.
|
||||||
|
*/
|
||||||
|
public static function queue(array $data): void {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['enabled']) || empty($s['api_url'])) return;
|
||||||
|
|
||||||
|
$queue = (array)get_option(self::OPT_QUEUE, []);
|
||||||
|
if (count($queue) >= self::QUEUE_MAX) array_shift($queue);
|
||||||
|
|
||||||
|
$queue[] = [
|
||||||
|
'ip' => sanitize_text_field($data['ip'] ?? ''),
|
||||||
|
'bot_type' => sanitize_text_field($data['bot_type'] ?? ''),
|
||||||
|
'action' => sanitize_text_field($data['action'] ?? 'blocked'),
|
||||||
|
'reason' => sanitize_text_field($data['reason'] ?? ''),
|
||||||
|
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
|
||||||
|
'request_uri' => sanitize_text_field($data['uri'] ?? ''),
|
||||||
|
'logged_at' => current_time('mysql'),
|
||||||
|
];
|
||||||
|
|
||||||
|
update_option(self::OPT_QUEUE, $queue);
|
||||||
|
|
||||||
|
if (count($queue) >= self::BATCH_SIZE) {
|
||||||
|
self::flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Flush ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function flush(): void {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['enabled']) || empty($s['api_url'])) return;
|
||||||
|
|
||||||
|
$queue = (array)get_option(self::OPT_QUEUE, []);
|
||||||
|
if (empty($queue)) return;
|
||||||
|
|
||||||
|
$batch = array_splice($queue, 0, self::BATCH_SIZE);
|
||||||
|
update_option(self::OPT_QUEUE, $queue);
|
||||||
|
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
|
||||||
|
|
||||||
|
$response = wp_remote_post(
|
||||||
|
trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
|
||||||
|
[
|
||||||
|
'timeout' => 15,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode([
|
||||||
|
'site_hash' => hash('sha256', home_url()),
|
||||||
|
'bots' => $batch,
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
// Re-queue failed batch
|
||||||
|
update_option(self::OPT_QUEUE, array_merge($batch, $queue));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$s['last_sync'] = time();
|
||||||
|
$s['sent_total'] = ($s['sent_total'] ?? 0) + count($batch);
|
||||||
|
update_option(self::OPT_SETTINGS, $s);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function flush_shutdown(): void {
|
||||||
|
$queue = (array)get_option(self::OPT_QUEUE, []);
|
||||||
|
if (count($queue) >= 5) self::flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test connection ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function test_connection(): array {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['api_url'])) {
|
||||||
|
return ['ok' => false, 'message' => 'No API URL configured.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = trailingslashit(esc_url_raw($s['api_url']));
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
|
||||||
|
|
||||||
|
$health = wp_remote_get($base . 'api/v1/health', ['timeout' => 8]);
|
||||||
|
if (is_wp_error($health)) {
|
||||||
|
return ['ok' => false, 'message' => 'Cannot reach API: ' . $health->get_error_message()];
|
||||||
|
}
|
||||||
|
if (wp_remote_retrieve_response_code($health) !== 200) {
|
||||||
|
return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($health)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token check with intentionally bad payload
|
||||||
|
$auth = wp_remote_post($base . 'api/v1/submit', [
|
||||||
|
'timeout' => 8,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode(['site_hash' => 'connectivity_test', 'bots' => []]),
|
||||||
|
]);
|
||||||
|
if (is_wp_error($auth)) {
|
||||||
|
return ['ok' => false, 'message' => 'Token check failed: ' . $auth->get_error_message()];
|
||||||
|
}
|
||||||
|
if (wp_remote_retrieve_response_code($auth) === 403) {
|
||||||
|
return ['ok' => false, 'message' => 'API reachable but token rejected (HTTP 403).'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => true, 'message' => 'Connection verified. API is reachable and token accepted.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Send history batch ───────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function send_history_batch(int $batch_size = 50): array {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['api_url'])) {
|
||||||
|
return ['ok' => false, 'message' => 'No API URL configured.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = ITK_Database::bot_table();
|
||||||
|
$last_id = (int)get_option('itk_bot_history_last_id', 0);
|
||||||
|
$total = ITK_Database::count_bot_rows();
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT %d",
|
||||||
|
$last_id, $batch_size
|
||||||
|
), ARRAY_A);
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
return ['ok' => true, 'sent' => 0, 'remaining' => 0, 'has_more' => false,
|
||||||
|
'message' => 'All records have already been sent.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bots = array_map(fn($r) => [
|
||||||
|
'ip' => $r['ip_address'],
|
||||||
|
'bot_type' => $r['bot_type'],
|
||||||
|
'action' => $r['action'],
|
||||||
|
'reason' => $r['reason'],
|
||||||
|
'user_agent' => $r['user_agent'],
|
||||||
|
'request_uri' => $r['request_uri'],
|
||||||
|
'logged_at' => $r['logged_at'],
|
||||||
|
], $rows);
|
||||||
|
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
|
||||||
|
|
||||||
|
$response = wp_remote_post(
|
||||||
|
trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
|
||||||
|
[
|
||||||
|
'timeout' => 30,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode([
|
||||||
|
'site_hash' => hash('sha256', home_url()),
|
||||||
|
'bots' => $bots,
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return ['ok' => false, 'message' => 'Request failed: ' . $response->get_error_message()];
|
||||||
|
}
|
||||||
|
if (wp_remote_retrieve_response_code($response) !== 200) {
|
||||||
|
return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($response)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_last = (int)end($rows)['id'];
|
||||||
|
$sent_total = (int)get_option('itk_bot_history_sent', 0) + count($rows);
|
||||||
|
update_option('itk_bot_history_last_id', $new_last);
|
||||||
|
update_option('itk_bot_history_sent', $sent_total);
|
||||||
|
$remaining = max(0, $total - $sent_total);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'sent' => count($rows),
|
||||||
|
'remaining' => $remaining,
|
||||||
|
'has_more' => $remaining > 0,
|
||||||
|
'message' => sprintf('Sent %d records. %d remaining.', count($rows), $remaining),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ class ITK_Bot_Blocker {
|
|||||||
if ($count >= $limit) {
|
if ($count >= $limit) {
|
||||||
// Over the limit – log and send 429.
|
// Over the limit – log and send 429.
|
||||||
if (!empty($options['log_blocked_attempts'])) {
|
if (!empty($options['log_blocked_attempts'])) {
|
||||||
ITK_Database::log_bot([
|
$event = [
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
'ua' => $ua,
|
'ua' => $ua,
|
||||||
'referrer' => '',
|
'referrer' => '',
|
||||||
@@ -132,7 +132,9 @@ class ITK_Bot_Blocker {
|
|||||||
'bot_type' => $name,
|
'bot_type' => $name,
|
||||||
'reason' => "Rate limited: {$count}/{$limit} req/min",
|
'reason' => "Rate limited: {$count}/{$limit} req/min",
|
||||||
'action' => 'rate_limited',
|
'action' => 'rate_limited',
|
||||||
]);
|
];
|
||||||
|
ITK_Database::log_bot($event);
|
||||||
|
ITK_Bot_API::queue($event);
|
||||||
}
|
}
|
||||||
status_header(429);
|
status_header(429);
|
||||||
header('Retry-After: 60');
|
header('Retry-After: 60');
|
||||||
@@ -157,7 +159,7 @@ class ITK_Bot_Blocker {
|
|||||||
array $options
|
array $options
|
||||||
): void {
|
): void {
|
||||||
if (!empty($options['log_blocked_attempts'])) {
|
if (!empty($options['log_blocked_attempts'])) {
|
||||||
ITK_Database::log_bot([
|
$event = [
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
'ua' => $ua,
|
'ua' => $ua,
|
||||||
'referrer' => $referrer,
|
'referrer' => $referrer,
|
||||||
@@ -165,7 +167,9 @@ class ITK_Bot_Blocker {
|
|||||||
'bot_type' => $bot_type,
|
'bot_type' => $bot_type,
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
'action' => 'blocked',
|
'action' => 'blocked',
|
||||||
]);
|
];
|
||||||
|
ITK_Database::log_bot($event);
|
||||||
|
ITK_Bot_API::queue($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
$code = $options['response_code'] ?? '403';
|
$code = $options['response_code'] ?? '403';
|
||||||
|
|||||||
@@ -174,13 +174,15 @@ class ITK_Honeypot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function log_block(string $form_type, string $reason): void {
|
private function log_block(string $form_type, string $reason): void {
|
||||||
ITK_Database::log_honeypot([
|
$event = [
|
||||||
'ip' => $this->get_ip(),
|
'ip' => $this->get_ip(),
|
||||||
'form' => $form_type,
|
'form' => $form_type,
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
'uri' => $_SERVER['REQUEST_URI'] ?? '',
|
'uri' => $_SERVER['REQUEST_URI'] ?? '',
|
||||||
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
]);
|
];
|
||||||
|
ITK_Database::log_honeypot($event);
|
||||||
|
ITK_HP_API::queue($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validate_comment(array $comment_data): array {
|
public function validate_comment(array $comment_data): array {
|
||||||
|
|||||||
243
includes/class-itk-hp-api.php
Normal file
243
includes/class-itk-hp-api.php
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Honeypot Central API Client
|
||||||
|
*
|
||||||
|
* Queues honeypot block events locally and batch-submits them to the
|
||||||
|
* central Honeypot API (HoneypotFields Docker stack).
|
||||||
|
*
|
||||||
|
* Ported from SmartHoneypotAPIClient (HoneypotFields v2.4.0).
|
||||||
|
*/
|
||||||
|
class ITK_HP_API {
|
||||||
|
|
||||||
|
const OPT_SETTINGS = 'itk_hp_api_settings';
|
||||||
|
const OPT_QUEUE = 'itk_hp_api_queue';
|
||||||
|
const CRON_HOOK = 'itk_hp_api_flush';
|
||||||
|
const QUEUE_MAX = 500;
|
||||||
|
const BATCH_SIZE = 50;
|
||||||
|
|
||||||
|
/* ── Bootstrap ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function register_cron(): void {
|
||||||
|
if (!wp_next_scheduled(self::CRON_HOOK)) {
|
||||||
|
wp_schedule_event(time(), 'itk_5min', self::CRON_HOOK);
|
||||||
|
}
|
||||||
|
add_action(self::CRON_HOOK, [self::class, 'flush']);
|
||||||
|
add_action('shutdown', [self::class, 'flush_shutdown']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clear_cron(): void {
|
||||||
|
wp_clear_scheduled_hook(self::CRON_HOOK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Settings ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function defaults(): array {
|
||||||
|
return [
|
||||||
|
'enabled' => false,
|
||||||
|
'api_url' => '',
|
||||||
|
'api_token' => '',
|
||||||
|
'last_sync' => 0,
|
||||||
|
'sent_total' => 0,
|
||||||
|
'connection_ok' => null,
|
||||||
|
'last_verified' => 0,
|
||||||
|
'last_error' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function settings(): array {
|
||||||
|
return array_merge(self::defaults(), (array)get_option(self::OPT_SETTINGS, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function token(): string {
|
||||||
|
if (defined('ITK_HP_API_TOKEN') && ITK_HP_API_TOKEN !== '') {
|
||||||
|
return (string)ITK_HP_API_TOKEN;
|
||||||
|
}
|
||||||
|
return self::settings()['api_token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Queue ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue one honeypot block event.
|
||||||
|
* Called from ITK_Honeypot::log_block() when API is enabled.
|
||||||
|
*/
|
||||||
|
public static function queue(array $event): void {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['enabled']) || empty($s['api_url'])) return;
|
||||||
|
|
||||||
|
$queue = (array)get_option(self::OPT_QUEUE, []);
|
||||||
|
if (count($queue) >= self::QUEUE_MAX) array_shift($queue);
|
||||||
|
|
||||||
|
$queue[] = [
|
||||||
|
'ip' => sanitize_text_field($event['ip'] ?? ''),
|
||||||
|
'form_type' => sanitize_text_field($event['form'] ?? 'Unknown'),
|
||||||
|
'reason' => sanitize_text_field($event['reason'] ?? ''),
|
||||||
|
'user_agent' => sanitize_textarea_field($event['ua'] ?? ''),
|
||||||
|
'blocked_at' => current_time('mysql'),
|
||||||
|
];
|
||||||
|
|
||||||
|
update_option(self::OPT_QUEUE, $queue);
|
||||||
|
|
||||||
|
// Auto-flush when batch is ready
|
||||||
|
if (count($queue) >= self::BATCH_SIZE) {
|
||||||
|
self::flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Flush ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function flush(): void {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['enabled']) || empty($s['api_url'])) return;
|
||||||
|
|
||||||
|
$queue = (array)get_option(self::OPT_QUEUE, []);
|
||||||
|
if (empty($queue)) return;
|
||||||
|
|
||||||
|
$batch = array_splice($queue, 0, self::BATCH_SIZE);
|
||||||
|
update_option(self::OPT_QUEUE, $queue); // save remainder
|
||||||
|
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
|
||||||
|
|
||||||
|
$response = wp_remote_post(
|
||||||
|
trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
|
||||||
|
[
|
||||||
|
'timeout' => 15,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode([
|
||||||
|
'site_hash' => hash('sha256', home_url()),
|
||||||
|
'blocks' => $batch,
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
// Re-queue failed batch at the front
|
||||||
|
$queue = array_merge($batch, $queue);
|
||||||
|
update_option(self::OPT_QUEUE, $queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$s['last_sync'] = time();
|
||||||
|
$s['sent_total'] = ($s['sent_total'] ?? 0) + count($batch);
|
||||||
|
update_option(self::OPT_SETTINGS, $s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emergency flush on PHP shutdown (small queue only).
|
||||||
|
*/
|
||||||
|
public static function flush_shutdown(): void {
|
||||||
|
$queue = (array)get_option(self::OPT_QUEUE, []);
|
||||||
|
if (count($queue) >= 5) self::flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test connection ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function test_connection(): array {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['api_url'])) {
|
||||||
|
return ['ok' => false, 'message' => 'No API URL configured.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = trailingslashit(esc_url_raw($s['api_url']));
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
|
||||||
|
|
||||||
|
// Step 1: reachability
|
||||||
|
$health = wp_remote_get($base . 'api/v1/health', ['timeout' => 8]);
|
||||||
|
if (is_wp_error($health)) {
|
||||||
|
return ['ok' => false, 'message' => 'Cannot reach API: ' . $health->get_error_message()];
|
||||||
|
}
|
||||||
|
if (wp_remote_retrieve_response_code($health) !== 200) {
|
||||||
|
return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($health)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: token check (intentionally bad payload → 400 = auth OK, 403 = wrong token)
|
||||||
|
$auth = wp_remote_post($base . 'api/v1/submit', [
|
||||||
|
'timeout' => 8,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode(['site_hash' => 'connectivity_test', 'blocks' => []]),
|
||||||
|
]);
|
||||||
|
if (is_wp_error($auth)) {
|
||||||
|
return ['ok' => false, 'message' => 'Token check failed: ' . $auth->get_error_message()];
|
||||||
|
}
|
||||||
|
if (wp_remote_retrieve_response_code($auth) === 403) {
|
||||||
|
return ['ok' => false, 'message' => 'API reachable but token rejected (HTTP 403).'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => true, 'message' => 'Connection verified. API is reachable and token accepted.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Send history batch ───────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function send_history_batch(int $batch_size = 50): array {
|
||||||
|
$s = self::settings();
|
||||||
|
if (empty($s['api_url'])) {
|
||||||
|
return ['ok' => false, 'message' => 'No API URL configured.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table = ITK_Database::honeypot_table();
|
||||||
|
$last_id = (int)get_option('itk_hp_history_last_id', 0);
|
||||||
|
$total = ITK_Database::count_honeypot_rows();
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$table} WHERE id > %d ORDER BY id ASC LIMIT %d",
|
||||||
|
$last_id, $batch_size
|
||||||
|
), ARRAY_A);
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
return ['ok' => true, 'sent' => 0, 'remaining' => 0, 'has_more' => false,
|
||||||
|
'message' => 'All records have already been sent.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = array_map(fn($r) => [
|
||||||
|
'ip' => $r['ip_address'],
|
||||||
|
'form_type' => $r['form_type'],
|
||||||
|
'reason' => $r['reason'],
|
||||||
|
'user_agent' => $r['user_agent'],
|
||||||
|
'blocked_at' => $r['blocked_at'],
|
||||||
|
], $rows);
|
||||||
|
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
$token = self::token();
|
||||||
|
if ($token !== '') $headers['Authorization'] = 'Bearer ' . $token;
|
||||||
|
|
||||||
|
$response = wp_remote_post(
|
||||||
|
trailingslashit(esc_url_raw($s['api_url'])) . 'api/v1/submit',
|
||||||
|
[
|
||||||
|
'timeout' => 30,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode([
|
||||||
|
'site_hash' => hash('sha256', home_url()),
|
||||||
|
'blocks' => $blocks,
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return ['ok' => false, 'message' => 'Request failed: ' . $response->get_error_message()];
|
||||||
|
}
|
||||||
|
if (wp_remote_retrieve_response_code($response) !== 200) {
|
||||||
|
return ['ok' => false, 'message' => 'API returned HTTP ' . wp_remote_retrieve_response_code($response)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_last = (int)end($rows)['id'];
|
||||||
|
$sent_total = (int)get_option('itk_hp_history_sent', 0) + count($rows);
|
||||||
|
update_option('itk_hp_history_last_id', $new_last);
|
||||||
|
update_option('itk_hp_history_sent', $sent_total);
|
||||||
|
$remaining = max(0, $total - $sent_total);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'sent' => count($rows),
|
||||||
|
'remaining' => $remaining,
|
||||||
|
'has_more' => $remaining > 0,
|
||||||
|
'message' => sprintf('Sent %d records. %d remaining.', count($rows), $remaining),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ define('ITK_URL', plugin_dir_url(__FILE__));
|
|||||||
define('ITK_BASENAME', plugin_basename(__FILE__));
|
define('ITK_BASENAME', plugin_basename(__FILE__));
|
||||||
|
|
||||||
require_once ITK_PATH . 'includes/class-itk-database.php';
|
require_once ITK_PATH . 'includes/class-itk-database.php';
|
||||||
|
require_once ITK_PATH . 'includes/class-itk-hp-api.php';
|
||||||
|
require_once ITK_PATH . 'includes/class-itk-bot-api.php';
|
||||||
require_once ITK_PATH . 'includes/class-itk-bot-blocker.php';
|
require_once ITK_PATH . 'includes/class-itk-bot-blocker.php';
|
||||||
require_once ITK_PATH . 'includes/class-itk-protection.php';
|
require_once ITK_PATH . 'includes/class-itk-protection.php';
|
||||||
require_once ITK_PATH . 'includes/class-itk-optimization.php';
|
require_once ITK_PATH . 'includes/class-itk-optimization.php';
|
||||||
@@ -39,6 +41,18 @@ class InformatiQ_Toolkit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function __construct() {
|
private function __construct() {
|
||||||
|
// Register custom cron interval (5 minutes)
|
||||||
|
add_filter('cron_schedules', function ($schedules) {
|
||||||
|
if (!isset($schedules['itk_5min'])) {
|
||||||
|
$schedules['itk_5min'] = ['interval' => 300, 'display' => 'Every 5 Minutes'];
|
||||||
|
}
|
||||||
|
return $schedules;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Boot API cron flushers
|
||||||
|
ITK_HP_API::register_cron();
|
||||||
|
ITK_Bot_API::register_cron();
|
||||||
|
|
||||||
new ITK_Bot_Blocker();
|
new ITK_Bot_Blocker();
|
||||||
new ITK_Protection();
|
new ITK_Protection();
|
||||||
new ITK_Optimization();
|
new ITK_Optimization();
|
||||||
@@ -137,6 +151,8 @@ class InformatiQ_Toolkit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function deactivate() {
|
public static function deactivate() {
|
||||||
|
ITK_HP_API::clear_cron();
|
||||||
|
ITK_Bot_API::clear_cron();
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user