/** * 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.scanResults + '

'; html += '
'; html += '
'; html += '' + (results.ok || 0) + ''; html += '' + strings.ok + ''; html += '
'; html += '
'; html += '' + results.modified.length + ''; html += '' + strings.modified + ''; html += '
'; html += '
'; html += '' + results.suspicious.length + ''; html += '' + strings.suspicious + ''; html += '
'; html += '
'; html += '' + results.extra.length + ''; html += '' + (strings.extra || 'Extra') + ''; html += '
'; if (ignoredCount > 0) { html += '
'; html += '' + ignoredCount + ''; html += '' + (strings.ignored || 'Ignored') + ''; html += '
'; } html += '
'; var computedTotal = (results.ok || 0) + results.modified.length + results.suspicious.length + results.extra.length + (ignoredCount || 0); html += '' + computedTotal + ''; html += '' + strings.totalScanned + ''; html += '
'; html += '
'; var bulkBar = '
' + '' + '' + '
'; var selectAllCell = ''; // Suspicious files table if (results.suspicious.length > 0) { html += '
'; html += '

' + strings.suspiciousFiles + '

'; html += '

' + strings.suspiciousWarning + '

'; html += bulkBar; html += '
'; html += ''; html += '' + selectAllCell + ''; html += ''; results.suspicious.forEach(function(file) { html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
' + strings.file + '' + strings.reason + '' + strings.type + '' + (strings.actions || 'Actions') + '
' + file.file + '' + (file.reason || strings.unknown) + '' + (file.type || strings.unknown) + '
'; html += '
'; } // Extra files table if (results.extra.length > 0) { html += '
'; html += '

' + (strings.extraFiles || 'Extra Files') + '

'; html += '

' + (strings.extraDescription || 'PHP files not in original distribution.') + '

'; html += bulkBar; html += '
'; html += ''; html += '' + selectAllCell + ''; html += ''; results.extra.forEach(function(file) { html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
' + strings.file + '' + strings.reason + '' + strings.type + '' + (strings.actions || 'Actions') + '
' + file.file + '' + (file.reason || strings.unknown) + '' + (file.type || strings.unknown) + '
'; html += '
'; } // Split critical config from regular modified var criticalModified = []; var regularModified = []; results.modified.forEach(function(file) { if (file.type === 'critical_config') { criticalModified.push(file); } else { regularModified.push(file); } }); // Critical config files section (same visual treatment as suspicious files) if (criticalModified.length > 0) { html += '
'; html += '

' + (strings.criticalConfigTitle || 'Critical config files modified') + '

'; html += '

' + (strings.criticalConfigDesc || '') + '

'; html += ''; html += ''; html += ''; var escHtml = function(s) { return $('').text(s).html(); }; criticalModified.forEach(function(file) { var fileId = file.file.replace(/[^a-z0-9]/gi, '-'); var baselineSize = file.baseline_size ? Number(file.baseline_size).toLocaleString() : '?'; var currentSize = file.current_size ? Number(file.current_size).toLocaleString() : '?'; var diff = file.diff || {}; var added = diff.added || []; var removed = diff.removed || []; var diffUnavailable = !!diff.unavailable; // Main row html += ''; html += ''; html += ''; html += ''; html += ''; // Expandable diff row html += ''; html += ''; html += ''; }); html += '
' + strings.file + '' + (strings.changes || 'Changes') + '' + (strings.actions || 'Actions') + '
' + file.file + ''; if (!diffUnavailable) { html += '+' + added.length + ' -' + removed.length + ' ' + (strings.diffLines || 'lines') + '
'; } html += '' + baselineSize + ' → ' + currentSize + ' bytes'; html += '
'; html += ' '; html += ''; html += '
'; html += '
'; } // Regular modified files table if (regularModified.length > 0) { html += '
'; html += '

' + strings.modifiedFiles + '

'; html += '

' + strings.modifiedDescription + '

'; html += bulkBar; html += '
'; html += ''; html += '' + selectAllCell + ''; html += ''; regularModified.forEach(function(file) { html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; }); html += '
' + strings.file + '' + strings.type + '' + (strings.actions || 'Actions') + '
' + file.file + '' + (file.type || strings.unknown) + '
'; html += '
'; } if (results.modified.length === 0 && results.suspicious.length === 0 && results.extra.length === 0) { html += '
' + strings.allClear + '
'; } $container.html(html).show(); // Initialize client-side pagination for file integrity tables this.initFileIntegrityPagination($container); }, /** * Initialize client-side pagination for file integrity tables * Works on both PHP-rendered (last results) and JS-rendered (live scan) tables * * @param {jQuery} $scope Optional scope to limit search (for live scan results) */ initFileIntegrityPagination: function($scope) { var perPage = 20; var $context = $scope || $(document); $context.find('table.vigilante-fi-paginated').each(function() { var $table = $(this); var $rows = $table.find('tbody tr'); var total = $rows.length; if (total === 0) { return; } // Store state on the parent container var $container = $table.closest('.vigilante-paginated-section'); $container.data('fi-page', 1); $container.data('fi-per-page', perPage); $container.data('fi-total', total); // Hide rows beyond first page (only if more than perPage) if (total > perPage) { $rows.each(function(index) { $(this).toggle(index < perPage); }); } // Build pagination controls (always show count, buttons only when needed) var $wrap = $container.find('.vigilante-fi-pagination-wrap'); var paginationHtml = Vigilante_Admin.buildFilePaginationHtml(1, perPage, total); $wrap.html(paginationHtml); }); }, /** * Build pagination HTML for file integrity tables */ buildFilePaginationHtml: function(page, perPage, total) { var totalPages = Math.ceil(total / perPage) || 1; var from = total > 0 ? ((page - 1) * perPage) + 1 : 0; var to = Math.min(page * perPage, total); var needsNav = total > perPage; var text = total > 0 ? (vigilanteAdmin.strings.paginationOf || '%1$d–%2$d of %3$d').replace('%1$d', from).replace('%2$d', to).replace('%3$d', total) : (vigilanteAdmin.strings.paginationEmpty || '0 items'); var prevDisabled = page <= 1 ? ' disabled' : ''; var nextDisabled = page >= totalPages ? ' disabled' : ''; var html = ''; if (needsNav) { html += ''; html += ''; } html += '' + text + ''; if (needsNav) { html += ''; html += ''; } html += ''; return html; }, /** * Navigate file integrity table pages (client-side) */ goToFilePage: function($container, direction) { var page = $container.data('fi-page') || 1; var perPage = $container.data('fi-per-page') || 20; var total = $container.data('fi-total') || 0; var totalPages = Math.ceil(total / perPage) || 1; switch (direction) { case 'first': page = 1; break; case 'prev': page = Math.max(1, page - 1); break; case 'next': page = Math.min(totalPages, page + 1); break; case 'last': page = totalPages; break; } $container.data('fi-page', page); // Show/hide rows var start = (page - 1) * perPage; var end = start + perPage; $container.find('table.vigilante-fi-paginated tbody tr').each(function(index) { $(this).toggle(index >= start && index < end); }); // Update controls var $wrap = $container.find('.vigilante-fi-pagination-wrap'); $wrap.html(this.buildFilePaginationHtml(page, perPage, total)); // Sync the "select all" header with what is now visible. // Selections in other pages remain checked and will be included // in any bulk action, but the header reflects only this page. var $allCb = $container.find('.vigilante-fi-cb-all'); if ($allCb.length) { var $visible = $container.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); } }, /** * Refresh file integrity pagination after row removal (ignore) */ refreshFilePagination: function($container) { var perPage = $container.data('fi-per-page') || 20; var $rows = $container.find('table.vigilante-fi-paginated tbody tr'); var total = $rows.length; $container.data('fi-total', total); if (total === 0) { $container.find('.vigilante-fi-pagination-wrap').empty(); return; } if (total <= perPage) { // Show all rows, display count only $rows.show(); $container.data('fi-page', 1); var $wrap = $container.find('.vigilante-fi-pagination-wrap'); $wrap.html(this.buildFilePaginationHtml(1, perPage, total)); return; } // Ensure current page is still valid var page = $container.data('fi-page') || 1; var totalPages = Math.ceil(total / perPage); if (page > totalPages) { page = totalPages; } $container.data('fi-page', page); // Show/hide rows for current page var start = (page - 1) * perPage; var end = start + perPage; $rows.each(function(index) { $(this).toggle(index >= start && index < end); }); var $wrap = $container.find('.vigilante-fi-pagination-wrap'); $wrap.html(this.buildFilePaginationHtml(page, perPage, total)); }, /** * Handle export settings */ handleExportSettings: function(e) { e.preventDefault(); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_export_settings', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { var blob = new Blob([response.data.content], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = response.data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } else { Vigilante_Admin.showNotice('error', response.data); } } }); }, /** * Handle import settings click */ handleImportSettings: function(e) { e.preventDefault(); $('#vigilante-import-file').trigger('click'); }, /** * Handle import file selection */ handleImportFile: function(e) { var file = e.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmOverwrite || 'This will overwrite your current settings.'))) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_import_settings', nonce: vigilanteAdmin.nonce, settings: e.target.result }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', response.data); setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.importFailed); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.importFailed); } }); }; reader.readAsText(file); }, /** * Handle reset settings */ handleResetSettings: function(e) { e.preventDefault(); if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmResetAll || 'This will reset ALL settings to defaults.'))) { return; } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_apply_preset', nonce: vigilanteAdmin.nonce, preset: 'reset' }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.settingsResetDefaults || 'Settings reset to defaults.'); setTimeout(function() { location.reload(); }, 1000); } else { Vigilante_Admin.showNotice('error', response.data); } } }); }, /** * Handle test headers */ handleTestHeaders: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var $results = $('#vigilante-headers-result'); $btn.prop('disabled', true).html(vigilanteAdmin.strings.testing + ' '); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_test_headers', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { Vigilante_Admin.displayHeaderResults(response.data, $results); } else { Vigilante_Admin.showNotice('error', response.data); } }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.testHeaders); } }); }, /** * Handle login URL notification button */ handleNotifyLoginUrl: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var $status = $btn.siblings('.vigilante-login-url-notify-status'); $btn.prop('disabled', true).text(vigilanteAdmin.strings.sending || 'Sending...'); $status.removeClass('success error').text(''); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_notify_login_url', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { var msg = response.data.sent + ' ' + (vigilanteAdmin.strings.notificationsSent || 'notifications sent'); if (response.data.failed > 0) { msg += ', ' + response.data.failed + ' ' + (vigilanteAdmin.strings.failed || 'failed'); } $status.addClass('success').text(msg); } else { $status.addClass('error').text(response.data || 'Error'); } }, error: function() { $status.addClass('error').text('Error'); }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.sendNotification || 'Send notification now'); } }); }, /** * Toggle login URL notification section based on URL input */ handleLoginUrlToggle: function() { var hasUrl = $('#vigilante_custom_login_url').val().trim().length > 0; $('.vigilante-login-url-notify-wrapper').toggleClass('vigilante-login-url-notify-disabled', !hasUrl); $('.vigilante-login-url-preview').toggle(hasUrl); }, /** * Display header test results */ displayHeaderResults: function(results, $container) { var gradeClass = 'vigilante-grade-' + results.grade.toLowerCase(); var html = '
'; html += '
'; html += '' + results.grade + ''; html += '
'; html += '

' + vigilanteAdmin.strings.score + ': ' + Math.round(results.score) + '%

'; html += '
'; if (results.headers.length > 0) { html += '

' + vigilanteAdmin.strings.enabledHeaders + ':

'; } if (results.missing.length > 0) { html += '

' + vigilanteAdmin.strings.missingHeaders + ':

'; } if (results.warnings && results.warnings.length > 0) { html += '

' + vigilanteAdmin.strings.warnings + ':

'; } $container.html(html).addClass('visible'); }, /** * Handle activity log filter */ handleLogFilter: function() { this.logPage = 1; this.loadActivityLogs(); }, /** * Handle activity log search with debounce (400ms, min 3 chars) */ handleLogSearch: function() { var self = this; clearTimeout(this.logSearchTimer); this.logSearchTimer = setTimeout(function() { var term = $('#vigilante-log-search').val().trim(); // Only search with 3+ chars, or clear search when empty if (term.length >= 3 || term.length === 0) { self.logPage = 1; self.loadActivityLogs(); } }, 400); }, /** * Load activity logs via AJAX (with filters and pagination) */ loadActivityLogs: function() { var type = $('#vigilante-log-type-filter').val(); var severity = $('#vigilante-log-severity-filter').val(); var method = $('#vigilante-log-method-filter').val(); var search = $('#vigilante-log-search').val(); var $refreshBtn = $('#vigilante-log-refresh'); // Visual feedback $refreshBtn.prop('disabled', true).addClass('updating-message'); var data = { action: 'vigilante_get_logs', nonce: vigilanteAdmin.nonce, per_page: this.logPerPage, page: this.logPage, type: type, severity: severity }; if (method) { data.request_method = method; } if (search && search.trim().length >= 3) { data.search = search.trim(); } $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: data, success: function(response) { if (response.success) { Vigilante_Admin.renderLogTable(response.data.logs); Vigilante_Admin.updateLogPagination(response.data.total); } else { Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error || 'Request failed'); }, complete: function() { $refreshBtn.prop('disabled', false).removeClass('updating-message'); } }); }, /** * Navigate activity log pages */ goToLogPage: function(direction) { var $pagination = $('#vigilante-log-pagination'); var total = parseInt($pagination.attr('data-total'), 10) || 0; var totalPages = Math.ceil(total / this.logPerPage) || 1; switch (direction) { case 'first': this.logPage = 1; break; case 'prev': this.logPage = Math.max(1, this.logPage - 1); break; case 'next': this.logPage = Math.min(totalPages, this.logPage + 1); break; case 'last': this.logPage = totalPages; break; } this.loadActivityLogs(); }, /** * Update activity log pagination controls */ updateLogPagination: function(total) { var $pagination = $('#vigilante-log-pagination'); total = parseInt(total, 10) || 0; var totalPages = Math.ceil(total / this.logPerPage) || 1; var needsNav = total > this.logPerPage; $pagination.attr('data-total', total); $pagination.attr('data-page', this.logPage); var from = total > 0 ? ((this.logPage - 1) * this.logPerPage) + 1 : 0; var to = Math.min(this.logPage * this.logPerPage, total); var text = total > 0 ? (vigilanteAdmin.strings.paginationOf || '%1$d–%2$d of %3$d').replace('%1$d', from).replace('%2$d', to).replace('%3$d', total) : (vigilanteAdmin.strings.paginationEmpty || '0 items'); var prevDisabled = this.logPage <= 1 ? ' disabled' : ''; var nextDisabled = this.logPage >= totalPages ? ' disabled' : ''; var html = ''; if (needsNav) { html += ''; html += ''; } html += '' + text + ''; if (needsNav) { html += ''; html += ''; } $pagination.html(html); }, /** * Render log table */ renderLogTable: function(logs) { var $tbody = $('#vigilante-activity-log-table tbody'); $tbody.empty(); if (logs.length === 0) { $tbody.append('' + (vigilanteAdmin.strings.noLogEntries || 'No log entries found.') + ''); return; } var self = this; var typeLabels = (vigilanteAdmin.strings && vigilanteAdmin.strings.eventTypeLabels) || {}; var severityLabels = (vigilanteAdmin.strings && vigilanteAdmin.strings.severityLabels) || {}; logs.forEach(function(log) { var details = { id: log.id, type: log.event_type || '', action: log.event_action || '', message: log.event_message || '', user: log.user_login || '', ip: log.ip_address || '', user_agent: log.user_agent || '', request_method: log.request_method || '', request_uri: log.request_uri || '', date: log.created_at || '', severity: log.severity || 'info', is_ip_whitelisted: !!log.is_ip_whitelisted, is_ip_blacklisted: !!log.is_ip_blacklisted, is_ua_whitelisted: !!log.is_ua_whitelisted, is_ua_blacklisted: !!log.is_ua_blacklisted }; var displayType = typeLabels[log.event_type] || log.event_type; var displaySeverity = severityLabels[log.severity] || log.severity; var methodHtml = '-'; if (log.request_method) { methodHtml = '' + self.escapeHtml(log.request_method) + ''; } var row = ''; row += '' + self.escapeHtml(log.created_at) + ''; row += '' + self.escapeHtml(displayType) + ''; row += '' + methodHtml + ''; row += '' + self.escapeHtml(displaySeverity) + ''; row += '' + self.escapeHtml(log.event_message) + ''; row += '' + self.escapeHtml(log.user_login || '-') + ''; row += '' + self.escapeHtml(log.ip_address) + ''; row += ''; row += ''; $tbody.append(row); }); }, /** * Handle export logs */ handleExportLogs: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var type = $('#vigilante-log-type-filter').val(); var severity = $('#vigilante-log-severity-filter').val(); var method = $('#vigilante-log-method-filter').val(); var search = $('#vigilante-log-search').val(); var data = { action: 'vigilante_get_logs', nonce: vigilanteAdmin.nonce, per_page: 9999 }; // Only add filters if they have values if (type && type !== '') { data.type = type; } if (severity && severity !== '') { data.severity = severity; } if (method && method !== '') { data.request_method = method; } if (search && search.trim().length >= 3) { data.search = search.trim(); } $btn.prop('disabled', true).text(vigilanteAdmin.strings.exporting || 'Exporting...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: data, success: function(response) { if (response.success && response.data.logs && response.data.logs.length > 0) { Vigilante_Admin.downloadCSV(response.data.logs); Vigilante_Admin.showNotice('success', (vigilanteAdmin.strings.logsExported || 'Logs exported (%d entries)').replace('%d', response.data.logs.length)); } else { Vigilante_Admin.showNotice('warning', vigilanteAdmin.strings.noLogsToExport || 'No logs to export'); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.exportFailed || 'Export failed'); }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.exportLogs || 'Export Logs'); } }); }, /** * Download logs as CSV */ downloadCSV: function(logs) { var csv = 'Date,Type,Method,Severity,Message,User,IP\n'; logs.forEach(function(log) { csv += '"' + log.created_at + '",'; csv += '"' + log.event_type + '",'; csv += '"' + (log.request_method || '') + '",'; csv += '"' + log.severity + '",'; csv += '"' + (log.event_message || '').replace(/"/g, '""') + '",'; csv += '"' + (log.user_login || '') + '",'; csv += '"' + log.ip_address + '"\n'; }); var blob = new Blob([csv], { type: 'text/csv' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'activity-log-' + new Date().toISOString().slice(0, 10) + '.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, /** * Handle create backup */ handleCreateBackup: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); $btn.prop('disabled', true).html(vigilanteAdmin.strings.loading + ' '); // Direct file download via a hidden form (same pattern as the // database backup): the server streams a ZIP with wp-config.php and // .htaccess as an attachment. var $form = $('
', { method: 'POST', action: vigilanteAdmin.ajaxUrl }); $form.append($('', { type: 'hidden', name: 'action', value: 'vigilante_download_files_backup' })); $form.append($('', { type: 'hidden', name: 'nonce', value: vigilanteAdmin.nonce })); $('body').append($form); $form.submit(); $form.remove(); // Re-enable the button after a short delay (download runs in the background). setTimeout(function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.createBackupNow || 'Create Backup Now'); }, 2000); }, // ================================================================= // DATABASE BACKUP HANDLERS // ================================================================= /** * Toggle database backup panel and load tables */ handleDbBackupToggle: function(e) { e.preventDefault(); var $panel = $('.vigilante-db-backup-panel'); var $btn = $(e.currentTarget); if ($panel.is(':visible')) { $panel.slideUp(200); return; } $panel.slideDown(200); // Load tables if not already loaded if ($panel.find('.vigilante-db-tables-content').is(':hidden')) { this.loadDbTables(); } }, /** * Handle settings search input */ searchActiveIndex: -1, handleSettingsSearch: function(e) { var query = ($('#vigilante-settings-search').val() || '').trim().toLowerCase(); var $results = $('#vigilante-settings-search-results'); var index = (vigilanteAdmin.searchIndex || []); if (query.length < 2) { $results.attr('hidden', true).empty(); this.searchActiveIndex = -1; return; } // Score every entry and sort before cutting. It used to stop at the // first 30 matches in index order, so a broad query ("password" // already returns 19) could hide the very row the user was after // behind whatever happened to be declared first. var scored = []; for (var i = 0; i < index.length; i++) { var entry = index[i]; var label = (entry.label || '').toLowerCase(); var labelEn = (entry.label_en || '').toLowerCase(); var section = (entry.section || '').toLowerCase(); var tabLabel = (entry.tab_label || '').toLowerCase(); var keywords = (entry.keywords || '').toLowerCase(); var score = 0; if (label === query || labelEn === query) { score = 100; } else if (label.indexOf(query) === 0 || labelEn.indexOf(query) === 0) { score = 80; } else if (label.indexOf(query) !== -1 || labelEn.indexOf(query) !== -1) { score = 60; } else if (section.indexOf(query) !== -1) { score = 40; } else if (tabLabel.indexOf(query) !== -1) { score = 35; } else if (keywords.indexOf(query) !== -1) { score = 30; } if (score > 0) { scored.push({ entry: entry, score: score, order: i }); } } scored.sort(function(a, b) { return (b.score - a.score) || (a.order - b.order); }); var matches = []; for (var t = 0; t < scored.length && matches.length < 30; t++) { matches.push(scored[t].entry); } var hiddenCount = scored.length - matches.length; if (!matches.length) { $results.html('
' + (vigilanteAdmin.strings.searchNoResults || 'No results') + '
').attr('hidden', false); this.searchActiveIndex = -1; return; } // Group by tab var grouped = {}; var tabOrder = []; for (var j = 0; j < matches.length; j++) { var m = matches[j]; if (!grouped[m.tab]) { grouped[m.tab] = { label: m.tab_label, items: [] }; tabOrder.push(m.tab); } grouped[m.tab].items.push(m); } var html = ''; var flatIdx = 0; for (var k = 0; k < tabOrder.length; k++) { var tab = tabOrder[k]; var group = grouped[tab]; html += '
'; html += '
' + this.escapeHtml(group.label) + '
'; for (var n = 0; n < group.items.length; n++) { var it = group.items[n]; var base = (vigilanteAdmin.adminUrl || 'admin.php?page=vigilante'); var href = base + '&tab=' + encodeURIComponent(it.tab) + '#' + encodeURIComponent(it.anchor); var activeClass = flatIdx === 0 ? ' vigilante-search-item-active' : ''; html += ''; html += '' + this.escapeHtml(it.label) + ''; html += '' + this.escapeHtml(it.section) + ''; html += ''; flatIdx++; } html += '
'; } if (hiddenCount > 0) { var moreTpl = vigilanteAdmin.strings.searchMoreResults || '%d more results, refine the search'; html += '
' + this.escapeHtml(moreTpl.replace('%d', hiddenCount)) + '
'; } $results.html(html).attr('hidden', false); this.searchActiveIndex = 0; }, /** * Handle keyboard navigation in search: arrows move highlight, Enter navigates, Escape closes */ handleSettingsSearchKey: function(e) { var $items = $('#vigilante-settings-search-results .vigilante-search-item'); if (e.key === 'Escape') { $('#vigilante-settings-search-results').attr('hidden', true); $('#vigilante-settings-search').blur(); return; } if (!$items.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); this.searchActiveIndex = Math.min(this.searchActiveIndex + 1, $items.length - 1); this.updateSearchActive(); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.searchActiveIndex = Math.max(this.searchActiveIndex - 1, 0); this.updateSearchActive(); } else if (e.key === 'Home') { e.preventDefault(); this.searchActiveIndex = 0; this.updateSearchActive(); } else if (e.key === 'End') { e.preventDefault(); this.searchActiveIndex = $items.length - 1; this.updateSearchActive(); } else if (e.key === 'Enter') { e.preventDefault(); var idx = this.searchActiveIndex >= 0 ? this.searchActiveIndex : 0; var $target = $items.eq(idx); if ($target.length) { window.location.href = $target.attr('href'); } } }, /** * Sync active highlight when user hovers a search result */ handleSettingsSearchHover: function(e) { var idx = parseInt($(e.currentTarget).attr('data-idx'), 10); if (isNaN(idx)) return; this.searchActiveIndex = idx; this.updateSearchActive(false); }, /** * Apply the active class to the current index and scroll it into view */ updateSearchActive: function(scroll) { var $items = $('#vigilante-settings-search-results .vigilante-search-item'); $items.removeClass('vigilante-search-item-active'); var $active = $items.eq(this.searchActiveIndex); if (!$active.length) return; $active.addClass('vigilante-search-item-active'); if (scroll !== false && $active[0] && typeof $active[0].scrollIntoView === 'function') { $active[0].scrollIntoView({ block: 'nearest' }); } }, /** * Format bytes as a human-readable size */ formatBytes: function(bytes) { if (!bytes || bytes < 0) return '0 B'; var units = ['B', 'KB', 'MB', 'GB', 'TB']; var i = 0; var n = bytes; while (n >= 1024 && i < units.length - 1) { n = n / 1024; i++; } return (i === 0 ? n.toFixed(0) : n.toFixed(1)) + ' ' + units[i]; }, /** * Update DB tables info: count + total size */ updateDbTablesInfo: function() { var $boxes = $('.vigilante-db-table-check'); var total = $boxes.length; if (!total) { $('.vigilante-db-tables-info').text(''); return; } var $checked = $boxes.filter(':checked'); var checkedCount = $checked.length; var bytes = 0; $checked.each(function() { bytes += parseInt($(this).attr('data-bytes'), 10) || 0; }); var sizeStr = Vigilante_Admin.formatBytes(bytes); var tpl; if (checkedCount === total) { tpl = vigilanteAdmin.strings.dbTablesTotal || '%1$d tables total (%2$s)'; $('.vigilante-db-tables-info').text(tpl.replace('%1$d', total).replace('%2$s', sizeStr)); } else { tpl = vigilanteAdmin.strings.dbTablesSelected || '%1$d tables selected (%2$s)'; $('.vigilante-db-tables-info').text(tpl.replace('%1$d', checkedCount).replace('%2$s', sizeStr)); } }, /** * Load database tables via AJAX */ loadDbTables: function() { $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_get_db_tables', nonce: vigilanteAdmin.nonce }, success: function(response) { if (!response.success) { Vigilante_Admin.showNotice('error', response.data); return; } var data = response.data; var coreHtml = ''; var otherHtml = ''; // Build core tables checkboxes if (data.core && data.core.length) { $.each(data.core, function(i, table) { coreHtml += ''; }); } // Build other tables checkboxes if (data.other && data.other.length) { $.each(data.other, function(i, table) { otherHtml += ''; }); $('#vigilante-db-other-group').show(); } $('#vigilante-db-core-tables').html(coreHtml); $('#vigilante-db-other-tables').html(otherHtml); Vigilante_Admin.updateDbTablesInfo(); $('.vigilante-db-tables-loading').hide(); $('.vigilante-db-tables-content').show(); } }); }, /** * Select/deselect all tables */ handleDbSelectAll: function(e) { var checked = $(e.currentTarget).prop('checked'); $('.vigilante-db-table-check').prop('checked', checked); this.updateDbTablesInfo(); }, /** * Update select-all state when individual checkbox changes */ handleDbTableCheck: function() { var total = $('.vigilante-db-table-check').length; var checked = $('.vigilante-db-table-check:checked').length; $('#vigilante-db-select-all').prop('checked', total === checked); this.updateDbTablesInfo(); }, /** * Download database backup */ handleDbBackupDownload: function(e) { e.preventDefault(); var tables = []; $('.vigilante-db-table-check:checked').each(function() { tables.push($(this).val()); }); if (tables.length === 0) { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.dbBackupNoTables); return; } var $btn = $(e.currentTarget); $btn.prop('disabled', true).html(vigilanteAdmin.strings.dbBackupDownloading + ' '); // Submit via hidden form for direct file download var $form = $('', { method: 'POST', action: vigilanteAdmin.ajaxUrl }); $form.append($('', { type: 'hidden', name: 'action', value: 'vigilante_download_db_backup' })); $form.append($('', { type: 'hidden', name: 'nonce', value: vigilanteAdmin.nonce })); $form.append($('', { type: 'hidden', name: 'tables', value: tables.join(',') })); $('body').append($form); $form.submit(); $form.remove(); // Re-enable button after a delay (download starts in background) setTimeout(function() { $btn.prop('disabled', false).text($btn.data('original-text') || vigilanteAdmin.strings.downloadBackup || 'Download Backup (.zip)'); Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.dbBackupSuccess); }, 3000); }, // ================================================================= // DATABASE PREFIX HANDLERS // ================================================================= /** * Regenerate random prefix */ handleDbRegeneratePrefix: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); $btn.prop('disabled', true); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_generate_prefix', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { $('#vigilante-new-prefix').text(response.data.prefix); } else { Vigilante_Admin.showNotice('error', response.data); } }, complete: function() { $btn.prop('disabled', false); } }); }, /** * Toggle the change prefix button based on checkbox */ handleDbPrefixConfirmToggle: function(e) { var checked = $(e.currentTarget).prop('checked'); $('.vigilante-db-change-prefix').prop('disabled', !checked); }, /** * Change database prefix */ handleDbChangePrefix: function(e) { e.preventDefault(); // Verify checkbox is checked if (!$('#vigilante-prefix-backup-confirm').prop('checked')) { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.dbPrefixCheckbox); return; } var newPrefix = $('#vigilante-new-prefix').text().trim(); if (!confirm(vigilanteAdmin.strings.dbPrefixConfirm)) { return; } var $btn = $(e.currentTarget); $btn.prop('disabled', true).html(vigilanteAdmin.strings.dbPrefixChanging + ' '); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_change_prefix', nonce: vigilanteAdmin.nonce, prefix: newPrefix }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.dbPrefixSuccess); // Reload page after prefix change (session will still work) setTimeout(function() { location.reload(); }, 2000); } else { Vigilante_Admin.showNotice('error', response.data); $btn.prop('disabled', false).text($btn.data('original-text')); } }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); $btn.prop('disabled', false).text($btn.data('original-text')); } }); }, /** * Show notice */ showNotice: function(type, message) { // Remove any existing notices first. $('.vigilante-notice').remove(); var $notice = $('
' + message + '
'); $('.vigilante-main').prepend($notice); // Scroll to top of the settings area so user sees the notice. $('html, body').animate({ scrollTop: $('.vigilante-admin-wrap').offset().top - 50 }, 300); setTimeout(function() { $notice.fadeOut(function() { $(this).remove(); }); }, 5000); }, /** * Handle view log details */ handleViewLogDetails: function(e) { e.preventDefault(); e.stopPropagation(); var $btn = $(e.currentTarget); var detailsStr = $btn.attr('data-details'); if (!detailsStr) { return; } var details; try { details = JSON.parse(detailsStr); } catch (err) { return; } var html = ''; var s = vigilanteAdmin.strings; // -- Section: Request -- html += '
'; html += '

' + this.escapeHtml(s.logRequest || 'Request') + '

'; html += ''; html += ''; if (details.request_method) { html += ''; } html += ''; html += ''; html += ''; html += ''; if (details.request_uri) { html += ''; } html += '
' + this.escapeHtml(s.logDate || 'Date') + '' + this.escapeHtml(details.date) + '
' + this.escapeHtml(s.logMethod || 'Method') + '' + this.escapeHtml(details.request_method) + '
' + this.escapeHtml(s.logType || 'Type') + '' + this.escapeHtml(details.type) + '
' + this.escapeHtml(s.logAction || 'Action') + '' + this.escapeHtml(details.action || '-') + '
' + this.escapeHtml(s.logSeverity || 'Severity') + '' + this.escapeHtml(details.severity || 'info') + '
' + this.escapeHtml(s.logMessage || 'Message') + '' + this.escapeHtml(details.message) + '
' + this.escapeHtml(s.logRequestUri || 'Address') + '' + this.escapeHtml(details.request_uri) + '
'; html += '
'; // -- Section: Client -- html += '
'; html += '

' + this.escapeHtml(s.logClient || 'Client') + '

'; html += ''; html += ''; // IP with lookup link if (details.ip && details.ip !== '0.0.0.0') { var ipEscaped = this.escapeHtml(details.ip); html += ''; } else { html += ''; } if (details.user_agent) { html += ''; } html += '
' + this.escapeHtml(s.logUser || 'User') + '' + this.escapeHtml(details.user || '-') + '
' + this.escapeHtml(s.logIpAddress || 'IP Address') + ''; html += '' + ipEscaped + ' '; html += ''; html += ''; html += '
' + this.escapeHtml(s.logIpAddress || 'IP Address') + '' + this.escapeHtml(details.ip) + '
' + this.escapeHtml(s.logUserAgent || 'User Agent') + '' + this.escapeHtml(details.user_agent) + '
'; // Action buttons for IP and UA html += '
'; if (details.ip && details.ip !== '0.0.0.0') { html += '' + this.escapeHtml(s.logIpLabel || 'IP:') + ''; if (details.is_ip_whitelisted) { html += ' '; } else { html += ' '; } if (details.is_ip_blacklisted) { html += ' '; } else { html += ' '; } } if (details.user_agent) { html += '' + this.escapeHtml(s.logUaLabel || 'UA:') + ''; if (details.is_ua_whitelisted) { html += ' '; } else { html += ' '; } if (details.is_ua_blacklisted) { html += ''; } else { html += ''; } } html += '
'; html += ''; html += '
'; // Event ID for reference html += ''; $('#vigilante-log-details-content').html(html); $('#vigilante-log-details-modal').fadeIn(200); }, /** * Handle adding IP or UA to firewall whitelist/blacklist */ handleAddToFirewallList: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var itemType = $btn.data('item-type'); var listType = $btn.data('list-type'); var value = $btn.data('value'); if (!itemType || !listType || !value) { return; } $btn.prop('disabled', true); var self = this; $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_add_to_firewall_list', nonce: vigilanteAdmin.nonce, item_type: itemType, list_type: listType, value: value }, success: function(response) { var $feedback = $('.vigilante-popup-feedback'); if (response.success) { $btn.text(vigilanteAdmin.strings.logAdded || 'Added!').addClass('vigilante-btn-added'); $feedback.html('' + response.data + '').slideDown(150); } else { $btn.prop('disabled', false); $feedback.html('' + (response.data || vigilanteAdmin.strings.logErrorAddingToList || 'Error adding to list') + '').slideDown(150); } }, error: function() { $btn.prop('disabled', false); $('.vigilante-popup-feedback').html('' + (vigilanteAdmin.strings.logRequestFailed || 'Request failed') + '').slideDown(150); } }); }, /** * Handle unblock firewall IP */ handleUnblockFirewallIp: function(e) { var $btn = $(e.currentTarget); var ip = $btn.data('ip'); if (!confirm(vigilanteAdmin.strings.confirmUnblockIp || 'Unblock this IP?')) { return; } $btn.prop('disabled', true).text(vigilanteAdmin.strings.loading || 'Loading...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_unblock_firewall_ip', nonce: vigilanteAdmin.nonce, ip: ip }, success: function(response) { if (response.success) { $btn.closest('tr').fadeOut(300, function() { var $tbody = $(this).closest('tbody'); $(this).remove(); // Hide the whole section if no more blocked IPs if ($tbody.children('tr').length === 0) { $tbody.closest('.vigilante-lockout-section').fadeOut(300); } }); } else { alert(response.data || 'Error'); $btn.prop('disabled', false).text(vigilanteAdmin.strings.unblock || 'Unblock'); } }, error: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.unblock || 'Unblock'); } }); }, /** * Handle close modal */ handleCloseModal: function(e) { if ($(e.target).hasClass('vigilante-modal') || $(e.target).hasClass('vigilante-modal-close')) { $('.vigilante-modal').hide(); } }, /** * Escape HTML */ escapeHtml: function(text) { if (text === null || text === undefined) return ''; if (typeof text !== 'string') { text = String(text); } var div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, /** * Debounce function */ debounce: function(func, wait) { var timeout; return function() { var context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; }, // ================================================================= // Force Password Reset functionality // ================================================================= /** * Selected users for password reset */ selectedPasswordResetUsers: [], /** * Initialize password reset handlers */ initPasswordReset: function() { var self = this; // User search for password reset var searchTimeout; $(document).on('input', '#vigilante-password-reset-search', function() { var query = $(this).val(); clearTimeout(searchTimeout); if (query.length < 2) { $('#vigilante-password-reset-results').hide().empty(); return; } searchTimeout = setTimeout(function() { self.searchUsersForPasswordReset(query); }, 300); }); // Close search results when clicking outside $(document).on('click', function(e) { if (!$(e.target).closest('.vigilante-user-search-wrapper').length) { $('#vigilante-password-reset-results').hide(); } }); // Reset selected users button $(document).on('click', '#vigilante-reset-selected-users', function(e) { e.preventDefault(); self.forceResetSelectedUsers(); }); // Reset all users button $(document).on('click', '#vigilante-reset-all-users', function(e) { e.preventDefault(); self.forceResetAllUsers(); }); // Reset by role - checkbox change $(document).on('change', '.vigilante-reset-role-checkbox', function() { self.updateRoleResetSummary(); }); // Reset by role button $(document).on('click', '#vigilante-reset-by-role', function(e) { e.preventDefault(); self.forceResetByRoles(); }); }, /** * Search users for password reset */ searchUsersForPasswordReset: function(query) { var self = this; var $results = $('#vigilante-password-reset-results'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_search_users_password_reset', nonce: vigilanteAdmin.nonce, query: query }, success: function(response) { if (response.success && response.data.length > 0) { var html = ''; $results.html(html).show(); // Handle user selection $results.find('li').on('click', function() { var userId = parseInt($(this).data('user-id')); var userName = $(this).find('strong').text(); var userEmail = $(this).find('small').first().text(); self.togglePasswordResetUser(userId, userName, userEmail); $(this).toggleClass('selected'); }); } else { $results.html('

' + (vigilanteAdmin.strings.noUsersFoundSearch || 'No users found') + '

').show(); } } }); }, /** * Toggle user selection for password reset */ togglePasswordResetUser: function(userId, userName, userEmail) { var index = this.selectedPasswordResetUsers.indexOf(userId); var $selectedContainer = $('#vigilante-password-reset-selected'); var $list = $selectedContainer.find('.vigilante-selected-users-list'); var $btn = $('#vigilante-reset-selected-users'); if (index === -1) { // Add user this.selectedPasswordResetUsers.push(userId); $list.append( '
  • ' + '' + this.escapeHtml(userName) + '' + '' + '
  • ' ); } else { // Remove user this.selectedPasswordResetUsers.splice(index, 1); $list.find('li[data-user-id="' + userId + '"]').remove(); } // Update UI if (this.selectedPasswordResetUsers.length > 0) { $selectedContainer.show(); $btn.prop('disabled', false); } else { $selectedContainer.hide(); $btn.prop('disabled', true); } // Handle remove button click $list.find('.vigilante-remove-user').off('click').on('click', function() { var uid = $(this).closest('li').data('user-id'); var idx = Vigilante_Admin.selectedPasswordResetUsers.indexOf(uid); if (idx !== -1) { Vigilante_Admin.selectedPasswordResetUsers.splice(idx, 1); } $(this).closest('li').remove(); $('#vigilante-password-reset-results').find('li[data-user-id="' + uid + '"]').removeClass('selected'); if (Vigilante_Admin.selectedPasswordResetUsers.length === 0) { $selectedContainer.hide(); $btn.prop('disabled', true); } }); }, /** * Force reset for selected users */ forceResetSelectedUsers: function() { var self = this; if (this.selectedPasswordResetUsers.length === 0) { return; } var currentUserId = parseInt(vigilanteAdmin.currentUserId || 0); var resettingSelf = this.selectedPasswordResetUsers.indexOf(currentUserId) !== -1; var confirmMsg = (vigilanteAdmin.strings.confirmForceReset || 'Force password reset for %d user(s)? A password reset email will be sent to each user.').replace('%d', this.selectedPasswordResetUsers.length); if (resettingSelf) { confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelf || 'WARNING: You are including yourself. Your session will end and you will need to set a new password.'); } if (!confirm(confirmMsg)) { return; } var $btn = $('#vigilante-reset-selected-users'); $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_force_password_reset', nonce: vigilanteAdmin.nonce, user_ids: this.selectedPasswordResetUsers }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); // Clear selection self.selectedPasswordResetUsers = []; $('#vigilante-password-reset-selected').hide().find('.vigilante-selected-users-list').empty(); $('#vigilante-password-reset-search').val(''); $('#vigilante-password-reset-results').hide().empty(); // Reload if resetting self if (response.data.resetting_self) { setTimeout(function() { window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php'; }, 2000); } else { setTimeout(function() { location.reload(); }, 1500); } } else { self.showNotice('error', response.data); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred'); }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetSelected || 'Force Reset for Selected Users'); } }); }, /** * Force reset for all users */ forceResetAllUsers: function() { var self = this; var includeSelf = $('#vigilante-reset-all-include-self').is(':checked'); var confirmMsg = vigilanteAdmin.strings.confirmForceResetAll || 'This will force ALL users to reset their password. All users will receive a password reset email. Are you sure you want to continue?'; if (includeSelf) { confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelfAll || 'WARNING: You are including yourself. Your session will end immediately.'); } if (!confirm(confirmMsg)) { return; } var $btn = $('#vigilante-reset-all-users'); $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_force_password_reset_all', nonce: vigilanteAdmin.nonce, include_self: includeSelf ? 1 : 0 }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); // Reload if resetting self if (response.data.resetting_self) { setTimeout(function() { window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php'; }, 2000); } else { setTimeout(function() { location.reload(); }, 1500); } } else { self.showNotice('error', response.data); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred'); }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetAll || 'Force Reset for ALL Users'); } }); }, /** * Update role reset summary when checkboxes change */ updateRoleResetSummary: function() { var totalCount = 0; var includesSelf = false; $('.vigilante-reset-role-checkbox:checked').each(function() { totalCount += parseInt($(this).data('count') || 0); if ($(this).closest('label').find('em').length > 0) { includesSelf = true; } }); var $summary = $('#vigilante-reset-role-summary'); var $selfOption = $('#vigilante-reset-role-self-option'); var $btn = $('#vigilante-reset-by-role'); if (totalCount > 0) { $('#vigilante-reset-role-count').text(totalCount); $summary.show(); $btn.prop('disabled', false); } else { $summary.hide(); $btn.prop('disabled', true); } if (includesSelf) { $selfOption.show(); } else { $selfOption.hide(); $('#vigilante-reset-role-include-self').prop('checked', false); } }, /** * Force reset for users with selected roles */ forceResetByRoles: function() { var self = this; var roles = []; $('.vigilante-reset-role-checkbox:checked').each(function() { roles.push($(this).val()); }); if (roles.length === 0) { self.showNotice('error', vigilanteAdmin.strings.noRolesSelected || 'Please select at least one role.'); return; } var totalCount = 0; $('.vigilante-reset-role-checkbox:checked').each(function() { totalCount += parseInt($(this).data('count') || 0); }); var includeSelf = $('#vigilante-reset-role-include-self').is(':checked'); var confirmMsg = (vigilanteAdmin.strings.confirmForceResetByRole || 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.').replace('%d', totalCount); if (includeSelf) { confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelfAll || 'WARNING: You are including yourself. Your session will end immediately.'); } if (!confirm(confirmMsg)) { return; } var $btn = $('#vigilante-reset-by-role'); $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_force_password_reset_by_role', nonce: vigilanteAdmin.nonce, roles: roles, include_self: includeSelf ? 1 : 0 }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); // Clear role checkboxes $('.vigilante-reset-role-checkbox').prop('checked', false); self.updateRoleResetSummary(); if (response.data.resetting_self) { setTimeout(function() { window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php'; }, 2000); } else { setTimeout(function() { location.reload(); }, 1500); } } else { self.showNotice('error', response.data); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred'); }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetByRole || 'Force Reset for Selected Roles'); } }); }, /** * Initialize user approval handlers */ initUserApproval: function() { var self = this; // Approve user $(document).on('click', '.vigilante-approve-user', function() { var $btn = $(this); var userId = $btn.data('user-id'); var $row = $btn.closest('tr'); if (!confirm(vigilanteAdmin.strings.confirmApprove || 'Approve this user?')) { return; } $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_approve_user', nonce: vigilanteAdmin.nonce, user_id: userId }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); $row.fadeOut(400, function() { $(this).remove(); self.updatePendingCount(); }); } else { self.showNotice('error', response.data); $btn.prop('disabled', false).text(vigilanteAdmin.strings.approve || 'Approve'); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.error); $btn.prop('disabled', false).text(vigilanteAdmin.strings.approve || 'Approve'); } }); }); // Reject user $(document).on('click', '.vigilante-reject-user', function() { var $btn = $(this); var userId = $btn.data('user-id'); var $row = $btn.closest('tr'); var reason = prompt(vigilanteAdmin.strings.rejectReason || 'Enter rejection reason (optional):'); if (reason === null) { return; // Cancelled } $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_reject_user', nonce: vigilanteAdmin.nonce, user_id: userId, reason: reason }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); $row.fadeOut(400, function() { $(this).remove(); self.updatePendingCount(); }); } else { self.showNotice('error', response.data); $btn.prop('disabled', false).text(vigilanteAdmin.strings.reject || 'Reject'); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.error); $btn.prop('disabled', false).text(vigilanteAdmin.strings.reject || 'Reject'); } }); }); }, /** * Update pending users count badge */ updatePendingCount: function() { var $badge = $('.vigilante-pending-users-section .vigilante-badge-warning'); var $table = $('.vigilante-pending-users-table tbody tr'); var count = $table.length; if (count === 0) { $badge.remove(); $('.vigilante-pending-users-table').replaceWith( '
    ' + '' + '

    ' + (vigilanteAdmin.strings.noPending || 'No pending registrations.') + '

    ' + '
    ' ); } else { $badge.text(count); } }, /** * Initialize session management handlers */ initSessionManagement: function() { var self = this; var searchTimer = null; this.selectedSessionUserId = null; // Search users for session management $(document).on('input', '#vigilante-session-user-search', function() { var query = $(this).val(); var $results = $('#vigilante-session-search-results'); clearTimeout(searchTimer); if (query.length < 2) { $results.hide().empty(); return; } searchTimer = setTimeout(function() { $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_search_users_password_reset', nonce: vigilanteAdmin.nonce, query: query }, success: function(response) { if (response.success && response.data.length > 0) { var html = ''; $results.html(html).show(); } else { $results.html('

    ' + (vigilanteAdmin.strings.noUsers || 'No users found') + '

    ').show(); } } }); }, 300); }); // Click handler for user selection in session search $(document).on('click', '#vigilante-session-search-results li', function() { var userId = $(this).data('user-id'); var userName = $(this).find('strong').text(); self.loadUserSessions(userId, userName); $('#vigilante-session-search-results').hide(); $('#vigilante-session-user-search').val(userName); }); // Revoke single session $(document).on('click', '.vigilante-revoke-session', function() { var $btn = $(this); var userId = $btn.attr('data-user-id'); var token = $btn.attr('data-token'); var $row = $btn.closest('tr'); var $paginatedContainer = $btn.closest('.vigilante-paginated-section'); // Fallback to row token if button token is invalid if (!token || token.length < 32) { token = $row.attr('data-token'); } if (!token || token.length < 32) { self.showNotice('error', vigilanteAdmin.strings.error || 'Invalid session token'); return; } if (!confirm(vigilanteAdmin.strings.confirmRevoke || 'Revoke this session?')) { return; } $btn.prop('disabled', true); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_revoke_session', nonce: vigilanteAdmin.nonce, user_id: userId, token: token }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); $row.fadeOut(400, function() { $(this).remove(); if ($paginatedContainer.length) { Vigilante_Admin.refreshFilePagination($paginatedContainer); } }); } else { self.showNotice('error', response.data); $btn.prop('disabled', false); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.error); $btn.prop('disabled', false); } }); }); // Revoke all other sessions (current user) $(document).on('click', '.vigilante-revoke-other-sessions', function() { var $btn = $(this); var userId = $btn.data('user-id'); if (!confirm(vigilanteAdmin.strings.confirmRevokeAll || 'Revoke all other sessions?')) { return; } $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_revoke_all_sessions', nonce: vigilanteAdmin.nonce, user_id: userId, include_current: 0 }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); setTimeout(function() { location.reload(); }, 1000); } else { self.showNotice('error', response.data); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.error); }, complete: function() { $btn.prop('disabled', false).text(vigilanteAdmin.strings.revokeOthers || 'Revoke All Other Sessions'); } }); }); // Revoke all sessions for a user (admin managing other users) $(document).on('click', '.vigilante-revoke-all-user-sessions', function() { var $btn = $(this); if (!self.selectedSessionUserId) { return; } if (!confirm(vigilanteAdmin.strings.confirmRevokeAllUser || 'Revoke ALL sessions for this user? They will be logged out everywhere.')) { return; } $btn.prop('disabled', true); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_revoke_all_sessions', nonce: vigilanteAdmin.nonce, user_id: self.selectedSessionUserId, include_current: 1 }, success: function(response) { if (response.success) { self.showNotice('success', response.data.message); $('#vigilante-user-sessions-container').hide(); $('#vigilante-session-user-search').val(''); } else { self.showNotice('error', response.data); } }, error: function() { self.showNotice('error', vigilanteAdmin.strings.error); }, complete: function() { $btn.prop('disabled', false); } }); }); }, /** * Load sessions for a specific user */ loadUserSessions: function(userId, userName) { var self = this; this.selectedSessionUserId = userId; var $container = $('#vigilante-user-sessions-container'); var $list = $('#vigilante-user-sessions-list'); $list.html('' + (vigilanteAdmin.strings.loading || 'Loading...') + ''); $container.show(); $('#vigilante-sessions-user-name').text((vigilanteAdmin.strings.sessionsFor || 'Sessions for: ') + userName); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_get_user_sessions', nonce: vigilanteAdmin.nonce, user_id: userId }, success: function(response) { if (response.success) { var sessions = response.data.sessions; if (sessions.length === 0) { $list.html('' + (vigilanteAdmin.strings.noSessions || 'No active sessions') + ''); return; } var html = ''; $.each(sessions, function(i, session) { var loginTime = session.login ? self.timeAgo(session.login) : (vigilanteAdmin.strings.unknown || 'Unknown'); html += '' + '' + self.escapeHtml(session.browser) + '' + '' + self.escapeHtml(session.ip) + '' + '' + loginTime + '' + '' + '' + '' + ''; }); $list.html(html); } else { $list.html('' + response.data + ''); } }, error: function() { $list.html('' + vigilanteAdmin.strings.error + ''); } }); }, /** * Format timestamp as time ago */ timeAgo: function(timestamp) { var seconds = Math.floor((Date.now() / 1000) - timestamp); var s = vigilanteAdmin.strings; var intervals = [ { label: s.timeYear || 'year', labelPlural: s.timeYears || 'years', seconds: 31536000 }, { label: s.timeMonth || 'month', labelPlural: s.timeMonths || 'months', seconds: 2592000 }, { label: s.timeDay || 'day', labelPlural: s.timeDays || 'days', seconds: 86400 }, { label: s.timeHour || 'hour', labelPlural: s.timeHours || 'hours', seconds: 3600 }, { label: s.timeMinute || 'minute', labelPlural: s.timeMinutes || 'minutes', seconds: 60 } ]; for (var i = 0; i < intervals.length; i++) { var count = Math.floor(seconds / intervals[i].seconds); if (count >= 1) { var unit = count > 1 ? intervals[i].labelPlural : intervals[i].label; return (s.timeAgo || '%1$d %2$s ago').replace('%1$d', count).replace('%2$s', unit); } } return s.justNow || 'Just now'; } }; /* --------------------------------------------------------------------- * Security Analyzer (Dashboard widget) — v2.1.0 * ------------------------------------------------------------------ */ var VigilanteAnalyzer = { $root: null, init: function() { this.$root = $('#vigilante-analyzer'); if (!this.$root.length) { // Widget not present on this page — still run hash focus in case a fix link was followed. this.hashFocusFlash(); return; } // Scan button — 2-phase AJAX. $(document).on('click', '#vigilante-analyzer-scan', this.handleScan.bind(this)); // Expand/collapse detailed breakdown. $(document).on('click', '.vigilante-analyzer-toggle', this.handleToggleDetails.bind(this)); // Save weekly cron + email toggle on change. $(document).on('change', '.vigilante-analyzer-weekly input[type="checkbox"]', this.handleSettingsChange.bind(this)); // Pulse the row we landed on if we came from a fix link. this.hashFocusFlash(); // Upgrade the server-rendered SVG (no-op if already correct). this.renderSparklineFromData(); }, /** * Kick off a scan. Phase 'fast' first for instant feedback, then 'slow' for remote checks. */ handleScan: function(e) { e.preventDefault(); if (this.isScanning) { return; } this.isScanning = true; var $btn = $('#vigilante-analyzer-scan'); var originalHtml = $btn.html(); $btn.data('original-html', originalHtml); $btn.prop('disabled', true) .html(' ' + (vigilanteAdmin.strings.analyzerFastPhase || 'Running fast checks…')); var self = this; // Expand the details section so the user sees results pour in. this.expandDetails(true); this.runScanPhase('fast') .done(function(fastReport) { self.applyReport(fastReport); $btn.html(' ' + (vigilanteAdmin.strings.analyzerSlowPhase || 'Running remote checks…')); self.runScanPhase('slow') .done(function(slowReport) { self.applyReport(slowReport); if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) { Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.analyzerScanComplete || 'Security scan complete.'); } }) .fail(function(msg) { if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) { Vigilante_Admin.showNotice('error', msg || vigilanteAdmin.strings.analyzerScanFailed || 'Security scan failed.'); } }) .always(function() { $btn.prop('disabled', false).html($btn.data('original-html') || originalHtml); self.isScanning = false; }); }) .fail(function(msg) { $btn.prop('disabled', false).html($btn.data('original-html') || originalHtml); self.isScanning = false; if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) { Vigilante_Admin.showNotice('error', msg || vigilanteAdmin.strings.analyzerScanFailed || 'Security scan failed.'); } }); }, runScanPhase: function(phase) { var deferred = $.Deferred(); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', timeout: 60000, data: { action: 'vigilante_analyzer_run', nonce: vigilanteAdmin.nonce, phase: phase } }).done(function(response) { if (response && response.success) { deferred.resolve(response.data); } else { deferred.reject(response && response.data ? response.data : null); } }).fail(function(xhr, status, err) { deferred.reject(err || status); }); return deferred.promise(); }, /** * Map a 0-100 score percentage to a quality tag {label, slug}, mirroring * Vigilante_Admin::analyzer_quality_tag() in PHP. */ qualityTag: function(pct) { pct = Math.max(0, Math.min(100, parseInt(pct, 10) || 0)); var s = vigilanteAdmin.strings || {}; // "Excellent" is reserved for a perfect score — a single missing point drops to Good. if (pct === 100) { return { label: s.analyzerQualityExcellent || 'Excellent', slug: 'a' }; } if (pct >= 70) { return { label: s.analyzerQualityGood || 'Good', slug: 'b' }; } if (pct >= 50) { return { label: s.analyzerQualityFair || 'Fair', slug: 'c' }; } if (pct >= 30) { return { label: s.analyzerQualityPoor || 'Poor', slug: 'd' }; } return { label: s.analyzerQualityCritical || 'Critical', slug: 'e' }; }, /** * Swap a {a,b,c,d,e} slug modifier class on an element. */ setQualityClass: function($el, baseClass, slug) { $el.removeClass(function(i, cls) { return (cls.match(new RegExp('(^|\\s)' + baseClass + '-[abcde](?=\\s|$)', 'g')) || []).join(' '); }); $el.addClass(baseClass + '-' + slug); }, /** * Merge a phase report into the widget DOM. */ applyReport: function(report) { if (!report || typeof report !== 'object') { return; } var $root = this.$root; var self = this; // Overall pass/warn/fail counts (summary). if (report.counts && typeof report.counts === 'object') { $root.find('[data-role="pass"]').text(report.counts.pass || 0); $root.find('[data-role="warn"]').text(report.counts.warn || 0); $root.find('[data-role="fail"]').text(report.counts.fail || 0); } // Main score circle + quality tag. if (report.grade) { var $card = $root.find('.vigilante-analyzer-score-card'); var $circle = $card.find('.vigilante-score-circle'); var gradeLc = String(report.grade).toLowerCase(); var scoreN = parseInt(report.score, 10) || 0; if (!$circle.length || $circle.hasClass('vigilante-grade-empty')) { $circle.remove(); $card.prepend( '
    ' + '' + this.esc(report.grade) + '' + '' + this.esc(scoreN) + '%' + '
    ' ); } else { $circle.removeClass(function(i, cls) { return (cls.match(/(^|\s)vigilante-grade-\S+/g) || []).join(' '); }).addClass('vigilante-grade-' + gradeLc); $circle.find('.vigilante-grade').text(report.grade); $circle.find('.vigilante-score-text').text(scoreN + '%'); } // Quality tag in the score card. var quality = this.qualityTag(scoreN); var $tag = $root.find('[data-role="quality-tag"]'); if ($tag.length) { this.setQualityClass($tag, 'vigilante-analyzer-quality', quality.slug); $tag.text(quality.label); } } // "Last scan" label in the header. if (report.ran_at) { var $ranAt = $root.find('[data-role="ran-at"]'); var justNow = vigilanteAdmin.strings.analyzerLastScanJustNow || 'Last scan just now'; $ranAt.html( '' + this.esc(justNow) ); } // Per-category rows: quality pill, state counts, score, bar, and check rows. if (report.categories && typeof report.categories === 'object') { $.each(report.categories, function(slug, cat) { var $cat = $root.find('.vigilante-analyzer-category[data-category="' + slug + '"]'); if (!$cat.length) { return; } var infoOnly = $cat.attr('data-info-only') === '1'; var counts = (cat.counts && typeof cat.counts === 'object') ? cat.counts : {}; if (!infoOnly) { var earned = parseInt(cat.earned, 10) || 0; var max = parseInt(cat.max, 10) || 0; var pct = max > 0 ? Math.round((earned / max) * 100) : 0; var cq = self.qualityTag(pct); $cat.find('[data-role="earned"]').text(earned); var $catQuality = $cat.find('[data-role="category-quality"]'); if ($catQuality.length) { self.setQualityClass($catQuality, 'vigilante-analyzer-quality', cq.slug); // Update only the label span; the pill also contains the // "X/Y tests" counter so a blanket .text() would wipe it. var $label = $catQuality.find('[data-role="category-quality-label"]'); if ($label.length) { $label.text(cq.label); } else { $catQuality.text(cq.label); } } // "X/Y tests" inside the pill. Total is everything that // scores (pass + warn + fail), info/skip are excluded // because they don't move the category score. var $tests = $cat.find('[data-role="category-tests"]'); if ($tests.length) { var passed = parseInt(counts.pass, 10) || 0; var total = passed + (parseInt(counts.warn, 10) || 0) + (parseInt(counts.fail, 10) || 0); var tpl = (vigilanteAdmin.strings && vigilanteAdmin.strings.testsCounter) || '%1$d/%2$d tests'; $tests.text(tpl.replace('%1$d', passed).replace('%2$d', total)); $tests.attr('data-passed', passed); $tests.attr('data-total', total); } // Progress bar (width + quality color). var $bar = $cat.find('[data-role="category-bar"]'); if ($bar.length) { $bar.css('width', pct + '%'); self.setQualityClass($bar, 'vigilante-analyzer-category-bar-fill', cq.slug); } } // State counts — show each chip only when count > 0. // Info-only categories (like reputation) also show an info count. self.updateCategoryState($cat, 'pass', parseInt(counts.pass, 10) || 0); self.updateCategoryState($cat, 'warn', parseInt(counts.warn, 10) || 0); self.updateCategoryState($cat, 'fail', parseInt(counts.fail, 10) || 0); if (infoOnly) { self.updateCategoryState($cat, 'info', parseInt(counts.info, 10) || 0); // Info-only categories replace the progress bar with a status pill // ("All clear" when nothing actionable, "N findings" otherwise). self.updateInfoStatus($cat, counts); } // Replace the check rows only for checks this phase actually returned. if (cat.checks && cat.checks.length) { var $list = $cat.find('[data-role="check-list"]'); $list.find('.vigilante-analyzer-check-empty').remove(); $.each(cat.checks, function(i, check) { var $existing = $list.find('[data-check-id="' + check.id + '"]'); var html = self.renderCheckRow(check); if ($existing.length) { $existing.replaceWith(html); } else { $list.append(html); } }); } }); } }, /** * Update a per-category state chip (pass/warn/fail). Adds the chip if it doesn't * exist yet, updates its number, or removes it when the count is zero. */ updateCategoryState: function($cat, state, count) { var $wrap = $cat.find('[data-role="category-states"]'); if (!$wrap.length) { return; } var selector = '.vigilante-analyzer-category-state--' + state; var $chip = $wrap.find(selector); if (count > 0) { if (!$chip.length) { var iconMap = { pass: 'yes', warn: 'warning', fail: 'no', info: 'info-outline' }; var titleMap = { pass: 'Passing', warn: 'Warning', fail: 'Failing', info: 'Informational' }; var title = titleMap[state] || ''; $chip = $( '' + '0' + '' + '' ); // Keep order: pass → warn → fail → info. if (state === 'pass') { $wrap.prepend($chip); } else if (state === 'warn') { var $passChip = $wrap.find('.vigilante-analyzer-category-state--pass'); if ($passChip.length) { $chip.insertAfter($passChip); } else { $wrap.prepend($chip); } } else if (state === 'fail') { var $infoChip = $wrap.find('.vigilante-analyzer-category-state--info'); if ($infoChip.length) { $chip.insertBefore($infoChip); } else { $wrap.append($chip); } } else { $wrap.append($chip); } } $chip.find('[data-role="state-' + state + '"]').text(count); } else if ($chip.length) { $chip.remove(); } }, /** * Refresh the "All clear" / "N findings" pill for an info-only category. * Runs after a scan replaces the counts on the server side. */ updateInfoStatus: function($cat, counts) { var $status = $cat.find('[data-role="info-status"]'); if (!$status.length) { return; } var warnN = parseInt(counts.warn, 10) || 0; var failN = parseInt(counts.fail, 10) || 0; var issues = warnN + failN; var strings = (vigilanteAdmin && vigilanteAdmin.strings) || {}; if (issues > 0) { var template = strings.analyzerInfoFindings || '%d findings'; var label = template.replace('%d', issues); $status .removeClass('vigilante-analyzer-category-status--clear') .addClass('vigilante-analyzer-category-status--attention') .html( '' + this.esc(label) ); } else { var clear = strings.analyzerInfoAllClear || 'All clear'; $status .removeClass('vigilante-analyzer-category-status--attention') .addClass('vigilante-analyzer-category-status--clear') .html( '' + this.esc(clear) ); } }, renderCheckRow: function(check) { var state = check.state || 'skip'; var icons = { pass: 'yes-alt', warn: 'warning', fail: 'dismiss', info: 'info', skip: 'minus' }; var icon = icons[state] || 'minus'; var scoreHtml = ''; var max = parseInt(check.max, 10) || 0; if (max > 0 && state !== 'info' && state !== 'skip') { scoreHtml = '' + this.esc((parseInt(check.score, 10) || 0) + '/' + max) + ''; } var fixHtml = ''; if (check.fix_link && (state === 'fail' || state === 'warn')) { fixHtml = '' + this.esc(vigilanteAdmin.strings.analyzerGoToSetting || 'Go to setting') + ''; } else if (check.fix_link && state === 'info') { var isExt = /^https?:\/\//i.test(check.fix_link); fixHtml = '' + this.esc(vigilanteAdmin.strings.analyzerLearnMore || 'Learn more') + ''; } var detailHtml = ''; if (check.detail) { detailHtml = '

    ' + this.esc(check.detail) + '

    '; } return '
  • ' + '' + '
    ' + '
    ' + '' + this.esc(check.label || '') + '' + scoreHtml + '
    ' + detailHtml + fixHtml + '
    ' + '
  • '; }, /** * Expand or collapse the detailed breakdown section. */ handleToggleDetails: function(e) { e.preventDefault(); var $btn = $(e.currentTarget); var expanded = $btn.attr('aria-expanded') === 'true'; this.expandDetails(!expanded); }, expandDetails: function(expand) { var $btn = this.$root.find('.vigilante-analyzer-toggle'); var $details = this.$root.find('.vigilante-analyzer-details'); var show = !!expand; $btn.attr('aria-expanded', show ? 'true' : 'false'); var label = show ? (vigilanteAdmin.strings.analyzerHideDetails || 'Hide detailed breakdown') : (vigilanteAdmin.strings.analyzerShowDetails || 'Show detailed breakdown'); // Preserve the chevron element; only swap the label text. var $chevron = $btn.find('.vigilante-analyzer-toggle-chevron').detach(); if (!$chevron.length) { $chevron = $(''); } $btn.text(label).append(' ').append($chevron); if (show) { $details.removeAttr('hidden'); } else { $details.attr('hidden', 'hidden'); } }, /** * Auto-save weekly scan + email toggles. */ handleSettingsChange: function(e) { var $input = $(e.currentTarget); var $weekly = this.$root.find('input[name="security_analyzer[weekly_scan_enabled]"]'); var $email = this.$root.find('input[name="security_analyzer[email_on_regression]"]'); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_analyzer_save_settings', nonce: vigilanteAdmin.nonce, weekly_scan_enabled: $weekly.is(':checked') ? 1 : 0, email_on_regression: $email.is(':checked') ? 1 : 0 }, success: function(response) { if (response && response.success) { if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) { Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.analyzerSettingsSaved || 'Analyzer settings saved.'); } } else { if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) { Vigilante_Admin.showNotice('error', (response && response.data) || vigilanteAdmin.strings.error); } $input.prop('checked', !$input.is(':checked')); } }, error: function() { if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); } $input.prop('checked', !$input.is(':checked')); } }); }, /** * Re-render the sparkline from data-points (no-op if no points). */ renderSparklineFromData: function() { var $wrap = this.$root.find('[data-role="sparkline"]'); if (!$wrap.length) { return; } var raw = $wrap.attr('data-points'); if (!raw) { return; } try { var points = JSON.parse(raw); if (!points || points.length < 2) { return; } // Only regenerate if the SVG is missing (e.g. server gave empty markup). if (!$wrap.find('svg').length) { $wrap.html(this.buildSparklineSvg(points)); } this.attachSparklineHotspots($wrap, points); } catch (err) { /* ignore parse errors */ } }, /** * Inject invisible hot-spots over each sparkline point and wire up a tooltip * that shows the score on hover/focus. */ attachSparklineHotspots: function($wrap, points) { if (!points || points.length < 2) { return; } // Idempotent: clear any previous hot-spots/tooltip first. $wrap.find('.vigilante-analyzer-sparkline-dot, .vigilante-analyzer-sparkline-tooltip').remove(); var SVG_W = 280, SVG_H = 60, PAD = 4; var usableW = SVG_W - PAD * 2; var usableH = SVG_H - PAD * 2; var step = usableW / Math.max(1, points.length - 1); var max = 100; var $tooltip = $(''); $wrap.append($tooltip); for (var i = 0; i < points.length; i++) { var x = PAD + (i * step); var y = PAD + (usableH - ((points[i] / max) * usableH)); var xPct = (x / SVG_W) * 100; var yPct = (y / SVG_H) * 100; var $dot = $('') .css({ left: xPct + '%', top: yPct + '%' }) .attr('data-score', points[i]) .attr('data-index', i) .attr('aria-label', String(points[i])); $wrap.append($dot); } $wrap.off('.vigSpark'); $wrap.on('mouseenter.vigSpark focus.vigSpark', '.vigilante-analyzer-sparkline-dot', function() { var $dot = $(this); $tooltip .text($dot.attr('data-score')) .css({ left: $dot.css('left'), top: $dot.css('top') }) .addClass('is-visible'); }); $wrap.on('mouseleave.vigSpark blur.vigSpark', '.vigilante-analyzer-sparkline-dot', function() { $tooltip.removeClass('is-visible'); }); }, buildSparklineSvg: function(points) { var width = 280; var height = 60; var padding = 4; var usableW = width - (padding * 2); var usableH = height - (padding * 2); var count = points.length; var step = usableW / Math.max(1, count - 1); var max = 100; var coords = []; for (var i = 0; i < count; i++) { var x = padding + (i * step); var y = padding + (usableH - ((points[i] / max) * usableH)); coords.push(x.toFixed(2) + ',' + y.toFixed(2)); } var path = 'M ' + coords.join(' L '); var lastX = padding + ((count - 1) * step); var lastY = padding + (usableH - ((points[count - 1] / max) * usableH)); return '' + '' + '' + ''; }, /** * When landing on a fix link (#field-* or #vigilante-section-*), * scroll smoothly and flash-highlight the target. */ hashFocusFlash: function() { var hash = window.location.hash; if (!hash || hash.length < 2) { return; } // Accept both section anchors and field anchors. if (!/^#(?:field-|vigilante-section-)/.test(hash)) { return; } setTimeout(function() { var $target; try { $target = $(hash); } catch (err) { return; } if (!$target || !$target.length) { return; } // Scroll to it smoothly. if ('scrollIntoView' in $target[0]) { $target[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Flash — remove any previous flash first. $('.vigilante-focus-flash').removeClass('vigilante-focus-flash'); $target.addClass('vigilante-focus-flash'); setTimeout(function() { $target.removeClass('vigilante-focus-flash'); }, 1800); }, 150); }, /** Escape text for HTML. */ esc: function(str) { return String(str == null ? '' : str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, /** Escape text for HTML attribute. */ escAttr: function(str) { return this.esc(str); } }; // Initialize on document ready $(document).ready(function() { Vigilante_Admin.init(); Vigilante_Admin.initPasswordReset(); Vigilante_Admin.initUserApproval(); Vigilante_Admin.initSessionManagement(); VigilanteAnalyzer.init(); // Shared "Send test email" button (Notification settings, File Integrity, Audit Alerts). $(document).on('click', '.vigilante-test-email-btn', function(e) { e.preventDefault(); var $btn = $(this); var $result = $btn.siblings('.vigilante-test-email-result'); var original = $btn.data('original-text') || $btn.text(); $btn.prop('disabled', true).text(vigilanteAdmin.strings.sendingTest || 'Sending...'); $result.text('').css('color', ''); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: 'vigilante_send_test_email', nonce: vigilanteAdmin.nonce }, success: function(response) { if (response && response.success) { $result.css('color', '#00a32a').text((response.data && response.data.message) || ''); } else { $result.css('color', '#d63638').text((response && response.data && response.data.message) || vigilanteAdmin.strings.error); } }, error: function() { $result.css('color', '#d63638').text(vigilanteAdmin.strings.error); }, complete: function() { $btn.prop('disabled', false).text(original); } }); }); }); /** * Security Headers settings recovery (2.10.0). * * Both buttons are disabled together while the request is in flight, so a * second click cannot fire a second restore. The page reloads afterwards * because the whole tab is rendered from the settings that just changed. */ $(function() { var SELECTOR = '#vigilante-recovery-restore, #vigilante-recovery-dismiss'; function recoveryRun(action) { $(SELECTOR).prop('disabled', true); $.ajax({ url: vigilanteAdmin.ajaxUrl, type: 'POST', data: { action: action, nonce: vigilanteAdmin.nonce }, success: function(response) { if (response.success) { Vigilante_Admin.showNotice('success', response.data); setTimeout(function() { location.reload(); }, 1200); return; } Vigilante_Admin.showNotice('error', response.data); $(SELECTOR).prop('disabled', false); }, error: function() { Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error); $(SELECTOR).prop('disabled', false); } }); } $(document).on('click', '#vigilante-recovery-restore', function(e) { e.preventDefault(); recoveryRun('vigilante_headers_recovery_restore'); }); $(document).on('click', '#vigilante-recovery-dismiss', function(e) { e.preventDefault(); recoveryRun('vigilante_headers_recovery_dismiss'); }); $(document).on('click', '#vigilante-recovery-undo', function(e) { e.preventDefault(); $(this).prop('disabled', true); recoveryRun('vigilante_headers_recovery_undo'); }); }); })(jQuery);