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

1,803 lines 67.7 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 section data
396 *
397 * @param string $section Section name.
398 * @param array $data Data to sanitize.
399 * @return array Sanitized data.
400 */
401 private function sanitize_section_data( $section, $data ) {
402 $sanitized = array();
403
404 switch ( $section ) {
405 case 'firewall':
406 $sanitized = $this->sanitize_firewall_data( $data );
407 break;
408
409 case 'login_security':
410 $sanitized = $this->sanitize_login_security_data( $data );
411 break;
412
413 case 'security_headers':
414 $sanitized = $this->sanitize_security_headers_data( $data );
415 break;
416
417 case 'activity_log':
418 // Activity log uses process_section_data() directly
419 $sanitized = $this->sanitize_generic_data( $data );
420 break;
421
422 case 'user_security':
423 case 'user_security_advanced':
424 $sanitized = $this->sanitize_user_security_data( $data );
425 break;
426
427 default:
428 // Generic sanitization
429 $sanitized = $this->sanitize_generic_data( $data );
430 break;
431 }
432
433 return $sanitized;
434 }
435
436 /**
437 * Sanitize user security data
438 *
439 * @param array $data Data to sanitize.
440 * @return array
441 */
442 private function sanitize_user_security_data( $data ) {
443 $user = isset( $data['user_security'] ) ? $data['user_security'] : $data;
444
445 $sanitized = array(
446 'block_insecure_usernames' => ! empty( $user['block_insecure_usernames'] ),
447 'force_strong_passwords' => ! empty( $user['force_strong_passwords'] ),
448 'min_password_length' => isset( $user['min_password_length'] ) ? absint( $user['min_password_length'] ) : 12,
449 'block_author_scanning' => ! empty( $user['block_author_scanning'] ),
450 'prevent_display_name_login_match' => ! empty( $user['prevent_display_name_login_match'] ),
451 );
452
453 // Admin monitoring
454 if ( isset( $user['admin_monitoring'] ) ) {
455 $sanitized['admin_monitoring'] = array(
456 'alert_new_admin' => ! empty( $user['admin_monitoring']['alert_new_admin'] ),
457 'alert_admin_email_change' => ! empty( $user['admin_monitoring']['alert_admin_email_change'] ),
458 'alert_permission_elevation' => ! empty( $user['admin_monitoring']['alert_permission_elevation'] ),
459 );
460 }
461
462 // Registration approval
463 if ( isset( $user['registration_approval'] ) ) {
464 $sanitized['registration_approval'] = array(
465 'enabled' => ! empty( $user['registration_approval']['enabled'] ),
466 'notify_admin' => ! empty( $user['registration_approval']['notify_admin'] ),
467 'auto_reject_days' => isset( $user['registration_approval']['auto_reject_days'] )
468 ? absint( $user['registration_approval']['auto_reject_days'] )
469 : 0,
470 'affected_roles' => isset( $user['registration_approval']['affected_roles'] )
471 ? array_map( 'sanitize_key', (array) $user['registration_approval']['affected_roles'] )
472 : array( 'subscriber' ),
473 );
474 }
475
476 // Session management
477 if ( isset( $user['session_management'] ) ) {
478 $sanitized['session_management'] = array(
479 'enabled' => ! empty( $user['session_management']['enabled'] ),
480 'show_in_profile' => ! empty( $user['session_management']['show_in_profile'] ),
481 );
482 }
483
484 // Session limits
485 if ( isset( $user['session_limits'] ) ) {
486 $sanitized['session_limits'] = array(
487 'enabled' => ! empty( $user['session_limits']['enabled'] ),
488 'max_sessions' => isset( $user['session_limits']['max_sessions'] )
489 ? absint( $user['session_limits']['max_sessions'] )
490 : 3,
491 'behavior' => isset( $user['session_limits']['behavior'] )
492 ? sanitize_key( $user['session_limits']['behavior'] )
493 : 'close_oldest',
494 'exclude_admins' => ! empty( $user['session_limits']['exclude_admins'] ),
495 );
496 }
497
498 // Password expiration
499 if ( isset( $user['password_expiration'] ) ) {
500 $sanitized['password_expiration'] = array(
501 'enabled' => ! empty( $user['password_expiration']['enabled'] ),
502 'expire_days' => isset( $user['password_expiration']['expire_days'] )
503 ? absint( $user['password_expiration']['expire_days'] )
504 : 90,
505 'warning_days' => isset( $user['password_expiration']['warning_days'] )
506 ? absint( $user['password_expiration']['warning_days'] )
507 : 14,
508 'affected_roles' => isset( $user['password_expiration']['affected_roles'] )
509 ? array_map( 'sanitize_key', (array) $user['password_expiration']['affected_roles'] )
510 : array( 'administrator', 'editor' ),
511 'excluded_users' => isset( $user['password_expiration']['excluded_users'] )
512 ? array_values( array_unique( array_filter( array_map( 'absint', (array) $user['password_expiration']['excluded_users'] ) ) ) )
513 : array(),
514 'password_history' => isset( $user['password_expiration']['password_history'] )
515 ? absint( $user['password_expiration']['password_history'] )
516 : 3,
517 'send_reminder' => ! empty( $user['password_expiration']['send_reminder'] ),
518 );
519 }
520
521 // Email verification
522 if ( isset( $user['email_verification'] ) ) {
523 $sanitized['email_verification'] = array(
524 'enabled' => ! empty( $user['email_verification']['enabled'] ),
525 'token_expiry_hours' => isset( $user['email_verification']['token_expiry_hours'] )
526 ? absint( $user['email_verification']['token_expiry_hours'] )
527 : 24,
528 'allow_resend' => ! empty( $user['email_verification']['allow_resend'] ),
529 'auto_delete_days' => isset( $user['email_verification']['auto_delete_days'] )
530 ? absint( $user['email_verification']['auto_delete_days'] )
531 : 7,
532 );
533 }
534
535 return $sanitized;
536 }
537
538 /**
539 * Sanitize firewall data
540 *
541 * @param array $data Data to sanitize.
542 * @return array
543 */
544 private function sanitize_firewall_data( $data ) {
545 $firewall = isset( $data['firewall'] ) ? $data['firewall'] : $data;
546
547 $proxy_header = sanitize_text_field( wp_unslash( $firewall['trusted_proxy_header'] ?? '' ) );
548 if ( ! in_array( $proxy_header, array( 'cf-connecting-ip', 'x-forwarded-for', 'x-real-ip' ), true ) ) {
549 $proxy_header = '';
550 }
551
552 return array(
553 'block_bad_query_strings' => ! empty( $firewall['block_bad_query_strings'] ),
554 'block_sql_injection' => ! empty( $firewall['block_sql_injection'] ),
555 'block_xss_attacks' => ! empty( $firewall['block_xss_attacks'] ),
556 'block_file_inclusion' => ! empty( $firewall['block_file_inclusion'] ),
557 'block_directory_traversal' => ! empty( $firewall['block_directory_traversal'] ),
558 'block_php_in_uploads' => ! empty( $firewall['block_php_in_uploads'] ),
559 'block_sensitive_files' => ! empty( $firewall['block_sensitive_files'] ),
560 'block_bad_bots' => ! empty( $firewall['block_bad_bots'] ),
561 'block_empty_user_agent' => ! empty( $firewall['block_empty_user_agent'] ),
562 'allowed_http_methods' => isset( $firewall['allowed_http_methods'] )
563 ? array_map( 'sanitize_text_field', (array) $firewall['allowed_http_methods'] )
564 : array( 'GET', 'POST', 'HEAD' ),
565 'rate_limiting' => array(
566 'enabled' => ! empty( $firewall['rate_limiting']['enabled'] ),
567 'requests_per_minute' => isset( $firewall['rate_limiting']['requests_per_minute'] )
568 ? absint( $firewall['rate_limiting']['requests_per_minute'] )
569 : 120,
570 'block_duration' => isset( $firewall['rate_limiting']['block_duration'] )
571 ? absint( $firewall['rate_limiting']['block_duration'] )
572 : 300,
573 'progressive' => ! empty( $firewall['rate_limiting']['progressive'] ),
574 'max_block_duration' => isset( $firewall['rate_limiting']['max_block_duration'] )
575 ? absint( $firewall['rate_limiting']['max_block_duration'] )
576 : 86400,
577 ),
578 'ip_whitelist' => $this->sanitize_ip_list( $firewall['ip_whitelist'] ?? '' ),
579 'ip_blacklist' => $this->sanitize_ip_list( $firewall['ip_blacklist'] ?? '' ),
580 'ua_whitelist' => $this->sanitize_ua_list( $firewall['ua_whitelist'] ?? '' ),
581 'ua_blacklist' => $this->sanitize_ua_list( $firewall['ua_blacklist'] ?? '' ),
582 'trusted_proxy_header' => $proxy_header,
583 );
584 }
585
586 /**
587 * Sanitize login security data
588 *
589 * @param array $data Data to sanitize.
590 * @return array
591 */
592 private function sanitize_login_security_data( $data ) {
593 $login = isset( $data['login_security'] ) ? $data['login_security'] : $data;
594
595 $sanitized = array(
596 'max_attempts' => isset( $login['max_attempts'] ) ? absint( $login['max_attempts'] ) : 5,
597 'lockout_duration' => isset( $login['lockout_duration'] ) ? absint( $login['lockout_duration'] ) : 1800,
598 'lockout_increment' => ! empty( $login['lockout_increment'] ),
599 'max_lockout_duration' => isset( $login['max_lockout_duration'] ) ? absint( $login['max_lockout_duration'] ) : 86400,
600 'hide_login_errors' => ! empty( $login['hide_login_errors'] ),
601 'disable_xmlrpc' => ! empty( $login['disable_xmlrpc'] ),
602 'disable_xmlrpc_pingback' => ! empty( $login['disable_xmlrpc_pingback'] ),
603 'disable_application_passwords' => ! empty( $login['disable_application_passwords'] ),
604 'notify_on_lockout' => ! empty( $login['notify_on_lockout'] ),
605 'notify_on_admin_login' => ! empty( $login['notify_on_admin_login'] ),
606 'notify_email' => isset( $login['notify_email'] ) ? sanitize_email( $login['notify_email'] ) : '',
607 'ip_whitelist' => $this->sanitize_ip_list( $login['ip_whitelist'] ?? '' ),
608 );
609
610 // Two-Factor Authentication
611 if ( isset( $login['two_factor'] ) ) {
612 $sanitized['two_factor'] = $this->sanitize_two_factor_data( $login['two_factor'] );
613 }
614
615 return $sanitized;
616 }
617
618 /**
619 * Sanitize security headers data
620 *
621 * @param array $data Data to sanitize.
622 * @return array
623 */
624 private function sanitize_security_headers_data( $data ) {
625 $headers = isset( $data['security_headers'] ) ? $data['security_headers'] : $data;
626
627 return array(
628 'enabled' => true,
629 'x_frame_options' => isset( $headers['x_frame_options'] ) ? sanitize_text_field( $headers['x_frame_options'] ) : 'SAMEORIGIN',
630 'x_content_type_options'=> ! empty( $headers['x_content_type_options'] ),
631 'referrer_policy' => isset( $headers['referrer_policy'] ) ? sanitize_text_field( $headers['referrer_policy'] ) : 'strict-origin-when-cross-origin',
632 'hsts' => array(
633 'enabled' => ! empty( $headers['hsts']['enabled'] ),
634 'max_age' => isset( $headers['hsts']['max_age'] ) ? absint( $headers['hsts']['max_age'] ) : 31536000,
635 'include_subdomains' => ! empty( $headers['hsts']['include_subdomains'] ),
636 'preload' => ! empty( $headers['hsts']['preload'] ),
637 ),
638 'csp' => array(
639 'enabled' => ! empty( $headers['csp']['enabled'] ),
640 'report_only' => ! empty( $headers['csp']['report_only'] ),
641 'directives' => isset( $headers['csp']['directives'] )
642 ? $this->sanitize_csp_directives( $headers['csp']['directives'] )
643 : array(),
644 ),
645 'permissions_policy' => array(
646 'enabled' => ! empty( $headers['permissions_policy']['enabled'] ),
647 ),
648 );
649 }
650
651 /**
652 * Sanitize activity log data
653 *
654 * @param array $data Data to sanitize.
655 * @return array
656 */
657 /**
658 * Sanitize generic data
659 *
660 * @param array $data Data to sanitize.
661 * @return array
662 */
663 private function sanitize_generic_data( $data ) {
664 $sanitized = array();
665
666 foreach ( $data as $key => $value ) {
667 if ( is_array( $value ) ) {
668 $sanitized[ $key ] = $this->sanitize_generic_data( $value );
669 } elseif ( is_bool( $value ) || in_array( $value, array( '0', '1', 0, 1 ), true ) ) {
670 $sanitized[ $key ] = (bool) $value;
671 } elseif ( is_numeric( $value ) ) {
672 $sanitized[ $key ] = absint( $value );
673 } else {
674 $sanitized[ $key ] = sanitize_text_field( $value );
675 }
676 }
677
678 return $sanitized;
679 }
680
681 /**
682 * Sanitize IP list
683 *
684 * @param string|array $ips IPs as string (newline separated) or array.
685 * @return array
686 */
687 private function sanitize_ip_list( $ips ) {
688 if ( is_string( $ips ) ) {
689 $ips = array_filter( array_map( 'trim', explode( "\n", $ips ) ) );
690 }
691
692 $sanitized = array();
693
694 foreach ( (array) $ips as $ip ) {
695 $ip = trim( $ip );
696 // Validate IP or CIDR
697 if ( filter_var( $ip, FILTER_VALIDATE_IP ) || preg_match( '/^[\d\.]+\/\d{1,2}$/', $ip ) ) {
698 $sanitized[] = $ip;
699 }
700 }
701
702 return $sanitized;
703 }
704
705 /**
706 * Sanitize User-Agent list
707 *
708 * @param string|array $uas User-Agent strings (newline-separated or array).
709 * @return array
710 */
711 private function sanitize_ua_list( $uas ) {
712 if ( is_string( $uas ) ) {
713 $uas = array_filter( array_map( 'trim', explode( "\n", $uas ) ) );
714 }
715
716 $sanitized = array();
717
718 foreach ( (array) $uas as $ua ) {
719 $ua = sanitize_text_field( trim( $ua ) );
720 if ( ! empty( $ua ) ) {
721 $sanitized[] = $ua;
722 }
723 }
724
725 return array_unique( $sanitized );
726 }
727
728 /**
729 * Sanitize CSP directives
730 *
731 * @param array $directives CSP directives.
732 * @return array
733 */
734 private function sanitize_csp_directives( $directives ) {
735 $sanitized = array();
736 $allowed_directives = array(
737 'default-src', 'script-src', 'style-src', 'img-src', 'font-src',
738 'connect-src', 'media-src', 'frame-src', 'frame-ancestors',
739 'base-uri', 'form-action', 'object-src', 'upgrade-insecure-requests',
740 );
741
742 foreach ( $allowed_directives as $directive ) {
743 if ( isset( $directives[ $directive ] ) ) {
744 if ( 'upgrade-insecure-requests' === $directive ) {
745 $sanitized[ $directive ] = ! empty( $directives[ $directive ] );
746 } else {
747 $sanitized[ $directive ] = sanitize_text_field( $directives[ $directive ] );
748 }
749 }
750 }
751
752 return $sanitized;
753 }
754
755 /**
756 * AJAX: Search users for 2FA exclusion
757 */
758 public function ajax_search_users_2fa() {
759 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
760
761 if ( ! current_user_can( 'manage_options' ) ) {
762 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
763 }
764
765 $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
766 $exclude = isset( $_POST['exclude'] ) ? array_map( 'absint', (array) $_POST['exclude'] ) : array();
767
768 if ( strlen( $query ) < 2 ) {
769 wp_send_json_error( __( 'Query too short.', 'vigilante' ) );
770 }
771
772 // Search users by login, email, or display name
773 $users = get_users( array(
774 'search' => '*' . $query . '*',
775 'search_columns' => array( 'user_login', 'user_email', 'display_name' ),
776 'exclude' => $exclude,
777 'number' => 10,
778 'orderby' => 'display_name',
779 'order' => 'ASC',
780 ) );
781
782 $results = array();
783
784 foreach ( $users as $user ) {
785 $results[] = array(
786 'ID' => $user->ID,
787 'user_login' => $user->user_login,
788 'user_email' => $user->user_email,
789 'display_name' => $user->display_name,
790 'avatar' => get_avatar_url( $user->ID, array( 'size' => 32 ) ),
791 );
792 }
793
794 wp_send_json_success( $results );
795 }
796
797 /**
798 * AJAX: Send 2FA activation notification
799 */
800 public function ajax_send_2fa_notification() {
801 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
802
803 if ( ! current_user_can( 'manage_options' ) ) {
804 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
805 }
806
807 $mode = isset( $_POST['mode'] ) ? sanitize_key( $_POST['mode'] ) : 'all';
808 $only_new = 'new' === $mode;
809
810 // Read settings to determine active 2FA method
811 $login_security = $this->settings->get_section( 'login_security' );
812 $two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array();
813 $method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email';
814
815 if ( 'totp' === $method ) {
816 // TOTP method: use TOTP class for styled activation emails
817 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
818 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
819 }
820
821 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
822 $roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' );
823 $excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array();
824 $site_name = get_bloginfo( 'name' );
825 $from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name;
826
827 if ( empty( $roles ) ) {
828 $roles = array( 'administrator' );
829 }
830
831 $args = array( 'role__in' => $roles );
832 if ( ! empty( $excluded ) ) {
833 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings.
834 $args['exclude'] = $excluded;
835 }
836 $users = get_users( $args );
837
838 $sent = 0;
839 $skipped = 0;
840 $failed = 0;
841
842 foreach ( $users as $user ) {
843 // Skip users who already have TOTP configured (unless sending to all)
844 if ( $only_new ) {
845 $totp_data = $this->database->get_totp_data( $user->ID );
846 if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) {
847 $skipped++;
848 continue;
849 }
850 if ( $this->database->user_was_2fa_notified( $user->ID ) ) {
851 $skipped++;
852 continue;
853 }
854 }
855
856 $email_sent = $totp->send_activation_email( $user, $site_name, $from_name );
857
858 if ( $email_sent ) {
859 $this->database->mark_2fa_notified( $user->ID );
860 $sent++;
861 } else {
862 $failed++;
863 }
864 }
865
866 wp_send_json_success( array(
867 'sent' => $sent,
868 'skipped' => $skipped,
869 'failed' => $failed,
870 ) );
871 } else {
872 // Email method: use email 2FA class
873 if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) {
874 require_once VIGILANTE_PLUGIN_DIR . 'includes/class-two-factor-email.php';
875 }
876
877 $two_factor_email = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log );
878 $result = $two_factor_email->send_activation_notifications( $only_new );
879
880 wp_send_json_success( $result );
881 }
882 }
883
884 /**
885 * AJAX: Search users with TOTP configured (for admin reset)
886 */
887 public function ajax_search_totp_users() {
888 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
889
890 if ( ! current_user_can( 'manage_options' ) ) {
891 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
892 }
893
894 $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
895
896 if ( strlen( $query ) < 2 ) {
897 wp_send_json_error( __( 'Query too short.', 'vigilante' ) );
898 }
899
900 $results = $this->database->search_totp_users( $query, 10 );
901
902 $users = array();
903 foreach ( $results as $row ) {
904 $avatar = get_avatar_url( $row['user_id'], array( 'size' => 32 ) );
905 $users[] = array(
906 'ID' => absint( $row['user_id'] ),
907 'display_name' => $row['display_name'],
908 'user_email' => $row['user_email'],
909 'configured_at' => $row['configured_at'],
910 'last_used_at' => $row['last_used_at'],
911 'avatar' => $avatar,
912 );
913 }
914
915 wp_send_json_success( $users );
916 }
917
918 /**
919 * AJAX: Reset TOTP for selected users (admin action)
920 */
921 public function ajax_reset_totp_users() {
922 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
923
924 if ( ! current_user_can( 'manage_options' ) ) {
925 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
926 }
927
928 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with array_map
929 $user_ids = isset( $_POST['user_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['user_ids'] ) ) : array();
930
931 if ( empty( $user_ids ) ) {
932 wp_send_json_error( __( 'No users selected.', 'vigilante' ) );
933 }
934
935 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
936 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
937 }
938
939 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
940 $count = 0;
941
942 foreach ( $user_ids as $uid ) {
943 if ( $uid > 0 ) {
944 $totp->reset_user_totp( $uid );
945 $count++;
946 }
947 }
948
949 wp_send_json_success( array(
950 /* translators: %d: Number of users reset */
951 'message' => sprintf( _n( 'TOTP reset for %d user.', 'TOTP reset for %d users.', $count, 'vigilante' ), $count ),
952 'count' => $count,
953 ) );
954 }
955
956 /**
957 * AJAX: Get TOTP setup data (secret + QR) for user profile
958 */
959 public function ajax_totp_get_setup() {
960 check_ajax_referer( 'vigilante_totp_profile', 'nonce' );
961
962 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
963
964 // Fallback to current user if user_id is 0
965 if ( 0 === $user_id ) {
966 $user_id = get_current_user_id();
967 }
968
969 if ( 0 === $user_id ) {
970 wp_send_json_error( __( 'Invalid user.', 'vigilante' ) );
971 }
972
973 // Permission check: own profile or admin
974 if ( get_current_user_id() !== $user_id && ! current_user_can( 'manage_options' ) ) {
975 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
976 }
977
978 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
979 wp_send_json_error( __( 'TOTP module not available.', 'vigilante' ) );
980 }
981
982 $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
983 $data = $totp->get_setup_data( $user_id );
984
985 if ( empty( $data ) ) {
986 wp_send_json_error( __( 'Could not generate setup data. User not found.', 'vigilante' ) );
987 }
988
989 wp_send_json_success( $data );
990 }
991
992 /**
993 * AJAX: Send login URL notification to users with admin access
994 */
995 public function ajax_notify_login_url() {
996 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
997
998 if ( ! current_user_can( 'manage_options' ) ) {
999 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1000 }
1001
1002 $login_options = $this->settings->get_section( 'login_security' );
1003 $custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : '';
1004
1005 if ( empty( $custom_url ) ) {
1006 wp_send_json_error( __( 'No custom login URL configured.', 'vigilante' ) );
1007 }
1008
1009 $login_url = home_url( $custom_url . '/' );
1010 $site_name = get_bloginfo( 'name' );
1011
1012 // Roles that can access wp-admin
1013 $admin_roles = array( 'administrator', 'editor', 'author', 'contributor' );
1014
1015 $users = get_users( array(
1016 'role__in' => $admin_roles,
1017 ) );
1018
1019 if ( empty( $users ) ) {
1020 wp_send_json_error( __( 'No users found.', 'vigilante' ) );
1021 }
1022
1023 $subject = sprintf(
1024 /* translators: %s: Site name */
1025 __( '[%s] Your login URL has changed', 'vigilante' ),
1026 $site_name
1027 );
1028
1029 // Build email body using template
1030 $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' ) );
1031 $body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) );
1032 $body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) );
1033 $body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) );
1034
1035 $sent = 0;
1036 $failed = 0;
1037
1038 foreach ( $users as $user ) {
1039 $result = Vigilante_Email_Template::send(
1040 $user->user_email,
1041 $subject,
1042 __( 'Login URL changed', 'vigilante' ),
1043 $body
1044 );
1045 if ( $result ) {
1046 $sent++;
1047 } else {
1048 $failed++;
1049 }
1050 }
1051
1052 if ( $this->activity_log ) {
1053 $this->activity_log->log(
1054 'login',
1055 'login_url_notified',
1056 sprintf(
1057 /* translators: 1: Sent count, 2: Failed count */
1058 __( 'Login URL notification sent: %1$d sent, %2$d failed', 'vigilante' ),
1059 $sent,
1060 $failed
1061 )
1062 );
1063 }
1064
1065 wp_send_json_success( array(
1066 'sent' => $sent,
1067 'failed' => $failed,
1068 ) );
1069 }
1070
1071 /**
1072 * Sanitize 2FA data within login security
1073 *
1074 * @param array $two_factor 2FA data to sanitize.
1075 * @return array
1076 */
1077 private function sanitize_two_factor_data( $two_factor ) {
1078 $valid_methods = array( 'email', 'totp' );
1079 $method = isset( $two_factor['method'] ) ? sanitize_key( $two_factor['method'] ) : 'email';
1080
1081 return array(
1082 'enabled' => ! empty( $two_factor['enabled'] ),
1083 'method' => in_array( $method, $valid_methods, true ) ? $method : 'email',
1084 'enforced_roles' => isset( $two_factor['enforced_roles'] )
1085 ? array_map( 'sanitize_key', (array) $two_factor['enforced_roles'] )
1086 : array( 'administrator', 'editor' ),
1087 'excluded_users' => isset( $two_factor['excluded_users'] )
1088 ? array_map( 'absint', (array) $two_factor['excluded_users'] )
1089 : array(),
1090 'remember_device_days' => isset( $two_factor['remember_device_days'] )
1091 ? absint( $two_factor['remember_device_days'] )
1092 : 30,
1093 'code_expiry_minutes' => isset( $two_factor['code_expiry_minutes'] )
1094 ? absint( $two_factor['code_expiry_minutes'] )
1095 : 10,
1096 'max_attempts' => isset( $two_factor['max_attempts'] )
1097 ? absint( $two_factor['max_attempts'] )
1098 : 3,
1099 'email_from_name' => isset( $two_factor['email_from_name'] )
1100 ? sanitize_text_field( $two_factor['email_from_name'] )
1101 : '',
1102 'notify_on_enable' => ! empty( $two_factor['notify_on_enable'] ),
1103 'grace_period_days' => isset( $two_factor['grace_period_days'] )
1104 ? min( 30, absint( $two_factor['grace_period_days'] ) )
1105 : 3,
1106 );
1107 }
1108
1109 /**
1110 * AJAX: Search users for password reset
1111 */
1112 public function ajax_search_users_password_reset() {
1113 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1114
1115 if ( ! current_user_can( 'manage_options' ) ) {
1116 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1117 }
1118
1119 $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
1120
1121 if ( strlen( $query ) < 2 ) {
1122 wp_send_json_error( __( 'Query too short.', 'vigilante' ) );
1123 }
1124
1125 // Search users by login, email, or display name
1126 $users = get_users( array(
1127 'search' => '*' . $query . '*',
1128 'search_columns' => array( 'user_login', 'user_email', 'display_name' ),
1129 'number' => 10,
1130 'orderby' => 'display_name',
1131 'order' => 'ASC',
1132 ) );
1133
1134 $results = array();
1135
1136 foreach ( $users as $user ) {
1137 $results[] = array(
1138 'ID' => $user->ID,
1139 'user_login' => $user->user_login,
1140 'user_email' => $user->user_email,
1141 'display_name' => $user->display_name,
1142 'avatar' => get_avatar_url( $user->ID, array( 'size' => 32 ) ),
1143 'roles' => implode( ', ', $user->roles ),
1144 );
1145 }
1146
1147 wp_send_json_success( $results );
1148 }
1149
1150 /**
1151 * AJAX: Force password reset for specific users
1152 * Uses native WordPress password reset flow
1153 */
1154 public function ajax_force_password_reset() {
1155 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1156
1157 if ( ! current_user_can( 'manage_options' ) ) {
1158 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1159 }
1160
1161 $user_ids = isset( $_POST['user_ids'] ) ? array_map( 'absint', (array) $_POST['user_ids'] ) : array();
1162 $current_user_id = get_current_user_id();
1163
1164 if ( empty( $user_ids ) ) {
1165 wp_send_json_error( __( 'No users selected.', 'vigilante' ) );
1166 }
1167
1168 // Check if current user is resetting themselves
1169 $resetting_self = in_array( $current_user_id, $user_ids, true );
1170
1171 // Create user security instance to use native reset
1172 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1173
1174 // Perform bulk reset
1175 $results = $user_security->force_password_reset_bulk( $user_ids, $current_user_id );
1176
1177 $message = sprintf(
1178 /* translators: %d: Number of users */
1179 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
1180 $results['success']
1181 );
1182
1183 if ( $results['failed'] > 0 ) {
1184 $message .= ' ' . sprintf(
1185 /* translators: %d: Number of failures */
1186 __( '%d failed.', 'vigilante' ),
1187 $results['failed']
1188 );
1189 }
1190
1191 wp_send_json_success( array(
1192 'message' => $message,
1193 'results' => $results,
1194 'resetting_self' => $resetting_self,
1195 ) );
1196 }
1197
1198 /**
1199 * AJAX: Force password reset for all users
1200 * Uses native WordPress password reset flow
1201 */
1202 public function ajax_force_password_reset_all() {
1203 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1204
1205 if ( ! current_user_can( 'manage_options' ) ) {
1206 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1207 }
1208
1209 $include_self = ! empty( $_POST['include_self'] );
1210 $current_user_id = get_current_user_id();
1211
1212 // Create user security instance to use native reset
1213 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1214
1215 // Perform reset for all users
1216 $results = $user_security->force_password_reset_all( $current_user_id, ! $include_self );
1217
1218 // Log the bulk action
1219 if ( $this->activity_log ) {
1220 $reset_by_user = get_userdata( $current_user_id );
1221 $this->activity_log->log(
1222 'user',
1223 'force_password_reset_all',
1224 sprintf(
1225 /* translators: 1: Number of users, 2: Admin username */
1226 __( 'Password reset forced for %1$d users by %2$s', 'vigilante' ),
1227 $results['success'],
1228 $reset_by_user ? $reset_by_user->user_login : __( 'System', 'vigilante' )
1229 ),
1230 array(
1231 'count' => $results['success'],
1232 'reset_by' => $current_user_id,
1233 'include_self' => $include_self,
1234 ),
1235 'warning'
1236 );
1237 }
1238
1239 $message = sprintf(
1240 /* translators: %d: Number of users */
1241 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
1242 $results['success']
1243 );
1244
1245 if ( $results['failed'] > 0 ) {
1246 $message .= ' ' . sprintf(
1247 /* translators: %d: Number of failures */
1248 __( '%d failed.', 'vigilante' ),
1249 $results['failed']
1250 );
1251 }
1252
1253 wp_send_json_success( array(
1254 'message' => $message,
1255 'results' => $results,
1256 'resetting_self' => $include_self,
1257 ) );
1258 }
1259
1260 /**
1261 * AJAX: Force password reset by role
1262 * Resets passwords for all users with the selected roles
1263 */
1264 public function ajax_force_password_reset_by_role() {
1265 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1266
1267 if ( ! current_user_can( 'manage_options' ) ) {
1268 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1269 }
1270
1271 $roles = isset( $_POST['roles'] ) ? array_map( 'sanitize_key', (array) $_POST['roles'] ) : array();
1272
1273 if ( empty( $roles ) ) {
1274 wp_send_json_error( __( 'No roles selected.', 'vigilante' ) );
1275 }
1276
1277 // Validate that submitted roles actually exist.
1278 $wp_roles = wp_roles();
1279 foreach ( $roles as $role ) {
1280 if ( ! isset( $wp_roles->roles[ $role ] ) ) {
1281 wp_send_json_error(
1282 sprintf(
1283 /* translators: %s: Role slug */
1284 __( 'Invalid role: %s', 'vigilante' ),
1285 $role
1286 )
1287 );
1288 }
1289 }
1290
1291 $include_self = ! empty( $_POST['include_self'] );
1292 $current_user_id = get_current_user_id();
1293
1294 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1295
1296 $results = $user_security->force_password_reset_by_roles(
1297 $roles,
1298 $current_user_id,
1299 ! $include_self
1300 );
1301
1302 // Log the action.
1303 if ( $this->activity_log ) {
1304 $reset_by_user = get_userdata( $current_user_id );
1305 $role_names = array();
1306
1307 foreach ( $roles as $role ) {
1308 $role_names[] = isset( $wp_roles->roles[ $role ] )
1309 ? translate_user_role( $wp_roles->roles[ $role ]['name'] )
1310 : $role;
1311 }
1312
1313 $this->activity_log->log(
1314 'user',
1315 'force_password_reset_by_role',
1316 sprintf(
1317 /* translators: 1: Number of users, 2: Role names, 3: Admin username */
1318 __( 'Password reset forced for %1$d users (roles: %2$s) by %3$s', 'vigilante' ),
1319 $results['success'],
1320 implode( ', ', $role_names ),
1321 $reset_by_user ? $reset_by_user->user_login : __( 'System', 'vigilante' )
1322 ),
1323 array(
1324 'count' => $results['success'],
1325 'roles' => $roles,
1326 'reset_by' => $current_user_id,
1327 'include_self' => $include_self,
1328 ),
1329 'warning'
1330 );
1331 }
1332
1333 $message = sprintf(
1334 /* translators: %d: Number of users */
1335 __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
1336 $results['success']
1337 );
1338
1339 if ( $results['failed'] > 0 ) {
1340 $message .= ' ' . sprintf(
1341 /* translators: %d: Number of failures */
1342 __( '%d failed.', 'vigilante' ),
1343 $results['failed']
1344 );
1345 }
1346
1347 // Check if current user was included via role membership.
1348 $resetting_self = false;
1349 if ( $include_self ) {
1350 $current_user = wp_get_current_user();
1351 $resetting_self = ! empty( array_intersect( $roles, $current_user->roles ) );
1352 }
1353
1354 wp_send_json_success( array(
1355 'message' => $message,
1356 'results' => $results,
1357 'resetting_self' => $resetting_self,
1358 ) );
1359 }
1360
1361 /**
1362 * AJAX: Approve pending user
1363 */
1364 public function ajax_approve_user() {
1365 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1366
1367 if ( ! current_user_can( 'manage_options' ) ) {
1368 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1369 }
1370
1371 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1372
1373 if ( ! $user_id ) {
1374 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1375 }
1376
1377 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1378 $result = $user_security->approve_user( $user_id, get_current_user_id() );
1379
1380 if ( $result ) {
1381 $user = get_userdata( $user_id );
1382 wp_send_json_success( array(
1383 'message' => sprintf(
1384 /* translators: %s: Username */
1385 __( 'User "%s" has been approved.', 'vigilante' ),
1386 $user ? $user->user_login : $user_id
1387 ),
1388 ) );
1389 } else {
1390 wp_send_json_error( __( 'Failed to approve user.', 'vigilante' ) );
1391 }
1392 }
1393
1394 /**
1395 * AJAX: Reject pending user
1396 */
1397 public function ajax_reject_user() {
1398 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1399
1400 if ( ! current_user_can( 'manage_options' ) ) {
1401 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1402 }
1403
1404 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1405 $reason = isset( $_POST['reason'] ) ? sanitize_text_field( wp_unslash( $_POST['reason'] ) ) : '';
1406
1407 if ( ! $user_id ) {
1408 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1409 }
1410
1411 $user = get_userdata( $user_id );
1412 $username = $user ? $user->user_login : $user_id;
1413
1414 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1415 $result = $user_security->reject_user( $user_id, get_current_user_id(), $reason );
1416
1417 if ( $result ) {
1418 wp_send_json_success( array(
1419 'message' => sprintf(
1420 /* translators: %s: Username */
1421 __( 'User "%s" has been rejected and deleted.', 'vigilante' ),
1422 $username
1423 ),
1424 ) );
1425 } else {
1426 wp_send_json_error( __( 'Failed to reject user.', 'vigilante' ) );
1427 }
1428 }
1429
1430 /**
1431 * AJAX: Get user sessions
1432 */
1433 public function ajax_get_user_sessions() {
1434 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1435
1436 if ( ! current_user_can( 'manage_options' ) ) {
1437 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1438 }
1439
1440 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1441
1442 if ( ! $user_id ) {
1443 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1444 }
1445
1446 $user = get_userdata( $user_id );
1447 if ( ! $user ) {
1448 wp_send_json_error( __( 'User not found.', 'vigilante' ) );
1449 }
1450
1451 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1452 $sessions = $user_security->get_user_sessions( $user_id );
1453
1454 wp_send_json_success( array(
1455 'user' => array(
1456 'ID' => $user->ID,
1457 'user_login' => $user->user_login,
1458 'display_name' => $user->display_name,
1459 ),
1460 'sessions' => $sessions,
1461 ) );
1462 }
1463
1464 /**
1465 * AJAX: Revoke specific session
1466 */
1467 public function ajax_revoke_session() {
1468 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1469
1470 if ( ! current_user_can( 'manage_options' ) ) {
1471 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1472 }
1473
1474 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1475 $token_hash = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
1476
1477 if ( ! $user_id || ! $token_hash ) {
1478 wp_send_json_error( __( 'Invalid parameters.', 'vigilante' ) );
1479 }
1480
1481 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1482 $result = $user_security->revoke_session( $user_id, $token_hash );
1483
1484 if ( $result ) {
1485 wp_send_json_success( array(
1486 'message' => __( 'Session revoked successfully.', 'vigilante' ),
1487 ) );
1488 } else {
1489 wp_send_json_error( __( 'Failed to revoke session.', 'vigilante' ) );
1490 }
1491 }
1492
1493 /**
1494 * AJAX: Revoke all sessions for a user
1495 */
1496 public function ajax_revoke_all_sessions() {
1497 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1498
1499 if ( ! current_user_can( 'manage_options' ) ) {
1500 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1501 }
1502
1503 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1504 $include_current = ! empty( $_POST['include_current'] );
1505
1506 if ( ! $user_id ) {
1507 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1508 }
1509
1510 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1511 $count = $user_security->revoke_all_sessions( $user_id, $include_current );
1512
1513 wp_send_json_success( array(
1514 'message' => sprintf(
1515 /* translators: %d: Number of sessions */
1516 __( '%d session(s) revoked.', 'vigilante' ),
1517 $count
1518 ),
1519 'count' => $count,
1520 ) );
1521 }
1522
1523 /**
1524 * AJAX: Activate Under Attack mode
1525 */
1526 public function ajax_activate_under_attack() {
1527 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1528
1529 if ( ! current_user_can( 'manage_options' ) ) {
1530 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1531 }
1532
1533 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1534
1535 if ( $under_attack->is_active() ) {
1536 wp_send_json_error( __( 'Under Attack mode is already active.', 'vigilante' ) );
1537 }
1538
1539 $result = $under_attack->activate();
1540
1541 if ( $result ) {
1542 wp_send_json_success( array(
1543 'message' => __( 'Under Attack mode activated.', 'vigilante' ),
1544 'remaining' => $under_attack->get_remaining_time(),
1545 'expires' => $under_attack->get_status()['activated_at'] + $under_attack->get_status()['duration'],
1546 ) );
1547 } else {
1548 wp_send_json_error( __( 'Failed to activate Under Attack mode.', 'vigilante' ) );
1549 }
1550 }
1551
1552 /**
1553 * AJAX: Deactivate Under Attack mode
1554 */
1555 public function ajax_deactivate_under_attack() {
1556 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1557
1558 if ( ! current_user_can( 'manage_options' ) ) {
1559 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1560 }
1561
1562 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1563
1564 if ( ! $under_attack->is_active() ) {
1565 wp_send_json_error( __( 'Under Attack mode is not active.', 'vigilante' ) );
1566 }
1567
1568 $result = $under_attack->deactivate( 'manual' );
1569
1570 if ( $result ) {
1571 wp_send_json_success( __( 'Under Attack mode deactivated.', 'vigilante' ) );
1572 } else {
1573 wp_send_json_error( __( 'Failed to deactivate Under Attack mode.', 'vigilante' ) );
1574 }
1575 }
1576
1577 /**
1578 * AJAX: Get Under Attack mode status
1579 */
1580 public function ajax_under_attack_status() {
1581 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1582
1583 if ( ! current_user_can( 'manage_options' ) ) {
1584 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1585 }
1586
1587 $under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log );
1588
1589 wp_send_json_success( array(
1590 'active' => $under_attack->is_active(),
1591 'remaining' => $under_attack->get_remaining_time(),
1592 ) );
1593 }
1594
1595 // =========================================================================
1596 // DATABASE BACKUP AJAX HANDLERS
1597 // =========================================================================
1598
1599 /**
1600 * AJAX: Get database tables list
1601 */
1602 public function ajax_get_db_tables() {
1603 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1604
1605 if ( ! current_user_can( 'manage_options' ) ) {
1606 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1607 }
1608
1609 $backup = new Vigilante_Database_Backup();
1610 $tables = $backup->get_tables();
1611
1612 wp_send_json_success( $tables );
1613 }
1614
1615 /**
1616 * AJAX: Download database backup
1617 *
1618 * Streams a ZIP file directly to the browser
1619 */
1620 public function ajax_download_db_backup() {
1621 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1622
1623 if ( ! current_user_can( 'manage_options' ) ) {
1624 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
1625 }
1626
1627 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
1628 $tables_raw = isset( $_POST['tables'] ) ? wp_unslash( $_POST['tables'] ) : '';
1629
1630 if ( empty( $tables_raw ) ) {
1631 wp_die( esc_html__( 'No tables selected.', 'vigilante' ), 400 );
1632 }
1633
1634 // Sanitize table names
1635 $tables = array_map( 'sanitize_key', explode( ',', $tables_raw ) );
1636 $tables = array_filter( $tables );
1637
1638 if ( empty( $tables ) ) {
1639 wp_die( esc_html__( 'No valid tables selected.', 'vigilante' ), 400 );
1640 }
1641
1642 $backup = new Vigilante_Database_Backup();
1643
1644 // Generate SQL dump
1645 $sql = $backup->generate_sql_dump( $tables );
1646 if ( is_wp_error( $sql ) ) {
1647 wp_die( esc_html( $sql->get_error_message() ), 500 );
1648 }
1649
1650 // Create ZIP
1651 $zip_path = $backup->create_zip( $sql );
1652 if ( is_wp_error( $zip_path ) ) {
1653 wp_die( esc_html( $zip_path->get_error_message() ), 500 );
1654 }
1655
1656 // Log the backup
1657 if ( $this->activity_log ) {
1658 $this->activity_log->log(
1659 'system',
1660 'database_backup',
1661 sprintf(
1662 /* translators: %d: Number of tables */
1663 __( 'Database backup created (%d tables)', 'vigilante' ),
1664 count( $tables )
1665 ),
1666 array( 'tables' => $tables ),
1667 'info'
1668 );
1669 }
1670
1671 // Stream download
1672 $backup->stream_download( $zip_path );
1673 }
1674
1675 // =========================================================================
1676 // DATABASE PREFIX AJAX HANDLERS
1677 // =========================================================================
1678
1679 /**
1680 * AJAX: Generate a new random prefix
1681 */
1682 public function ajax_generate_prefix() {
1683 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1684
1685 if ( ! current_user_can( 'manage_options' ) ) {
1686 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1687 }
1688
1689 $db_prefix = new Vigilante_Database_Prefix();
1690 $prefix = $db_prefix->generate_prefix();
1691
1692 wp_send_json_success( array( 'prefix' => $prefix ) );
1693 }
1694
1695 /**
1696 * AJAX: Change the database prefix
1697 */
1698 public function ajax_change_prefix() {
1699 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1700
1701 if ( ! current_user_can( 'manage_options' ) ) {
1702 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1703 }
1704
1705 $new_prefix = isset( $_POST['prefix'] ) ? sanitize_key( $_POST['prefix'] ) : '';
1706
1707 // Restore the underscore that sanitize_key might not strip but ensure it ends with one
1708 if ( ! empty( $new_prefix ) && substr( $new_prefix, -1 ) !== '_' ) {
1709 $new_prefix .= '_';
1710 }
1711
1712 if ( empty( $new_prefix ) ) {
1713 wp_send_json_error( __( 'Invalid prefix provided.', 'vigilante' ) );
1714 }
1715
1716 $db_prefix = new Vigilante_Database_Prefix();
1717
1718 // Validate first
1719 $valid = $db_prefix->validate_prefix( $new_prefix );
1720 if ( is_wp_error( $valid ) ) {
1721 wp_send_json_error( $valid->get_error_message() );
1722 }
1723
1724 // Log before changing (since after change, the log table will have new prefix)
1725 $old_prefix = $db_prefix->get_current_prefix();
1726
1727 // Execute the change
1728 $result = $db_prefix->change_prefix( $new_prefix );
1729
1730 if ( is_wp_error( $result ) ) {
1731 wp_send_json_error( $result->get_error_message() );
1732 }
1733
1734 // Log success (table has already been renamed, but the activity log object may still work for this request)
1735 if ( $this->activity_log ) {
1736 $this->activity_log->log(
1737 'system',
1738 'prefix_changed',
1739 sprintf(
1740 /* translators: 1: Old prefix, 2: New prefix */
1741 __( 'Database prefix changed from %1$s to %2$s', 'vigilante' ),
1742 $old_prefix,
1743 $new_prefix
1744 ),
1745 array(
1746 'old_prefix' => $old_prefix,
1747 'new_prefix' => $new_prefix,
1748 ),
1749 'warning'
1750 );
1751 }
1752
1753 wp_send_json_success( array(
1754 'message' => __( 'Database prefix changed successfully.', 'vigilante' ),
1755 'old_prefix' => $old_prefix,
1756 'new_prefix' => $new_prefix,
1757 ) );
1758 }
1759
1760 /**
1761 * AJAX: Unblock an IP from firewall rate limiting
1762 */
1763 public function ajax_unblock_firewall_ip() {
1764 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1765
1766 if ( ! current_user_can( 'manage_options' ) ) {
1767 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1768 }
1769
1770 $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
1771
1772 if ( empty( $ip ) ) {
1773 wp_send_json_error( __( 'No IP address provided.', 'vigilante' ) );
1774 }
1775
1776 $result = Vigilante_Firewall::unblock_ip( $ip );
1777
1778 if ( $result ) {
1779 // Log the manual unblock
1780 if ( $this->activity_log ) {
1781 $this->activity_log->log(
1782 'firewall',
1783 'unblocked',
1784 sprintf(
1785 /* translators: %s: IP address */
1786 __( 'IP %s manually unblocked from rate limiting', 'vigilante' ),
1787 $ip
1788 ),
1789 array( 'ip' => $ip ),
1790 'info'
1791 );
1792 }
1793 wp_send_json_success( sprintf(
1794 /* translators: %s: IP address */
1795 __( 'IP %s has been unblocked.', 'vigilante' ),
1796 $ip
1797 ) );
1798 } else {
1799 wp_send_json_error( __( 'IP not found in active blocks.', 'vigilante' ) );
1800 }
1801 }
1802
1803 }