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>
This commit is contained in:
310
assets/css/admin.css
Normal file
310
assets/css/admin.css
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
/* ============================================================
|
||||||
|
InformatiQ Toolkit – Admin CSS
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* ── Page wrapper ─────────────────────────────────────────── */
|
||||||
|
.itk-wrap { max-width: 1400px; }
|
||||||
|
|
||||||
|
.itk-page-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 20px 0 16px;
|
||||||
|
}
|
||||||
|
.itk-logo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: #1a4a8a;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tab navigation ───────────────────────────────────────── */
|
||||||
|
.itk-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
border-bottom: 2px solid #c3c4c7;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.itk-tab {
|
||||||
|
padding: 8px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #2c3338;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
background: #f0f0f1;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
.itk-tab:hover { background: #fff; color: #1a4a8a; }
|
||||||
|
.itk-tab-active {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #c3c4c7;
|
||||||
|
color: #1a4a8a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Settings grid ────────────────────────────────────────── */
|
||||||
|
.itk-settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.itk-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
.itk-card h2 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1a4a8a;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toggle rows ──────────────────────────────────────────── */
|
||||||
|
.itk-toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.itk-toggle-row:last-child { border-bottom: none; }
|
||||||
|
.itk-toggle-info { flex: 1; }
|
||||||
|
.itk-toggle-label { display: block; font-weight: 600; font-size: 13px; color: #2c3338; }
|
||||||
|
.itk-toggle-desc { display: block; font-size: 11px; color: #646970; margin-top: 2px; }
|
||||||
|
|
||||||
|
/* ── Toggle switch ────────────────────────────────────────── */
|
||||||
|
.itk-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.itk-switch input { opacity: 0; width: 0; height: 0; }
|
||||||
|
.itk-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
inset: 0;
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 24px;
|
||||||
|
transition: .25s;
|
||||||
|
}
|
||||||
|
.itk-slider:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 18px; height: 18px;
|
||||||
|
left: 3px; bottom: 3px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: .25s;
|
||||||
|
}
|
||||||
|
input:checked + .itk-slider { background: #2271b1; }
|
||||||
|
input:checked + .itk-slider:before { transform: translateX(20px); }
|
||||||
|
|
||||||
|
/* ── Dashboard ────────────────────────────────────────────── */
|
||||||
|
.itk-dashboard {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 280px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (max-width: 960px) { .itk-dashboard { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* Monitor panel – terminal style */
|
||||||
|
.itk-monitor-panel {
|
||||||
|
background: #0d1117;
|
||||||
|
color: #58a6ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
.itk-monitor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #21262d;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.itk-monitor-title { font-weight: 700; font-size: 13px; letter-spacing: 1px; color: #79c0ff; }
|
||||||
|
.itk-monitor-blink {
|
||||||
|
background: #238636;
|
||||||
|
color: #fff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
animation: itk-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes itk-pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
|
||||||
|
|
||||||
|
/* Stat cards (inside monitor) */
|
||||||
|
.itk-stat-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.itk-stat-card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #21262d;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
min-width: 110px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.itk-stat-num { font-size: 2em; font-weight: 700; color: #58a6ff; line-height: 1.2; }
|
||||||
|
.itk-stat-lbl { font-size: 10px; color: #8b949e; letter-spacing: .5px; text-transform: uppercase; }
|
||||||
|
.itk-green { color: #3fb950; }
|
||||||
|
.itk-yellow { color: #d29922; }
|
||||||
|
|
||||||
|
/* Chart sections */
|
||||||
|
.itk-chart-section { margin-bottom: 20px; }
|
||||||
|
.itk-chart-title {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #8b949e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.itk-bar-chart { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.itk-bar-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
||||||
|
.itk-bar-label { width: 130px; color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.itk-bar-track { flex: 1; background: #21262d; border-radius: 3px; height: 14px; overflow: hidden; }
|
||||||
|
.itk-bar-fill { height: 100%; background: linear-gradient(90deg, #1f6feb, #58a6ff); border-radius: 3px; transition: width .4s; }
|
||||||
|
.itk-bar-hp { background: linear-gradient(90deg, #6e40c9, #a371f7) !important; }
|
||||||
|
.itk-bar-count { width: 40px; text-align: right; color: #8b949e; font-size: 11px; }
|
||||||
|
|
||||||
|
/* Mini table (top IPs) */
|
||||||
|
.itk-mini-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||||
|
.itk-mini-table th { color: #8b949e; font-weight: normal; padding: 4px 8px; border-bottom: 1px solid #21262d; }
|
||||||
|
.itk-mini-table td { color: #c9d1d9; padding: 4px 8px; border-bottom: 1px solid #161b22; }
|
||||||
|
.itk-mini-table a { color: #58a6ff; }
|
||||||
|
.itk-lookup { color: #8b949e; font-size: 10px; }
|
||||||
|
|
||||||
|
/* Quick status sidebar */
|
||||||
|
.itk-quick-status {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
.itk-quick-status .itk-chart-title { color: #1a4a8a; }
|
||||||
|
.itk-module-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.itk-module-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.itk-dot-on { background: #00a32a; }
|
||||||
|
.itk-dot-off { background: #c3c4c7; }
|
||||||
|
.itk-module-label { flex: 1; color: #2c3338; }
|
||||||
|
.itk-module-status { font-size: 10px; font-weight: 700; letter-spacing: .5px; }
|
||||||
|
.itk-dot-on ~ .itk-module-status { color: #00a32a; }
|
||||||
|
.itk-dot-off ~ .itk-module-status { color: #c3c4c7; }
|
||||||
|
|
||||||
|
/* ── Log tables ───────────────────────────────────────────── */
|
||||||
|
.itk-log-page {}
|
||||||
|
.itk-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.itk-filters input[type=text],
|
||||||
|
.itk-filters select { height: 32px; padding: 0 8px; }
|
||||||
|
.itk-count { color: #646970; font-size: 13px; margin: 0 0 8px; }
|
||||||
|
.itk-log-table { font-size: 12px; }
|
||||||
|
.itk-log-table th { white-space: nowrap; }
|
||||||
|
.itk-nowrap { white-space: nowrap; }
|
||||||
|
.itk-ua { color: #646970; font-size: 11px; max-width: 220px; word-break: break-all; }
|
||||||
|
.itk-uri { font-size: 11px; color: #646970; max-width: 200px; word-break: break-all; }
|
||||||
|
.itk-no-results { text-align: center; padding: 20px; color: #646970; }
|
||||||
|
.itk-filter-link { font-size: 10px; color: #2271b1; text-decoration: none; }
|
||||||
|
.itk-filter-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.itk-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.itk-badge-block { background: #ffecec; color: #b32d2e; border: 1px solid #f7c5c5; }
|
||||||
|
.itk-badge-warn { background: #fef3cd; color: #856404; border: 1px solid #fde68a; }
|
||||||
|
.itk-badge-hp { background: #f3e8ff; color: #6e40c9; border: 1px solid #d4a9ff; }
|
||||||
|
|
||||||
|
/* Pager */
|
||||||
|
.itk-pager {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.itk-pager span { color: #646970; }
|
||||||
|
|
||||||
|
/* Danger button */
|
||||||
|
.itk-btn-danger { color: #b32d2e !important; border-color: #b32d2e !important; }
|
||||||
|
.itk-btn-danger:hover { background: #b32d2e !important; color: #fff !important; }
|
||||||
|
|
||||||
|
/* ── Config editor ────────────────────────────────────────── */
|
||||||
|
.itk-config-editor {}
|
||||||
|
.itk-config-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.itk-config-tab {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #f0f0f1;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #2c3338;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.itk-config-tab.active { background: #2271b1; color: #fff; border-color: #2271b1; }
|
||||||
|
.itk-config-editor-area { background: #fff; border: 1px solid #c3c4c7; border-radius: 6px; padding: 20px; }
|
||||||
|
.itk-config-textarea {
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toggle feedback ──────────────────────────────────────── */
|
||||||
|
.itk-toggle-saving { opacity: .6; pointer-events: none; }
|
||||||
|
.itk-toggle-saved { color: #00a32a; font-size: 11px; }
|
||||||
|
.itk-toggle-error { color: #b32d2e; font-size: 11px; }
|
||||||
79
assets/js/admin.js
Normal file
79
assets/js/admin.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/* InformatiQ Toolkit – Admin JS */
|
||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ── Toggle switches (AJAX) ───────────────────────────────── */
|
||||||
|
$(document).on('change', '.itk-toggle-input', function () {
|
||||||
|
var $input = $(this);
|
||||||
|
var $row = $input.closest('.itk-toggle-row');
|
||||||
|
var option = $input.data('option');
|
||||||
|
var setting = $input.data('setting');
|
||||||
|
var value = $input.is(':checked') ? 1 : 0;
|
||||||
|
|
||||||
|
$row.addClass('itk-toggle-saving');
|
||||||
|
|
||||||
|
$.post(itkAdmin.ajaxUrl, {
|
||||||
|
action: 'itk_save_setting',
|
||||||
|
nonce: itkAdmin.nonce,
|
||||||
|
option: option,
|
||||||
|
setting: setting,
|
||||||
|
value: value
|
||||||
|
})
|
||||||
|
.done(function (res) {
|
||||||
|
if (res.success) {
|
||||||
|
showFeedback($row, 'itk-toggle-saved', 'Saved');
|
||||||
|
} else {
|
||||||
|
$input.prop('checked', !$input.is(':checked')); // revert
|
||||||
|
showFeedback($row, 'itk-toggle-error', 'Error saving');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
$input.prop('checked', !$input.is(':checked'));
|
||||||
|
showFeedback($row, 'itk-toggle-error', 'Request failed');
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
$row.removeClass('itk-toggle-saving');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showFeedback($row, cls, msg) {
|
||||||
|
$row.find('.itk-feedback').remove();
|
||||||
|
var $fb = $('<span class="itk-feedback ' + cls + '">' + msg + '</span>');
|
||||||
|
$row.append($fb);
|
||||||
|
setTimeout(function () { $fb.fadeOut(400, function () { $(this).remove(); }); }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Config file editor (AJAX) ────────────────────────────── */
|
||||||
|
$('#itk-save-config').on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $btn = $(this);
|
||||||
|
var file = $btn.data('file');
|
||||||
|
var content = $('#itk-config-content').val();
|
||||||
|
var $status = $('#itk-config-status');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('Saving…');
|
||||||
|
$status.hide();
|
||||||
|
|
||||||
|
$.post(itkAdmin.ajaxUrl, {
|
||||||
|
action: 'itk_save_config_file',
|
||||||
|
nonce: itkAdmin.nonce,
|
||||||
|
file: file,
|
||||||
|
content: content
|
||||||
|
})
|
||||||
|
.done(function (res) {
|
||||||
|
if (res.success) {
|
||||||
|
$status.text('Saved!').css('color', '#00a32a').show();
|
||||||
|
} else {
|
||||||
|
$status.text('Error: ' + (res.data || 'unknown')).css('color', '#b32d2e').show();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
$status.text('Request failed.').css('color', '#b32d2e').show();
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
$btn.prop('disabled', false).text('Save File');
|
||||||
|
setTimeout(function () { $status.fadeOut(); }, 3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
6
config/allowed-ips.conf
Normal file
6
config/allowed-ips.conf
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
194.56.239.153
|
||||||
|
109.69.48.0
|
||||||
|
195.154.47.0
|
||||||
|
127.0.0.1
|
||||||
|
192.168.0.0/24
|
||||||
|
192.168.1.1/24
|
||||||
406
config/badbots.conf
Normal file
406
config/badbots.conf
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# OpenAI bots are handled separately in the plugin code
|
||||||
|
|
||||||
|
# Common malicious bots and user agents from .htaccess
|
||||||
|
jorgee
|
||||||
|
morfeus
|
||||||
|
firefox/40.1
|
||||||
|
firefox/34.0
|
||||||
|
firefox/32.1
|
||||||
|
firefox/19.0
|
||||||
|
firefox/38.0
|
||||||
|
firefox/18.0
|
||||||
|
wget
|
||||||
|
curl
|
||||||
|
libwww-perl
|
||||||
|
WinHttp
|
||||||
|
okhttp
|
||||||
|
python
|
||||||
|
java
|
||||||
|
WebReaper
|
||||||
|
WebSauger
|
||||||
|
Website eXtractor
|
||||||
|
Website Quester
|
||||||
|
Webster
|
||||||
|
WebStripper
|
||||||
|
WebWhacker
|
||||||
|
WebZIP
|
||||||
|
Whacker
|
||||||
|
BatchFTP
|
||||||
|
HTTrack
|
||||||
|
Harvest
|
||||||
|
Collector
|
||||||
|
Copier
|
||||||
|
Extractor
|
||||||
|
lftp
|
||||||
|
libWeb/clsHTTP
|
||||||
|
Mirror
|
||||||
|
Net Vampire
|
||||||
|
Offline Explorer
|
||||||
|
Offline Navigator
|
||||||
|
PageGrabber
|
||||||
|
Sucker
|
||||||
|
SuperHTTP
|
||||||
|
Teleport
|
||||||
|
Vacuum
|
||||||
|
Web Sucker
|
||||||
|
WebAuto
|
||||||
|
WebBandit
|
||||||
|
Webclipping.com
|
||||||
|
WebCopier
|
||||||
|
WebEnhancer
|
||||||
|
WebFetch
|
||||||
|
WebLeacher
|
||||||
|
WWWOFFLE
|
||||||
|
WWW-Collector-E
|
||||||
|
Go-Ahead-Got-It
|
||||||
|
gotit
|
||||||
|
GrabNet
|
||||||
|
lwp-trivial
|
||||||
|
LWP::Simple
|
||||||
|
Magnet
|
||||||
|
Mag-Net
|
||||||
|
moget
|
||||||
|
MIDown tool
|
||||||
|
NetSpider
|
||||||
|
NetZIP
|
||||||
|
Reaper
|
||||||
|
Recorder
|
||||||
|
ReGet
|
||||||
|
RepoMonkey
|
||||||
|
Siphon
|
||||||
|
SiteSnagger
|
||||||
|
AppsViewer
|
||||||
|
Lynx
|
||||||
|
Acunetix
|
||||||
|
FHscan
|
||||||
|
Baidu
|
||||||
|
Yandex
|
||||||
|
Download Demon
|
||||||
|
Download Devil
|
||||||
|
Download Wonder
|
||||||
|
EirGrabber
|
||||||
|
EasyDL
|
||||||
|
Mass Downloader
|
||||||
|
RealDownload
|
||||||
|
SmartDownload
|
||||||
|
EmailCollector
|
||||||
|
EmailSiphon
|
||||||
|
EmailWolf
|
||||||
|
WebEMailExtrac
|
||||||
|
EmailSiphon
|
||||||
|
Mail
|
||||||
|
slurp
|
||||||
|
MJ12
|
||||||
|
FastProbe
|
||||||
|
spbot
|
||||||
|
dotbot
|
||||||
|
semrush
|
||||||
|
Daum
|
||||||
|
duckduckgo
|
||||||
|
teoma
|
||||||
|
Aboundex
|
||||||
|
80legs
|
||||||
|
360Spider
|
||||||
|
Cogentbot
|
||||||
|
Alexibot
|
||||||
|
asterias
|
||||||
|
attach
|
||||||
|
BackDoorBot
|
||||||
|
BackWeb
|
||||||
|
Bandit
|
||||||
|
Bigfoot
|
||||||
|
Black.Hole
|
||||||
|
BlackWidow
|
||||||
|
BlowFish
|
||||||
|
BotALot
|
||||||
|
Buddy
|
||||||
|
BuiltBotTough
|
||||||
|
Bullseye
|
||||||
|
BunnySlippers
|
||||||
|
Cegbfeieh
|
||||||
|
CheeseBot
|
||||||
|
CherryPicker
|
||||||
|
ChinaClaw
|
||||||
|
CopyRightCheck
|
||||||
|
cosmos
|
||||||
|
Crescent
|
||||||
|
Custo
|
||||||
|
AIBOT
|
||||||
|
DISCo
|
||||||
|
DIIbot
|
||||||
|
DittoSpyder
|
||||||
|
dragonfly
|
||||||
|
Drip
|
||||||
|
eCatch
|
||||||
|
ebingbong
|
||||||
|
EroCrawler
|
||||||
|
EyeNetIE
|
||||||
|
Foobot
|
||||||
|
flunky
|
||||||
|
FrontPage
|
||||||
|
Grafula
|
||||||
|
hloader
|
||||||
|
HMView
|
||||||
|
humanlinks
|
||||||
|
IlseBot
|
||||||
|
Indy Library
|
||||||
|
InfoNaviRobot
|
||||||
|
InfoTekies
|
||||||
|
Intelliseek
|
||||||
|
InterGET
|
||||||
|
Internet Ninja
|
||||||
|
Iria
|
||||||
|
Jakarta
|
||||||
|
JennyBot
|
||||||
|
JetCar
|
||||||
|
JOC
|
||||||
|
JustView
|
||||||
|
Jyxobot
|
||||||
|
Kenjin.Spider
|
||||||
|
Keyword.Density
|
||||||
|
larbin
|
||||||
|
LexiBot
|
||||||
|
likse
|
||||||
|
MarkWatch
|
||||||
|
Mata.Hari
|
||||||
|
Memo
|
||||||
|
Microsoft.URL
|
||||||
|
Microsoft URL Control
|
||||||
|
MIIxpc
|
||||||
|
Missigua Locator
|
||||||
|
Mister PiX
|
||||||
|
NAMEPROTECT
|
||||||
|
Navroad
|
||||||
|
NearSite
|
||||||
|
NetAnts
|
||||||
|
Netcraft
|
||||||
|
NetMechanic
|
||||||
|
NextGenSearchBot
|
||||||
|
NICErsPRO
|
||||||
|
niki-bot
|
||||||
|
NimbleCrawler
|
||||||
|
Ninja
|
||||||
|
NPbot
|
||||||
|
Octopus
|
||||||
|
Openfind
|
||||||
|
OutfoxBot
|
||||||
|
Papa Foto
|
||||||
|
pavuk
|
||||||
|
pcBrowser
|
||||||
|
PHP version tracker
|
||||||
|
Pockey
|
||||||
|
ProPowerBot/2.14
|
||||||
|
ProWebWalker
|
||||||
|
psbot
|
||||||
|
Pump
|
||||||
|
QueryN.Metasearch
|
||||||
|
SlySearch
|
||||||
|
Snake
|
||||||
|
Snapbot
|
||||||
|
Snoopy
|
||||||
|
sogou
|
||||||
|
SpaceBison
|
||||||
|
SpankBot
|
||||||
|
spanner
|
||||||
|
Sqworm
|
||||||
|
Stripper
|
||||||
|
SuperBot
|
||||||
|
Surfbot
|
||||||
|
suzuran
|
||||||
|
Szukacz/1.4
|
||||||
|
tAkeOut
|
||||||
|
Telesoft
|
||||||
|
TurnitinBot/1.5
|
||||||
|
The.Intraformant
|
||||||
|
TheNomad
|
||||||
|
TightTwatBot
|
||||||
|
Titan
|
||||||
|
True_Robot
|
||||||
|
turingos
|
||||||
|
TurnitinBot
|
||||||
|
URLy.Warning
|
||||||
|
VCI
|
||||||
|
VoidEYE
|
||||||
|
WebmasterWorldForumBot
|
||||||
|
WebGo IS
|
||||||
|
Widow
|
||||||
|
WISENutbot
|
||||||
|
Xaldon
|
||||||
|
Zeus
|
||||||
|
ZmEu
|
||||||
|
Zyborg
|
||||||
|
crawle
|
||||||
|
igdeSpyder
|
||||||
|
Robot
|
||||||
|
Aport
|
||||||
|
spider
|
||||||
|
Parser
|
||||||
|
ahref
|
||||||
|
zoom
|
||||||
|
Powermarks
|
||||||
|
SafeDNS
|
||||||
|
BLEXBot
|
||||||
|
aria2
|
||||||
|
wikido
|
||||||
|
Qwantify
|
||||||
|
DotBot
|
||||||
|
FatBot
|
||||||
|
grapeshot
|
||||||
|
Nutch
|
||||||
|
linkdexbot
|
||||||
|
Twitterbot
|
||||||
|
Google-HTTP-Java-Client
|
||||||
|
MetaCommentBot
|
||||||
|
Veoozbot
|
||||||
|
ScoutJet
|
||||||
|
DomainAppender
|
||||||
|
Windows 2005
|
||||||
|
Go-http-client
|
||||||
|
Drupal
|
||||||
|
OrangeBot
|
||||||
|
CCBot
|
||||||
|
WBSearchBot
|
||||||
|
SEOkicks
|
||||||
|
WHR
|
||||||
|
sqlmap
|
||||||
|
ltx71
|
||||||
|
aiHitBot
|
||||||
|
InfoPath
|
||||||
|
Superfeedr
|
||||||
|
rogerbot
|
||||||
|
Alltop
|
||||||
|
heritrix
|
||||||
|
indiensolidaritet
|
||||||
|
Experibot
|
||||||
|
magpie
|
||||||
|
RSSInclude
|
||||||
|
wp-android
|
||||||
|
XML-RPC.NET
|
||||||
|
Synapse
|
||||||
|
GimmeUSAbot
|
||||||
|
istellabot
|
||||||
|
interfax
|
||||||
|
vebidoobot
|
||||||
|
oBot
|
||||||
|
Jetty
|
||||||
|
mozilla16.2.exe
|
||||||
|
dataaccessd
|
||||||
|
(compatible;)
|
||||||
|
Dalvik
|
||||||
|
eCairn
|
||||||
|
istellabot
|
||||||
|
InetURL
|
||||||
|
BazQux
|
||||||
|
Wotbox
|
||||||
|
null
|
||||||
|
scrapy-redis
|
||||||
|
weborama-fetcher
|
||||||
|
TrapitAgent
|
||||||
|
UNKNOWN
|
||||||
|
SeznamBot
|
||||||
|
Dataprovider
|
||||||
|
msnbot-Products
|
||||||
|
masscan
|
||||||
|
istellabot
|
||||||
|
BUbiNG
|
||||||
|
.NET
|
||||||
|
cliqzbot
|
||||||
|
Deepnet
|
||||||
|
Ziba
|
||||||
|
SMTBot
|
||||||
|
MojeekBot
|
||||||
|
linqia
|
||||||
|
portscout
|
||||||
|
Dataprovider
|
||||||
|
ia_archiver
|
||||||
|
Dalvik
|
||||||
|
MEGAsync
|
||||||
|
GroupHigh
|
||||||
|
Moreover
|
||||||
|
YisouSpider
|
||||||
|
YahooCacheSystem
|
||||||
|
Clickagy
|
||||||
|
Go-http-client
|
||||||
|
SMUrlExpander
|
||||||
|
XoviBot
|
||||||
|
MSIE3.00
|
||||||
|
MSIE2.00
|
||||||
|
MSIE4.00
|
||||||
|
MSIECrawler
|
||||||
|
Windows 2005
|
||||||
|
Windows 2008
|
||||||
|
Windows 2004
|
||||||
|
Windows 2003
|
||||||
|
Windows 2002
|
||||||
|
XoviBot
|
||||||
|
Qwantify
|
||||||
|
BOT for JCE
|
||||||
|
Jorgee
|
||||||
|
YaK
|
||||||
|
iTunes
|
||||||
|
Mechanize
|
||||||
|
Mail.RU_Bot
|
||||||
|
zgrab
|
||||||
|
Owler
|
||||||
|
Barkrowler
|
||||||
|
SearchmetricsBot
|
||||||
|
extlinks
|
||||||
|
archive-it
|
||||||
|
BDCbot
|
||||||
|
SuperPagesUrlVerifyBot
|
||||||
|
Siteimprove
|
||||||
|
Freshbot
|
||||||
|
WebDAV
|
||||||
|
ips-agent
|
||||||
|
PiplBot
|
||||||
|
coccocbot-web
|
||||||
|
Alexa Toolbar
|
||||||
|
scrapinghub
|
||||||
|
Twingly
|
||||||
|
sysscan
|
||||||
|
trendictionbot0
|
||||||
|
DnyzBot
|
||||||
|
rogerbot
|
||||||
|
GridBot
|
||||||
|
DnyzBot
|
||||||
|
PiplBot
|
||||||
|
BoardReader
|
||||||
|
SafeDNSBot
|
||||||
|
Insideview
|
||||||
|
coccocbot
|
||||||
|
PolycomVVX
|
||||||
|
^Mozilla/5.0$
|
||||||
|
^The Knowledge AI
|
||||||
|
SputnikBot
|
||||||
|
od-database-crawler
|
||||||
|
Hype%20Machine
|
||||||
|
The Hype Machine Engine
|
||||||
|
Apache-HttpClient
|
||||||
|
Goodzer
|
||||||
|
Knowledge
|
||||||
|
Linguee
|
||||||
|
serpstatbot
|
||||||
|
PHP/5
|
||||||
|
PHP/4
|
||||||
|
PHP/3
|
||||||
|
Thumbtack-Thunderdome
|
||||||
|
Googlebot-Image
|
||||||
|
Googlebot-Video
|
||||||
|
bingpreview
|
||||||
|
msnbot-media
|
||||||
|
Exabot
|
||||||
|
Image Stripper
|
||||||
|
Image Sucker
|
||||||
|
Express WebPictures
|
||||||
|
Web Image Collector
|
||||||
|
Web.Image.Collector
|
||||||
|
YandexImages
|
||||||
|
Firefox mutant
|
||||||
|
Ukraine Local
|
||||||
|
Mozilla/3.Mozilla/2.01
|
||||||
|
Mozilla.*NEWT
|
||||||
|
LinkextractorPro
|
||||||
|
LinkScan/8.1a.Unix
|
||||||
|
LNSpiderguy
|
||||||
|
LinkWalker
|
||||||
|
Xenu
|
||||||
23
config/goodbots.conf
Normal file
23
config/goodbots.conf
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Good/Legitimate bots - these are rate-limited but NOT blocked
|
||||||
|
# Format: BotName|rate_per_minute
|
||||||
|
# Lines starting with # are comments
|
||||||
|
|
||||||
|
Googlebot|60
|
||||||
|
Bingbot|60
|
||||||
|
DuckDuckBot|60
|
||||||
|
Baiduspider|30
|
||||||
|
YandexBot|30
|
||||||
|
Sogou|20
|
||||||
|
Applebot|30
|
||||||
|
facebot|30
|
||||||
|
ia_archiver|20
|
||||||
|
Twitterbot|30
|
||||||
|
LinkedInBot|30
|
||||||
|
Slurp|30
|
||||||
|
MJ12bot|20
|
||||||
|
AhrefsBot|10
|
||||||
|
SemrushBot|10
|
||||||
|
DotBot|20
|
||||||
|
PetalBot|20
|
||||||
|
Bytespider|20
|
||||||
|
GPTBot|0
|
||||||
254
config/networks.conf
Normal file
254
config/networks.conf
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# IP addresses and networks to block extracted from .htaccess
|
||||||
|
|
||||||
|
# Aliyun
|
||||||
|
121.40.0.0/14
|
||||||
|
121.40.0.0/15
|
||||||
|
|
||||||
|
# Cyveillance subnets
|
||||||
|
38.100.19.8/29
|
||||||
|
38.100.21.0/24
|
||||||
|
38.100.41.64/26
|
||||||
|
38.105.71.0/25
|
||||||
|
38.105.83.0/27
|
||||||
|
38.112.21.140/30
|
||||||
|
38.118.42.32/29
|
||||||
|
65.213.208.128/27
|
||||||
|
65.222.176.96/27
|
||||||
|
65.222.185.72/29
|
||||||
|
|
||||||
|
# Poneytelecom subnets
|
||||||
|
62.4.0.0/19
|
||||||
|
62.210.0.0/16
|
||||||
|
195.154.0.0/16
|
||||||
|
212.47.224.0/19
|
||||||
|
212.83.128.0/19
|
||||||
|
212.83.160.0/19
|
||||||
|
212.129.0.0/18
|
||||||
|
|
||||||
|
# Ecatel & Leaseweb subnets
|
||||||
|
80.82.64.0/24
|
||||||
|
80.82.65.0/24
|
||||||
|
80.82.66.0/24
|
||||||
|
80.82.67.0/24
|
||||||
|
80.82.68.0/24
|
||||||
|
80.82.69.0/24
|
||||||
|
80.82.70.0/24
|
||||||
|
80.82.76.0/24
|
||||||
|
80.82.77.0/24
|
||||||
|
80.82.78.0/24
|
||||||
|
80.82.79.0/24
|
||||||
|
89.248.160.0/21
|
||||||
|
89.248.168.0/24
|
||||||
|
89.248.169.0/24
|
||||||
|
89.248.170.0/23
|
||||||
|
89.248.172.0/23
|
||||||
|
89.248.174.0/24
|
||||||
|
93.174.88.0/21
|
||||||
|
94.102.48.0/20
|
||||||
|
188.72.106.0/24
|
||||||
|
188.72.117.0/24
|
||||||
|
185.56.80.125
|
||||||
|
|
||||||
|
# Aboundex
|
||||||
|
173.192.34.95
|
||||||
|
|
||||||
|
# Bluecoat
|
||||||
|
8.21.4.254
|
||||||
|
65.46.48.192/30
|
||||||
|
65.160.238.176/28
|
||||||
|
85.92.222.0/24
|
||||||
|
206.51.36.0/22
|
||||||
|
216.52.23.0/24
|
||||||
|
|
||||||
|
# Cyberpatrol
|
||||||
|
38.103.17.160/27
|
||||||
|
|
||||||
|
# Internet Identity - Anti-Phishing
|
||||||
|
66.113.96.0/20
|
||||||
|
70.35.113.192/27
|
||||||
|
|
||||||
|
# Ironport
|
||||||
|
204.15.80.0/22
|
||||||
|
|
||||||
|
# Lightspeed Systems Security
|
||||||
|
66.17.15.128/26
|
||||||
|
69.84.207.32/27
|
||||||
|
69.84.207.128/25
|
||||||
|
|
||||||
|
# Layered Technologies
|
||||||
|
72.36.128.0/17
|
||||||
|
72.232.0.0/16
|
||||||
|
72.233.0.0/17
|
||||||
|
216.32.0.0/14
|
||||||
|
|
||||||
|
# M86
|
||||||
|
67.192.231.224/29
|
||||||
|
208.90.236.0/22
|
||||||
|
|
||||||
|
# Phish-Inspector.com
|
||||||
|
209.147.127.208/28
|
||||||
|
|
||||||
|
# Prescient Software, Inc. Phishmongers
|
||||||
|
198.186.190.0/23
|
||||||
|
198.186.192.0/23
|
||||||
|
198.186.194.0/24
|
||||||
|
|
||||||
|
# urlfilterdb
|
||||||
|
207.210.99.32/29
|
||||||
|
|
||||||
|
# websense-in.car1.sandiego1.level3.net
|
||||||
|
4.53.120.22
|
||||||
|
|
||||||
|
# Websense
|
||||||
|
66.194.6.0/24
|
||||||
|
67.117.201.128/28
|
||||||
|
69.67.32.0/20
|
||||||
|
131.191.87.0/24
|
||||||
|
204.15.64.0/21
|
||||||
|
208.80.192.0/21
|
||||||
|
212.62.26.64/27
|
||||||
|
213.168.226.0/24
|
||||||
|
213.168.241.0/30
|
||||||
|
213.168.242.0/30
|
||||||
|
213.236.150.16/28
|
||||||
|
|
||||||
|
# IP Strada & co.
|
||||||
|
162.211.104.0/22
|
||||||
|
162.218.56.0/21
|
||||||
|
198.89.232.0/21
|
||||||
|
199.15.232.0/21
|
||||||
|
199.15.232.0/24
|
||||||
|
199.15.233.0/24
|
||||||
|
199.15.234.0/24
|
||||||
|
199.15.235.0/24
|
||||||
|
199.15.237.0/24
|
||||||
|
199.15.238.0/24
|
||||||
|
199.15.239.0/24
|
||||||
|
|
||||||
|
# DigitalOcean
|
||||||
|
45.55.100.0/22
|
||||||
|
45.55.116.0/22
|
||||||
|
67.207.66.0/24
|
||||||
|
104.131.192.0/19
|
||||||
|
104.131.224.0/19
|
||||||
|
107.170.0.0/17
|
||||||
|
107.170.128.0/19
|
||||||
|
107.170.160.0/19
|
||||||
|
138.197.240.0/22
|
||||||
|
138.197.252.0/22
|
||||||
|
159.203.152.0/22
|
||||||
|
162.243.0.0/17
|
||||||
|
162.243.191.0/24
|
||||||
|
162.243.192.0/18
|
||||||
|
192.241.160.0/19
|
||||||
|
192.241.240.0/20
|
||||||
|
|
||||||
|
# vHoster Ukraine doing WP bruteforce
|
||||||
|
91.200.12.0/22
|
||||||
|
|
||||||
|
# Drake Holdings
|
||||||
|
192.92.196.0/24
|
||||||
|
204.79.180.0/24
|
||||||
|
|
||||||
|
# Hetzner Denies
|
||||||
|
193.47.99.0/24
|
||||||
|
188.40.0.0/16
|
||||||
|
185.12.64.0/22
|
||||||
|
178.63.0.0/16
|
||||||
|
176.9.0.0/16
|
||||||
|
213.239.192.0/18
|
||||||
|
213.133.96.0/19
|
||||||
|
88.198.0.0/16
|
||||||
|
85.10.192.0/18
|
||||||
|
78.46.0.0/15
|
||||||
|
5.9.0.0/17
|
||||||
|
5.9.0.0/16
|
||||||
|
46.4.0.0/16
|
||||||
|
88.99.0.0/16
|
||||||
|
91.220.49.0/24
|
||||||
|
91.233.8.0/22
|
||||||
|
94.130.0.0/16
|
||||||
|
95.216.0.0/16
|
||||||
|
95.217.0.0/16
|
||||||
|
136.243.0.0/16
|
||||||
|
138.201.0.0/16
|
||||||
|
144.76.0.0/16
|
||||||
|
148.251.0.0/16
|
||||||
|
176.102.168.0/21
|
||||||
|
185.50.120.0/23
|
||||||
|
185.107.52.0/22
|
||||||
|
185.126.28.0/22
|
||||||
|
185.136.140.0/23
|
||||||
|
185.141.200.0/24
|
||||||
|
185.141.202.0/24
|
||||||
|
185.171.224.0/22
|
||||||
|
185.185.26.0/23
|
||||||
|
185.189.228.0/24
|
||||||
|
185.189.230.0/24
|
||||||
|
185.189.231.0/24
|
||||||
|
185.209.124.0/22
|
||||||
|
185.216.237.0/24
|
||||||
|
185.228.8.0/22
|
||||||
|
193.25.170.0/23
|
||||||
|
193.110.6.0/23
|
||||||
|
193.223.77.0/24
|
||||||
|
194.42.180.0/22
|
||||||
|
194.42.184.0/22
|
||||||
|
194.145.226.0/24
|
||||||
|
195.60.226.0/24
|
||||||
|
195.248.224.0/24
|
||||||
|
197.242.84.0/22
|
||||||
|
|
||||||
|
# Seznam bot
|
||||||
|
77.75.72.0/23
|
||||||
|
77.75.74.0/24
|
||||||
|
77.75.75.0/24
|
||||||
|
77.75.76.0/23
|
||||||
|
77.75.78.0/23
|
||||||
|
185.66.188.0/22
|
||||||
|
|
||||||
|
# Quasi Networks - Spammers
|
||||||
|
145.249.104.0/22
|
||||||
|
185.216.140.0/23
|
||||||
|
188.72.103.0/24
|
||||||
|
188.72.106.0/24
|
||||||
|
188.72.117.0/24
|
||||||
|
196.16.0.0/14
|
||||||
|
213.184.105.0/24
|
||||||
|
213.184.113.0/24
|
||||||
|
213.184.115.0/24
|
||||||
|
213.184.117.0/24
|
||||||
|
|
||||||
|
# DataShack / Wholesale Internet / VPN Consumer Network / My Server Planet / VoIP DediNet & co.
|
||||||
|
63.141.224.0/19
|
||||||
|
69.30.192.0/24
|
||||||
|
69.30.204.0/24
|
||||||
|
69.30.220.0/24
|
||||||
|
69.30.228.0/24
|
||||||
|
69.30.235.0/24
|
||||||
|
69.30.237.0/24
|
||||||
|
69.197.148.0/24
|
||||||
|
69.197.152.0/24
|
||||||
|
69.197.170.0/24
|
||||||
|
69.197.171.0/24
|
||||||
|
69.197.173.0/24
|
||||||
|
69.197.178.0/24
|
||||||
|
74.91.16.0/20
|
||||||
|
104.37.30.0/24
|
||||||
|
107.150.32.0/19
|
||||||
|
142.54.160.0/19
|
||||||
|
173.46.91.0/24
|
||||||
|
173.46.93.0/24
|
||||||
|
192.151.144.0/20
|
||||||
|
192.187.96.0/19
|
||||||
|
198.204.224.0/19
|
||||||
|
199.168.96.0/21
|
||||||
|
204.12.199.0/24
|
||||||
|
204.12.200.0/24
|
||||||
|
204.12.203.0/24
|
||||||
|
204.12.205.0/24
|
||||||
|
204.12.245.0/24
|
||||||
|
208.67.0.0/24
|
||||||
|
208.67.1.0/24
|
||||||
|
208.110.85.0/24
|
||||||
|
208.110.87.0/24
|
||||||
66
config/payloads.conf
Normal file
66
config/payloads.conf
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Known attack payload regex patterns
|
||||||
|
# One pattern per line, these are checked against request parameters and user input
|
||||||
|
# Lines starting with # are comments
|
||||||
|
|
||||||
|
# XSS attack patterns
|
||||||
|
# Pattern for alert/prompt/confirm execution
|
||||||
|
/(?:<|%3C|<)(?:script|iframe|svg|img|a).*?(?:alert|prompt|confirm|eval)\s*\(.*?\)/i
|
||||||
|
# Pattern for script injection
|
||||||
|
/(?:<|%3C|<)script.*?(?:>|%3E|>)/i
|
||||||
|
# Pattern for event handlers like onerror, onload, etc.
|
||||||
|
/\bon(?:error|load|click|mouseover|focus|blur)\s*=\s*["']?(?:alert|prompt|confirm|eval)/i
|
||||||
|
# Pattern for javascript: protocol
|
||||||
|
/javascript\s*:\s*(?:alert|prompt|confirm|eval)/i
|
||||||
|
# Pattern for data URI scheme with script
|
||||||
|
/data\s*:\s*(?:text|application)\/(?:javascript|html).*?base64/i
|
||||||
|
|
||||||
|
# SQL Injection patterns
|
||||||
|
# Pattern for basic SQL injection attempts
|
||||||
|
/(?:'\s*OR\s*'[\w\d]+'?\s*=\s*'[\w\d]+)|(?:"\s*OR\s*"[\w\d]+"?\s*=\s*"[\w\d]+")/i
|
||||||
|
# Pattern for SQL comments
|
||||||
|
/(?:--|#|\/\*)[^\w\d]*(?:union|select|insert|update|delete|drop|alter)/i
|
||||||
|
# Pattern for UNION SELECT attempts
|
||||||
|
/union\s+(?:all\s+)?select/i
|
||||||
|
# Pattern for SQL batch commands
|
||||||
|
/;\s*(?:drop|alter|create|truncate|rename|insert|update|delete)/i
|
||||||
|
|
||||||
|
# Remote file inclusion patterns
|
||||||
|
# Pattern for external URL inclusion
|
||||||
|
/(?:https?|ftp|php|data|file):\/\/[^\s\n"')>]+/i
|
||||||
|
# Pattern for directory traversal
|
||||||
|
/(?:\.\.\/|\.\.\\|\.\.\%2f|\.\.\%5c)[^\s\n"')>]+/i
|
||||||
|
# Pattern for PHP wrapper usage
|
||||||
|
/php:\/\/(?:filter|input|memory|output|temp)/i
|
||||||
|
|
||||||
|
# Command injection patterns
|
||||||
|
# Pattern for shell command execution
|
||||||
|
/[;&|`]\s*(?:ls|cat|cd|pwd|echo|rm|cp|mv|sudo|chmod|chown|wget|curl)/i
|
||||||
|
# Pattern for command substitution
|
||||||
|
/\$\([^\)]*\)|`[^`]*`/i
|
||||||
|
# Pattern for direct system command injection
|
||||||
|
/system\s*\(|exec\s*\(|shell_exec\s*\(|passthru\s*\(|eval\s*\(/i
|
||||||
|
|
||||||
|
# Local file inclusion patterns
|
||||||
|
# Pattern for path traversal
|
||||||
|
/(?:\/|\\|\.\.|%2f|%5c)(?:etc|bin|usr|home|var|root|windows|system32)/i
|
||||||
|
# Pattern for sensitive file access
|
||||||
|
/(?:\/|\\|\.\.|%2f|%5c)(?:passwd|shadow|hosts|config|wp-config|web\.config)/i
|
||||||
|
|
||||||
|
# XML/XXE injection patterns
|
||||||
|
/<!(?:DOCTYPE|ENTITY)[\s\S]*?(?:SYSTEM|PUBLIC)[\s\S]*?["']/i
|
||||||
|
|
||||||
|
# CSRF token extraction
|
||||||
|
/(?:csrf|xsrf|token|auth)["']?\s*[:=]\s*["']?[a-zA-Z0-9_-]+/i
|
||||||
|
|
||||||
|
# Serialization attacks
|
||||||
|
/[ORCo]:[0-9]+:/i
|
||||||
|
|
||||||
|
# General suspicious patterns
|
||||||
|
# Pattern for base64 encoded payloads
|
||||||
|
/(?:[A-Za-z0-9+\/]{20,}={0,2})/
|
||||||
|
# Pattern for hex encoded payloads
|
||||||
|
/(?:0x[A-Fa-f0-9]{10,})/
|
||||||
|
# Pattern for URL encoded characters sequence
|
||||||
|
/(?:%[0-9A-Fa-f]{2}){8,}/
|
||||||
|
# Pattern for large number of special characters
|
||||||
|
/[!@#$%^&*()_+\-=\[\]{}|;':",./<>?]{10,}/
|
||||||
35
config/referrers.conf
Normal file
35
config/referrers.conf
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Known spam or malicious referrers from .htaccess
|
||||||
|
free-social-buttions.com
|
||||||
|
best-seo-offer.com
|
||||||
|
buttons-for-your-website.com
|
||||||
|
www1.free-social-buttons.com
|
||||||
|
www2.free-social-buttons.com
|
||||||
|
www3.free-social-buttons.com
|
||||||
|
100dollars-seo.com.com
|
||||||
|
anonymizeme.pro
|
||||||
|
site.ru
|
||||||
|
www4.free-social-buttons.com
|
||||||
|
free-social-buttons.com
|
||||||
|
buttons-for-website.com
|
||||||
|
social-buttons.com
|
||||||
|
anticrawler.org
|
||||||
|
blackhatworth.com
|
||||||
|
best-seo-offer.com
|
||||||
|
buttons-for-your-website.com
|
||||||
|
best-seo-solution.com
|
||||||
|
adcash.com
|
||||||
|
darodar.com
|
||||||
|
priceg.com
|
||||||
|
hulfingtonpost.com
|
||||||
|
gobongo.info
|
||||||
|
slftsdybbg.ru
|
||||||
|
ilovevitaly.com
|
||||||
|
ilovevitaly.co
|
||||||
|
ilovevitaly.ru
|
||||||
|
webmonetizer.net
|
||||||
|
make-money-online.com
|
||||||
|
cenoval.ru
|
||||||
|
o-o-6-o-o.com
|
||||||
|
7makemoneyonline.com
|
||||||
|
semalt.com
|
||||||
|
keywords-monitoring-success.com
|
||||||
862
includes/class-itk-admin.php
Normal file
862
includes/class-itk-admin.php
Normal file
@@ -0,0 +1,862 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Admin
|
||||||
|
*
|
||||||
|
* Single admin page with tabs:
|
||||||
|
* Dashboard | Bot Blocker | Protection | Optimization | Honeypot | Bot Logs | Honeypot Logs | Config Files
|
||||||
|
*/
|
||||||
|
class ITK_Admin {
|
||||||
|
|
||||||
|
const MENU_SLUG = 'informatiq-toolkit';
|
||||||
|
const NONCE_ACTION = 'itk_admin';
|
||||||
|
const PER_PAGE = 25;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
add_action('admin_menu', [$this, 'add_menu']);
|
||||||
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
|
||||||
|
add_action('admin_init', [$this, 'handle_actions']);
|
||||||
|
add_action('wp_ajax_itk_save_setting', [$this, 'ajax_save_setting']);
|
||||||
|
add_action('wp_ajax_itk_save_config_file',[$this, 'ajax_save_config_file']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_menu(): void {
|
||||||
|
add_options_page(
|
||||||
|
'InformatiQ Toolkit',
|
||||||
|
'InformatiQ Toolkit',
|
||||||
|
'manage_options',
|
||||||
|
self::MENU_SLUG,
|
||||||
|
[$this, 'render_page']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enqueue_assets(string $hook): void {
|
||||||
|
if ($hook !== 'settings_page_' . self::MENU_SLUG) return;
|
||||||
|
|
||||||
|
wp_enqueue_style('itk-admin', ITK_URL . 'assets/css/admin.css', [], ITK_VERSION);
|
||||||
|
wp_enqueue_script('itk-admin', ITK_URL . 'assets/js/admin.js', ['jquery'], ITK_VERSION, true);
|
||||||
|
wp_localize_script('itk-admin', 'itkAdmin', [
|
||||||
|
'nonce' => wp_create_nonce(self::NONCE_ACTION),
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Actions (form submissions) ───────────────────────────── */
|
||||||
|
|
||||||
|
public function handle_actions(): void {
|
||||||
|
if (!isset($_POST['itk_action']) || !check_admin_referer(self::NONCE_ACTION)) return;
|
||||||
|
if (!current_user_can('manage_options')) wp_die('Unauthorized');
|
||||||
|
|
||||||
|
switch ($_POST['itk_action']) {
|
||||||
|
case 'clear_bot_log':
|
||||||
|
ITK_Database::clear_bot_log();
|
||||||
|
$this->redirect(['tab' => 'bot-logs', 'cleared' => 1]);
|
||||||
|
break;
|
||||||
|
case 'clear_honeypot_log':
|
||||||
|
ITK_Database::clear_honeypot_log();
|
||||||
|
$this->redirect(['tab' => 'honeypot-logs', 'cleared' => 1]);
|
||||||
|
break;
|
||||||
|
case 'save_settings_security':
|
||||||
|
$this->save_settings_form('itk_security', [
|
||||||
|
'response_code', 'redirect_url', 'custom_message',
|
||||||
|
], [
|
||||||
|
'log_blocked_attempts',
|
||||||
|
]);
|
||||||
|
$this->redirect(['tab' => 'bot-blocker', 'saved' => 1]);
|
||||||
|
break;
|
||||||
|
case 'save_settings_login':
|
||||||
|
$this->save_settings_form('itk_security', [
|
||||||
|
'custom_login_slug',
|
||||||
|
], [
|
||||||
|
'enable_custom_login',
|
||||||
|
]);
|
||||||
|
$this->redirect(['tab' => 'protection', 'saved' => 1]);
|
||||||
|
break;
|
||||||
|
case 'save_settings_honeypot':
|
||||||
|
$this->save_settings_form('itk_honeypot', [
|
||||||
|
'min_time', 'max_time', 'retain_days',
|
||||||
|
], []);
|
||||||
|
$this->redirect(['tab' => 'honeypot', 'saved' => 1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a subset of fields from $_POST['itk_*'] into a WP option.
|
||||||
|
* $text_fields = scalar fields (sanitize_text_field)
|
||||||
|
* $toggle_fields = checkbox fields (0 or 1)
|
||||||
|
*/
|
||||||
|
private function save_settings_form(string $option, array $text_fields, array $toggle_fields): void {
|
||||||
|
$opts = get_option($option, []);
|
||||||
|
$posted = $_POST[$option] ?? [];
|
||||||
|
|
||||||
|
foreach ($text_fields as $key) {
|
||||||
|
if (isset($posted[$key])) {
|
||||||
|
$opts[$key] = sanitize_text_field(wp_unslash($posted[$key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($toggle_fields as $key) {
|
||||||
|
$opts[$key] = !empty($posted[$key]) ? 1 : 0;
|
||||||
|
}
|
||||||
|
update_option($option, $opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function redirect(array $args): void {
|
||||||
|
wp_redirect(add_query_arg(array_merge(['page' => self::MENU_SLUG], $args), admin_url('options-general.php')));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── AJAX: save single toggle setting ─────────────────────── */
|
||||||
|
|
||||||
|
public function ajax_save_setting(): void {
|
||||||
|
check_ajax_referer(self::NONCE_ACTION, 'nonce');
|
||||||
|
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
|
||||||
|
|
||||||
|
$option = sanitize_key($_POST['option'] ?? '');
|
||||||
|
$setting = sanitize_key($_POST['setting'] ?? '');
|
||||||
|
$value = (int)($_POST['value'] ?? 0);
|
||||||
|
|
||||||
|
$allowed = ['itk_security','itk_optimization','itk_honeypot'];
|
||||||
|
if (!in_array($option, $allowed, true) || empty($setting)) {
|
||||||
|
wp_send_json_error('invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
$opts = get_option($option, []);
|
||||||
|
$opts[$setting] = $value;
|
||||||
|
update_option($option, $opts);
|
||||||
|
|
||||||
|
wp_send_json_success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── AJAX: save config file ───────────────────────────────── */
|
||||||
|
|
||||||
|
public function ajax_save_config_file(): void {
|
||||||
|
check_ajax_referer(self::NONCE_ACTION, 'nonce');
|
||||||
|
if (!current_user_can('manage_options')) wp_send_json_error('unauthorized');
|
||||||
|
|
||||||
|
$file = sanitize_key($_POST['file'] ?? '');
|
||||||
|
$content = wp_unslash($_POST['content'] ?? '');
|
||||||
|
|
||||||
|
$allowed = [
|
||||||
|
'badbots' => ITK_PATH . 'config/badbots.conf',
|
||||||
|
'goodbots' => ITK_PATH . 'config/goodbots.conf',
|
||||||
|
'referrers' => ITK_PATH . 'config/referrers.conf',
|
||||||
|
'networks' => ITK_PATH . 'config/networks.conf',
|
||||||
|
'allowed-ips'=> ITK_PATH . 'config/allowed-ips.conf',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($allowed[$file])) wp_send_json_error('invalid file');
|
||||||
|
|
||||||
|
$result = file_put_contents($allowed[$file], sanitize_textarea_field($content));
|
||||||
|
if ($result === false) {
|
||||||
|
wp_send_json_error('write failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear transient caches
|
||||||
|
delete_transient('itk_bots_list');
|
||||||
|
delete_transient('itk_referrers_list');
|
||||||
|
delete_transient('itk_networks_list');
|
||||||
|
delete_transient('itk_goodbots_list');
|
||||||
|
|
||||||
|
wp_send_json_success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main page render ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function render_page(): void {
|
||||||
|
if (!current_user_can('manage_options')) return;
|
||||||
|
$tab = sanitize_key($_GET['tab'] ?? 'dashboard');
|
||||||
|
?>
|
||||||
|
<div class="wrap itk-wrap">
|
||||||
|
<h1 class="itk-page-title">
|
||||||
|
<span class="itk-logo">IQ</span> InformatiQ Toolkit
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<?php if (!empty($_GET['cleared'])): ?>
|
||||||
|
<div class="notice notice-success is-dismissible"><p>Logs cleared successfully.</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($_GET['saved'])): ?>
|
||||||
|
<div class="notice notice-success is-dismissible"><p>Settings saved.</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<nav class="itk-tabs">
|
||||||
|
<?php
|
||||||
|
$tabs = [
|
||||||
|
'dashboard' => 'Dashboard',
|
||||||
|
'bot-blocker' => 'Bot Blocker',
|
||||||
|
'protection' => 'Protection',
|
||||||
|
'optimization' => 'Optimization',
|
||||||
|
'honeypot' => 'Honeypot',
|
||||||
|
'bot-logs' => 'Bot Logs',
|
||||||
|
'honeypot-logs' => 'Honeypot Logs',
|
||||||
|
'config-files' => 'Config Files',
|
||||||
|
];
|
||||||
|
foreach ($tabs as $slug => $label):
|
||||||
|
$active = $tab === $slug ? 'itk-tab-active' : '';
|
||||||
|
$url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=' . $slug);
|
||||||
|
?>
|
||||||
|
<a href="<?= esc_url($url) ?>" class="itk-tab <?= $active ?>"><?= esc_html($label) ?></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="itk-tab-content">
|
||||||
|
<?php
|
||||||
|
match ($tab) {
|
||||||
|
'dashboard' => $this->tab_dashboard(),
|
||||||
|
'bot-blocker' => $this->tab_bot_blocker(),
|
||||||
|
'protection' => $this->tab_protection(),
|
||||||
|
'optimization' => $this->tab_optimization(),
|
||||||
|
'honeypot' => $this->tab_honeypot(),
|
||||||
|
'bot-logs' => $this->tab_bot_logs(),
|
||||||
|
'honeypot-logs' => $this->tab_honeypot_logs(),
|
||||||
|
'config-files' => $this->tab_config_files(),
|
||||||
|
default => $this->tab_dashboard(),
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: DASHBOARD
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_dashboard(): void {
|
||||||
|
$bot_stats = ITK_Database::get_bot_stats();
|
||||||
|
$hp_stats = ITK_Database::get_honeypot_stats();
|
||||||
|
?>
|
||||||
|
<div class="itk-dashboard">
|
||||||
|
|
||||||
|
<!-- ── Bot Activity Panel ── -->
|
||||||
|
<section class="itk-monitor-panel">
|
||||||
|
<div class="itk-monitor-header">
|
||||||
|
<span class="itk-monitor-title">■ BOT ACTIVITY MONITOR</span>
|
||||||
|
<span class="itk-monitor-blink">LIVE</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="itk-stat-row">
|
||||||
|
<div class="itk-stat-card">
|
||||||
|
<div class="itk-stat-num"><?= number_format($bot_stats['total']) ?></div>
|
||||||
|
<div class="itk-stat-lbl">Total Blocked</div>
|
||||||
|
</div>
|
||||||
|
<div class="itk-stat-card">
|
||||||
|
<div class="itk-stat-num itk-green"><?= number_format($bot_stats['today']) ?></div>
|
||||||
|
<div class="itk-stat-lbl">Today</div>
|
||||||
|
</div>
|
||||||
|
<div class="itk-stat-card">
|
||||||
|
<div class="itk-stat-num itk-yellow"><?= number_format($bot_stats['rate_limited']) ?></div>
|
||||||
|
<div class="itk-stat-lbl">Rate Limited</div>
|
||||||
|
</div>
|
||||||
|
<div class="itk-stat-card">
|
||||||
|
<div class="itk-stat-num"><?= number_format($hp_stats['total']) ?></div>
|
||||||
|
<div class="itk-stat-lbl">Honeypot Catches</div>
|
||||||
|
</div>
|
||||||
|
<div class="itk-stat-card">
|
||||||
|
<div class="itk-stat-num itk-green"><?= number_format($hp_stats['today']) ?></div>
|
||||||
|
<div class="itk-stat-lbl">Honeypot Today</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top bot types bar chart -->
|
||||||
|
<?php if (!empty($bot_stats['top_bot_types'])): ?>
|
||||||
|
<div class="itk-chart-section">
|
||||||
|
<div class="itk-chart-title">TOP THREAT SOURCES</div>
|
||||||
|
<div class="itk-bar-chart">
|
||||||
|
<?php
|
||||||
|
$max = max(1, (int)$bot_stats['top_bot_types'][0]->cnt);
|
||||||
|
foreach ($bot_stats['top_bot_types'] as $row):
|
||||||
|
$pct = round(($row->cnt / $max) * 100);
|
||||||
|
?>
|
||||||
|
<div class="itk-bar-row">
|
||||||
|
<span class="itk-bar-label"><?= esc_html($row->bot_type ?: 'Unknown') ?></span>
|
||||||
|
<div class="itk-bar-track">
|
||||||
|
<div class="itk-bar-fill" style="width:<?= $pct ?>%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="itk-bar-count"><?= number_format($row->cnt) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Top honeypot form types -->
|
||||||
|
<?php if (!empty($hp_stats['top_forms'])): ?>
|
||||||
|
<div class="itk-chart-section">
|
||||||
|
<div class="itk-chart-title">HONEYPOT – TOP TARGETED FORMS</div>
|
||||||
|
<div class="itk-bar-chart">
|
||||||
|
<?php
|
||||||
|
$max = max(1, (int)$hp_stats['top_forms'][0]->cnt);
|
||||||
|
foreach ($hp_stats['top_forms'] as $row):
|
||||||
|
$pct = round(($row->cnt / $max) * 100);
|
||||||
|
?>
|
||||||
|
<div class="itk-bar-row">
|
||||||
|
<span class="itk-bar-label"><?= esc_html($row->form_type) ?></span>
|
||||||
|
<div class="itk-bar-track">
|
||||||
|
<div class="itk-bar-fill itk-bar-hp" style="width:<?= $pct ?>%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="itk-bar-count"><?= number_format($row->cnt) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Top IPs -->
|
||||||
|
<?php if (!empty($bot_stats['top_ips'])): ?>
|
||||||
|
<div class="itk-chart-section">
|
||||||
|
<div class="itk-chart-title">TOP OFFENDER IPs</div>
|
||||||
|
<table class="itk-mini-table">
|
||||||
|
<tr><th>IP Address</th><th>Hits</th></tr>
|
||||||
|
<?php foreach ($bot_stats['top_ips'] as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="<?= esc_url(admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=bot-logs&hp_ip=' . urlencode($row->ip_address))) ?>"><?= esc_html($row->ip_address) ?></a>
|
||||||
|
<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-lookup">[lookup]</a>
|
||||||
|
</td>
|
||||||
|
<td><?= number_format($row->cnt) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Quick Settings Status ── -->
|
||||||
|
<section class="itk-quick-status">
|
||||||
|
<div class="itk-chart-title">ACTIVE MODULES</div>
|
||||||
|
<?php
|
||||||
|
$sec = get_option('itk_security', []);
|
||||||
|
$opt = get_option('itk_optimization', []);
|
||||||
|
$hp = get_option('itk_honeypot', []);
|
||||||
|
$modules = [
|
||||||
|
'Bot Blocker' => !empty($sec['block_malicious_bots']),
|
||||||
|
'OpenAI Block' => !empty($sec['block_openai_bots']),
|
||||||
|
'Good Bot Rate Limit'=> !empty($sec['rate_limit_good_bots']),
|
||||||
|
'Network Block' => !empty($sec['block_bad_networks']),
|
||||||
|
'Login Protection' => !empty($sec['protect_wp_login']),
|
||||||
|
'Security Headers' => !empty($sec['add_security_headers']),
|
||||||
|
'Block XML-RPC' => !empty($sec['block_xmlrpc']),
|
||||||
|
'Honeypot' => !empty($hp['enabled']),
|
||||||
|
'Remove WP Version' => !empty($opt['remove_wp_version']),
|
||||||
|
'Disable Emoji' => !empty($opt['remove_emoji']),
|
||||||
|
'Admin Branding' => !empty($opt['admin_branding']),
|
||||||
|
];
|
||||||
|
foreach ($modules as $label => $active):
|
||||||
|
?>
|
||||||
|
<div class="itk-module-row">
|
||||||
|
<span class="itk-module-dot <?= $active ? 'itk-dot-on' : 'itk-dot-off' ?>"></span>
|
||||||
|
<span class="itk-module-label"><?= esc_html($label) ?></span>
|
||||||
|
<span class="itk-module-status"><?= $active ? 'ACTIVE' : 'off' ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: BOT BLOCKER
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_bot_blocker(): void {
|
||||||
|
$opts = get_option('itk_security', []);
|
||||||
|
?>
|
||||||
|
<div class="itk-settings-grid">
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Bot Blocking</h2>
|
||||||
|
<?php
|
||||||
|
$toggles = [
|
||||||
|
'block_openai_bots' => ['OpenAI / GPT Bots', 'Block GPTBot, ChatGPT-User, OAI-SearchBot'],
|
||||||
|
'block_malicious_bots' => ['Malicious Bots', 'Block bots listed in badbots.conf'],
|
||||||
|
'block_bad_referrers' => ['Bad Referrers', 'Block requests from spam referrer domains'],
|
||||||
|
'block_bad_networks' => ['Bad Networks', 'Block IP ranges listed in networks.conf'],
|
||||||
|
'rate_limit_good_bots' => ['Rate-Limit Good Bots', 'Apply crawl-rate limits to Googlebot, Bingbot, etc. (configurable in goodbots.conf)'],
|
||||||
|
];
|
||||||
|
foreach ($toggles as $key => [$label, $desc]):
|
||||||
|
$this->render_toggle('itk_security', $key, $label, $desc, $opts);
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Response Settings</h2>
|
||||||
|
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=bot-blocker">
|
||||||
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
|
<input type="hidden" name="itk_action" value="save_settings_security">
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th>Response Code</th>
|
||||||
|
<td>
|
||||||
|
<select name="itk_security[response_code]">
|
||||||
|
<?php
|
||||||
|
$codes = ['301_custom' => '301 – Redirect to custom URL', '403' => '403 Forbidden', '410' => '410 Gone', '503' => '503 Service Unavailable'];
|
||||||
|
$cur = $opts['response_code'] ?? '301_custom';
|
||||||
|
foreach ($codes as $val => $lbl):
|
||||||
|
?>
|
||||||
|
<option value="<?= esc_attr($val) ?>" <?= selected($cur, $val, false) ?>><?= esc_html($lbl) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Redirect URL</th>
|
||||||
|
<td><input type="url" name="itk_security[redirect_url]" value="<?= esc_attr($opts['redirect_url'] ?? '') ?>" class="regular-text"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Custom Message</th>
|
||||||
|
<td><input type="text" name="itk_security[custom_message]" value="<?= esc_attr($opts['custom_message'] ?? 'Access denied.') ?>" class="regular-text"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Log Blocked Attempts</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="itk_security[log_blocked_attempts]" value="1" <?= checked(!empty($opts['log_blocked_attempts'])) ?>>
|
||||||
|
Log all blocked attempts to the database
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php submit_button('Save Response Settings'); ?>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: PROTECTION
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_protection(): void {
|
||||||
|
$opts = get_option('itk_security', []);
|
||||||
|
?>
|
||||||
|
<div class="itk-settings-grid">
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>WordPress Protection</h2>
|
||||||
|
<?php
|
||||||
|
$toggles = [
|
||||||
|
'protect_wp_login' => ['WP Login IP Whitelist', 'Restrict wp-login.php to IPs in allowed-ips.conf'],
|
||||||
|
'add_security_headers' => ['Security Headers', 'Add X-Content-Type-Options, X-Frame-Options, X-XSS-Protection headers'],
|
||||||
|
'protect_wp_includes' => ['Protect WP Core Files', 'Block direct access to wp-includes, wp-admin/includes'],
|
||||||
|
'protect_uploads' => ['Block PHP in Uploads', 'Deny PHP file access and uploads in /wp-content/uploads/'],
|
||||||
|
'block_xmlrpc' => ['Block XML-RPC', 'Deny all access to xmlrpc.php'],
|
||||||
|
'block_malicious_queries' => ['Block Malicious Queries', 'Detect and block SQLi, XSS, and command injection in query strings'],
|
||||||
|
'block_author_scans' => ['Block Author Scans', 'Redirect ?author=N requests to prevent username enumeration'],
|
||||||
|
];
|
||||||
|
foreach ($toggles as $key => [$label, $desc]):
|
||||||
|
$this->render_toggle('itk_security', $key, $label, $desc, $opts);
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Custom Login URL</h2>
|
||||||
|
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=protection">
|
||||||
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
|
<input type="hidden" name="itk_action" value="save_settings_login">
|
||||||
|
|
||||||
|
<?php $this->render_toggle('itk_security', 'enable_custom_login', 'Enable Custom Login URL', 'Replace /wp-login.php with a custom slug', $opts); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th>Login Slug</th>
|
||||||
|
<td>
|
||||||
|
<code><?= esc_html(home_url('/')) ?></code>
|
||||||
|
<input type="text" name="itk_security[custom_login_slug]" value="<?= esc_attr($opts['custom_login_slug'] ?? 'thoushallpass') ?>" style="width:200px">
|
||||||
|
<p class="description">Characters: letters, numbers, dashes only.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php submit_button('Save Login Settings'); ?>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: OPTIMIZATION
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_optimization(): void {
|
||||||
|
$opts = get_option('itk_optimization', []);
|
||||||
|
?>
|
||||||
|
<div class="itk-settings-grid">
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Performance & Cleanup</h2>
|
||||||
|
<?php
|
||||||
|
$toggles = [
|
||||||
|
'remove_wp_version' => ['Remove WP Version', 'Strip WordPress version from <head> and all enqueued assets'],
|
||||||
|
'remove_script_versions' => ['Remove Asset Versions', 'Remove ?ver= query string from CSS/JS URLs'],
|
||||||
|
'remove_emoji' => ['Remove Emojis', 'Disable WordPress emoji scripts and styles'],
|
||||||
|
'deregister_wp_embed' => ['Remove WP Embed', 'Deregister the wp-embed script'],
|
||||||
|
'remove_wp_head_noise' => ['Clean WP Head', 'Remove RSD, wlwmanifest, feed links, and adjacent post links from <head>'],
|
||||||
|
'limit_revisions' => ['Limit Revisions', 'Keep only 3 post revisions and autosave every 5 minutes'],
|
||||||
|
'defer_js' => ['Defer JavaScript', 'Add defer attribute to non-critical scripts'],
|
||||||
|
'limit_heartbeat' => ['Limit Heartbeat', 'Restrict WordPress heartbeat to post editor pages only'],
|
||||||
|
'stop_empty_search_redirect'=> ['Fix Empty Search', 'Prevent redirect loop on empty search queries'],
|
||||||
|
'use_google_jquery' => ['Use Google jQuery', 'Load jQuery from Google CDN instead of local'],
|
||||||
|
'dns_prefetch' => ['DNS Prefetch', 'Enable DNS prefetching via meta header'],
|
||||||
|
];
|
||||||
|
foreach ($toggles as $key => [$label, $desc]):
|
||||||
|
$this->render_toggle('itk_optimization', $key, $label, $desc, $opts);
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Security Tweaks</h2>
|
||||||
|
<?php
|
||||||
|
$toggles = [
|
||||||
|
'hide_login_errors' => ['Hide Login Errors', 'Replace specific login error messages with a generic one'],
|
||||||
|
'remove_author_class' => ['Remove Author Class', 'Strip admin username from comment CSS classes'],
|
||||||
|
'remove_default_userfields'=> ['Remove User Fields', 'Remove AIM, Jabber, YIM from user profiles'],
|
||||||
|
'clean_bad_content' => ['Clean Bad Content', 'Remove empty tags, inline styles, and font tags on save'],
|
||||||
|
'change_author_base' => ['Change Author URL Base', "Change /author/ to /writer/ in author archive URLs"],
|
||||||
|
'disable_xml_rpc' => ['Disable XML-RPC (via filter)', 'Filter-based XML-RPC disable (additional to the blocker)'],
|
||||||
|
];
|
||||||
|
foreach ($toggles as $key => [$label, $desc]):
|
||||||
|
$this->render_toggle('itk_optimization', $key, $label, $desc, $opts);
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>UI / Branding</h2>
|
||||||
|
<?php
|
||||||
|
$toggles = [
|
||||||
|
'disable_dashboard_widgets' => ['Disable Core Widgets', 'Remove WordPress News and WPEngine news dashboard widgets'],
|
||||||
|
'unregister_default_widgets'=> ['Unregister Sidebar Widgets', 'Remove Calendar, Archives, Meta, Search, Tag Cloud widgets'],
|
||||||
|
'disable_comments_url' => ['Remove Comment URL Field', 'Hide the website URL field from comment forms'],
|
||||||
|
'remove_admin_bar_links' => ['Clean Admin Bar', 'Remove WordPress logo, links, and noisy items from the toolbar'],
|
||||||
|
'admin_branding' => ['InformatiQ Branding', 'Add InformatiQ logo widget, toolbar link, admin notice, and custom footer'],
|
||||||
|
'disable_floc' => ['Disable FLoC', 'Add Permissions-Policy: interest-cohort=() header'],
|
||||||
|
'lightbox_images' => ['Lightbox Images', 'Add rel="lightbox" to image links in post content'],
|
||||||
|
'featured_image_rss' => ['Featured Image in RSS', 'Include featured image in RSS feed entries'],
|
||||||
|
];
|
||||||
|
foreach ($toggles as $key => [$label, $desc]):
|
||||||
|
$this->render_toggle('itk_optimization', $key, $label, $desc, $opts);
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: HONEYPOT
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_honeypot(): void {
|
||||||
|
$opts = get_option('itk_honeypot', []);
|
||||||
|
?>
|
||||||
|
<div class="itk-settings-grid">
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Honeypot Fields</h2>
|
||||||
|
<?php
|
||||||
|
$toggles = [
|
||||||
|
'enabled' => ['Enable Honeypot', 'Inject invisible honeypot fields into all protected forms'],
|
||||||
|
'protect_comments' => ['Comments', 'Protect comment forms'],
|
||||||
|
'protect_login' => ['Login Form', 'Protect wp-login.php login'],
|
||||||
|
'protect_register' => ['Registration', 'Protect user registration'],
|
||||||
|
'protect_lost_password' => ['Lost Password', 'Protect lost password form'],
|
||||||
|
'protect_woocommerce' => ['WooCommerce', 'Protect WooCommerce checkout and registration'],
|
||||||
|
'protect_cf7' => ['Contact Form 7', 'Protect CF7 forms'],
|
||||||
|
'protect_elementor' => ['Elementor Forms', 'Protect Elementor Pro form widget'],
|
||||||
|
'protect_gravity' => ['Gravity Forms', 'Protect Gravity Forms'],
|
||||||
|
'protect_search' => ['Search Form', 'Protect the WordPress search form'],
|
||||||
|
];
|
||||||
|
foreach ($toggles as $key => [$label, $desc]):
|
||||||
|
$this->render_toggle('itk_honeypot', $key, $label, $desc, $opts);
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="itk-card">
|
||||||
|
<h2>Timing Rules</h2>
|
||||||
|
<form method="post" action="options-general.php?page=<?= self::MENU_SLUG ?>&tab=honeypot">
|
||||||
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
|
<input type="hidden" name="itk_action" value="save_settings_honeypot">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th>Minimum Submit Time (seconds)</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="itk_honeypot[min_time]" value="<?= (int)($opts['min_time'] ?? 3) ?>" min="1" max="60">
|
||||||
|
<p class="description">Block submissions faster than this (bots submit instantly).</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Maximum Submit Time (seconds)</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="itk_honeypot[max_time]" value="<?= (int)($opts['max_time'] ?? 7200) ?>" min="60">
|
||||||
|
<p class="description">Block submissions older than this (stale/replayed forms).</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Retain Logs (days)</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="itk_honeypot[retain_days]" value="<?= (int)($opts['retain_days'] ?? 90) ?>" min="1">
|
||||||
|
<p class="description">Automatically prune logs older than this many days.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php submit_button('Save Honeypot Settings'); ?>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: BOT LOGS
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_bot_logs(): void {
|
||||||
|
$search = sanitize_text_field($_GET['hp_search'] ?? '');
|
||||||
|
$filter_ip= sanitize_text_field($_GET['hp_ip'] ?? '');
|
||||||
|
$filter_bt= sanitize_text_field($_GET['hp_bot'] ?? '');
|
||||||
|
$filter_ac= sanitize_key($_GET['hp_action'] ?? '');
|
||||||
|
$paged = max(1, (int)($_GET['paged'] ?? 1));
|
||||||
|
$offset = ($paged - 1) * self::PER_PAGE;
|
||||||
|
|
||||||
|
$args = ['per_page' => self::PER_PAGE, 'offset' => $offset];
|
||||||
|
if ($search) $args['search'] = $search;
|
||||||
|
if ($filter_ip) $args['ip'] = $filter_ip;
|
||||||
|
if ($filter_bt) $args['bot_type'] = $filter_bt;
|
||||||
|
if ($filter_ac) $args['action'] = $filter_ac;
|
||||||
|
|
||||||
|
$rows = ITK_Database::get_bot_rows($args);
|
||||||
|
$total = ITK_Database::count_bot_rows($args);
|
||||||
|
$bot_types = ITK_Database::get_bot_types();
|
||||||
|
$total_pages= max(1, (int)ceil($total / self::PER_PAGE));
|
||||||
|
|
||||||
|
$base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=bot-logs');
|
||||||
|
?>
|
||||||
|
<div class="itk-log-page">
|
||||||
|
<!-- Filters -->
|
||||||
|
<form method="get" class="itk-filters">
|
||||||
|
<input type="hidden" name="page" value="<?= self::MENU_SLUG ?>">
|
||||||
|
<input type="hidden" name="tab" value="bot-logs">
|
||||||
|
<input type="text" name="hp_search" placeholder="Search IP, UA, reason…" value="<?= esc_attr($search) ?>">
|
||||||
|
<input type="text" name="hp_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
|
||||||
|
<select name="hp_bot">
|
||||||
|
<option value="">All bot types</option>
|
||||||
|
<?php foreach ($bot_types as $bt): ?>
|
||||||
|
<option value="<?= esc_attr($bt) ?>" <?= selected($filter_bt, $bt, false) ?>><?= esc_html($bt) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<select name="hp_action">
|
||||||
|
<option value="">All actions</option>
|
||||||
|
<option value="blocked" <?= selected($filter_ac, 'blocked', false) ?>>Blocked</option>
|
||||||
|
<option value="rate_limited" <?= selected($filter_ac, 'rate_limited', false) ?>>Rate Limited</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" class="button" value="Filter">
|
||||||
|
<a href="<?= esc_url($base_url) ?>" class="button">Reset</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="itk-count">Showing <?= count($rows) ?> of <?= number_format($total) ?> result(s)</p>
|
||||||
|
|
||||||
|
<table class="itk-log-table widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date / Time</th><th>IP</th><th>Bot Type</th>
|
||||||
|
<th>Action</th><th>Reason</th><th>URI</th><th>User Agent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($rows)): ?>
|
||||||
|
<tr><td colspan="7" class="itk-no-results">No bot activity logged yet.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($rows as $row):
|
||||||
|
$action_class = $row->action === 'rate_limited' ? 'itk-badge-warn' : 'itk-badge-block';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="itk-nowrap"><?= esc_html($row->logged_at) ?></td>
|
||||||
|
<td>
|
||||||
|
<?= esc_html($row->ip_address) ?>
|
||||||
|
<a href="<?= esc_url($base_url . '&hp_ip=' . urlencode($row->ip_address)) ?>" class="itk-filter-link">[filter]</a>
|
||||||
|
<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-filter-link">[lookup]</a>
|
||||||
|
</td>
|
||||||
|
<td><?= esc_html($row->bot_type) ?></td>
|
||||||
|
<td><span class="itk-badge <?= $action_class ?>"><?= esc_html($row->action) ?></span></td>
|
||||||
|
<td><?= esc_html($row->reason) ?></td>
|
||||||
|
<td class="itk-uri"><?= esc_html(substr($row->request_uri, 0, 80)) ?></td>
|
||||||
|
<td class="itk-ua"><?= esc_html(substr($row->user_agent, 0, 100)) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php $this->render_pager($paged, $total_pages, $base_url); ?>
|
||||||
|
|
||||||
|
<!-- Clear logs -->
|
||||||
|
<form method="post" style="margin-top:16px" onsubmit="return confirm('Delete ALL bot log entries?')">
|
||||||
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
|
<input type="hidden" name="itk_action" value="clear_bot_log">
|
||||||
|
<input type="submit" class="button button-secondary itk-btn-danger" value="Clear All Bot Logs">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: HONEYPOT LOGS
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_honeypot_logs(): void {
|
||||||
|
$search = sanitize_text_field($_GET['hp_search'] ?? '');
|
||||||
|
$filter_ip = sanitize_text_field($_GET['hp_ip'] ?? '');
|
||||||
|
$filter_form = sanitize_text_field($_GET['hp_form'] ?? '');
|
||||||
|
$paged = max(1, (int)($_GET['paged'] ?? 1));
|
||||||
|
$offset = ($paged - 1) * self::PER_PAGE;
|
||||||
|
|
||||||
|
$args = ['per_page' => self::PER_PAGE, 'offset' => $offset];
|
||||||
|
if ($search) $args['search'] = $search;
|
||||||
|
if ($filter_ip) $args['ip'] = $filter_ip;
|
||||||
|
if ($filter_form) $args['form'] = $filter_form;
|
||||||
|
|
||||||
|
$rows = ITK_Database::get_honeypot_rows($args);
|
||||||
|
$total = ITK_Database::count_honeypot_rows($args);
|
||||||
|
$form_types = ITK_Database::get_honeypot_form_types();
|
||||||
|
$total_pages = max(1, (int)ceil($total / self::PER_PAGE));
|
||||||
|
|
||||||
|
$base_url = admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=honeypot-logs');
|
||||||
|
?>
|
||||||
|
<div class="itk-log-page">
|
||||||
|
<form method="get" class="itk-filters">
|
||||||
|
<input type="hidden" name="page" value="<?= self::MENU_SLUG ?>">
|
||||||
|
<input type="hidden" name="tab" value="honeypot-logs">
|
||||||
|
<input type="text" name="hp_search" placeholder="Search IP, UA, reason…" value="<?= esc_attr($search) ?>">
|
||||||
|
<input type="text" name="hp_ip" placeholder="Filter by IP" value="<?= esc_attr($filter_ip) ?>">
|
||||||
|
<select name="hp_form">
|
||||||
|
<option value="">All form types</option>
|
||||||
|
<?php foreach ($form_types as $ft): ?>
|
||||||
|
<option value="<?= esc_attr($ft) ?>" <?= selected($filter_form, $ft, false) ?>><?= esc_html($ft) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<input type="submit" class="button" value="Filter">
|
||||||
|
<a href="<?= esc_url($base_url) ?>" class="button">Reset</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="itk-count">Showing <?= count($rows) ?> of <?= number_format($total) ?> result(s)</p>
|
||||||
|
|
||||||
|
<table class="itk-log-table widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date / Time</th><th>IP</th><th>Form Type</th>
|
||||||
|
<th>Reason</th><th>URI</th><th>User Agent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($rows)): ?>
|
||||||
|
<tr><td colspan="6" class="itk-no-results">No honeypot catches yet.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($rows as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="itk-nowrap"><?= esc_html($row->blocked_at) ?></td>
|
||||||
|
<td>
|
||||||
|
<?= esc_html($row->ip_address) ?>
|
||||||
|
<a href="<?= esc_url($base_url . '&hp_ip=' . urlencode($row->ip_address)) ?>" class="itk-filter-link">[filter]</a>
|
||||||
|
<a href="https://ipinfo.io/<?= urlencode($row->ip_address) ?>" target="_blank" class="itk-filter-link">[lookup]</a>
|
||||||
|
</td>
|
||||||
|
<td><span class="itk-badge itk-badge-hp"><?= esc_html($row->form_type) ?></span></td>
|
||||||
|
<td><?= esc_html($row->reason) ?></td>
|
||||||
|
<td class="itk-uri"><?= esc_html(substr($row->request_uri, 0, 80)) ?></td>
|
||||||
|
<td class="itk-ua"><?= esc_html(substr($row->user_agent, 0, 100)) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php $this->render_pager($paged, $total_pages, $base_url); ?>
|
||||||
|
|
||||||
|
<form method="post" style="margin-top:16px" onsubmit="return confirm('Delete ALL honeypot log entries?')">
|
||||||
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
|
<input type="hidden" name="itk_action" value="clear_honeypot_log">
|
||||||
|
<input type="submit" class="button button-secondary itk-btn-danger" value="Clear All Honeypot Logs">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════
|
||||||
|
* TAB: CONFIG FILES
|
||||||
|
* ══════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
private function tab_config_files(): void {
|
||||||
|
$files = [
|
||||||
|
'badbots' => ['Bad Bots', 'config/badbots.conf', 'One bot user-agent substring per line. Lines starting with # are comments.'],
|
||||||
|
'goodbots' => ['Good Bots', 'config/goodbots.conf', 'Format: BotName|rate_per_minute (0 = always block)'],
|
||||||
|
'referrers' => ['Bad Referrers', 'config/referrers.conf', 'One domain substring per line.'],
|
||||||
|
'networks' => ['Bad Networks', 'config/networks.conf', 'One IP or CIDR range per line (e.g. 1.2.3.0/24).'],
|
||||||
|
'allowed-ips' => ['Allowed IPs', 'config/allowed-ips.conf','IPs/CIDRs allowed to access wp-login.php (one per line).'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$active_file = sanitize_key($_GET['file'] ?? 'badbots');
|
||||||
|
if (!isset($files[$active_file])) $active_file = 'badbots';
|
||||||
|
[$title, $path, $desc] = $files[$active_file];
|
||||||
|
$full_path = ITK_PATH . $path;
|
||||||
|
$content = file_exists($full_path) ? file_get_contents($full_path) : '';
|
||||||
|
?>
|
||||||
|
<div class="itk-config-editor">
|
||||||
|
<div class="itk-config-tabs">
|
||||||
|
<?php foreach ($files as $slug => [$label]): ?>
|
||||||
|
<a href="<?= esc_url(admin_url('options-general.php?page=' . self::MENU_SLUG . '&tab=config-files&file=' . $slug)) ?>"
|
||||||
|
class="itk-config-tab <?= $slug === $active_file ? 'active' : '' ?>"><?= esc_html($label) ?></a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="itk-config-editor-area">
|
||||||
|
<h3><?= esc_html($title) ?> <code><?= esc_html($path) ?></code></h3>
|
||||||
|
<p class="description"><?= esc_html($desc) ?></p>
|
||||||
|
<textarea id="itk-config-content" rows="25" class="itk-config-textarea"><?= esc_textarea($content) ?></textarea>
|
||||||
|
<p>
|
||||||
|
<button id="itk-save-config" class="button button-primary" data-file="<?= esc_attr($active_file) ?>">Save File</button>
|
||||||
|
<span id="itk-config-status" style="margin-left:10px;color:green;display:none">Saved!</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Shared helpers ───────────────────────────────────────── */
|
||||||
|
|
||||||
|
private function render_toggle(string $option, string $key, string $label, string $desc, array $opts): void {
|
||||||
|
$checked = !empty($opts[$key]);
|
||||||
|
?>
|
||||||
|
<div class="itk-toggle-row">
|
||||||
|
<div class="itk-toggle-info">
|
||||||
|
<span class="itk-toggle-label"><?= esc_html($label) ?></span>
|
||||||
|
<span class="itk-toggle-desc"><?= esc_html($desc) ?></span>
|
||||||
|
</div>
|
||||||
|
<label class="itk-switch">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="itk-toggle-input"
|
||||||
|
data-option="<?= esc_attr($option) ?>"
|
||||||
|
data-setting="<?= esc_attr($key) ?>"
|
||||||
|
<?= $checked ? 'checked' : '' ?>>
|
||||||
|
<span class="itk-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render_pager(int $paged, int $total_pages, string $base_url): void {
|
||||||
|
if ($total_pages <= 1) return;
|
||||||
|
echo '<div class="itk-pager">';
|
||||||
|
if ($paged > 1) {
|
||||||
|
echo '<a href="' . esc_url(add_query_arg('paged', $paged - 1, $base_url)) . '" class="button">« Prev</a>';
|
||||||
|
}
|
||||||
|
echo '<span>' . sprintf('Page %d of %d', $paged, $total_pages) . '</span>';
|
||||||
|
if ($paged < $total_pages) {
|
||||||
|
echo '<a href="' . esc_url(add_query_arg('paged', $paged + 1, $base_url)) . '" class="button">Next »</a>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
305
includes/class-itk-bot-blocker.php
Normal file
305
includes/class-itk-bot-blocker.php
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Bot Blocker
|
||||||
|
*
|
||||||
|
* Handles detection and blocking of malicious bots, bad referrers, and bad
|
||||||
|
* networks. Good/legitimate bots are rate-limited instead of blocked.
|
||||||
|
*
|
||||||
|
* Deactivation bug fix: every check method reads options at call time so
|
||||||
|
* toggling a setting via AJAX takes effect immediately without any hook
|
||||||
|
* re-registration.
|
||||||
|
*/
|
||||||
|
class ITK_Bot_Blocker {
|
||||||
|
|
||||||
|
private string $badbots_file;
|
||||||
|
private string $referrers_file;
|
||||||
|
private string $networks_file;
|
||||||
|
private string $goodbots_file;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->badbots_file = ITK_PATH . 'config/badbots.conf';
|
||||||
|
$this->referrers_file = ITK_PATH . 'config/referrers.conf';
|
||||||
|
$this->networks_file = ITK_PATH . 'config/networks.conf';
|
||||||
|
$this->goodbots_file = ITK_PATH . 'config/goodbots.conf';
|
||||||
|
|
||||||
|
// Always hook; each method guards itself with its own option check.
|
||||||
|
add_action('init', [$this, 'check_request'], 1);
|
||||||
|
add_filter('robots_txt', [$this, 'modify_robots_txt'], 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main entry point ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function check_request(): void {
|
||||||
|
// Never block logged-in admins.
|
||||||
|
if (is_admin() || (function_exists('current_user_can') && current_user_can('manage_options'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
|
$referrer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||||
|
$ip = $this->get_client_ip();
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
|
||||||
|
// ── 1. Rate-limit good/legitimate bots ─────────────────
|
||||||
|
if (!empty($options['rate_limit_good_bots'])) {
|
||||||
|
$good_bot = $this->identify_good_bot($ua);
|
||||||
|
if ($good_bot !== null) {
|
||||||
|
$this->handle_good_bot($good_bot, $ua, $ip, $uri);
|
||||||
|
return; // Handled – don't fall through to block checks.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Block OpenAI bots ───────────────────────────────
|
||||||
|
if (!empty($options['block_openai_bots']) && $this->is_openai_bot($ua)) {
|
||||||
|
$this->block('OpenAI bot detected', 'openai', $ua, $referrer, $ip, $uri, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Block malicious bots ────────────────────────────
|
||||||
|
if (!empty($options['block_malicious_bots']) && $this->is_malicious_bot($ua)) {
|
||||||
|
$this->block('Malicious bot detected', 'malicious_bot', $ua, $referrer, $ip, $uri, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Block bad referrers ─────────────────────────────
|
||||||
|
if (!empty($options['block_bad_referrers']) && $this->is_bad_referrer($referrer)) {
|
||||||
|
$this->block('Bad referrer detected', 'bad_referrer', $ua, $referrer, $ip, $uri, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. Block bad networks ──────────────────────────────
|
||||||
|
if (!empty($options['block_bad_networks']) && $this->is_bad_network($ip)) {
|
||||||
|
$this->block('IP in blocked network', 'bad_network', $ua, $referrer, $ip, $uri, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Good-bot rate limiting ───────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns ['name' => string, 'limit' => int] or null if not a known good bot.
|
||||||
|
* A limit of 0 means "never allow" (treat as blocked).
|
||||||
|
*/
|
||||||
|
private function identify_good_bot(string $ua): ?array {
|
||||||
|
if (empty($ua)) return null;
|
||||||
|
|
||||||
|
$cache_key = 'itk_goodbots_list';
|
||||||
|
$list = get_transient($cache_key);
|
||||||
|
if ($list === false) {
|
||||||
|
$list = [];
|
||||||
|
if (file_exists($this->goodbots_file)) {
|
||||||
|
foreach (file($this->goodbots_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
|
$parts = explode('|', $line, 2);
|
||||||
|
$list[] = ['name' => trim($parts[0]), 'limit' => isset($parts[1]) ? (int)$parts[1] : 30];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_transient($cache_key, $list, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($list as $entry) {
|
||||||
|
if (stripos($ua, $entry['name']) !== false) {
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handle_good_bot(array $bot, string $ua, string $ip, string $uri): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
$name = $bot['name'];
|
||||||
|
$limit = (int)$bot['limit'];
|
||||||
|
|
||||||
|
// Limit of 0 = always block this "good" bot (e.g. GPTBot still in goodbots.conf)
|
||||||
|
if ($limit === 0) {
|
||||||
|
$this->block("Good bot with limit 0: {$name}", $name, $ua, '', $ip, $uri, $options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sliding window: track hits per bot per minute using transients.
|
||||||
|
$window = (int)(time() / 60); // 1-minute window
|
||||||
|
$tk_key = 'itk_rl_' . md5($name) . '_' . $window;
|
||||||
|
$count = (int)get_transient($tk_key);
|
||||||
|
|
||||||
|
if ($count >= $limit) {
|
||||||
|
// Over the limit – log and send 429.
|
||||||
|
if (!empty($options['log_blocked_attempts'])) {
|
||||||
|
ITK_Database::log_bot([
|
||||||
|
'ip' => $ip,
|
||||||
|
'ua' => $ua,
|
||||||
|
'referrer' => '',
|
||||||
|
'uri' => $uri,
|
||||||
|
'bot_type' => $name,
|
||||||
|
'reason' => "Rate limited: {$count}/{$limit} req/min",
|
||||||
|
'action' => 'rate_limited',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
status_header(429);
|
||||||
|
header('Retry-After: 60');
|
||||||
|
header('X-ITK-Rate-Limit: ' . $limit);
|
||||||
|
echo 'Too Many Requests. Crawl-delay: 60';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Under the limit – increment counter and allow through.
|
||||||
|
set_transient($tk_key, $count + 1, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Blocking ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
private function block(
|
||||||
|
string $reason,
|
||||||
|
string $bot_type,
|
||||||
|
string $ua,
|
||||||
|
string $referrer,
|
||||||
|
string $ip,
|
||||||
|
string $uri,
|
||||||
|
array $options
|
||||||
|
): void {
|
||||||
|
if (!empty($options['log_blocked_attempts'])) {
|
||||||
|
ITK_Database::log_bot([
|
||||||
|
'ip' => $ip,
|
||||||
|
'ua' => $ua,
|
||||||
|
'referrer' => $referrer,
|
||||||
|
'uri' => $uri,
|
||||||
|
'bot_type' => $bot_type,
|
||||||
|
'reason' => $reason,
|
||||||
|
'action' => 'blocked',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $options['response_code'] ?? '403';
|
||||||
|
$message = $options['custom_message'] ?? 'Access denied.';
|
||||||
|
$redir = $options['redirect_url'] ?? '';
|
||||||
|
|
||||||
|
if ($code === '301_custom' && !empty($redir)) {
|
||||||
|
header('Location: ' . esc_url_raw($redir), true, 301);
|
||||||
|
} else {
|
||||||
|
status_header((int)$code ?: 403);
|
||||||
|
echo esc_html($message);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Detection helpers ────────────────────────────────────── */
|
||||||
|
|
||||||
|
private function is_openai_bot(string $ua): bool {
|
||||||
|
if (empty($ua)) return false;
|
||||||
|
foreach (['GPTBot', 'ChatGPT-User', 'OAI-SearchBot', 'whisper'] as $b) {
|
||||||
|
if (stripos($ua, $b) !== false) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function is_malicious_bot(string $ua): bool {
|
||||||
|
if (empty($ua)) return false;
|
||||||
|
foreach ($this->load_conf_list($this->badbots_file, 'itk_bots_list') as $bot) {
|
||||||
|
if (stripos($ua, $bot) !== false) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function is_bad_referrer(string $referrer): bool {
|
||||||
|
if (empty($referrer)) return false;
|
||||||
|
foreach ($this->load_conf_list($this->referrers_file, 'itk_referrers_list') as $ref) {
|
||||||
|
if (stripos($referrer, $ref) !== false) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function is_bad_network(string $ip): bool {
|
||||||
|
if (empty($ip) || $ip === 'UNKNOWN') return false;
|
||||||
|
foreach ($this->load_conf_list($this->networks_file, 'itk_networks_list') as $network) {
|
||||||
|
if (filter_var($network, FILTER_VALIDATE_IP)) {
|
||||||
|
if ($ip === $network) return true;
|
||||||
|
} elseif (strpos($network, '/') !== false) {
|
||||||
|
if ($this->ip_in_cidr($ip, $network)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Robots.txt ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function modify_robots_txt(string $output, string $public): string {
|
||||||
|
if ($public === '0') return $output;
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['block_openai_bots'])) return $output;
|
||||||
|
|
||||||
|
$output .= "\n# InformatiQ Toolkit – AI bot disallow\n";
|
||||||
|
foreach (['GPTBot', 'ChatGPT-User', 'OAI-SearchBot'] as $bot) {
|
||||||
|
$output .= "User-agent: {$bot}\nDisallow: /\n\n";
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Config file readers ──────────────────────────────────── */
|
||||||
|
|
||||||
|
private function load_conf_list(string $file, string $cache_key): array {
|
||||||
|
$cached = get_transient($cache_key);
|
||||||
|
if ($cached !== false) return $cached;
|
||||||
|
|
||||||
|
if (!file_exists($file) || filesize($file) > 1048576) return [];
|
||||||
|
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
$list = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
|
if (strlen($line) <= 200 && !preg_match('/[<>"\']/', $line)) {
|
||||||
|
$list[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient($cache_key, $list, 300);
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invalidate_cache(): void {
|
||||||
|
delete_transient('itk_bots_list');
|
||||||
|
delete_transient('itk_referrers_list');
|
||||||
|
delete_transient('itk_networks_list');
|
||||||
|
delete_transient('itk_goodbots_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── IP utilities ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function get_client_ip(): string {
|
||||||
|
$keys = [
|
||||||
|
'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED',
|
||||||
|
'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED',
|
||||||
|
'REMOTE_ADDR',
|
||||||
|
];
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (empty($_SERVER[$key])) continue;
|
||||||
|
$ip = trim(explode(',', $_SERVER[$key])[0]);
|
||||||
|
if ($key !== 'REMOTE_ADDR' && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
if ($key === 'REMOTE_ADDR' && filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ip_in_cidr(string $ip, string $cidr): bool {
|
||||||
|
if (strpos($cidr, '/') === false) return false;
|
||||||
|
[$subnet, $mask] = explode('/', $cidr, 2);
|
||||||
|
if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false;
|
||||||
|
if (!is_numeric($mask) || $mask < 0 || $mask > 32) return false;
|
||||||
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false;
|
||||||
|
|
||||||
|
$ip_long = ip2long($ip);
|
||||||
|
$sub_long = ip2long($subnet);
|
||||||
|
$mask_dec = ~((1 << (32 - (int)$mask)) - 1);
|
||||||
|
return ($ip_long & $mask_dec) === ($sub_long & $mask_dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Accessors for admin ──────────────────────────────────── */
|
||||||
|
|
||||||
|
public function get_badbots_file(): string { return $this->badbots_file; }
|
||||||
|
public function get_referrers_file(): string { return $this->referrers_file; }
|
||||||
|
public function get_networks_file(): string { return $this->networks_file; }
|
||||||
|
public function get_goodbots_file(): string { return $this->goodbots_file; }
|
||||||
|
}
|
||||||
283
includes/class-itk-database.php
Normal file
283
includes/class-itk-database.php
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database helper for InformatiQ Toolkit.
|
||||||
|
* Manages two log tables: bot_log and honeypot_log.
|
||||||
|
*/
|
||||||
|
class ITK_Database {
|
||||||
|
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
const DB_VERSION_OPTION = 'itk_db_version';
|
||||||
|
|
||||||
|
/* ── Table names ──────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function bot_table(): string {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . 'itk_bot_log';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function honeypot_table(): string {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . 'itk_honeypot_log';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Install / upgrade ────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function install() {
|
||||||
|
global $wpdb;
|
||||||
|
$charset = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
$sql_bot = "CREATE TABLE " . self::bot_table() . " (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
logged_at DATETIME NOT NULL,
|
||||||
|
ip_address VARCHAR(45) NOT NULL DEFAULT '',
|
||||||
|
user_agent TEXT NOT NULL,
|
||||||
|
referrer VARCHAR(1000) NOT NULL DEFAULT '',
|
||||||
|
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
|
||||||
|
bot_type VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
action VARCHAR(20) NOT NULL DEFAULT 'blocked',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY ip_address (ip_address),
|
||||||
|
KEY logged_at (logged_at),
|
||||||
|
KEY bot_type (bot_type),
|
||||||
|
KEY action (action)
|
||||||
|
) {$charset};";
|
||||||
|
|
||||||
|
$sql_hp = "CREATE TABLE " . self::honeypot_table() . " (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
blocked_at DATETIME NOT NULL,
|
||||||
|
ip_address VARCHAR(45) NOT NULL DEFAULT '',
|
||||||
|
form_type VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
reason VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
request_uri VARCHAR(1000) NOT NULL DEFAULT '',
|
||||||
|
user_agent TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY ip_address (ip_address),
|
||||||
|
KEY blocked_at (blocked_at),
|
||||||
|
KEY form_type (form_type)
|
||||||
|
) {$charset};";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta($sql_bot);
|
||||||
|
dbDelta($sql_hp);
|
||||||
|
|
||||||
|
update_option(self::DB_VERSION_OPTION, self::DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Bot log ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function log_bot(array $data): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->insert(
|
||||||
|
self::bot_table(),
|
||||||
|
[
|
||||||
|
'logged_at' => current_time('mysql'),
|
||||||
|
'ip_address' => sanitize_text_field($data['ip'] ?? ''),
|
||||||
|
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
|
||||||
|
'referrer' => esc_url_raw(substr($data['referrer'] ?? '', 0, 1000)),
|
||||||
|
'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)),
|
||||||
|
'bot_type' => sanitize_text_field($data['bot_type'] ?? ''),
|
||||||
|
'reason' => sanitize_text_field($data['reason'] ?? ''),
|
||||||
|
'action' => sanitize_text_field($data['action'] ?? 'blocked'),
|
||||||
|
],
|
||||||
|
['%s','%s','%s','%s','%s','%s','%s','%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_bot_rows(array $args = []): array {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::bot_table();
|
||||||
|
$limit = max(1, (int)($args['per_page'] ?? 25));
|
||||||
|
$offset = max(0, (int)($args['offset'] ?? 0));
|
||||||
|
$where = '1=1';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($args['action'])) {
|
||||||
|
$where .= ' AND action = %s';
|
||||||
|
$params[] = $args['action'];
|
||||||
|
}
|
||||||
|
if (!empty($args['bot_type'])) {
|
||||||
|
$where .= ' AND bot_type = %s';
|
||||||
|
$params[] = $args['bot_type'];
|
||||||
|
}
|
||||||
|
if (!empty($args['ip'])) {
|
||||||
|
$where .= ' AND ip_address = %s';
|
||||||
|
$params[] = $args['ip'];
|
||||||
|
}
|
||||||
|
if (!empty($args['search'])) {
|
||||||
|
$like = '%' . $wpdb->esc_like($args['search']) . '%';
|
||||||
|
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
|
||||||
|
$params[] = $like; $params[] = $like; $params[] = $like;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[] = $limit;
|
||||||
|
$params[] = $offset;
|
||||||
|
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY logged_at DESC LIMIT %d OFFSET %d";
|
||||||
|
return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function count_bot_rows(array $args = []): int {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::bot_table();
|
||||||
|
$where = '1=1';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($args['action'])) {
|
||||||
|
$where .= ' AND action = %s';
|
||||||
|
$params[] = $args['action'];
|
||||||
|
}
|
||||||
|
if (!empty($args['bot_type'])) {
|
||||||
|
$where .= ' AND bot_type = %s';
|
||||||
|
$params[] = $args['bot_type'];
|
||||||
|
}
|
||||||
|
if (!empty($args['ip'])) {
|
||||||
|
$where .= ' AND ip_address = %s';
|
||||||
|
$params[] = $args['ip'];
|
||||||
|
}
|
||||||
|
if (!empty($args['search'])) {
|
||||||
|
$like = '%' . $wpdb->esc_like($args['search']) . '%';
|
||||||
|
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
|
||||||
|
$params[] = $like; $params[] = $like; $params[] = $like;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
|
||||||
|
return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_bot_stats(): array {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::bot_table();
|
||||||
|
$today = current_time('Y-m-d');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}"),
|
||||||
|
'today' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(logged_at)=%s", $today)),
|
||||||
|
'blocked' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE action='blocked'"),
|
||||||
|
'rate_limited' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE action='rate_limited'"),
|
||||||
|
'top_bot_types' => $wpdb->get_results("SELECT bot_type, COUNT(*) as cnt FROM {$table} WHERE bot_type != '' GROUP BY bot_type ORDER BY cnt DESC LIMIT 8") ?: [],
|
||||||
|
'top_ips' => $wpdb->get_results("SELECT ip_address, COUNT(*) as cnt FROM {$table} GROUP BY ip_address ORDER BY cnt DESC LIMIT 5") ?: [],
|
||||||
|
'last_24h_counts' => $wpdb->get_results("SELECT DATE_FORMAT(logged_at,'%H:00') as hour, COUNT(*) as cnt FROM {$table} WHERE logged_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY hour ORDER BY hour ASC") ?: [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_bot_types(): array {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->get_col("SELECT DISTINCT bot_type FROM " . self::bot_table() . " WHERE bot_type != '' ORDER BY bot_type ASC") ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clear_bot_log(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query("TRUNCATE TABLE " . self::bot_table());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function prune_bot_log(int $days): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM " . self::bot_table() . " WHERE logged_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
|
||||||
|
$days
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Honeypot log ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public static function log_honeypot(array $data): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->insert(
|
||||||
|
self::honeypot_table(),
|
||||||
|
[
|
||||||
|
'blocked_at' => current_time('mysql'),
|
||||||
|
'ip_address' => sanitize_text_field($data['ip'] ?? ''),
|
||||||
|
'form_type' => sanitize_text_field($data['form'] ?? 'Unknown'),
|
||||||
|
'reason' => sanitize_text_field($data['reason'] ?? ''),
|
||||||
|
'request_uri' => esc_url_raw(substr($data['uri'] ?? '', 0, 1000)),
|
||||||
|
'user_agent' => sanitize_textarea_field($data['ua'] ?? ''),
|
||||||
|
],
|
||||||
|
['%s','%s','%s','%s','%s','%s']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_honeypot_rows(array $args = []): array {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::honeypot_table();
|
||||||
|
$limit = max(1, (int)($args['per_page'] ?? 25));
|
||||||
|
$offset = max(0, (int)($args['offset'] ?? 0));
|
||||||
|
$where = '1=1';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($args['form'])) {
|
||||||
|
$where .= ' AND form_type = %s';
|
||||||
|
$params[] = $args['form'];
|
||||||
|
}
|
||||||
|
if (!empty($args['ip'])) {
|
||||||
|
$where .= ' AND ip_address = %s';
|
||||||
|
$params[] = $args['ip'];
|
||||||
|
}
|
||||||
|
if (!empty($args['search'])) {
|
||||||
|
$like = '%' . $wpdb->esc_like($args['search']) . '%';
|
||||||
|
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
|
||||||
|
$params[] = $like; $params[] = $like; $params[] = $like;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params[] = $limit;
|
||||||
|
$params[] = $offset;
|
||||||
|
$sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY blocked_at DESC LIMIT %d OFFSET %d";
|
||||||
|
return $wpdb->get_results($wpdb->prepare($sql, $params)) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function count_honeypot_rows(array $args = []): int {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::honeypot_table();
|
||||||
|
$where = '1=1';
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($args['form'])) {
|
||||||
|
$where .= ' AND form_type = %s';
|
||||||
|
$params[] = $args['form'];
|
||||||
|
}
|
||||||
|
if (!empty($args['ip'])) {
|
||||||
|
$where .= ' AND ip_address = %s';
|
||||||
|
$params[] = $args['ip'];
|
||||||
|
}
|
||||||
|
if (!empty($args['search'])) {
|
||||||
|
$like = '%' . $wpdb->esc_like($args['search']) . '%';
|
||||||
|
$where .= ' AND (ip_address LIKE %s OR user_agent LIKE %s OR reason LIKE %s)';
|
||||||
|
$params[] = $like; $params[] = $like; $params[] = $like;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
|
||||||
|
return (int)($params ? $wpdb->get_var($wpdb->prepare($sql, $params)) : $wpdb->get_var($sql));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_honeypot_stats(): array {
|
||||||
|
global $wpdb;
|
||||||
|
$table = self::honeypot_table();
|
||||||
|
$today = current_time('Y-m-d');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => (int)$wpdb->get_var("SELECT COUNT(*) FROM {$table}"),
|
||||||
|
'today' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE DATE(blocked_at)=%s", $today)),
|
||||||
|
'top_forms' => $wpdb->get_results("SELECT form_type, COUNT(*) as cnt FROM {$table} GROUP BY form_type ORDER BY cnt DESC LIMIT 8") ?: [],
|
||||||
|
'top_ips' => $wpdb->get_results("SELECT ip_address, COUNT(*) as cnt FROM {$table} GROUP BY ip_address ORDER BY cnt DESC LIMIT 5") ?: [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_honeypot_form_types(): array {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->get_col("SELECT DISTINCT form_type FROM " . self::honeypot_table() . " ORDER BY form_type ASC") ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clear_honeypot_log(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query("TRUNCATE TABLE " . self::honeypot_table());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function prune_honeypot_log(int $days): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM " . self::honeypot_table() . " WHERE blocked_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
|
||||||
|
$days
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
277
includes/class-itk-honeypot.php
Normal file
277
includes/class-itk-honeypot.php
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Honeypot
|
||||||
|
*
|
||||||
|
* Ported from HoneypotFields v2.4.0.
|
||||||
|
* Injects invisible honeypot fields into all major form types and blocks
|
||||||
|
* submissions that fill them (bots) or arrive too fast/too slow.
|
||||||
|
* Uses ITK_Database for logging instead of a separate table.
|
||||||
|
*/
|
||||||
|
class ITK_Honeypot {
|
||||||
|
|
||||||
|
const FIELD_PREFIX = '_hp_trap_';
|
||||||
|
const TOKEN_FIELD = '_hp_token';
|
||||||
|
const TIME_FIELD = '_hp_ts';
|
||||||
|
|
||||||
|
private static array $opts = [];
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
self::$opts = get_option('itk_honeypot', []);
|
||||||
|
|
||||||
|
if (empty(self::$opts['enabled'])) return;
|
||||||
|
|
||||||
|
// Inject honeypot into forms
|
||||||
|
add_action('comment_form', [$this, 'inject_comment']);
|
||||||
|
add_action('login_form', [$this, 'inject_generic']);
|
||||||
|
add_action('register_form', [$this, 'inject_generic']);
|
||||||
|
add_action('lostpassword_form', [$this, 'inject_generic']);
|
||||||
|
add_action('wp_head', [$this, 'inject_honeypot_style']);
|
||||||
|
|
||||||
|
// Validate on submission
|
||||||
|
add_filter('preprocess_comment', [$this, 'validate_comment'], 1);
|
||||||
|
add_action('authenticate', [$this, 'validate_login'], 1, 3);
|
||||||
|
add_action('register_post', [$this, 'validate_register'], 1, 3);
|
||||||
|
add_action('lostpassword_post', [$this, 'validate_lost_password'], 1);
|
||||||
|
|
||||||
|
// WooCommerce
|
||||||
|
if (!empty(self::$opts['protect_woocommerce']) && class_exists('WooCommerce')) {
|
||||||
|
add_action('woocommerce_checkout_before_customer_details', [$this, 'inject_generic']);
|
||||||
|
add_action('woocommerce_checkout_process', [$this, 'validate_woo_checkout']);
|
||||||
|
add_action('woocommerce_register_form', [$this, 'inject_generic']);
|
||||||
|
add_action('woocommerce_process_registration_errors', [$this, 'validate_woo_registration'], 10, 3);
|
||||||
|
add_action('woocommerce_login_form', [$this, 'inject_generic']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact Form 7
|
||||||
|
if (!empty(self::$opts['protect_cf7']) && class_exists('WPCF7')) {
|
||||||
|
add_action('wpcf7_form_elements', [$this, 'inject_cf7']);
|
||||||
|
add_filter('wpcf7_before_send_mail', [$this, 'validate_cf7'], 10, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elementor forms
|
||||||
|
if (!empty(self::$opts['protect_elementor']) && defined('ELEMENTOR_VERSION')) {
|
||||||
|
add_action('elementor/frontend/after_enqueue_scripts', [$this, 'elementor_enqueue']);
|
||||||
|
add_action('elementor_pro/forms/validation', [$this, 'validate_elementor'], 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gravity Forms
|
||||||
|
if (!empty(self::$opts['protect_gravity']) && class_exists('GFForms')) {
|
||||||
|
add_filter('gform_form_tag', [$this, 'inject_gravity'], 10, 2);
|
||||||
|
add_filter('gform_validation', [$this, 'validate_gravity']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search form
|
||||||
|
if (!empty(self::$opts['protect_search'])) {
|
||||||
|
add_action('get_search_form', [$this, 'inject_search']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue JS token generator
|
||||||
|
add_action('wp_enqueue_scripts', [$this, 'enqueue_token_script']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Style (hide honeypot fields) ─────────────────────────── */
|
||||||
|
|
||||||
|
public function inject_honeypot_style(): void {
|
||||||
|
echo '<style>.itk-hp-field{display:none!important;visibility:hidden!important;opacity:0!important;position:absolute!important;left:-9999px!important;top:-9999px!important;}</style>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── JS token (HMAC-based anti-CSRF) ─────────────────────── */
|
||||||
|
|
||||||
|
public function enqueue_token_script(): void {
|
||||||
|
$secret = $this->get_page_secret();
|
||||||
|
wp_add_inline_script('jquery-core', "
|
||||||
|
(function(){
|
||||||
|
var s='" . esc_js($secret) . "',n='" . esc_js(wp_create_nonce('itk_hp')) . "';
|
||||||
|
document.querySelectorAll('." . self::TOKEN_FIELD . "').forEach(function(f){
|
||||||
|
f.value=btoa(s+'|'+Date.now()+'|'+n);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('." . self::TIME_FIELD . "').forEach(function(f){
|
||||||
|
f.value=Math.floor(Date.now()/1000);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
", 'after');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Field HTML generator ─────────────────────────────────── */
|
||||||
|
|
||||||
|
private function honeypot_html(string $form_type = ''): string {
|
||||||
|
$field = self::FIELD_PREFIX . substr(md5(uniqid()), 0, 8);
|
||||||
|
$ts = time();
|
||||||
|
$label = ['Your email address', 'Website URL', 'Full name'][array_rand(['a','b','c'])];
|
||||||
|
return sprintf(
|
||||||
|
'<div class="itk-hp-field" aria-hidden="true" tabindex="-1" style="display:none!important">
|
||||||
|
<label>%s <input type="text" name="%s" value="" autocomplete="off" tabindex="-1"></label>
|
||||||
|
<input type="hidden" name="%s" class="%s" value="">
|
||||||
|
<input type="hidden" name="%s" class="%s" value="%d">
|
||||||
|
</div>',
|
||||||
|
esc_html($label),
|
||||||
|
esc_attr($field),
|
||||||
|
self::TOKEN_FIELD, self::TOKEN_FIELD,
|
||||||
|
self::TIME_FIELD, self::TIME_FIELD,
|
||||||
|
$ts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Injectors ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function inject_generic(): void {
|
||||||
|
if (empty(self::$opts['enabled'])) return;
|
||||||
|
echo $this->honeypot_html(); // phpcs:ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inject_comment(): void {
|
||||||
|
if (empty(self::$opts['protect_comments'])) return;
|
||||||
|
echo $this->honeypot_html('comment'); // phpcs:ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inject_search(string $form): string {
|
||||||
|
return $form . $this->honeypot_html('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inject_cf7(string $content): string {
|
||||||
|
return $content . $this->honeypot_html('cf7');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inject_gravity(string $tag, array $form): string {
|
||||||
|
return $tag . $this->honeypot_html('gravity');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function elementor_enqueue(): void {
|
||||||
|
// Elementor injects via JS – add hidden fields via wp_footer
|
||||||
|
add_action('wp_footer', [$this, 'inject_generic']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Validators ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
private function check_honeypot(string $form_type): bool {
|
||||||
|
// 1. Honeypot field must be empty
|
||||||
|
foreach ($_POST as $key => $val) {
|
||||||
|
if (strpos($key, self::FIELD_PREFIX) === 0 && !empty($val)) {
|
||||||
|
$this->log_block($form_type, 'Honeypot field filled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Timing check
|
||||||
|
$opts = get_option('itk_honeypot', []);
|
||||||
|
$min_t = max(1, (int)($opts['min_time'] ?? 3));
|
||||||
|
$max_t = max(60, (int)($opts['max_time'] ?? 7200));
|
||||||
|
$ts = (int)($_POST[self::TIME_FIELD] ?? 0);
|
||||||
|
$elapsed = time() - $ts;
|
||||||
|
|
||||||
|
if ($ts > 0 && $elapsed < $min_t) {
|
||||||
|
$this->log_block($form_type, "Submitted too fast ({$elapsed}s)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($ts > 0 && $elapsed > $max_t) {
|
||||||
|
$this->log_block($form_type, "Submitted too slow ({$elapsed}s > {$max_t}s)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function log_block(string $form_type, string $reason): void {
|
||||||
|
ITK_Database::log_honeypot([
|
||||||
|
'ip' => $this->get_ip(),
|
||||||
|
'form' => $form_type,
|
||||||
|
'reason' => $reason,
|
||||||
|
'uri' => $_SERVER['REQUEST_URI'] ?? '',
|
||||||
|
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_comment(array $comment_data): array {
|
||||||
|
if (empty(self::$opts['protect_comments'])) return $comment_data;
|
||||||
|
if (!$this->check_honeypot('comment')) {
|
||||||
|
wp_die('Spam detected. Please go back and try again.', 'Spam Blocked', ['response' => 403]);
|
||||||
|
}
|
||||||
|
return $comment_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_login($user, string $username, string $password) {
|
||||||
|
if (empty(self::$opts['protect_login'])) return $user;
|
||||||
|
if (!empty($username) && !$this->check_honeypot('login')) {
|
||||||
|
return new WP_Error('honeypot_blocked', 'Access denied.');
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_register($sanitized_user_login, $user_email, \WP_Error $errors): void {
|
||||||
|
if (empty(self::$opts['protect_register'])) return;
|
||||||
|
if (!$this->check_honeypot('register')) {
|
||||||
|
$errors->add('honeypot_blocked', 'Spam registration detected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_lost_password(\WP_Error $errors): void {
|
||||||
|
if (empty(self::$opts['protect_lost_password'])) return;
|
||||||
|
if (!$this->check_honeypot('lostpassword')) {
|
||||||
|
$errors->add('honeypot_blocked', 'Access denied.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_woo_checkout(): void {
|
||||||
|
if (!$this->check_honeypot('woo_checkout')) {
|
||||||
|
wc_add_notice('Spam submission detected. Please refresh and try again.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_woo_registration(\WP_Error $errors, $username, $email): \WP_Error {
|
||||||
|
if (!$this->check_honeypot('woo_register')) {
|
||||||
|
$errors->add('honeypot_blocked', 'Spam registration detected.');
|
||||||
|
}
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_cf7($contact_form, &$abort, $submission): void {
|
||||||
|
if (!$this->check_honeypot('cf7')) {
|
||||||
|
$abort = true;
|
||||||
|
$contact_form->set_status('spam');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_elementor($record, $ajax_handler): void {
|
||||||
|
if (!$this->check_honeypot('elementor')) {
|
||||||
|
$ajax_handler->add_error_message('Spam detected. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate_gravity(array $validation_result): array {
|
||||||
|
if (!$this->check_honeypot('gravity')) {
|
||||||
|
$validation_result['is_valid'] = false;
|
||||||
|
foreach ($validation_result['form']['fields'] as &$field) {
|
||||||
|
$field->failed_validation = true;
|
||||||
|
$field->validation_message = 'Spam detected.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $validation_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Helpers ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
private function get_ip(): string {
|
||||||
|
$keys = [
|
||||||
|
'HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED',
|
||||||
|
'HTTP_X_CLUSTER_CLIENT_IP','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR',
|
||||||
|
];
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
if (!empty($_SERVER[$k])) {
|
||||||
|
$ip = trim(explode(',', $_SERVER[$k])[0]);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_page_secret(): string {
|
||||||
|
$secret = get_option('itk_hp_secret');
|
||||||
|
if (!$secret) {
|
||||||
|
$secret = wp_generate_password(32, false);
|
||||||
|
update_option('itk_hp_secret', $secret);
|
||||||
|
}
|
||||||
|
return $secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
359
includes/class-itk-optimization.php
Normal file
359
includes/class-itk-optimization.php
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Optimization
|
||||||
|
*
|
||||||
|
* Merged from informatiq-wp-secure optimization class and informatiq-utils.
|
||||||
|
* All features are toggleable via itk_optimization option.
|
||||||
|
*/
|
||||||
|
class ITK_Optimization {
|
||||||
|
|
||||||
|
private array $opts;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->opts = get_option('itk_optimization', []);
|
||||||
|
$this->init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function on(string $key): bool {
|
||||||
|
return !empty($this->opts[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function init(): void {
|
||||||
|
// ── Version / meta ──────────────────────────────────────
|
||||||
|
if ($this->on('remove_wp_version')) {
|
||||||
|
remove_action('wp_head', 'wp_generator');
|
||||||
|
add_filter('the_generator', '__return_empty_string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Login errors ────────────────────────────────────────
|
||||||
|
if ($this->on('hide_login_errors')) {
|
||||||
|
add_filter('login_errors', fn() => 'Something went wrong.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Comment class ───────────────────────────────────────
|
||||||
|
if ($this->on('remove_author_class')) {
|
||||||
|
add_filter('comment_class', [$this, 'remove_comment_author_class']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Script / style versions ─────────────────────────────
|
||||||
|
if ($this->on('remove_script_versions')) {
|
||||||
|
add_filter('style_loader_src', [$this, 'remove_version_param'], 999);
|
||||||
|
add_filter('script_loader_src', [$this, 'remove_version_param'], 999);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Author base ─────────────────────────────────────────
|
||||||
|
if ($this->on('change_author_base')) {
|
||||||
|
add_action('init', [$this, 'change_author_base']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Revisions ───────────────────────────────────────────
|
||||||
|
if ($this->on('limit_revisions') && !defined('WP_POST_REVISIONS')) {
|
||||||
|
define('WP_POST_REVISIONS', 3);
|
||||||
|
}
|
||||||
|
if (!defined('AUTOSAVE_INTERVAL')) {
|
||||||
|
define('AUTOSAVE_INTERVAL', 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Emoji ───────────────────────────────────────────────
|
||||||
|
if ($this->on('remove_emoji')) {
|
||||||
|
add_action('init', [$this, 'disable_emojis']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── User fields ─────────────────────────────────────────
|
||||||
|
if ($this->on('remove_default_userfields')) {
|
||||||
|
add_filter('user_contactmethods', [$this, 'remove_default_userfields']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Content cleanup ─────────────────────────────────────
|
||||||
|
if ($this->on('clean_bad_content')) {
|
||||||
|
add_filter('content_save_pre', [$this, 'clean_bad_content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WP head noise ───────────────────────────────────────
|
||||||
|
if ($this->on('remove_wp_head_noise')) {
|
||||||
|
$this->remove_wp_head_noise();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── XML-RPC ─────────────────────────────────────────────
|
||||||
|
if ($this->on('disable_xml_rpc')) {
|
||||||
|
add_filter('xmlrpc_enabled', '__return_false');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WP Embed ────────────────────────────────────────────
|
||||||
|
if ($this->on('deregister_wp_embed')) {
|
||||||
|
add_action('wp_footer', [$this, 'deregister_wp_embed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empty search ────────────────────────────────────────
|
||||||
|
if ($this->on('stop_empty_search_redirect')) {
|
||||||
|
add_filter('request', [$this, 'stop_empty_search']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Widgets ─────────────────────────────────────────────
|
||||||
|
if ($this->on('unregister_default_widgets')) {
|
||||||
|
add_action('widgets_init', [$this, 'unregister_default_widgets'], 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Defer JS ────────────────────────────────────────────
|
||||||
|
if ($this->on('defer_js')) {
|
||||||
|
add_filter('script_loader_tag', [$this, 'defer_js'], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Heartbeat ───────────────────────────────────────────
|
||||||
|
if ($this->on('limit_heartbeat')) {
|
||||||
|
add_filter('wpe_heartbeat_allowed_pages', [$this, 'limit_heartbeat_pages']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dashboard widgets ───────────────────────────────────
|
||||||
|
if ($this->on('disable_dashboard_widgets')) {
|
||||||
|
add_action('admin_init', [$this, 'disable_dashboard_widgets'], 9999);
|
||||||
|
}
|
||||||
|
add_action('wp_dashboard_setup', [$this, 'add_itq_dashboard_widget']);
|
||||||
|
|
||||||
|
// ── Comment URL field ───────────────────────────────────
|
||||||
|
if ($this->on('disable_comments_url')) {
|
||||||
|
add_filter('comment_form_default_fields', [$this, 'remove_comment_url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Google FLoC / Permissions-Policy ────────────────────
|
||||||
|
if ($this->on('disable_floc')) {
|
||||||
|
add_filter('wp_headers', [$this, 'disable_floc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lightbox images ─────────────────────────────────────
|
||||||
|
if ($this->on('lightbox_images')) {
|
||||||
|
add_filter('the_content', [$this, 'add_lightbox_rel']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Admin bar cleanup ───────────────────────────────────
|
||||||
|
if ($this->on('remove_admin_bar_links')) {
|
||||||
|
add_action('wp_before_admin_bar_render', [$this, 'remove_admin_bar_links']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Admin branding ──────────────────────────────────────
|
||||||
|
if ($this->on('admin_branding')) {
|
||||||
|
add_filter('admin_footer_text', [$this, 'admin_footer_text']);
|
||||||
|
add_action('admin_bar_menu', [$this, 'toolbar_link'], 999);
|
||||||
|
add_action('admin_bar_menu', [$this, 'remove_wp_logo'], 999);
|
||||||
|
add_action('admin_notices', [$this, 'admin_notice']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RSS featured image ──────────────────────────────────
|
||||||
|
if ($this->on('featured_image_rss')) {
|
||||||
|
add_filter('the_excerpt_rss', [$this, 'featured_to_rss']);
|
||||||
|
add_filter('the_content_feed', [$this, 'featured_to_rss']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DNS prefetch ────────────────────────────────────────
|
||||||
|
if ($this->on('dns_prefetch')) {
|
||||||
|
add_action('wp_head', [$this, 'dns_prefetch'], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Google jQuery ───────────────────────────────────────
|
||||||
|
if ($this->on('use_google_jquery')) {
|
||||||
|
add_action('init', [$this, 'use_google_jquery']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Callback implementations ─────────────────────────────── */
|
||||||
|
|
||||||
|
public function remove_comment_author_class(array $classes): array {
|
||||||
|
return array_filter($classes, fn($c) => strpos($c, 'comment-author-') === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove_version_param(string $src): string {
|
||||||
|
return strpos($src, 'ver=') ? remove_query_arg('ver', $src) : $src;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function change_author_base(): void {
|
||||||
|
global $wp_rewrite;
|
||||||
|
$wp_rewrite->author_base = 'writer';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable_emojis(): void {
|
||||||
|
remove_action('wp_head', 'print_emoji_detection_script', 7);
|
||||||
|
remove_action('admin_print_scripts','print_emoji_detection_script');
|
||||||
|
remove_action('wp_print_styles', 'print_emoji_styles');
|
||||||
|
remove_action('admin_print_styles','print_emoji_styles');
|
||||||
|
remove_filter('the_content_feed', 'wp_staticize_emoji');
|
||||||
|
remove_filter('comment_text_rss', 'wp_staticize_emoji');
|
||||||
|
remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
|
||||||
|
add_filter('tiny_mce_plugins', fn($p) => is_array($p) ? array_diff($p, ['wpemoji']) : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove_default_userfields(array $fields): array {
|
||||||
|
foreach (['aim','jabber','yim'] as $f) unset($fields[$f]);
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clean_bad_content(string $content): string {
|
||||||
|
return preg_replace([
|
||||||
|
"~<p[^>]*>\s?</p>~",
|
||||||
|
"~<a[^>]*>\s?</a>~",
|
||||||
|
"~<font[^>]*>~",
|
||||||
|
"~<\/font>~",
|
||||||
|
"~style\=\"[^\"]*\"~",
|
||||||
|
"~<span[^>]*>\s?</span>~",
|
||||||
|
], '', $content) ?? $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function remove_wp_head_noise(): void {
|
||||||
|
remove_action('wp_head', 'wlwmanifest_link');
|
||||||
|
remove_action('wp_head', 'rsd_link');
|
||||||
|
remove_action('wp_head', 'wp_generator');
|
||||||
|
remove_action('wp_head', 'start_post_rel_link');
|
||||||
|
remove_action('wp_head', 'index_rel_link');
|
||||||
|
remove_action('wp_head', 'feed_links_extra', 3);
|
||||||
|
remove_action('wp_head', 'feed_links', 2);
|
||||||
|
remove_action('wp_head', 'parent_post_rel_link', 10, 0);
|
||||||
|
remove_action('wp_head', 'start_post_rel_link', 10, 0);
|
||||||
|
remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deregister_wp_embed(): void {
|
||||||
|
wp_deregister_script('wp-embed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stop_empty_search(array $vars): array {
|
||||||
|
if (isset($_GET['s']) && empty($_GET['s'])) $vars['s'] = ' ';
|
||||||
|
return $vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unregister_default_widgets(): void {
|
||||||
|
foreach ([
|
||||||
|
'WP_Widget_Calendar', 'WP_Widget_Archives', 'WP_Widget_Meta',
|
||||||
|
'WP_Widget_Search', 'WP_Widget_Tag_Cloud',
|
||||||
|
] as $w) {
|
||||||
|
if (class_exists($w)) unregister_widget($w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defer_js(string $tag): string {
|
||||||
|
$defer = [
|
||||||
|
'owl-carousel.min.js','mansonry.js','imgloaded.js',
|
||||||
|
'jquery.magnific-popup.min.js','bgswitcher.js','exit.js',
|
||||||
|
'lazyload.js','app.js',
|
||||||
|
'add-to-cart.min.js','cart-fragments.min.js','woocommerce.min.js',
|
||||||
|
'wp-embed.min.js',
|
||||||
|
];
|
||||||
|
foreach ($defer as $s) {
|
||||||
|
if (strpos($tag, $s) !== false) {
|
||||||
|
return str_replace(' src', ' defer="defer" src', $tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function limit_heartbeat_pages(array $allowed): array {
|
||||||
|
return ['index.php','admin.php','edit.php','post.php','post-new.php'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable_dashboard_widgets(): void {
|
||||||
|
remove_meta_box('dashboard_primary', 'dashboard', 'core');
|
||||||
|
remove_meta_box('wpe_dify_news_feed', 'dashboard', 'normal');
|
||||||
|
global $wp_meta_boxes;
|
||||||
|
unset($wp_meta_boxes['dashboard']['normal']['core']['dashboard_right_now']);
|
||||||
|
unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_secondary']);
|
||||||
|
unset($wp_meta_boxes['dashboard']['side']['core']['dashboard_quick_press']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_itq_dashboard_widget(): void {
|
||||||
|
wp_add_dashboard_widget(
|
||||||
|
'itk_info_widget',
|
||||||
|
'Developed & Maintained by',
|
||||||
|
[$this, 'itq_dashboard_widget_content']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function itq_dashboard_widget_content(): void {
|
||||||
|
echo '<div style="text-align:center">'
|
||||||
|
. '<a href="https://informatiq.services" target="_blank">'
|
||||||
|
. '<img src="https://informatiq.services/images/logo_IQ_transparentAsset_1.png" width="200"></a>'
|
||||||
|
. '<br><strong>Strategic Solutions, Intelligent IT Services</strong>'
|
||||||
|
. '<br><br>Email: <a href="mailto:support@informatiq.services">support@informatiq.services</a>'
|
||||||
|
. '<br>Phone: (+34) 971 560 060 | Emergency: (+34) 643 732 407'
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove_comment_url(array $fields): array {
|
||||||
|
unset($fields['url']);
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable_floc(array $headers): array {
|
||||||
|
$headers['Permissions-Policy'] = 'interest-cohort=()';
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_lightbox_rel(string $content): string {
|
||||||
|
global $post;
|
||||||
|
$title = isset($post->post_title) ? esc_attr($post->post_title) : '';
|
||||||
|
$pattern = '/<a(.*?)href=([\'"])(.*?)\.(bmp|gif|jpeg|jpg|png)([\'"])(.*?)>/i';
|
||||||
|
$replace = '<a$1href=$2$3.$4$5 rel="lightbox" title="' . $title . '"$6>';
|
||||||
|
return preg_replace($pattern, $replace, $content) ?? $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove_admin_bar_links(): void {
|
||||||
|
global $wp_admin_bar;
|
||||||
|
foreach (['wp-logo','about','wporg','documentation','support-forums','feedback','comments'] as $node) {
|
||||||
|
$wp_admin_bar->remove_menu($node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function admin_footer_text(): string {
|
||||||
|
return '<a href="https://wordpress.org" target="_blank">WordPress</a> Core | '
|
||||||
|
. 'Customizations by <a href="https://informatiq.services/" target="_blank"><strong>InformatiQ Services</strong></a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toolbar_link(\WP_Admin_Bar $bar): void {
|
||||||
|
$bar->add_node([
|
||||||
|
'id' => 'itq-support',
|
||||||
|
'title' => 'InformatiQ Services',
|
||||||
|
'href' => 'https://informatiq.services',
|
||||||
|
'meta' => ['class' => 'itq-support', 'title' => 'Strategic Solutions, Intelligent IT Services'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove_wp_logo(\WP_Admin_Bar $bar): void {
|
||||||
|
$bar->remove_node('wp-logo');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function admin_notice(): void {
|
||||||
|
echo '<div class="updated notice"><p>'
|
||||||
|
. esc_html__('This website has been developed and is being hosted and maintained by', 'informatiq-toolkit')
|
||||||
|
. ' <a href="https://informatiq.services" target="_blank">InformatiQ</a></p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function featured_to_rss(string $content): string {
|
||||||
|
global $post;
|
||||||
|
if (!empty($post->ID) && has_post_thumbnail($post->ID)) {
|
||||||
|
$content = get_the_post_thumbnail($post->ID, 'thumbnail', ['style' => 'float:left;margin:0 15px 15px 0']) . $content;
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dns_prefetch(): void {
|
||||||
|
echo "\n<!-- DNS Prefetch -->\n"
|
||||||
|
. '<meta http-equiv="x-dns-prefetch-control" content="on">' . "\n"
|
||||||
|
. "<!-- /DNS Prefetch -->\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function use_google_jquery(): void {
|
||||||
|
if (is_admin()) return;
|
||||||
|
wp_deregister_script('jquery');
|
||||||
|
wp_register_script('jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js', [], null, true);
|
||||||
|
wp_enqueue_script('jquery');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Option update (called by admin AJAX) ─────────────────── */
|
||||||
|
|
||||||
|
public function update_options(array $opts): void {
|
||||||
|
$this->opts = $opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_options(): array {
|
||||||
|
return $this->opts;
|
||||||
|
}
|
||||||
|
}
|
||||||
306
includes/class-itk-protection.php
Normal file
306
includes/class-itk-protection.php
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ITK Protection
|
||||||
|
*
|
||||||
|
* Handles wp-login protection, security headers, sensitive file blocking,
|
||||||
|
* malicious query blocking, and custom login URL.
|
||||||
|
*
|
||||||
|
* Deactivation bug fix: every method checks its own option key before acting.
|
||||||
|
* Hooks are always registered; guards live inside the callbacks.
|
||||||
|
*/
|
||||||
|
class ITK_Protection {
|
||||||
|
|
||||||
|
private array $allowed_ips = [];
|
||||||
|
private string $allowed_ips_file;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->allowed_ips_file = ITK_PATH . 'config/allowed-ips.conf';
|
||||||
|
$this->load_allowed_ips();
|
||||||
|
|
||||||
|
add_action('init', [$this, 'protect_wp_login'], 0);
|
||||||
|
add_action('init', [$this, 'block_sensitive_files'], 0);
|
||||||
|
add_action('init', [$this, 'block_malicious_queries'], 0);
|
||||||
|
add_action('init', [$this, 'block_author_scans'], 0);
|
||||||
|
add_action('init', [$this, 'custom_login_url'], 0);
|
||||||
|
add_action('send_headers', [$this, 'add_security_headers']);
|
||||||
|
add_action('wp_loaded', [$this, 'wp_loaded_custom_login']);
|
||||||
|
|
||||||
|
add_filter('the_generator', '__return_empty_string');
|
||||||
|
add_filter('wp_redirect', [$this, 'redirect_filter'], 10, 2);
|
||||||
|
add_filter('network_site_url', [$this, 'network_url_filter'], 10, 3);
|
||||||
|
add_filter('site_url', [$this, 'site_url_filter'], 10, 4);
|
||||||
|
add_filter('wp_handle_upload_prefilter', [$this, 'filter_uploaded_files']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── wp-login protection ──────────────────────────────────── */
|
||||||
|
|
||||||
|
public function protect_wp_login(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['protect_wp_login'])) return;
|
||||||
|
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
if (strpos($uri, 'wp-login.php') === false) return;
|
||||||
|
|
||||||
|
$ip = $this->get_client_ip();
|
||||||
|
$is_allowed = false;
|
||||||
|
foreach ($this->allowed_ips as $allowed) {
|
||||||
|
if ($ip === $allowed) { $is_allowed = true; break; }
|
||||||
|
if (strpos($allowed, '/') !== false && $this->ip_in_cidr($ip, $allowed)) {
|
||||||
|
$is_allowed = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$is_allowed) {
|
||||||
|
$this->send_403($options, 'Access to login page not allowed from your IP address.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
|
$protocol = $_SERVER['SERVER_PROTOCOL'] ?? '';
|
||||||
|
if (empty($ua) || $protocol === 'HTTP/1.0') {
|
||||||
|
$this->send_403($options, 'Invalid request detected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Security headers ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function add_security_headers(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['add_security_headers'])) return;
|
||||||
|
if (headers_sent()) return;
|
||||||
|
|
||||||
|
header_remove('X-Powered-By');
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sensitive file blocking ──────────────────────────────── */
|
||||||
|
|
||||||
|
public function block_sensitive_files(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($options['protect_wp_includes'])) {
|
||||||
|
if (preg_match('#^/wp-includes/[^/]+\.php$#i', $uri)) {
|
||||||
|
$this->send_403($options, 'Access to this file is not allowed.');
|
||||||
|
}
|
||||||
|
if (preg_match('#^/wp-admin/includes/#i', $uri)) {
|
||||||
|
$this->send_403($options, 'Access to this file is not allowed.');
|
||||||
|
}
|
||||||
|
if (preg_match('#^/wp-includes/theme-compat/#i', $uri)) {
|
||||||
|
$this->send_403($options, 'Access to this file is not allowed.');
|
||||||
|
}
|
||||||
|
if (preg_match('#/wp-includes/js/tinymce/langs/.+\.php#i', $uri)) {
|
||||||
|
$this->send_403($options, 'Access to this file is not allowed.');
|
||||||
|
}
|
||||||
|
if (preg_match('#(license\.txt|wp-config-sample\.php|readme\.html)$#i', $uri)) {
|
||||||
|
$this->send_403($options, 'Access to this file is not allowed.');
|
||||||
|
}
|
||||||
|
if (preg_match('#(?:^|/)\.(?!well-known)#', $uri)) {
|
||||||
|
$this->send_403($options, 'Access to hidden files is not allowed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($options['protect_uploads'])) {
|
||||||
|
if (preg_match('#^/wp-content/uploads/.*\.(?:php[1-6]?|pht|phtml?)$#i', $uri)) {
|
||||||
|
$this->send_403($options, 'PHP files are not allowed in the uploads directory.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($options['block_xmlrpc'])) {
|
||||||
|
if (strpos($uri, 'xmlrpc.php') !== false) {
|
||||||
|
$this->send_403($options, 'XML-RPC is disabled on this site.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Malicious query blocking ─────────────────────────────── */
|
||||||
|
|
||||||
|
public function block_malicious_queries(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['block_malicious_queries'])) return;
|
||||||
|
|
||||||
|
$qs = $_SERVER['QUERY_STRING'] ?? '';
|
||||||
|
if (empty($qs)) return;
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'(eval\()',
|
||||||
|
'(127\.0\.0\.1)',
|
||||||
|
'([a-z0-9]{2000})',
|
||||||
|
'(javascript:)(.*)(;)',
|
||||||
|
'(base64_encode)(.*)(\()',
|
||||||
|
'(GLOBALS|REQUEST)(=|\[|%)',
|
||||||
|
'(<|%3C)(.*)script(.*)(>|%3)',
|
||||||
|
'(boot\.ini|etc/passwd|self/environ)',
|
||||||
|
'(thumbs?(_editor|open)?|tim(thumb)?)\.php',
|
||||||
|
'(\'|\\")(.*)(drop|insert|md5|select|union)',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match('#' . $pattern . '#i', $qs)) {
|
||||||
|
$this->send_403($options, 'Malicious query detected.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = strtolower($_SERVER['REQUEST_METHOD'] ?? '');
|
||||||
|
if (preg_match('#^(connect|debug|delete|move|put|trace|track)$#', $method)) {
|
||||||
|
$this->send_403($options, 'This request method is not allowed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Author scan blocking ─────────────────────────────────── */
|
||||||
|
|
||||||
|
public function block_author_scans(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['block_author_scans'])) return;
|
||||||
|
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
$qs = $_SERVER['QUERY_STRING'] ?? '';
|
||||||
|
|
||||||
|
if (strpos($uri, '/wp-admin') === false && preg_match('/author=\d+/i', $qs)) {
|
||||||
|
wp_redirect(home_url(), 301);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Custom login URL ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
public function custom_login_url(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['enable_custom_login'])) return;
|
||||||
|
|
||||||
|
$slug = $this->custom_slug($options);
|
||||||
|
$path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? '';
|
||||||
|
$qs = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? '';
|
||||||
|
|
||||||
|
if (strpos($path, '/' . $slug) !== false) {
|
||||||
|
if (!session_id()) session_start();
|
||||||
|
$_SESSION['itk_login_access'] = time();
|
||||||
|
require_once ABSPATH . 'wp-login.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocked = ['/wp-login.php', '/wp-admin/', '/login/', '/admin/'];
|
||||||
|
foreach ($blocked as $b) {
|
||||||
|
if (strpos($path, $b) !== false) {
|
||||||
|
if (defined('DOING_AJAX') && DOING_AJAX) return;
|
||||||
|
if (!session_id()) session_start();
|
||||||
|
if (isset($_SESSION['itk_login_access']) && (time() - $_SESSION['itk_login_access']) < 300) return;
|
||||||
|
if (is_user_logged_in()) return;
|
||||||
|
$this->send_403($options, 'Access denied. Please use the correct login URL.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wp_loaded_custom_login(): void {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['enable_custom_login'])) return;
|
||||||
|
|
||||||
|
global $pagenow;
|
||||||
|
if ($pagenow === 'wp-login.php') {
|
||||||
|
if (!session_id()) session_start();
|
||||||
|
if (!isset($_SESSION['itk_login_access'])) {
|
||||||
|
$this->send_403($options, 'Access denied. Please use the correct login URL.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function redirect_filter(string $location, int $status): string {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['enable_custom_login'])) return $location;
|
||||||
|
return str_replace('wp-login.php', $this->custom_slug($options), $location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function network_url_filter(string $url, string $path): string {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['enable_custom_login'])) return $url;
|
||||||
|
if (strpos($path, 'wp-login.php') !== false) {
|
||||||
|
return str_replace('wp-login.php', $this->custom_slug($options), $url);
|
||||||
|
}
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function site_url_filter(string $url, string $path): string {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['enable_custom_login'])) return $url;
|
||||||
|
if (strpos($path, 'wp-login.php') !== false) {
|
||||||
|
return str_replace('wp-login.php', $this->custom_slug($options), $url);
|
||||||
|
}
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── File upload filter ───────────────────────────────────── */
|
||||||
|
|
||||||
|
public function filter_uploaded_files(array $file): array {
|
||||||
|
$options = get_option('itk_security', []);
|
||||||
|
if (empty($options['protect_uploads'])) return $file;
|
||||||
|
|
||||||
|
if (preg_match('/\.(php|phtml|php\d|pht|exe|dll|asp|aspx|jsp|cgi|pl)$/i', $file['name'] ?? '')) {
|
||||||
|
$file['error'] = 'PHP and executable files cannot be uploaded.';
|
||||||
|
}
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Helpers ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
private function send_403(array $options, string $message): void {
|
||||||
|
$code = $options['response_code'] ?? '403';
|
||||||
|
$redir = $options['redirect_url'] ?? '';
|
||||||
|
if ($code === '301_custom' && !empty($redir)) {
|
||||||
|
header('Location: ' . esc_url_raw($redir), true, 301);
|
||||||
|
} else {
|
||||||
|
status_header(403);
|
||||||
|
echo esc_html($message);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function custom_slug(array $options): string {
|
||||||
|
return !empty($options['custom_login_slug']) ? $options['custom_login_slug'] : 'thoushallpass';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function load_allowed_ips(): void {
|
||||||
|
$defaults = ['127.0.0.1', '::1'];
|
||||||
|
if (file_exists($this->allowed_ips_file)) {
|
||||||
|
$lines = file($this->allowed_ips_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line !== '' && $line[0] !== '#') {
|
||||||
|
$this->allowed_ips[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($this->allowed_ips)) {
|
||||||
|
$this->allowed_ips = $defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function get_client_ip(): string {
|
||||||
|
$keys = [
|
||||||
|
'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED',
|
||||||
|
'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED',
|
||||||
|
'REMOTE_ADDR',
|
||||||
|
];
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (empty($_SERVER[$key])) continue;
|
||||||
|
$ip = trim(explode(',', $_SERVER[$key])[0]);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
|
||||||
|
}
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ip_in_cidr(string $ip, string $cidr): bool {
|
||||||
|
[$subnet, $mask] = explode('/', $cidr, 2);
|
||||||
|
if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false;
|
||||||
|
if (!is_numeric($mask) || $mask < 0 || $mask > 32) return false;
|
||||||
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false;
|
||||||
|
$mask_dec = ~((1 << (32 - (int)$mask)) - 1);
|
||||||
|
return (ip2long($ip) & $mask_dec) === (ip2long($subnet) & $mask_dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_allowed_ips(): array { return $this->allowed_ips; }
|
||||||
|
public function get_allowed_ips_file(): string { return $this->allowed_ips_file; }
|
||||||
|
}
|
||||||
147
informatiq-toolkit.php
Normal file
147
informatiq-toolkit.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: InformatiQ Toolkit
|
||||||
|
* Plugin URI: https://informatiq.services
|
||||||
|
* Description: All-in-one security, optimization, and anti-spam toolkit. Bot blocking with dashboard, login protection, honeypot forms, and WordPress optimizations.
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: Mălin Cenușă
|
||||||
|
* Author URI: https://mălin.ro
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
* Text Domain: informatiq-toolkit
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
define('ITK_VERSION', '1.0.0');
|
||||||
|
define('ITK_PATH', plugin_dir_path(__FILE__));
|
||||||
|
define('ITK_URL', plugin_dir_url(__FILE__));
|
||||||
|
define('ITK_BASENAME', plugin_basename(__FILE__));
|
||||||
|
|
||||||
|
require_once ITK_PATH . 'includes/class-itk-database.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-optimization.php';
|
||||||
|
require_once ITK_PATH . 'includes/class-itk-honeypot.php';
|
||||||
|
require_once ITK_PATH . 'includes/class-itk-admin.php';
|
||||||
|
|
||||||
|
class InformatiQ_Toolkit {
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function instance() {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
new ITK_Bot_Blocker();
|
||||||
|
new ITK_Protection();
|
||||||
|
new ITK_Optimization();
|
||||||
|
new ITK_Honeypot();
|
||||||
|
|
||||||
|
if (is_admin()) {
|
||||||
|
new ITK_Admin();
|
||||||
|
}
|
||||||
|
|
||||||
|
add_filter('plugin_action_links_' . ITK_BASENAME, [$this, 'add_settings_link']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_settings_link($links) {
|
||||||
|
array_unshift($links, '<a href="' . admin_url('options-general.php?page=informatiq-toolkit') . '">Settings</a>');
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function activate() {
|
||||||
|
ITK_Database::install();
|
||||||
|
|
||||||
|
// Default security settings
|
||||||
|
if (!get_option('itk_security')) {
|
||||||
|
add_option('itk_security', [
|
||||||
|
'block_openai_bots' => 1,
|
||||||
|
'block_malicious_bots' => 1,
|
||||||
|
'block_bad_referrers' => 1,
|
||||||
|
'block_bad_networks' => 1,
|
||||||
|
'rate_limit_good_bots' => 1,
|
||||||
|
'protect_wp_login' => 1,
|
||||||
|
'protect_wp_includes' => 1,
|
||||||
|
'protect_uploads' => 1,
|
||||||
|
'block_author_scans' => 1,
|
||||||
|
'block_malicious_queries'=> 1,
|
||||||
|
'add_security_headers' => 1,
|
||||||
|
'block_xmlrpc' => 1,
|
||||||
|
'enable_custom_login' => 0,
|
||||||
|
'custom_login_slug' => 'thoushallpass',
|
||||||
|
'response_code' => '301_custom',
|
||||||
|
'redirect_url' => 'https://example.com/blocked',
|
||||||
|
'custom_message' => 'Access denied.',
|
||||||
|
'log_blocked_attempts' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default optimization settings
|
||||||
|
if (!get_option('itk_optimization')) {
|
||||||
|
add_option('itk_optimization', [
|
||||||
|
'remove_wp_version' => 1,
|
||||||
|
'hide_login_errors' => 1,
|
||||||
|
'remove_author_class' => 1,
|
||||||
|
'remove_script_versions' => 1,
|
||||||
|
'change_author_base' => 1,
|
||||||
|
'limit_revisions' => 1,
|
||||||
|
'remove_emoji' => 1,
|
||||||
|
'remove_default_userfields'=> 1,
|
||||||
|
'clean_bad_content' => 1,
|
||||||
|
'remove_wp_head_noise' => 1,
|
||||||
|
'disable_xml_rpc' => 1,
|
||||||
|
'deregister_wp_embed' => 1,
|
||||||
|
'stop_empty_search_redirect'=> 1,
|
||||||
|
'unregister_default_widgets'=> 1,
|
||||||
|
'defer_js' => 1,
|
||||||
|
'limit_heartbeat' => 1,
|
||||||
|
'disable_dashboard_widgets'=> 1,
|
||||||
|
'disable_comments_url' => 1,
|
||||||
|
'disable_floc' => 1,
|
||||||
|
'lightbox_images' => 1,
|
||||||
|
'remove_admin_bar_links' => 1,
|
||||||
|
'admin_branding' => 1,
|
||||||
|
'use_google_jquery' => 0,
|
||||||
|
'featured_image_rss' => 1,
|
||||||
|
'dns_prefetch' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default honeypot settings
|
||||||
|
if (!get_option('itk_honeypot')) {
|
||||||
|
add_option('itk_honeypot', [
|
||||||
|
'enabled' => 1,
|
||||||
|
'protect_comments' => 1,
|
||||||
|
'protect_login' => 1,
|
||||||
|
'protect_register' => 1,
|
||||||
|
'protect_lost_password'=> 1,
|
||||||
|
'protect_woocommerce' => 1,
|
||||||
|
'protect_cf7' => 1,
|
||||||
|
'protect_elementor' => 1,
|
||||||
|
'protect_gravity' => 1,
|
||||||
|
'protect_search' => 1,
|
||||||
|
'min_time' => 3,
|
||||||
|
'max_time' => 7200,
|
||||||
|
'retain_days' => 90,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deactivate() {
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register_activation_hook(__FILE__, ['InformatiQ_Toolkit', 'activate']);
|
||||||
|
register_deactivation_hook(__FILE__, ['InformatiQ_Toolkit', 'deactivate']);
|
||||||
|
|
||||||
|
add_action('plugins_loaded', ['InformatiQ_Toolkit', 'instance']);
|
||||||
20
uninstall.php
Normal file
20
uninstall.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
if (!defined('WP_UNINSTALL_PLUGIN')) exit;
|
||||||
|
|
||||||
|
// Remove all plugin options
|
||||||
|
delete_option('itk_security');
|
||||||
|
delete_option('itk_optimization');
|
||||||
|
delete_option('itk_honeypot');
|
||||||
|
delete_option('itk_hp_secret');
|
||||||
|
delete_option('itk_db_version');
|
||||||
|
|
||||||
|
// Remove transients
|
||||||
|
delete_transient('itk_bots_list');
|
||||||
|
delete_transient('itk_referrers_list');
|
||||||
|
delete_transient('itk_networks_list');
|
||||||
|
delete_transient('itk_goodbots_list');
|
||||||
|
|
||||||
|
// Drop database tables
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}itk_bot_log");
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}itk_honeypot_log");
|
||||||
Reference in New Issue
Block a user