PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.10.2
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.10.2
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 / admin / class-admin-ajax.php

class-admin-ajax.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.10.2, at admin/class-admin-ajax.php

1,502 lines 53.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin AJAX Trait
4 *
5 * AJAX handlers and helper methods for Vigilante_Admin class
6 *
7 * @package Vigilante
8 */
9
10 // Prevent direct access
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Trait for AJAX handlers
17 * To be used in Vigilante_Admin class
18 */
19 trait Vigilante_Admin_Ajax {
20
21 /**
22 * AJAX: Apply preset
23 */
24 // ajax_apply_preset() is defined in class-admin.php directly (not in this trait)
25
26 /**
27 * AJAX: Clear lockouts
28 */
29 public function ajax_clear_lockouts() {
30 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
31
32 if ( ! current_user_can( 'manage_options' ) ) {
33 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
34 }
35
36 $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
37
38 if ( ! empty( $ip ) ) {
39 // Clear specific IP
40 $result = $this->database->clear_lockout( $ip );
41 } else {
42 // Clear all
43 $result = $this->database->clear_all_lockouts();
44 }
45
46 if ( $result ) {
47 wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) );
48 } else {
49 wp_send_json_error( __( 'Failed to clear lockouts.', 'vigilante' ) );
50 }
51 }
52
53 /**
54 * AJAX: Clear logs
55 */
56 public function ajax_clear_logs() {
57 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
58
59 if ( ! current_user_can( 'manage_options' ) ) {
60 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
61 }
62
63 $result = $this->activity_log->clear_all_logs();
64
65 if ( $result ) {
66 wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) );
67 } else {
68 wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) );
69 }
70 }
71
72 /**
73 * AJAX: Run file integrity scan
74 */
75 public function ajax_run_scan() {
76 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
77
78 if ( ! current_user_can( 'manage_options' ) ) {
79 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
80 }
81
82 try {
83 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
84 require_once VIGILANTE_PLUGIN_DIR . 'includes/class-file-integrity.php';
85 }
86
87 if ( ! $this->settings ) {
88 wp_send_json_error( 'Settings not initialized' );
89 }
90
91 $activity_log = isset( $this->activity_log ) ? $this->activity_log : null;
92 $database = isset( $this->database ) ? $this->database : null;
93
94 $file_integrity = new Vigilante_File_Integrity( $this->settings, $database, $activity_log );
95 $results = $file_integrity->run_scan();
96
97 // Save results for display
98 update_option( 'vigilante_last_integrity_scan', time() );
99 update_option( 'vigilante_last_integrity_results', $results );
100
101 wp_send_json_success( array(
102 'message' => __( 'Scan completed.', 'vigilante' ),
103 'results' => $results,
104 ) );
105 } catch ( Exception $e ) {
106 wp_send_json_error( 'Exception: ' . $e->getMessage() );
107 } catch ( Error $e ) {
108 wp_send_json_error( 'PHP Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
109 }
110 }
111
112 /**
113 * AJAX: Approve a critical config file modification
114 *
115 * Updates the baseline hash for a single critical file (wp-config.php
116 * or .htaccess), accepting the current content as legitimate.
117 */
118 public function ajax_approve_critical_file() {
119 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
120
121 if ( ! current_user_can( 'manage_options' ) ) {
122 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
123 }
124
125 // The request carries an opaque key instead of the file name: hosting
126 // WAFs (e.g. ModSecurity with OWASP CRS rule 930130) reject any POST
127 // whose arguments contain the literal "wp-config.php", which made
128 // this Approve button fail with a generic AJAX error behind such
129 // firewalls. The server-side map below is the real security gate.
130 $file_keys = array(
131 'cfg' => 'wp-config.php',
132 'hta' => '.htaccess',
133 );
134 $file_key = isset( $_POST['file_key'] ) ? sanitize_key( $_POST['file_key'] ) : '';
135 $file = isset( $file_keys[ $file_key ] ) ? $file_keys[ $file_key ] : '';
136 $allowed = array( 'wp-config.php', '.htaccess' );
137 if ( ! in_array( $file, $allowed, true ) ) {
138 wp_send_json_error( __( 'Invalid file.', 'vigilante' ) );
139 }
140
141 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
142 require_once VIGILANTE_PLUGIN_DIR . 'includes/class-file-integrity.php';
143 }
144
145 $activity_log = isset( $this->activity_log ) ? $this->activity_log : null;
146 $database = isset( $this->database ) ? $this->database : null;
147
148 $fi = new Vigilante_File_Integrity( $this->settings, $database, $activity_log );
149 $result = $fi->update_critical_file_baseline( $file );
150
151 if ( $result ) {
152 // Log the approval in the activity log
153 if ( $activity_log ) {
154 $activity_log->log(
155 'file',
156 'critical_file_approved',
157 sprintf(
158 /* translators: %s: file name */
159 __( 'Critical config file modification approved: %s', 'vigilante' ),
160 $file
161 ),
162 array( 'file' => $file ),
163 'info'
164 );
165 }
166
167 // Update stored scan results to remove the approved file
168 $last_results = get_option( 'vigilante_last_integrity_results', array() );
169 if ( ! empty( $last_results['modified'] ) ) {
170 $last_results['modified'] = array_values(
171 array_filter(
172 $last_results['modified'],
173 function ( $item ) use ( $file ) {
174 return ! ( is_array( $item ) && isset( $item['file'] ) && $item['file'] === $file );
175 }
176 )
177 );
178 update_option( 'vigilante_last_integrity_results', $last_results );
179 }
180
181 wp_send_json_success( array(
182 'message' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ),
183 'file' => $file,
184 ) );
185 } else {
186 wp_send_json_error( __( 'Failed to update baseline.', 'vigilante' ) );
187 }
188 }
189
190 /**
191 * AJAX: Get activity logs
192 */
193 public function ajax_get_logs() {
194 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
195
196 if ( ! current_user_can( 'manage_options' ) ) {
197 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
198 }
199
200 $per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 50;
201 // Limit max to prevent memory issues
202 $per_page = min( $per_page, 10000 );
203
204 $args = array(
205 'per_page' => $per_page,
206 'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
207 );
208
209 if ( ! empty( $_POST['type'] ) ) {
210 $args['event_type'] = sanitize_key( $_POST['type'] );
211 }
212
213 if ( ! empty( $_POST['severity'] ) ) {
214 $args['severity'] = sanitize_key( $_POST['severity'] );
215 }
216
217 if ( ! empty( $_POST['request_method'] ) ) {
218 $args['request_method'] = sanitize_text_field( wp_unslash( $_POST['request_method'] ) );
219 }
220
221 if ( ! empty( $_POST['search'] ) ) {
222 $args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
223 }
224
225 if ( ! $this->activity_log ) {
226 wp_send_json_error( 'Activity log not initialized' );
227 }
228
229 $logs = $this->activity_log->get_logs( $args );
230 $total = $this->activity_log->get_logs_count( $args );
231
232 // Attach firewall list flags so the popup can show "In whitelist"/"In blacklist"
233 // states when users paginate or filter without reloading the page.
234 $firewall_options = $this->settings->get_section( 'firewall' );
235 $ip_whitelist = $firewall_options['ip_whitelist'] ?? array();
236 $ip_blacklist = $firewall_options['ip_blacklist'] ?? array();
237 $ua_whitelist = $firewall_options['ua_whitelist'] ?? array();
238 $ua_blacklist = $firewall_options['ua_blacklist'] ?? array();
239
240 foreach ( $logs as $log ) {
241 $ip_val = (string) ( $log->ip_address ?? '' );
242 $ua_val = (string) ( $log->user_agent ?? '' );
243 $log->is_ip_whitelisted = ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) );
244 $log->is_ip_blacklisted = ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) );
245 $log->is_ua_whitelisted = ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) );
246 $log->is_ua_blacklisted = ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) );
247 $log->request_uri = Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' );
248 }
249
250 wp_send_json_success( array(
251 'logs' => $logs,
252 'total' => $total,
253 ) );
254 }
255
256 /**
257 * AJAX: Add IP or User-Agent to firewall whitelist/blacklist
258 */
259 public function ajax_add_to_firewall_list() {
260 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
261
262 if ( ! current_user_can( 'manage_options' ) ) {
263 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
264 }
265
266 $value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( $_POST['value'] ) ) : '';
267 $list_type = isset( $_POST['list_type'] ) ? sanitize_key( $_POST['list_type'] ) : '';
268 $item_type = isset( $_POST['item_type'] ) ? sanitize_key( $_POST['item_type'] ) : '';
269
270 if ( empty( $value ) || empty( $list_type ) || empty( $item_type ) ) {
271 wp_send_json_error( __( 'Missing parameters.', 'vigilante' ) );
272 }
273
274 // Validate list_type and item_type
275 $valid_lists = array( 'whitelist', 'blacklist' );
276 $valid_items = array( 'ip', 'ua' );
277
278 if ( ! in_array( $list_type, $valid_lists, true ) || ! in_array( $item_type, $valid_items, true ) ) {
279 wp_send_json_error( __( 'Invalid parameters.', 'vigilante' ) );
280 }
281
282 // Validate IP if item_type is ip
283 if ( 'ip' === $item_type && ! filter_var( $value, FILTER_VALIDATE_IP ) ) {
284 wp_send_json_error( __( 'Invalid IP address.', 'vigilante' ) );
285 }
286
287 $option_key = $item_type . '_' . $list_type; // ip_whitelist, ip_blacklist, ua_whitelist, ua_blacklist
288 $options = $this->settings->get_section( 'firewall' );
289 $list = isset( $options[ $option_key ] ) ? (array) $options[ $option_key ] : array();
290
291 // Check if already in list
292 if ( in_array( $value, $list, true ) ) {
293 wp_send_json_error(
294 sprintf(
295 /* translators: %s: the value being added */
296 __( '%s is already in this list.', 'vigilante' ),
297 $value
298 )
299 );
300 }
301
302 // Add to list
303 $list[] = $value;
304
305 // Check opposite list and remove if present
306 $opposite_type = ( 'whitelist' === $list_type ) ? 'blacklist' : 'whitelist';
307 $opposite_key = $item_type . '_' . $opposite_type;
308 $removed_from_opposite = false;
309
310 // Save
311 $all_options = get_option( Vigilante_Settings::OPTION_NAME, array() );
312 if ( ! isset( $all_options['firewall'] ) ) {
313 $all_options['firewall'] = array();
314 }
315 $all_options['firewall'][ $option_key ] = $list;
316
317 // Remove from opposite list if found
318 if ( ! empty( $all_options['firewall'][ $opposite_key ] ) && is_array( $all_options['firewall'][ $opposite_key ] ) ) {
319 $opposite_list = $all_options['firewall'][ $opposite_key ];
320 $filtered = array_values( array_filter( $opposite_list, function( $item ) use ( $value ) {
321 return $item !== $value;
322 } ) );
323
324 if ( count( $filtered ) < count( $opposite_list ) ) {
325 $all_options['firewall'][ $opposite_key ] = $filtered;
326 $removed_from_opposite = true;
327 }
328 }
329
330 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
331 update_option( Vigilante_Settings::OPTION_NAME, $all_options );
332 $this->settings->clear_cache();
333
334 // Whitelist entries feed the .htaccess exception conditions (Server
335 // Protection), so the block must be rewritten with the updated list.
336 // Saving from the Firewall tab does this via apply_section_changes();
337 // this handler writes the option directly, so it regenerates here.
338 if ( 'whitelist' === $list_type ) {
339 $fresh_settings = new Vigilante_Settings();
340 $sh = $fresh_settings->get_section( 'security_headers' );
341
342 $needs_htaccess_block = ! empty( $all_options['modules']['firewall'] )
343 || ! empty( $sh['hide_server_signature'] )
344 || ! empty( $sh['remove_fingerprinting_headers'] );
345
346 if ( $needs_htaccess_block ) {
347 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
348 $htaccess = new Vigilante_Htaccess_Protection( $fresh_settings );
349 $htaccess->apply_rules();
350 }
351 }
352
353 $list_label = ( 'whitelist' === $list_type )
354 ? __( 'whitelist', 'vigilante' )
355 : __( 'blacklist', 'vigilante' );
356
357 $message = sprintf(
358 /* translators: 1: the value added, 2: list name */
359 __( '%1$s added to %2$s.', 'vigilante' ),
360 $value,
361 $list_label
362 );
363
364 if ( $removed_from_opposite ) {
365 $opposite_label = ( 'whitelist' === $opposite_type )
366 ? __( 'whitelist', 'vigilante' )
367 : __( 'blacklist', 'vigilante' );
368
369 $message .= ' ' . sprintf(
370 /* translators: %s: opposite list name */
371 __( 'Automatically removed from %s.', 'vigilante' ),
372 $opposite_label
373 );
374 }
375
376 wp_send_json_success( $message );
377 }
378
379 /**
380 * AJAX: Test security headers
381 */
382 public function ajax_test_headers() {
383 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
384
385 if ( ! current_user_can( 'manage_options' ) ) {
386 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
387 }
388
389 $security_headers = new Vigilante_Security_Headers( $this->settings );
390 $results = $security_headers->test_headers();
391
392 wp_send_json_success( $results );
393 }
394
395 /**
396 * Sanitize activity log data
397 *
398 * @param array $data Data to sanitize.
399 * @return array
400 */
401 /**
402 * Sanitize IP list
403 *
404 * @param string|array $ips IPs as string (newline separated) or array.
405 * @return array
406 */
407 private function sanitize_ip_list( $ips ) {
408 if ( is_string( $ips ) ) {
409 $ips = array_filter( array_map( 'trim', explode( "\n", $ips ) ) );
410 }
411
412 $sanitized = array();
413
414 foreach ( (array) $ips as $ip ) {
415 $ip = trim( $ip );
416 // Validate IP or CIDR
417 if ( filter_var( $ip, FILTER_VALIDATE_IP ) || preg_match( '/^[\d\.]+\/\d{1,2}$/', $ip ) ) {
418 $sanitized[] = $ip;
419 }
420 }
421
422 return $sanitized;
423 }
424
425 /**
426 * Sanitize User-Agent list
427 *
428 * @param string|array $uas User-Agent strings (newline-separated or array).
429 * @return array
430 */
431 private function sanitize_ua_list( $uas ) {
432 if ( is_string( $uas ) ) {
433 $uas = array_filter( array_map( 'trim', explode( "\n", $uas ) ) );
434 }
435
436 $sanitized = array();
437
438 foreach ( (array) $uas as $ua ) {
439 $ua = sanitize_text_field( trim( $ua ) );
440 if ( ! empty( $ua ) ) {
441 $sanitized[] = $ua;
442 }
443 }
444
445 return array_unique( $sanitized );
446 }
447
448 /**
449 * AJAX: Search users for 2FA exclusion
450 */
451 public function ajax_search_users_2fa() {
452 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
453
454 if ( ! current_user_can( 'manage_options' ) ) {
455 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
456 }
457
458 $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
459 $exclude = isset( $_POST['exclude'] ) ? array_map( 'absint', (array) $_POST['exclude'] ) : array();
460
461 if ( strlen( $query ) < 2 ) {
462 wp_send_json_error( __( 'Query too short.', 'vigilante' ) );
463 }
464
465 // Search users by login, email, or display name
466 $users = get_users( array(
467 'search' => '*' . $query . '*',
468 'search_columns' => array( 'user_login', 'user_email', 'display_name' ),
469 'exclude' => $exclude,
470 'number' => 10,
471 'orderby' => 'display_name',
472 'order' => 'ASC',
473 ) );
474
475 $results = array();
476
477 foreach ( $users as $user ) {
478 $results[] = array(
479 'ID' => $user->ID,
480 'user_login' => $user->user_login,
481 'user_email' => $user->user_email,
482 'display_name' => $user->display_name,
483 'avatar' => get_avatar_url( $user->ID, array( 'size' => 32 ) ),
484 );
485 }
486
487 wp_send_json_success( $results );
488 }
489
490 /**
491 * AJAX: Send 2FA activation notification
492 */
493 public function ajax_send_2fa_notification() {
494 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
495
496 if ( ! current_user_can( 'manage_options' ) ) {
497 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
498 }
499
500 $mode = isset( $_POST['mode'] ) ? sanitize_key( $_POST['mode'] ) : 'all';
501 $only_new = 'new' === $mode;
502
503 // Read settings to determine active 2FA method
504 $login_security = $this->settings->get_section( 'login_security' );
505 $two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array();
506 $method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email';
507
508 if ( 'totp' === $method ) {
509 // TOTP method: use TOTP class for styled activation emails
510 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
511 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
512 }
513
514 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
515 $roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' );
516 $excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array();
517 $site_name = get_bloginfo( 'name' );
518 $from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name;
519
520 if ( empty( $roles ) ) {
521 $roles = array( 'administrator' );
522 }
523
524 $args = array( 'role__in' => $roles );
525 if ( ! empty( $excluded ) ) {
526 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings.
527 $args['exclude'] = $excluded;
528 }
529 $users = get_users( $args );
530
531 $sent = 0;
532 $skipped = 0;
533 $failed = 0;
534
535 foreach ( $users as $user ) {
536 // Skip users who already have TOTP configured (unless sending to all)
537 if ( $only_new ) {
538 $totp_data = $this->database->get_totp_data( $user->ID );
539 if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) {
540 $skipped++;
541 continue;
542 }
543 if ( $this->database->user_was_2fa_notified( $user->ID ) ) {
544 $skipped++;
545 continue;
546 }
547 }
548
549 $email_sent = $totp->send_activation_email( $user, $site_name, $from_name );
550
551 if ( $email_sent ) {
552 $this->database->mark_2fa_notified( $user->ID );
553 $sent++;
554 } else {
555 $failed++;
556 }
557 }
558
559 wp_send_json_success( array(
560 'sent' => $sent,
561 'skipped' => $skipped,
562 'failed' => $failed,
563 ) );
564 } else {
565 // Email method: use email 2FA class
566 if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) {
567 require_once VIGILANTE_PLUGIN_DIR . 'includes/class-two-factor-email.php';
568 }
569
570 $two_factor_email = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log );
571 $result = $two_factor_email->send_activation_notifications( $only_new );
572
573 wp_send_json_success( $result );
574 }
575 }
576
577 /**
578 * AJAX: Search users with TOTP configured (for admin reset)
579 */
580 public function ajax_search_totp_users() {
581 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
582
583 if ( ! current_user_can( 'manage_options' ) ) {
584 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
585 }
586
587 $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
588
589 if ( strlen( $query ) < 2 ) {
590 wp_send_json_error( __( 'Query too short.', 'vigilante' ) );
591 }
592
593 $results = $this->database->search_totp_users( $query, 10 );
594
595 $users = array();
596 foreach ( $results as $row ) {
597 $avatar = get_avatar_url( $row['user_id'], array( 'size' => 32 ) );
598 $users[] = array(
599 'ID' => absint( $row['user_id'] ),
600 'display_name' => $row['display_name'],
601 'user_email' => $row['user_email'],
602 'configured_at' => $row['configured_at'],
603 'last_used_at' => $row['last_used_at'],
604 'avatar' => $avatar,
605 );
606 }
607
608 wp_send_json_success( $users );
609 }
610
611 /**
612 * AJAX: Reset TOTP for selected users (admin action)
613 */
614 public function ajax_reset_totp_users() {
615 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
616
617 if ( ! current_user_can( 'manage_options' ) ) {
618 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
619 }
620
621 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with array_map
622 $user_ids = isset( $_POST['user_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['user_ids'] ) ) : array();
623
624 if ( empty( $user_ids ) ) {
625 wp_send_json_error( __( 'No users selected.', 'vigilante' ) );
626 }
627
628 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
629 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
630 }
631
632 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
633 $count = 0;
634
635 foreach ( $user_ids as $uid ) {
636 if ( $uid > 0 ) {
637 $totp->reset_user_totp( $uid );
638 $count++;
639 }
640 }
641
642 wp_send_json_success( array(
643 /* translators: %d: Number of users reset */
644 'message' => sprintf( _n( 'TOTP reset for %d user.', 'TOTP reset for %d users.', $count, 'vigilante' ), $count ),
645 'count' => $count,
646 ) );
647 }
648
649 /**
650 * AJAX: Get TOTP setup data (secret + QR) for user profile
651 */
652 public function ajax_totp_get_setup() {
653 check_ajax_referer( 'vigilante_totp_profile', 'nonce' );
654
655 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
656
657 // Fallback to current user if user_id is 0
658 if ( 0 === $user_id ) {
659 $user_id = get_current_user_id();
660 }
661
662 if ( 0 === $user_id ) {
663 wp_send_json_error( __( 'Invalid user.', 'vigilante' ) );
664 }
665
666 // Permission check: own profile or admin
667 if ( get_current_user_id() !== $user_id && ! current_user_can( 'manage_options' ) ) {
668 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
669 }
670
671 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
672 wp_send_json_error( __( 'TOTP module not available.', 'vigilante' ) );
673 }
674
675 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
676 $data = $totp->get_setup_data( $user_id );
677
678 if ( empty( $data ) ) {
679 wp_send_json_error( __( 'Could not generate setup data. User not found.', 'vigilante' ) );
680 }
681
682 wp_send_json_success( $data );
683 }
684
685 /**
686 * AJAX: Send login URL notification to users with admin access
687 */
688 public function ajax_notify_login_url() {
689 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
690
691 if ( ! current_user_can( 'manage_options' ) ) {
692 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
693 }
694
695 $login_options = $this->settings->get_section( 'login_security' );
696 $custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : '';
697
698 if ( empty( $custom_url ) ) {
699 wp_send_json_error( __( 'No custom login URL configured.', 'vigilante' ) );
700 }
701
702 $login_url = home_url( $custom_url . '/' );
703 $site_name = get_bloginfo( 'name' );
704
705 // Roles that can access wp-admin
706 $admin_roles = array( 'administrator', 'editor', 'author', 'contributor' );
707
708 $users = get_users( array(
709 'role__in' => $admin_roles,
710 ) );
711
712 if ( empty( $users ) ) {
713 wp_send_json_error( __( 'No users found.', 'vigilante' ) );
714 }
715
716 $subject = sprintf(
717 /* translators: %s: Site name */
718 __( '[%s] Your login URL has changed', 'vigilante' ),
719 $site_name
720 );
721
722 // Build email body using template
723 $body = Vigilante_Email_Template::p( __( 'The login URL for the admin area has been changed. Please save the new URL below and use it from now on.', 'vigilante' ) );
724 $body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) );
725 $body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) );
726 $body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) );
727
728 $sent = 0;
729 $failed = 0;
730
731 foreach ( $users as $user ) {
732 $result = Vigilante_Email_Template::send(
733 $user->user_email,
734 $subject,
735 __( 'Login URL changed', 'vigilante' ),
736 $body
737 );
738 if ( $result ) {
739 $sent++;
740 } else {
741 $failed++;
742 }
743 }
744
745 if ( $this->activity_log ) {
746 $this->activity_log->log(
747 'login',
748 'login_url_notified',
749 sprintf(
750 /* translators: 1: Sent count, 2: Failed count */
751 __( 'Login URL notification sent: %1$d sent, %2$d failed', 'vigilante' ),
752 $sent,
753 $failed
754 )
755 );
756 }
757
758 wp_send_json_success( array(
759 'sent' => $sent,
760 'failed' => $failed,
761 ) );
762 }
763
764 /**
765 * Sanitize 2FA data within login security
766 *
767 * @param array $two_factor 2FA data to sanitize.
768 * @return array
769 */
770 private function sanitize_two_factor_data( $two_factor ) {
771 $valid_methods = array( 'email', 'totp' );
772 $method = isset( $two_factor['method'] ) ? sanitize_key( $two_factor['method'] ) : 'email';
773
774 return array(
775 'enabled' => ! empty( $two_factor['enabled'] ),
776 'method' => in_array( $method, $valid_methods, true ) ? $method : 'email',
777 'enforced_roles' => isset( $two_factor['enforced_roles'] )
778 ? array_map( 'sanitize_key', (array) $two_factor['enforced_roles'] )
779 : array( 'administrator', 'editor' ),
780 'excluded_users' => isset( $two_factor['excluded_users'] )
781 ? array_map( 'absint', (array) $two_factor['excluded_users'] )
782 : array(),
783 'remember_device_days' => isset( $two_factor['remember_device_days'] )
784 ? absint( $two_factor['remember_device_days'] )
785 : 30,
786 'code_expiry_minutes' => isset( $two_factor['code_expiry_minutes'] )
787 ? absint( $two_factor['code_expiry_minutes'] )
788 : 10,
789 'max_attempts' => isset( $two_factor['max_attempts'] )
790 ? absint( $two_factor['max_attempts'] )
791 : 3,
792 'email_from_name' => isset( $two_factor['email_from_name'] )
793 ? sanitize_text_field( $two_factor['email_from_name'] )
794 : '',
795 'notify_on_enable' => ! empty( $two_factor['notify_on_enable'] ),
796 'grace_period_days' => isset( $two_factor['grace_period_days'] )
797 ? min( 30, absint( $two_factor['grace_period_days'] ) )
798 : 3,
799 );
800 }
801
802 /**
803 * AJAX: Search users for password reset
804 */
805 public function ajax_search_users_password_reset() {
806 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
807
808 if ( ! current_user_can( 'manage_options' ) ) {
809 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
810 }
811
812 $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
813
814 if ( strlen( $query ) < 2 ) {
815 wp_send_json_error( __( 'Query too short.', 'vigilante' ) );
816 }
817
818 // Search users by login, email, or display name
819 $users = get_users( array(
820 'search' => '*' . $query . '*',
821 'search_columns' => array( 'user_login', 'user_email', 'display_name' ),
822 'number' => 10,
823 'orderby' => 'display_name',
824 'order' => 'ASC',
825 ) );
826
827 $results = array();
828
829 foreach ( $users as $user ) {
830 $results[] = array(
831 'ID' => $user->ID,
832 'user_login' => $user->user_login,
833 'user_email' => $user->user_email,
834 'display_name' => $user->display_name,
835 'avatar' => get_avatar_url( $user->ID, array( 'size' => 32 ) ),
836 'roles' => implode( ', ', $user->roles ),
837 );
838 }
839
840 wp_send_json_success( $results );
841 }
842
843 /**
844 * AJAX: Force password reset for specific users
845 * Uses native WordPress password reset flow
846 */
847 public function ajax_force_password_reset() {
848 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
849
850 if ( ! current_user_can( 'manage_options' ) ) {
851 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
852 }
853
854 $user_ids = isset( $_POST['user_ids'] ) ? array_map( 'absint', (array) $_POST['user_ids'] ) : array();
855 $current_user_id = get_current_user_id();
856
857 if ( empty( $user_ids ) ) {
858 wp_send_json_error( __( 'No users selected.', 'vigilante' ) );
859 }
860
861 // Check if current user is resetting themselves
862 $resetting_self = in_array( $current_user_id, $user_ids, true );
863
864 // Create user security instance to use native reset
865 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
866
867 // Perform bulk reset
868 $results = $user_security->force_password_reset_bulk( $user_ids, $current_user_id );
869
870 $message = sprintf(
871 /* translators: %d: Number of users */
872 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
873 $results['success']
874 );
875
876 if ( $results['failed'] > 0 ) {
877 $message .= ' ' . sprintf(
878 /* translators: %d: Number of failures */
879 __( '%d failed.', 'vigilante' ),
880 $results['failed']
881 );
882 }
883
884 wp_send_json_success( array(
885 'message' => $message,
886 'results' => $results,
887 'resetting_self' => $resetting_self,
888 ) );
889 }
890
891 /**
892 * AJAX: Force password reset for all users
893 * Uses native WordPress password reset flow
894 */
895 public function ajax_force_password_reset_all() {
896 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
897
898 if ( ! current_user_can( 'manage_options' ) ) {
899 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
900 }
901
902 $include_self = ! empty( $_POST['include_self'] );
903 $current_user_id = get_current_user_id();
904
905 // Create user security instance to use native reset
906 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
907
908 // Perform reset for all users
909 $results = $user_security->force_password_reset_all( $current_user_id, ! $include_self );
910
911 // Log the bulk action
912 if ( $this->activity_log ) {
913 $reset_by_user = get_userdata( $current_user_id );
914 $this->activity_log->log(
915 'user',
916 'force_password_reset_all',
917 sprintf(
918 /* translators: 1: Number of users, 2: Admin username */
919 __( 'Password reset forced for %1$d users by %2$s', 'vigilante' ),
920 $results['success'],
921 $reset_by_user ? $reset_by_user->user_login : __( 'System', 'vigilante' )
922 ),
923 array(
924 'count' => $results['success'],
925 'reset_by' => $current_user_id,
926 'include_self' => $include_self,
927 ),
928 'warning'
929 );
930 }
931
932 $message = sprintf(
933 /* translators: %d: Number of users */
934 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
935 $results['success']
936 );
937
938 if ( $results['failed'] > 0 ) {
939 $message .= ' ' . sprintf(
940 /* translators: %d: Number of failures */
941 __( '%d failed.', 'vigilante' ),
942 $results['failed']
943 );
944 }
945
946 wp_send_json_success( array(
947 'message' => $message,
948 'results' => $results,
949 'resetting_self' => $include_self,
950 ) );
951 }
952
953 /**
954 * AJAX: Force password reset by role
955 * Resets passwords for all users with the selected roles
956 */
957 public function ajax_force_password_reset_by_role() {
958 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
959
960 if ( ! current_user_can( 'manage_options' ) ) {
961 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
962 }
963
964 $roles = isset( $_POST['roles'] ) ? array_map( 'sanitize_key', (array) $_POST['roles'] ) : array();
965
966 if ( empty( $roles ) ) {
967 wp_send_json_error( __( 'No roles selected.', 'vigilante' ) );
968 }
969
970 // Validate that submitted roles actually exist.
971 $wp_roles = wp_roles();
972 foreach ( $roles as $role ) {
973 if ( ! isset( $wp_roles->roles[ $role ] ) ) {
974 wp_send_json_error(
975 sprintf(
976 /* translators: %s: Role slug */
977 __( 'Invalid role: %s', 'vigilante' ),
978 $role
979 )
980 );
981 }
982 }
983
984 $include_self = ! empty( $_POST['include_self'] );
985 $current_user_id = get_current_user_id();
986
987 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
988
989 $results = $user_security->force_password_reset_by_roles(
990 $roles,
991 $current_user_id,
992 ! $include_self
993 );
994
995 // Log the action.
996 if ( $this->activity_log ) {
997 $reset_by_user = get_userdata( $current_user_id );
998 $role_names = array();
999
1000 foreach ( $roles as $role ) {
1001 $role_names[] = isset( $wp_roles->roles[ $role ] )
1002 ? translate_user_role( $wp_roles->roles[ $role ]['name'] )
1003 : $role;
1004 }
1005
1006 $this->activity_log->log(
1007 'user',
1008 'force_password_reset_by_role',
1009 sprintf(
1010 /* translators: 1: Number of users, 2: Role names, 3: Admin username */
1011 __( 'Password reset forced for %1$d users (roles: %2$s) by %3$s', 'vigilante' ),
1012 $results['success'],
1013 implode( ', ', $role_names ),
1014 $reset_by_user ? $reset_by_user->user_login : __( 'System', 'vigilante' )
1015 ),
1016 array(
1017 'count' => $results['success'],
1018 'roles' => $roles,
1019 'reset_by' => $current_user_id,
1020 'include_self' => $include_self,
1021 ),
1022 'warning'
1023 );
1024 }
1025
1026 $message = sprintf(
1027 /* translators: %d: Number of users */
1028 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
1029 $results['success']
1030 );
1031
1032 if ( $results['failed'] > 0 ) {
1033 $message .= ' ' . sprintf(
1034 /* translators: %d: Number of failures */
1035 __( '%d failed.', 'vigilante' ),
1036 $results['failed']
1037 );
1038 }
1039
1040 // Check if current user was included via role membership.
1041 $resetting_self = false;
1042 if ( $include_self ) {
1043 $current_user = wp_get_current_user();
1044 $resetting_self = ! empty( array_intersect( $roles, $current_user->roles ) );
1045 }
1046
1047 wp_send_json_success( array(
1048 'message' => $message,
1049 'results' => $results,
1050 'resetting_self' => $resetting_self,
1051 ) );
1052 }
1053
1054 /**
1055 * AJAX: Approve pending user
1056 */
1057 public function ajax_approve_user() {
1058 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1059
1060 if ( ! current_user_can( 'manage_options' ) ) {
1061 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1062 }
1063
1064 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1065
1066 if ( ! $user_id ) {
1067 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1068 }
1069
1070 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1071 $result = $user_security->approve_user( $user_id, get_current_user_id() );
1072
1073 if ( $result ) {
1074 $user = get_userdata( $user_id );
1075 wp_send_json_success( array(
1076 'message' => sprintf(
1077 /* translators: %s: Username */
1078 __( 'User "%s" has been approved.', 'vigilante' ),
1079 $user ? $user->user_login : $user_id
1080 ),
1081 ) );
1082 } else {
1083 wp_send_json_error( __( 'Failed to approve user.', 'vigilante' ) );
1084 }
1085 }
1086
1087 /**
1088 * AJAX: Reject pending user
1089 */
1090 public function ajax_reject_user() {
1091 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1092
1093 if ( ! current_user_can( 'manage_options' ) ) {
1094 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1095 }
1096
1097 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1098 $reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : '';
1099
1100 if ( ! $user_id ) {
1101 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1102 }
1103
1104 $user = get_userdata( $user_id );
1105 $username = $user ? $user->user_login : $user_id;
1106
1107 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1108 $result = $user_security->reject_user( $user_id, get_current_user_id(), $reason );
1109
1110 if ( $result ) {
1111 wp_send_json_success( array(
1112 'message' => sprintf(
1113 /* translators: %s: Username */
1114 __( 'User "%s" has been rejected and deleted.', 'vigilante' ),
1115 $username
1116 ),
1117 ) );
1118 } else {
1119 wp_send_json_error( __( 'Failed to reject user.', 'vigilante' ) );
1120 }
1121 }
1122
1123 /**
1124 * AJAX: Get user sessions
1125 */
1126 public function ajax_get_user_sessions() {
1127 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1128
1129 if ( ! current_user_can( 'manage_options' ) ) {
1130 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1131 }
1132
1133 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1134
1135 if ( ! $user_id ) {
1136 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1137 }
1138
1139 $user = get_userdata( $user_id );
1140 if ( ! $user ) {
1141 wp_send_json_error( __( 'User not found.', 'vigilante' ) );
1142 }
1143
1144 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1145 $sessions = $user_security->get_user_sessions( $user_id );
1146
1147 wp_send_json_success( array(
1148 'user' => array(
1149 'ID' => $user->ID,
1150 'user_login' => $user->user_login,
1151 'display_name' => $user->display_name,
1152 ),
1153 'sessions' => $sessions,
1154 ) );
1155 }
1156
1157 /**
1158 * AJAX: Revoke specific session
1159 */
1160 public function ajax_revoke_session() {
1161 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1162
1163 if ( ! current_user_can( 'manage_options' ) ) {
1164 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1165 }
1166
1167 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1168 $token_hash = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
1169
1170 if ( ! $user_id || ! $token_hash ) {
1171 wp_send_json_error( __( 'Invalid parameters.', 'vigilante' ) );
1172 }
1173
1174 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1175 $result = $user_security->revoke_session( $user_id, $token_hash );
1176
1177 if ( $result ) {
1178 wp_send_json_success( array(
1179 'message' => __( 'Session revoked successfully.', 'vigilante' ),
1180 ) );
1181 } else {
1182 wp_send_json_error( __( 'Failed to revoke session.', 'vigilante' ) );
1183 }
1184 }
1185
1186 /**
1187 * AJAX: Revoke all sessions for a user
1188 */
1189 public function ajax_revoke_all_sessions() {
1190 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1191
1192 if ( ! current_user_can( 'manage_options' ) ) {
1193 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1194 }
1195
1196 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1197 $include_current = ! empty( $_POST['include_current'] );
1198
1199 if ( ! $user_id ) {
1200 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1201 }
1202
1203 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1204 $count = $user_security->revoke_all_sessions( $user_id, $include_current );
1205
1206 wp_send_json_success( array(
1207 'message' => sprintf(
1208 /* translators: %d: Number of sessions */
1209 __( '%d session(s) revoked.', 'vigilante' ),
1210 $count
1211 ),
1212 'count' => $count,
1213 ) );
1214 }
1215
1216 /**
1217 * AJAX: Activate Under Attack mode
1218 */
1219 public function ajax_activate_under_attack() {
1220 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1221
1222 if ( ! current_user_can( 'manage_options' ) ) {
1223 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1224 }
1225
1226 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1227
1228 if ( $under_attack->is_active() ) {
1229 wp_send_json_error( __( 'Under Attack mode is already active.', 'vigilante' ) );
1230 }
1231
1232 $result = $under_attack->activate();
1233
1234 if ( $result ) {
1235 wp_send_json_success( array(
1236 'message' => __( 'Under Attack mode activated.', 'vigilante' ),
1237 'remaining' => $under_attack->get_remaining_time(),
1238 'expires' => $under_attack->get_status()['activated_at'] + $under_attack->get_status()['duration'],
1239 ) );
1240 } else {
1241 wp_send_json_error( __( 'Failed to activate Under Attack mode.', 'vigilante' ) );
1242 }
1243 }
1244
1245 /**
1246 * AJAX: Deactivate Under Attack mode
1247 */
1248 public function ajax_deactivate_under_attack() {
1249 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1250
1251 if ( ! current_user_can( 'manage_options' ) ) {
1252 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1253 }
1254
1255 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1256
1257 if ( ! $under_attack->is_active() ) {
1258 wp_send_json_error( __( 'Under Attack mode is not active.', 'vigilante' ) );
1259 }
1260
1261 $result = $under_attack->deactivate( 'manual' );
1262
1263 if ( $result ) {
1264 wp_send_json_success( __( 'Under Attack mode deactivated.', 'vigilante' ) );
1265 } else {
1266 wp_send_json_error( __( 'Failed to deactivate Under Attack mode.', 'vigilante' ) );
1267 }
1268 }
1269
1270 /**
1271 * AJAX: Get Under Attack mode status
1272 */
1273 public function ajax_under_attack_status() {
1274 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1275
1276 if ( ! current_user_can( 'manage_options' ) ) {
1277 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1278 }
1279
1280 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1281
1282 wp_send_json_success( array(
1283 'active' => $under_attack->is_active(),
1284 'remaining' => $under_attack->get_remaining_time(),
1285 ) );
1286 }
1287
1288 // =========================================================================
1289 // DATABASE BACKUP AJAX HANDLERS
1290 // =========================================================================
1291
1292 /**
1293 * AJAX: Get database tables list
1294 */
1295 public function ajax_get_db_tables() {
1296 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1297
1298 if ( ! current_user_can( 'manage_options' ) ) {
1299 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1300 }
1301
1302 $backup = new Vigilante_Database_Backup();
1303 $tables = $backup->get_tables();
1304
1305 wp_send_json_success( $tables );
1306 }
1307
1308 /**
1309 * AJAX: Download database backup
1310 *
1311 * Streams a ZIP file directly to the browser
1312 */
1313 public function ajax_download_db_backup() {
1314 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1315
1316 if ( ! current_user_can( 'manage_options' ) ) {
1317 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
1318 }
1319
1320 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
1321 $tables_raw = isset( $_POST['tables'] ) ? wp_unslash( $_POST['tables'] ) : '';
1322
1323 if ( empty( $tables_raw ) ) {
1324 wp_die( esc_html__( 'No tables selected.', 'vigilante' ), 400 );
1325 }
1326
1327 // Sanitize table names
1328 $tables = array_map( 'sanitize_key', explode( ',', $tables_raw ) );
1329 $tables = array_filter( $tables );
1330
1331 if ( empty( $tables ) ) {
1332 wp_die( esc_html__( 'No valid tables selected.', 'vigilante' ), 400 );
1333 }
1334
1335 $backup = new Vigilante_Database_Backup();
1336
1337 // Generate SQL dump
1338 $sql = $backup->generate_sql_dump( $tables );
1339 if ( is_wp_error( $sql ) ) {
1340 wp_die( esc_html( $sql->get_error_message() ), 500 );
1341 }
1342
1343 // Create ZIP
1344 $zip_path = $backup->create_zip( $sql );
1345 if ( is_wp_error( $zip_path ) ) {
1346 wp_die( esc_html( $zip_path->get_error_message() ), 500 );
1347 }
1348
1349 // Log the backup
1350 if ( $this->activity_log ) {
1351 $this->activity_log->log(
1352 'system',
1353 'database_backup',
1354 sprintf(
1355 /* translators: %d: Number of tables */
1356 __( 'Database backup created (%d tables)', 'vigilante' ),
1357 count( $tables )
1358 ),
1359 array( 'tables' => $tables ),
1360 'info'
1361 );
1362 }
1363
1364 // Stream download
1365 $backup->stream_download( $zip_path );
1366 }
1367
1368 // =========================================================================
1369 // DATABASE PREFIX AJAX HANDLERS
1370 // =========================================================================
1371
1372 /**
1373 * AJAX: Generate a new random prefix
1374 */
1375 public function ajax_generate_prefix() {
1376 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1377
1378 if ( ! current_user_can( 'manage_options' ) ) {
1379 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1380 }
1381
1382 $db_prefix = new Vigilante_Database_Prefix();
1383 $prefix = $db_prefix->generate_prefix();
1384
1385 wp_send_json_success( array( 'prefix' => $prefix ) );
1386 }
1387
1388 /**
1389 * AJAX: Change the database prefix
1390 */
1391 public function ajax_change_prefix() {
1392 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1393
1394 if ( ! current_user_can( 'manage_options' ) ) {
1395 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1396 }
1397
1398 $new_prefix = isset( $_POST['prefix'] ) ? sanitize_key( $_POST['prefix'] ) : '';
1399
1400 // Restore the underscore that sanitize_key might not strip but ensure it ends with one
1401 if ( ! empty( $new_prefix ) && substr( $new_prefix, -1 ) !== '_' ) {
1402 $new_prefix .= '_';
1403 }
1404
1405 if ( empty( $new_prefix ) ) {
1406 wp_send_json_error( __( 'Invalid prefix provided.', 'vigilante' ) );
1407 }
1408
1409 $db_prefix = new Vigilante_Database_Prefix();
1410
1411 // On a network the prefix is shared by every site: main site + network admin only
1412 $allowed = $db_prefix->can_change_prefix();
1413 if ( is_wp_error( $allowed ) ) {
1414 wp_send_json_error( $allowed->get_error_message() );
1415 }
1416
1417 // Validate first
1418 $valid = $db_prefix->validate_prefix( $new_prefix );
1419 if ( is_wp_error( $valid ) ) {
1420 wp_send_json_error( $valid->get_error_message() );
1421 }
1422
1423 // Log before changing (since after change, the log table will have new prefix)
1424 $old_prefix = $db_prefix->get_current_prefix();
1425
1426 // Execute the change
1427 $result = $db_prefix->change_prefix( $new_prefix );
1428
1429 if ( is_wp_error( $result ) ) {
1430 wp_send_json_error( $result->get_error_message() );
1431 }
1432
1433 // Log success (table has already been renamed, but the activity log object may still work for this request)
1434 if ( $this->activity_log ) {
1435 $this->activity_log->log(
1436 'system',
1437 'prefix_changed',
1438 sprintf(
1439 /* translators: 1: Old prefix, 2: New prefix */
1440 __( 'Database prefix changed from %1$s to %2$s', 'vigilante' ),
1441 $old_prefix,
1442 $new_prefix
1443 ),
1444 array(
1445 'old_prefix' => $old_prefix,
1446 'new_prefix' => $new_prefix,
1447 ),
1448 'warning'
1449 );
1450 }
1451
1452 wp_send_json_success( array(
1453 'message' => __( 'Database prefix changed successfully.', 'vigilante' ),
1454 'old_prefix' => $old_prefix,
1455 'new_prefix' => $new_prefix,
1456 ) );
1457 }
1458
1459 /**
1460 * AJAX: Unblock an IP from firewall rate limiting
1461 */
1462 public function ajax_unblock_firewall_ip() {
1463 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1464
1465 if ( ! current_user_can( 'manage_options' ) ) {
1466 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1467 }
1468
1469 $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
1470
1471 if ( empty( $ip ) ) {
1472 wp_send_json_error( __( 'No IP address provided.', 'vigilante' ) );
1473 }
1474
1475 $result = Vigilante_Firewall::unblock_ip( $ip );
1476
1477 if ( $result ) {
1478 // Log the manual unblock
1479 if ( $this->activity_log ) {
1480 $this->activity_log->log(
1481 'firewall',
1482 'unblocked',
1483 sprintf(
1484 /* translators: %s: IP address */
1485 __( 'IP %s manually unblocked from rate limiting', 'vigilante' ),
1486 $ip
1487 ),
1488 array( 'ip' => $ip ),
1489 'info'
1490 );
1491 }
1492 wp_send_json_success( sprintf(
1493 /* translators: %s: IP address */
1494 __( 'IP %s has been unblocked.', 'vigilante' ),
1495 $ip
1496 ) );
1497 } else {
1498 wp_send_json_error( __( 'IP not found in active blocks.', 'vigilante' ) );
1499 }
1500 }
1501
1502 }