PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / assets / js / admin.js

admin.js in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.11.8, at assets/js/admin.js

4,379 lines 194.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Vigilante Admin JavaScript
3 *
4 * @package Vigilante
5 */
6
7 (function($) {
8 'use strict';
9
10 var Vigilante_Admin = {
11
12 /**
13 * Initialize
14 */
15 init: function() {
16 this.bindEvents();
17 this.initModuleToggles();
18 this.initFileIntegrityPagination();
19
20 // Activity log pagination state
21 this.logPage = 1;
22 this.logPerPage = 20;
23 this.logSearchTimer = null;
24 },
25
26 /**
27 * Bind events
28 */
29 bindEvents: function() {
30 // Save settings forms
31 $(document).on('submit', '.vigilante-settings-form', this.handleFormSubmit.bind(this));
32
33 // Module toggles in dashboard
34 $(document).on('change', '.vigilante-module-item input[type="checkbox"]', this.handleModuleToggle.bind(this));
35
36 // Apply preset
37 $(document).on('click', '.vigilante-preset-btn', this.handleApplyPreset.bind(this));
38
39 // Reset section to defaults
40 $(document).on('click', '.vigilante-reset-section-btn', this.handleResetSection.bind(this));
41
42 // Clear lockouts
43 $(document).on('click', '.vigilante-clear-lockout', this.handleClearLockout.bind(this));
44 $(document).on('click', '.vigilante-clear-all-lockouts', this.handleClearAllLockouts.bind(this));
45
46 // Unblock firewall rate-limited IPs
47 $(document).on('click', '.vigilante-unblock-firewall-ip', this.handleUnblockFirewallIp.bind(this));
48
49 // Clear logs
50 $(document).on('click', '.vigilante-clear-logs', this.handleClearLogs.bind(this));
51
52 // Run scan
53 $(document).on('click', '.vigilante-run-scan', this.handleRunScan.bind(this));
54 $(document).on('click', '.vigilante-ignore-file', this.handleIgnoreFile.bind(this));
55 $(document).on('click', '.vigilante-unignore-file', this.handleUnignoreFile.bind(this));
56 $(document).on('click', '.vigilante-clear-ignored', this.handleClearIgnored.bind(this));
57
58 // Closed plugins ignore controls (separate slug-based list, not file paths).
59 $(document).on('click', '.vigilante-ignore-closed-plugin', this.handleIgnoreClosedPlugin.bind(this));
60 $(document).on('click', '.vigilante-unignore-closed-plugin', this.handleUnignoreClosedPlugin.bind(this));
61 $(document).on('click', '.vigilante-clear-ignored-closed-plugins', this.handleClearIgnoredClosedPlugins.bind(this));
62
63 // Bulk selection inside file integrity tables
64 $(document).on('change', '.vigilante-fi-cb-all', this.handleFiSelectAll.bind(this));
65 $(document).on('change', '.vigilante-fi-cb', this.handleFiCheckboxChange.bind(this));
66 $(document).on('click', '.vigilante-bulk-ignore', this.handleBulkIgnore.bind(this));
67 $(document).on('click', '.vigilante-bulk-unignore', this.handleBulkUnignore.bind(this));
68
69 // Clear scan results
70 $(document).on('click', '.vigilante-clear-scan', this.handleClearScan.bind(this));
71
72 // Critical config file actions
73 $(document).on('click', '.vigilante-approve-critical-file', this.handleApproveCriticalFile.bind(this));
74 $(document).on('click', '.vigilante-toggle-critical-content', this.handleToggleCriticalContent.bind(this));
75
76 // Export/Import settings
77 $(document).on('click', '.vigilante-export-settings', this.handleExportSettings.bind(this));
78 $(document).on('click', '.vigilante-import-settings', this.handleImportSettings.bind(this));
79 $(document).on('change', '#vigilante-import-file', this.handleImportFile.bind(this));
80
81 // Reset settings
82 $(document).on('click', '.vigilante-reset-settings', this.handleResetSettings.bind(this));
83
84 // Test headers
85 $(document).on('click', '.vigilante-test-headers', this.handleTestHeaders.bind(this));
86
87 // Login URL notification
88 $(document).on('click', '#vigilante_notify_login_url', this.handleNotifyLoginUrl.bind(this));
89 $(document).on('input', '#vigilante_custom_login_url', this.handleLoginUrlToggle.bind(this));
90
91 // Activity log filters
92 $(document).on('change', '#vigilante-log-type-filter, #vigilante-log-severity-filter, #vigilante-log-method-filter', this.handleLogFilter.bind(this));
93 $(document).on('click', '#vigilante-log-refresh', this.handleLogFilter.bind(this));
94
95 // Activity log search (debounced, min 3 chars)
96 $(document).on('input', '#vigilante-log-search', this.handleLogSearch.bind(this));
97
98 // Activity log pagination
99 $(document).on('click', '#vigilante-log-pagination .vigilante-page-first', function() { Vigilante_Admin.goToLogPage('first'); });
100 $(document).on('click', '#vigilante-log-pagination .vigilante-page-prev', function() { Vigilante_Admin.goToLogPage('prev'); });
101 $(document).on('click', '#vigilante-log-pagination .vigilante-page-next', function() { Vigilante_Admin.goToLogPage('next'); });
102 $(document).on('click', '#vigilante-log-pagination .vigilante-page-last', function() { Vigilante_Admin.goToLogPage('last'); });
103
104 // File integrity client-side pagination
105 $(document).on('click', '.vigilante-fi-pagination .vigilante-page-first', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'first'); });
106 $(document).on('click', '.vigilante-fi-pagination .vigilante-page-prev', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'prev'); });
107 $(document).on('click', '.vigilante-fi-pagination .vigilante-page-next', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'next'); });
108 $(document).on('click', '.vigilante-fi-pagination .vigilante-page-last', function() { Vigilante_Admin.goToFilePage($(this).closest('.vigilante-paginated-section'), 'last'); });
109
110 // Export logs
111 $(document).on('click', '.vigilante-export-logs', this.handleExportLogs.bind(this));
112
113 // Create backup
114 $(document).on('click', '.vigilante-create-backup', this.handleCreateBackup.bind(this));
115
116 // View log details modal
117 $(document).on('click', '.vigilante-view-log-details', this.handleViewLogDetails.bind(this));
118 $(document).on('click', '.vigilante-modal-close, .vigilante-modal', this.handleCloseModal.bind(this));
119 $(document).on('click', '.vigilante-add-to-list', this.handleAddToFirewallList.bind(this));
120 $(document).on('click', '.vigilante-modal-content', function(e) {
121 e.stopPropagation();
122 });
123
124 // Under Attack mode
125 $(document).on('click', '.vigilante-ua-activate', this.handleUnderAttackActivate.bind(this));
126 $(document).on('click', '.vigilante-ua-deactivate', this.handleUnderAttackDeactivate.bind(this));
127
128 // Database backup
129 $(document).on('click', '.vigilante-db-backup-toggle', this.handleDbBackupToggle.bind(this));
130 $(document).on('click', '.vigilante-db-backup-download', this.handleDbBackupDownload.bind(this));
131 $(document).on('change', '#vigilante-db-select-all', this.handleDbSelectAll.bind(this));
132 $(document).on('change', '.vigilante-db-table-check', this.handleDbTableCheck.bind(this));
133
134 // Database prefix
135 $(document).on('click', '.vigilante-db-regenerate-prefix', this.handleDbRegeneratePrefix.bind(this));
136 $(document).on('change', '#vigilante-prefix-backup-confirm', this.handleDbPrefixConfirmToggle.bind(this));
137 $(document).on('click', '.vigilante-db-change-prefix', this.handleDbChangePrefix.bind(this));
138
139 // Settings search
140 $(document).on('input', '#vigilante-settings-search', this.handleSettingsSearch.bind(this));
141 $(document).on('focus', '#vigilante-settings-search', this.handleSettingsSearch.bind(this));
142 $(document).on('keydown', '#vigilante-settings-search', this.handleSettingsSearchKey.bind(this));
143 $(document).on('mouseenter', '.vigilante-search-item', this.handleSettingsSearchHover.bind(this));
144 $(document).on('click', function(e) {
145 if (!$(e.target).closest('.vigilante-search-wrapper').length) {
146 $('#vigilante-settings-search-results').attr('hidden', true);
147 }
148 });
149 // Global "/" shortcut to focus search
150 $(document).on('keydown', function(e) {
151 if (e.key !== '/') return;
152 var $t = $(e.target);
153 if ($t.is('input, textarea, select') || $t.is('[contenteditable="true"]')) return;
154 var $search = $('#vigilante-settings-search');
155 if (!$search.length) return;
156 e.preventDefault();
157 $search.focus().select();
158 });
159
160 // Start Under Attack countdown if active
161 this.initUnderAttackCountdown();
162 },
163
164 /**
165 * Initialize module toggles
166 */
167 initModuleToggles: function() {
168 $(document).on('change', '.vigilante-module-item input[type="checkbox"]', function() {
169 var $item = $(this).closest('.vigilante-module-item');
170 var module = $(this).data('module');
171 var enabled = $(this).is(':checked');
172
173 $item.toggleClass('enabled', enabled).toggleClass('disabled', !enabled);
174
175 // Save module state
176 Vigilante_Admin.saveModuleState(module, enabled);
177 });
178 },
179
180 /**
181 * Save module state
182 */
183 saveModuleState: function(module, enabled) {
184 var $item = $('.vigilante-module-item input[data-module="' + module + '"]').closest('.vigilante-module-item');
185
186 // Show saving state
187 $item.addClass('vigilante-saving');
188
189 $.ajax({
190 url: vigilanteAdmin.ajaxUrl,
191 type: 'POST',
192 data: {
193 action: 'vigilante_save_settings',
194 nonce: vigilanteAdmin.nonce,
195 section: 'modules',
196 data: 'modules[' + module + ']=' + (enabled ? '1' : '0')
197 },
198 success: function(response) {
199 if (response.success) {
200 $item.removeClass('vigilante-saving').addClass('vigilante-saved');
201 // Show success then reload to update recommendations and all UI
202 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.saved);
203 setTimeout(function() {
204 location.reload();
205 }, 800);
206 } else {
207 $item.removeClass('vigilante-saving');
208 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error);
209 }
210 },
211 error: function() {
212 $item.removeClass('vigilante-saving');
213 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
214 }
215 });
216 },
217
218 /**
219 * Update security score after module toggle
220 */
221 updateSecurityScore: function() {
222 var total = $('.vigilante-module-item').length;
223 var enabled = $('.vigilante-module-item.enabled').length;
224 var score = total > 0 ? Math.round((enabled / total) * 100) : 0;
225
226 // Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29)
227 var grade;
228 if (score >= 90) {
229 grade = 'A';
230 } else if (score >= 70) {
231 grade = 'B';
232 } else if (score >= 50) {
233 grade = 'C';
234 } else if (score >= 30) {
235 grade = 'D';
236 } else {
237 grade = 'E';
238 }
239
240 // Update score display
241 $('.vigilante-score-text').text(score + '%');
242 $('.vigilante-grade').text(grade);
243 $('.vigilante-score-circle').removeClass('vigilante-grade-a vigilante-grade-b vigilante-grade-c vigilante-grade-d vigilante-grade-e')
244 .addClass('vigilante-grade-' + grade.toLowerCase());
245
246 // Update modules count text - use fixed string to prevent text accumulation
247 var $countText = $('.vigilante-security-score p');
248 if ($countText.length) {
249 var modulesText = (vigilanteAdmin.strings.modulesEnabled || '%1$d / %2$d modules enabled').replace('%1$d', enabled).replace('%2$d', total);
250 $countText.text(modulesText);
251 }
252 },
253
254 /**
255 * Update configuration status badge
256 */
257 updateConfigBadge: function(preset) {
258 var $badge = $('.vigilante-config-status .vigilante-preset-badge');
259 if ($badge.length) {
260 $badge.removeClass('vigilante-preset-standard vigilante-preset-maximum vigilante-preset-custom vigilante-preset-under-attack');
261
262 if (vigilanteAdmin.underAttack && vigilanteAdmin.underAttack.active) {
263 $badge.addClass('vigilante-preset-under-attack').text(vigilanteAdmin.strings.underAttackLabel || 'Under Attack');
264 } else if (preset === 'standard') {
265 $badge.addClass('vigilante-preset-standard').text(vigilanteAdmin.strings.standardLabel || 'Standard');
266 } else if (preset === 'maximum') {
267 $badge.addClass('vigilante-preset-maximum').text(vigilanteAdmin.strings.maximumLabel || 'Maximum Security');
268 } else {
269 $badge.addClass('vigilante-preset-custom').text(vigilanteAdmin.strings.customConfig || 'Custom Configuration');
270 }
271 }
272 },
273
274 /**
275 * Handle form submit
276 */
277 handleFormSubmit: function(e) {
278 e.preventDefault();
279
280 var $form = $(e.currentTarget);
281 var $btn = $form.find('.vigilante-save-btn');
282 var section = $form.data('section');
283
284 $btn.prop('disabled', true).text(vigilanteAdmin.strings.saving);
285 $form.addClass('vigilante-loading');
286
287 // Build form data including unchecked checkboxes
288 var formData = Vigilante_Admin.serializeFormWithCheckboxes($form);
289
290 $.ajax({
291 url: vigilanteAdmin.ajaxUrl,
292 type: 'POST',
293 data: {
294 action: 'vigilante_save_settings',
295 nonce: vigilanteAdmin.nonce,
296 section: section,
297 data: formData
298 },
299 success: function(response) {
300 if (response.success) {
301 var msg = response.data && response.data.message ? response.data.message : (response.data || vigilanteAdmin.strings.saved);
302 Vigilante_Admin.showNotice('success', msg);
303 } else {
304 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error);
305 }
306 },
307 error: function(xhr, status, error) {
308 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
309 },
310 complete: function() {
311 $btn.prop('disabled', false).text($btn.data('original-text') || vigilanteAdmin.strings.saved);
312 $form.removeClass('vigilante-loading');
313
314 // Reset button text after delay
315 setTimeout(function() {
316 $btn.text($btn.data('original-text') || vigilanteAdmin.strings.saveSettings || 'Save Settings');
317 }, 2000);
318 }
319 });
320 },
321
322 /**
323 * Handle module toggle in dashboard
324 */
325 handleModuleToggle: function(e) {
326 var $checkbox = $(e.currentTarget);
327 var module = $checkbox.data('module');
328 var enabled = $checkbox.is(':checked');
329 var $item = $checkbox.closest('.vigilante-module-item');
330
331 // Visual feedback
332 $item.addClass('vigilante-loading');
333
334 // Build data for modules section
335 var formData = 'modules[' + module + ']=' + (enabled ? '1' : '0');
336
337 $.ajax({
338 url: vigilanteAdmin.ajaxUrl,
339 type: 'POST',
340 data: {
341 action: 'vigilante_save_settings',
342 nonce: vigilanteAdmin.nonce,
343 section: 'modules',
344 data: formData
345 },
346 success: function(response) {
347 if (response.success) {
348 // Update visual state
349 if (enabled) {
350 $item.removeClass('disabled').addClass('enabled');
351 } else {
352 $item.removeClass('enabled').addClass('disabled');
353 }
354 Vigilante_Admin.updateSecurityScore();
355 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.saved);
356
357 // Update config badge to Custom (since settings were manually changed)
358 Vigilante_Admin.updateConfigBadge('custom');
359
360 // Clear active preset indicator
361 $('.vigilante-preset-card').removeClass('vigilante-preset-active');
362 $('.vigilante-active-indicator').remove();
363 } else {
364 // Revert checkbox on error
365 $checkbox.prop('checked', !enabled);
366 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error);
367 }
368 },
369 error: function() {
370 // Revert checkbox on error
371 $checkbox.prop('checked', !enabled);
372 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
373 },
374 complete: function() {
375 $item.removeClass('vigilante-loading');
376 }
377 });
378 },
379
380 /**
381 * Serialize form including unchecked checkboxes
382 */
383 serializeFormWithCheckboxes: function($form) {
384 var data = $form.serializeArray();
385 var checkboxNames = [];
386
387 // Find all checkboxes and add unchecked ones with value 0
388 $form.find('input[type="checkbox"]').each(function() {
389 var name = $(this).attr('name');
390 if (name && checkboxNames.indexOf(name) === -1) {
391 checkboxNames.push(name);
392 if (!$(this).is(':checked')) {
393 data.push({ name: name, value: '0' });
394 }
395 }
396 });
397
398 // Convert to query string
399 return $.param(data);
400 },
401
402 /**
403 * Handle apply preset
404 */
405 handleApplyPreset: function(e) {
406 e.preventDefault();
407
408 var preset = $(e.currentTarget).data('preset');
409
410 if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmApplyPreset || 'Apply the "%s" preset?').replace('%s', preset))) {
411 return;
412 }
413
414 $.ajax({
415 url: vigilanteAdmin.ajaxUrl,
416 type: 'POST',
417 data: {
418 action: 'vigilante_apply_preset',
419 nonce: vigilanteAdmin.nonce,
420 preset: preset
421 },
422 success: function(response) {
423 if (response.success) {
424 Vigilante_Admin.showNotice('success', response.data);
425 setTimeout(function() {
426 location.reload();
427 }, 1000);
428 } else {
429 Vigilante_Admin.showNotice('error', response.data);
430 }
431 },
432 error: function() {
433 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
434 }
435 });
436 },
437
438 /**
439 * Handle Under Attack mode activation
440 */
441 handleUnderAttackActivate: function(e) {
442 e.preventDefault();
443
444 if (!confirm(vigilanteAdmin.strings.underAttackConfirmActivate)) {
445 return;
446 }
447
448 var $btn = $(e.currentTarget);
449 $btn.prop('disabled', true).text(vigilanteAdmin.strings.underAttackActivating);
450
451 $.ajax({
452 url: vigilanteAdmin.ajaxUrl,
453 type: 'POST',
454 data: {
455 action: 'vigilante_activate_under_attack',
456 nonce: vigilanteAdmin.nonce
457 },
458 success: function(response) {
459 if (response.success) {
460 Vigilante_Admin.showNotice('success', response.data.message);
461 setTimeout(function() {
462 location.reload();
463 }, 1000);
464 } else {
465 Vigilante_Admin.showNotice('error', response.data);
466 $btn.prop('disabled', false).text(vigilanteAdmin.strings.underAttackActivate || 'Activate for 4 hours');
467 }
468 },
469 error: function() {
470 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
471 $btn.prop('disabled', false).text(vigilanteAdmin.strings.underAttackActivate || 'Activate for 4 hours');
472 }
473 });
474 },
475
476 /**
477 * Handle Under Attack mode deactivation
478 */
479 handleUnderAttackDeactivate: function(e) {
480 e.preventDefault();
481
482 if (!confirm(vigilanteAdmin.strings.underAttackConfirmDeactivate)) {
483 return;
484 }
485
486 var $btn = $(e.currentTarget);
487 $btn.prop('disabled', true).text(vigilanteAdmin.strings.underAttackDeactivating);
488
489 $.ajax({
490 url: vigilanteAdmin.ajaxUrl,
491 type: 'POST',
492 data: {
493 action: 'vigilante_deactivate_under_attack',
494 nonce: vigilanteAdmin.nonce
495 },
496 success: function(response) {
497 if (response.success) {
498 Vigilante_Admin.showNotice('success', response.data);
499 setTimeout(function() {
500 location.reload();
501 }, 1000);
502 } else {
503 Vigilante_Admin.showNotice('error', response.data);
504 $btn.prop('disabled', false).text(vigilanteAdmin.strings.deactivate || 'Deactivate');
505 }
506 },
507 error: function() {
508 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
509 $btn.prop('disabled', false).text(vigilanteAdmin.strings.deactivate || 'Deactivate');
510 }
511 });
512 },
513
514 /**
515 * Initialize Under Attack countdown timer
516 */
517 initUnderAttackCountdown: function() {
518 var $countdown = $('.vigilante-ua-countdown');
519 if (!$countdown.length) {
520 return;
521 }
522
523 var expiresAt = parseInt($countdown.data('expires'), 10);
524 if (!expiresAt) {
525 return;
526 }
527
528 var $timeEl = $countdown.find('.vigilante-ua-time');
529
530 var updateCountdown = function() {
531 var now = Math.floor(Date.now() / 1000);
532 var remaining = expiresAt - now;
533
534 if (remaining <= 0) {
535 location.reload();
536 return;
537 }
538
539 var hours = Math.floor(remaining / 3600);
540 var mins = Math.floor((remaining % 3600) / 60);
541
542 var text = vigilanteAdmin.strings.underAttackRemaining || '%1$dh %2$dm remaining';
543 text = text.replace('%1$d', hours).replace('%2$d', mins);
544 $timeEl.text(text);
545 };
546
547 // Run immediately, then update every minute
548 updateCountdown();
549 setInterval(updateCountdown, 60000);
550 },
551
552 /**
553 * Handle reset section to defaults
554 */
555 handleResetSection: function(e) {
556 e.preventDefault();
557
558 var $btn = $(e.currentTarget);
559 var $form = $btn.closest('form');
560 var section = $form.data('section');
561
562 if (!section) {
563 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.couldNotDetermineSection || 'Could not determine section.');
564 return;
565 }
566
567 if (!confirm(vigilanteAdmin.strings.confirmResetSection || 'Reset this section to default values? This cannot be undone.')) {
568 return;
569 }
570
571 var originalText = $btn.data('original-text') || $btn.text();
572 $btn.prop('disabled', true).html(vigilanteAdmin.strings.loading + ' <span class="vigilante-spinner"></span>');
573
574 $.ajax({
575 url: vigilanteAdmin.ajaxUrl,
576 type: 'POST',
577 data: {
578 action: 'vigilante_reset_section',
579 nonce: vigilanteAdmin.nonce,
580 section: section
581 },
582 success: function(response) {
583 if (response.success) {
584 Vigilante_Admin.showNotice('success', response.data.message || vigilanteAdmin.strings.sectionResetDefaults || 'Section reset to defaults.');
585 setTimeout(function() {
586 location.reload();
587 }, 1000);
588 } else {
589 Vigilante_Admin.showNotice('error', response.data);
590 }
591 },
592 error: function() {
593 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
594 },
595 complete: function() {
596 $btn.prop('disabled', false).text(originalText);
597 }
598 });
599 },
600
601 /**
602 * Handle clear single lockout
603 */
604 handleClearLockout: function(e) {
605 e.preventDefault();
606
607 var $btn = $(e.currentTarget);
608 var ip = $btn.data('ip');
609
610 $.ajax({
611 url: vigilanteAdmin.ajaxUrl,
612 type: 'POST',
613 data: {
614 action: 'vigilante_clear_lockouts',
615 nonce: vigilanteAdmin.nonce,
616 ip: ip
617 },
618 success: function(response) {
619 if (response.success) {
620 var $container = $btn.closest('.vigilante-paginated-section');
621 $btn.closest('tr').fadeOut(function() {
622 $(this).remove();
623 Vigilante_Admin.refreshFilePagination($container);
624 });
625 } else {
626 Vigilante_Admin.showNotice('error', response.data);
627 }
628 }
629 });
630 },
631
632 /**
633 * Handle clear all lockouts
634 */
635 handleClearAllLockouts: function(e) {
636 e.preventDefault();
637
638 if (!confirm(vigilanteAdmin.strings.confirm)) {
639 return;
640 }
641
642 $.ajax({
643 url: vigilanteAdmin.ajaxUrl,
644 type: 'POST',
645 data: {
646 action: 'vigilante_clear_lockouts',
647 nonce: vigilanteAdmin.nonce
648 },
649 success: function(response) {
650 if (response.success) {
651 location.reload();
652 } else {
653 Vigilante_Admin.showNotice('error', response.data);
654 }
655 }
656 });
657 },
658
659 /**
660 * Handle clear logs
661 */
662 handleClearLogs: function(e) {
663 e.preventDefault();
664
665 if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmClearLogs || 'This will delete all activity logs.'))) {
666 return;
667 }
668
669 $.ajax({
670 url: vigilanteAdmin.ajaxUrl,
671 type: 'POST',
672 data: {
673 action: 'vigilante_clear_logs',
674 nonce: vigilanteAdmin.nonce
675 },
676 success: function(response) {
677 if (response.success) {
678 location.reload();
679 } else {
680 Vigilante_Admin.showNotice('error', response.data);
681 }
682 }
683 });
684 },
685
686 /**
687 * Handle run scan
688 */
689 handleRunScan: function(e) {
690 e.preventDefault();
691 e.stopPropagation();
692
693 var $btn = $(e.currentTarget);
694 var $results = $('#vigilante-scan-results');
695 var $lastResults = $('#vigilante-last-scan-results').closest('.vigilante-settings-section');
696
697 // Hide previous results section
698 $lastResults.hide();
699 $results.empty().hide();
700
701 $btn.prop('disabled', true).html(vigilanteAdmin.strings.scanning + ' <span class="vigilante-spinner"></span>');
702
703 $.ajax({
704 url: vigilanteAdmin.ajaxUrl,
705 type: 'POST',
706 timeout: 120000,
707 data: {
708 action: 'vigilante_run_scan',
709 nonce: vigilanteAdmin.nonce
710 },
711 success: function(response) {
712 if (response.success) {
713 // Reload the page so the server-side render shows ALL findings
714 // in their canonical sections (Suspicious, Extra, Critical Config,
715 // Closed + Removed Plugins, Modified). The legacy in-place JS
716 // render only knows the file-level categories and would leave
717 // the Closed + Removed subsection hidden after a scan.
718 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.scanComplete);
719 setTimeout(function() { location.reload(); }, 600);
720 } else {
721 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.scanFailed || 'Scan failed');
722 $btn.prop('disabled', false).text(vigilanteAdmin.strings.runScanNow || 'Run Scan Now');
723 }
724 },
725 error: function(xhr, status, error) {
726 Vigilante_Admin.showNotice('error', (vigilanteAdmin.strings.scanError || 'Scan error: %s').replace('%s', error));
727 $btn.prop('disabled', false).text(vigilanteAdmin.strings.runScanNow || 'Run Scan Now');
728 }
729 });
730 },
731
732 /**
733 * Handle clear scan results
734 */
735 handleClearScan: function(e) {
736 e.preventDefault();
737 e.stopPropagation();
738
739 if (!confirm(vigilanteAdmin.strings.confirmClearScan || 'Are you sure you want to clear all scan results?')) {
740 return;
741 }
742
743 var $btn = $(e.currentTarget);
744 var originalText = $btn.text();
745 $btn.prop('disabled', true).text(vigilanteAdmin.strings.clearing || 'Clearing...');
746
747 $.ajax({
748 url: vigilanteAdmin.ajaxUrl,
749 type: 'POST',
750 data: {
751 action: 'vigilante_clear_scan',
752 nonce: vigilanteAdmin.nonce
753 },
754 success: function(response) {
755 if (response.success) {
756 // Hide all scan results
757 $('#vigilante-scan-results').empty().hide();
758 $('#vigilante-last-scan-results').closest('.vigilante-settings-section').fadeOut();
759 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.scanResultsCleared || 'Scan results cleared. Page will reload...');
760 // Reload page to reflect changes
761 setTimeout(function() {
762 location.reload();
763 }, 1000);
764 } else {
765 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.failedClearResults || 'Failed to clear results');
766 $btn.prop('disabled', false).text(originalText);
767 }
768 },
769 error: function(xhr, status, error) {
770 Vigilante_Admin.showNotice('error', (vigilanteAdmin.strings.ajaxError || 'AJAX Error: %s').replace('%s', error));
771 $btn.prop('disabled', false).text(originalText);
772 },
773 complete: function() {
774 // Button text restored in success/error handlers
775 }
776 });
777 },
778
779 /**
780 * Handle toggle critical file content viewer
781 */
782 handleToggleCriticalContent: function(e) {
783 e.preventDefault();
784 var $btn = $(e.currentTarget);
785 var targetId = $btn.data('target');
786 var $content = $('#' + targetId);
787
788 // Use toggle instead of slideUp/slideDown because target can be a TR
789 // (animations on table rows are inconsistent across browsers)
790 if ($content.is(':visible')) {
791 $content.hide();
792 $btn.text($btn.data('label-show'));
793 } else {
794 $content.show();
795 $btn.text($btn.data('label-hide'));
796 }
797 },
798
799 /**
800 * Handle approve critical file button click
801 */
802 handleApproveCriticalFile: function(e) {
803 e.preventDefault();
804 var $btn = $(e.currentTarget);
805 var file = $btn.data('file');
806 var strings = vigilanteAdmin.strings;
807 var originalText = $btn.text();
808
809 // The POST carries an opaque key, never the file name: WAFs like
810 // ModSecurity (OWASP CRS 930130) block any request argument
811 // containing the literal "wp-config.php", which made Approve fail
812 // with a generic AJAX error on hardened hostings. The PHP handler
813 // maps the key back against its own whitelist.
814 var fileKey = { 'wp-config.php': 'cfg', '.htaccess': 'hta' }[ file ] || '';
815
816 $btn.prop('disabled', true).text(strings.approving || 'Approving...');
817
818 $.ajax({
819 url: vigilanteAdmin.ajaxUrl,
820 type: 'POST',
821 data: {
822 action: 'vigilante_approve_critical_file',
823 nonce: vigilanteAdmin.nonce,
824 file_key: fileKey
825 },
826 success: function(response) {
827 if (response.success) {
828 // Remove the approved file row and its diff row from the table.
829 // The diff row carries an id derived from the file name (same as
830 // sanitize_html_class() on PHP side), so we target it directly
831 // instead of relying on adjacency in the DOM.
832 var $row = $btn.closest('tr');
833 var fileId = String(file).replace(/[^a-z0-9_-]/gi, '-');
834 $('#vigilante-critical-content-' + fileId).remove();
835 $row.fadeOut(300, function() {
836 $(this).remove();
837 // If no more critical config rows, remove the entire section
838 if ($('.vigilante-critical-config-files tbody tr').length === 0) {
839 $('.vigilante-critical-config-files').fadeOut(300, function() { $(this).remove(); });
840 }
841 });
842 Vigilante_Admin.showNotice('success', strings.criticalApproved || response.data.message);
843 } else {
844 Vigilante_Admin.showNotice('error', response.data || 'Failed to approve.');
845 $btn.prop('disabled', false).text(originalText);
846 }
847 },
848 error: function(xhr, status, error) {
849 Vigilante_Admin.showNotice('error', (strings.ajaxError || 'AJAX Error: %s').replace('%s', error));
850 $btn.prop('disabled', false).text(originalText);
851 }
852 });
853 },
854
855 /**
856 * Handle ignore file button click
857 */
858 handleIgnoreFile: function(e) {
859 e.preventDefault();
860 var $btn = $(e.currentTarget);
861 var file = $btn.data('file');
862 var strings = vigilanteAdmin.strings;
863
864 $btn.prop('disabled', true).text(strings.ignoring || 'Ignoring...');
865
866 $.ajax({
867 url: vigilanteAdmin.ajaxUrl,
868 type: 'POST',
869 data: {
870 action: 'vigilante_ignore_file',
871 nonce: vigilanteAdmin.nonce,
872 file: file
873 },
874 success: function(response) {
875 if (response.success) {
876 var $container = $btn.closest('.vigilante-paginated-section');
877 // Remove the row from the table
878 $btn.closest('tr').fadeOut(300, function() {
879 $(this).remove();
880 // Refresh pagination after row removal
881 Vigilante_Admin.refreshFilePagination($container);
882 });
883 Vigilante_Admin.showNotice('success', strings.fileIgnored || 'File added to ignored list.');
884 } else {
885 Vigilante_Admin.showNotice('error', response.data || 'Failed to ignore file');
886 $btn.prop('disabled', false).text(strings.ignore || 'Ignore');
887 }
888 },
889 error: function() {
890 $btn.prop('disabled', false).text(strings.ignore || 'Ignore');
891 }
892 });
893 },
894
895 /**
896 * Handle stop ignoring file
897 */
898 handleUnignoreFile: function(e) {
899 e.preventDefault();
900 var $btn = $(e.currentTarget);
901 var file = $btn.data('file');
902 var strings = vigilanteAdmin.strings;
903
904 $.ajax({
905 url: vigilanteAdmin.ajaxUrl,
906 type: 'POST',
907 data: {
908 action: 'vigilante_unignore_file',
909 nonce: vigilanteAdmin.nonce,
910 file: file
911 },
912 success: function(response) {
913 if (response.success) {
914 var $container = $btn.closest('.vigilante-paginated-section');
915 $btn.closest('tr').fadeOut(300, function() {
916 $(this).remove();
917 Vigilante_Admin.refreshFilePagination($container);
918 });
919 Vigilante_Admin.showNotice('success', strings.fileUnignored || 'File removed from ignored list.');
920 } else {
921 Vigilante_Admin.showNotice('error', response.data || 'Failed');
922 }
923 }
924 });
925 },
926
927 /**
928 * Handle clear all ignored files
929 */
930 handleClearIgnored: function(e) {
931 e.preventDefault();
932 var strings = vigilanteAdmin.strings;
933
934 if (!confirm(strings.confirmClearIgnored || 'Remove all files from the ignored list?')) {
935 return;
936 }
937
938 $.ajax({
939 url: vigilanteAdmin.ajaxUrl,
940 type: 'POST',
941 data: {
942 action: 'vigilante_clear_ignored',
943 nonce: vigilanteAdmin.nonce
944 },
945 success: function(response) {
946 if (response.success) {
947 Vigilante_Admin.showNotice('success', strings.ignoredCleared || 'Ignored files list cleared. Page will reload...');
948 setTimeout(function() {
949 location.reload();
950 }, 1000);
951 }
952 }
953 });
954 },
955
956 /**
957 * Ignore a closed/removed plugin slug. Reloads to refresh the section.
958 */
959 handleIgnoreClosedPlugin: function(e) {
960 e.preventDefault();
961 var $btn = $(e.currentTarget);
962 var slug = $btn.data('slug');
963 var strings = vigilanteAdmin.strings || {};
964
965 $btn.prop('disabled', true).text(strings.ignoring || 'Ignoring…');
966
967 $.ajax({
968 url: vigilanteAdmin.ajaxUrl,
969 type: 'POST',
970 data: {
971 action: 'vigilante_ignore_closed_plugin',
972 nonce: vigilanteAdmin.nonce,
973 slug: slug
974 },
975 success: function(response) {
976 if (response && response.success) {
977 Vigilante_Admin.showNotice('success', (response.data) || 'Plugin ignored.');
978 setTimeout(function() { location.reload(); }, 600);
979 } else {
980 Vigilante_Admin.showNotice('error', (response && response.data) || 'Failed.');
981 $btn.prop('disabled', false).text(strings.ignore || 'Ignore');
982 }
983 },
984 error: function(xhr, status, error) {
985 Vigilante_Admin.showNotice('error', (strings.ajaxError || 'AJAX Error: %s').replace('%s', error));
986 $btn.prop('disabled', false).text(strings.ignore || 'Ignore');
987 }
988 });
989 },
990
991 /**
992 * Stop ignoring a closed/removed plugin slug.
993 */
994 handleUnignoreClosedPlugin: function(e) {
995 e.preventDefault();
996 var $btn = $(e.currentTarget);
997 var slug = $btn.data('slug');
998 var strings = vigilanteAdmin.strings || {};
999
1000 $btn.prop('disabled', true).text(strings.unignoring || 'Restoring…');
1001
1002 $.ajax({
1003 url: vigilanteAdmin.ajaxUrl,
1004 type: 'POST',
1005 data: {
1006 action: 'vigilante_unignore_closed_plugin',
1007 nonce: vigilanteAdmin.nonce,
1008 slug: slug
1009 },
1010 success: function(response) {
1011 if (response && response.success) {
1012 Vigilante_Admin.showNotice('success', (response.data) || 'Plugin restored to the active list.');
1013 setTimeout(function() { location.reload(); }, 600);
1014 } else {
1015 Vigilante_Admin.showNotice('error', (response && response.data) || 'Failed.');
1016 $btn.prop('disabled', false).text(strings.stopIgnoring || 'Stop ignoring');
1017 }
1018 },
1019 error: function(xhr, status, error) {
1020 Vigilante_Admin.showNotice('error', (strings.ajaxError || 'AJAX Error: %s').replace('%s', error));
1021 $btn.prop('disabled', false).text(strings.stopIgnoring || 'Stop ignoring');
1022 }
1023 });
1024 },
1025
1026 /**
1027 * Clear the entire ignored closed plugins list.
1028 */
1029 handleClearIgnoredClosedPlugins: function(e) {
1030 e.preventDefault();
1031 var strings = vigilanteAdmin.strings || {};
1032
1033 if (!confirm(strings.confirmClearIgnoredClosedPlugins || 'Remove all plugins from the ignored closed plugins list? They will reappear in the main list and in email alerts.')) {
1034 return;
1035 }
1036
1037 $.ajax({
1038 url: vigilanteAdmin.ajaxUrl,
1039 type: 'POST',
1040 data: {
1041 action: 'vigilante_clear_ignored_closed_plugins',
1042 nonce: vigilanteAdmin.nonce
1043 },
1044 success: function(response) {
1045 if (response && response.success) {
1046 Vigilante_Admin.showNotice('success', (response.data) || 'Ignored closed plugins list cleared.');
1047 setTimeout(function() { location.reload(); }, 1000);
1048 }
1049 }
1050 });
1051 },
1052
1053 /**
1054 * Toggle all checkboxes inside a file integrity table when the
1055 * header "select all" checkbox changes. Only operates on visible
1056 * rows so it plays nice with client-side pagination.
1057 */
1058 handleFiSelectAll: function(e) {
1059 var $cb = $(e.currentTarget);
1060 var $section = $cb.closest('.vigilante-paginated-section');
1061 $section.find('table.vigilante-fi-paginated tbody tr:visible .vigilante-fi-cb').prop('checked', $cb.prop('checked'));
1062 this.refreshBulkBar($section);
1063 },
1064
1065 /**
1066 * Refresh bulk bar state after individual checkbox change.
1067 */
1068 handleFiCheckboxChange: function(e) {
1069 var $section = $(e.currentTarget).closest('.vigilante-paginated-section');
1070 this.refreshBulkBar($section);
1071
1072 // Sync the header "select all" with the current visible state
1073 var $allCb = $section.find('.vigilante-fi-cb-all');
1074 var $visible = $section.find('table.vigilante-fi-paginated tbody tr:visible .vigilante-fi-cb');
1075 var $checked = $visible.filter(':checked');
1076 $allCb.prop('checked', $visible.length > 0 && $checked.length === $visible.length);
1077 },
1078
1079 /**
1080 * Update the count label and enable/disable the bulk action button.
1081 */
1082 refreshBulkBar: function($section) {
1083 var strings = vigilanteAdmin.strings;
1084 var mode = $section.data('bulk-mode'); // 'ignore' or 'unignore'
1085 var $checked = $section.find('.vigilante-fi-cb:checked');
1086 var count = $checked.length;
1087 var $btn = $section.find(mode === 'unignore' ? '.vigilante-bulk-unignore' : '.vigilante-bulk-ignore');
1088 var $count = $section.find('.vigilante-fi-bulk-count');
1089
1090 $btn.prop('disabled', count === 0);
1091
1092 if (count === 0) {
1093 $count.text('');
1094 } else {
1095 var template = (strings.bulkSelectedCount || '%d selected');
1096 $count.text(template.replace('%d', count));
1097 }
1098 },
1099
1100 /**
1101 * Collect the file paths checked inside a section.
1102 */
1103 collectSelectedFiles: function($section) {
1104 var files = [];
1105 $section.find('.vigilante-fi-cb:checked').each(function() {
1106 var v = $(this).val();
1107 if (v) {
1108 files.push(v);
1109 }
1110 });
1111 return files;
1112 },
1113
1114 /**
1115 * Bulk add to ignored list.
1116 */
1117 handleBulkIgnore: function(e) {
1118 e.preventDefault();
1119 var self = this;
1120 var strings = vigilanteAdmin.strings;
1121 var $btn = $(e.currentTarget);
1122 var $section = $btn.closest('.vigilante-paginated-section');
1123 var files = this.collectSelectedFiles($section);
1124
1125 if (files.length === 0) {
1126 Vigilante_Admin.showNotice('error', strings.bulkNoSelection || 'Select at least one file first.');
1127 return;
1128 }
1129
1130 if (!confirm(strings.bulkConfirmIgnore || 'Ignore the selected files?')) {
1131 return;
1132 }
1133
1134 var originalText = $btn.text();
1135 $btn.prop('disabled', true).text(strings.bulkProcessing || 'Processing...');
1136
1137 $.ajax({
1138 url: vigilanteAdmin.ajaxUrl,
1139 type: 'POST',
1140 data: {
1141 action: 'vigilante_bulk_ignore_files',
1142 nonce: vigilanteAdmin.nonce,
1143 files: files
1144 },
1145 success: function(response) {
1146 if (response.success) {
1147 // Remove processed rows from this section
1148 files.forEach(function(file) {
1149 $section.find('.vigilante-fi-cb').filter(function() {
1150 return $(this).val() === file;
1151 }).closest('tr').remove();
1152 });
1153 $section.find('.vigilante-fi-cb-all').prop('checked', false);
1154 Vigilante_Admin.refreshFilePagination($section);
1155 self.refreshBulkBar($section);
1156 Vigilante_Admin.showNotice('success', (response.data && response.data.message) || strings.fileIgnored);
1157 } else {
1158 Vigilante_Admin.showNotice('error', (response.data && response.data.message) || response.data || 'Failed to ignore files');
1159 $btn.prop('disabled', false).text(originalText);
1160 }
1161 },
1162 error: function() {
1163 $btn.prop('disabled', false).text(originalText);
1164 },
1165 // Restore the label on success too. refreshBulkBar() re-derives the
1166 // disabled state from the remaining selection but never rewrites the
1167 // button text, so without this the button stays on "Processing...".
1168 complete: function() {
1169 $btn.text(originalText);
1170 }
1171 });
1172 },
1173
1174 /**
1175 * Bulk remove from ignored list.
1176 */
1177 handleBulkUnignore: function(e) {
1178 e.preventDefault();
1179 var self = this;
1180 var strings = vigilanteAdmin.strings;
1181 var $btn = $(e.currentTarget);
1182 var $section = $btn.closest('.vigilante-paginated-section');
1183 var files = this.collectSelectedFiles($section);
1184
1185 if (files.length === 0) {
1186 Vigilante_Admin.showNotice('error', strings.bulkNoSelection || 'Select at least one file first.');
1187 return;
1188 }
1189
1190 if (!confirm(strings.bulkConfirmUnignore || 'Remove the selected files from ignored list?')) {
1191 return;
1192 }
1193
1194 var originalText = $btn.text();
1195 $btn.prop('disabled', true).text(strings.bulkProcessing || 'Processing...');
1196
1197 $.ajax({
1198 url: vigilanteAdmin.ajaxUrl,
1199 type: 'POST',
1200 data: {
1201 action: 'vigilante_bulk_unignore_files',
1202 nonce: vigilanteAdmin.nonce,
1203 files: files
1204 },
1205 success: function(response) {
1206 if (response.success) {
1207 files.forEach(function(file) {
1208 $section.find('.vigilante-fi-cb').filter(function() {
1209 return $(this).val() === file;
1210 }).closest('tr').remove();
1211 });
1212 $section.find('.vigilante-fi-cb-all').prop('checked', false);
1213 Vigilante_Admin.refreshFilePagination($section);
1214 self.refreshBulkBar($section);
1215 Vigilante_Admin.showNotice('success', (response.data && response.data.message) || strings.fileUnignored);
1216
1217 // If the ignored table is now empty, hide its section.
1218 if ($section.find('table.vigilante-fi-paginated tbody tr').length === 0) {
1219 $('#vigilante-section-fi-ignored').fadeOut(300, function() { $(this).remove(); });
1220 }
1221 } else {
1222 Vigilante_Admin.showNotice('error', (response.data && response.data.message) || response.data || 'Failed');
1223 $btn.prop('disabled', false).text(originalText);
1224 }
1225 },
1226 error: function() {
1227 $btn.prop('disabled', false).text(originalText);
1228 },
1229 // Restore the label on success too. refreshBulkBar() re-derives the
1230 // disabled state from the remaining selection but never rewrites the
1231 // button text, so without this the button stays on "Processing...".
1232 complete: function() {
1233 $btn.text(originalText);
1234 }
1235 });
1236 },
1237
1238 /**
1239 * Display scan results
1240 */
1241 displayScanResults: function(results, $container, ignoredCount) {
1242 // Ensure arrays exist
1243 results.modified = results.modified || [];
1244 results.suspicious = results.suspicious || [];
1245 results.extra = results.extra || [];
1246 results.missing = results.missing || [];
1247
1248 var strings = vigilanteAdmin.strings;
1249 var esc = Vigilante_Admin.escapeHtml;
1250
1251 var html = '<h2>' + strings.scanResults + '</h2>';
1252 html += '<div class="vigilante-scan-summary">';
1253 html += '<div class="vigilante-scan-stat vigilante-stat-ok">';
1254 html += '<span class="vigilante-stat-number">' + (results.ok || 0) + '</span>';
1255 html += '<span class="vigilante-stat-label">' + strings.ok + '</span>';
1256 html += '</div>';
1257 html += '<div class="vigilante-scan-stat vigilante-stat-modified">';
1258 html += '<span class="vigilante-stat-number">' + results.modified.length + '</span>';
1259 html += '<span class="vigilante-stat-label">' + strings.modified + '</span>';
1260 html += '</div>';
1261 html += '<div class="vigilante-scan-stat vigilante-stat-suspicious">';
1262 html += '<span class="vigilante-stat-number">' + results.suspicious.length + '</span>';
1263 html += '<span class="vigilante-stat-label">' + strings.suspicious + '</span>';
1264 html += '</div>';
1265 html += '<div class="vigilante-scan-stat vigilante-stat-extra">';
1266 html += '<span class="vigilante-stat-number">' + results.extra.length + '</span>';
1267 html += '<span class="vigilante-stat-label">' + (strings.extra || 'Extra') + '</span>';
1268 html += '</div>';
1269 if (ignoredCount > 0) {
1270 html += '<div class="vigilante-scan-stat vigilante-stat-ignored">';
1271 html += '<span class="vigilante-stat-number">' + ignoredCount + '</span>';
1272 html += '<span class="vigilante-stat-label">' + (strings.ignored || 'Ignored') + '</span>';
1273 html += '</div>';
1274 }
1275 html += '<div class="vigilante-scan-stat">';
1276 var computedTotal = (results.ok || 0) + results.modified.length + results.suspicious.length + results.extra.length + (ignoredCount || 0);
1277 html += '<span class="vigilante-stat-number">' + computedTotal + '</span>';
1278 html += '<span class="vigilante-stat-label">' + strings.totalScanned + '</span>';
1279 html += '</div>';
1280 html += '</div>';
1281
1282 var bulkBar = '<div class="vigilante-fi-bulk-bar">'
1283 + '<button type="button" class="button vigilante-bulk-ignore" disabled>' + (strings.bulkIgnoreSelected || 'Ignore selected') + '</button>'
1284 + '<span class="vigilante-fi-bulk-count" aria-live="polite"></span>'
1285 + '</div>';
1286 var selectAllCell = '<td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="' + (strings.selectAll || 'Select all') + '"></td>';
1287
1288 // Suspicious files table
1289 if (results.suspicious.length > 0) {
1290 html += '<div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore">';
1291 html += '<h3 style="color: #d63638;">' + strings.suspiciousFiles + '</h3>';
1292 html += '<p class="description" style="color: #d63638;">' + strings.suspiciousWarning + '</p>';
1293 html += bulkBar;
1294 html += '<div class="vigilante-fi-pagination-wrap"></div>';
1295 html += '<table class="wp-list-table widefat fixed striped vigilante-fi-paginated">';
1296 html += '<thead><tr>' + selectAllCell + '<th>' + strings.file + '</th><th style="width: 250px;">' + strings.reason + '</th><th style="width: 120px;">' + strings.type + '</th><th style="width: 80px;">' + (strings.actions || 'Actions') + '</th></tr></thead>';
1297 html += '<tbody>';
1298 results.suspicious.forEach(function(file) {
1299 html += '<tr>';
1300 html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + esc(file.file) + '"></th>';
1301 html += '<td><code style="color: #d63638;">' + esc(file.file) + '</code></td>';
1302 html += '<td>' + esc(file.reason || strings.unknown) + '</td>';
1303 html += '<td>' + esc(file.type || strings.unknown) + '</td>';
1304 html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + esc(file.file) + '">' + (strings.ignore || 'Ignore') + '</button></td>';
1305 html += '</tr>';
1306 });
1307 html += '</tbody></table>';
1308 html += '</div>';
1309 }
1310
1311 // Extra files table
1312 if (results.extra.length > 0) {
1313 html += '<div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore">';
1314 html += '<h3 style="color: #b32d2e;">' + (strings.extraFiles || 'Extra Files') + '</h3>';
1315 html += '<p class="description">' + (strings.extraDescription || 'PHP files not in original distribution.') + '</p>';
1316 html += bulkBar;
1317 html += '<div class="vigilante-fi-pagination-wrap"></div>';
1318 html += '<table class="wp-list-table widefat fixed striped vigilante-fi-paginated">';
1319 html += '<thead><tr>' + selectAllCell + '<th>' + strings.file + '</th><th style="width: 250px;">' + strings.reason + '</th><th style="width: 120px;">' + strings.type + '</th><th style="width: 80px;">' + (strings.actions || 'Actions') + '</th></tr></thead>';
1320 html += '<tbody>';
1321 results.extra.forEach(function(file) {
1322 html += '<tr>';
1323 html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + esc(file.file) + '"></th>';
1324 html += '<td><code style="color: #b32d2e;">' + esc(file.file) + '</code></td>';
1325 html += '<td>' + esc(file.reason || strings.unknown) + '</td>';
1326 html += '<td>' + esc(file.type || strings.unknown) + '</td>';
1327 html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + esc(file.file) + '">' + (strings.ignore || 'Ignore') + '</button></td>';
1328 html += '</tr>';
1329 });
1330 html += '</tbody></table>';
1331 html += '</div>';
1332 }
1333
1334 // Split critical config from regular modified
1335 var criticalModified = [];
1336 var regularModified = [];
1337 results.modified.forEach(function(file) {
1338 if (file.type === 'critical_config') {
1339 criticalModified.push(file);
1340 } else {
1341 regularModified.push(file);
1342 }
1343 });
1344
1345 // Critical config files section (same visual treatment as suspicious files)
1346 if (criticalModified.length > 0) {
1347 html += '<div class="vigilante-file-list vigilante-critical-config-files">';
1348 html += '<h3 style="color: #e36210;">' + (strings.criticalConfigTitle || 'Critical config files modified') + '</h3>';
1349 html += '<p class="description">' + (strings.criticalConfigDesc || '') + '</p>';
1350 html += '<table class="wp-list-table widefat fixed striped">';
1351 html += '<thead><tr><th>' + strings.file + '</th><th style="width: 200px;">' + (strings.changes || 'Changes') + '</th><th style="width: 220px;">' + (strings.actions || 'Actions') + '</th></tr></thead>';
1352 html += '<tbody>';
1353
1354 // Delega en el escapador del objeto, que codifica comillas. La
1355 // version anterior era $('<span>').text(s).html(), que no las toca:
1356 // aqui solo se usa en posicion de texto, pero es el patron del
1357 // CVE-2026-81754 esperando a que alguien lo mueva a un atributo.
1358 var escHtml = Vigilante_Admin.escapeHtml;
1359
1360 criticalModified.forEach(function(file) {
1361 var fileId = file.file.replace(/[^a-z0-9]/gi, '-');
1362 var baselineSize = file.baseline_size ? Number(file.baseline_size).toLocaleString() : '?';
1363 var currentSize = file.current_size ? Number(file.current_size).toLocaleString() : '?';
1364 var diff = file.diff || {};
1365 var added = diff.added || [];
1366 var removed = diff.removed || [];
1367 var diffUnavailable = !!diff.unavailable;
1368
1369 // Main row
1370 html += '<tr>';
1371 html += '<td><code style="color: #e36210;">' + esc(file.file) + '</code></td>';
1372 html += '<td>';
1373 if (!diffUnavailable) {
1374 html += '<span style="color: #007017;">+' + added.length + '</span> <span style="color: #b32d2e;">-' + removed.length + '</span> ' + (strings.diffLines || 'lines') + '<br>';
1375 }
1376 html += '<small style="color: #50575e;">' + baselineSize + ' &rarr; ' + currentSize + ' bytes</small>';
1377 html += '</td>';
1378 html += '<td>';
1379 html += '<button type="button" class="button button-small vigilante-toggle-critical-content" data-target="vigilante-critical-content-' + fileId + '" data-label-show="' + (strings.reviewChanges || 'Review changes') + '" data-label-hide="' + (strings.hideChanges || 'Hide changes') + '">' + (strings.reviewChanges || 'Review changes') + '</button> ';
1380
1381 // Same gate as the AJAX handler behind the button, and as the
1382 // first render in PHP. Since 2.11.3 approving these two files
1383 // takes a network administrator, so painting the button to
1384 // anybody else buys them a Permission denied and no reason why.
1385 //
1386 // Truthy test on purpose: wp_localize_script() casts every
1387 // scalar to a string, so the PHP boolean arrives here as "1"
1388 // or "" and never as true. Measured on the network, admin of
1389 // a subsite against network admin. A === true would gate
1390 // nobody and would look right.
1391 if (vigilanteAdmin.approvalLocked) {
1392 html += '<span class="description" style="display:block;margin-top:4px;">' + escHtml(strings.approvalLockedNotice || '') + '</span>';
1393 } else {
1394 html += '<button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="' + esc(file.file) + '">' + (strings.approve || 'Approve') + '</button>';
1395 }
1396 html += '</td>';
1397 html += '</tr>';
1398
1399 // Expandable diff row
1400 html += '<tr id="vigilante-critical-content-' + fileId + '" class="vigilante-critical-content-row" style="display:none;">';
1401 html += '<td colspan="3" style="padding: 0;">';
1402 html += '<div class="vigilante-critical-content" style="max-height: 400px; overflow: auto; background: #fff; padding: 10px; font-size: 12px; line-height: 1.5; font-family: Consolas, Monaco, monospace; border-top: 1px solid #c3c4c7;">';
1403 if (diff.network) {
1404 html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + escHtml(strings.diffNetwork || '') + '</p>';
1405 } else if (diff.rescan) {
1406 html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + escHtml(strings.diffRescan || '') + '</p>';
1407 } else if (diff.redaction) {
1408 html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + escHtml(strings.diffRedaction || '') + '</p>';
1409 } else if (diffUnavailable) {
1410 html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + (strings.diffUnavailable || 'Diff not available.') + '</p>';
1411 } else if (added.length === 0 && removed.length === 0) {
1412 html += '<p style="color: #50575e; font-style: italic; margin: 0;">' + (strings.diffEmpty || 'No line changes detected.') + '</p>';
1413 } else {
1414 removed.forEach(function(rline) {
1415 html += '<div style="background: #fbeaea; color: #b32d2e; padding: 1px 4px; white-space: pre-wrap; word-wrap: break-word;"><span style="display: inline-block; width: 50px; color: #999; user-select: none;">' + (rline.line || 0) + '</span>- ' + escHtml(rline.content || '') + '</div>';
1416 });
1417 added.forEach(function(aline) {
1418 html += '<div style="background: #e6f4e9; color: #007017; padding: 1px 4px; white-space: pre-wrap; word-wrap: break-word;"><span style="display: inline-block; width: 50px; color: #999; user-select: none;">' + (aline.line || 0) + '</span>+ ' + escHtml(aline.content || '') + '</div>';
1419 });
1420 }
1421 html += '</div>';
1422 html += '</td>';
1423 html += '</tr>';
1424 });
1425
1426 html += '</tbody></table>';
1427 html += '</div>';
1428 }
1429
1430 // Regular modified files table
1431 if (regularModified.length > 0) {
1432 html += '<div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore">';
1433 html += '<h3>' + strings.modifiedFiles + '</h3>';
1434 html += '<p class="description">' + strings.modifiedDescription + '</p>';
1435 html += bulkBar;
1436 html += '<div class="vigilante-fi-pagination-wrap"></div>';
1437 html += '<table class="wp-list-table widefat fixed striped vigilante-fi-paginated">';
1438 html += '<thead><tr>' + selectAllCell + '<th>' + strings.file + '</th><th style="width: 100px;">' + strings.type + '</th><th style="width: 80px;">' + (strings.actions || 'Actions') + '</th></tr></thead>';
1439 html += '<tbody>';
1440 regularModified.forEach(function(file) {
1441 html += '<tr>';
1442 html += '<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="' + esc(file.file) + '"></th>';
1443 html += '<td><code>' + esc(file.file) + '</code></td>';
1444 html += '<td>' + esc(file.type || strings.unknown) + '</td>';
1445 html += '<td><button type="button" class="button button-small vigilante-ignore-file" data-file="' + esc(file.file) + '">' + (strings.ignore || 'Ignore') + '</button></td>';
1446 html += '</tr>';
1447 });
1448 html += '</tbody></table>';
1449 html += '</div>';
1450 }
1451
1452 if (results.modified.length === 0 && results.suspicious.length === 0 && results.extra.length === 0) {
1453 html += '<div class="vigilante-all-clear"><span class="dashicons dashicons-yes-alt"></span> ' + strings.allClear + '</div>';
1454 }
1455
1456 $container.html(html).show();
1457
1458 // Initialize client-side pagination for file integrity tables
1459 this.initFileIntegrityPagination($container);
1460 },
1461
1462 /**
1463 * Initialize client-side pagination for file integrity tables
1464 * Works on both PHP-rendered (last results) and JS-rendered (live scan) tables
1465 *
1466 * @param {jQuery} $scope Optional scope to limit search (for live scan results)
1467 */
1468 initFileIntegrityPagination: function($scope) {
1469 var perPage = 20;
1470 var $context = $scope || $(document);
1471
1472 $context.find('table.vigilante-fi-paginated').each(function() {
1473 var $table = $(this);
1474 var $rows = $table.find('tbody tr');
1475 var total = $rows.length;
1476
1477 if (total === 0) {
1478 return;
1479 }
1480
1481 // Store state on the parent container
1482 var $container = $table.closest('.vigilante-paginated-section');
1483 $container.data('fi-page', 1);
1484 $container.data('fi-per-page', perPage);
1485 $container.data('fi-total', total);
1486
1487 // Hide rows beyond first page (only if more than perPage)
1488 if (total > perPage) {
1489 $rows.each(function(index) {
1490 $(this).toggle(index < perPage);
1491 });
1492 }
1493
1494 // Build pagination controls (always show count, buttons only when needed)
1495 var $wrap = $container.find('.vigilante-fi-pagination-wrap');
1496 var paginationHtml = Vigilante_Admin.buildFilePaginationHtml(1, perPage, total);
1497 $wrap.html(paginationHtml);
1498 });
1499 },
1500
1501 /**
1502 * Build pagination HTML for file integrity tables
1503 */
1504 buildFilePaginationHtml: function(page, perPage, total) {
1505 var totalPages = Math.ceil(total / perPage) || 1;
1506 var from = total > 0 ? ((page - 1) * perPage) + 1 : 0;
1507 var to = Math.min(page * perPage, total);
1508 var needsNav = total > perPage;
1509
1510 var text = total > 0
1511 ? (vigilanteAdmin.strings.paginationOf || '%1$d–%2$d of %3$d').replace('%1$d', from).replace('%2$d', to).replace('%3$d', total)
1512 : (vigilanteAdmin.strings.paginationEmpty || '0 items');
1513
1514 var prevDisabled = page <= 1 ? ' disabled' : '';
1515 var nextDisabled = page >= totalPages ? ' disabled' : '';
1516
1517 var html = '<span class="vigilante-pagination vigilante-fi-pagination">';
1518 if (needsNav) {
1519 html += '<button type="button" class="vigilante-page-first" title="First"' + prevDisabled + '>&laquo;</button>';
1520 html += '<button type="button" class="vigilante-page-prev" title="Previous"' + prevDisabled + '>&lsaquo;</button>';
1521 }
1522 html += '<span class="vigilante-page-info">' + text + '</span>';
1523 if (needsNav) {
1524 html += '<button type="button" class="vigilante-page-next" title="Next"' + nextDisabled + '>&rsaquo;</button>';
1525 html += '<button type="button" class="vigilante-page-last" title="Last"' + nextDisabled + '>&raquo;</button>';
1526 }
1527 html += '</span>';
1528
1529 return html;
1530 },
1531
1532 /**
1533 * Navigate file integrity table pages (client-side)
1534 */
1535 goToFilePage: function($container, direction) {
1536 var page = $container.data('fi-page') || 1;
1537 var perPage = $container.data('fi-per-page') || 20;
1538 var total = $container.data('fi-total') || 0;
1539 var totalPages = Math.ceil(total / perPage) || 1;
1540
1541 switch (direction) {
1542 case 'first': page = 1; break;
1543 case 'prev': page = Math.max(1, page - 1); break;
1544 case 'next': page = Math.min(totalPages, page + 1); break;
1545 case 'last': page = totalPages; break;
1546 }
1547
1548 $container.data('fi-page', page);
1549
1550 // Show/hide rows
1551 var start = (page - 1) * perPage;
1552 var end = start + perPage;
1553 $container.find('table.vigilante-fi-paginated tbody tr').each(function(index) {
1554 $(this).toggle(index >= start && index < end);
1555 });
1556
1557 // Update controls
1558 var $wrap = $container.find('.vigilante-fi-pagination-wrap');
1559 $wrap.html(this.buildFilePaginationHtml(page, perPage, total));
1560
1561 // Sync the "select all" header with what is now visible.
1562 // Selections in other pages remain checked and will be included
1563 // in any bulk action, but the header reflects only this page.
1564 var $allCb = $container.find('.vigilante-fi-cb-all');
1565 if ($allCb.length) {
1566 var $visible = $container.find('table.vigilante-fi-paginated tbody tr:visible .vigilante-fi-cb');
1567 var $checked = $visible.filter(':checked');
1568 $allCb.prop('checked', $visible.length > 0 && $checked.length === $visible.length);
1569 }
1570 },
1571
1572 /**
1573 * Refresh file integrity pagination after row removal (ignore)
1574 */
1575 refreshFilePagination: function($container) {
1576 var perPage = $container.data('fi-per-page') || 20;
1577 var $rows = $container.find('table.vigilante-fi-paginated tbody tr');
1578 var total = $rows.length;
1579
1580 $container.data('fi-total', total);
1581
1582 if (total === 0) {
1583 $container.find('.vigilante-fi-pagination-wrap').empty();
1584 return;
1585 }
1586
1587 if (total <= perPage) {
1588 // Show all rows, display count only
1589 $rows.show();
1590 $container.data('fi-page', 1);
1591 var $wrap = $container.find('.vigilante-fi-pagination-wrap');
1592 $wrap.html(this.buildFilePaginationHtml(1, perPage, total));
1593 return;
1594 }
1595
1596 // Ensure current page is still valid
1597 var page = $container.data('fi-page') || 1;
1598 var totalPages = Math.ceil(total / perPage);
1599 if (page > totalPages) {
1600 page = totalPages;
1601 }
1602 $container.data('fi-page', page);
1603
1604 // Show/hide rows for current page
1605 var start = (page - 1) * perPage;
1606 var end = start + perPage;
1607 $rows.each(function(index) {
1608 $(this).toggle(index >= start && index < end);
1609 });
1610
1611 var $wrap = $container.find('.vigilante-fi-pagination-wrap');
1612 $wrap.html(this.buildFilePaginationHtml(page, perPage, total));
1613 },
1614
1615 /**
1616 * Handle export settings
1617 */
1618 handleExportSettings: function(e) {
1619 e.preventDefault();
1620
1621 $.ajax({
1622 url: vigilanteAdmin.ajaxUrl,
1623 type: 'POST',
1624 data: {
1625 action: 'vigilante_export_settings',
1626 nonce: vigilanteAdmin.nonce
1627 },
1628 success: function(response) {
1629 if (response.success) {
1630 var blob = new Blob([response.data.content], { type: 'application/json' });
1631 var url = URL.createObjectURL(blob);
1632 var a = document.createElement('a');
1633 a.href = url;
1634 a.download = response.data.filename;
1635 document.body.appendChild(a);
1636 a.click();
1637 document.body.removeChild(a);
1638 URL.revokeObjectURL(url);
1639 } else {
1640 Vigilante_Admin.showNotice('error', response.data);
1641 }
1642 }
1643 });
1644 },
1645
1646 /**
1647 * Handle import settings click
1648 */
1649 handleImportSettings: function(e) {
1650 e.preventDefault();
1651 $('#vigilante-import-file').trigger('click');
1652 },
1653
1654 /**
1655 * Handle import file selection
1656 */
1657 handleImportFile: function(e) {
1658 var file = e.target.files[0];
1659 if (!file) return;
1660
1661 var reader = new FileReader();
1662 reader.onload = function(e) {
1663 if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmOverwrite || 'This will overwrite your current settings.'))) {
1664 return;
1665 }
1666
1667 $.ajax({
1668 url: vigilanteAdmin.ajaxUrl,
1669 type: 'POST',
1670 data: {
1671 action: 'vigilante_import_settings',
1672 nonce: vigilanteAdmin.nonce,
1673 settings: e.target.result
1674 },
1675 success: function(response) {
1676 if (response.success) {
1677 Vigilante_Admin.showNotice('success', response.data);
1678 setTimeout(function() {
1679 location.reload();
1680 }, 1000);
1681 } else {
1682 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.importFailed);
1683 }
1684 },
1685 error: function() {
1686 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.importFailed);
1687 }
1688 });
1689 };
1690 reader.readAsText(file);
1691 },
1692
1693 /**
1694 * Handle reset settings
1695 */
1696 handleResetSettings: function(e) {
1697 e.preventDefault();
1698
1699 if (!confirm(vigilanteAdmin.strings.confirm + ' ' + (vigilanteAdmin.strings.confirmResetAll || 'This will reset ALL settings to defaults.'))) {
1700 return;
1701 }
1702
1703 $.ajax({
1704 url: vigilanteAdmin.ajaxUrl,
1705 type: 'POST',
1706 data: {
1707 action: 'vigilante_apply_preset',
1708 nonce: vigilanteAdmin.nonce,
1709 preset: 'reset'
1710 },
1711 success: function(response) {
1712 if (response.success) {
1713 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.settingsResetDefaults || 'Settings reset to defaults.');
1714 setTimeout(function() {
1715 location.reload();
1716 }, 1000);
1717 } else {
1718 Vigilante_Admin.showNotice('error', response.data);
1719 }
1720 }
1721 });
1722 },
1723
1724 /**
1725 * Handle test headers
1726 */
1727 handleTestHeaders: function(e) {
1728 e.preventDefault();
1729
1730 var $btn = $(e.currentTarget);
1731 var $results = $('#vigilante-headers-result');
1732
1733 $btn.prop('disabled', true).html(vigilanteAdmin.strings.testing + ' <span class="vigilante-spinner"></span>');
1734
1735 $.ajax({
1736 url: vigilanteAdmin.ajaxUrl,
1737 type: 'POST',
1738 data: {
1739 action: 'vigilante_test_headers',
1740 nonce: vigilanteAdmin.nonce
1741 },
1742 success: function(response) {
1743 if (response.success) {
1744 Vigilante_Admin.displayHeaderResults(response.data, $results);
1745 } else {
1746 Vigilante_Admin.showNotice('error', response.data);
1747 }
1748 },
1749 complete: function() {
1750 $btn.prop('disabled', false).text(vigilanteAdmin.strings.testHeaders);
1751 }
1752 });
1753 },
1754
1755 /**
1756 * Handle login URL notification button
1757 */
1758 handleNotifyLoginUrl: function(e) {
1759 e.preventDefault();
1760
1761 var $btn = $(e.currentTarget);
1762 var $status = $btn.siblings('.vigilante-login-url-notify-status');
1763
1764 $btn.prop('disabled', true).text(vigilanteAdmin.strings.sending || 'Sending...');
1765 $status.removeClass('success error').text('');
1766
1767 $.ajax({
1768 url: vigilanteAdmin.ajaxUrl,
1769 type: 'POST',
1770 data: {
1771 action: 'vigilante_notify_login_url',
1772 nonce: vigilanteAdmin.nonce
1773 },
1774 success: function(response) {
1775 if (response.success) {
1776 var msg = response.data.sent + ' ' + (vigilanteAdmin.strings.notificationsSent || 'notifications sent');
1777 if (response.data.failed > 0) {
1778 msg += ', ' + response.data.failed + ' ' + (vigilanteAdmin.strings.failed || 'failed');
1779 }
1780 $status.addClass('success').text(msg);
1781 } else {
1782 $status.addClass('error').text(response.data || 'Error');
1783 }
1784 },
1785 error: function() {
1786 $status.addClass('error').text('Error');
1787 },
1788 complete: function() {
1789 $btn.prop('disabled', false).text(vigilanteAdmin.strings.sendNotification || 'Send notification now');
1790 }
1791 });
1792 },
1793
1794 /**
1795 * Toggle login URL notification section based on URL input
1796 */
1797 handleLoginUrlToggle: function() {
1798 var hasUrl = $('#vigilante_custom_login_url').val().trim().length > 0;
1799 $('.vigilante-login-url-notify-wrapper').toggleClass('vigilante-login-url-notify-disabled', !hasUrl);
1800 $('.vigilante-login-url-preview').toggle(hasUrl);
1801 },
1802
1803 /**
1804 * Display header test results
1805 */
1806 displayHeaderResults: function(results, $container) {
1807 var gradeClass = 'vigilante-grade-' + results.grade.toLowerCase();
1808 var html = '<div class="vigilante-security-score" style="text-align: center; padding: 20px;">';
1809 html += '<div class="vigilante-score-circle ' + gradeClass + '">';
1810 html += '<span class="vigilante-grade">' + results.grade + '</span>';
1811 html += '</div>';
1812 html += '<p style="text-align: center;"><strong>' + vigilanteAdmin.strings.score + ':</strong> ' + Math.round(results.score) + '%</p>';
1813 html += '</div>';
1814
1815 if (results.headers.length > 0) {
1816 html += '<p><strong>' + vigilanteAdmin.strings.enabledHeaders + ':</strong></p><ul>';
1817 results.headers.forEach(function(header) {
1818 html += '<li class="enabled">&#10003; ' + header + '</li>';
1819 });
1820 html += '</ul>';
1821 }
1822
1823 if (results.missing.length > 0) {
1824 html += '<p><strong>' + vigilanteAdmin.strings.missingHeaders + ':</strong></p><ul>';
1825 results.missing.forEach(function(header) {
1826 html += '<li class="missing">&#10007; ' + header + '</li>';
1827 });
1828 html += '</ul>';
1829 }
1830
1831 if (results.warnings && results.warnings.length > 0) {
1832 html += '<p><strong>' + vigilanteAdmin.strings.warnings + ':</strong></p><ul>';
1833 results.warnings.forEach(function(warning) {
1834 html += '<li class="warning">&#9888; ' + warning + '</li>';
1835 });
1836 html += '</ul>';
1837 }
1838
1839 $container.html(html).addClass('visible');
1840 },
1841
1842 /**
1843 * Handle activity log filter
1844 */
1845 handleLogFilter: function() {
1846 this.logPage = 1;
1847 this.loadActivityLogs();
1848 },
1849
1850 /**
1851 * Handle activity log search with debounce (400ms, min 3 chars)
1852 */
1853 handleLogSearch: function() {
1854 var self = this;
1855 clearTimeout(this.logSearchTimer);
1856 this.logSearchTimer = setTimeout(function() {
1857 var term = $('#vigilante-log-search').val().trim();
1858 // Only search with 3+ chars, or clear search when empty
1859 if (term.length >= 3 || term.length === 0) {
1860 self.logPage = 1;
1861 self.loadActivityLogs();
1862 }
1863 }, 400);
1864 },
1865
1866 /**
1867 * Load activity logs via AJAX (with filters and pagination)
1868 */
1869 loadActivityLogs: function() {
1870 var type = $('#vigilante-log-type-filter').val();
1871 var severity = $('#vigilante-log-severity-filter').val();
1872 var method = $('#vigilante-log-method-filter').val();
1873 var search = $('#vigilante-log-search').val();
1874 var $refreshBtn = $('#vigilante-log-refresh');
1875
1876 // Visual feedback
1877 $refreshBtn.prop('disabled', true).addClass('updating-message');
1878
1879 var data = {
1880 action: 'vigilante_get_logs',
1881 nonce: vigilanteAdmin.nonce,
1882 per_page: this.logPerPage,
1883 page: this.logPage,
1884 type: type,
1885 severity: severity
1886 };
1887
1888 if (method) {
1889 data.request_method = method;
1890 }
1891
1892 if (search && search.trim().length >= 3) {
1893 data.search = search.trim();
1894 }
1895
1896 $.ajax({
1897 url: vigilanteAdmin.ajaxUrl,
1898 type: 'POST',
1899 data: data,
1900 success: function(response) {
1901 if (response.success) {
1902 Vigilante_Admin.renderLogTable(response.data.logs);
1903 Vigilante_Admin.updateLogPagination(response.data.total);
1904 } else {
1905 Vigilante_Admin.showNotice('error', response.data || vigilanteAdmin.strings.error);
1906 }
1907 },
1908 error: function() {
1909 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error || 'Request failed');
1910 },
1911 complete: function() {
1912 $refreshBtn.prop('disabled', false).removeClass('updating-message');
1913 }
1914 });
1915 },
1916
1917 /**
1918 * Navigate activity log pages
1919 */
1920 goToLogPage: function(direction) {
1921 var $pagination = $('#vigilante-log-pagination');
1922 var total = parseInt($pagination.attr('data-total'), 10) || 0;
1923 var totalPages = Math.ceil(total / this.logPerPage) || 1;
1924
1925 switch (direction) {
1926 case 'first': this.logPage = 1; break;
1927 case 'prev': this.logPage = Math.max(1, this.logPage - 1); break;
1928 case 'next': this.logPage = Math.min(totalPages, this.logPage + 1); break;
1929 case 'last': this.logPage = totalPages; break;
1930 }
1931
1932 this.loadActivityLogs();
1933 },
1934
1935 /**
1936 * Update activity log pagination controls
1937 */
1938 updateLogPagination: function(total) {
1939 var $pagination = $('#vigilante-log-pagination');
1940 total = parseInt(total, 10) || 0;
1941 var totalPages = Math.ceil(total / this.logPerPage) || 1;
1942 var needsNav = total > this.logPerPage;
1943
1944 $pagination.attr('data-total', total);
1945 $pagination.attr('data-page', this.logPage);
1946
1947 var from = total > 0 ? ((this.logPage - 1) * this.logPerPage) + 1 : 0;
1948 var to = Math.min(this.logPage * this.logPerPage, total);
1949
1950 var text = total > 0
1951 ? (vigilanteAdmin.strings.paginationOf || '%1$d–%2$d of %3$d').replace('%1$d', from).replace('%2$d', to).replace('%3$d', total)
1952 : (vigilanteAdmin.strings.paginationEmpty || '0 items');
1953
1954 var prevDisabled = this.logPage <= 1 ? ' disabled' : '';
1955 var nextDisabled = this.logPage >= totalPages ? ' disabled' : '';
1956
1957 var html = '';
1958 if (needsNav) {
1959 html += '<button type="button" class="vigilante-page-first"' + prevDisabled + '>&laquo;</button>';
1960 html += '<button type="button" class="vigilante-page-prev"' + prevDisabled + '>&lsaquo;</button>';
1961 }
1962 html += '<span class="vigilante-page-info">' + text + '</span>';
1963 if (needsNav) {
1964 html += '<button type="button" class="vigilante-page-next"' + nextDisabled + '>&rsaquo;</button>';
1965 html += '<button type="button" class="vigilante-page-last"' + nextDisabled + '>&raquo;</button>';
1966 }
1967
1968 $pagination.html(html);
1969 },
1970
1971 /**
1972 * Render log table
1973 */
1974 renderLogTable: function(logs) {
1975 var $tbody = $('#vigilante-activity-log-table tbody');
1976 $tbody.empty();
1977
1978 if (logs.length === 0) {
1979 $tbody.append('<tr><td colspan="8">' + (vigilanteAdmin.strings.noLogEntries || 'No log entries found.') + '</td></tr>');
1980 return;
1981 }
1982
1983 var self = this;
1984 var typeLabels = (vigilanteAdmin.strings && vigilanteAdmin.strings.eventTypeLabels) || {};
1985 var severityLabels = (vigilanteAdmin.strings && vigilanteAdmin.strings.severityLabels) || {};
1986
1987 logs.forEach(function(log) {
1988 var details = {
1989 id: log.id,
1990 type: log.event_type || '',
1991 action: log.event_action || '',
1992 message: log.event_message || '',
1993 user: log.user_login || '',
1994 ip: log.ip_address || '',
1995 user_agent: log.user_agent || '',
1996 request_method: log.request_method || '',
1997 request_uri: log.request_uri || '',
1998 date: log.created_at || '',
1999 severity: log.severity || 'info',
2000 is_ip_whitelisted: !!log.is_ip_whitelisted,
2001 is_ip_blacklisted: !!log.is_ip_blacklisted,
2002 is_ua_whitelisted: !!log.is_ua_whitelisted,
2003 is_ua_blacklisted: !!log.is_ua_blacklisted
2004 };
2005
2006 var displayType = typeLabels[log.event_type] || log.event_type;
2007 var displaySeverity = severityLabels[log.severity] || log.severity;
2008
2009 var methodHtml = '-';
2010 if (log.request_method) {
2011 methodHtml = '<span class="vigilante-method-label vigilante-method-' + self.escapeHtml(log.request_method.toLowerCase()) + '">' + self.escapeHtml(log.request_method) + '</span>';
2012 }
2013
2014 var row = '<tr class="vigilante-severity-' + self.escapeHtml(log.severity) + '">';
2015 row += '<td>' + self.escapeHtml(log.created_at) + '</td>';
2016 row += '<td>' + self.escapeHtml(displayType) + '</td>';
2017 row += '<td>' + methodHtml + '</td>';
2018 row += '<td><span class="vigilante-badge vigilante-badge-' + self.escapeHtml(log.severity) + '">' + self.escapeHtml(displaySeverity) + '</span></td>';
2019 row += '<td>' + self.escapeHtml(log.event_message) + '</td>';
2020 row += '<td>' + self.escapeHtml(log.user_login || '-') + '</td>';
2021 row += '<td><code>' + self.escapeHtml(log.ip_address) + '</code></td>';
2022 row += '<td><button type="button" class="button button-small vigilante-view-log-details" data-details=\'' + self.escapeHtml(JSON.stringify(details)) + '\'>' + (vigilanteAdmin.strings.view || 'View') + '</button></td>';
2023 row += '</tr>';
2024 $tbody.append(row);
2025 });
2026 },
2027
2028 /**
2029 * Handle export logs
2030 */
2031 handleExportLogs: function(e) {
2032 e.preventDefault();
2033
2034 var $btn = $(e.currentTarget);
2035 var type = $('#vigilante-log-type-filter').val();
2036 var severity = $('#vigilante-log-severity-filter').val();
2037 var method = $('#vigilante-log-method-filter').val();
2038 var search = $('#vigilante-log-search').val();
2039
2040 var data = {
2041 action: 'vigilante_get_logs',
2042 nonce: vigilanteAdmin.nonce,
2043 per_page: 9999
2044 };
2045
2046 // Only add filters if they have values
2047 if (type && type !== '') {
2048 data.type = type;
2049 }
2050 if (severity && severity !== '') {
2051 data.severity = severity;
2052 }
2053 if (method && method !== '') {
2054 data.request_method = method;
2055 }
2056 if (search && search.trim().length >= 3) {
2057 data.search = search.trim();
2058 }
2059
2060 $btn.prop('disabled', true).text(vigilanteAdmin.strings.exporting || 'Exporting...');
2061
2062 $.ajax({
2063 url: vigilanteAdmin.ajaxUrl,
2064 type: 'POST',
2065 data: data,
2066 success: function(response) {
2067 if (response.success && response.data.logs && response.data.logs.length > 0) {
2068 Vigilante_Admin.downloadCSV(response.data.logs);
2069 Vigilante_Admin.showNotice('success', (vigilanteAdmin.strings.logsExported || 'Logs exported (%d entries)').replace('%d', response.data.logs.length));
2070 } else {
2071 Vigilante_Admin.showNotice('warning', vigilanteAdmin.strings.noLogsToExport || 'No logs to export');
2072 }
2073 },
2074 error: function() {
2075 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.exportFailed || 'Export failed');
2076 },
2077 complete: function() {
2078 $btn.prop('disabled', false).text(vigilanteAdmin.strings.exportLogs || 'Export Logs');
2079 }
2080 });
2081 },
2082
2083 /**
2084 * Download logs as CSV
2085 *
2086 * Every cell goes through csvCell(): quotes are doubled in all columns,
2087 * not only the message, and a value starting with = + - @ or a control
2088 * character gets an apostrophe in front so a spreadsheet shows it
2089 * instead of running it as a formula (S12). Action, address and
2090 * User-Agent were added for B38: the columns that help diagnose an
2091 * entry were exactly the ones the export left out.
2092 */
2093 downloadCSV: function(logs) {
2094 var csvCell = function(value) {
2095 var s = (value === null || value === undefined) ? '' : String(value);
2096 if (/^[=+\-@\t\r]/.test(s)) {
2097 s = "'" + s;
2098 }
2099 return '"' + s.replace(/"/g, '""') + '"';
2100 };
2101 var csv = 'Date,Type,Action,Method,Severity,Message,User,IP,Address,User Agent\n';
2102
2103 logs.forEach(function(log) {
2104 csv += [
2105 csvCell(log.created_at),
2106 csvCell(log.event_type),
2107 csvCell(log.event_action),
2108 csvCell(log.request_method),
2109 csvCell(log.severity),
2110 csvCell(log.event_message),
2111 csvCell(log.user_login),
2112 csvCell(log.ip_address),
2113 csvCell(log.request_uri),
2114 csvCell(log.user_agent)
2115 ].join(',') + '\n';
2116 });
2117
2118 var blob = new Blob([csv], { type: 'text/csv' });
2119 var url = URL.createObjectURL(blob);
2120 var a = document.createElement('a');
2121 a.href = url;
2122 a.download = 'activity-log-' + new Date().toISOString().slice(0, 10) + '.csv';
2123 document.body.appendChild(a);
2124 a.click();
2125 document.body.removeChild(a);
2126 URL.revokeObjectURL(url);
2127 },
2128
2129 /**
2130 * Handle create backup
2131 */
2132 handleCreateBackup: function(e) {
2133 e.preventDefault();
2134
2135 var $btn = $(e.currentTarget);
2136 $btn.prop('disabled', true).html(vigilanteAdmin.strings.loading + ' <span class="vigilante-spinner"></span>');
2137
2138 // Direct file download via a hidden form (same pattern as the
2139 // database backup): the server streams a ZIP with wp-config.php and
2140 // .htaccess as an attachment.
2141 var $form = $('<form>', { method: 'POST', action: vigilanteAdmin.ajaxUrl });
2142 $form.append($('<input>', { type: 'hidden', name: 'action', value: 'vigilante_download_files_backup' }));
2143 $form.append($('<input>', { type: 'hidden', name: 'nonce', value: vigilanteAdmin.nonce }));
2144 $('body').append($form);
2145 $form.submit();
2146 $form.remove();
2147
2148 // Re-enable the button after a short delay (download runs in the background).
2149 setTimeout(function() {
2150 $btn.prop('disabled', false).text(vigilanteAdmin.strings.createBackupNow || 'Create Backup Now');
2151 }, 2000);
2152 },
2153
2154 // =================================================================
2155 // DATABASE BACKUP HANDLERS
2156 // =================================================================
2157
2158 /**
2159 * Toggle database backup panel and load tables
2160 */
2161 handleDbBackupToggle: function(e) {
2162 e.preventDefault();
2163
2164 var $panel = $('.vigilante-db-backup-panel');
2165 var $btn = $(e.currentTarget);
2166
2167 if ($panel.is(':visible')) {
2168 $panel.slideUp(200);
2169 return;
2170 }
2171
2172 $panel.slideDown(200);
2173
2174 // Load tables if not already loaded
2175 if ($panel.find('.vigilante-db-tables-content').is(':hidden')) {
2176 this.loadDbTables();
2177 }
2178 },
2179
2180 /**
2181 * Handle settings search input
2182 */
2183 searchActiveIndex: -1,
2184
2185 handleSettingsSearch: function(e) {
2186 var query = ($('#vigilante-settings-search').val() || '').trim().toLowerCase();
2187 var $results = $('#vigilante-settings-search-results');
2188 var index = (vigilanteAdmin.searchIndex || []);
2189
2190 if (query.length < 2) {
2191 $results.attr('hidden', true).empty();
2192 this.searchActiveIndex = -1;
2193 return;
2194 }
2195
2196 // Score every entry and sort before cutting. It used to stop at the
2197 // first 30 matches in index order, so a broad query ("password"
2198 // already returns 19) could hide the very row the user was after
2199 // behind whatever happened to be declared first.
2200 var scored = [];
2201 for (var i = 0; i < index.length; i++) {
2202 var entry = index[i];
2203 var label = (entry.label || '').toLowerCase();
2204 var labelEn = (entry.label_en || '').toLowerCase();
2205 var section = (entry.section || '').toLowerCase();
2206 var tabLabel = (entry.tab_label || '').toLowerCase();
2207 var keywords = (entry.keywords || '').toLowerCase();
2208 var score = 0;
2209
2210 if (label === query || labelEn === query) {
2211 score = 100;
2212 } else if (label.indexOf(query) === 0 || labelEn.indexOf(query) === 0) {
2213 score = 80;
2214 } else if (label.indexOf(query) !== -1 || labelEn.indexOf(query) !== -1) {
2215 score = 60;
2216 } else if (section.indexOf(query) !== -1) {
2217 score = 40;
2218 } else if (tabLabel.indexOf(query) !== -1) {
2219 score = 35;
2220 } else if (keywords.indexOf(query) !== -1) {
2221 score = 30;
2222 }
2223
2224 if (score > 0) {
2225 scored.push({ entry: entry, score: score, order: i });
2226 }
2227 }
2228
2229 scored.sort(function(a, b) {
2230 return (b.score - a.score) || (a.order - b.order);
2231 });
2232
2233 var matches = [];
2234 for (var t = 0; t < scored.length && matches.length < 30; t++) {
2235 matches.push(scored[t].entry);
2236 }
2237 var hiddenCount = scored.length - matches.length;
2238
2239 if (!matches.length) {
2240 $results.html('<div class="vigilante-search-empty">' + (vigilanteAdmin.strings.searchNoResults || 'No results') + '</div>').attr('hidden', false);
2241 this.searchActiveIndex = -1;
2242 return;
2243 }
2244
2245 // Group by tab
2246 var grouped = {};
2247 var tabOrder = [];
2248 for (var j = 0; j < matches.length; j++) {
2249 var m = matches[j];
2250 if (!grouped[m.tab]) {
2251 grouped[m.tab] = { label: m.tab_label, items: [] };
2252 tabOrder.push(m.tab);
2253 }
2254 grouped[m.tab].items.push(m);
2255 }
2256
2257 var html = '';
2258 var flatIdx = 0;
2259 for (var k = 0; k < tabOrder.length; k++) {
2260 var tab = tabOrder[k];
2261 var group = grouped[tab];
2262 html += '<div class="vigilante-search-group">';
2263 html += '<div class="vigilante-search-group-title">' + this.escapeHtml(group.label) + '</div>';
2264 for (var n = 0; n < group.items.length; n++) {
2265 var it = group.items[n];
2266 var base = (vigilanteAdmin.adminUrl || 'admin.php?page=vigilante');
2267 var href = base + '&tab=' + encodeURIComponent(it.tab) + '#' + encodeURIComponent(it.anchor);
2268 var activeClass = flatIdx === 0 ? ' vigilante-search-item-active' : '';
2269 html += '<a class="vigilante-search-item' + activeClass + '" data-idx="' + flatIdx + '" role="option" href="' + href + '">';
2270 html += '<span class="vigilante-search-item-label">' + this.escapeHtml(it.label) + '</span>';
2271 html += '<span class="vigilante-search-item-section">' + this.escapeHtml(it.section) + '</span>';
2272 html += '</a>';
2273 flatIdx++;
2274 }
2275 html += '</div>';
2276 }
2277
2278 if (hiddenCount > 0) {
2279 var moreTpl = vigilanteAdmin.strings.searchMoreResults || '%d more results, refine the search';
2280 html += '<div class="vigilante-search-more">' + this.escapeHtml(moreTpl.replace('%d', hiddenCount)) + '</div>';
2281 }
2282
2283 $results.html(html).attr('hidden', false);
2284 this.searchActiveIndex = 0;
2285 },
2286
2287 /**
2288 * Handle keyboard navigation in search: arrows move highlight, Enter navigates, Escape closes
2289 */
2290 handleSettingsSearchKey: function(e) {
2291 var $items = $('#vigilante-settings-search-results .vigilante-search-item');
2292
2293 if (e.key === 'Escape') {
2294 $('#vigilante-settings-search-results').attr('hidden', true);
2295 $('#vigilante-settings-search').blur();
2296 return;
2297 }
2298
2299 if (!$items.length) return;
2300
2301 if (e.key === 'ArrowDown') {
2302 e.preventDefault();
2303 this.searchActiveIndex = Math.min(this.searchActiveIndex + 1, $items.length - 1);
2304 this.updateSearchActive();
2305 } else if (e.key === 'ArrowUp') {
2306 e.preventDefault();
2307 this.searchActiveIndex = Math.max(this.searchActiveIndex - 1, 0);
2308 this.updateSearchActive();
2309 } else if (e.key === 'Home') {
2310 e.preventDefault();
2311 this.searchActiveIndex = 0;
2312 this.updateSearchActive();
2313 } else if (e.key === 'End') {
2314 e.preventDefault();
2315 this.searchActiveIndex = $items.length - 1;
2316 this.updateSearchActive();
2317 } else if (e.key === 'Enter') {
2318 e.preventDefault();
2319 var idx = this.searchActiveIndex >= 0 ? this.searchActiveIndex : 0;
2320 var $target = $items.eq(idx);
2321 if ($target.length) {
2322 window.location.href = $target.attr('href');
2323 }
2324 }
2325 },
2326
2327 /**
2328 * Sync active highlight when user hovers a search result
2329 */
2330 handleSettingsSearchHover: function(e) {
2331 var idx = parseInt($(e.currentTarget).attr('data-idx'), 10);
2332 if (isNaN(idx)) return;
2333 this.searchActiveIndex = idx;
2334 this.updateSearchActive(false);
2335 },
2336
2337 /**
2338 * Apply the active class to the current index and scroll it into view
2339 */
2340 updateSearchActive: function(scroll) {
2341 var $items = $('#vigilante-settings-search-results .vigilante-search-item');
2342 $items.removeClass('vigilante-search-item-active');
2343 var $active = $items.eq(this.searchActiveIndex);
2344 if (!$active.length) return;
2345 $active.addClass('vigilante-search-item-active');
2346 if (scroll !== false && $active[0] && typeof $active[0].scrollIntoView === 'function') {
2347 $active[0].scrollIntoView({ block: 'nearest' });
2348 }
2349 },
2350
2351 /**
2352 * Format bytes as a human-readable size
2353 */
2354 formatBytes: function(bytes) {
2355 if (!bytes || bytes < 0) return '0 B';
2356 var units = ['B', 'KB', 'MB', 'GB', 'TB'];
2357 var i = 0;
2358 var n = bytes;
2359 while (n >= 1024 && i < units.length - 1) {
2360 n = n / 1024;
2361 i++;
2362 }
2363 return (i === 0 ? n.toFixed(0) : n.toFixed(1)) + ' ' + units[i];
2364 },
2365
2366 /**
2367 * Update DB tables info: count + total size
2368 */
2369 updateDbTablesInfo: function() {
2370 var $boxes = $('.vigilante-db-table-check');
2371 var total = $boxes.length;
2372 if (!total) {
2373 $('.vigilante-db-tables-info').text('');
2374 return;
2375 }
2376 var $checked = $boxes.filter(':checked');
2377 var checkedCount = $checked.length;
2378 var bytes = 0;
2379 $checked.each(function() {
2380 bytes += parseInt($(this).attr('data-bytes'), 10) || 0;
2381 });
2382 var sizeStr = Vigilante_Admin.formatBytes(bytes);
2383 var tpl;
2384 if (checkedCount === total) {
2385 tpl = vigilanteAdmin.strings.dbTablesTotal || '%1$d tables total (%2$s)';
2386 $('.vigilante-db-tables-info').text(tpl.replace('%1$d', total).replace('%2$s', sizeStr));
2387 } else {
2388 tpl = vigilanteAdmin.strings.dbTablesSelected || '%1$d tables selected (%2$s)';
2389 $('.vigilante-db-tables-info').text(tpl.replace('%1$d', checkedCount).replace('%2$s', sizeStr));
2390 }
2391 },
2392
2393 /**
2394 * Load database tables via AJAX
2395 */
2396 loadDbTables: function() {
2397 $.ajax({
2398 url: vigilanteAdmin.ajaxUrl,
2399 type: 'POST',
2400 data: {
2401 action: 'vigilante_get_db_tables',
2402 nonce: vigilanteAdmin.nonce
2403 },
2404 success: function(response) {
2405 if (!response.success) {
2406 Vigilante_Admin.showNotice('error', response.data);
2407 return;
2408 }
2409
2410 var data = response.data;
2411 var coreHtml = '';
2412 var otherHtml = '';
2413
2414 // Build core tables checkboxes
2415 if (data.core && data.core.length) {
2416 $.each(data.core, function(i, table) {
2417 coreHtml += '<label class="vigilante-db-table-label">';
2418 coreHtml += '<input type="checkbox" class="vigilante-db-table-check" value="' + table.name + '" data-bytes="' + (table.bytes || 0) + '" checked>';
2419 coreHtml += '<span class="vigilante-db-table-name" title="' + table.name + '">' + table.short + '</span>';
2420 coreHtml += '<span class="vigilante-db-table-meta">' + table.rows + ' rows &middot; ' + table.size + '</span>';
2421 coreHtml += '</label>';
2422 });
2423 }
2424
2425 // Build other tables checkboxes
2426 if (data.other && data.other.length) {
2427 $.each(data.other, function(i, table) {
2428 otherHtml += '<label class="vigilante-db-table-label">';
2429 otherHtml += '<input type="checkbox" class="vigilante-db-table-check" value="' + table.name + '" data-bytes="' + (table.bytes || 0) + '" checked>';
2430 otherHtml += '<span class="vigilante-db-table-name" title="' + table.name + '">' + table.short + '</span>';
2431 otherHtml += '<span class="vigilante-db-table-meta">' + table.rows + ' rows &middot; ' + table.size + '</span>';
2432 otherHtml += '</label>';
2433 });
2434 $('#vigilante-db-other-group').show();
2435 }
2436
2437 $('#vigilante-db-core-tables').html(coreHtml);
2438 $('#vigilante-db-other-tables').html(otherHtml);
2439
2440 Vigilante_Admin.updateDbTablesInfo();
2441
2442 $('.vigilante-db-tables-loading').hide();
2443 $('.vigilante-db-tables-content').show();
2444 }
2445 });
2446 },
2447
2448 /**
2449 * Select/deselect all tables
2450 */
2451 handleDbSelectAll: function(e) {
2452 var checked = $(e.currentTarget).prop('checked');
2453 $('.vigilante-db-table-check').prop('checked', checked);
2454 this.updateDbTablesInfo();
2455 },
2456
2457 /**
2458 * Update select-all state when individual checkbox changes
2459 */
2460 handleDbTableCheck: function() {
2461 var total = $('.vigilante-db-table-check').length;
2462 var checked = $('.vigilante-db-table-check:checked').length;
2463 $('#vigilante-db-select-all').prop('checked', total === checked);
2464 this.updateDbTablesInfo();
2465 },
2466
2467 /**
2468 * Download database backup
2469 */
2470 handleDbBackupDownload: function(e) {
2471 e.preventDefault();
2472
2473 var tables = [];
2474 $('.vigilante-db-table-check:checked').each(function() {
2475 tables.push($(this).val());
2476 });
2477
2478 if (tables.length === 0) {
2479 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.dbBackupNoTables);
2480 return;
2481 }
2482
2483 var $btn = $(e.currentTarget);
2484 $btn.prop('disabled', true).html(vigilanteAdmin.strings.dbBackupDownloading + ' <span class="vigilante-spinner"></span>');
2485
2486 // Submit via hidden form for direct file download
2487 var $form = $('<form>', {
2488 method: 'POST',
2489 action: vigilanteAdmin.ajaxUrl
2490 });
2491
2492 $form.append($('<input>', { type: 'hidden', name: 'action', value: 'vigilante_download_db_backup' }));
2493 $form.append($('<input>', { type: 'hidden', name: 'nonce', value: vigilanteAdmin.nonce }));
2494 $form.append($('<input>', { type: 'hidden', name: 'tables', value: tables.join(',') }));
2495
2496 $('body').append($form);
2497 $form.submit();
2498 $form.remove();
2499
2500 // Re-enable button after a delay (download starts in background)
2501 setTimeout(function() {
2502 $btn.prop('disabled', false).text($btn.data('original-text') || vigilanteAdmin.strings.downloadBackup || 'Download Backup (.zip)');
2503 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.dbBackupSuccess);
2504 }, 3000);
2505 },
2506
2507 // =================================================================
2508 // DATABASE PREFIX HANDLERS
2509 // =================================================================
2510
2511 /**
2512 * Regenerate random prefix
2513 */
2514 handleDbRegeneratePrefix: function(e) {
2515 e.preventDefault();
2516
2517 var $btn = $(e.currentTarget);
2518 $btn.prop('disabled', true);
2519
2520 $.ajax({
2521 url: vigilanteAdmin.ajaxUrl,
2522 type: 'POST',
2523 data: {
2524 action: 'vigilante_generate_prefix',
2525 nonce: vigilanteAdmin.nonce
2526 },
2527 success: function(response) {
2528 if (response.success) {
2529 $('#vigilante-new-prefix').text(response.data.prefix);
2530 } else {
2531 Vigilante_Admin.showNotice('error', response.data);
2532 }
2533 },
2534 complete: function() {
2535 $btn.prop('disabled', false);
2536 }
2537 });
2538 },
2539
2540 /**
2541 * Toggle the change prefix button based on checkbox
2542 */
2543 handleDbPrefixConfirmToggle: function(e) {
2544 var checked = $(e.currentTarget).prop('checked');
2545 $('.vigilante-db-change-prefix').prop('disabled', !checked);
2546 },
2547
2548 /**
2549 * Change database prefix
2550 */
2551 handleDbChangePrefix: function(e) {
2552 e.preventDefault();
2553
2554 // Verify checkbox is checked
2555 if (!$('#vigilante-prefix-backup-confirm').prop('checked')) {
2556 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.dbPrefixCheckbox);
2557 return;
2558 }
2559
2560 var newPrefix = $('#vigilante-new-prefix').text().trim();
2561
2562 if (!confirm(vigilanteAdmin.strings.dbPrefixConfirm)) {
2563 return;
2564 }
2565
2566 var $btn = $(e.currentTarget);
2567 $btn.prop('disabled', true).html(vigilanteAdmin.strings.dbPrefixChanging + ' <span class="vigilante-spinner"></span>');
2568
2569 $.ajax({
2570 url: vigilanteAdmin.ajaxUrl,
2571 type: 'POST',
2572 data: {
2573 action: 'vigilante_change_prefix',
2574 nonce: vigilanteAdmin.nonce,
2575 prefix: newPrefix
2576 },
2577 success: function(response) {
2578 if (response.success) {
2579 Vigilante_Admin.showNotice('success', vigilanteAdmin.strings.dbPrefixSuccess);
2580 // Reload page after prefix change (session will still work)
2581 setTimeout(function() {
2582 location.reload();
2583 }, 2000);
2584 } else {
2585 Vigilante_Admin.showNotice('error', response.data);
2586 $btn.prop('disabled', false).text($btn.data('original-text'));
2587 }
2588 },
2589 error: function() {
2590 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
2591 $btn.prop('disabled', false).text($btn.data('original-text'));
2592 }
2593 });
2594 },
2595
2596 /**
2597 * Show notice
2598 */
2599 showNotice: function(type, message) {
2600 // Remove any existing notices first.
2601 $('.vigilante-notice').remove();
2602
2603 var $notice = $('<div class="vigilante-notice vigilante-notice-' + type + '">' + message + '</div>');
2604 $('.vigilante-main').prepend($notice);
2605
2606 // Scroll to top of the settings area so user sees the notice.
2607 $('html, body').animate({
2608 scrollTop: $('.vigilante-admin-wrap').offset().top - 50
2609 }, 300);
2610
2611 setTimeout(function() {
2612 $notice.fadeOut(function() {
2613 $(this).remove();
2614 });
2615 }, 5000);
2616 },
2617
2618 /**
2619 * Handle view log details
2620 */
2621 handleViewLogDetails: function(e) {
2622 e.preventDefault();
2623 e.stopPropagation();
2624
2625 var $btn = $(e.currentTarget);
2626 var detailsStr = $btn.attr('data-details');
2627
2628 if (!detailsStr) {
2629 return;
2630 }
2631
2632 var details;
2633 try {
2634 details = JSON.parse(detailsStr);
2635 } catch (err) {
2636 return;
2637 }
2638
2639 var html = '';
2640
2641 var s = vigilanteAdmin.strings;
2642
2643 // -- Section: Request --
2644 html += '<div class="vigilante-popup-section">';
2645 html += '<h4 class="vigilante-popup-section-title">' + this.escapeHtml(s.logRequest || 'Request') + '</h4>';
2646 html += '<table class="vigilante-details-table">';
2647 html += '<tr><th>' + this.escapeHtml(s.logDate || 'Date') + '</th><td>' + this.escapeHtml(details.date) + '</td></tr>';
2648 if (details.request_method) {
2649 html += '<tr><th>' + this.escapeHtml(s.logMethod || 'Method') + '</th><td><span class="vigilante-method-label vigilante-method-' + this.escapeHtml(details.request_method.toLowerCase()) + '">' + this.escapeHtml(details.request_method) + '</span></td></tr>';
2650 }
2651 html += '<tr><th>' + this.escapeHtml(s.logType || 'Type') + '</th><td>' + this.escapeHtml(details.type) + '</td></tr>';
2652 html += '<tr><th>' + this.escapeHtml(s.logAction || 'Action') + '</th><td>' + this.escapeHtml(details.action || '-') + '</td></tr>';
2653 html += '<tr><th>' + this.escapeHtml(s.logSeverity || 'Severity') + '</th><td><span class="vigilante-badge vigilante-badge-' + this.escapeHtml(details.severity || 'info') + '">' + this.escapeHtml(details.severity || 'info') + '</span></td></tr>';
2654 html += '<tr><th>' + this.escapeHtml(s.logMessage || 'Message') + '</th><td>' + this.escapeHtml(details.message) + '</td></tr>';
2655 if (details.request_uri) {
2656 html += '<tr><th>' + this.escapeHtml(s.logRequestUri || 'Address') + '</th><td class="vigilante-ua-cell"><code>' + this.escapeHtml(details.request_uri) + '</code></td></tr>';
2657 }
2658 html += '</table>';
2659 html += '</div>';
2660
2661 // -- Section: Client --
2662 html += '<div class="vigilante-popup-section">';
2663 html += '<h4 class="vigilante-popup-section-title">' + this.escapeHtml(s.logClient || 'Client') + '</h4>';
2664 html += '<table class="vigilante-details-table">';
2665 html += '<tr><th>' + this.escapeHtml(s.logUser || 'User') + '</th><td>' + this.escapeHtml(details.user || '-') + '</td></tr>';
2666
2667 // IP with lookup link
2668 if (details.ip && details.ip !== '0.0.0.0') {
2669 var ipEscaped = this.escapeHtml(details.ip);
2670 html += '<tr><th>' + this.escapeHtml(s.logIpAddress || 'IP Address') + '</th><td>';
2671 html += '<code>' + ipEscaped + '</code> ';
2672 html += '<a href="https://abuseipdb.com/check/' + encodeURIComponent(details.ip) + '" target="_blank" rel="noopener noreferrer" class="vigilante-ip-lookup" title="Lookup IP">';
2673 html += '<span class="dashicons dashicons-search"></span></a>';
2674 html += '</td></tr>';
2675 } else {
2676 html += '<tr><th>' + this.escapeHtml(s.logIpAddress || 'IP Address') + '</th><td><code>' + this.escapeHtml(details.ip) + '</code></td></tr>';
2677 }
2678
2679 if (details.user_agent) {
2680 html += '<tr><th>' + this.escapeHtml(s.logUserAgent || 'User Agent') + '</th><td class="vigilante-ua-cell"><code>' + this.escapeHtml(details.user_agent) + '</code></td></tr>';
2681 }
2682 html += '</table>';
2683
2684 // Action buttons for IP and UA
2685 html += '<div class="vigilante-popup-actions">';
2686 if (details.ip && details.ip !== '0.0.0.0') {
2687 html += '<span class="vigilante-popup-actions-label">' + this.escapeHtml(s.logIpLabel || 'IP:') + '</span>';
2688 if (details.is_ip_whitelisted) {
2689 html += '<button type="button" class="button button-small vigilante-btn-added" disabled><span class="dashicons dashicons-yes-alt"></span> ' + this.escapeHtml(s.logInWhitelist || 'In whitelist') + '</button> ';
2690 } else {
2691 html += '<button type="button" class="button button-small vigilante-add-to-list" data-item-type="ip" data-list-type="whitelist" data-value="' + this.escapeHtml(details.ip) + '"><span class="dashicons dashicons-yes-alt"></span> ' + this.escapeHtml(s.logWhitelist || 'Whitelist') + '</button> ';
2692 }
2693 if (details.is_ip_blacklisted) {
2694 html += '<button type="button" class="button button-small vigilante-btn-added" disabled><span class="dashicons dashicons-dismiss"></span> ' + this.escapeHtml(s.logInBlacklist || 'In blacklist') + '</button> ';
2695 } else {
2696 html += '<button type="button" class="button button-small vigilante-add-to-list vigilante-btn-danger" data-item-type="ip" data-list-type="blacklist" data-value="' + this.escapeHtml(details.ip) + '"><span class="dashicons dashicons-dismiss"></span> ' + this.escapeHtml(s.logBlacklist || 'Blacklist') + '</button> ';
2697 }
2698 }
2699 if (details.user_agent) {
2700 html += '<span class="vigilante-popup-actions-label">' + this.escapeHtml(s.logUaLabel || 'UA:') + '</span>';
2701 if (details.is_ua_whitelisted) {
2702 html += '<button type="button" class="button button-small vigilante-btn-added" disabled><span class="dashicons dashicons-yes-alt"></span> ' + this.escapeHtml(s.logInWhitelist || 'In whitelist') + '</button> ';
2703 } else {
2704 html += '<button type="button" class="button button-small vigilante-add-to-list" data-item-type="ua" data-list-type="whitelist" data-value="' + this.escapeHtml(details.user_agent) + '"><span class="dashicons dashicons-yes-alt"></span> ' + this.escapeHtml(s.logWhitelist || 'Whitelist') + '</button> ';
2705 }
2706 if (details.is_ua_blacklisted) {
2707 html += '<button type="button" class="button button-small vigilante-btn-added" disabled><span class="dashicons dashicons-dismiss"></span> ' + this.escapeHtml(s.logInBlacklist || 'In blacklist') + '</button>';
2708 } else {
2709 html += '<button type="button" class="button button-small vigilante-add-to-list vigilante-btn-danger" data-item-type="ua" data-list-type="blacklist" data-value="' + this.escapeHtml(details.user_agent) + '"><span class="dashicons dashicons-dismiss"></span> ' + this.escapeHtml(s.logBlacklist || 'Blacklist') + '</button>';
2710 }
2711 }
2712 html += '</div>';
2713 html += '<div class="vigilante-popup-feedback" style="display:none;"></div>';
2714 html += '</div>';
2715
2716 // Event ID for reference
2717 html += '<div class="vigilante-popup-footer">';
2718 html += '<span class="vigilante-popup-id">ID: ' + this.escapeHtml(details.id) + '</span>';
2719 html += '</div>';
2720
2721 $('#vigilante-log-details-content').html(html);
2722 $('#vigilante-log-details-modal').fadeIn(200);
2723 },
2724
2725 /**
2726 * Handle adding IP or UA to firewall whitelist/blacklist
2727 */
2728 handleAddToFirewallList: function(e) {
2729 e.preventDefault();
2730 var $btn = $(e.currentTarget);
2731 var itemType = $btn.data('item-type');
2732 var listType = $btn.data('list-type');
2733 // attr(), not data(): jQuery runs JSON.parse on values that look like
2734 // JSON, so a User-Agent such as {"a":1} would arrive as an object.
2735 var value = $btn.attr('data-value');
2736
2737 if (!itemType || !listType || !value) {
2738 return;
2739 }
2740
2741 $btn.prop('disabled', true);
2742
2743 var self = this;
2744 $.ajax({
2745 url: vigilanteAdmin.ajaxUrl,
2746 type: 'POST',
2747 data: {
2748 action: 'vigilante_add_to_firewall_list',
2749 nonce: vigilanteAdmin.nonce,
2750 item_type: itemType,
2751 list_type: listType,
2752 value: value
2753 },
2754 success: function(response) {
2755 var $feedback = $('.vigilante-popup-feedback');
2756 if (response.success) {
2757 $btn.text(vigilanteAdmin.strings.logAdded || 'Added!').addClass('vigilante-btn-added');
2758 $feedback.html('<span class="vigilante-popup-msg vigilante-popup-msg-success">' + response.data + '</span>').slideDown(150);
2759 } else {
2760 $btn.prop('disabled', false);
2761 $feedback.html('<span class="vigilante-popup-msg vigilante-popup-msg-warning">' + (response.data || vigilanteAdmin.strings.logErrorAddingToList || 'Error adding to list') + '</span>').slideDown(150);
2762 }
2763 },
2764 error: function() {
2765 $btn.prop('disabled', false);
2766 $('.vigilante-popup-feedback').html('<span class="vigilante-popup-msg vigilante-popup-msg-error">' + (vigilanteAdmin.strings.logRequestFailed || 'Request failed') + '</span>').slideDown(150);
2767 }
2768 });
2769 },
2770
2771 /**
2772 * Handle unblock firewall IP
2773 */
2774 handleUnblockFirewallIp: function(e) {
2775 var $btn = $(e.currentTarget);
2776 var ip = $btn.data('ip');
2777
2778 if (!confirm(vigilanteAdmin.strings.confirmUnblockIp || 'Unblock this IP?')) {
2779 return;
2780 }
2781
2782 $btn.prop('disabled', true).text(vigilanteAdmin.strings.loading || 'Loading...');
2783
2784 $.ajax({
2785 url: vigilanteAdmin.ajaxUrl,
2786 type: 'POST',
2787 data: {
2788 action: 'vigilante_unblock_firewall_ip',
2789 nonce: vigilanteAdmin.nonce,
2790 ip: ip
2791 },
2792 success: function(response) {
2793 if (response.success) {
2794 $btn.closest('tr').fadeOut(300, function() {
2795 var $tbody = $(this).closest('tbody');
2796 $(this).remove();
2797 // Hide the whole section if no more blocked IPs
2798 if ($tbody.children('tr').length === 0) {
2799 $tbody.closest('.vigilante-lockout-section').fadeOut(300);
2800 }
2801 });
2802 } else {
2803 alert(response.data || 'Error');
2804 $btn.prop('disabled', false).text(vigilanteAdmin.strings.unblock || 'Unblock');
2805 }
2806 },
2807 error: function() {
2808 $btn.prop('disabled', false).text(vigilanteAdmin.strings.unblock || 'Unblock');
2809 }
2810 });
2811 },
2812
2813 /**
2814 * Handle close modal
2815 */
2816 handleCloseModal: function(e) {
2817 if ($(e.target).hasClass('vigilante-modal') || $(e.target).hasClass('vigilante-modal-close')) {
2818 $('.vigilante-modal').hide();
2819 }
2820 },
2821
2822 /**
2823 * Escape HTML
2824 */
2825 escapeHtml: function(text) {
2826 if (text === null || text === undefined) return '';
2827 if (typeof text !== 'string') {
2828 text = String(text);
2829 }
2830 // Quotes must be encoded too: this helper is also used in attribute
2831 // position, where an unescaped quote closes the attribute early.
2832 return text
2833 .replace(/&/g, '&amp;')
2834 .replace(/</g, '&lt;')
2835 .replace(/>/g, '&gt;')
2836 .replace(/"/g, '&quot;')
2837 .replace(/'/g, '&#039;');
2838 },
2839
2840 /**
2841 * Debounce function
2842 */
2843 debounce: function(func, wait) {
2844 var timeout;
2845 return function() {
2846 var context = this, args = arguments;
2847 clearTimeout(timeout);
2848 timeout = setTimeout(function() {
2849 func.apply(context, args);
2850 }, wait);
2851 };
2852 },
2853
2854 // =================================================================
2855 // Force Password Reset functionality
2856 // =================================================================
2857
2858 /**
2859 * Selected users for password reset
2860 */
2861 selectedPasswordResetUsers: [],
2862
2863 /**
2864 * Initialize password reset handlers
2865 */
2866 initPasswordReset: function() {
2867 var self = this;
2868
2869 // User search for password reset
2870 var searchTimeout;
2871 $(document).on('input', '#vigilante-password-reset-search', function() {
2872 var query = $(this).val();
2873 clearTimeout(searchTimeout);
2874
2875 if (query.length < 2) {
2876 $('#vigilante-password-reset-results').hide().empty();
2877 return;
2878 }
2879
2880 searchTimeout = setTimeout(function() {
2881 self.searchUsersForPasswordReset(query);
2882 }, 300);
2883 });
2884
2885 // Close search results when clicking outside
2886 $(document).on('click', function(e) {
2887 if (!$(e.target).closest('.vigilante-user-search-wrapper').length) {
2888 $('#vigilante-password-reset-results').hide();
2889 }
2890 });
2891
2892 // Reset selected users button
2893 $(document).on('click', '#vigilante-reset-selected-users', function(e) {
2894 e.preventDefault();
2895 self.forceResetSelectedUsers();
2896 });
2897
2898 // Reset all users button
2899 $(document).on('click', '#vigilante-reset-all-users', function(e) {
2900 e.preventDefault();
2901 self.forceResetAllUsers();
2902 });
2903
2904 // Reset by role - checkbox change
2905 $(document).on('change', '.vigilante-reset-role-checkbox', function() {
2906 self.updateRoleResetSummary();
2907 });
2908
2909 // Reset by role button
2910 $(document).on('click', '#vigilante-reset-by-role', function(e) {
2911 e.preventDefault();
2912 self.forceResetByRoles();
2913 });
2914 },
2915
2916 /**
2917 * Search users for password reset
2918 */
2919 searchUsersForPasswordReset: function(query) {
2920 var self = this;
2921 var $results = $('#vigilante-password-reset-results');
2922
2923 $.ajax({
2924 url: vigilanteAdmin.ajaxUrl,
2925 type: 'POST',
2926 data: {
2927 action: 'vigilante_search_users_password_reset',
2928 nonce: vigilanteAdmin.nonce,
2929 query: query
2930 },
2931 success: function(response) {
2932 if (response.success && response.data.length > 0) {
2933 var html = '<ul class="vigilante-user-search-list">';
2934 response.data.forEach(function(user) {
2935 var isSelected = self.selectedPasswordResetUsers.indexOf(user.ID) !== -1;
2936 html += '<li data-user-id="' + user.ID + '" class="' + (isSelected ? 'selected' : '') + '">';
2937 html += '<img src="' + self.escapeHtml(user.avatar) + '" alt="" class="vigilante-user-avatar">';
2938 html += '<div class="vigilante-user-info">';
2939 html += '<strong>' + self.escapeHtml(user.display_name) + '</strong>';
2940 html += '<br><small>' + self.escapeHtml(user.user_login) + ' - ' + self.escapeHtml(user.user_email) + '</small>';
2941 html += '<br><small class="vigilante-user-roles">' + self.escapeHtml(user.roles) + '</small>';
2942 html += '</div>';
2943 html += '</li>';
2944 });
2945 html += '</ul>';
2946 $results.html(html).show();
2947
2948 // Handle user selection
2949 $results.find('li').on('click', function() {
2950 var userId = parseInt($(this).data('user-id'));
2951 var userName = $(this).find('strong').text();
2952 var userEmail = $(this).find('small').first().text();
2953 self.togglePasswordResetUser(userId, userName, userEmail);
2954 $(this).toggleClass('selected');
2955 });
2956 } else {
2957 $results.html('<p class="vigilante-no-results">' + (vigilanteAdmin.strings.noUsersFoundSearch || 'No users found') + '</p>').show();
2958 }
2959 }
2960 });
2961 },
2962
2963 /**
2964 * Toggle user selection for password reset
2965 */
2966 togglePasswordResetUser: function(userId, userName, userEmail) {
2967 var index = this.selectedPasswordResetUsers.indexOf(userId);
2968 var $selectedContainer = $('#vigilante-password-reset-selected');
2969 var $list = $selectedContainer.find('.vigilante-selected-users-list');
2970 var $btn = $('#vigilante-reset-selected-users');
2971
2972 if (index === -1) {
2973 // Add user
2974 this.selectedPasswordResetUsers.push(userId);
2975 $list.append(
2976 '<li data-user-id="' + userId + '">' +
2977 '<span>' + this.escapeHtml(userName) + '</span>' +
2978 '<button type="button" class="vigilante-remove-user" title="Remove">&times;</button>' +
2979 '</li>'
2980 );
2981 } else {
2982 // Remove user
2983 this.selectedPasswordResetUsers.splice(index, 1);
2984 $list.find('li[data-user-id="' + userId + '"]').remove();
2985 }
2986
2987 // Update UI
2988 if (this.selectedPasswordResetUsers.length > 0) {
2989 $selectedContainer.show();
2990 $btn.prop('disabled', false);
2991 } else {
2992 $selectedContainer.hide();
2993 $btn.prop('disabled', true);
2994 }
2995
2996 // Handle remove button click
2997 $list.find('.vigilante-remove-user').off('click').on('click', function() {
2998 var uid = $(this).closest('li').data('user-id');
2999 var idx = Vigilante_Admin.selectedPasswordResetUsers.indexOf(uid);
3000 if (idx !== -1) {
3001 Vigilante_Admin.selectedPasswordResetUsers.splice(idx, 1);
3002 }
3003 $(this).closest('li').remove();
3004 $('#vigilante-password-reset-results').find('li[data-user-id="' + uid + '"]').removeClass('selected');
3005
3006 if (Vigilante_Admin.selectedPasswordResetUsers.length === 0) {
3007 $selectedContainer.hide();
3008 $btn.prop('disabled', true);
3009 }
3010 });
3011 },
3012
3013 /**
3014 * Force reset for selected users
3015 */
3016 forceResetSelectedUsers: function() {
3017 var self = this;
3018
3019 if (this.selectedPasswordResetUsers.length === 0) {
3020 return;
3021 }
3022
3023 var currentUserId = parseInt(vigilanteAdmin.currentUserId || 0);
3024 var resettingSelf = this.selectedPasswordResetUsers.indexOf(currentUserId) !== -1;
3025
3026 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);
3027 if (resettingSelf) {
3028 confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelf || 'WARNING: You are including yourself. Your session will end and you will need to set a new password.');
3029 }
3030
3031 if (!confirm(confirmMsg)) {
3032 return;
3033 }
3034
3035 var $btn = $('#vigilante-reset-selected-users');
3036 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
3037
3038 $.ajax({
3039 url: vigilanteAdmin.ajaxUrl,
3040 type: 'POST',
3041 data: {
3042 action: 'vigilante_force_password_reset',
3043 nonce: vigilanteAdmin.nonce,
3044 user_ids: this.selectedPasswordResetUsers
3045 },
3046 success: function(response) {
3047 if (response.success) {
3048 self.showNotice('success', response.data.message);
3049
3050 // Clear selection
3051 self.selectedPasswordResetUsers = [];
3052 $('#vigilante-password-reset-selected').hide().find('.vigilante-selected-users-list').empty();
3053 $('#vigilante-password-reset-search').val('');
3054 $('#vigilante-password-reset-results').hide().empty();
3055
3056 // Reload if resetting self
3057 if (response.data.resetting_self) {
3058 setTimeout(function() {
3059 window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php';
3060 }, 2000);
3061 } else {
3062 setTimeout(function() {
3063 location.reload();
3064 }, 1500);
3065 }
3066 } else {
3067 self.showNotice('error', response.data);
3068 }
3069 },
3070 error: function() {
3071 self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred');
3072 },
3073 complete: function() {
3074 $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetSelected || 'Force Reset for Selected Users');
3075 }
3076 });
3077 },
3078
3079 /**
3080 * Force reset for all users
3081 */
3082 forceResetAllUsers: function() {
3083 var self = this;
3084 var includeSelf = $('#vigilante-reset-all-include-self').is(':checked');
3085
3086 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?';
3087 if (includeSelf) {
3088 confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelfAll || 'WARNING: You are including yourself. Your session will end immediately.');
3089 }
3090
3091 if (!confirm(confirmMsg)) {
3092 return;
3093 }
3094
3095 var $btn = $('#vigilante-reset-all-users');
3096 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
3097
3098 $.ajax({
3099 url: vigilanteAdmin.ajaxUrl,
3100 type: 'POST',
3101 data: {
3102 action: 'vigilante_force_password_reset_all',
3103 nonce: vigilanteAdmin.nonce,
3104 include_self: includeSelf ? 1 : 0
3105 },
3106 success: function(response) {
3107 if (response.success) {
3108 self.showNotice('success', response.data.message);
3109
3110 // Reload if resetting self
3111 if (response.data.resetting_self) {
3112 setTimeout(function() {
3113 window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php';
3114 }, 2000);
3115 } else {
3116 setTimeout(function() {
3117 location.reload();
3118 }, 1500);
3119 }
3120 } else {
3121 self.showNotice('error', response.data);
3122 }
3123 },
3124 error: function() {
3125 self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred');
3126 },
3127 complete: function() {
3128 $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetAll || 'Force Reset for ALL Users');
3129 }
3130 });
3131 },
3132
3133 /**
3134 * Update role reset summary when checkboxes change
3135 */
3136 updateRoleResetSummary: function() {
3137 var totalCount = 0;
3138 var includesSelf = false;
3139
3140 $('.vigilante-reset-role-checkbox:checked').each(function() {
3141 totalCount += parseInt($(this).data('count') || 0);
3142 if ($(this).closest('label').find('em').length > 0) {
3143 includesSelf = true;
3144 }
3145 });
3146
3147 var $summary = $('#vigilante-reset-role-summary');
3148 var $selfOption = $('#vigilante-reset-role-self-option');
3149 var $btn = $('#vigilante-reset-by-role');
3150
3151 if (totalCount > 0) {
3152 $('#vigilante-reset-role-count').text(totalCount);
3153 $summary.show();
3154 $btn.prop('disabled', false);
3155 } else {
3156 $summary.hide();
3157 $btn.prop('disabled', true);
3158 }
3159
3160 if (includesSelf) {
3161 $selfOption.show();
3162 } else {
3163 $selfOption.hide();
3164 $('#vigilante-reset-role-include-self').prop('checked', false);
3165 }
3166 },
3167
3168 /**
3169 * Force reset for users with selected roles
3170 */
3171 forceResetByRoles: function() {
3172 var self = this;
3173 var roles = [];
3174
3175 $('.vigilante-reset-role-checkbox:checked').each(function() {
3176 roles.push($(this).val());
3177 });
3178
3179 if (roles.length === 0) {
3180 self.showNotice('error', vigilanteAdmin.strings.noRolesSelected || 'Please select at least one role.');
3181 return;
3182 }
3183
3184 var totalCount = 0;
3185 $('.vigilante-reset-role-checkbox:checked').each(function() {
3186 totalCount += parseInt($(this).data('count') || 0);
3187 });
3188
3189 var includeSelf = $('#vigilante-reset-role-include-self').is(':checked');
3190
3191 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);
3192 if (includeSelf) {
3193 confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelfAll || 'WARNING: You are including yourself. Your session will end immediately.');
3194 }
3195
3196 if (!confirm(confirmMsg)) {
3197 return;
3198 }
3199
3200 var $btn = $('#vigilante-reset-by-role');
3201 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
3202
3203 $.ajax({
3204 url: vigilanteAdmin.ajaxUrl,
3205 type: 'POST',
3206 data: {
3207 action: 'vigilante_force_password_reset_by_role',
3208 nonce: vigilanteAdmin.nonce,
3209 roles: roles,
3210 include_self: includeSelf ? 1 : 0
3211 },
3212 success: function(response) {
3213 if (response.success) {
3214 self.showNotice('success', response.data.message);
3215
3216 // Clear role checkboxes
3217 $('.vigilante-reset-role-checkbox').prop('checked', false);
3218 self.updateRoleResetSummary();
3219
3220 if (response.data.resetting_self) {
3221 setTimeout(function() {
3222 window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php';
3223 }, 2000);
3224 } else {
3225 setTimeout(function() {
3226 location.reload();
3227 }, 1500);
3228 }
3229 } else {
3230 self.showNotice('error', response.data);
3231 }
3232 },
3233 error: function() {
3234 self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred');
3235 },
3236 complete: function() {
3237 $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetByRole || 'Force Reset for Selected Roles');
3238 }
3239 });
3240 },
3241
3242 /**
3243 * Initialize user approval handlers
3244 */
3245 initUserApproval: function() {
3246 var self = this;
3247
3248 // Approve user
3249 $(document).on('click', '.vigilante-approve-user', function() {
3250 var $btn = $(this);
3251 var userId = $btn.data('user-id');
3252 var $row = $btn.closest('tr');
3253
3254 if (!confirm(vigilanteAdmin.strings.confirmApprove || 'Approve this user?')) {
3255 return;
3256 }
3257
3258 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
3259
3260 $.ajax({
3261 url: vigilanteAdmin.ajaxUrl,
3262 type: 'POST',
3263 data: {
3264 action: 'vigilante_approve_user',
3265 nonce: vigilanteAdmin.nonce,
3266 user_id: userId
3267 },
3268 success: function(response) {
3269 if (response.success) {
3270 self.showNotice('success', response.data.message);
3271 $row.fadeOut(400, function() {
3272 $(this).remove();
3273 self.updatePendingCount();
3274 });
3275 } else {
3276 self.showNotice('error', response.data);
3277 $btn.prop('disabled', false).text(vigilanteAdmin.strings.approve || 'Approve');
3278 }
3279 },
3280 error: function() {
3281 self.showNotice('error', vigilanteAdmin.strings.error);
3282 $btn.prop('disabled', false).text(vigilanteAdmin.strings.approve || 'Approve');
3283 }
3284 });
3285 });
3286
3287 // Reject user
3288 $(document).on('click', '.vigilante-reject-user', function() {
3289 var $btn = $(this);
3290 var userId = $btn.data('user-id');
3291 var $row = $btn.closest('tr');
3292
3293 var reason = prompt(vigilanteAdmin.strings.rejectReason || 'Enter rejection reason (optional):');
3294 if (reason === null) {
3295 return; // Cancelled
3296 }
3297
3298 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
3299
3300 $.ajax({
3301 url: vigilanteAdmin.ajaxUrl,
3302 type: 'POST',
3303 data: {
3304 action: 'vigilante_reject_user',
3305 nonce: vigilanteAdmin.nonce,
3306 user_id: userId,
3307 reason: reason
3308 },
3309 success: function(response) {
3310 if (response.success) {
3311 self.showNotice('success', response.data.message);
3312 $row.fadeOut(400, function() {
3313 $(this).remove();
3314 self.updatePendingCount();
3315 });
3316 } else {
3317 self.showNotice('error', response.data);
3318 $btn.prop('disabled', false).text(vigilanteAdmin.strings.reject || 'Reject');
3319 }
3320 },
3321 error: function() {
3322 self.showNotice('error', vigilanteAdmin.strings.error);
3323 $btn.prop('disabled', false).text(vigilanteAdmin.strings.reject || 'Reject');
3324 }
3325 });
3326 });
3327 },
3328
3329 /**
3330 * Update pending users count badge
3331 */
3332 updatePendingCount: function() {
3333 var $badge = $('.vigilante-pending-users-section .vigilante-badge-warning');
3334 var $table = $('.vigilante-pending-users-table tbody tr');
3335 var count = $table.length;
3336
3337 if (count === 0) {
3338 $badge.remove();
3339 $('.vigilante-pending-users-table').replaceWith(
3340 '<div class="vigilante-no-lockouts">' +
3341 '<span class="dashicons dashicons-yes-alt"></span>' +
3342 '<p>' + (vigilanteAdmin.strings.noPending || 'No pending registrations.') + '</p>' +
3343 '</div>'
3344 );
3345 } else {
3346 $badge.text(count);
3347 }
3348 },
3349
3350 /**
3351 * Initialize session management handlers
3352 */
3353 initSessionManagement: function() {
3354 var self = this;
3355 var searchTimer = null;
3356 this.selectedSessionUserId = null;
3357
3358 // Search users for session management
3359 $(document).on('input', '#vigilante-session-user-search', function() {
3360 var query = $(this).val();
3361 var $results = $('#vigilante-session-search-results');
3362
3363 clearTimeout(searchTimer);
3364
3365 if (query.length < 2) {
3366 $results.hide().empty();
3367 return;
3368 }
3369
3370 searchTimer = setTimeout(function() {
3371 $.ajax({
3372 url: vigilanteAdmin.ajaxUrl,
3373 type: 'POST',
3374 data: {
3375 action: 'vigilante_search_users_password_reset',
3376 nonce: vigilanteAdmin.nonce,
3377 query: query
3378 },
3379 success: function(response) {
3380 if (response.success && response.data.length > 0) {
3381 var html = '<ul class="vigilante-user-search-list">';
3382 $.each(response.data, function(i, user) {
3383 html += '<li data-user-id="' + user.ID + '">' +
3384 '<img src="' + self.escapeHtml(user.avatar) + '" class="vigilante-user-avatar" alt="">' +
3385 '<div class="vigilante-user-info">' +
3386 '<strong>' + self.escapeHtml(user.display_name) + '</strong><br>' +
3387 '<small>' + self.escapeHtml(user.user_email) + '</small>' +
3388 '</div>' +
3389 '</li>';
3390 });
3391 html += '</ul>';
3392 $results.html(html).show();
3393 } else {
3394 $results.html('<p class="vigilante-no-results">' + (vigilanteAdmin.strings.noUsers || 'No users found') + '</p>').show();
3395 }
3396 }
3397 });
3398 }, 300);
3399 });
3400
3401 // Click handler for user selection in session search
3402 $(document).on('click', '#vigilante-session-search-results li', function() {
3403 var userId = $(this).data('user-id');
3404 var userName = $(this).find('strong').text();
3405 self.loadUserSessions(userId, userName);
3406 $('#vigilante-session-search-results').hide();
3407 $('#vigilante-session-user-search').val(userName);
3408 });
3409
3410 // Revoke single session
3411 $(document).on('click', '.vigilante-revoke-session', function() {
3412 var $btn = $(this);
3413 var userId = $btn.attr('data-user-id');
3414 var token = $btn.attr('data-token');
3415 var $row = $btn.closest('tr');
3416 var $paginatedContainer = $btn.closest('.vigilante-paginated-section');
3417
3418 // Fallback to row token if button token is invalid
3419 if (!token || token.length < 32) {
3420 token = $row.attr('data-token');
3421 }
3422
3423 if (!token || token.length < 32) {
3424 self.showNotice('error', vigilanteAdmin.strings.error || 'Invalid session token');
3425 return;
3426 }
3427
3428 if (!confirm(vigilanteAdmin.strings.confirmRevoke || 'Revoke this session?')) {
3429 return;
3430 }
3431
3432 $btn.prop('disabled', true);
3433
3434 $.ajax({
3435 url: vigilanteAdmin.ajaxUrl,
3436 type: 'POST',
3437 data: {
3438 action: 'vigilante_revoke_session',
3439 nonce: vigilanteAdmin.nonce,
3440 user_id: userId,
3441 token: token
3442 },
3443 success: function(response) {
3444 if (response.success) {
3445 self.showNotice('success', response.data.message);
3446 $row.fadeOut(400, function() {
3447 $(this).remove();
3448 if ($paginatedContainer.length) {
3449 Vigilante_Admin.refreshFilePagination($paginatedContainer);
3450 }
3451 });
3452 } else {
3453 self.showNotice('error', response.data);
3454 $btn.prop('disabled', false);
3455 }
3456 },
3457 error: function() {
3458 self.showNotice('error', vigilanteAdmin.strings.error);
3459 $btn.prop('disabled', false);
3460 }
3461 });
3462 });
3463
3464 // Revoke all other sessions (current user)
3465 $(document).on('click', '.vigilante-revoke-other-sessions', function() {
3466 var $btn = $(this);
3467 var userId = $btn.data('user-id');
3468
3469 if (!confirm(vigilanteAdmin.strings.confirmRevokeAll || 'Revoke all other sessions?')) {
3470 return;
3471 }
3472
3473 $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
3474
3475 $.ajax({
3476 url: vigilanteAdmin.ajaxUrl,
3477 type: 'POST',
3478 data: {
3479 action: 'vigilante_revoke_all_sessions',
3480 nonce: vigilanteAdmin.nonce,
3481 user_id: userId,
3482 include_current: 0
3483 },
3484 success: function(response) {
3485 if (response.success) {
3486 self.showNotice('success', response.data.message);
3487 setTimeout(function() {
3488 location.reload();
3489 }, 1000);
3490 } else {
3491 self.showNotice('error', response.data);
3492 }
3493 },
3494 error: function() {
3495 self.showNotice('error', vigilanteAdmin.strings.error);
3496 },
3497 complete: function() {
3498 $btn.prop('disabled', false).text(vigilanteAdmin.strings.revokeOthers || 'Revoke All Other Sessions');
3499 }
3500 });
3501 });
3502
3503 // Revoke all sessions for a user (admin managing other users)
3504 $(document).on('click', '.vigilante-revoke-all-user-sessions', function() {
3505 var $btn = $(this);
3506
3507 if (!self.selectedSessionUserId) {
3508 return;
3509 }
3510
3511 if (!confirm(vigilanteAdmin.strings.confirmRevokeAllUser || 'Revoke ALL sessions for this user? They will be logged out everywhere.')) {
3512 return;
3513 }
3514
3515 $btn.prop('disabled', true);
3516
3517 $.ajax({
3518 url: vigilanteAdmin.ajaxUrl,
3519 type: 'POST',
3520 data: {
3521 action: 'vigilante_revoke_all_sessions',
3522 nonce: vigilanteAdmin.nonce,
3523 user_id: self.selectedSessionUserId,
3524 include_current: 1
3525 },
3526 success: function(response) {
3527 if (response.success) {
3528 self.showNotice('success', response.data.message);
3529 $('#vigilante-user-sessions-container').hide();
3530 $('#vigilante-session-user-search').val('');
3531 } else {
3532 self.showNotice('error', response.data);
3533 }
3534 },
3535 error: function() {
3536 self.showNotice('error', vigilanteAdmin.strings.error);
3537 },
3538 complete: function() {
3539 $btn.prop('disabled', false);
3540 }
3541 });
3542 });
3543 },
3544
3545 /**
3546 * Load sessions for a specific user
3547 */
3548 loadUserSessions: function(userId, userName) {
3549 var self = this;
3550 this.selectedSessionUserId = userId;
3551 var $container = $('#vigilante-user-sessions-container');
3552 var $list = $('#vigilante-user-sessions-list');
3553
3554 $list.html('<tr><td colspan="4">' + (vigilanteAdmin.strings.loading || 'Loading...') + '</td></tr>');
3555 $container.show();
3556 $('#vigilante-sessions-user-name').text((vigilanteAdmin.strings.sessionsFor || 'Sessions for: ') + userName);
3557
3558 $.ajax({
3559 url: vigilanteAdmin.ajaxUrl,
3560 type: 'POST',
3561 data: {
3562 action: 'vigilante_get_user_sessions',
3563 nonce: vigilanteAdmin.nonce,
3564 user_id: userId
3565 },
3566 success: function(response) {
3567 if (response.success) {
3568 var sessions = response.data.sessions;
3569 if (sessions.length === 0) {
3570 $list.html('<tr><td colspan="4">' + (vigilanteAdmin.strings.noSessions || 'No active sessions') + '</td></tr>');
3571 return;
3572 }
3573
3574 var html = '';
3575 $.each(sessions, function(i, session) {
3576 var loginTime = session.login ? self.timeAgo(session.login) : (vigilanteAdmin.strings.unknown || 'Unknown');
3577 html += '<tr data-token="' + session.token_hash + '">' +
3578 '<td>' + self.escapeHtml(session.browser) + '</td>' +
3579 '<td><code>' + self.escapeHtml(session.ip) + '</code></td>' +
3580 '<td>' + loginTime + '</td>' +
3581 '<td>' +
3582 '<button type="button" class="button button-small vigilante-revoke-session" ' +
3583 'data-user-id="' + userId + '" data-token="' + session.token_hash + '">' +
3584 (vigilanteAdmin.strings.revoke || 'Revoke') +
3585 '</button>' +
3586 '</td>' +
3587 '</tr>';
3588 });
3589 $list.html(html);
3590 } else {
3591 $list.html('<tr><td colspan="4">' + response.data + '</td></tr>');
3592 }
3593 },
3594 error: function() {
3595 $list.html('<tr><td colspan="4">' + vigilanteAdmin.strings.error + '</td></tr>');
3596 }
3597 });
3598 },
3599
3600 /**
3601 * Format timestamp as time ago
3602 */
3603 timeAgo: function(timestamp) {
3604 var seconds = Math.floor((Date.now() / 1000) - timestamp);
3605 var s = vigilanteAdmin.strings;
3606 var intervals = [
3607 { label: s.timeYear || 'year', labelPlural: s.timeYears || 'years', seconds: 31536000 },
3608 { label: s.timeMonth || 'month', labelPlural: s.timeMonths || 'months', seconds: 2592000 },
3609 { label: s.timeDay || 'day', labelPlural: s.timeDays || 'days', seconds: 86400 },
3610 { label: s.timeHour || 'hour', labelPlural: s.timeHours || 'hours', seconds: 3600 },
3611 { label: s.timeMinute || 'minute', labelPlural: s.timeMinutes || 'minutes', seconds: 60 }
3612 ];
3613
3614 for (var i = 0; i < intervals.length; i++) {
3615 var count = Math.floor(seconds / intervals[i].seconds);
3616 if (count >= 1) {
3617 var unit = count > 1 ? intervals[i].labelPlural : intervals[i].label;
3618 return (s.timeAgo || '%1$d %2$s ago').replace('%1$d', count).replace('%2$s', unit);
3619 }
3620 }
3621 return s.justNow || 'Just now';
3622 }
3623 };
3624
3625 /* ---------------------------------------------------------------------
3626 * Security Analyzer (Dashboard widget) — v2.1.0
3627 * ------------------------------------------------------------------ */
3628
3629 var VigilanteAnalyzer = {
3630
3631 $root: null,
3632
3633 init: function() {
3634 this.$root = $('#vigilante-analyzer');
3635 if (!this.$root.length) {
3636 // Widget not present on this page — still run hash focus in case a fix link was followed.
3637 this.hashFocusFlash();
3638 return;
3639 }
3640
3641 // Scan button — 2-phase AJAX.
3642 $(document).on('click', '#vigilante-analyzer-scan', this.handleScan.bind(this));
3643
3644 // Expand/collapse detailed breakdown.
3645 $(document).on('click', '.vigilante-analyzer-toggle', this.handleToggleDetails.bind(this));
3646
3647 // Save weekly cron + email toggle on change.
3648 $(document).on('change',
3649 '.vigilante-analyzer-weekly input[type="checkbox"]',
3650 this.handleSettingsChange.bind(this));
3651
3652 // Pulse the row we landed on if we came from a fix link.
3653 this.hashFocusFlash();
3654
3655 // Upgrade the server-rendered SVG (no-op if already correct).
3656 this.renderSparklineFromData();
3657 },
3658
3659 /**
3660 * Kick off a scan. Phase 'fast' first for instant feedback, then 'slow' for remote checks.
3661 */
3662 handleScan: function(e) {
3663 e.preventDefault();
3664 if (this.isScanning) {
3665 return;
3666 }
3667 this.isScanning = true;
3668
3669 var $btn = $('#vigilante-analyzer-scan');
3670 var originalHtml = $btn.html();
3671 $btn.data('original-html', originalHtml);
3672 $btn.prop('disabled', true)
3673 .html('<span class="dashicons dashicons-update vigilante-analyzer-spin"></span> '
3674 + (vigilanteAdmin.strings.analyzerFastPhase || 'Running fast checks…'));
3675
3676 var self = this;
3677
3678 // Expand the details section so the user sees results pour in.
3679 this.expandDetails(true);
3680
3681 this.runScanPhase('fast')
3682 .done(function(fastReport) {
3683 self.applyReport(fastReport);
3684 $btn.html('<span class="dashicons dashicons-update vigilante-analyzer-spin"></span> '
3685 + (vigilanteAdmin.strings.analyzerSlowPhase || 'Running remote checks…'));
3686
3687 self.runScanPhase('slow')
3688 .done(function(slowReport) {
3689 self.applyReport(slowReport);
3690 if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) {
3691 Vigilante_Admin.showNotice('success',
3692 vigilanteAdmin.strings.analyzerScanComplete || 'Security scan complete.');
3693 }
3694 })
3695 .fail(function(msg) {
3696 if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) {
3697 Vigilante_Admin.showNotice('error',
3698 msg || vigilanteAdmin.strings.analyzerScanFailed || 'Security scan failed.');
3699 }
3700 })
3701 .always(function() {
3702 $btn.prop('disabled', false).html($btn.data('original-html') || originalHtml);
3703 self.isScanning = false;
3704 });
3705 })
3706 .fail(function(msg) {
3707 $btn.prop('disabled', false).html($btn.data('original-html') || originalHtml);
3708 self.isScanning = false;
3709 if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) {
3710 Vigilante_Admin.showNotice('error',
3711 msg || vigilanteAdmin.strings.analyzerScanFailed || 'Security scan failed.');
3712 }
3713 });
3714 },
3715
3716 runScanPhase: function(phase) {
3717 var deferred = $.Deferred();
3718 $.ajax({
3719 url: vigilanteAdmin.ajaxUrl,
3720 type: 'POST',
3721 timeout: 60000,
3722 data: {
3723 action: 'vigilante_analyzer_run',
3724 nonce: vigilanteAdmin.nonce,
3725 phase: phase
3726 }
3727 }).done(function(response) {
3728 if (response && response.success) {
3729 deferred.resolve(response.data);
3730 } else {
3731 deferred.reject(response && response.data ? response.data : null);
3732 }
3733 }).fail(function(xhr, status, err) {
3734 deferred.reject(err || status);
3735 });
3736 return deferred.promise();
3737 },
3738
3739 /**
3740 * Map a 0-100 score percentage to a quality tag {label, slug}, mirroring
3741 * Vigilante_Admin::analyzer_quality_tag() in PHP.
3742 */
3743 qualityTag: function(pct) {
3744 pct = Math.max(0, Math.min(100, parseInt(pct, 10) || 0));
3745 var s = vigilanteAdmin.strings || {};
3746 // "Excellent" is reserved for a perfect score — a single missing point drops to Good.
3747 if (pct === 100) { return { label: s.analyzerQualityExcellent || 'Excellent', slug: 'a' }; }
3748 if (pct >= 70) { return { label: s.analyzerQualityGood || 'Good', slug: 'b' }; }
3749 if (pct >= 50) { return { label: s.analyzerQualityFair || 'Fair', slug: 'c' }; }
3750 if (pct >= 30) { return { label: s.analyzerQualityPoor || 'Poor', slug: 'd' }; }
3751 return { label: s.analyzerQualityCritical || 'Critical', slug: 'e' };
3752 },
3753
3754 /**
3755 * Swap a {a,b,c,d,e} slug modifier class on an element.
3756 */
3757 setQualityClass: function($el, baseClass, slug) {
3758 $el.removeClass(function(i, cls) {
3759 return (cls.match(new RegExp('(^|\\s)' + baseClass + '-[abcde](?=\\s|$)', 'g')) || []).join(' ');
3760 });
3761 $el.addClass(baseClass + '-' + slug);
3762 },
3763
3764 /**
3765 * Merge a phase report into the widget DOM.
3766 */
3767 applyReport: function(report) {
3768 if (!report || typeof report !== 'object') {
3769 return;
3770 }
3771 var $root = this.$root;
3772 var self = this;
3773
3774 // Overall pass/warn/fail counts (summary).
3775 if (report.counts && typeof report.counts === 'object') {
3776 $root.find('[data-role="pass"]').text(report.counts.pass || 0);
3777 $root.find('[data-role="warn"]').text(report.counts.warn || 0);
3778 $root.find('[data-role="fail"]').text(report.counts.fail || 0);
3779 }
3780
3781 // Main score circle + quality tag.
3782 if (report.grade) {
3783 var $card = $root.find('.vigilante-analyzer-score-card');
3784 var $circle = $card.find('.vigilante-score-circle');
3785 var gradeLc = String(report.grade).toLowerCase();
3786 var scoreN = parseInt(report.score, 10) || 0;
3787
3788 if (!$circle.length || $circle.hasClass('vigilante-grade-empty')) {
3789 $circle.remove();
3790 $card.prepend(
3791 '<div class="vigilante-score-circle vigilante-grade-'
3792 + this.escAttr(gradeLc)
3793 + '">'
3794 + '<span class="vigilante-grade">' + this.esc(report.grade) + '</span>'
3795 + '<span class="vigilante-score-text">' + this.esc(scoreN) + '%</span>'
3796 + '</div>'
3797 );
3798 } else {
3799 $circle.removeClass(function(i, cls) {
3800 return (cls.match(/(^|\s)vigilante-grade-\S+/g) || []).join(' ');
3801 }).addClass('vigilante-grade-' + gradeLc);
3802 $circle.find('.vigilante-grade').text(report.grade);
3803 $circle.find('.vigilante-score-text').text(scoreN + '%');
3804 }
3805
3806 // Quality tag in the score card.
3807 var quality = this.qualityTag(scoreN);
3808 var $tag = $root.find('[data-role="quality-tag"]');
3809 if ($tag.length) {
3810 this.setQualityClass($tag, 'vigilante-analyzer-quality', quality.slug);
3811 $tag.text(quality.label);
3812 }
3813 }
3814
3815 // "Last scan" label in the header.
3816 if (report.ran_at) {
3817 var $ranAt = $root.find('[data-role="ran-at"]');
3818 var justNow = vigilanteAdmin.strings.analyzerLastScanJustNow
3819 || 'Last scan just now';
3820 $ranAt.html(
3821 '<span class="dashicons dashicons-clock" aria-hidden="true"></span>'
3822 + this.esc(justNow)
3823 );
3824 }
3825
3826 // Per-category rows: quality pill, state counts, score, bar, and check rows.
3827 if (report.categories && typeof report.categories === 'object') {
3828 $.each(report.categories, function(slug, cat) {
3829 var $cat = $root.find('.vigilante-analyzer-category[data-category="' + slug + '"]');
3830 if (!$cat.length) {
3831 return;
3832 }
3833
3834 var infoOnly = $cat.attr('data-info-only') === '1';
3835 var counts = (cat.counts && typeof cat.counts === 'object') ? cat.counts : {};
3836
3837 if (!infoOnly) {
3838 var earned = parseInt(cat.earned, 10) || 0;
3839 var max = parseInt(cat.max, 10) || 0;
3840 var pct = max > 0 ? Math.round((earned / max) * 100) : 0;
3841 var cq = self.qualityTag(pct);
3842
3843 $cat.find('[data-role="earned"]').text(earned);
3844
3845 var $catQuality = $cat.find('[data-role="category-quality"]');
3846 if ($catQuality.length) {
3847 self.setQualityClass($catQuality, 'vigilante-analyzer-quality', cq.slug);
3848 // Update only the label span; the pill also contains the
3849 // "X/Y tests" counter so a blanket .text() would wipe it.
3850 var $label = $catQuality.find('[data-role="category-quality-label"]');
3851 if ($label.length) {
3852 $label.text(cq.label);
3853 } else {
3854 $catQuality.text(cq.label);
3855 }
3856 }
3857
3858 // "X/Y tests" inside the pill. Total is everything that
3859 // scores (pass + warn + fail), info/skip are excluded
3860 // because they don't move the category score.
3861 var $tests = $cat.find('[data-role="category-tests"]');
3862 if ($tests.length) {
3863 var passed = parseInt(counts.pass, 10) || 0;
3864 var total = passed
3865 + (parseInt(counts.warn, 10) || 0)
3866 + (parseInt(counts.fail, 10) || 0);
3867 var tpl = (vigilanteAdmin.strings && vigilanteAdmin.strings.testsCounter)
3868 || '%1$d/%2$d tests';
3869 $tests.text(tpl.replace('%1$d', passed).replace('%2$d', total));
3870 $tests.attr('data-passed', passed);
3871 $tests.attr('data-total', total);
3872 }
3873
3874 // Progress bar (width + quality color).
3875 var $bar = $cat.find('[data-role="category-bar"]');
3876 if ($bar.length) {
3877 $bar.css('width', pct + '%');
3878 self.setQualityClass($bar, 'vigilante-analyzer-category-bar-fill', cq.slug);
3879 }
3880 }
3881
3882 // State counts — show each chip only when count > 0.
3883 // Info-only categories (like reputation) also show an info count.
3884 self.updateCategoryState($cat, 'pass', parseInt(counts.pass, 10) || 0);
3885 self.updateCategoryState($cat, 'warn', parseInt(counts.warn, 10) || 0);
3886 self.updateCategoryState($cat, 'fail', parseInt(counts.fail, 10) || 0);
3887 if (infoOnly) {
3888 self.updateCategoryState($cat, 'info', parseInt(counts.info, 10) || 0);
3889 // Info-only categories replace the progress bar with a status pill
3890 // ("All clear" when nothing actionable, "N findings" otherwise).
3891 self.updateInfoStatus($cat, counts);
3892 }
3893
3894 // Replace the check rows only for checks this phase actually returned.
3895 if (cat.checks && cat.checks.length) {
3896 var $list = $cat.find('[data-role="check-list"]');
3897 $list.find('.vigilante-analyzer-check-empty').remove();
3898
3899 $.each(cat.checks, function(i, check) {
3900 var $existing = $list.find('[data-check-id="' + check.id + '"]');
3901 var html = self.renderCheckRow(check);
3902 if ($existing.length) {
3903 $existing.replaceWith(html);
3904 } else {
3905 $list.append(html);
3906 }
3907 });
3908 }
3909 });
3910 }
3911 },
3912
3913 /**
3914 * Update a per-category state chip (pass/warn/fail). Adds the chip if it doesn't
3915 * exist yet, updates its number, or removes it when the count is zero.
3916 */
3917 updateCategoryState: function($cat, state, count) {
3918 var $wrap = $cat.find('[data-role="category-states"]');
3919 if (!$wrap.length) {
3920 return;
3921 }
3922 var selector = '.vigilante-analyzer-category-state--' + state;
3923 var $chip = $wrap.find(selector);
3924
3925 if (count > 0) {
3926 if (!$chip.length) {
3927 var iconMap = { pass: 'yes', warn: 'warning', fail: 'no', info: 'info-outline' };
3928 var titleMap = {
3929 pass: 'Passing',
3930 warn: 'Warning',
3931 fail: 'Failing',
3932 info: 'Informational'
3933 };
3934 var title = titleMap[state] || '';
3935 $chip = $(
3936 '<span class="vigilante-analyzer-category-state vigilante-analyzer-category-state--'
3937 + state + '" title="' + this.escAttr(title) + '">'
3938 + '<span data-role="state-' + state + '">0</span>'
3939 + '<span class="dashicons dashicons-' + iconMap[state] + '" aria-hidden="true"></span>'
3940 + '</span>'
3941 );
3942 // Keep order: pass → warn → fail → info.
3943 if (state === 'pass') {
3944 $wrap.prepend($chip);
3945 } else if (state === 'warn') {
3946 var $passChip = $wrap.find('.vigilante-analyzer-category-state--pass');
3947 if ($passChip.length) { $chip.insertAfter($passChip); } else { $wrap.prepend($chip); }
3948 } else if (state === 'fail') {
3949 var $infoChip = $wrap.find('.vigilante-analyzer-category-state--info');
3950 if ($infoChip.length) { $chip.insertBefore($infoChip); } else { $wrap.append($chip); }
3951 } else {
3952 $wrap.append($chip);
3953 }
3954 }
3955 $chip.find('[data-role="state-' + state + '"]').text(count);
3956 } else if ($chip.length) {
3957 $chip.remove();
3958 }
3959 },
3960
3961 /**
3962 * Refresh the "All clear" / "N findings" pill for an info-only category.
3963 * Runs after a scan replaces the counts on the server side.
3964 */
3965 updateInfoStatus: function($cat, counts) {
3966 var $status = $cat.find('[data-role="info-status"]');
3967 if (!$status.length) {
3968 return;
3969 }
3970 var warnN = parseInt(counts.warn, 10) || 0;
3971 var failN = parseInt(counts.fail, 10) || 0;
3972 var issues = warnN + failN;
3973 var strings = (vigilanteAdmin && vigilanteAdmin.strings) || {};
3974
3975 if (issues > 0) {
3976 var template = strings.analyzerInfoFindings || '%d findings';
3977 var label = template.replace('%d', issues);
3978 $status
3979 .removeClass('vigilante-analyzer-category-status--clear')
3980 .addClass('vigilante-analyzer-category-status--attention')
3981 .html(
3982 '<span class="dashicons dashicons-warning" aria-hidden="true"></span>'
3983 + this.esc(label)
3984 );
3985 } else {
3986 var clear = strings.analyzerInfoAllClear || 'All clear';
3987 $status
3988 .removeClass('vigilante-analyzer-category-status--attention')
3989 .addClass('vigilante-analyzer-category-status--clear')
3990 .html(
3991 '<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>'
3992 + this.esc(clear)
3993 );
3994 }
3995 },
3996
3997 renderCheckRow: function(check) {
3998 var state = check.state || 'skip';
3999 var icons = {
4000 pass: 'yes-alt',
4001 warn: 'warning',
4002 fail: 'dismiss',
4003 info: 'info',
4004 skip: 'minus'
4005 };
4006 var icon = icons[state] || 'minus';
4007
4008 var scoreHtml = '';
4009 var max = parseInt(check.max, 10) || 0;
4010 if (max > 0 && state !== 'info' && state !== 'skip') {
4011 scoreHtml = '<span class="vigilante-analyzer-check-score">'
4012 + this.esc((parseInt(check.score, 10) || 0) + '/' + max)
4013 + '</span>';
4014 }
4015
4016 var fixHtml = '';
4017 if (check.fix_link && (state === 'fail' || state === 'warn')) {
4018 fixHtml = '<a href="' + this.escAttr(check.fix_link) + '" class="vigilante-analyzer-fix-link">'
4019 + this.esc(vigilanteAdmin.strings.analyzerGoToSetting || 'Go to setting')
4020 + '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
4021 } else if (check.fix_link && state === 'info') {
4022 var isExt = /^https?:\/\//i.test(check.fix_link);
4023 fixHtml = '<a href="' + this.escAttr(check.fix_link) + '" class="vigilante-analyzer-fix-link"'
4024 + (isExt ? ' target="_blank" rel="noopener noreferrer"' : '') + '>'
4025 + this.esc(vigilanteAdmin.strings.analyzerLearnMore || 'Learn more')
4026 + '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">&rarr;</span></a>';
4027 }
4028
4029 var detailHtml = '';
4030 if (check.detail) {
4031 detailHtml = '<p class="vigilante-analyzer-check-detail">' + this.esc(check.detail) + '</p>';
4032 }
4033
4034 return '<li class="vigilante-analyzer-check vigilante-analyzer-check--' + this.escAttr(state) + '"'
4035 + ' data-check-id="' + this.escAttr(check.id || '') + '">'
4036 + '<span class="vigilante-analyzer-check-icon dashicons dashicons-' + this.escAttr(icon) + '" aria-hidden="true"></span>'
4037 + '<div class="vigilante-analyzer-check-body">'
4038 + '<div class="vigilante-analyzer-check-label">'
4039 + '<span>' + this.esc(check.label || '') + '</span>'
4040 + scoreHtml
4041 + '</div>'
4042 + detailHtml
4043 + fixHtml
4044 + '</div>'
4045 + '</li>';
4046 },
4047
4048 /**
4049 * Expand or collapse the detailed breakdown section.
4050 */
4051 handleToggleDetails: function(e) {
4052 e.preventDefault();
4053 var $btn = $(e.currentTarget);
4054 var expanded = $btn.attr('aria-expanded') === 'true';
4055 this.expandDetails(!expanded);
4056 },
4057
4058 expandDetails: function(expand) {
4059 var $btn = this.$root.find('.vigilante-analyzer-toggle');
4060 var $details = this.$root.find('.vigilante-analyzer-details');
4061 var show = !!expand;
4062
4063 $btn.attr('aria-expanded', show ? 'true' : 'false');
4064
4065 var label = show
4066 ? (vigilanteAdmin.strings.analyzerHideDetails || 'Hide detailed breakdown')
4067 : (vigilanteAdmin.strings.analyzerShowDetails || 'Show detailed breakdown');
4068
4069 // Preserve the chevron element; only swap the label text.
4070 var $chevron = $btn.find('.vigilante-analyzer-toggle-chevron').detach();
4071 if (!$chevron.length) {
4072 $chevron = $('<span class="vigilante-analyzer-toggle-chevron" aria-hidden="true"></span>');
4073 }
4074 $btn.text(label).append(' ').append($chevron);
4075
4076 if (show) {
4077 $details.removeAttr('hidden');
4078 } else {
4079 $details.attr('hidden', 'hidden');
4080 }
4081 },
4082
4083 /**
4084 * Auto-save weekly scan + email toggles.
4085 */
4086 handleSettingsChange: function(e) {
4087 var $input = $(e.currentTarget);
4088 var $weekly = this.$root.find('input[name="security_analyzer[weekly_scan_enabled]"]');
4089 var $email = this.$root.find('input[name="security_analyzer[email_on_regression]"]');
4090
4091 $.ajax({
4092 url: vigilanteAdmin.ajaxUrl,
4093 type: 'POST',
4094 data: {
4095 action: 'vigilante_analyzer_save_settings',
4096 nonce: vigilanteAdmin.nonce,
4097 weekly_scan_enabled: $weekly.is(':checked') ? 1 : 0,
4098 email_on_regression: $email.is(':checked') ? 1 : 0
4099 },
4100 success: function(response) {
4101 if (response && response.success) {
4102 if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) {
4103 Vigilante_Admin.showNotice('success',
4104 vigilanteAdmin.strings.analyzerSettingsSaved || 'Analyzer settings saved.');
4105 }
4106 } else {
4107 if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) {
4108 Vigilante_Admin.showNotice('error',
4109 (response && response.data) || vigilanteAdmin.strings.error);
4110 }
4111 $input.prop('checked', !$input.is(':checked'));
4112 }
4113 },
4114 error: function() {
4115 if (typeof Vigilante_Admin !== 'undefined' && Vigilante_Admin.showNotice) {
4116 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
4117 }
4118 $input.prop('checked', !$input.is(':checked'));
4119 }
4120 });
4121 },
4122
4123 /**
4124 * Re-render the sparkline from data-points (no-op if no points).
4125 */
4126 renderSparklineFromData: function() {
4127 var $wrap = this.$root.find('[data-role="sparkline"]');
4128 if (!$wrap.length) {
4129 return;
4130 }
4131 var raw = $wrap.attr('data-points');
4132 if (!raw) {
4133 return;
4134 }
4135 try {
4136 var points = JSON.parse(raw);
4137 if (!points || points.length < 2) {
4138 return;
4139 }
4140 // Only regenerate if the SVG is missing (e.g. server gave empty markup).
4141 if (!$wrap.find('svg').length) {
4142 $wrap.html(this.buildSparklineSvg(points));
4143 }
4144 this.attachSparklineHotspots($wrap, points);
4145 } catch (err) { /* ignore parse errors */ }
4146 },
4147
4148 /**
4149 * Inject invisible hot-spots over each sparkline point and wire up a tooltip
4150 * that shows the score on hover/focus.
4151 */
4152 attachSparklineHotspots: function($wrap, points) {
4153 if (!points || points.length < 2) {
4154 return;
4155 }
4156 // Idempotent: clear any previous hot-spots/tooltip first.
4157 $wrap.find('.vigilante-analyzer-sparkline-dot, .vigilante-analyzer-sparkline-tooltip').remove();
4158
4159 var SVG_W = 280, SVG_H = 60, PAD = 4;
4160 var usableW = SVG_W - PAD * 2;
4161 var usableH = SVG_H - PAD * 2;
4162 var step = usableW / Math.max(1, points.length - 1);
4163 var max = 100;
4164
4165 var $tooltip = $('<div class="vigilante-analyzer-sparkline-tooltip" aria-hidden="true"></div>');
4166 $wrap.append($tooltip);
4167
4168 for (var i = 0; i < points.length; i++) {
4169 var x = PAD + (i * step);
4170 var y = PAD + (usableH - ((points[i] / max) * usableH));
4171 var xPct = (x / SVG_W) * 100;
4172 var yPct = (y / SVG_H) * 100;
4173
4174 var $dot = $('<button type="button" class="vigilante-analyzer-sparkline-dot"></button>')
4175 .css({ left: xPct + '%', top: yPct + '%' })
4176 .attr('data-score', points[i])
4177 .attr('data-index', i)
4178 .attr('aria-label', String(points[i]));
4179 $wrap.append($dot);
4180 }
4181
4182 $wrap.off('.vigSpark');
4183 $wrap.on('mouseenter.vigSpark focus.vigSpark', '.vigilante-analyzer-sparkline-dot', function() {
4184 var $dot = $(this);
4185 $tooltip
4186 .text($dot.attr('data-score'))
4187 .css({ left: $dot.css('left'), top: $dot.css('top') })
4188 .addClass('is-visible');
4189 });
4190 $wrap.on('mouseleave.vigSpark blur.vigSpark', '.vigilante-analyzer-sparkline-dot', function() {
4191 $tooltip.removeClass('is-visible');
4192 });
4193 },
4194
4195 buildSparklineSvg: function(points) {
4196 var width = 280;
4197 var height = 60;
4198 var padding = 4;
4199 var usableW = width - (padding * 2);
4200 var usableH = height - (padding * 2);
4201 var count = points.length;
4202 var step = usableW / Math.max(1, count - 1);
4203 var max = 100;
4204
4205 var coords = [];
4206 for (var i = 0; i < count; i++) {
4207 var x = padding + (i * step);
4208 var y = padding + (usableH - ((points[i] / max) * usableH));
4209 coords.push(x.toFixed(2) + ',' + y.toFixed(2));
4210 }
4211 var path = 'M ' + coords.join(' L ');
4212 var lastX = padding + ((count - 1) * step);
4213 var lastY = padding + (usableH - ((points[count - 1] / max) * usableH));
4214
4215 return '<svg viewBox="0 0 ' + width + ' ' + height + '" preserveAspectRatio="none" '
4216 + 'role="img" focusable="false">'
4217 + '<path d="' + path + '" fill="none" stroke="currentColor" '
4218 + 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
4219 + '<circle cx="' + lastX.toFixed(2) + '" cy="' + lastY.toFixed(2) + '" r="3" fill="currentColor"/>'
4220 + '</svg>';
4221 },
4222
4223 /**
4224 * When landing on a fix link (#field-*, #vigilante-section-* or any
4225 * other #vigilante-* target), scroll smoothly and flash-highlight it.
4226 *
4227 * The pattern used to name the two families literally, which left the
4228 * one anchor that follows neither out in the cold: the weekly report
4229 * email links to #vigilante-analyzer, so that link landed on the
4230 * dashboard with no scroll and no flash. Widened in 2.11.1 to the
4231 * whole vigilante- prefix, which is still narrow enough that a hash
4232 * written by anything else on the page cannot reach the selector.
4233 */
4234 hashFocusFlash: function() {
4235 var hash = window.location.hash;
4236 if (!hash || hash.length < 2) {
4237 return;
4238 }
4239 // Accept field anchors and every anchor the plugin owns.
4240 if (!/^#(?:field-|vigilante-)/.test(hash)) {
4241 return;
4242 }
4243
4244 setTimeout(function() {
4245 var $target;
4246 try {
4247 $target = $(hash);
4248 } catch (err) { return; }
4249 if (!$target || !$target.length) {
4250 return;
4251 }
4252
4253 // Scroll to it smoothly.
4254 if ('scrollIntoView' in $target[0]) {
4255 $target[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
4256 }
4257
4258 // Flash — remove any previous flash first.
4259 $('.vigilante-focus-flash').removeClass('vigilante-focus-flash');
4260 $target.addClass('vigilante-focus-flash');
4261 setTimeout(function() {
4262 $target.removeClass('vigilante-focus-flash');
4263 }, 1800);
4264 }, 150);
4265 },
4266
4267 /** Escape text for HTML. */
4268 esc: function(str) {
4269 return String(str == null ? '' : str)
4270 .replace(/&/g, '&amp;')
4271 .replace(/</g, '&lt;')
4272 .replace(/>/g, '&gt;')
4273 .replace(/"/g, '&quot;')
4274 .replace(/'/g, '&#039;');
4275 },
4276
4277 /** Escape text for HTML attribute. */
4278 escAttr: function(str) {
4279 return this.esc(str);
4280 }
4281 };
4282
4283 // Initialize on document ready
4284 $(document).ready(function() {
4285 Vigilante_Admin.init();
4286 Vigilante_Admin.initPasswordReset();
4287 Vigilante_Admin.initUserApproval();
4288 Vigilante_Admin.initSessionManagement();
4289 VigilanteAnalyzer.init();
4290
4291 // Shared "Send test email" button (Notification settings, File Integrity, Audit Alerts).
4292 $(document).on('click', '.vigilante-test-email-btn', function(e) {
4293 e.preventDefault();
4294 var $btn = $(this);
4295 var $result = $btn.siblings('.vigilante-test-email-result');
4296 var original = $btn.data('original-text') || $btn.text();
4297 $btn.prop('disabled', true).text(vigilanteAdmin.strings.sendingTest || 'Sending...');
4298 $result.text('').css('color', '');
4299 $.ajax({
4300 url: vigilanteAdmin.ajaxUrl,
4301 type: 'POST',
4302 data: {
4303 action: 'vigilante_send_test_email',
4304 nonce: vigilanteAdmin.nonce
4305 },
4306 success: function(response) {
4307 if (response && response.success) {
4308 $result.css('color', '#00a32a').text((response.data && response.data.message) || '');
4309 } else {
4310 $result.css('color', '#d63638').text((response && response.data && response.data.message) || vigilanteAdmin.strings.error);
4311 }
4312 },
4313 error: function() {
4314 $result.css('color', '#d63638').text(vigilanteAdmin.strings.error);
4315 },
4316 complete: function() {
4317 $btn.prop('disabled', false).text(original);
4318 }
4319 });
4320 });
4321 });
4322
4323 /**
4324 * Security Headers settings recovery (2.10.0).
4325 *
4326 * Both buttons are disabled together while the request is in flight, so a
4327 * second click cannot fire a second restore. The page reloads afterwards
4328 * because the whole tab is rendered from the settings that just changed.
4329 */
4330 $(function() {
4331 var SELECTOR = '#vigilante-recovery-restore, #vigilante-recovery-dismiss';
4332
4333 function recoveryRun(action) {
4334 $(SELECTOR).prop('disabled', true);
4335
4336 $.ajax({
4337 url: vigilanteAdmin.ajaxUrl,
4338 type: 'POST',
4339 data: {
4340 action: action,
4341 nonce: vigilanteAdmin.nonce
4342 },
4343 success: function(response) {
4344 if (response.success) {
4345 Vigilante_Admin.showNotice('success', response.data);
4346 setTimeout(function() {
4347 location.reload();
4348 }, 1200);
4349 return;
4350 }
4351
4352 Vigilante_Admin.showNotice('error', response.data);
4353 $(SELECTOR).prop('disabled', false);
4354 },
4355 error: function() {
4356 Vigilante_Admin.showNotice('error', vigilanteAdmin.strings.error);
4357 $(SELECTOR).prop('disabled', false);
4358 }
4359 });
4360 }
4361
4362 $(document).on('click', '#vigilante-recovery-restore', function(e) {
4363 e.preventDefault();
4364 recoveryRun('vigilante_headers_recovery_restore');
4365 });
4366
4367 $(document).on('click', '#vigilante-recovery-dismiss', function(e) {
4368 e.preventDefault();
4369 recoveryRun('vigilante_headers_recovery_dismiss');
4370 });
4371
4372 $(document).on('click', '#vigilante-recovery-undo', function(e) {
4373 e.preventDefault();
4374 $(this).prop('disabled', true);
4375 recoveryRun('vigilante_headers_recovery_undo');
4376 });
4377 });
4378
4379 })(jQuery);