/** * Vigilante Admin JavaScript * * @package Vigilante */ (function($) { 'use strict'; var Vigilante_Admin = { /** * Initialize */ init: function() { this.bindEvents(); this.initModuleToggles(); this.initFileIntegrityPagination(); // Activity log pagination state this.logPage = 1; this.logPerPage = 20; this.logSearchTimer = null; }, /** * Bind events */ bindEvents: function() { // Save settings forms $(document).on('submit', '.vigilante-settings-form', this.handleFormSubmit.bind(this)); // Module toggles in dashboard $(document).on('change', '.vigilante-module-item input[type="checkbox"]', this.handleModuleToggle.bind(this)); // Apply preset $(document).on('click', '.vigilante-preset-btn', this.handleApplyPreset.bind(this)); // Reset section to defaults $(document).on('click', '.vigilante-reset-section-btn', this.handleResetSection.bind(this)); // Clear lockouts $(document).on('click', '.vigilante-clear-lockout', this.handleClearLockout.bind(this)); $(document).on('click', '.vigilante-clear-all-lockouts', this.handleClearAllLockouts.bind(this)); // Unblock firewall rate-limited IPs $(document).on('click', '.vigilante-unblock-firewall-ip', this.handleUnblockFirewallIp.bind(this)); // Clear logs $(document).on('click', '.vigilante-clear-logs', this.handleClearLogs.bind(this)); // Run scan $(document).on('click', '.vigilante-run-scan', this.handleRunScan.bind(this)); $(document).on('click', '.vigilante-ignore-file', this.handleIgnoreFile.bind(this)); $(document).on('click', '.vigilante-unignore-file', this.handleUnignoreFile.bind(this)); $(document).on('click', '.vigilante-clear-ignored', this.handleClearIgnored.bind(this)); // Closed plugins ignore controls (separate slug-based list, not file paths). $(document).on('click', '.vigilante-ignore-closed-plugin', this.handleIgnoreClosedPlugin.bind(this)); $(document).on('click', '.vigilante-unignore-closed-plugin', this.handleUnignoreClosedPlugin.bind(this)); $(document).on('click', '.vigilante-clear-ignored-closed-plugins', this.handleClearIgnoredClosedPlugins.bind(this)); // Bulk selection inside file integrity tables $(document).on('change', '.vigilante-fi-cb-all', this.handleFiSelectAll.bind(this)); $(document).on('change', '.vigilante-fi-cb', this.handleFiCheckboxChange.bind(this)); $(document).on('click', '.vigilante-bulk-ignore', this.handleBulkIgnore.bind(this)); $(document).on('click', '.vigilante-bulk-unignore', this.handleBulkUnignore.bind(this)); // Clear scan results $(document).on('click', '.vigilante-clear-scan', this.handleClearScan.bind(this)); // Critical config file actions $(document).on('click', '.vigilante-approve-critical-file', this.handleApproveCriticalFile.bind(this)); $(document).on('click', '.vigilante-toggle-critical-content', this.handleToggleCriticalContent.bind(this)); // Export/Import settings $(document).on('click', '.vigilante-export-settings', this.handleExportSettings.bind(this)); $(document).on('click', '.vigilante-import-settings', this.handleImportSettings.bind(this)); $(document).on('change', '#vigilante-import-file', this.handleImportFile.bind(this)); // Reset settings $(document).on('click', '.vigilante-reset-settings', this.handleResetSettings.bind(this)); // Test headers $(document).on('click', '.vigilante-test-headers', this.handleTestHeaders.bind(this)); // Login URL notification $(document).on('click', '#vigilante_notify_login_url', this.handleNotifyLoginUrl.bind(this)); $(document).on('input', '#vigilante_custom_login_url', this.handleLoginUrlToggle.bind(this)); // Activity log filters $(document).on('change', '#vigilante-log-type-filter, #vigilante-log-severity-filter, #vigilante-log-method-filter', this.handleLogFilter.bind(this)); $(document).on('click', '#vigilante-log-refresh', this.handleLogFilter.bind(this)); // Activity log search (debounced, min 3 chars) $(document).on('input', '#vigilante-log-search', this.handleLogSearch.bind(this)); // Activity log pagination $(document).on('click', '#vigilante-log-pagination .vigilante-page-first', function() { Vigilante_Admin.goToLogPage('first'); }); $(document).on('click', '#vigilante-log-pagination .vigilante-page-prev', function() { Vigilante_Admin.goToLogPage('prev'); }); $(document).on('click', '#vigilante-log-pagination .vigilante-page-next', function() { Vigilante_Admin.goToLogPage('next'); }); $(document).on('click', '#vigilante-log-pagination .vigilante-page-last', function() { Vigilante_Admin.goToLogPage('last'); }); // File integrity client-side pagination $(document).on('click', '.vigilante-fi-pagination .vigilante-page-first', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'first'); }); $(document).on('click', '.vigilante-fi-pagination .vigilante-page-prev', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'prev'); }); $(document).on('click', '.vigilante-fi-pagination .vigilante-page-next', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'next'); }); $(document).on('click', '.vigilante-fi-pagination .vigilante-page-last', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'last'); }); // Export logs $(document).on('click', '.vigilante-export-logs', this.handleExportLogs.bind(this)); // Create backup $(document).on('click', '.vigilante-create-backup', this.handleCreateBackup.bind(this)); // View log details modal $(document).on('click', '.vigilante-view-log-details', this.handleViewLogDetails.bind(this)); $(document).on('click', '.vigilante-modal-close, .vigilante-modal', this.handleCloseModal.bind(this)); $(document).on('click', '.vigilante-add-to-list', this.handleAddToFirewallList.bind(this)); $(document).on('click', '.vigilante-modal-content', function(e) { e.stopPropagation(); }); // Under Attack mode $(document).on('click', '.vigilante-ua-activate', this.handleUnderAttackActivate.bind(this)); $(document).on('click', '.vigilante-ua-deactivate', this.handleUnderAttackDeactivate.bind(this)); // Database backup $(document).on('click', '.vigilante-db-backup-toggle', this.handleDbBackupToggle.bind(this)); $(document).on('click', '.vigilante-db-backup-download', this.handleDbBackupDownload.bind(this)); $(document).on('change', '#vigilante-db-select-all', this.handleDbSelectAll.bind(this)); $(document).on('change', '.vigilante-db-table-check', this.handleDbTableCheck.bind(this)); // Database prefix $(document).on('click', '.vigilante-db-regenerate-prefix', this.handleDbRegeneratePrefix.bind(this)); $(document).on('change', '#vigilante-prefix-backup-confirm', this.handleDbPrefixConfirmToggle.bind(this)); $(document).on('click', '.vigilante-db-change-prefix', this.handleDbChangePrefix.bind(this)); // Settings search $(document).on('input', '#vigilante-settings-search', this.handleSettingsSearch.bind(this)); $(document).on('focus', '#vigilante-settings-search', this.handleSettingsSearch.bind(this)); $(document).on('keydown', '#vigilante-settings-search', this.handleSettingsSearchKey.bind(this)); $(document).on('mouseenter', '.vigilante-search-item', this.handleSettingsSearchHover.bind(this)); $(document).on('click', function(e) { if (!$(e.target).closest('.vigilante-search-wrapper').length) { $('#vigilante-settings-search-results').attr('hidden', true); } }); // Global "/" shortcut to focus search $(document).on('keydown', function(e) { if (e.key !== '/') return; var $t = $(e.target); if ($t.is('input, textarea, select') || $t.is('[contenteditable="true"]')) return; var $search = $('#vigilante-settings-search'); if (!$search.length) return; e.preventDefault(); $search.focus().select(); }); // Start Under Attack countdown if active this.initUnderAttackCountdown(); }, /** * Initialize module toggles */ initModuleToggles: function() { $(document).on('change', '.vigilante-module-item input[type="checkbox"]', function() { var $item = $(this).closest('.vigilante-module-item'); var module = $(this).data('module'); var enabled = $(this).is(':checked'); $item.toggleClass('enabled', enabled).toggleClass('disabled', !enabled); // Save module state Vigilante_Admin.saveModuleState(module, enabled); }); }, /** * Save module state */ saveModuleState: function(module, enabled) { var $item = $('.vigilante-module-item input[data-module="' + module + '"]').closest('.vigilante-module-item'); // Show saving state $item.addClass('vigilante-saving'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_save_settings', nonce: vigilanteAdmin.nonce, section: 'modules', data: 'modules[' + module + ']=' + (enabled ? '1' : '0') }, success: function(response) { if (response.success) { $item.removeClass('vigilante-saving').addClass('vigilante-saved'); // Show success then reload to update recommendations and all UI Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.saved); setTimeout(function() { location.reload(); }, 800); } else { $item.removeClass('vigilante-saving'); Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error); } }, error: function() { $item.removeClass('vigilante-saving'); Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); } }); }, /** * Update security score after module toggle */ updateSecurityScore: function() { var total = $('.vigilante-module-item').length; var enabled = $('.vigilante-module-item.enabled').length; var score = total > 0 ? Math.round((enabled / total) * 100) : 0; // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29) var grade; if (score >= 90) { grade = 'A'; } else if (score >= 70) { grade = 'B'; } else if (score >= 50) { grade = 'C'; } else if (score >= 30) { grade = 'D'; } else { grade = 'E'; } // Update score display $('.vigilante-score-text').text(score + '%'); $('.vigilante-grade').text(grade); $('.vigilante-score-circle').removeClass('vigilante-grade-a vigilante-grade-b vigilante-grade-c vigilante-grade-d vigilante-grade-e') .addClass('vigilante-grade-' + grade.toLowerCase()); // Update modules count text - use fixed string to prevent text accumulation var $countText = $('.vigilante-security-score p'); if ($countText.length) { var modulesText = (vigilanteAdmin.strings.modulesEnabled || '%1$d / %2$d modules enabled').replace('%1$d', enabled).replace('%2$d', total); $countText.text(modulesText); } }, /** * Update configuration status badge */ updateConfigBadge: function(preset) { var $badge = $('.vigilante-config-status .vigilante-preset-badge'); if ($badge.length) { $badge.removeClass('vigilante-preset-standard vigilante-preset-maximum vigilante-preset-custom vigilante-preset-under-attack'); if (vigilanteAdmin.underAttack && vigilanteAdmin.underAttack.active) { $badge.addClass('vigilante-preset-under-attack').text(vigilanteAdmin.strings.underAttackLabel || 'Under Attack'); } else if (preset === 'standard') { $badge.addClass('vigilante-preset-standard').text(vigilanteAdmin.strings.standardLabel || 'Standard'); } else if (preset === 'maximum') { $badge.addClass('vigilante-preset-maximum').text(vigilanteAdmin.strings.maximumLabel || 'Maximum Security'); } else { $badge.addClass('vigilante-preset-custom').text(vigilanteAdmin.strings.customConfig || 'Custom Configuration'); } } }, /** * Handle form submit */ handleFormSubmit: function(e) { e.preventDefault(); var $form = $(e.currentTarget); var $btn = $form.find('.vigilante-save-btn'); var section = $form.data('section'); $btn.prop('disabled', true).text(vigilanteAdmin.strings.saving); $form.addClass('vigilante-loading'); // Build form data including unchecked checkboxes var formData = Vigilante_Admin.serializeFormWithCheckboxes($form); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_save_settings', nonce: vigilanteAdmin.nonce, section: section, data: formData }, success: function(response) { if (response.success) { var msg = response.data && response.data.message ? response.data.message : (response.data || vigilanteAdmin.strings.saved); Vigilante_Admin.showNotice('success', msg); } else { Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error); } }, error: function(xhr, status, error) { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); }, complete: function() { $btn.prop('disabled', false).text($btn.data('original-text') || vigilanteAdmin.strings.saved); $form.removeClass('vigilante-loading'); // Reset button text after delay setTimeout(function() { $btn.text($btn.data('original-text') || vigilanteAdmin.strings.saveSettings || 'Save Settings'); }, 2000); } }); }, /** * Handle module toggle in dashboard */ handleModuleToggle: function(e) { var $checkbox = $(e.currentTarget); var module = $checkbox.data('module'); var enabled = $checkbox.is(':checked'); var $item = $checkbox.closest('.vigilante-module-item'); // Visual feedback $item.addClass('vigilante-loading'); // Build data for modules section var formData = 'modules[' + module + ']=' + (enabled ? '1' : '0'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_save_settings', nonce: vigilanteAdmin.nonce, section: 'modules', data: formData }, success: function(response) { if (response.success) { // Update visual state if (enabled) { $item.removeClass('disabled').addClass('enabled'); } else { $item.removeClass('enabled').addClass('disabled'); } Vigilante_Admin.updateSecurityScore(); Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.saved); // Update config badge to Custom (since settings were manually changed) Vigilante_Admin.updateConfigBadge('custom'); // Clear active preset indicator $('.vigilante-preset-card').removeClass('vigilante-preset-active'); $('.vigilante-active-indicator').remove(); } else { // Revert checkbox on error $checkbox.prop('checked', !enabled); Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error); } }, error: function() { // Revert checkbox on error $checkbox.prop('checked', !enabled); Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); }, complete: function() { $item.removeClass('vigilante-loading'); } }); }, /** * Serialize form including unchecked checkboxes */ serializeFormWithCheckboxes: function($form) { var data = $form.serializeArray(); var checkboxNames = []; // Find all checkboxes and add unchecked ones with value 0 $form.find('input[type="checkbox"]').each(function() { var name = $(this).attr('name'); if (name && checkboxNames.indexOf(name) === -1) { checkboxNames.push(name); if (!$(this).is(':checked')) { data.push({ name: name, value: '0' }); } } }); // Convert to query string return $.param(data); }, /** * Handle apply preset */ handleApplyPreset: function(e) { e.preventDefault(); var preset = $(e.currentTarget).data('preset'); if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmApplyPreset || 'Apply the "%s" preset?').replace('%s', preset))) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_apply_preset', nonce: vigilanteAdmin.nonce, preset: preset }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', response.data); setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); } }); }, /** * Handle Under Attack mode activation */ handleUnderAttackActivate: function(e) { e.preventDefault(); if (!confirm(vigilanteAdmin.strings.underAttackConfirmActivate)) { return; } var $btn = $(e.currentTarget); $btn.prop('disabled', true).text(vigilanteAdmin.strings.underAttackActivating); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_activate_under_attack', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', response.data.message); setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data); $btn.prop('disabled', false).text(vigilanteAdmin.strings.underAttackActivate || 'Activate for 4 hours'); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); $btn.prop('disabled', false).text(vigilanteAdmin.strings.underAttackActivate || 'Activate for 4 hours'); } }); }, /** * Handle Under Attack mode deactivation */ handleUnderAttackDeactivate: function(e) { e.preventDefault(); if (!confirm(vigilanteAdmin.strings.underAttackConfirmDeactivate)) { return; } var $btn = $(e.currentTarget); $btn.prop('disabled', true).text(vigilanteAdmin.strings.underAttackDeactivating); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_deactivate_under_attack', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', response.data); setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data); $btn.prop('disabled', false).text(vigilanteAdmin.strings.deactivate || 'Deactivate'); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); $btn.prop('disabled', false).text(vigilanteAdmin.strings.deactivate || 'Deactivate'); } }); }, /** * Initialize Under Attack countdown timer */ initUnderAttackCountdown: function() { var $countdown = $('.vigilante-ua-countdown'); if (!$countdown.length) { return; } var expiresAt = parseInt($countdown.data('expires'), 10); if (!expiresAt) { return; } var $timeEl = $countdown.find('.vigilante-ua-time'); var updateCountdown = function() { var now = Math.floor(Date.now() / 1000); var remaining = expiresAt - now; if (remaining <= 0) { location.reload(); return; } var hours = Math.floor(remaining / 3600); var mins = Math.floor((remaining % 3600) / 60); var text = vigilanteAdmin.strings.underAttackRemaining || '%1$dh %2$dm remaining'; text = text.replace('%1$d', hours).replace('%2$d', mins); $timeEl.text(text); }; // Run immediately, then update every minute updateCountdown(); setInterval(updateCountdown, 60000); }, /** * Handle reset section to defaults */ handleResetSection: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var $form = $btn.closest('form'); var section = $form.data('section'); if (!section) { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.couldNotDetermineSection || 'Could not determine section.'); return; } if (!confirm(vigilanteAdmin.strings.confirmResetSection || 'Reset this section to default values? This cannot be undone.')) { return; } var originalText = $btn.data('original-text') || $btn.text(); $btn.prop('disabled', true).html(vigilanteAdmin.strings.loading + ' '); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_reset_section', nonce: vigilanteAdmin.nonce, section: section }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', response.data.message || vigilanteAdmin.strings.sectionResetDefaults || 'Section reset to defaults.'); setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); }, complete: function() { $btn.prop('disabled', false).text(originalText); } }); }, /** * Handle clear single lockout */ handleClearLockout: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var ip = $btn.data('ip'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_clear_lockouts', nonce: vigilanteAdmin.nonce, ip: ip }, success: function(response) { if (response.success) { var $container = $btn.closest('.vigilante-paginated-section'); $btn.closest('tr').fadeOut(function() { $(this).remove(); Vigilante_Admin.refreshFilePagination($container); }); } else { Vigilante_Admin.showNotice('error', response.data); } } }); }, /** * Handle clear all lockouts */ handleClearAllLockouts: function(e) { e.preventDefault(); if (!confirm(vigilanteAdmin.strings.confirm)) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_clear_lockouts', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { location.reload(); } else { Vigilante_Admin.showNotice('error', response.data); } } }); }, /** * Handle clear logs */ handleClearLogs: function(e) { e.preventDefault(); if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmClearLogs || 'This will delete all activity logs.'))) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_clear_logs', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { location.reload(); } else { Vigilante_Admin.showNotice('error', response.data); } } }); }, /** * Handle run scan */ handleRunScan: function(e) { e.preventDefault(); e.stopPropagation(); var $btn = $(e.currentTarget); var $results = $('#vigilante-scan-results'); var $lastResults = $('#vigilante-last-scan-results').closest('.vigilante-settings-section'); // Hide previous results section $lastResults.hide(); $results.empty().hide(); $btn.prop('disabled', true).html(vigilanteAdmin.strings.scanning + ' '); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', timeout: 120000, data: { action: 'vigilante_run_scan', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { // Reload the page so the server-side render shows ALL findings // in their canonical sections (Suspicious, Extra, Critical Config, // Closed + Removed Plugins, Modified). The legacy in-place JS // render only knows the file-level categories and would leave // the Closed + Removed subsection hidden after a scan. Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.scanComplete); setTimeout(function() { location.reload(); }, 600); } else { Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.scanFailed || 'Scan failed'); $btn.prop('disabled', false).text(vigilanteAdmin.strings.runScanNow || 'Run Scan Now'); } }, error: function(xhr, status, error) { Vigilante_Admin.showNotice('error', (vigilanteAdmin.strings.scanError || 'Scan error: %s').replace('%s', error)); $btn.prop('disabled', false).text(vigilanteAdmin.strings.runScanNow || 'Run Scan Now'); } }); }, /** * Handle clear scan results */ handleClearScan: function(e) { e.preventDefault(); e.stopPropagation(); if (!confirm(vigilanteAdmin.strings.confirmClearScan || 'Are you sure you want to clear all scan results?')) { return; } var $btn = $(e.currentTarget); var originalText = $btn.text(); $btn.prop('disabled', true).text(vigilanteAdmin.strings.clearing || 'Clearing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_clear_scan', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { // Hide all scan results $('#vigilante-scan-results').empty().hide(); $('#vigilante-last-scan-results').closest('.vigilante-settings-section').fadeOut(); Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.scanResultsCleared || 'Scan results cleared. Page will reload...'); // Reload page to reflect changes setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.failedClearResults || 'Failed to clear results'); $btn.prop('disabled', false).text(originalText); } }, error: function(xhr, status, error) { Vigilante_Admin.showNotice('error', (vigilanteAdmin.strings.ajaxError || 'AJAX Error: %s').replace('%s', error)); $btn.prop('disabled', false).text(originalText); }, complete: function() { // Button text restored in success/error handlers } }); }, /** * Handle toggle critical file content viewer */ handleToggleCriticalContent: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var targetId = $btn.data('target'); var $content = $('#' + targetId); // Use toggle instead of slideUp/slideDown because target can be a TR // (animations on table rows are inconsistent across browsers) if ($content.is(':visible')) { $content.hide(); $btn.text($btn.data('label-show')); } else { $content.show(); $btn.text($btn.data('label-hide')); } }, /** * Handle approve critical file button click */ handleApproveCriticalFile: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var file = $btn.data('file'); var strings = vigilanteAdmin.strings; var originalText = $btn.text(); // The POST carries an opaque key, never the file name: WAFs like // ModSecurity (OWASP CRS 930130) block any request argument // containing the literal "wp-config.php", which made Approve fail // with a generic AJAX error on hardened hostings. The PHP handler // maps the key back against its own whitelist. var fileKey = { 'wp-config.php': 'cfg', '.htaccess': 'hta' }[ file ] || ''; $btn.prop('disabled', true).text(strings.approving || 'Approving...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_approve_critical_file', nonce: vigilanteAdmin.nonce, file_key: fileKey }, success: function(response) { if (response.success) { // Remove the approved file row and its diff row from the table. // The diff row carries an id derived from the file name (same as // sanitize_html_class() on PHP side), so we target it directly // instead of relying on adjacency in the DOM. var $row = $btn.closest('tr'); var fileId = String(file).replace(/[^a-z0-9_-]/gi, '-'); $('#vigilante-critical-content-' + fileId).remove(); $row.fadeOut(300, function() { $(this).remove(); // If no more critical config rows, remove the entire section if ($('.vigilante-critical-config-files tbody tr').length === 0) { $('.vigilante-critical-config-files').fadeOut(300, function() { $(this).remove(); }); } }); Vigilante_Admin.showNotice('success', strings.criticalApproved || response.data.message); } else { Vigilante_Admin.showNotice('error', response.data || 'Failed to approve.'); $btn.prop('disabled', false).text(originalText); } }, error: function(xhr, status, error) { Vigilante_Admin.showNotice('error', (strings.ajaxError || 'AJAX Error: %s').replace('%s', error)); $btn.prop('disabled', false).text(originalText); } }); }, /** * Handle ignore file button click */ handleIgnoreFile: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var file = $btn.data('file'); var strings = vigilanteAdmin.strings; $btn.prop('disabled', true).text(strings.ignoring || 'Ignoring...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_ignore_file', nonce: vigilanteAdmin.nonce, file: file }, success: function(response) { if (response.success) { var $container = $btn.closest('.vigilante-paginated-section'); // Remove the row from the table $btn.closest('tr').fadeOut(300, function() { $(this).remove(); // Refresh pagination after row removal Vigilante_Admin.refreshFilePagination($container); }); Vigilante_Admin.showNotice('success', strings.fileIgnored || 'File added to ignored list.'); } else { Vigilante_Admin.showNotice('error', response.data || 'Failed to ignore file'); $btn.prop('disabled', false).text(strings.ignore || 'Ignore'); } }, error: function() { $btn.prop('disabled', false).text(strings.ignore || 'Ignore'); } }); }, /** * Handle stop ignoring file */ handleUnignoreFile: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var file = $btn.data('file'); var strings = vigilanteAdmin.strings; $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_unignore_file', nonce: vigilanteAdmin.nonce, file: file }, success: function(response) { if (response.success) { var $container = $btn.closest('.vigilante-paginated-section'); $btn.closest('tr').fadeOut(300, function() { $(this).remove(); Vigilante_Admin.refreshFilePagination($container); }); Vigilante_Admin.showNotice('success', strings.fileUnignored || 'File removed from ignored list.'); } else { Vigilante_Admin.showNotice('error', response.data || 'Failed'); } } }); }, /** * Handle clear all ignored files */ handleClearIgnored: function(e) { e.preventDefault(); var strings = vigilanteAdmin.strings; if (!confirm(strings.confirmClearIgnored || 'Remove all files from the ignored list?')) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_clear_ignored', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', strings.ignoredCleared || 'Ignored files list cleared. Page will reload...'); setTimeout(function() { location.reload(); }, 1000); } } }); }, /** * Ignore a closed/removed plugin slug. Reloads to refresh the section. */ handleIgnoreClosedPlugin: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var slug = $btn.data('slug'); var strings = vigilanteAdmin.strings || {}; $btn.prop('disabled', true).text(strings.ignoring || 'Ignoring…'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_ignore_closed_plugin', nonce: vigilanteAdmin.nonce, slug: slug }, success: function(response) { if (response && response.success) { Vigilante_Admin.showNotice('success', (response.data) || 'Plugin ignored.'); setTimeout(function() { location.reload(); }, 600); } else { Vigilante_Admin.showNotice('error', (response && response.data) || 'Failed.'); $btn.prop('disabled', false).text(strings.ignore || 'Ignore'); } }, error: function(xhr, status, error) { Vigilante_Admin.showNotice('error', (strings.ajaxError || 'AJAX Error: %s').replace('%s', error)); $btn.prop('disabled', false).text(strings.ignore || 'Ignore'); } }); }, /** * Stop ignoring a closed/removed plugin slug. */ handleUnignoreClosedPlugin: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var slug = $btn.data('slug'); var strings = vigilanteAdmin.strings || {}; $btn.prop('disabled', true).text(strings.unignoring || 'Restoring…'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_unignore_closed_plugin', nonce: vigilanteAdmin.nonce, slug: slug }, success: function(response) { if (response && response.success) { Vigilante_Admin.showNotice('success', (response.data) || 'Plugin restored to the active list.'); setTimeout(function() { location.reload(); }, 600); } else { Vigilante_Admin.showNotice('error', (response && response.data) || 'Failed.'); $btn.prop('disabled', false).text(strings.stopIgnoring || 'Stop ignoring'); } }, error: function(xhr, status, error) { Vigilante_Admin.showNotice('error', (strings.ajaxError || 'AJAX Error: %s').replace('%s', error)); $btn.prop('disabled', false).text(strings.stopIgnoring || 'Stop ignoring'); } }); }, /** * Clear the entire ignored closed plugins list. */ handleClearIgnoredClosedPlugins: function(e) { e.preventDefault(); var strings = vigilanteAdmin.strings || {}; if (!confirm(strings.confirmClearIgnoredClosedPlugins || 'Remove all plugins from the ignored closed plugins list? They will reappear in the main list and in email alerts.')) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_clear_ignored_closed_plugins', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response && response.success) { Vigilante_Admin.showNotice('success', (response.data) || 'Ignored closed plugins list cleared.'); setTimeout(function() { location.reload(); }, 1000); } } }); }, /** * Toggle all checkboxes inside a file integrity table when the * header "select all" checkbox changes. Only operates on visible * rows so it plays nice with client-side pagination. */ handleFiSelectAll: function(e) { var $cb = $(e.currentTarget); var $section = $cb.closest('.vigilante-paginated-section'); $section.find('table.vigilante-fi-paginated tbody tr:visible .vigilante-fi-cb').prop('checked', $cb.prop('checked')); this.refreshBulkBar($section); }, /** * Refresh bulk bar state after individual checkbox change. */ handleFiCheckboxChange: function(e) { var $section = $(e.currentTarget).closest('.vigilante-paginated-section'); this.refreshBulkBar($section); // Sync the header "select all" with the current visible state var $allCb = $section.find('.vigilante-fi-cb-all'); var $visible = $section.find('table.vigilante-fi-paginated tbody tr:visible .vigilante-fi-cb'); var $checked = $visible.filter(':checked'); $allCb.prop('checked', $visible.length > 0 && $checked.length === $visible.length); }, /** * Update the count label and enable/disable the bulk action button. */ refreshBulkBar: function($section) { var strings = vigilanteAdmin.strings; var mode = $section.data('bulk-mode'); // 'ignore' or 'unignore' var $checked = $section.find('.vigilante-fi-cb:checked'); var count = $checked.length; var $btn = $section.find(mode === 'unignore' ? '.vigilante-bulk-unignore' : '.vigilante-bulk-ignore'); var $count = $section.find('.vigilante-fi-bulk-count'); $btn.prop('disabled', count === 0); if (count === 0) { $count.text(''); } else { var template = (strings.bulkSelectedCount || '%d selected'); $count.text(template.replace('%d', count)); } }, /** * Collect the file paths checked inside a section. */ collectSelectedFiles: function($section) { var files = []; $section.find('.vigilante-fi-cb:checked').each(function() { var v = $(this).val(); if (v) { files.push(v); } }); return files; }, /** * Bulk add to ignored list. */ handleBulkIgnore: function(e) { e.preventDefault(); var self = this; var strings = vigilanteAdmin.strings; var $btn = $(e.currentTarget); var $section = $btn.closest('.vigilante-paginated-section'); var files = this.collectSelectedFiles($section); if (files.length === 0) { Vigilante_Admin.showNotice('error', strings.bulkNoSelection || 'Select at least one file first.'); return; } if (!confirm(strings.bulkConfirmIgnore || 'Ignore the selected files?')) { return; } var originalText = $btn.text(); $btn.prop('disabled', true).text(strings.bulkProcessing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_bulk_ignore_files', nonce: vigilanteAdmin.nonce, files: files }, success: function(response) { if (response.success) { // Remove processed rows from this section files.forEach(function(file) { $section.find('.vigilante-fi-cb').filter(function() { return $(this).val() === file; }).closest('tr').remove(); }); $section.find('.vigilante-fi-cb-all').prop('checked', false); Vigilante_Admin.refreshFilePagination($section); self.refreshBulkBar($section); Vigilante_Admin.showNotice('success', (response.data && response.data.message) || strings.fileIgnored); } else { Vigilante_Admin.showNotice('error', (response.data && response.data.message) || response.data || 'Failed to ignore files'); $btn.prop('disabled', false).text(originalText); } }, error: function() { $btn.prop('disabled', false).text(originalText); }, // Restore the label on success too. refreshBulkBar() re-derives the // disabled state from the remaining selection but never rewrites the // button text, so without this the button stays on "Processing...". complete: function() { $btn.text(originalText); } }); }, /** * Bulk remove from ignored list. */ handleBulkUnignore: function(e) { e.preventDefault(); var self = this; var strings = vigilanteAdmin.strings; var $btn = $(e.currentTarget); var $section = $btn.closest('.vigilante-paginated-section'); var files = this.collectSelectedFiles($section); if (files.length === 0) { Vigilante_Admin.showNotice('error', strings.bulkNoSelection || 'Select at least one file first.'); return; } if (!confirm(strings.bulkConfirmUnignore || 'Remove the selected files from ignored list?')) { return; } var originalText = $btn.text(); $btn.prop('disabled', true).text(strings.bulkProcessing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_bulk_unignore_files', nonce: vigilanteAdmin.nonce, files: files }, success: function(response) { if (response.success) { files.forEach(function(file) { $section.find('.vigilante-fi-cb').filter(function() { return $(this).val() === file; }).closest('tr').remove(); }); $section.find('.vigilante-fi-cb-all').prop('checked', false); Vigilante_Admin.refreshFilePagination($section); self.refreshBulkBar($section); Vigilante_Admin.showNotice('success', (response.data && response.data.message) || strings.fileUnignored); // If the ignored table is now empty, hide its section. if ($section.find('table.vigilante-fi-paginated tbody tr').length === 0) { $('#vigilante-section-fi-ignored').fadeOut(300, function() { $(this).remove(); }); } } else { Vigilante_Admin.showNotice('error', (response.data && response.data.message) || response.data || 'Failed'); $btn.prop('disabled', false).text(originalText); } }, error: function() { $btn.prop('disabled', false).text(originalText); }, // Restore the label on success too. refreshBulkBar() re-derives the // disabled state from the remaining selection but never rewrites the // button text, so without this the button stays on "Processing...". complete: function() { $btn.text(originalText); } }); }, /** * Display scan results */ displayScanResults: function(results, $container, ignoredCount) { // Ensure arrays exist results.modified = results.modified || []; results.suspicious = results.suspicious || []; results.extra = results.extra || []; results.missing = results.missing || []; var strings = vigilanteAdmin.strings; var html = '
' + strings.suspiciousWarning + '
'; html += bulkBar; html += ''; html += '| ' + strings.file + ' | ' + strings.reason + ' | ' + strings.type + ' | ' + (strings.actions || 'Actions') + ' | |
|---|---|---|---|---|
| '; html += ' | ' + file.file + ' | ';
html += '' + (file.reason || strings.unknown) + ' | '; html += '' + (file.type || strings.unknown) + ' | '; html += ''; html += ' |
' + (strings.extraDescription || 'PHP files not in original distribution.') + '
'; html += bulkBar; html += ''; html += '| ' + strings.file + ' | ' + strings.reason + ' | ' + strings.type + ' | ' + (strings.actions || 'Actions') + ' | |
|---|---|---|---|---|
| '; html += ' | ' + file.file + ' | ';
html += '' + (file.reason || strings.unknown) + ' | '; html += '' + (file.type || strings.unknown) + ' | '; html += ''; html += ' |
' + (strings.criticalConfigDesc || '') + '
'; html += '| ' + strings.file + ' | ' + (strings.changes || 'Changes') + ' | ' + (strings.actions || 'Actions') + ' |
|---|---|---|
' + file.file + ' | ';
html += '';
if (!diffUnavailable) {
html += '+' + added.length + ' -' + removed.length + ' ' + (strings.diffLines || 'lines') + ' '; } html += '' + baselineSize + ' → ' + currentSize + ' bytes'; html += ' | ';
html += ''; html += ' '; html += ''; html += ' | '; html += '
' + strings.modifiedDescription + '
'; html += bulkBar; html += ''; html += '| ' + strings.file + ' | ' + strings.type + ' | ' + (strings.actions || 'Actions') + ' | |
|---|---|---|---|
| '; html += ' | ' + file.file + ' | ';
html += '' + (file.type || strings.unknown) + ' | '; html += ''; html += ' |
' + vigilanteAdmin.strings.score + ': ' + Math.round(results.score) + '%
'; html += '' + vigilanteAdmin.strings.enabledHeaders + ':
' + vigilanteAdmin.strings.missingHeaders + ':
' + vigilanteAdmin.strings.warnings + ':
' + self.escapeHtml(log.ip_address) + '