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

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