| 1 |
<?php |
| 2 |
/** |
| 3 |
* Admin Class |
| 4 |
* |
| 5 |
* Handles admin interface and settings page |
| 6 |
* |
| 7 |
* @package Vigilante |
| 8 |
*/ |
| 9 |
|
| 10 |
// Prevent direct access |
| 11 |
if ( ! defined( 'ABSPATH' ) ) { |
| 12 |
exit; |
| 13 |
} |
| 14 |
|
| 15 |
// Load AJAX trait |
| 16 |
require_once VIGILANTE_ADMIN_DIR . 'class-admin-ajax.php'; |
| 17 |
|
| 18 |
// Load promotional banner class |
| 19 |
require_once VIGILANTE_INCLUDES_DIR . 'class-ayudawp-promo-banner.php'; |
| 20 |
|
| 21 |
/** |
| 22 |
* Class Vigilante_Admin |
| 23 |
* |
| 24 |
* Manages the admin settings interface |
| 25 |
*/ |
| 26 |
class Vigilante_Admin { |
| 27 |
|
| 28 |
use Vigilante_Admin_Ajax; |
| 29 |
use Vigilante_Admin_Analyzer_Ajax; |
| 30 |
use Vigilante_Admin_Audit_Alerts_Ajax; |
| 31 |
use Vigilante_Admin_Recovery_Ajax; |
| 32 |
|
| 33 |
/** |
| 34 |
* Settings instance |
| 35 |
* |
| 36 |
* @var Vigilante_Settings |
| 37 |
*/ |
| 38 |
private $settings; |
| 39 |
|
| 40 |
/** |
| 41 |
* Database instance |
| 42 |
* |
| 43 |
* @var Vigilante_Database |
| 44 |
*/ |
| 45 |
private $database; |
| 46 |
|
| 47 |
/** |
| 48 |
* Activity log instance |
| 49 |
* |
| 50 |
* @var Vigilante_Activity_Log |
| 51 |
*/ |
| 52 |
private $activity_log; |
| 53 |
|
| 54 |
/** |
| 55 |
* Current tab |
| 56 |
* |
| 57 |
* @var string |
| 58 |
*/ |
| 59 |
private $current_tab = 'dashboard'; |
| 60 |
|
| 61 |
/** |
| 62 |
* Available tabs |
| 63 |
* |
| 64 |
* @var array |
| 65 |
*/ |
| 66 |
private $tabs = array(); |
| 67 |
|
| 68 |
/** |
| 69 |
* Constructor |
| 70 |
* |
| 71 |
* @param Vigilante_Settings $settings Settings instance. |
| 72 |
* @param Vigilante_Database $database Database instance. |
| 73 |
* @param Vigilante_Activity_Log $activity_log Activity log instance. |
| 74 |
*/ |
| 75 |
public function __construct( $settings, $database, $activity_log ) { |
| 76 |
$this->settings = $settings; |
| 77 |
$this->database = $database; |
| 78 |
$this->activity_log = $activity_log; |
| 79 |
|
| 80 |
$this->setup_tabs(); |
| 81 |
$this->init_hooks(); |
| 82 |
} |
| 83 |
|
| 84 |
/** |
| 85 |
* Setup available tabs |
| 86 |
*/ |
| 87 |
private function setup_tabs() { |
| 88 |
$this->tabs = array( |
| 89 |
'dashboard' => __( 'Dashboard', 'vigilante' ), |
| 90 |
'firewall' => __( 'Firewall', 'vigilante' ), |
| 91 |
'headers' => __( 'Security Headers', 'vigilante' ), |
| 92 |
'login' => __( 'Login Security', 'vigilante' ), |
| 93 |
'rest-api' => __( 'REST API', 'vigilante' ), |
| 94 |
'users' => __( 'User Security', 'vigilante' ), |
| 95 |
'wp-hardening' => __( 'WP Hardening', 'vigilante' ), |
| 96 |
'file-integrity' => __( 'File Integrity', 'vigilante' ), |
| 97 |
'activity-log' => __( 'Security Audit', 'vigilante' ), |
| 98 |
'tools' => __( 'Settings & Tools', 'vigilante' ), |
| 99 |
); |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Initialize hooks |
| 104 |
*/ |
| 105 |
private function init_hooks() { |
| 106 |
add_action( 'admin_menu', array( $this, 'add_menu' ) ); |
| 107 |
add_action( 'admin_init', array( $this, 'redirect_submenu_shortcuts' ) ); |
| 108 |
add_action( 'admin_init', array( $this, 'register_settings' ) ); |
| 109 |
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); |
| 110 |
add_action( 'admin_notices', array( $this, 'show_admin_notices' ) ); |
| 111 |
|
| 112 |
// Highlight correct submenu based on active tab |
| 113 |
add_filter( 'submenu_file', array( $this, 'highlight_submenu_tab' ) ); |
| 114 |
|
| 115 |
// Set browser tab title to show plugin name and active tab |
| 116 |
add_filter( 'admin_title', array( $this, 'set_admin_page_title' ), 10, 2 ); |
| 117 |
|
| 118 |
// AJAX handlers |
| 119 |
add_action( 'wp_ajax_vigilante_save_settings', array( $this, 'ajax_save_settings' ) ); |
| 120 |
add_action( 'wp_ajax_vigilante_apply_preset', array( $this, 'ajax_apply_preset' ) ); |
| 121 |
add_action( 'wp_ajax_vigilante_reset_section', array( $this, 'ajax_reset_section' ) ); |
| 122 |
add_action( 'wp_ajax_vigilante_clear_lockouts', array( $this, 'ajax_clear_lockouts' ) ); |
| 123 |
add_action( 'wp_ajax_vigilante_clear_logs', array( $this, 'ajax_clear_logs' ) ); |
| 124 |
add_action( 'wp_ajax_vigilante_run_scan', array( $this, 'ajax_run_scan' ) ); |
| 125 |
add_action( 'wp_ajax_vigilante_clear_scan', array( $this, 'ajax_clear_scan' ) ); |
| 126 |
add_action( 'wp_ajax_vigilante_ignore_file', array( $this, 'ajax_ignore_file' ) ); |
| 127 |
add_action( 'wp_ajax_vigilante_unignore_file', array( $this, 'ajax_unignore_file' ) ); |
| 128 |
add_action( 'wp_ajax_vigilante_bulk_ignore_files', array( $this, 'ajax_bulk_ignore_files' ) ); |
| 129 |
add_action( 'wp_ajax_vigilante_bulk_unignore_files', array( $this, 'ajax_bulk_unignore_files' ) ); |
| 130 |
add_action( 'wp_ajax_vigilante_clear_ignored', array( $this, 'ajax_clear_ignored' ) ); |
| 131 |
add_action( 'wp_ajax_vigilante_ignore_closed_plugin', array( $this, 'ajax_ignore_closed_plugin' ) ); |
| 132 |
add_action( 'wp_ajax_vigilante_unignore_closed_plugin', array( $this, 'ajax_unignore_closed_plugin' ) ); |
| 133 |
add_action( 'wp_ajax_vigilante_clear_ignored_closed_plugins', array( $this, 'ajax_clear_ignored_closed_plugins' ) ); |
| 134 |
add_action( 'wp_ajax_vigilante_approve_critical_file', array( $this, 'ajax_approve_critical_file' ) ); |
| 135 |
add_action( 'wp_ajax_vigilante_export_settings', array( $this, 'ajax_export_settings' ) ); |
| 136 |
add_action( 'wp_ajax_vigilante_import_settings', array( $this, 'ajax_import_settings' ) ); |
| 137 |
add_action( 'wp_ajax_vigilante_get_logs', array( $this, 'ajax_get_logs' ) ); |
| 138 |
add_action( 'wp_ajax_vigilante_test_headers', array( $this, 'ajax_test_headers' ) ); |
| 139 |
add_action( 'wp_ajax_vigilante_download_files_backup', array( $this, 'ajax_download_files_backup' ) ); |
| 140 |
|
| 141 |
// 2FA AJAX handlers |
| 142 |
add_action( 'wp_ajax_vigilante_search_users_2fa', array( $this, 'ajax_search_users_2fa' ) ); |
| 143 |
add_action( 'wp_ajax_vigilante_send_2fa_notification', array( $this, 'ajax_send_2fa_notification' ) ); |
| 144 |
add_action( 'wp_ajax_vigilante_search_totp_users', array( $this, 'ajax_search_totp_users' ) ); |
| 145 |
add_action( 'wp_ajax_vigilante_reset_totp_users', array( $this, 'ajax_reset_totp_users' ) ); |
| 146 |
add_action( 'wp_ajax_vigilante_totp_get_setup', array( $this, 'ajax_totp_get_setup' ) ); |
| 147 |
add_action( 'wp_ajax_vigilante_notify_login_url', array( $this, 'ajax_notify_login_url' ) ); |
| 148 |
|
| 149 |
// Password Reset AJAX handlers |
| 150 |
add_action( 'wp_ajax_vigilante_search_users_password_reset', array( $this, 'ajax_search_users_password_reset' ) ); |
| 151 |
add_action( 'wp_ajax_vigilante_force_password_reset', array( $this, 'ajax_force_password_reset' ) ); |
| 152 |
add_action( 'wp_ajax_vigilante_force_password_reset_all', array( $this, 'ajax_force_password_reset_all' ) ); |
| 153 |
add_action( 'wp_ajax_vigilante_force_password_reset_by_role', array( $this, 'ajax_force_password_reset_by_role' ) ); |
| 154 |
|
| 155 |
// User approval AJAX handlers |
| 156 |
add_action( 'wp_ajax_vigilante_approve_user', array( $this, 'ajax_approve_user' ) ); |
| 157 |
add_action( 'wp_ajax_vigilante_reject_user', array( $this, 'ajax_reject_user' ) ); |
| 158 |
|
| 159 |
// Session management AJAX handlers |
| 160 |
add_action( 'wp_ajax_vigilante_get_user_sessions', array( $this, 'ajax_get_user_sessions' ) ); |
| 161 |
add_action( 'wp_ajax_vigilante_revoke_session', array( $this, 'ajax_revoke_session' ) ); |
| 162 |
add_action( 'wp_ajax_vigilante_revoke_all_sessions', array( $this, 'ajax_revoke_all_sessions' ) ); |
| 163 |
|
| 164 |
// Under Attack mode AJAX handlers |
| 165 |
add_action( 'wp_ajax_vigilante_activate_under_attack', array( $this, 'ajax_activate_under_attack' ) ); |
| 166 |
add_action( 'wp_ajax_vigilante_deactivate_under_attack', array( $this, 'ajax_deactivate_under_attack' ) ); |
| 167 |
add_action( 'wp_ajax_vigilante_under_attack_status', array( $this, 'ajax_under_attack_status' ) ); |
| 168 |
|
| 169 |
// Database backup AJAX handlers |
| 170 |
add_action( 'wp_ajax_vigilante_get_db_tables', array( $this, 'ajax_get_db_tables' ) ); |
| 171 |
add_action( 'wp_ajax_vigilante_download_db_backup', array( $this, 'ajax_download_db_backup' ) ); |
| 172 |
|
| 173 |
// Database prefix AJAX handlers |
| 174 |
add_action( 'wp_ajax_vigilante_generate_prefix', array( $this, 'ajax_generate_prefix' ) ); |
| 175 |
add_action( 'wp_ajax_vigilante_change_prefix', array( $this, 'ajax_change_prefix' ) ); |
| 176 |
|
| 177 |
// Firewall list management from activity log popup |
| 178 |
add_action( 'wp_ajax_vigilante_add_to_firewall_list', array( $this, 'ajax_add_to_firewall_list' ) ); |
| 179 |
add_action( 'wp_ajax_vigilante_unblock_firewall_ip', array( $this, 'ajax_unblock_firewall_ip' ) ); |
| 180 |
|
| 181 |
// Security Analyzer AJAX handlers (v2.1.0) |
| 182 |
add_action( 'wp_ajax_vigilante_analyzer_run', array( $this, 'ajax_analyzer_run' ) ); |
| 183 |
add_action( 'wp_ajax_vigilante_analyzer_history', array( $this, 'ajax_analyzer_history' ) ); |
| 184 |
add_action( 'wp_ajax_vigilante_analyzer_dismiss_notice', array( $this, 'ajax_analyzer_dismiss_notice' ) ); |
| 185 |
add_action( 'wp_ajax_vigilante_analyzer_save_settings', array( $this, 'ajax_analyzer_save_settings' ) ); |
| 186 |
|
| 187 |
// Security Headers settings recovery (2.10.0) |
| 188 |
add_action( 'wp_ajax_vigilante_headers_recovery_restore', array( $this, 'ajax_headers_recovery_restore' ) ); |
| 189 |
add_action( 'wp_ajax_vigilante_headers_recovery_undo', array( $this, 'ajax_headers_recovery_undo' ) ); |
| 190 |
add_action( 'wp_ajax_vigilante_headers_recovery_dismiss', array( $this, 'ajax_headers_recovery_dismiss' ) ); |
| 191 |
|
| 192 |
// Shared "Send test email" handler — Notification settings, File Integrity, Audit Alerts (v2.8.0) |
| 193 |
add_action( 'wp_ajax_vigilante_send_test_email', array( $this, 'ajax_send_test_email' ) ); |
| 194 |
|
| 195 |
// Run migrations on admin load |
| 196 |
add_action( 'admin_init', array( $this, 'run_migrations' ) ); |
| 197 |
} |
| 198 |
|
| 199 |
/** |
| 200 |
* Run database migrations based on stored version |
| 201 |
*/ |
| 202 |
public function run_migrations() { |
| 203 |
/* |
| 204 |
* admin-ajax.php fires admin_init before it decides who is asking |
| 205 |
* (wp-admin/admin-ajax.php:45), so until 2.11.10 an anonymous POST to |
| 206 |
* admin-ajax.php with any action ran every pending migration. That is |
| 207 |
* not a read: the migrations rewrite wp-config.php through |
| 208 |
* apply_security_constants(), rewrite the root .htaccess, move user meta |
| 209 |
* of the whole network and can rebuild the file integrity baseline, |
| 210 |
* taking whatever is on disk as approved. Reproduced on 12 sep 2026 with |
| 211 |
* curl and no cookies, and found by the file-by-file review of 2.11.10. |
| 212 |
* |
| 213 |
* Migrations are maintenance for whoever administers the site, so they |
| 214 |
* wait for an administrator to load a screen. Nothing is lost by |
| 215 |
* waiting: every migration is idempotent and version gated. |
| 216 |
*/ |
| 217 |
if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { |
| 218 |
return; |
| 219 |
} |
| 220 |
|
| 221 |
$db_version = get_option( 'vigilante_db_version', '0' ); |
| 222 |
|
| 223 |
// 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines |
| 224 |
if ( version_compare( $db_version, '1.2.3', '<' ) ) { |
| 225 |
$this->migrate_fix_ip_lists(); |
| 226 |
update_option( 'vigilante_db_version', '1.2.3' ); |
| 227 |
} |
| 228 |
|
| 229 |
// 1.3.0: Add request_method column to activity log table |
| 230 |
if ( version_compare( $db_version, '1.3.0', '<' ) ) { |
| 231 |
$this->database->run_migrations(); |
| 232 |
} |
| 233 |
|
| 234 |
// 1.9.0: Re-apply wp-config constants (performance constants removed from managed list) |
| 235 |
if ( version_compare( $db_version, '1.9.0', '<' ) ) { |
| 236 |
if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) { |
| 237 |
require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php'; |
| 238 |
$wpconfig = new Vigilante_Wpconfig_Security( $this->settings ); |
| 239 |
$wpconfig->apply_security_constants(); |
| 240 |
} |
| 241 |
update_option( 'vigilante_db_version', '1.9.0' ); |
| 242 |
} |
| 243 |
|
| 244 |
// 1.10.0: Clean up orphaned email fields (centralized notification recipients) |
| 245 |
if ( version_compare( $db_version, '1.10.0', '<' ) ) { |
| 246 |
$this->migrate_cleanup_email_fields(); |
| 247 |
update_option( 'vigilante_db_version', '1.10.0' ); |
| 248 |
} |
| 249 |
|
| 250 |
// 1.11.0: Remove stale 'enabled' key from activity_log settings |
| 251 |
// + Convert additional_recipients from string to array |
| 252 |
if ( version_compare( $db_version, '1.11.0', '<' ) ) { |
| 253 |
$options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 254 |
$changed = false; |
| 255 |
|
| 256 |
if ( isset( $options['activity_log']['enabled'] ) ) { |
| 257 |
unset( $options['activity_log']['enabled'] ); |
| 258 |
$changed = true; |
| 259 |
} |
| 260 |
|
| 261 |
// Convert corrupted string to array for additional_recipients |
| 262 |
if ( isset( $options['email']['additional_recipients'] ) && is_string( $options['email']['additional_recipients'] ) ) { |
| 263 |
$raw = trim( $options['email']['additional_recipients'] ); |
| 264 |
if ( ! empty( $raw ) ) { |
| 265 |
$emails = array_filter( array_map( 'trim', preg_split( '/[\r\n,; ]+/', $raw ) ) ); |
| 266 |
$options['email']['additional_recipients'] = array_values( array_filter( $emails, 'is_email' ) ); |
| 267 |
} else { |
| 268 |
$options['email']['additional_recipients'] = array(); |
| 269 |
} |
| 270 |
$changed = true; |
| 271 |
} |
| 272 |
|
| 273 |
if ( $changed ) { |
| 274 |
update_option( Vigilante_Settings::OPTION_NAME, $options ); |
| 275 |
} |
| 276 |
update_option( 'vigilante_db_version', '1.11.0' ); |
| 277 |
} |
| 278 |
|
| 279 |
// 1.12.1: Regenerate htaccess (WooCommerce IPN exclusion in bot blocking rule) |
| 280 |
if ( version_compare( $db_version, '1.12.1', '<' ) ) { |
| 281 |
if ( ! empty( $this->settings->get_section( 'firewall' )['block_bad_bots'] ) ) { |
| 282 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php'; |
| 283 |
$htaccess = new Vigilante_Htaccess_Protection( $this->settings ); |
| 284 |
$htaccess->apply_rules(); |
| 285 |
} |
| 286 |
update_option( 'vigilante_db_version', '1.12.1' ); |
| 287 |
} |
| 288 |
|
| 289 |
// 1.14.0: Generate critical config files baseline (wp-config.php, .htaccess) |
| 290 |
if ( version_compare( $db_version, '1.14.0', '<' ) ) { |
| 291 |
if ( ! class_exists( 'Vigilante_File_Integrity' ) ) { |
| 292 |
require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php'; |
| 293 |
} |
| 294 |
$fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); |
| 295 |
|
| 296 |
/* |
| 297 |
* Only when there is nothing on record. This migration exists to |
| 298 |
* create the baseline that did not exist, never to discard the one |
| 299 |
* the owner approved: rebuilding it from the files takes whatever |
| 300 |
* is on disk right now as approved, so a wp-config.php modified and |
| 301 |
* awaiting review would be blessed in silence. |
| 302 |
* |
| 303 |
* And this is not theory. vigilante_db_version is written on two |
| 304 |
* different scales into the same option: this file counts in plugin |
| 305 |
* versions (2.11.0) and Vigilante_Database counts in schema |
| 306 |
* versions, currently 1.4.0 (class-database.php:322 and :380). For |
| 307 |
* version_compare, 1.4.0 is LOWER than 1.14.0, so any site whose |
| 308 |
* option was last written by the schema runs this migration again. |
| 309 |
* Measured on the Multisite install on 10 sep 2026: one of the three |
| 310 |
* sites was sitting on 1.4.0. |
| 311 |
*/ |
| 312 |
if ( ! $fi->get_critical_files_baseline() ) { |
| 313 |
$fi->regenerate_all_baselines(); |
| 314 |
} |
| 315 |
|
| 316 |
update_option( 'vigilante_db_version', '1.14.0' ); |
| 317 |
} |
| 318 |
|
| 319 |
// 2.0.0: Move hide_server_signature and remove_fingerprinting_headers |
| 320 |
// from firewall section to security_headers section |
| 321 |
if ( version_compare( $db_version, '2.0.0', '<' ) ) { |
| 322 |
$options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 323 |
$changed = false; |
| 324 |
|
| 325 |
foreach ( array( 'hide_server_signature', 'remove_fingerprinting_headers' ) as $key ) { |
| 326 |
if ( isset( $options['firewall'][ $key ] ) ) { |
| 327 |
if ( ! isset( $options['security_headers'][ $key ] ) ) { |
| 328 |
$options['security_headers'][ $key ] = $options['firewall'][ $key ]; |
| 329 |
} |
| 330 |
unset( $options['firewall'][ $key ] ); |
| 331 |
$changed = true; |
| 332 |
} |
| 333 |
} |
| 334 |
|
| 335 |
if ( $changed ) { |
| 336 |
update_option( Vigilante_Settings::OPTION_NAME, $options ); |
| 337 |
} |
| 338 |
update_option( 'vigilante_db_version', '2.0.0' ); |
| 339 |
} |
| 340 |
|
| 341 |
// 2.6.1: Two things happen here. |
| 342 |
// |
| 343 |
// 1. Re-apply wp-config constants so the block is rewritten with |
| 344 |
// "if ( ! defined() )" guards around every define(). Without guards, |
| 345 |
// non-standard setups that pre-define WordPress constants outside |
| 346 |
// wp-config.php (custom bootstraps that load constants from .env or |
| 347 |
// similar) hit a fatal "Constant already defined" when wp-config.php |
| 348 |
// is parsed and reaches our block. |
| 349 |
// |
| 350 |
// 2. Drop the cached Security Check report. The cached "max" per |
| 351 |
// category was frozen at scan time; with the internal category |
| 352 |
// bumping from 22 to 28 points (closed_plugins added in 2.6.0), |
| 353 |
// the cached report would keep displaying 22/22 until the next |
| 354 |
// full scan. Clearing it forces a fresh scan with the new caps. |
| 355 |
if ( version_compare( $db_version, '2.6.1', '<' ) ) { |
| 356 |
if ( $this->settings->is_module_enabled( 'wp_hardening' ) ) { |
| 357 |
require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php'; |
| 358 |
$wpconfig = new Vigilante_Wpconfig_Security( $this->settings ); |
| 359 |
$wpconfig->apply_security_constants(); |
| 360 |
} |
| 361 |
delete_option( 'vigilante_analyzer_last_scan' ); |
| 362 |
|
| 363 |
// Schedule an immediate background scan so the dashboard widget |
| 364 |
// doesn't display "Last scan: never" right after the upgrade. |
| 365 |
// Reuses the same hook the post-Under-Attack flow uses. |
| 366 |
if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) { |
| 367 |
wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' ); |
| 368 |
} |
| 369 |
|
| 370 |
update_option( 'vigilante_db_version', '2.6.1' ); |
| 371 |
} |
| 372 |
|
| 373 |
// 2.9.3: Regenerate the .htaccess protection block. The bad-bots |
| 374 |
// User-Agent list dropped substring-prone tokens that 403'd |
| 375 |
// legitimate clients (e.g. "rma" matched inside "Performance" and |
| 376 |
// blocked WP Rocket's page fetch), and the blocking rules now honour |
| 377 |
// the firewall IP / User-Agent whitelists as negated exceptions. |
| 378 |
// Existing sites only rewrite the block when Server Protection is |
| 379 |
// saved, so the upgrade has to refresh it once itself (same pattern |
| 380 |
// as the 1.12.1 WooCommerce IPN migration). |
| 381 |
if ( version_compare( $db_version, '2.9.3', '<' ) ) { |
| 382 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php'; |
| 383 |
$htaccess = new Vigilante_Htaccess_Protection( $this->settings ); |
| 384 |
|
| 385 |
if ( $htaccess->are_rules_active() ) { |
| 386 |
$htaccess->apply_rules(); |
| 387 |
} |
| 388 |
|
| 389 |
update_option( 'vigilante_db_version', '2.9.3' ); |
| 390 |
} |
| 391 |
|
| 392 |
/* |
| 393 |
* 2.9.8: the mixed content handling changes shape. "Upgrade Insecure |
| 394 |
* Requests" becomes a setting of its own, and Fix Mixed Content ships |
| 395 |
* off, where before it shipped on and carried the directive with it. |
| 396 |
* Both have to be written down for sites that are updating, so their |
| 397 |
* pages keep loading exactly what they loaded yesterday. |
| 398 |
* |
| 399 |
* Read the RAW stored options, not get_section(): that one merges the |
| 400 |
* defaults, so a site that never stored the key would be read with the |
| 401 |
* new default and silently lose the behaviour it had. Absent means the |
| 402 |
* site was running on the old default, which was on. |
| 403 |
*/ |
| 404 |
if ( version_compare( $db_version, '2.9.8', '<' ) ) { |
| 405 |
$raw = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 406 |
$stored = ( is_array( $raw ) && isset( $raw['security_headers'] ) && is_array( $raw['security_headers'] ) ) ? $raw['security_headers'] : array(); |
| 407 |
$had_fix = array_key_exists( 'fix_mixed_content', $stored ) ? ! empty( $stored['fix_mixed_content'] ) : true; |
| 408 |
|
| 409 |
/* |
| 410 |
* Merge, never replace. update_section() overwrites the whole |
| 411 |
* section, so passing just these two keys wiped every other header |
| 412 |
* setting the site had stored (HSTS, CSP, cross-origin policies, |
| 413 |
* the HTTPS switches, Server Identity) and left the screen showing |
| 414 |
* factory defaults while the .htaccess kept serving the old values. |
| 415 |
*/ |
| 416 |
$this->settings->update_section( |
| 417 |
'security_headers', |
| 418 |
array_merge( |
| 419 |
$stored, |
| 420 |
array( |
| 421 |
'fix_mixed_content' => $had_fix, |
| 422 |
'upgrade_insecure_requests' => $had_fix, |
| 423 |
) |
| 424 |
) |
| 425 |
); |
| 426 |
|
| 427 |
update_option( 'vigilante_db_version', '2.9.8' ); |
| 428 |
} |
| 429 |
|
| 430 |
/* |
| 431 |
* 2.9.9: drop the settings that no code has read for versions. |
| 432 |
* |
| 433 |
* They were carried in the defaults and therefore written into every |
| 434 |
* saved configuration, they show up in an exported configuration, and |
| 435 |
* anyone reading them assumes a feature exists behind them. Removing |
| 436 |
* them from the defaults is not enough: the stored copies survive, so |
| 437 |
* they are swept here too. Nothing reads them, so nothing changes. |
| 438 |
*/ |
| 439 |
if ( version_compare( $db_version, '2.9.9', '<' ) ) { |
| 440 |
$raw = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 441 |
$dead = array( |
| 442 |
'firewall' => array( 'country_blocking', 'protected_file_extensions' ), |
| 443 |
'file_integrity' => array( 'suspicious_patterns' ), |
| 444 |
'backup' => array( 'auto_backup', 'backup_before_update' ), |
| 445 |
'advanced' => array( 'block_author_archives', 'disable_embeds', 'uninstall_cleanup', 'debug_mode' ), |
| 446 |
); |
| 447 |
|
| 448 |
$changed = false; |
| 449 |
foreach ( $dead as $section => $keys ) { |
| 450 |
if ( ! isset( $raw[ $section ] ) || ! is_array( $raw[ $section ] ) ) { |
| 451 |
continue; |
| 452 |
} |
| 453 |
foreach ( $keys as $key ) { |
| 454 |
if ( array_key_exists( $key, $raw[ $section ] ) ) { |
| 455 |
unset( $raw[ $section ][ $key ] ); |
| 456 |
$changed = true; |
| 457 |
} |
| 458 |
} |
| 459 |
} |
| 460 |
|
| 461 |
if ( $changed ) { |
| 462 |
update_option( Vigilante_Settings::OPTION_NAME, $raw ); |
| 463 |
} |
| 464 |
|
| 465 |
update_option( 'vigilante_db_version', '2.9.9' ); |
| 466 |
} |
| 467 |
|
| 468 |
/* |
| 469 |
* 2.11.0: security release (audit of 28 Aug 2026). Runs here and not |
| 470 |
* from Vigilante_Database::needs_update(): this option is shared with |
| 471 |
* that class, and on any updated site it already holds a plugin version |
| 472 |
* (2.9.9 or later), so a bump of DB_VERSION would never fire. |
| 473 |
* create_tables() widens the email code column through dbDelta (varchar |
| 474 |
* 6 to 64, the code is stored hashed since 2.11.0) and purge_for_2_11_0() |
| 475 |
* does what dbDelta cannot: it empties the trusted devices, which were |
| 476 |
* identified by User-Agent until now (S1), and the pending email codes, |
| 477 |
* stored in clear until now (S11). Every remembered device asks for the |
| 478 |
* second factor once more after this update, and the changelog says so. |
| 479 |
*/ |
| 480 |
if ( version_compare( $db_version, '2.11.0', '<' ) ) { |
| 481 |
$this->database->create_tables(); |
| 482 |
$this->database->purge_for_2_11_0(); |
| 483 |
|
| 484 |
update_option( 'vigilante_db_version', '2.11.0' ); |
| 485 |
} |
| 486 |
|
| 487 |
/* |
| 488 |
* 2.11.9: clear the raw .htaccess copies that older versions left in |
| 489 |
* options, on the first admin load after the update. Uninstall already |
| 490 |
* removes them, but that only fires when the plugin is deleted, so a |
| 491 |
* site that keeps the plugin carried them until now. Three stores, each |
| 492 |
* a copy of a file that can hold secrets (a SetEnv token, an |
| 493 |
* Authorization header): the same exposure the wp.org review flagged as |
| 494 |
* 4.4, on the paths its fix did not reach. |
| 495 |
* |
| 496 |
* - vigilante_htaccess_history: up to five raw copies, by design, until |
| 497 |
* 2.11.8. The writer is gone, nothing reads it, so it is deleted. |
| 498 |
* - vigilante_htaccess_backup: the single rollback buffer, normally |
| 499 |
* cleared in the finally of each write; a copy only lingers if a write |
| 500 |
* crashed mid-operation. Nothing outside one write reads it, so a |
| 501 |
* leftover is deleted. |
| 502 |
* - vigilante_htaccess_pre_migration: still read by the header recovery, |
| 503 |
* but older versions stored the whole file where only our own block is |
| 504 |
* ever used. Truncated to that block, so the feature keeps working and |
| 505 |
* nothing outside our markers stays in the option. |
| 506 |
*/ |
| 507 |
if ( version_compare( $db_version, '2.11.9', '<' ) ) { |
| 508 |
delete_option( 'vigilante_htaccess_history' ); |
| 509 |
delete_option( 'vigilante_htaccess_backup' ); |
| 510 |
|
| 511 |
$snapshot = get_option( 'vigilante_htaccess_pre_migration' ); |
| 512 |
if ( is_array( $snapshot ) && isset( $snapshot['content'] ) && '' !== (string) $snapshot['content'] ) { |
| 513 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php'; |
| 514 |
$block = Vigilante_Htaccess_Recovery::get_raw_block(); |
| 515 |
|
| 516 |
if ( '' === $block ) { |
| 517 |
delete_option( 'vigilante_htaccess_pre_migration' ); |
| 518 |
} elseif ( $block !== $snapshot['content'] ) { |
| 519 |
$snapshot['content'] = $block; |
| 520 |
update_option( 'vigilante_htaccess_pre_migration', $snapshot, false ); |
| 521 |
} |
| 522 |
} |
| 523 |
|
| 524 |
update_option( 'vigilante_db_version', '2.11.9' ); |
| 525 |
} |
| 526 |
|
| 527 |
/* |
| 528 |
* 2.11.10: the pending-approval flag becomes one per site on a network. |
| 529 |
* Until 2.11.9 it was a single global user meta, so the queue was shared |
| 530 |
* across the whole network. Moving the key is not enough: the accounts |
| 531 |
* already waiting carry the old key, and reading only the new one would |
| 532 |
* let them log in. So they are moved here, each to the site it belongs |
| 533 |
* to, and the old key is removed only once the new one is written. |
| 534 |
*/ |
| 535 |
if ( version_compare( $db_version, '2.11.10', '<' ) ) { |
| 536 |
$this->migrate_pending_approval_per_site(); |
| 537 |
|
| 538 |
update_option( 'vigilante_db_version', '2.11.10' ); |
| 539 |
} |
| 540 |
} |
| 541 |
|
| 542 |
/** |
| 543 |
* Move the pending-approval flag of a network to a key per site |
| 544 |
* |
| 545 |
* Runs once for the whole network, not once per site: the data it moves is |
| 546 |
* global, so the guard is a network option and any site may be the one that |
| 547 |
* does it. On a single site the key does not change and there is nothing to |
| 548 |
* do. |
| 549 |
* |
| 550 |
* Each waiting account goes to its primary site, or to the only site it |
| 551 |
* belongs to; one that belongs to none goes to the main site rather than |
| 552 |
* nowhere, because losing the flag would silently approve it. |
| 553 |
* |
| 554 |
* @since 2.11.10 |
| 555 |
*/ |
| 556 |
private function migrate_pending_approval_per_site() { |
| 557 |
global $wpdb; |
| 558 |
|
| 559 |
if ( ! is_multisite() ) { |
| 560 |
return; |
| 561 |
} |
| 562 |
|
| 563 |
if ( get_site_option( 'vigilante_pending_per_site_done' ) ) { |
| 564 |
return; |
| 565 |
} |
| 566 |
|
| 567 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off migration of the plugin's own user meta; the meta API has no "list every user with this key". |
| 568 |
$user_ids = $wpdb->get_col( |
| 569 |
$wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", 'vigilante_pending_approval' ) |
| 570 |
); |
| 571 |
|
| 572 |
foreach ( (array) $user_ids as $user_id ) { |
| 573 |
$user_id = (int) $user_id; |
| 574 |
if ( ! $user_id ) { |
| 575 |
continue; |
| 576 |
} |
| 577 |
|
| 578 |
$pending = get_user_meta( $user_id, 'vigilante_pending_approval', true ); |
| 579 |
$since = get_user_meta( $user_id, 'vigilante_pending_since', true ); |
| 580 |
|
| 581 |
/* |
| 582 |
* Every site the account belongs to, not its primary one. The global |
| 583 |
* flag does not say where the registration happened, and the first |
| 584 |
* version of this guessed the primary blog: an account that |
| 585 |
* registered on B while its primary was A came out pending on A and |
| 586 |
* free to log in on B, which is the very site it had never been |
| 587 |
* approved on. Found by the cross review of 2.11.10. |
| 588 |
* |
| 589 |
* Marking every site it belongs to fails closed instead: the account |
| 590 |
* stays blocked wherever it can log in, and shows up in the queue of |
| 591 |
* each of those sites so somebody can actually act on it. An account |
| 592 |
* that belongs to no site goes to the main one rather than nowhere, |
| 593 |
* because losing the flag would silently approve it. |
| 594 |
*/ |
| 595 |
/* |
| 596 |
* With $all true, because the default leaves out archived, spam and |
| 597 |
* deleted sites (wp-includes/user.php:1113-1117): a site archived on |
| 598 |
* the day this runs would lose the flag, and the account would walk |
| 599 |
* in unapproved the moment it was brought back. Found by the second |
| 600 |
* cross review of 2.11.10. |
| 601 |
*/ |
| 602 |
$blog_ids = array(); |
| 603 |
|
| 604 |
foreach ( get_blogs_of_user( $user_id, true ) as $blog ) { |
| 605 |
if ( ! empty( $blog->userblog_id ) ) { |
| 606 |
$blog_ids[] = (int) $blog->userblog_id; |
| 607 |
} |
| 608 |
} |
| 609 |
|
| 610 |
if ( empty( $blog_ids ) ) { |
| 611 |
$blog_ids[] = (int) get_main_site_id(); |
| 612 |
} |
| 613 |
|
| 614 |
foreach ( array_unique( $blog_ids ) as $blog_id ) { |
| 615 |
$prefix = $wpdb->get_blog_prefix( $blog_id ); |
| 616 |
|
| 617 |
update_user_meta( $user_id, $prefix . 'vigilante_pending_approval', $pending ); |
| 618 |
if ( '' !== $since && false !== $since ) { |
| 619 |
update_user_meta( $user_id, $prefix . 'vigilante_pending_since', $since ); |
| 620 |
} |
| 621 |
} |
| 622 |
|
| 623 |
delete_user_meta( $user_id, 'vigilante_pending_approval' ); |
| 624 |
delete_user_meta( $user_id, 'vigilante_pending_since' ); |
| 625 |
} |
| 626 |
|
| 627 |
update_site_option( 'vigilante_pending_per_site_done', 1 ); |
| 628 |
} |
| 629 |
|
| 630 |
/** |
| 631 |
* Migration: Remove orphaned email fields from saved options |
| 632 |
* |
| 633 |
* v1.10.0 centralized notification recipients into email section. |
| 634 |
* Old per-module notify_email fields and dead email section fields |
| 635 |
* are removed to avoid confusion. |
| 636 |
*/ |
| 637 |
private function migrate_cleanup_email_fields() { |
| 638 |
$options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 639 |
$modified = false; |
| 640 |
|
| 641 |
// Remove orphaned fields from email section |
| 642 |
$dead_email_keys = array( 'enabled', 'from_name', 'from_email', 'admin_email', 'send_activation_email', 'custom_email' ); |
| 643 |
if ( isset( $options['email'] ) && is_array( $options['email'] ) ) { |
| 644 |
foreach ( $dead_email_keys as $key ) { |
| 645 |
if ( array_key_exists( $key, $options['email'] ) ) { |
| 646 |
unset( $options['email'][ $key ] ); |
| 647 |
$modified = true; |
| 648 |
} |
| 649 |
} |
| 650 |
// Ensure new fields exist with defaults |
| 651 |
if ( ! array_key_exists( 'send_to_admin_email', $options['email'] ) ) { |
| 652 |
$options['email']['send_to_admin_email'] = true; |
| 653 |
$modified = true; |
| 654 |
} |
| 655 |
if ( ! array_key_exists( 'additional_recipients', $options['email'] ) ) { |
| 656 |
$options['email']['additional_recipients'] = ''; |
| 657 |
$modified = true; |
| 658 |
} |
| 659 |
} |
| 660 |
|
| 661 |
// Remove notify_email from login_security |
| 662 |
if ( isset( $options['login_security']['notify_email'] ) ) { |
| 663 |
unset( $options['login_security']['notify_email'] ); |
| 664 |
$modified = true; |
| 665 |
} |
| 666 |
|
| 667 |
// Remove notify_email from file_integrity |
| 668 |
if ( isset( $options['file_integrity']['notify_email'] ) ) { |
| 669 |
unset( $options['file_integrity']['notify_email'] ); |
| 670 |
$modified = true; |
| 671 |
} |
| 672 |
|
| 673 |
if ( $modified ) { |
| 674 |
update_option( Vigilante_Settings::OPTION_NAME, $options ); |
| 675 |
// Clear settings cache so the plugin uses clean data immediately |
| 676 |
$this->settings->clear_cache(); |
| 677 |
} |
| 678 |
} |
| 679 |
|
| 680 |
/** |
| 681 |
* Migration: Fix IP whitelist/blacklist entries merged into single line |
| 682 |
* |
| 683 |
* Prior to 1.2.3, sanitize_text_field() stripped newlines from textarea data, |
| 684 |
* causing multiple IPs to be stored as a single space-separated string. |
| 685 |
*/ |
| 686 |
private function migrate_fix_ip_lists() { |
| 687 |
$options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 688 |
$fixed = false; |
| 689 |
|
| 690 |
foreach ( array( 'ip_whitelist', 'ip_blacklist' ) as $key ) { |
| 691 |
if ( ! empty( $options['firewall'][ $key ] ) && is_array( $options['firewall'][ $key ] ) ) { |
| 692 |
$new_list = array(); |
| 693 |
foreach ( $options['firewall'][ $key ] as $entry ) { |
| 694 |
// Split entries that were joined by spaces |
| 695 |
$parts = preg_split( '/\s+/', trim( $entry ) ); |
| 696 |
foreach ( $parts as $part ) { |
| 697 |
$part = trim( $part ); |
| 698 |
if ( '' !== $part ) { |
| 699 |
$new_list[] = $part; |
| 700 |
} |
| 701 |
} |
| 702 |
} |
| 703 |
if ( count( $new_list ) !== count( $options['firewall'][ $key ] ) ) { |
| 704 |
$options['firewall'][ $key ] = array_unique( $new_list ); |
| 705 |
$fixed = true; |
| 706 |
} |
| 707 |
} |
| 708 |
} |
| 709 |
|
| 710 |
if ( $fixed ) { |
| 711 |
update_option( Vigilante_Settings::OPTION_NAME, $options ); |
| 712 |
// Clear settings cache so changes take effect immediately |
| 713 |
$this->settings->clear_cache(); |
| 714 |
} |
| 715 |
} |
| 716 |
|
| 717 |
/** |
| 718 |
* Add admin menu page in last position |
| 719 |
*/ |
| 720 |
public function add_menu() { |
| 721 |
$menu_title = __( 'Vigilant', 'vigilante' ); |
| 722 |
|
| 723 |
// Count pending approvals (separate concern, always red if present) |
| 724 |
$pending_count = $this->get_pending_approvals_count(); |
| 725 |
|
| 726 |
// Get security issues with severity |
| 727 |
$security_status = $this->get_security_status_for_badge(); |
| 728 |
|
| 729 |
// Total count for badge |
| 730 |
$total_badge = $pending_count + $security_status['count']; |
| 731 |
|
| 732 |
if ( $total_badge > 0 ) { |
| 733 |
// Determine badge color: |
| 734 |
// - Red (awaiting-mod): pending approvals OR critical modules disabled |
| 735 |
// - Orange (update-plugins): only non-critical modules disabled |
| 736 |
if ( $pending_count > 0 || $security_status['has_critical'] ) { |
| 737 |
$badge_class = 'awaiting-mod'; |
| 738 |
} else { |
| 739 |
$badge_class = 'update-plugins vigilante-badge-warning'; |
| 740 |
} |
| 741 |
|
| 742 |
$menu_title .= sprintf( |
| 743 |
' <span class="%s count-%d"><span class="pending-count">%d</span></span>', |
| 744 |
esc_attr( $badge_class ), |
| 745 |
$total_badge, |
| 746 |
$total_badge |
| 747 |
); |
| 748 |
} |
| 749 |
|
| 750 |
add_menu_page( |
| 751 |
__( 'Vigilant', 'vigilante' ), |
| 752 |
$menu_title, |
| 753 |
'manage_options', |
| 754 |
'vigilante', |
| 755 |
array( $this, 'render_settings_page' ), |
| 756 |
'dashicons-shield', |
| 757 |
999 |
| 758 |
); |
| 759 |
|
| 760 |
// Rename auto-generated first submenu to "Dashboard" |
| 761 |
add_submenu_page( |
| 762 |
'vigilante', |
| 763 |
__( 'Dashboard', 'vigilante' ), |
| 764 |
__( 'Dashboard', 'vigilante' ), |
| 765 |
'manage_options', |
| 766 |
'vigilante', |
| 767 |
array( $this, 'render_settings_page' ) |
| 768 |
); |
| 769 |
|
| 770 |
// Security Audit shortcut |
| 771 |
add_submenu_page( |
| 772 |
'vigilante', |
| 773 |
__( 'Security Audit', 'vigilante' ), |
| 774 |
__( 'Security Audit', 'vigilante' ), |
| 775 |
'manage_options', |
| 776 |
'vigilante-activity-log', |
| 777 |
array( $this, 'redirect_to_tab' ) |
| 778 |
); |
| 779 |
|
| 780 |
// File Integrity shortcut |
| 781 |
add_submenu_page( |
| 782 |
'vigilante', |
| 783 |
__( 'File Integrity', 'vigilante' ), |
| 784 |
__( 'File Integrity', 'vigilante' ), |
| 785 |
'manage_options', |
| 786 |
'vigilante-file-integrity', |
| 787 |
array( $this, 'redirect_to_tab' ) |
| 788 |
); |
| 789 |
} |
| 790 |
|
| 791 |
/** |
| 792 |
* Redirect submenu shortcuts early, before headers are sent |
| 793 |
*/ |
| 794 |
public function redirect_submenu_shortcuts() { |
| 795 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect |
| 796 |
$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; |
| 797 |
|
| 798 |
$tab_map = array( |
| 799 |
'vigilante-activity-log' => 'activity-log', |
| 800 |
'vigilante-file-integrity' => 'file-integrity', |
| 801 |
); |
| 802 |
|
| 803 |
if ( isset( $tab_map[ $page ] ) ) { |
| 804 |
wp_safe_redirect( admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ) ); |
| 805 |
exit; |
| 806 |
} |
| 807 |
} |
| 808 |
|
| 809 |
/** |
| 810 |
* Fallback redirect for submenu shortcuts (JS-based) |
| 811 |
*/ |
| 812 |
public function redirect_to_tab() { |
| 813 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just reading page slug for redirect |
| 814 |
$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; |
| 815 |
|
| 816 |
$tab_map = array( |
| 817 |
'vigilante-activity-log' => 'activity-log', |
| 818 |
'vigilante-file-integrity' => 'file-integrity', |
| 819 |
); |
| 820 |
|
| 821 |
if ( isset( $tab_map[ $page ] ) ) { |
| 822 |
$url = admin_url( 'admin.php?page=vigilante&tab=' . $tab_map[ $page ] ); |
| 823 |
echo '<script>window.location.replace(' . wp_json_encode( esc_url( $url ) ) . ');</script>'; |
| 824 |
} |
| 825 |
} |
| 826 |
|
| 827 |
/** |
| 828 |
* Highlight the correct submenu item based on active tab |
| 829 |
* |
| 830 |
* @param string $submenu_file Current submenu file. |
| 831 |
* @return string |
| 832 |
*/ |
| 833 |
public function highlight_submenu_tab( $submenu_file ) { |
| 834 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab for menu highlight only |
| 835 |
$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; |
| 836 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 837 |
$tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : ''; |
| 838 |
|
| 839 |
if ( 'vigilante' !== $page || empty( $tab ) ) { |
| 840 |
return $submenu_file; |
| 841 |
} |
| 842 |
|
| 843 |
$tab_to_submenu = array( |
| 844 |
'activity-log' => 'vigilante-activity-log', |
| 845 |
'file-integrity' => 'vigilante-file-integrity', |
| 846 |
); |
| 847 |
|
| 848 |
if ( isset( $tab_to_submenu[ $tab ] ) ) { |
| 849 |
return $tab_to_submenu[ $tab ]; |
| 850 |
} |
| 851 |
|
| 852 |
return $submenu_file; |
| 853 |
} |
| 854 |
|
| 855 |
/** |
| 856 |
* Set browser tab title to show plugin name and active tab |
| 857 |
* |
| 858 |
* Changes "Dashboard ‹ Site Name — WordPress" to |
| 859 |
* "Vigilant > Dashboard ‹ Site Name — WordPress" |
| 860 |
* |
| 861 |
* @param string $admin_title Full admin title. |
| 862 |
* @param string $title Page title from add_menu_page/add_submenu_page. |
| 863 |
* @return string Modified title. |
| 864 |
*/ |
| 865 |
public function set_admin_page_title( $admin_title, $title ) { |
| 866 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug for title only |
| 867 |
$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; |
| 868 |
|
| 869 |
// Only modify on Vigilante pages |
| 870 |
if ( 0 !== strpos( $page, 'vigilante' ) ) { |
| 871 |
return $admin_title; |
| 872 |
} |
| 873 |
|
| 874 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 875 |
$tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard'; |
| 876 |
|
| 877 |
if ( isset( $this->tabs[ $tab ] ) ) { |
| 878 |
$tab_label = $this->tabs[ $tab ]; |
| 879 |
} else { |
| 880 |
$tab_label = __( 'Dashboard', 'vigilante' ); |
| 881 |
} |
| 882 |
|
| 883 |
$plugin_title = __( 'Vigilant', 'vigilante' ) . ' › ' . $tab_label; |
| 884 |
|
| 885 |
// Replace the original page title portion |
| 886 |
return str_replace( $title, $plugin_title, $admin_title ); |
| 887 |
} |
| 888 |
|
| 889 |
/** |
| 890 |
* Get count of users pending approval |
| 891 |
* |
| 892 |
* @return int Count of pending users. |
| 893 |
*/ |
| 894 |
private function get_pending_approvals_count() { |
| 895 |
// Prevent early execution before WordPress is ready |
| 896 |
if ( ! did_action( 'plugins_loaded' ) ) { |
| 897 |
return 0; |
| 898 |
} |
| 899 |
|
| 900 |
/* |
| 901 |
* Counted whether the feature is on or off. An account already waiting |
| 902 |
* stays blocked when it is switched off (see init_enforcement_hooks()), |
| 903 |
* so reporting zero there hid people who cannot log in and whom nobody |
| 904 |
* could see to approve. Found by the cross review of 2.11.10. |
| 905 |
*/ |
| 906 |
|
| 907 |
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context. |
| 908 |
$args = array( |
| 909 |
'meta_key' => Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_approval' ), |
| 910 |
'meta_value' => '1', |
| 911 |
'fields' => 'ID', |
| 912 |
); |
| 913 |
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value |
| 914 |
|
| 915 |
// Same query as Vigilante_User_Security::get_pending_users(), and for the |
| 916 |
// same reason: the meta key already scopes this to the site, and adding |
| 917 |
// core's membership filter on top hid the accounts that have no role yet. |
| 918 |
if ( is_multisite() ) { |
| 919 |
$args['blog_id'] = 0; |
| 920 |
} |
| 921 |
|
| 922 |
$pending_users = get_users( $args ); |
| 923 |
|
| 924 |
return count( $pending_users ); |
| 925 |
} |
| 926 |
|
| 927 |
/** |
| 928 |
* Calculate comprehensive security score (0-100) |
| 929 |
* |
| 930 |
* @param array $options Plugin options. |
| 931 |
* @return int Security score. |
| 932 |
*/ |
| 933 |
private function calculate_security_score( $options ) { |
| 934 |
$score = 0; |
| 935 |
$max_score = 0; |
| 936 |
|
| 937 |
// Module scores (60 points total) |
| 938 |
$module_weights = array( |
| 939 |
'firewall' => 10, |
| 940 |
'security_headers' => 8, |
| 941 |
'login_security' => 10, |
| 942 |
'rest_api_security' => 6, |
| 943 |
'user_security' => 8, |
| 944 |
'wp_hardening' => 8, |
| 945 |
'file_integrity' => 5, |
| 946 |
'activity_log' => 5, |
| 947 |
); |
| 948 |
|
| 949 |
foreach ( $module_weights as $module => $weight ) { |
| 950 |
$max_score += $weight; |
| 951 |
if ( ! empty( $options['modules'][ $module ] ) ) { |
| 952 |
$score += $weight; |
| 953 |
} |
| 954 |
} |
| 955 |
|
| 956 |
// Firewall details (10 points) |
| 957 |
if ( ! empty( $options['modules']['firewall'] ) ) { |
| 958 |
$firewall = $options['firewall'] ?? array(); |
| 959 |
$max_score += 10; |
| 960 |
|
| 961 |
$firewall_checks = array( |
| 962 |
'block_sql_injection', |
| 963 |
'block_xss_attacks', |
| 964 |
'block_bad_query_strings', |
| 965 |
'block_file_inclusion', |
| 966 |
'block_directory_traversal', |
| 967 |
); |
| 968 |
|
| 969 |
$firewall_enabled = 0; |
| 970 |
foreach ( $firewall_checks as $check ) { |
| 971 |
if ( ! empty( $firewall[ $check ] ) ) { |
| 972 |
$firewall_enabled++; |
| 973 |
} |
| 974 |
} |
| 975 |
$score += min( 10, $firewall_enabled * 2 ); |
| 976 |
} |
| 977 |
|
| 978 |
// Login security details (10 points) |
| 979 |
if ( ! empty( $options['modules']['login_security'] ) ) { |
| 980 |
$login = $options['login_security'] ?? array(); |
| 981 |
$max_score += 10; |
| 982 |
|
| 983 |
// Max attempts configured |
| 984 |
if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) { |
| 985 |
$score += 3; |
| 986 |
} |
| 987 |
// XML-RPC disabled |
| 988 |
if ( ! empty( $login['disable_xmlrpc'] ) ) { |
| 989 |
$score += 3; |
| 990 |
} |
| 991 |
// 2FA enabled |
| 992 |
if ( ! empty( $login['two_factor']['enabled'] ) ) { |
| 993 |
$score += 4; |
| 994 |
} |
| 995 |
} |
| 996 |
|
| 997 |
// Security headers details (10 points) |
| 998 |
if ( ! empty( $options['modules']['security_headers'] ) ) { |
| 999 |
$headers = $options['security_headers'] ?? array(); |
| 1000 |
$max_score += 10; |
| 1001 |
|
| 1002 |
if ( ! empty( $headers['x_frame_options'] ) ) { |
| 1003 |
$score += 2; |
| 1004 |
} |
| 1005 |
if ( ! empty( $headers['x_content_type_options'] ) ) { |
| 1006 |
$score += 2; |
| 1007 |
} |
| 1008 |
if ( ! empty( $headers['hsts']['enabled'] ) ) { |
| 1009 |
$score += 3; |
| 1010 |
} |
| 1011 |
if ( ! empty( $headers['csp']['enabled'] ) ) { |
| 1012 |
$score += 3; |
| 1013 |
} |
| 1014 |
} |
| 1015 |
|
| 1016 |
// User security details (10 points) |
| 1017 |
if ( ! empty( $options['modules']['user_security'] ) ) { |
| 1018 |
$user = $options['user_security'] ?? array(); |
| 1019 |
$max_score += 10; |
| 1020 |
|
| 1021 |
if ( ! empty( $user['block_insecure_usernames'] ) ) { |
| 1022 |
$score += 3; |
| 1023 |
} |
| 1024 |
if ( ! empty( $user['force_strong_passwords'] ) ) { |
| 1025 |
$score += 3; |
| 1026 |
} |
| 1027 |
if ( ! empty( $user['password_expiration']['enabled'] ) ) { |
| 1028 |
$score += 2; |
| 1029 |
} |
| 1030 |
if ( ! empty( $user['email_verification']['enabled'] ) ) { |
| 1031 |
$score += 2; |
| 1032 |
} |
| 1033 |
} |
| 1034 |
|
| 1035 |
// Audit alerts details (6 points) - only when Security Audit is on, |
| 1036 |
// because the alerting layer rides on top of the activity log. Leaving |
| 1037 |
// both alert legs off keeps these points unearned. |
| 1038 |
if ( ! empty( $options['modules']['activity_log'] ) ) { |
| 1039 |
$alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array(); |
| 1040 |
$max_score += 6; |
| 1041 |
if ( Vigilante_Audit_Alerts::immediate_is_active( $alerts ) ) { |
| 1042 |
$score += 3; |
| 1043 |
} |
| 1044 |
if ( Vigilante_Audit_Alerts::threshold_is_active( $alerts ) ) { |
| 1045 |
$score += 3; |
| 1046 |
} |
| 1047 |
} |
| 1048 |
|
| 1049 |
// Environment checks (8 points) - penalize insecure server configuration |
| 1050 |
$max_score += 8; |
| 1051 |
$env_score = 8; |
| 1052 |
|
| 1053 |
// WP_DEBUG active in production is a security risk (exposes paths, errors) |
| 1054 |
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { |
| 1055 |
$env_score -= 5; |
| 1056 |
} |
| 1057 |
|
| 1058 |
// Accounts with insecure usernames (targeted by brute force attacks) |
| 1059 |
$insecure_admins = $this->get_insecure_admin_usernames(); |
| 1060 |
if ( ! empty( $insecure_admins ) ) { |
| 1061 |
$env_score -= 3; |
| 1062 |
} |
| 1063 |
|
| 1064 |
$score += max( 0, $env_score ); |
| 1065 |
|
| 1066 |
return $max_score > 0 ? round( ( $score / $max_score ) * 100 ) : 0; |
| 1067 |
} |
| 1068 |
|
| 1069 |
/** |
| 1070 |
* Get security recommendations based on current settings |
| 1071 |
* |
| 1072 |
* @param array $options Plugin options. |
| 1073 |
* @return array Array of recommendations. |
| 1074 |
*/ |
| 1075 |
private function get_security_recommendations( $options ) { |
| 1076 |
$recommendations = array(); |
| 1077 |
|
| 1078 |
// Critical: Firewall disabled |
| 1079 |
if ( empty( $options['modules']['firewall'] ) ) { |
| 1080 |
$recommendations[] = array( |
| 1081 |
'icon' => 'warning', |
| 1082 |
'priority' => 'critical', |
| 1083 |
'message' => __( 'Enable Firewall to protect against common attacks.', 'vigilante' ), |
| 1084 |
); |
| 1085 |
} |
| 1086 |
|
| 1087 |
// Critical: Login security disabled |
| 1088 |
if ( empty( $options['modules']['login_security'] ) ) { |
| 1089 |
$recommendations[] = array( |
| 1090 |
'icon' => 'warning', |
| 1091 |
'priority' => 'critical', |
| 1092 |
'message' => __( 'Enable Login Security to prevent brute force attacks.', 'vigilante' ), |
| 1093 |
); |
| 1094 |
} |
| 1095 |
|
| 1096 |
// High: Security headers disabled |
| 1097 |
if ( empty( $options['modules']['security_headers'] ) ) { |
| 1098 |
$recommendations[] = array( |
| 1099 |
'icon' => 'admin-generic', |
| 1100 |
'priority' => 'high', |
| 1101 |
'message' => __( 'Enable Security Headers to protect against clickjacking and XSS.', 'vigilante' ), |
| 1102 |
); |
| 1103 |
} |
| 1104 |
|
| 1105 |
// High: User security disabled |
| 1106 |
if ( empty( $options['modules']['user_security'] ) ) { |
| 1107 |
$recommendations[] = array( |
| 1108 |
'icon' => 'admin-users', |
| 1109 |
'priority' => 'high', |
| 1110 |
'message' => __( 'Enable User Security to enforce password policies and username protection.', 'vigilante' ), |
| 1111 |
); |
| 1112 |
} |
| 1113 |
|
| 1114 |
// High: 2FA not enabled (only if login security is active) |
| 1115 |
$login = $options['login_security'] ?? array(); |
| 1116 |
if ( ! empty( $options['modules']['login_security'] ) && empty( $login['two_factor']['enabled'] ) ) { |
| 1117 |
$recommendations[] = array( |
| 1118 |
'icon' => 'shield', |
| 1119 |
'priority' => 'high', |
| 1120 |
'message' => __( 'Enable Two-Factor Authentication for enhanced login security.', 'vigilante' ), |
| 1121 |
'tab' => 'login', |
| 1122 |
); |
| 1123 |
} |
| 1124 |
|
| 1125 |
// Medium: REST API security disabled |
| 1126 |
if ( empty( $options['modules']['rest_api_security'] ) ) { |
| 1127 |
$recommendations[] = array( |
| 1128 |
'icon' => 'rest-api', |
| 1129 |
'priority' => 'medium', |
| 1130 |
'message' => __( 'Enable REST API Security to control API access and prevent enumeration.', 'vigilante' ), |
| 1131 |
); |
| 1132 |
} |
| 1133 |
|
| 1134 |
// Medium: WP Hardening disabled |
| 1135 |
if ( empty( $options['modules']['wp_hardening'] ) ) { |
| 1136 |
$recommendations[] = array( |
| 1137 |
'icon' => 'lock', |
| 1138 |
'priority' => 'medium', |
| 1139 |
'message' => __( 'Enable WP Hardening to remove version info and protect core files.', 'vigilante' ), |
| 1140 |
); |
| 1141 |
} |
| 1142 |
|
| 1143 |
// Medium: File integrity disabled |
| 1144 |
if ( empty( $options['modules']['file_integrity'] ) ) { |
| 1145 |
$recommendations[] = array( |
| 1146 |
'icon' => 'media-text', |
| 1147 |
'priority' => 'medium', |
| 1148 |
'message' => __( 'Enable File Integrity to detect unauthorized file changes.', 'vigilante' ), |
| 1149 |
); |
| 1150 |
} |
| 1151 |
|
| 1152 |
// Medium: Security Audit disabled |
| 1153 |
if ( empty( $options['modules']['activity_log'] ) ) { |
| 1154 |
$recommendations[] = array( |
| 1155 |
'icon' => 'list-view', |
| 1156 |
'priority' => 'medium', |
| 1157 |
'message' => __( 'Enable Security Audit to track security events.', 'vigilante' ), |
| 1158 |
); |
| 1159 |
} |
| 1160 |
|
| 1161 |
// Low: XML-RPC enabled (only if login security is active) |
| 1162 |
if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) { |
| 1163 |
$recommendations[] = array( |
| 1164 |
'icon' => 'info', |
| 1165 |
'priority' => 'low', |
| 1166 |
'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ), |
| 1167 |
'tab' => 'login', |
| 1168 |
); |
| 1169 |
} |
| 1170 |
|
| 1171 |
// Low: Strong passwords not enforced (only if user security is active) |
| 1172 |
$user = $options['user_security'] ?? array(); |
| 1173 |
if ( ! empty( $options['modules']['user_security'] ) && empty( $user['force_strong_passwords'] ) ) { |
| 1174 |
$recommendations[] = array( |
| 1175 |
'icon' => 'admin-users', |
| 1176 |
'priority' => 'low', |
| 1177 |
'message' => __( 'Enforce strong passwords for all users.', 'vigilante' ), |
| 1178 |
'tab' => 'users', |
| 1179 |
); |
| 1180 |
} |
| 1181 |
|
| 1182 |
// High: WP_DEBUG active in production (regardless of Vigilante settings) |
| 1183 |
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { |
| 1184 |
$recommendations[] = array( |
| 1185 |
'icon' => 'warning', |
| 1186 |
'priority' => 'high', |
| 1187 |
'message' => __( 'WP_DEBUG is active. Debug mode exposes sensitive information and should be disabled in production.', 'vigilante' ), |
| 1188 |
'tab' => 'wp-hardening', |
| 1189 |
); |
| 1190 |
} |
| 1191 |
|
| 1192 |
// Low: Users with display name matching login username (only if user security active) |
| 1193 |
if ( ! empty( $options['modules']['user_security'] ) ) { |
| 1194 |
$exposed_users = $this->get_users_with_exposed_login(); |
| 1195 |
if ( ! empty( $exposed_users ) ) { |
| 1196 |
$recommendations[] = array( |
| 1197 |
'icon' => 'admin-users', |
| 1198 |
'priority' => 'low', |
| 1199 |
'message' => sprintf( |
| 1200 |
/* translators: %s: Comma-separated list of usernames */ |
| 1201 |
__( 'These users have their login username as display name (publicly visible): %s', 'vigilante' ), |
| 1202 |
implode( ', ', $exposed_users ) |
| 1203 |
), |
| 1204 |
'tab' => 'users', |
| 1205 |
); |
| 1206 |
} |
| 1207 |
} |
| 1208 |
|
| 1209 |
// High: Accounts with insecure usernames (regardless of module status) |
| 1210 |
$insecure_admins = $this->get_insecure_admin_usernames(); |
| 1211 |
if ( ! empty( $insecure_admins ) ) { |
| 1212 |
$recommendations[] = array( |
| 1213 |
'icon' => 'warning', |
| 1214 |
'priority' => 'high', |
| 1215 |
'message' => sprintf( |
| 1216 |
/* translators: %s: Comma-separated list of usernames */ |
| 1217 |
__( 'Insecure usernames detected: %s. These are commonly targeted in brute force attacks. Create new accounts with unique usernames and remove these.', 'vigilante' ), |
| 1218 |
implode( ', ', $insecure_admins ) |
| 1219 |
), |
| 1220 |
); |
| 1221 |
} |
| 1222 |
|
| 1223 |
// Critical: Closed or removed plugins detected by the daily check. |
| 1224 |
// Reads from the cached state map populated by Vigilante_Plugin_Status, so |
| 1225 |
// there is no extra HTTP call here. Ignored slugs are filtered out so the |
| 1226 |
// recommendation respects the user's per-slug Ignore decisions. |
| 1227 |
if ( ! empty( $options['modules']['file_integrity'] ) && ! empty( $options['file_integrity']['check_closed_plugins'] ) ) { |
| 1228 |
if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { |
| 1229 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 1230 |
} |
| 1231 |
$closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); |
| 1232 |
$closed_active = $closed_checker->get_closed_plugins(); |
| 1233 |
if ( ! empty( $closed_active ) ) { |
| 1234 |
$names = array(); |
| 1235 |
foreach ( $closed_active as $slug => $entry ) { |
| 1236 |
$names[] = isset( $entry['name'] ) ? $entry['name'] : $slug; |
| 1237 |
} |
| 1238 |
$recommendations[] = array( |
| 1239 |
'icon' => 'warning', |
| 1240 |
'priority' => 'critical', |
| 1241 |
'message' => sprintf( |
| 1242 |
/* translators: 1: count, 2: comma-separated plugin names */ |
| 1243 |
_n( |
| 1244 |
'%1$d closed plugin detected on this site: %2$s. Closures in WordPress.org usually indicate malware, security issues or supply chain compromises. Uninstall and replace as soon as possible.', |
| 1245 |
'%1$d closed plugins detected on this site: %2$s. Closures in WordPress.org usually indicate malware, security issues or supply chain compromises. Uninstall and replace as soon as possible.', |
| 1246 |
count( $closed_active ), |
| 1247 |
'vigilante' |
| 1248 |
), |
| 1249 |
count( $closed_active ), |
| 1250 |
implode( ', ', $names ) |
| 1251 |
), |
| 1252 |
'tab' => 'file-integrity', |
| 1253 |
); |
| 1254 |
} |
| 1255 |
} |
| 1256 |
|
| 1257 |
// Medium: Security Audit is on but no audit alert is configured. The |
| 1258 |
// alerting layer only makes sense while the activity log is running. |
| 1259 |
if ( ! empty( $options['modules']['activity_log'] ) ) { |
| 1260 |
$alerts = isset( $options['audit_alerts'] ) ? (array) $options['audit_alerts'] : array(); |
| 1261 |
if ( ! Vigilante_Audit_Alerts::has_active_alerts( $alerts ) ) { |
| 1262 |
$recommendations[] = array( |
| 1263 |
'icon' => 'email-alt', |
| 1264 |
'priority' => 'medium', |
| 1265 |
'message' => __( 'Set up Audit Alerts to get an email when something important happens (a new admin, a closed plugin, or an attack in progress).', 'vigilante' ), |
| 1266 |
'tab' => 'activity-log', |
| 1267 |
); |
| 1268 |
} |
| 1269 |
} |
| 1270 |
|
| 1271 |
// Sort by priority |
| 1272 |
$priority_order = array( 'critical' => 0, 'high' => 1, 'medium' => 2, 'low' => 3 ); |
| 1273 |
usort( $recommendations, function( $a, $b ) use ( $priority_order ) { |
| 1274 |
return ( $priority_order[ $a['priority'] ] ?? 99 ) - ( $priority_order[ $b['priority'] ] ?? 99 ); |
| 1275 |
} ); |
| 1276 |
|
| 1277 |
return $recommendations; |
| 1278 |
} |
| 1279 |
|
| 1280 |
/** |
| 1281 |
* Get users whose display name matches their login username |
| 1282 |
* |
| 1283 |
* Limited to administrators and editors for performance and relevance. |
| 1284 |
* Cached with transient to avoid repeated queries on every dashboard load. |
| 1285 |
* |
| 1286 |
* @return array Array of usernames with exposed login. |
| 1287 |
*/ |
| 1288 |
private function get_users_with_exposed_login() { |
| 1289 |
$cache_key = 'vigilante_exposed_display_names'; |
| 1290 |
$cached = get_transient( $cache_key ); |
| 1291 |
|
| 1292 |
if ( false !== $cached ) { |
| 1293 |
return $cached; |
| 1294 |
} |
| 1295 |
|
| 1296 |
$exposed = array(); |
| 1297 |
$users = get_users( array( |
| 1298 |
'role__in' => array( 'administrator', 'editor' ), |
| 1299 |
'fields' => array( 'ID', 'user_login', 'display_name' ), |
| 1300 |
) ); |
| 1301 |
|
| 1302 |
foreach ( $users as $user ) { |
| 1303 |
if ( strcasecmp( $user->display_name, $user->user_login ) === 0 ) { |
| 1304 |
$exposed[] = $user->user_login; |
| 1305 |
} |
| 1306 |
} |
| 1307 |
|
| 1308 |
// Cache for 12 hours |
| 1309 |
set_transient( $cache_key, $exposed, 12 * HOUR_IN_SECONDS ); |
| 1310 |
|
| 1311 |
return $exposed; |
| 1312 |
} |
| 1313 |
|
| 1314 |
/** |
| 1315 |
* Get accounts with insecure usernames |
| 1316 |
* |
| 1317 |
* Checks for common default usernames that are targeted by brute force attacks. |
| 1318 |
* Detects any user regardless of role (consistent with username creation blocking). |
| 1319 |
* Uses WordPress object cache via get_user_by() so no transient needed. |
| 1320 |
* |
| 1321 |
* @return array Array of insecure usernames found. |
| 1322 |
*/ |
| 1323 |
private function get_insecure_admin_usernames() { |
| 1324 |
$priority_usernames = array( 'admin', 'administrator', 'root', 'test', 'user', 'guest', 'info', 'sysadmin', 'webmaster' ); |
| 1325 |
$found = array(); |
| 1326 |
|
| 1327 |
foreach ( $priority_usernames as $username ) { |
| 1328 |
$user = get_user_by( 'login', $username ); |
| 1329 |
if ( $user ) { |
| 1330 |
$found[] = $username; |
| 1331 |
} |
| 1332 |
} |
| 1333 |
|
| 1334 |
return $found; |
| 1335 |
} |
| 1336 |
|
| 1337 |
/** |
| 1338 |
* Get security status for menu badge |
| 1339 |
* |
| 1340 |
* Returns count of disabled modules and whether there are critical issues. |
| 1341 |
* Critical = Firewall or Login Security disabled. |
| 1342 |
* |
| 1343 |
* @return array Array with 'count' and 'has_critical'. |
| 1344 |
*/ |
| 1345 |
public function get_security_status_for_badge() { |
| 1346 |
$options = $this->settings->get_all_options(); |
| 1347 |
$modules = $options['modules'] ?? array(); |
| 1348 |
|
| 1349 |
// Count disabled modules |
| 1350 |
$disabled_count = 0; |
| 1351 |
$has_critical = false; |
| 1352 |
|
| 1353 |
// Critical modules - if disabled, badge is red |
| 1354 |
$critical_modules = array( 'firewall', 'login_security' ); |
| 1355 |
|
| 1356 |
foreach ( $modules as $module => $enabled ) { |
| 1357 |
// Handle both boolean and string values ('1', '0', true, false) |
| 1358 |
$is_enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN ); |
| 1359 |
|
| 1360 |
if ( ! $is_enabled ) { |
| 1361 |
$disabled_count++; |
| 1362 |
|
| 1363 |
// Check if this is a critical module |
| 1364 |
if ( in_array( $module, $critical_modules, true ) ) { |
| 1365 |
$has_critical = true; |
| 1366 |
} |
| 1367 |
} |
| 1368 |
} |
| 1369 |
|
| 1370 |
return array( |
| 1371 |
'count' => $disabled_count, |
| 1372 |
'has_critical' => $has_critical, |
| 1373 |
); |
| 1374 |
} |
| 1375 |
|
| 1376 |
/** |
| 1377 |
* Get count of security issues for menu badge (deprecated, use get_security_status_for_badge) |
| 1378 |
* |
| 1379 |
* @return int Count of critical/high issues. |
| 1380 |
*/ |
| 1381 |
public function get_security_issues_count() { |
| 1382 |
$status = $this->get_security_status_for_badge(); |
| 1383 |
return $status['count']; |
| 1384 |
} |
| 1385 |
|
| 1386 |
/** |
| 1387 |
* Register settings |
| 1388 |
*/ |
| 1389 |
public function register_settings() { |
| 1390 |
register_setting( |
| 1391 |
'vigilante_options', |
| 1392 |
Vigilante_Settings::OPTION_NAME, |
| 1393 |
array( $this->settings, 'validate_options' ) |
| 1394 |
); |
| 1395 |
} |
| 1396 |
|
| 1397 |
/** |
| 1398 |
* Index behind the settings search box. |
| 1399 |
* |
| 1400 |
* Each entry points at one settings row. The search matches on the label, |
| 1401 |
* on its English original and on 'keywords', which are extra terms someone |
| 1402 |
* might type instead of the label itself. |
| 1403 |
* |
| 1404 |
* Those keywords are wrapped in _x() with the context "settings search |
| 1405 |
* keywords" so every locale can supply its own: the source strings are in |
| 1406 |
* English, and a Spanish user typing "contrasena" or a German one typing |
| 1407 |
* "Kennwort" only reaches the password settings if that locale translated |
| 1408 |
* them. Translators can add, drop or replace terms freely, one per space; |
| 1409 |
* they are never displayed, only matched against what the user types. |
| 1410 |
* |
| 1411 |
* The list is maintained by hand, so a new settings row needs an entry here |
| 1412 |
* or it cannot be found. It had drifted to 68 of 131 rows before 2.9.7. |
| 1413 |
* |
| 1414 |
* @return array |
| 1415 |
*/ |
| 1416 |
private function get_search_index() { |
| 1417 |
return array( |
| 1418 |
// Firewall - Main |
| 1419 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block bad bots', 'vigilante' ), 'label_en' => 'Block bad bots', 'keywords' => _x( 'block bad bots blocking blocked deny malicious harmful bot crawler crawlers spider scraper robots', 'settings search keywords', 'vigilante' ) ), |
| 1420 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block malicious requests', 'vigilante' ), 'label_en' => 'Block malicious requests', 'keywords' => _x( 'block malicious requests blocking blocked deny attack attacks exploit injection sqli xss rfi lfi request traffic', 'settings search keywords', 'vigilante' ) ), |
| 1421 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Rate limiting', 'vigilante' ), 'label_en' => 'Rate limiting', 'keywords' => _x( 'rate limiting throttle flood burst limit limits', 'settings search keywords', 'vigilante' ) ), |
| 1422 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Brute force protection', 'vigilante' ), 'label_en' => 'Brute force protection', 'keywords' => _x( 'brute force protection bruteforce login', 'settings search keywords', 'vigilante' ) ), |
| 1423 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'IP Whitelist', 'vigilante' ), 'label_en' => 'IP Whitelist', 'keywords' => _x( 'ip whitelist ips address addresses cidr ipv4 ipv6 allowlist allowed trusted', 'settings search keywords', 'vigilante' ) ), |
| 1424 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'IP Blacklist', 'vigilante' ), 'label_en' => 'IP Blacklist', 'keywords' => _x( 'ip blacklist ips address addresses cidr ipv4 ipv6 blocklist denylist banned', 'settings search keywords', 'vigilante' ) ), |
| 1425 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'User-Agent Whitelist', 'vigilante' ), 'label_en' => 'User-Agent Whitelist', 'keywords' => _x( 'user-agent whitelist ua useragent browser allowlist allowed trusted user', 'settings search keywords', 'vigilante' ) ), |
| 1426 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'User-Agent Blacklist', 'vigilante' ), 'label_en' => 'User-Agent Blacklist', 'keywords' => _x( 'user-agent blacklist ua useragent browser blocklist denylist banned user', 'settings search keywords', 'vigilante' ) ), |
| 1427 |
// Firewall - Server Protection |
| 1428 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Directory Browsing', 'vigilante' ), 'label_en' => 'Directory Browsing', 'keywords' => _x( 'directory browsing folder folders listing indexing index', 'settings search keywords', 'vigilante' ) ), |
| 1429 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Protect wp-config.php', 'vigilante' ), 'label_en' => 'Protect wp-config.php', 'keywords' => _x( 'protect wp-config php protection secure lock', 'settings search keywords', 'vigilante' ) ), |
| 1430 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'field-protect-wp-cron', 'label' => __( 'Protect wp-cron.php', 'vigilante' ), 'label_en' => 'Protect wp-cron.php', 'keywords' => _x( 'protect wp-cron php protection secure lock cron scheduled tasks block spam', 'settings search keywords', 'vigilante' ) ), |
| 1431 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Protect wp-includes', 'vigilante' ), 'label_en' => 'Protect wp-includes', 'keywords' => _x( 'protect wp-includes protection secure lock', 'settings search keywords', 'vigilante' ) ), |
| 1432 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'PHP in Uploads', 'vigilante' ), 'label_en' => 'PHP in Uploads', 'keywords' => _x( 'php in uploads media upload', 'settings search keywords', 'vigilante' ) ), |
| 1433 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Sensitive Files', 'vigilante' ), 'label_en' => 'Sensitive Files', 'keywords' => _x( 'sensitive files private secret file log', 'settings search keywords', 'vigilante' ) ), |
| 1434 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Server Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-server', 'label' => __( 'Limit HTTP Methods', 'vigilante' ), 'label_en' => 'Limit HTTP Methods', 'keywords' => _x( 'limit http methods', 'settings search keywords', 'vigilante' ) ), |
| 1435 |
// Security Headers |
| 1436 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'X-Frame-Options', 'vigilante' ), 'label_en' => 'X-Frame-Options', 'keywords' => _x( 'x-frame-options headers', 'settings search keywords', 'vigilante' ) ), |
| 1437 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'X-Content-Type-Options', 'vigilante' ), 'label_en' => 'X-Content-Type-Options', 'keywords' => _x( 'x-content-type-options content headers', 'settings search keywords', 'vigilante' ) ), |
| 1438 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Referrer-Policy', 'vigilante' ), 'label_en' => 'Referrer-Policy', 'keywords' => _x( 'referrer-policy headers', 'settings search keywords', 'vigilante' ) ), |
| 1439 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'HSTS', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'HSTS', 'vigilante' ), 'label_en' => 'HSTS', 'keywords' => _x( 'hsts strict transport security ssl tls https headers', 'settings search keywords', 'vigilante' ) ), |
| 1440 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Content Security Policy', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Content Security Policy', 'vigilante' ), 'label_en' => 'Content Security Policy', 'keywords' => _x( 'content security policy csp xss headers', 'settings search keywords', 'vigilante' ) ), |
| 1441 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Server Identity', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Server Signature', 'vigilante' ), 'label_en' => 'Server Signature', 'keywords' => _x( 'server signature fingerprint banner', 'settings search keywords', 'vigilante' ) ), |
| 1442 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Server Identity', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Remove Fingerprinting Headers', 'vigilante' ), 'label_en' => 'Remove Fingerprinting Headers', 'keywords' => _x( 'remove fingerprinting headers fingerprint banner header http', 'settings search keywords', 'vigilante' ) ), |
| 1443 |
// Security Headers - Cross-Origin Policies |
| 1444 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Cross-Origin Policies', 'vigilante' ), 'anchor' => 'vigilante-section-headers-cross-origin', 'label' => __( 'Cross-Origin-Opener-Policy (COOP)', 'vigilante' ), 'label_en' => 'Cross-Origin-Opener-Policy (COOP)', 'keywords' => _x( 'coop cross-origin opener policy popup popups window opener tag assistant google isolation browsing context headers', 'settings search keywords', 'vigilante' ) ), |
| 1445 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Cross-Origin Policies', 'vigilante' ), 'anchor' => 'vigilante-section-headers-cross-origin', 'label' => __( 'Cross-Origin-Embedder-Policy (COEP)', 'vigilante' ), 'label_en' => 'Cross-Origin-Embedder-Policy (COEP)', 'keywords' => _x( 'coep cross-origin embedder policy require-corp credentialless embed embeds iframe fonts headers', 'settings search keywords', 'vigilante' ) ), |
| 1446 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Cross-Origin Policies', 'vigilante' ), 'anchor' => 'vigilante-section-headers-cross-origin', 'label' => __( 'Cross-Origin-Resource-Policy (CORP)', 'vigilante' ), 'label_en' => 'Cross-Origin-Resource-Policy (CORP)', 'keywords' => _x( 'corp cross-origin resource policy hotlink hotlinking cdn images assets headers', 'settings search keywords', 'vigilante' ) ), |
| 1447 |
// Login Security |
| 1448 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Custom login URL', 'vigilante' ), 'label_en' => 'Custom login URL', 'keywords' => _x( 'custom login url signin log-in access slug', 'settings search keywords', 'vigilante' ) ), |
| 1449 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Two-Factor Authentication', 'vigilante' ), 'label_en' => 'Two-Factor Authentication', 'keywords' => _x( 'two-factor authentication 2fa mfa otp totp authenticator', 'settings search keywords', 'vigilante' ) ), |
| 1450 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( '2FA', 'vigilante' ), 'label_en' => '2FA', 'keywords' => _x( '2fa two-factor mfa otp totp authenticator', 'settings search keywords', 'vigilante' ) ), |
| 1451 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Failed login attempts', 'vigilante' ), 'label_en' => 'Failed login attempts', 'keywords' => _x( 'failed login attempts signin log-in access tries retries', 'settings search keywords', 'vigilante' ) ), |
| 1452 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Lockout', 'vigilante' ), 'label_en' => 'Lockout', 'keywords' => _x( 'lockout lock ban block login', 'settings search keywords', 'vigilante' ) ), |
| 1453 |
// REST API |
| 1454 |
array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Access Mode', 'vigilante' ), 'label_en' => 'Access Mode', 'keywords' => _x( 'access mode rest api', 'settings search keywords', 'vigilante' ) ), |
| 1455 |
array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Block User Enumeration', 'vigilante' ), 'label_en' => 'Block User Enumeration', 'keywords' => _x( 'block user enumeration blocking blocked deny users account author slug', 'settings search keywords', 'vigilante' ) ), |
| 1456 |
array( 'tab' => 'rest-api', 'tab_label' => __( 'REST API', 'vigilante' ), 'section' => __( 'REST API Security', 'vigilante' ), 'anchor' => 'vigilante-section-rest-api-main', 'label' => __( 'Disable JSONP', 'vigilante' ), 'label_en' => 'Disable JSONP', 'keywords' => _x( 'disable jsonp', 'settings search keywords', 'vigilante' ) ), |
| 1457 |
// User Security |
| 1458 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Username protection', 'vigilante' ), 'label_en' => 'Username protection', 'keywords' => _x( 'username protection admin', 'settings search keywords', 'vigilante' ) ), |
| 1459 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Password strength', 'vigilante' ), 'label_en' => 'Password strength', 'keywords' => _x( 'password strength passwords credentials', 'settings search keywords', 'vigilante' ) ), |
| 1460 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin monitoring', 'vigilante' ), 'label_en' => 'Admin monitoring', 'keywords' => _x( 'admin monitoring administrator administrators', 'settings search keywords', 'vigilante' ) ), |
| 1461 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Registration approval', 'vigilante' ), 'label_en' => 'Registration approval', 'keywords' => _x( 'registration approval signup register approve moderate', 'settings search keywords', 'vigilante' ) ), |
| 1462 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Session limits', 'vigilante' ), 'label_en' => 'Session limits', 'keywords' => _x( 'session limits sessions concurrent', 'settings search keywords', 'vigilante' ) ), |
| 1463 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Password expiration', 'vigilante' ), 'label_en' => 'Password expiration', 'keywords' => _x( 'password expiration passwords credentials expiry expire caducity', 'settings search keywords', 'vigilante' ) ), |
| 1464 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Email verification', 'vigilante' ), 'label_en' => 'Email verification', 'keywords' => _x( 'email verification mail notification notify verify confirm', 'settings search keywords', 'vigilante' ) ), |
| 1465 |
// WP Hardening |
| 1466 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Database Hardening', 'vigilante' ), 'label_en' => 'Database Hardening', 'keywords' => _x( 'database hardening db mysql tables', 'settings search keywords', 'vigilante' ) ), |
| 1467 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Database prefix', 'vigilante' ), 'label_en' => 'Database prefix', 'keywords' => _x( 'database prefix db mysql tables table', 'settings search keywords', 'vigilante' ) ), |
| 1468 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable file editing', 'vigilante' ), 'label_en' => 'Disable file editing', 'keywords' => _x( 'disable file editing files editor edit', 'settings search keywords', 'vigilante' ) ), |
| 1469 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable plugin/theme installation', 'vigilante' ), 'label_en' => 'Disable plugin/theme installation', 'keywords' => _x( 'disable plugin theme installation install', 'settings search keywords', 'vigilante' ) ), |
| 1470 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Force SSL admin', 'vigilante' ), 'label_en' => 'Force SSL admin', 'keywords' => _x( 'force ssl admin bruteforce administrator administrators tls https', 'settings search keywords', 'vigilante' ) ), |
| 1471 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'field-disable-wp-cron', 'label' => __( 'Disable WP Cron', 'vigilante' ), 'label_en' => 'Disable WP Cron', 'keywords' => _x( 'disable wp cron scheduled tasks wp-cron', 'settings search keywords', 'vigilante' ) ), |
| 1472 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Comment Security', 'vigilante' ), 'label_en' => 'Comment Security', 'keywords' => _x( 'comment security comments spam honeypot url', 'settings search keywords', 'vigilante' ) ), |
| 1473 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Header Cleanup', 'vigilante' ), 'label_en' => 'Header Cleanup', 'keywords' => _x( 'header cleanup headers http meta generator rsd wlwmanifest', 'settings search keywords', 'vigilante' ) ), |
| 1474 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove WordPress version', 'vigilante' ), 'label_en' => 'Remove WordPress version', 'keywords' => _x( 'remove wordpress version generator meta', 'settings search keywords', 'vigilante' ) ), |
| 1475 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'field-remove-wp-version-assets', 'label' => __( 'Remove version from assets', 'vigilante' ), 'label_en' => 'Remove version from assets', 'keywords' => _x( 'remove version from assets', 'settings search keywords', 'vigilante' ) ), |
| 1476 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-xmlrpc', 'label' => __( 'Disable XML-RPC', 'vigilante' ), 'label_en' => 'Disable XML-RPC', 'keywords' => _x( 'disable xml-rpc xmlrpc rpc remote jetpack app pingback trackback', 'settings search keywords', 'vigilante' ) ), |
| 1477 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'RSS Feed Settings', 'vigilante' ), 'label_en' => 'RSS Feed Settings', 'keywords' => _x( 'rss feed settings feeds atom', 'settings search keywords', 'vigilante' ) ), |
| 1478 |
// File Integrity |
| 1479 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'File Integrity Monitoring', 'vigilante' ), 'label_en' => 'File Integrity Monitoring', 'keywords' => _x( 'file integrity monitoring files checksum checksums tamper', 'settings search keywords', 'vigilante' ) ), |
| 1480 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan schedule', 'vigilante' ), 'label_en' => 'Scan schedule', 'keywords' => _x( 'scan schedule scans scanning check cron', 'settings search keywords', 'vigilante' ) ), |
| 1481 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Instant alert', 'vigilante' ), 'label_en' => 'Instant alert', 'keywords' => _x( 'instant alert alerts notification warning email', 'settings search keywords', 'vigilante' ) ), |
| 1482 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'Ignored Files', 'vigilante' ), 'anchor' => 'vigilante-section-fi-ignored', 'label' => __( 'Ignored Files', 'vigilante' ), 'label_en' => 'Ignored Files', 'keywords' => _x( 'ignored files file exclude', 'settings search keywords', 'vigilante' ) ), |
| 1483 |
// Security Audit |
| 1484 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Retention', 'vigilante' ), 'label_en' => 'Retention', 'keywords' => _x( 'retention keep days storage log', 'settings search keywords', 'vigilante' ) ), |
| 1485 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Events to Log', 'vigilante' ), 'label_en' => 'Events to Log', 'keywords' => _x( 'events to log', 'settings search keywords', 'vigilante' ) ), |
| 1486 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Option Tracking', 'vigilante' ), 'label_en' => 'Option Tracking', 'keywords' => _x( 'option tracking', 'settings search keywords', 'vigilante' ) ), |
| 1487 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Security Audit Settings', 'vigilante' ), 'anchor' => 'vigilante-section-audit-settings', 'label' => __( 'Exclusions', 'vigilante' ), 'label_en' => 'Exclusions', 'keywords' => _x( 'exclusions roles ip', 'settings search keywords', 'vigilante' ) ), |
| 1488 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Audit Alerts', 'vigilante' ), 'label_en' => 'Audit Alerts', 'keywords' => _x( 'audit alerts email mail warning critical', 'settings search keywords', 'vigilante' ) ), |
| 1489 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'field-audit-alerts-immediate', 'label' => __( 'Immediate alerts', 'vigilante' ), 'label_en' => 'Immediate alerts', 'keywords' => _x( 'immediate alerts email mail critical warning', 'settings search keywords', 'vigilante' ) ), |
| 1490 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'field-audit-alerts-threshold', 'label' => __( 'Threshold alerts', 'vigilante' ), 'label_en' => 'Threshold alerts', 'keywords' => _x( 'threshold alerts email mail login', 'settings search keywords', 'vigilante' ) ), |
| 1491 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Recent Activity', 'vigilante' ), 'anchor' => 'vigilante-section-audit-recent', 'label' => __( 'Recent Activity', 'vigilante' ), 'label_en' => 'Recent Activity', 'keywords' => _x( 'recent activity log', 'settings search keywords', 'vigilante' ) ), |
| 1492 |
// Settings & Tools |
| 1493 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Notification settings', 'vigilante' ), 'label_en' => 'Notification settings', 'keywords' => _x( 'notification settings email', 'settings search keywords', 'vigilante' ) ), |
| 1494 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Additional Recipients', 'vigilante' ), 'label_en' => 'Additional Recipients', 'keywords' => _x( 'additional recipients email recipient', 'settings search keywords', 'vigilante' ) ), |
| 1495 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Export Settings', 'vigilante' ), 'label_en' => 'Export Settings', 'keywords' => _x( 'export settings json', 'settings search keywords', 'vigilante' ) ), |
| 1496 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Import Settings', 'vigilante' ), 'label_en' => 'Import Settings', 'keywords' => _x( 'import settings json', 'settings search keywords', 'vigilante' ) ), |
| 1497 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Reset to Defaults', 'vigilante' ), 'label_en' => 'Reset to Defaults', 'keywords' => _x( 'reset to defaults', 'settings search keywords', 'vigilante' ) ), |
| 1498 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Create Backup', 'vigilante' ), 'label_en' => 'Create Backup', 'keywords' => _x( 'create backup', 'settings search keywords', 'vigilante' ) ), |
| 1499 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Tools', 'vigilante' ), 'anchor' => 'vigilante-section-tools-main', 'label' => __( 'Database Backup', 'vigilante' ), 'label_en' => 'Database Backup', 'keywords' => _x( 'database backup db mysql tables', 'settings search keywords', 'vigilante' ) ), |
| 1500 |
// Entradas anadidas en la 2.9.7 tras comprobar que el indice cubria 68 de |
| 1501 |
// las 131 filas de ajustes: buscar XML-RPC, por ejemplo, no devolvia nada. |
| 1502 |
// El indice se mantiene a mano, asi que al anadir una fila de ajustes hay |
| 1503 |
// que anadirla tambien aqui. |
| 1504 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'WordPress Admin Email', 'vigilante' ), 'label_en' => 'WordPress Admin Email', 'keywords' => _x( 'wordpress admin email administrator administrators mail notification notify', 'settings search keywords', 'vigilante' ) ), |
| 1505 |
array( 'tab' => 'tools', 'tab_label' => __( 'Settings & Tools', 'vigilante' ), 'section' => __( 'Notification settings', 'vigilante' ), 'anchor' => 'vigilante-section-tools-notifications', 'label' => __( 'Plugin Deactivation', 'vigilante' ), 'label_en' => 'Plugin Deactivation', 'keywords' => _x( 'plugin deactivation', 'settings search keywords', 'vigilante' ) ), |
| 1506 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block Bad Query Strings', 'vigilante' ), 'label_en' => 'Block Bad Query Strings', 'keywords' => _x( 'block bad query strings blocking blocked deny malicious harmful', 'settings search keywords', 'vigilante' ) ), |
| 1507 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'SQL Injection Protection', 'vigilante' ), 'label_en' => 'SQL Injection Protection', 'keywords' => _x( 'sql injection protection', 'settings search keywords', 'vigilante' ) ), |
| 1508 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'XSS Protection', 'vigilante' ), 'label_en' => 'XSS Protection', 'keywords' => _x( 'xss protection', 'settings search keywords', 'vigilante' ) ), |
| 1509 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'File Inclusion Protection', 'vigilante' ), 'label_en' => 'File Inclusion Protection', 'keywords' => _x( 'file inclusion protection files', 'settings search keywords', 'vigilante' ) ), |
| 1510 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Directory Traversal Protection', 'vigilante' ), 'label_en' => 'Directory Traversal Protection', 'keywords' => _x( 'directory traversal protection folder folders', 'settings search keywords', 'vigilante' ) ), |
| 1511 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Enable Rate Limiting', 'vigilante' ), 'label_en' => 'Enable Rate Limiting', 'keywords' => _x( 'enable rate limiting throttle flood burst limit limits', 'settings search keywords', 'vigilante' ) ), |
| 1512 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Requests per Minute', 'vigilante' ), 'label_en' => 'Requests per Minute', 'keywords' => _x( 'requests per minute request traffic', 'settings search keywords', 'vigilante' ) ), |
| 1513 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Block Duration (seconds)', 'vigilante' ), 'label_en' => 'Block Duration (seconds)', 'keywords' => _x( 'block duration seconds blocking blocked deny', 'settings search keywords', 'vigilante' ) ), |
| 1514 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Progressive Blocking', 'vigilante' ), 'label_en' => 'Progressive Blocking', 'keywords' => _x( 'progressive blocking', 'settings search keywords', 'vigilante' ) ), |
| 1515 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Maximum Block Duration', 'vigilante' ), 'label_en' => 'Maximum Block Duration', 'keywords' => _x( 'maximum block duration blocking blocked deny', 'settings search keywords', 'vigilante' ) ), |
| 1516 |
array( 'tab' => 'firewall', 'tab_label' => __( 'Firewall', 'vigilante' ), 'section' => __( 'Firewall Protection', 'vigilante' ), 'anchor' => 'vigilante-section-firewall-main', 'label' => __( 'Visitor IP detection', 'vigilante' ), 'label_en' => 'Visitor IP detection', 'keywords' => _x( 'visitor ip detection ips address addresses cidr ipv4 ipv6', 'settings search keywords', 'vigilante' ) ), |
| 1517 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Max Login Attempts', 'vigilante' ), 'label_en' => 'Max Login Attempts', 'keywords' => _x( 'max login attempts signin log-in access tries retries', 'settings search keywords', 'vigilante' ) ), |
| 1518 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Lockout Duration', 'vigilante' ), 'label_en' => 'Lockout Duration', 'keywords' => _x( 'lockout duration lock ban block', 'settings search keywords', 'vigilante' ) ), |
| 1519 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Progressive Lockout', 'vigilante' ), 'label_en' => 'Progressive Lockout', 'keywords' => _x( 'progressive lockout lock ban block', 'settings search keywords', 'vigilante' ) ), |
| 1520 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Hide Login Errors', 'vigilante' ), 'label_en' => 'Hide Login Errors', 'keywords' => _x( 'hide login errors signin log-in access error debug log', 'settings search keywords', 'vigilante' ) ), |
| 1521 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Disable Application Passwords', 'vigilante' ), 'label_en' => 'Disable Application Passwords', 'keywords' => _x( 'disable application passwords password credentials', 'settings search keywords', 'vigilante' ) ), |
| 1522 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Login URL Slug', 'vigilante' ), 'label_en' => 'Login URL Slug', 'keywords' => _x( 'login url slug signin log-in access path', 'settings search keywords', 'vigilante' ) ), |
| 1523 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify users', 'vigilante' ), 'label_en' => 'Notify users', 'keywords' => _x( 'notify users notification alert email user accounts', 'settings search keywords', 'vigilante' ) ), |
| 1524 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify on Lockout', 'vigilante' ), 'label_en' => 'Notify on Lockout', 'keywords' => _x( 'notify on lockout notification alert email lock ban block', 'settings search keywords', 'vigilante' ) ), |
| 1525 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify on Admin Login', 'vigilante' ), 'label_en' => 'Notify on Admin Login', 'keywords' => _x( 'notify on admin login notification alert email administrator administrators signin log-in access', 'settings search keywords', 'vigilante' ) ), |
| 1526 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Current settings', 'vigilante' ), 'label_en' => 'Current settings', 'keywords' => _x( 'current settings', 'settings search keywords', 'vigilante' ) ), |
| 1527 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Blocked IPs', 'vigilante' ), 'label_en' => 'Blocked IPs', 'keywords' => _x( 'blocked ips', 'settings search keywords', 'vigilante' ) ), |
| 1528 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Enable 2FA', 'vigilante' ), 'label_en' => 'Enable 2FA', 'keywords' => _x( 'enable 2fa two-factor mfa otp totp authenticator', 'settings search keywords', 'vigilante' ) ), |
| 1529 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Verification method', 'vigilante' ), 'label_en' => 'Verification method', 'keywords' => _x( 'verification method verify confirm', 'settings search keywords', 'vigilante' ) ), |
| 1530 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Enforce for roles', 'vigilante' ), 'label_en' => 'Enforce for roles', 'keywords' => _x( 'enforce for roles role capabilities', 'settings search keywords', 'vigilante' ) ), |
| 1531 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Exclude specific users', 'vigilante' ), 'label_en' => 'Exclude specific users', 'keywords' => _x( 'exclude specific users user accounts', 'settings search keywords', 'vigilante' ) ), |
| 1532 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Remember device', 'vigilante' ), 'label_en' => 'Remember device', 'keywords' => _x( 'remember device', 'settings search keywords', 'vigilante' ) ), |
| 1533 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Grace period', 'vigilante' ), 'label_en' => 'Grace period', 'keywords' => _x( 'grace period', 'settings search keywords', 'vigilante' ) ), |
| 1534 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Email sender name', 'vigilante' ), 'label_en' => 'Email sender name', 'keywords' => _x( 'email sender name mail notification notify names', 'settings search keywords', 'vigilante' ) ), |
| 1535 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Reset user TOTP', 'vigilante' ), 'label_en' => 'Reset user TOTP', 'keywords' => _x( 'reset user totp users account 2fa authenticator app', 'settings search keywords', 'vigilante' ) ), |
| 1536 |
array( 'tab' => 'login', 'tab_label' => __( 'Login Security', 'vigilante' ), 'section' => __( 'Login Protection Status', 'vigilante' ), 'anchor' => 'vigilante-section-login-main', 'label' => __( 'Notify on enable', 'vigilante' ), 'label_en' => 'Notify on enable', 'keywords' => _x( 'notify on enable notification alert email', 'settings search keywords', 'vigilante' ) ), |
| 1537 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Enable CSP', 'vigilante' ), 'label_en' => 'Enable CSP', 'keywords' => _x( 'enable csp content security policy', 'settings search keywords', 'vigilante' ) ), |
| 1538 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Report Only Mode', 'vigilante' ), 'label_en' => 'Report Only Mode', 'keywords' => _x( 'report only mode', 'settings search keywords', 'vigilante' ) ), |
| 1539 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Redirect HTTP to HTTPS', 'vigilante' ), 'label_en' => 'Redirect HTTP to HTTPS', 'keywords' => _x( 'redirect http to https redirection forward ssl tls secure', 'settings search keywords', 'vigilante' ) ), |
| 1540 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Fix Mixed Content', 'vigilante' ), 'label_en' => 'Fix Mixed Content', 'keywords' => _x( 'fix mixed content insecure http', 'settings search keywords', 'vigilante' ) ), |
| 1541 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'field-upgrade-insecure-requests', 'label' => __( 'Upgrade Insecure Requests', 'vigilante' ), 'label_en' => 'Upgrade Insecure Requests', 'keywords' => _x( 'upgrade insecure requests mixed content csp https external resources', 'settings search keywords', 'vigilante' ) ), |
| 1542 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Rewrite Site Address on Activation', 'vigilante' ), 'label_en' => 'Rewrite Site Address on Activation', 'keywords' => _x( 'rewrite site address on activation', 'settings search keywords', 'vigilante' ) ), |
| 1543 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Enable HSTS', 'vigilante' ), 'label_en' => 'Enable HSTS', 'keywords' => _x( 'enable hsts strict transport security', 'settings search keywords', 'vigilante' ) ), |
| 1544 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Max Age', 'vigilante' ), 'label_en' => 'Max Age', 'keywords' => _x( 'max age', 'settings search keywords', 'vigilante' ) ), |
| 1545 |
array( 'tab' => 'headers', 'tab_label' => __( 'Security Headers', 'vigilante' ), 'section' => __( 'Security Headers', 'vigilante' ), 'anchor' => 'vigilante-section-headers-main', 'label' => __( 'Include Subdomains', 'vigilante' ), 'label_en' => 'Include Subdomains', 'keywords' => _x( 'include subdomains', 'settings search keywords', 'vigilante' ) ), |
| 1546 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Block Insecure Usernames', 'vigilante' ), 'label_en' => 'Block Insecure Usernames', 'keywords' => _x( 'block insecure usernames blocking blocked deny weak unsafe', 'settings search keywords', 'vigilante' ) ), |
| 1547 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Enforce Strong Passwords', 'vigilante' ), 'label_en' => 'Enforce Strong Passwords', 'keywords' => _x( 'enforce strong passwords complexity password credentials', 'settings search keywords', 'vigilante' ) ), |
| 1548 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Minimum Password Length', 'vigilante' ), 'label_en' => 'Minimum Password Length', 'keywords' => _x( 'minimum password length passwords credentials characters', 'settings search keywords', 'vigilante' ) ), |
| 1549 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Password Requirements', 'vigilante' ), 'label_en' => 'Password Requirements', 'keywords' => _x( 'password requirements passwords credentials', 'settings search keywords', 'vigilante' ) ), |
| 1550 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Apply Password Rules To', 'vigilante' ), 'label_en' => 'Apply Password Rules To', 'keywords' => _x( 'apply password rules to passwords credentials', 'settings search keywords', 'vigilante' ) ), |
| 1551 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Block Author Scanning', 'vigilante' ), 'label_en' => 'Block Author Scanning', 'keywords' => _x( 'block author scanning blocking blocked deny authors enumeration probing', 'settings search keywords', 'vigilante' ) ), |
| 1552 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Username & password protection', 'vigilante' ), 'anchor' => 'vigilante-section-users-password', 'label' => __( 'Display Name Protection', 'vigilante' ), 'label_en' => 'Display Name Protection', 'keywords' => _x( 'display name protection public visible names', 'settings search keywords', 'vigilante' ) ), |
| 1553 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'New Administrator Alert', 'vigilante' ), 'label_en' => 'New Administrator Alert', 'keywords' => _x( 'new administrator alert alerts notification warning', 'settings search keywords', 'vigilante' ) ), |
| 1554 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin Email Change Alert', 'vigilante' ), 'label_en' => 'Admin Email Change Alert', 'keywords' => _x( 'admin email change alert administrator administrators mail notification notify alerts warning', 'settings search keywords', 'vigilante' ) ), |
| 1555 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Permission Elevation Alert', 'vigilante' ), 'label_en' => 'Permission Elevation Alert', 'keywords' => _x( 'permission elevation alert alerts notification warning', 'settings search keywords', 'vigilante' ) ), |
| 1556 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Admin monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-users-admin-monitoring', 'label' => __( 'Admin Password Change Alert', 'vigilante' ), 'label_en' => 'Admin Password Change Alert', 'keywords' => _x( 'admin password change alert administrator administrators passwords credentials alerts notification warning', 'settings search keywords', 'vigilante' ) ), |
| 1557 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Enable Registration Approval', 'vigilante' ), 'label_en' => 'Enable Registration Approval', 'keywords' => _x( 'enable registration approval signup register approve moderate', 'settings search keywords', 'vigilante' ) ), |
| 1558 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Notify Admin', 'vigilante' ), 'label_en' => 'Notify Admin', 'keywords' => _x( 'notify admin notification alert email administrator administrators', 'settings search keywords', 'vigilante' ) ), |
| 1559 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Registration approval', 'vigilante' ), 'anchor' => 'vigilante-section-users-registration', 'label' => __( 'Auto-reject After', 'vigilante' ), 'label_en' => 'Auto-reject After', 'keywords' => _x( 'auto-reject after', 'settings search keywords', 'vigilante' ) ), |
| 1560 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Enable Session Limits', 'vigilante' ), 'label_en' => 'Enable Session Limits', 'keywords' => _x( 'enable session limits sessions concurrent', 'settings search keywords', 'vigilante' ) ), |
| 1561 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Maximum Sessions', 'vigilante' ), 'label_en' => 'Maximum Sessions', 'keywords' => _x( 'maximum sessions session concurrent', 'settings search keywords', 'vigilante' ) ), |
| 1562 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'When Limit Exceeded', 'vigilante' ), 'label_en' => 'When Limit Exceeded', 'keywords' => _x( 'when limit exceeded', 'settings search keywords', 'vigilante' ) ), |
| 1563 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Session limits', 'vigilante' ), 'anchor' => 'vigilante-section-users-sessions', 'label' => __( 'Exclude Administrators', 'vigilante' ), 'label_en' => 'Exclude Administrators', 'keywords' => _x( 'exclude administrators', 'settings search keywords', 'vigilante' ) ), |
| 1564 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Enable Password Expiration', 'vigilante' ), 'label_en' => 'Enable Password Expiration', 'keywords' => _x( 'enable password expiration passwords credentials expiry expire caducity', 'settings search keywords', 'vigilante' ) ), |
| 1565 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Expire After', 'vigilante' ), 'label_en' => 'Expire After', 'keywords' => _x( 'expire after', 'settings search keywords', 'vigilante' ) ), |
| 1566 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Warning Period', 'vigilante' ), 'label_en' => 'Warning Period', 'keywords' => _x( 'warning period', 'settings search keywords', 'vigilante' ) ), |
| 1567 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Password History', 'vigilante' ), 'label_en' => 'Password History', 'keywords' => _x( 'password history passwords credentials reuse previous', 'settings search keywords', 'vigilante' ) ), |
| 1568 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Email Reminder', 'vigilante' ), 'label_en' => 'Email Reminder', 'keywords' => _x( 'email reminder mail notification notify', 'settings search keywords', 'vigilante' ) ), |
| 1569 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Affected Roles', 'vigilante' ), 'label_en' => 'Affected Roles', 'keywords' => _x( 'affected roles role capabilities', 'settings search keywords', 'vigilante' ) ), |
| 1570 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Password expiration', 'vigilante' ), 'anchor' => 'vigilante-section-users-password-exp', 'label' => __( 'Exclude specific users', 'vigilante' ), 'label_en' => 'Exclude specific users', 'keywords' => _x( 'exclude specific users user accounts', 'settings search keywords', 'vigilante' ) ), |
| 1571 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Enable Email Verification', 'vigilante' ), 'label_en' => 'Enable Email Verification', 'keywords' => _x( 'enable email verification mail notification notify verify confirm', 'settings search keywords', 'vigilante' ) ), |
| 1572 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Link Expiration', 'vigilante' ), 'label_en' => 'Link Expiration', 'keywords' => _x( 'link expiration expiry expire caducity', 'settings search keywords', 'vigilante' ) ), |
| 1573 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Allow Resend', 'vigilante' ), 'label_en' => 'Allow Resend', 'keywords' => _x( 'allow resend', 'settings search keywords', 'vigilante' ) ), |
| 1574 |
array( 'tab' => 'users', 'tab_label' => __( 'User Security', 'vigilante' ), 'section' => __( 'Email verification', 'vigilante' ), 'anchor' => 'vigilante-section-users-email-verify', 'label' => __( 'Auto-delete Unverified', 'vigilante' ), 'label_en' => 'Auto-delete Unverified', 'keywords' => _x( 'auto-delete unverified', 'settings search keywords', 'vigilante' ) ), |
| 1575 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'Current prefix', 'vigilante' ), 'label_en' => 'Current prefix', 'keywords' => _x( 'current prefix database db table tables mysql', 'settings search keywords', 'vigilante' ) ), |
| 1576 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Database Hardening', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-database', 'label' => __( 'New prefix', 'vigilante' ), 'label_en' => 'New prefix', 'keywords' => _x( 'new prefix database db table tables mysql', 'settings search keywords', 'vigilante' ) ), |
| 1577 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable File Editor', 'vigilante' ), 'label_en' => 'Disable File Editor', 'keywords' => _x( 'disable file editor files edit editing', 'settings search keywords', 'vigilante' ) ), |
| 1578 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Disable File Modifications', 'vigilante' ), 'label_en' => 'Disable File Modifications', 'keywords' => _x( 'disable file modifications files modify install update', 'settings search keywords', 'vigilante' ) ), |
| 1579 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'wp-config.php Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-wpconfig', 'label' => __( 'Hide PHP errors from visitors', 'vigilante' ), 'label_en' => 'Hide PHP errors from visitors', 'keywords' => _x( 'hide php errors from visitors error debug log', 'settings search keywords', 'vigilante' ) ), |
| 1580 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'XML-RPC', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-xmlrpc', 'label' => __( 'XML-RPC access', 'vigilante' ), 'label_en' => 'XML-RPC access', 'keywords' => _x( 'xml-rpc access xmlrpc rpc remote jetpack app', 'settings search keywords', 'vigilante' ) ), |
| 1581 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Disable Pingbacks', 'vigilante' ), 'label_en' => 'Disable Pingbacks', 'keywords' => _x( 'disable pingbacks pingback ping', 'settings search keywords', 'vigilante' ) ), |
| 1582 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Disable Trackbacks', 'vigilante' ), 'label_en' => 'Disable Trackbacks', 'keywords' => _x( 'disable trackbacks trackback ping', 'settings search keywords', 'vigilante' ) ), |
| 1583 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Require Moderation', 'vigilante' ), 'label_en' => 'Require Moderation', 'keywords' => _x( 'require moderation moderate approve', 'settings search keywords', 'vigilante' ) ), |
| 1584 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Close Old Comments', 'vigilante' ), 'label_en' => 'Close Old Comments', 'keywords' => _x( 'close old comments comment discussion', 'settings search keywords', 'vigilante' ) ), |
| 1585 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Comment Security', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-comments', 'label' => __( 'Honeypot Protection', 'vigilante' ), 'label_en' => 'Honeypot Protection', 'keywords' => _x( 'honeypot protection spam bots trap', 'settings search keywords', 'vigilante' ) ), |
| 1586 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove Generator', 'vigilante' ), 'label_en' => 'Remove Generator', 'keywords' => _x( 'remove generator version meta', 'settings search keywords', 'vigilante' ) ), |
| 1587 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove RSD Link', 'vigilante' ), 'label_en' => 'Remove RSD Link', 'keywords' => _x( 'remove rsd link discovery', 'settings search keywords', 'vigilante' ) ), |
| 1588 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove WLW Manifest', 'vigilante' ), 'label_en' => 'Remove WLW Manifest', 'keywords' => _x( 'remove wlw manifest wlwmanifest', 'settings search keywords', 'vigilante' ) ), |
| 1589 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove Shortlink', 'vigilante' ), 'label_en' => 'Remove Shortlink', 'keywords' => _x( 'remove shortlink link', 'settings search keywords', 'vigilante' ) ), |
| 1590 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'Header Cleanup', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-headers', 'label' => __( 'Remove REST API Link', 'vigilante' ), 'label_en' => 'Remove REST API Link', 'keywords' => _x( 'remove rest api link json endpoint', 'settings search keywords', 'vigilante' ) ), |
| 1591 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'Disable Feeds', 'vigilante' ), 'label_en' => 'Disable Feeds', 'keywords' => _x( 'disable feeds feed rss atom syndication', 'settings search keywords', 'vigilante' ) ), |
| 1592 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'Disable If No Content', 'vigilante' ), 'label_en' => 'Disable If No Content', 'keywords' => _x( 'disable if no content', 'settings search keywords', 'vigilante' ) ), |
| 1593 |
array( 'tab' => 'wp-hardening', 'tab_label' => __( 'WP Hardening', 'vigilante' ), 'section' => __( 'RSS Feed Settings', 'vigilante' ), 'anchor' => 'vigilante-section-hardening-rss', 'label' => __( 'Remove Feed Version', 'vigilante' ), 'label_en' => 'Remove Feed Version', 'keywords' => _x( 'remove feed version feeds rss atom', 'settings search keywords', 'vigilante' ) ), |
| 1594 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Alert on severity', 'vigilante' ), 'label_en' => 'Alert on severity', 'keywords' => _x( 'alert on severity alerts notification warning level critical', 'settings search keywords', 'vigilante' ) ), |
| 1595 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Time window', 'vigilante' ), 'label_en' => 'Time window', 'keywords' => _x( 'time window', 'settings search keywords', 'vigilante' ) ), |
| 1596 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Thresholds per category', 'vigilante' ), 'label_en' => 'Thresholds per category', 'keywords' => _x( 'thresholds per category threshold limit', 'settings search keywords', 'vigilante' ) ), |
| 1597 |
array( 'tab' => 'activity-log', 'tab_label' => __( 'Security Audit', 'vigilante' ), 'section' => __( 'Audit Alerts', 'vigilante' ), 'anchor' => 'vigilante-section-audit-alerts', 'label' => __( 'Recipients', 'vigilante' ), 'label_en' => 'Recipients', 'keywords' => _x( 'recipients email recipient', 'settings search keywords', 'vigilante' ) ), |
| 1598 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Automatic Scans', 'vigilante' ), 'label_en' => 'Automatic Scans', 'keywords' => _x( 'automatic scans scan scanning', 'settings search keywords', 'vigilante' ) ), |
| 1599 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan Frequency', 'vigilante' ), 'label_en' => 'Scan Frequency', 'keywords' => _x( 'scan frequency scans scanning check', 'settings search keywords', 'vigilante' ) ), |
| 1600 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Email Notifications', 'vigilante' ), 'label_en' => 'Email Notifications', 'keywords' => _x( 'email notifications mail notification notify', 'settings search keywords', 'vigilante' ) ), |
| 1601 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Test email', 'vigilante' ), 'label_en' => 'Test email', 'keywords' => _x( 'test email mail notification notify', 'settings search keywords', 'vigilante' ) ), |
| 1602 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Scan Scope', 'vigilante' ), 'label_en' => 'Scan Scope', 'keywords' => _x( 'scan scope scans scanning check', 'settings search keywords', 'vigilante' ) ), |
| 1603 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Excluded Paths', 'vigilante' ), 'label_en' => 'Excluded Paths', 'keywords' => _x( 'excluded paths exclude exclusions ignore ignored path folder folders', 'settings search keywords', 'vigilante' ) ), |
| 1604 |
array( 'tab' => 'file-integrity', 'tab_label' => __( 'File Integrity', 'vigilante' ), 'section' => __( 'File Integrity Monitoring', 'vigilante' ), 'anchor' => 'vigilante-section-fi-monitoring', 'label' => __( 'Excluded Extensions', 'vigilante' ), 'label_en' => 'Excluded Extensions', 'keywords' => _x( 'excluded extensions exclude exclusions ignore ignored extension filetype', 'settings search keywords', 'vigilante' ) ), |
| 1605 |
); |
| 1606 |
} |
| 1607 |
|
| 1608 |
/** |
| 1609 |
* Enqueue admin assets |
| 1610 |
* |
| 1611 |
* @param string $hook Current admin page. |
| 1612 |
*/ |
| 1613 |
public function enqueue_assets( $hook ) { |
| 1614 |
// toplevel_page_vigilante for top-level menu page |
| 1615 |
if ( 'toplevel_page_vigilante' !== $hook ) { |
| 1616 |
return; |
| 1617 |
} |
| 1618 |
|
| 1619 |
wp_enqueue_style( |
| 1620 |
'vigilante-admin', |
| 1621 |
VIGILANTE_ASSETS_URL . 'css/admin.css', |
| 1622 |
array(), |
| 1623 |
VIGILANTE_VERSION |
| 1624 |
); |
| 1625 |
|
| 1626 |
wp_enqueue_script( |
| 1627 |
'vigilante-admin', |
| 1628 |
VIGILANTE_ASSETS_URL . 'js/admin.js', |
| 1629 |
array( 'jquery' ), |
| 1630 |
VIGILANTE_VERSION, |
| 1631 |
true |
| 1632 |
); |
| 1633 |
|
| 1634 |
wp_localize_script( 'vigilante-admin', 'vigilanteAdmin', array( |
| 1635 |
'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 1636 |
'nonce' => wp_create_nonce( 'vigilante_admin_nonce' ), |
| 1637 |
'currentUserId' => get_current_user_id(), |
| 1638 |
'logoutUrl' => wp_logout_url( wp_login_url() ), |
| 1639 |
'adminUrl' => admin_url( 'admin.php?page=vigilante' ), |
| 1640 |
'searchIndex' => $this->get_search_index(), |
| 1641 |
// The scan repaints this table from JavaScript, so the same gate |
| 1642 |
// has to travel with it or half the screen keeps the dead button. |
| 1643 |
'approvalLocked' => $this->critical_approval_locked(), |
| 1644 |
'underAttack' => array( |
| 1645 |
'active' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->is_active(), |
| 1646 |
'remaining' => ( new Vigilante_Under_Attack( $this->settings, $this->activity_log ) )->get_remaining_time(), |
| 1647 |
), |
| 1648 |
'strings' => array( |
| 1649 |
'saving' => __( 'Saving...', 'vigilante' ), |
| 1650 |
'sendingTest' => __( 'Sending...', 'vigilante' ), |
| 1651 |
'saved' => __( 'Settings saved', 'vigilante' ), |
| 1652 |
'error' => __( 'Error saving settings', 'vigilante' ), |
| 1653 |
'confirm' => __( 'Are you sure?', 'vigilante' ), |
| 1654 |
'scanning' => __( 'Scanning...', 'vigilante' ), |
| 1655 |
'scanComplete' => __( 'Scan complete', 'vigilante' ), |
| 1656 |
'loading' => __( 'Loading...', 'vigilante' ), |
| 1657 |
'searching' => __( 'Searching...', 'vigilante' ), |
| 1658 |
'noUsersFound' => __( 'No users found', 'vigilante' ), |
| 1659 |
'searchError' => __( 'Error searching users', 'vigilante' ), |
| 1660 |
'sending' => __( 'Sending...', 'vigilante' ), |
| 1661 |
'sendNotification' => __( 'Send notification now', 'vigilante' ), |
| 1662 |
'notificationsSent' => __( 'notifications sent', 'vigilante' ), |
| 1663 |
'skipped' => __( 'skipped', 'vigilante' ), |
| 1664 |
'failed' => __( 'failed', 'vigilante' ), |
| 1665 |
'customConfig' => __( 'Custom Configuration', 'vigilante' ), |
| 1666 |
// Header tester strings |
| 1667 |
'testHeaders' => __( 'Test Headers', 'vigilante' ), |
| 1668 |
'testing' => __( 'Testing...', 'vigilante' ), |
| 1669 |
'score' => __( 'Score', 'vigilante' ), |
| 1670 |
'enabledHeaders' => __( 'Enabled headers', 'vigilante' ), |
| 1671 |
'missingHeaders' => __( 'Missing headers', 'vigilante' ), |
| 1672 |
'warnings' => __( 'Warnings', 'vigilante' ), |
| 1673 |
// File integrity scan results strings |
| 1674 |
'scanResults' => __( 'Scan Results', 'vigilante' ), |
| 1675 |
'ok' => __( 'OK', 'vigilante' ), |
| 1676 |
'modified' => __( 'Modified', 'vigilante' ), |
| 1677 |
'suspicious' => __( 'Suspicious', 'vigilante' ), |
| 1678 |
'totalScanned' => __( 'Total Scanned', 'vigilante' ), |
| 1679 |
'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ), |
| 1680 |
'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ), |
| 1681 |
'file' => __( 'File', 'vigilante' ), |
| 1682 |
'reason' => __( 'Reason', 'vigilante' ), |
| 1683 |
'type' => __( 'Type', 'vigilante' ), |
| 1684 |
'unknown' => __( 'Unknown', 'vigilante' ), |
| 1685 |
'modifiedFiles' => __( 'Modified Files', 'vigilante' ), |
| 1686 |
'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ), |
| 1687 |
'extraFiles' => __( 'Extra Files', 'vigilante' ), |
| 1688 |
'extra' => __( 'Extra', 'vigilante' ), |
| 1689 |
'ignored' => __( 'Ignored', 'vigilante' ), |
| 1690 |
'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ), |
| 1691 |
'actions' => __( 'Actions', 'vigilante' ), |
| 1692 |
'ignore' => __( 'Ignore', 'vigilante' ), |
| 1693 |
'ignoring' => __( 'Ignoring...', 'vigilante' ), |
| 1694 |
'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ), |
| 1695 |
'fileUnignored' => __( 'File removed from ignored list.', 'vigilante' ), |
| 1696 |
'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1697 |
'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ), |
| 1698 |
'selectAll' => __( 'Select all', 'vigilante' ), |
| 1699 |
'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ), |
| 1700 |
'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ), |
| 1701 |
'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ), |
| 1702 |
'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ), |
| 1703 |
'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1704 |
'bulkProcessing' => __( 'Processing...', 'vigilante' ), |
| 1705 |
/* translators: %d: number of files selected for bulk action. */ |
| 1706 |
'bulkSelectedCount' => __( '%d selected', 'vigilante' ), |
| 1707 |
'allClear' => __( 'All files verified - no issues found!', 'vigilante' ), |
| 1708 |
'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ), |
| 1709 |
'criticalConfigDesc' => __( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ), |
| 1710 |
'approve' => __( 'Approve', 'vigilante' ), |
| 1711 |
'approving' => __( 'Approving...', 'vigilante' ), |
| 1712 |
'approvalLockedNotice' => $this->critical_approval_notice(), |
| 1713 |
'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ), |
| 1714 |
'reviewChanges' => __( 'Review changes', 'vigilante' ), |
| 1715 |
'hideChanges' => __( 'Hide changes', 'vigilante' ), |
| 1716 |
'changes' => __( 'Changes', 'vigilante' ), |
| 1717 |
'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ), |
| 1718 |
'diffNetwork' => __( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ), |
| 1719 |
'diffRescan' => __( 'Run a new scan to see the line changes of this file.', 'vigilante' ), |
| 1720 |
'diffRedaction' => __( 'The line changes of this file are not shown because a value in it could not be hidden safely. The change itself is still detected.', 'vigilante' ), |
| 1721 |
'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ), |
| 1722 |
'diffLines' => __( 'lines', 'vigilante' ), |
| 1723 |
// Under Attack mode strings |
| 1724 |
'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ), |
| 1725 |
'underAttackConfirmDeactivate' => __( 'Deactivate Under Attack mode?', 'vigilante' ), |
| 1726 |
'underAttackActivating' => __( 'Activating...', 'vigilante' ), |
| 1727 |
'underAttackDeactivating' => __( 'Deactivating...', 'vigilante' ), |
| 1728 |
// Database backup strings |
| 1729 |
'dbBackupDownloading' => __( 'Generating backup...', 'vigilante' ), |
| 1730 |
'dbBackupNoTables' => __( 'Please select at least one table.', 'vigilante' ), |
| 1731 |
'dbBackupSuccess' => __( 'Database backup downloaded successfully.', 'vigilante' ), |
| 1732 |
// Firewall unblock |
| 1733 |
'confirmUnblockIp' => __( 'Unblock this IP from firewall rate limiting?', 'vigilante' ), |
| 1734 |
// Database prefix strings |
| 1735 |
'dbPrefixConfirm' => __( 'This operation will change your database prefix. It is irreversible. Make sure you have a current database backup before proceeding.', 'vigilante' ), |
| 1736 |
'dbPrefixChanging' => __( 'Changing prefix...', 'vigilante' ), |
| 1737 |
'dbPrefixSuccess' => __( 'Database prefix changed successfully. The page will reload now.', 'vigilante' ), |
| 1738 |
'dbPrefixCheckbox' => __( 'You must confirm that you have a database backup.', 'vigilante' ), |
| 1739 |
/* translators: 1: Hours, 2: Minutes */ |
| 1740 |
'underAttackRemaining' => __( '%1$dh %2$dm remaining', 'vigilante' ), |
| 1741 |
'underAttackLabel' => __( 'Under Attack', 'vigilante' ), |
| 1742 |
'standardLabel' => __( 'Standard', 'vigilante' ), |
| 1743 |
'maximumLabel' => __( 'Maximum Security', 'vigilante' ), |
| 1744 |
'deactivate' => __( 'Deactivate', 'vigilante' ), |
| 1745 |
'underAttackActivate' => __( 'Activate for 4 hours', 'vigilante' ), |
| 1746 |
// Settings strings |
| 1747 |
'saveSettings' => __( 'Save Settings', 'vigilante' ), |
| 1748 |
'settingsResetDefaults' => __( 'Settings reset to defaults.', 'vigilante' ), |
| 1749 |
'confirmOverwrite' => __( 'This will overwrite your current settings.', 'vigilante' ), |
| 1750 |
'importFailed' => __( 'Could not import settings. Check the file and try again.', 'vigilante' ), |
| 1751 |
/* translators: 1: tests passed, 2: total tests in this category */ |
| 1752 |
'testsCounter' => __( '%1$d/%2$d tests', 'vigilante' ), |
| 1753 |
'confirmResetAll' => __( 'This will reset ALL settings to defaults.', 'vigilante' ), |
| 1754 |
'couldNotDetermineSection' => __( 'Could not determine section.', 'vigilante' ), |
| 1755 |
'confirmResetSection' => __( 'Reset this section to default values? This cannot be undone.', 'vigilante' ), |
| 1756 |
'sectionResetDefaults' => __( 'Section reset to defaults.', 'vigilante' ), |
| 1757 |
/* translators: %s: preset name */ |
| 1758 |
'confirmApplyPreset' => __( 'Apply the "%s" preset?', 'vigilante' ), |
| 1759 |
// Scan strings |
| 1760 |
'scanFailed' => __( 'Scan failed', 'vigilante' ), |
| 1761 |
/* translators: %s: error message */ |
| 1762 |
'scanError' => __( 'Scan error: %s', 'vigilante' ), |
| 1763 |
'runScanNow' => __( 'Run Scan Now', 'vigilante' ), |
| 1764 |
'confirmClearScan' => __( 'Are you sure you want to clear all scan results?', 'vigilante' ), |
| 1765 |
'clearing' => __( 'Clearing...', 'vigilante' ), |
| 1766 |
'scanResultsCleared' => __( 'Scan results cleared. Page will reload...', 'vigilante' ), |
| 1767 |
'failedClearResults' => __( 'Failed to clear results', 'vigilante' ), |
| 1768 |
/* translators: %s: error message */ |
| 1769 |
'ajaxError' => __( 'AJAX Error: %s', 'vigilante' ), |
| 1770 |
// Activity log popup strings |
| 1771 |
'logRequest' => __( 'Request', 'vigilante' ), |
| 1772 |
'logDate' => __( 'Date', 'vigilante' ), |
| 1773 |
'logMethod' => __( 'Method', 'vigilante' ), |
| 1774 |
'logType' => __( 'Type', 'vigilante' ), |
| 1775 |
'logAction' => __( 'Action', 'vigilante' ), |
| 1776 |
'logSeverity' => __( 'Severity', 'vigilante' ), |
| 1777 |
'logMessage' => __( 'Message', 'vigilante' ), |
| 1778 |
'logRequestUri' => __( 'Address', 'vigilante' ), |
| 1779 |
'logClient' => __( 'Client', 'vigilante' ), |
| 1780 |
'logUser' => __( 'User', 'vigilante' ), |
| 1781 |
'logIpAddress' => __( 'IP Address', 'vigilante' ), |
| 1782 |
'logUserAgent' => __( 'User Agent', 'vigilante' ), |
| 1783 |
'logIpLabel' => __( 'IP:', 'vigilante' ), |
| 1784 |
'logUaLabel' => __( 'UA:', 'vigilante' ), |
| 1785 |
'logWhitelist' => __( 'Whitelist', 'vigilante' ), |
| 1786 |
'logBlacklist' => __( 'Blacklist', 'vigilante' ), |
| 1787 |
'logInWhitelist' => __( 'In whitelist', 'vigilante' ), |
| 1788 |
'logInBlacklist' => __( 'In blacklist', 'vigilante' ), |
| 1789 |
'logAdded' => __( 'Added!', 'vigilante' ), |
| 1790 |
'logErrorAddingToList' => __( 'Error adding to list', 'vigilante' ), |
| 1791 |
'logRequestFailed' => __( 'Request failed', 'vigilante' ), |
| 1792 |
// Activity log table strings |
| 1793 |
'noLogEntries' => __( 'No log entries found.', 'vigilante' ), |
| 1794 |
'view' => __( 'View', 'vigilante' ), |
| 1795 |
'confirmClearLogs' => __( 'This will delete all audit logs.', 'vigilante' ), |
| 1796 |
// Export logs strings |
| 1797 |
'exporting' => __( 'Exporting...', 'vigilante' ), |
| 1798 |
/* translators: %d: number of entries */ |
| 1799 |
'logsExported' => __( 'Logs exported (%d entries)', 'vigilante' ), |
| 1800 |
'noLogsToExport' => __( 'No logs to export', 'vigilante' ), |
| 1801 |
'exportFailed' => __( 'Export failed', 'vigilante' ), |
| 1802 |
'exportLogs' => __( 'Export Logs', 'vigilante' ), |
| 1803 |
// Backup strings |
| 1804 |
'backupCreated' => __( 'Backup created successfully.', 'vigilante' ), |
| 1805 |
'createBackupNow' => __( 'Download Backup', 'vigilante' ), |
| 1806 |
'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ), |
| 1807 |
/* translators: %d: number of tables */ |
| 1808 |
'tablesCount' => __( '%d tables', 'vigilante' ), |
| 1809 |
/* translators: 1: table count, 2: human-readable size */ |
| 1810 |
'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ), |
| 1811 |
/* translators: 1: selected count, 2: human-readable size */ |
| 1812 |
'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ), |
| 1813 |
// Settings search strings |
| 1814 |
'searchNoResults' => __( 'No matching settings found.', 'vigilante' ), |
| 1815 |
/* translators: %d: number of results that did not fit in the list. */ |
| 1816 |
'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ), |
| 1817 |
'searchInTab' => __( 'in', 'vigilante' ), |
| 1818 |
// Modules string |
| 1819 |
/* translators: 1: enabled count, 2: total count */ |
| 1820 |
'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ), |
| 1821 |
// Activity log label maps for JS rendering |
| 1822 |
'eventTypeLabels' => array( |
| 1823 |
'login' => __( 'Login', 'vigilante' ), |
| 1824 |
'user' => __( 'User', 'vigilante' ), |
| 1825 |
'content' => __( 'Content', 'vigilante' ), |
| 1826 |
'plugin' => __( 'Plugin', 'vigilante' ), |
| 1827 |
'theme' => __( 'Theme', 'vigilante' ), |
| 1828 |
'settings' => __( 'Settings', 'vigilante' ), |
| 1829 |
'comment' => __( 'Comment', 'vigilante' ), |
| 1830 |
'media' => __( 'Media', 'vigilante' ), |
| 1831 |
'firewall' => __( 'Firewall', 'vigilante' ), |
| 1832 |
'file' => __( 'File', 'vigilante' ), |
| 1833 |
'security' => __( 'Security', 'vigilante' ), |
| 1834 |
'system' => __( 'System', 'vigilante' ), |
| 1835 |
), |
| 1836 |
'severityLabels' => array( |
| 1837 |
'info' => __( 'Info', 'vigilante' ), |
| 1838 |
'warning' => __( 'Warning', 'vigilante' ), |
| 1839 |
'critical' => __( 'Critical', 'vigilante' ), |
| 1840 |
), |
| 1841 |
// Password reset strings |
| 1842 |
'noUsersFoundSearch' => __( 'No users found', 'vigilante' ), |
| 1843 |
/* translators: %d: number of users */ |
| 1844 |
'confirmForceReset' => __( 'Force password reset for %d user(s)? A password reset email will be sent to each user.', 'vigilante' ), |
| 1845 |
'warningResettingSelf' => __( 'WARNING: You are including yourself. Your session will end and you will need to set a new password.', 'vigilante' ), |
| 1846 |
'processing' => __( 'Processing...', 'vigilante' ), |
| 1847 |
'anErrorOccurred' => __( 'An error occurred', 'vigilante' ), |
| 1848 |
'forceResetSelected' => __( 'Force Reset for Selected Users', 'vigilante' ), |
| 1849 |
'confirmForceResetAll' => __( 'This will force ALL users to reset their password. All users will receive a password reset email. Are you sure you want to continue?', 'vigilante' ), |
| 1850 |
'warningResettingSelfAll' => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ), |
| 1851 |
'forceResetAll' => __( 'Force Reset for ALL Users', 'vigilante' ), |
| 1852 |
// Role-based password reset strings |
| 1853 |
/* translators: %d: number of users */ |
| 1854 |
'confirmForceResetByRole' => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ), |
| 1855 |
'noRolesSelected' => __( 'Please select at least one role.', 'vigilante' ), |
| 1856 |
'forceResetByRole' => __( 'Force Reset for Selected Roles', 'vigilante' ), |
| 1857 |
// User approval strings |
| 1858 |
'confirmApprove' => __( 'Approve this user?', 'vigilante' ), |
| 1859 |
'approve' => __( 'Approve', 'vigilante' ), |
| 1860 |
'rejectReason' => __( 'Enter rejection reason (optional):', 'vigilante' ), |
| 1861 |
'reject' => __( 'Reject', 'vigilante' ), |
| 1862 |
'noPending' => __( 'No pending registrations.', 'vigilante' ), |
| 1863 |
// Session management strings |
| 1864 |
'confirmRevoke' => __( 'Revoke this session?', 'vigilante' ), |
| 1865 |
'confirmRevokeAll' => __( 'Revoke all other sessions?', 'vigilante' ), |
| 1866 |
'revokeOthers' => __( 'Revoke All Other Sessions', 'vigilante' ), |
| 1867 |
'confirmRevokeAllUser' => __( 'Revoke ALL sessions for this user? They will be logged out everywhere.', 'vigilante' ), |
| 1868 |
'sessionsFor' => __( 'Sessions for:', 'vigilante' ), |
| 1869 |
'noSessions' => __( 'No active sessions', 'vigilante' ), |
| 1870 |
'revoke' => __( 'Revoke', 'vigilante' ), |
| 1871 |
'noUsers' => __( 'No users found', 'vigilante' ), |
| 1872 |
// Time ago strings |
| 1873 |
'timeYear' => __( 'year', 'vigilante' ), |
| 1874 |
'timeYears' => __( 'years', 'vigilante' ), |
| 1875 |
'timeMonth' => __( 'month', 'vigilante' ), |
| 1876 |
'timeMonths' => __( 'months', 'vigilante' ), |
| 1877 |
'timeDay' => __( 'day', 'vigilante' ), |
| 1878 |
'timeDays' => __( 'days', 'vigilante' ), |
| 1879 |
'timeHour' => __( 'hour', 'vigilante' ), |
| 1880 |
'timeHours' => __( 'hours', 'vigilante' ), |
| 1881 |
'timeMinute' => __( 'minute', 'vigilante' ), |
| 1882 |
'timeMinutes' => __( 'minutes', 'vigilante' ), |
| 1883 |
/* translators: %1$d: count, %2$s: time unit */ |
| 1884 |
'timeAgo' => __( '%1$d %2$s ago', 'vigilante' ), |
| 1885 |
'justNow' => __( 'Just now', 'vigilante' ), |
| 1886 |
// Pagination strings |
| 1887 |
/* translators: 1: first item number, 2: last item number, 3: total items */ |
| 1888 |
'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ), |
| 1889 |
'paginationEmpty' => __( '0 items', 'vigilante' ), |
| 1890 |
// Security Analyzer strings |
| 1891 |
'analyzerScanNow' => __( 'Scan now', 'vigilante' ), |
| 1892 |
'analyzerScanning' => __( 'Scanning…', 'vigilante' ), |
| 1893 |
'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ), |
| 1894 |
'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ), |
| 1895 |
'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ), |
| 1896 |
'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ), |
| 1897 |
'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ), |
| 1898 |
'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ), |
| 1899 |
'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ), |
| 1900 |
'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ), |
| 1901 |
'analyzerJustNow' => __( 'just now', 'vigilante' ), |
| 1902 |
'analyzerAgo' => __( 'ago', 'vigilante' ), |
| 1903 |
'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ), |
| 1904 |
'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ), |
| 1905 |
'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ), |
| 1906 |
'analyzerQualityGood' => __( 'Good', 'vigilante' ), |
| 1907 |
'analyzerQualityFair' => __( 'Fair', 'vigilante' ), |
| 1908 |
'analyzerQualityPoor' => __( 'Poor', 'vigilante' ), |
| 1909 |
'analyzerQualityCritical' => __( 'Critical', 'vigilante' ), |
| 1910 |
'analyzerPts' => __( 'pts', 'vigilante' ), |
| 1911 |
'analyzerLearnMore' => __( 'Learn more', 'vigilante' ), |
| 1912 |
'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ), |
| 1913 |
/* translators: %d: number of findings in an info-only category */ |
| 1914 |
'analyzerInfoFindings' => __( '%d findings', 'vigilante' ), |
| 1915 |
), |
| 1916 |
) ); |
| 1917 |
|
| 1918 |
// 2FA Admin assets |
| 1919 |
wp_enqueue_style( |
| 1920 |
'vigilante-2fa-admin', |
| 1921 |
VIGILANTE_ASSETS_URL . 'css/two-factor-admin.css', |
| 1922 |
array( 'vigilante-admin' ), |
| 1923 |
VIGILANTE_VERSION |
| 1924 |
); |
| 1925 |
|
| 1926 |
wp_enqueue_script( |
| 1927 |
'vigilante-2fa-admin', |
| 1928 |
VIGILANTE_ASSETS_URL . 'js/two-factor-admin.js', |
| 1929 |
array( 'jquery', 'vigilante-admin' ), |
| 1930 |
VIGILANTE_VERSION, |
| 1931 |
true |
| 1932 |
); |
| 1933 |
} |
| 1934 |
|
| 1935 |
/** |
| 1936 |
* Show admin notices |
| 1937 |
*/ |
| 1938 |
public function show_admin_notices() { |
| 1939 |
// Activation notice |
| 1940 |
if ( get_transient( 'vigilante_activated' ) ) { |
| 1941 |
?> |
| 1942 |
<div class="notice notice-success is-dismissible vigilante-activation-notice"> |
| 1943 |
<p> |
| 1944 |
<span class="dashicons dashicons-shield vigilante-notice-icon"></span> |
| 1945 |
<strong><?php esc_html_e( 'Vigilant activated successfully!', 'vigilante' ); ?></strong> |
| 1946 |
<?php esc_html_e( 'Security protection is now active.', 'vigilante' ); ?> |
| 1947 |
<a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante' ) ); ?>"> |
| 1948 |
<?php esc_html_e( 'Configure settings', 'vigilante' ); ?> |
| 1949 |
</a> |
| 1950 |
</p> |
| 1951 |
</div> |
| 1952 |
<?php |
| 1953 |
delete_transient( 'vigilante_activated' ); |
| 1954 |
} |
| 1955 |
|
| 1956 |
// Under Attack mode notice (non-dismissible, shown on all admin pages) |
| 1957 |
$ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() ); |
| 1958 |
if ( ! empty( $ua_status['active'] ) ) { |
| 1959 |
$ua_remaining = ( $ua_status['activated_at'] + $ua_status['duration'] ) - time(); |
| 1960 |
if ( $ua_remaining > 0 ) { |
| 1961 |
$ua_hours = floor( $ua_remaining / 3600 ); |
| 1962 |
$ua_mins = floor( ( $ua_remaining % 3600 ) / 60 ); |
| 1963 |
$dashboard_url = admin_url( 'admin.php?page=vigilante' ); |
| 1964 |
?> |
| 1965 |
<div class="notice notice-warning vigilante-ua-notice"> |
| 1966 |
<p> |
| 1967 |
<span class="dashicons dashicons-shield"></span> |
| 1968 |
<strong><?php esc_html_e( 'Under Attack mode is active', 'vigilante' ); ?></strong> |
| 1969 |
— |
| 1970 |
<?php |
| 1971 |
printf( |
| 1972 |
/* translators: 1: Hours, 2: Minutes */ |
| 1973 |
esc_html__( '%1$dh %2$dm remaining.', 'vigilante' ), |
| 1974 |
absint( $ua_hours ), |
| 1975 |
absint( $ua_mins ) |
| 1976 |
); |
| 1977 |
?> |
| 1978 |
<a href="<?php echo esc_url( $dashboard_url ); ?>"> |
| 1979 |
<?php esc_html_e( 'Go to Vigilant dashboard', 'vigilante' ); ?> |
| 1980 |
</a> |
| 1981 |
</p> |
| 1982 |
<p> |
| 1983 |
<em><?php esc_html_e( 'Vigilant has applied the Maximum preset plus extra hardening on top of your previous configuration. Any changes you make to Vigilant settings while this mode is active will be reverted when it ends.', 'vigilante' ); ?></em> |
| 1984 |
</p> |
| 1985 |
<?php |
| 1986 |
// The cache-bypass rules could not be written (a host where |
| 1987 |
// WordPress cannot write files by itself, a held lock, a |
| 1988 |
// failed read-back): show them, so they can be added by hand. |
| 1989 |
$ua_instance = new Vigilante_Under_Attack( $this->settings, $this->activity_log ); |
| 1990 |
if ( $ua_instance->cache_rules_missing() ) : |
| 1991 |
?> |
| 1992 |
<p> |
| 1993 |
<strong><?php esc_html_e( 'The cache-bypass rules could not be written to your .htaccess.', 'vigilante' ); ?></strong> |
| 1994 |
<?php esc_html_e( 'Without them a page cache may keep serving stored pages during the attack. Add this block at the top of the .htaccess in your site root (the activity log records why it was not written):', 'vigilante' ); ?> |
| 1995 |
</p> |
| 1996 |
<textarea readonly rows="9" class="large-text code" onclick="this.select();"><?php echo esc_textarea( Vigilante_Under_Attack::get_cache_bypass_block() ); ?></textarea> |
| 1997 |
<?php endif; ?> |
| 1998 |
</div> |
| 1999 |
<?php |
| 2000 |
} |
| 2001 |
} |
| 2002 |
|
| 2003 |
// Proxy/CDN detection: if IP detection is set to "direct" but requests |
| 2004 |
// arrive with a forwarded-for header carrying a different valid IP, the |
| 2005 |
// site is very likely behind a proxy/CDN that has not been declared, so |
| 2006 |
// the firewall is seeing the proxy IP for every visitor. Guide the admin. |
| 2007 |
// |
| 2008 |
// Two deliberate silencers (2.9.2): |
| 2009 |
// - A loopback forwarded IP (::1 / 127.x) means the person browsing IS |
| 2010 |
// the machine itself: that only happens in local development stacks |
| 2011 |
// (Local's nginx router, Docker...), never behind a production |
| 2012 |
// proxy/CDN, so the notice would be pure noise there. |
| 2013 |
// - The notice is dismissible and the dismissal persists site-wide via |
| 2014 |
// the vigilante_dismissed_notices option (the admin evaluated it and |
| 2015 |
// decided; nagging forever helps nobody). |
| 2016 |
$dismissed_notices = get_option( 'vigilante_dismissed_notices', array() ); |
| 2017 |
if ( |
| 2018 |
current_user_can( 'manage_options' ) |
| 2019 |
&& '' === Vigilante_IP_Utils::trusted_proxy_header() |
| 2020 |
&& ! isset( $dismissed_notices['proxy_detection'] ) |
| 2021 |
) { |
| 2022 |
$remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; |
| 2023 |
$detected = ''; |
| 2024 |
foreach ( Vigilante_IP_Utils::trusted_header_map() as $proxy_label => $server_key ) { |
| 2025 |
if ( empty( $_SERVER[ $server_key ] ) ) { |
| 2026 |
continue; |
| 2027 |
} |
| 2028 |
$candidate = sanitize_text_field( wp_unslash( $_SERVER[ $server_key ] ) ); |
| 2029 |
if ( false !== strpos( $candidate, ',' ) ) { |
| 2030 |
$parts = explode( ',', $candidate ); |
| 2031 |
$candidate = trim( $parts[0] ); |
| 2032 |
} |
| 2033 |
if ( ! filter_var( $candidate, FILTER_VALIDATE_IP ) || $candidate === $remote ) { |
| 2034 |
continue; |
| 2035 |
} |
| 2036 |
// Loopback forwarded IP = local development, not a real proxy. |
| 2037 |
if ( '::1' === $candidate || 0 === strpos( $candidate, '127.' ) ) { |
| 2038 |
continue; |
| 2039 |
} |
| 2040 |
$detected = $proxy_label; |
| 2041 |
break; |
| 2042 |
} |
| 2043 |
|
| 2044 |
if ( '' !== $detected ) { |
| 2045 |
$firewall_url = admin_url( 'admin.php?page=vigilante&tab=firewall' ); |
| 2046 |
$detail_message = sprintf( |
| 2047 |
/* translators: %s: detected forwarded header name wrapped in a code tag. */ |
| 2048 |
esc_html__( 'Requests are arriving with a %s header, but visitor IP detection is set to direct connection. The firewall is reading the proxy address instead of the real visitor IP, which affects the IP lists and rate limiting.', 'vigilante' ), |
| 2049 |
'<code>' . esc_html( strtoupper( $detected ) ) . '</code>' |
| 2050 |
); |
| 2051 |
?> |
| 2052 |
<div class="notice notice-warning is-dismissible vigilante-proxy-notice"> |
| 2053 |
<p> |
| 2054 |
<span class="dashicons dashicons-shield"></span> |
| 2055 |
<strong><?php esc_html_e( 'Vigilant: this site looks like it is behind a proxy or CDN', 'vigilante' ); ?></strong> |
| 2056 |
</p> |
| 2057 |
<p><?php echo wp_kses_post( $detail_message ); ?></p> |
| 2058 |
<p> |
| 2059 |
<a href="<?php echo esc_url( $firewall_url ); ?>" class="button button-secondary"> |
| 2060 |
<?php esc_html_e( 'Set your proxy in Firewall settings', 'vigilante' ); ?> |
| 2061 |
</a> |
| 2062 |
</p> |
| 2063 |
</div> |
| 2064 |
<script> |
| 2065 |
jQuery( function ( $ ) { |
| 2066 |
$( document ).on( 'click', '.vigilante-proxy-notice .notice-dismiss', function () { |
| 2067 |
$.post( ajaxurl, { |
| 2068 |
action: 'vigilante_dismiss_notice', |
| 2069 |
notice_id: 'proxy_detection', |
| 2070 |
nonce: <?php echo wp_json_encode( wp_create_nonce( 'vigilante_dismiss_notice' ) ); ?> |
| 2071 |
} ); |
| 2072 |
} ); |
| 2073 |
} ); |
| 2074 |
</script> |
| 2075 |
<?php |
| 2076 |
} |
| 2077 |
} |
| 2078 |
} |
| 2079 |
|
| 2080 |
/** |
| 2081 |
* Render settings page |
| 2082 |
*/ |
| 2083 |
public function render_settings_page() { |
| 2084 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 2085 |
$this->current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'dashboard'; |
| 2086 |
|
| 2087 |
if ( ! array_key_exists( $this->current_tab, $this->tabs ) ) { |
| 2088 |
$this->current_tab = 'dashboard'; |
| 2089 |
} |
| 2090 |
?> |
| 2091 |
<div class="wrap vigilante-admin-wrap"> |
| 2092 |
<div class="vigilante-header-row"> |
| 2093 |
<h1 class="vigilante-page-title"> |
| 2094 |
<img src="<?php echo esc_url( VIGILANTE_ASSETS_URL . 'images/icon.png' ); ?>" alt="Vigilante" class="vigilante-title-icon"> |
| 2095 |
<?php esc_html_e( 'Vigilant', 'vigilante' ); ?> |
| 2096 |
<span class="vigilante-version">v<?php echo esc_html( VIGILANTE_VERSION ); ?></span> |
| 2097 |
</h1> |
| 2098 |
<div class="vigilante-search-wrapper"> |
| 2099 |
<div class="vigilante-search-input-wrap"> |
| 2100 |
<span class="vigilante-search-icon dashicons dashicons-search" aria-hidden="true"></span> |
| 2101 |
<input type="search" id="vigilante-settings-search" class="vigilante-settings-search" aria-label="<?php esc_attr_e( 'Search settings', 'vigilante' ); ?>" placeholder="<?php esc_attr_e( 'Search settings…', 'vigilante' ); ?>" autocomplete="off"> |
| 2102 |
<span class="vigilante-search-shortcut" aria-hidden="true">/</span> |
| 2103 |
</div> |
| 2104 |
<div id="vigilante-settings-search-results" class="vigilante-search-results" hidden role="listbox"></div> |
| 2105 |
</div> |
| 2106 |
</div> |
| 2107 |
|
| 2108 |
<?php $this->render_tabs(); ?> |
| 2109 |
|
| 2110 |
<div class="vigilante-content"> |
| 2111 |
<div class="vigilante-main"> |
| 2112 |
<?php $this->render_tab_content(); ?> |
| 2113 |
</div> |
| 2114 |
<div class="vigilante-sidebar"> |
| 2115 |
<?php $this->render_sidebar(); ?> |
| 2116 |
</div> |
| 2117 |
</div> |
| 2118 |
</div> |
| 2119 |
<?php |
| 2120 |
} |
| 2121 |
|
| 2122 |
/** |
| 2123 |
* Render tabs navigation |
| 2124 |
*/ |
| 2125 |
private function render_tabs() { |
| 2126 |
$options = $this->settings->get_all_options(); |
| 2127 |
|
| 2128 |
// Map tabs to modules |
| 2129 |
$tab_to_module = array( |
| 2130 |
'firewall' => 'firewall', |
| 2131 |
'headers' => 'security_headers', |
| 2132 |
'login' => 'login_security', |
| 2133 |
'rest-api' => 'rest_api_security', |
| 2134 |
'users' => 'user_security', |
| 2135 |
'wp-hardening' => 'wp_hardening', |
| 2136 |
'file-integrity' => 'file_integrity', |
| 2137 |
'activity-log' => 'activity_log', |
| 2138 |
); |
| 2139 |
?> |
| 2140 |
<nav class="nav-tab-wrapper vigilante-nav-tabs"> |
| 2141 |
<?php foreach ( $this->tabs as $tab_id => $tab_name ) : |
| 2142 |
$is_disabled = false; |
| 2143 |
$module = isset( $tab_to_module[ $tab_id ] ) ? $tab_to_module[ $tab_id ] : null; |
| 2144 |
if ( $module && empty( $options['modules'][ $module ] ) ) { |
| 2145 |
$is_disabled = true; |
| 2146 |
} |
| 2147 |
?> |
| 2148 |
<a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $tab_id ) ); ?>" |
| 2149 |
class="nav-tab <?php echo $this->current_tab === $tab_id ? 'nav-tab-active' : ''; ?> <?php echo $is_disabled ? 'vigilante-tab-disabled' : ''; ?>" |
| 2150 |
<?php if ( $is_disabled ) : ?>title="<?php esc_attr_e( 'Module Disabled', 'vigilante' ); ?>"<?php endif; ?>> |
| 2151 |
<?php echo esc_html( $tab_name ); ?> |
| 2152 |
<?php if ( $is_disabled ) : ?><span class="vigilante-tab-off">OFF</span><?php endif; ?> |
| 2153 |
</a> |
| 2154 |
<?php endforeach; ?> |
| 2155 |
</nav> |
| 2156 |
<?php |
| 2157 |
} |
| 2158 |
|
| 2159 |
/** |
| 2160 |
* Render current tab content |
| 2161 |
*/ |
| 2162 |
private function render_tab_content() { |
| 2163 |
$method = 'render_tab_' . str_replace( '-', '_', $this->current_tab ); |
| 2164 |
|
| 2165 |
if ( method_exists( $this, $method ) ) { |
| 2166 |
$this->$method(); |
| 2167 |
} else { |
| 2168 |
$this->render_tab_coming_soon(); |
| 2169 |
} |
| 2170 |
} |
| 2171 |
|
| 2172 |
/** |
| 2173 |
* Render coming soon placeholder |
| 2174 |
*/ |
| 2175 |
private function render_tab_coming_soon() { |
| 2176 |
?> |
| 2177 |
<div class="vigilante-settings-section"> |
| 2178 |
<h2><?php esc_html_e( 'Coming Soon', 'vigilante' ); ?></h2> |
| 2179 |
<p><?php esc_html_e( 'This section is under development.', 'vigilante' ); ?></p> |
| 2180 |
</div> |
| 2181 |
<?php |
| 2182 |
} |
| 2183 |
|
| 2184 |
/** |
| 2185 |
* Values to display for a section that this site does not control |
| 2186 |
* |
| 2187 |
* On a subsite the stored options are its own copy, which nothing acts on: |
| 2188 |
* wp-config.php and .htaccess are written from the main site. Painting the |
| 2189 |
* local copy describes a configuration that is not running, so a subsite |
| 2190 |
* admin sees a box ticked here and the constant absent from the file, or the |
| 2191 |
* other way round. Read the main site's values instead, which are the ones in |
| 2192 |
* force, and fall back to the local ones if they cannot be read. |
| 2193 |
* |
| 2194 |
* @since 2.9.8 |
| 2195 |
* |
| 2196 |
* @param string $section Settings section. |
| 2197 |
* @return array |
| 2198 |
*/ |
| 2199 |
private function get_section_for_display( $section ) { |
| 2200 |
$local = $this->settings->get_section( $section ); |
| 2201 |
|
| 2202 |
if ( ! $this->shared_files_locked() ) { |
| 2203 |
return $local; |
| 2204 |
} |
| 2205 |
|
| 2206 |
// shared_files_locked() is only true on multisite, where get_blog_option() exists. |
| 2207 |
$main = get_blog_option( get_main_site_id(), Vigilante_Settings::OPTION_NAME, array() ); |
| 2208 |
|
| 2209 |
if ( ! is_array( $main ) || empty( $main[ $section ] ) || ! is_array( $main[ $section ] ) ) { |
| 2210 |
return $local; |
| 2211 |
} |
| 2212 |
|
| 2213 |
return wp_parse_args( $main[ $section ], $local ); |
| 2214 |
} |
| 2215 |
|
| 2216 |
/** |
| 2217 |
* Whether the sections that write wp-config.php and .htaccess are read-only here |
| 2218 |
* |
| 2219 |
* True on a network when this is not the main site, or the user is not a |
| 2220 |
* network administrator. See Vigilante_Settings::can_write_shared_files(). |
| 2221 |
* |
| 2222 |
* @since 2.9.8 |
| 2223 |
* |
| 2224 |
* @return bool |
| 2225 |
*/ |
| 2226 |
private function shared_files_locked() { |
| 2227 |
return ! Vigilante_Settings::can_write_shared_files(); |
| 2228 |
} |
| 2229 |
|
| 2230 |
/** |
| 2231 |
* Whether this is the main site and the user cannot change what it builds the shared files from |
| 2232 |
* |
| 2233 |
* See Vigilante_Settings::get_main_site_file_settings(). On a subsite those |
| 2234 |
* settings only act on that site, so they are never locked there. |
| 2235 |
* |
| 2236 |
* @since 2.11.6 |
| 2237 |
* |
| 2238 |
* @return bool |
| 2239 |
*/ |
| 2240 |
private function main_site_files_locked() { |
| 2241 |
return $this->shared_files_locked() && Vigilante_Settings::owns_shared_files(); |
| 2242 |
} |
| 2243 |
|
| 2244 |
/** |
| 2245 |
* Sentence added to a bulk change when some settings were left as they were |
| 2246 |
* |
| 2247 |
* Importing a file, applying a preset and restoring the defaults touch every |
| 2248 |
* section at once, so the user is told that the shared file settings did |
| 2249 |
* not move. |
| 2250 |
* |
| 2251 |
* @since 2.11.6 |
| 2252 |
* |
| 2253 |
* @return string Empty when the user can change every setting. |
| 2254 |
*/ |
| 2255 |
private function locked_file_settings_message() { |
| 2256 |
if ( ! Vigilante_Settings::get_locked_file_settings() ) { |
| 2257 |
return ''; |
| 2258 |
} |
| 2259 |
|
| 2260 |
return ' ' . __( 'The settings that end up in wp-config.php or .htaccess were left as they were.', 'vigilante' ) . ' ' . Vigilante_Settings::get_shared_files_notice(); |
| 2261 |
} |
| 2262 |
|
| 2263 |
/** |
| 2264 |
* Print the shared-files notice for a section that cannot be edited here |
| 2265 |
* |
| 2266 |
* @since 2.9.8 |
| 2267 |
*/ |
| 2268 |
private function render_shared_files_notice() { |
| 2269 |
if ( ! $this->shared_files_locked() ) { |
| 2270 |
return; |
| 2271 |
} |
| 2272 |
?> |
| 2273 |
<div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 2274 |
<p style="margin:0;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></p> |
| 2275 |
</div> |
| 2276 |
<?php |
| 2277 |
} |
| 2278 |
|
| 2279 |
/** |
| 2280 |
* Acting on another user's account needs permission over that user |
| 2281 |
* |
| 2282 |
* Since 2.10.3 the handlers behind these tools ask for edit_user over the |
| 2283 |
* target, which is the rule WordPress itself applies. On a network the core |
| 2284 |
* grants edit_user only to network administrators, so for anybody else these |
| 2285 |
* controls do nothing. Better to say so than to paint a button that silently |
| 2286 |
* skips every user. |
| 2287 |
* |
| 2288 |
* @since 2.10.4 |
| 2289 |
* @return bool |
| 2290 |
*/ |
| 2291 |
private function forwarded_chain_readings() { |
| 2292 |
// Shown, not decided on: the firewall resolves the address elsewhere. |
| 2293 |
$chain = Vigilante_IP_Utils::trusted_forwarded_for(); |
| 2294 |
|
| 2295 |
if ( '' === $chain ) { |
| 2296 |
return array(); |
| 2297 |
} |
| 2298 |
|
| 2299 |
$public = array(); |
| 2300 |
|
| 2301 |
foreach ( explode( ',', $chain ) as $entry ) { |
| 2302 |
$address = Vigilante_IP_Utils::unmap_ipv4( trim( $entry ) ); |
| 2303 |
|
| 2304 |
if ( filter_var( $address, FILTER_VALIDATE_IP ) && ! Vigilante_IP_Utils::is_own_network( $address ) ) { |
| 2305 |
$public[] = $address; |
| 2306 |
} |
| 2307 |
} |
| 2308 |
|
| 2309 |
if ( count( $public ) < 2 ) { |
| 2310 |
return array(); |
| 2311 |
} |
| 2312 |
|
| 2313 |
return array( |
| 2314 |
'now' => Vigilante_IP_Utils::client_from_chain( $chain ), |
| 2315 |
'before' => $public[0], |
| 2316 |
); |
| 2317 |
} |
| 2318 |
|
| 2319 |
/** |
| 2320 |
* Whether the user tools of this screen are out of reach for this user |
| 2321 |
* |
| 2322 |
* @return bool |
| 2323 |
*/ |
| 2324 |
private function user_actions_locked() { |
| 2325 |
// On a single site edit_user maps to edit_users, which a custom role with |
| 2326 |
// manage_options may lack: since 2.11.8 approving and rejecting a pending |
| 2327 |
// registration ask for it, so the buttons have to say so there too. |
| 2328 |
return is_multisite() ? ! current_user_can( 'manage_network_users' ) : ! current_user_can( 'edit_users' ); |
| 2329 |
} |
| 2330 |
|
| 2331 |
/** |
| 2332 |
* Print the notice for user tools that cannot be used from this site |
| 2333 |
* |
| 2334 |
* @since 2.10.4 |
| 2335 |
*/ |
| 2336 |
private function render_user_actions_notice() { |
| 2337 |
if ( ! $this->user_actions_locked() ) { |
| 2338 |
return; |
| 2339 |
} |
| 2340 |
?> |
| 2341 |
<div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 2342 |
<?php if ( is_multisite() ) : ?> |
| 2343 |
<p style="margin:0;"><?php esc_html_e( 'These tools act on user accounts, which on a network belong to the whole network rather than to one site. WordPress reserves that to network administrators, so they are managed from the network admin.', 'vigilante' ); ?></p> |
| 2344 |
<?php else : ?> |
| 2345 |
<p style="margin:0;"><?php esc_html_e( 'These tools act on other user accounts, and your role cannot edit users, so they are not available to you.', 'vigilante' ); ?></p> |
| 2346 |
<?php endif; ?> |
| 2347 |
</div> |
| 2348 |
<?php |
| 2349 |
} |
| 2350 |
|
| 2351 |
/** |
| 2352 |
* Approving a change to the shared config files needs the network |
| 2353 |
* |
| 2354 |
* Since 2.11.3 the handler behind the Approve button asks for |
| 2355 |
* manage_network_options, because the two files it approves, wp-config.php |
| 2356 |
* and the root .htaccess, belong to the installation, and so does the |
| 2357 |
* record of them. The button, though, went on being painted for everybody, |
| 2358 |
* so the administrator of a subsite saw the warning, saw the button, |
| 2359 |
* pressed it and got "Permission denied" with no explanation. That is |
| 2360 |
* exactly what user_actions_locked() above exists to avoid, one release |
| 2361 |
* later and one screen over. Flagged by @calzbert. |
| 2362 |
* |
| 2363 |
* @since 2.11.4 |
| 2364 |
* @return bool |
| 2365 |
*/ |
| 2366 |
private function critical_approval_locked() { |
| 2367 |
return is_multisite() && ! current_user_can( 'manage_network_options' ); |
| 2368 |
} |
| 2369 |
|
| 2370 |
/** |
| 2371 |
* The line that replaces the Approve button where it cannot be used |
| 2372 |
* |
| 2373 |
* @since 2.11.4 |
| 2374 |
* @return string |
| 2375 |
*/ |
| 2376 |
private function critical_approval_notice() { |
| 2377 |
return __( 'These files belong to the whole network rather than to this site, so a change to them is approved from the network admin.', 'vigilante' ); |
| 2378 |
} |
| 2379 |
|
| 2380 |
/** |
| 2381 |
* Check if module is disabled and render warning |
| 2382 |
* |
| 2383 |
* @param string $module_key Module key. |
| 2384 |
* @return bool True if disabled. |
| 2385 |
*/ |
| 2386 |
private function render_module_disabled_notice( $module_key ) { |
| 2387 |
if ( $this->settings->is_module_enabled( $module_key ) ) { |
| 2388 |
return false; |
| 2389 |
} |
| 2390 |
|
| 2391 |
$module_labels = $this->settings->get_module_labels(); |
| 2392 |
$module_name = isset( $module_labels[ $module_key ] ) ? $module_labels[ $module_key ] : $module_key; |
| 2393 |
?> |
| 2394 |
<div class="notice notice-warning vigilante-module-disabled-notice"> |
| 2395 |
<p> |
| 2396 |
<strong><?php esc_html_e( 'Module Disabled', 'vigilante' ); ?></strong> - |
| 2397 |
<?php |
| 2398 |
printf( |
| 2399 |
/* translators: %s: Module name */ |
| 2400 |
esc_html__( 'The %s module is currently disabled. Enable it from the Dashboard to use these settings.', 'vigilante' ), |
| 2401 |
esc_html( $module_name ) |
| 2402 |
); |
| 2403 |
?> |
| 2404 |
</p> |
| 2405 |
<p> |
| 2406 |
<a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=dashboard' ) ); ?>" class="button"> |
| 2407 |
<?php esc_html_e( 'Go to Dashboard', 'vigilante' ); ?> |
| 2408 |
</a> |
| 2409 |
</p> |
| 2410 |
</div> |
| 2411 |
<?php |
| 2412 |
return true; |
| 2413 |
} |
| 2414 |
|
| 2415 |
/** |
| 2416 |
* Render the Security Analyzer (Security Check) widget + expandable full report. |
| 2417 |
* |
| 2418 |
* Lives in the Dashboard tab between the Configuration Score status card and |
| 2419 |
* the modules grid. Uses the last persisted scan to hydrate server-side so the |
| 2420 |
* first paint shows real data; "Scan now" runs the 2-phase AJAX to refresh. |
| 2421 |
* |
| 2422 |
* @param array $last_scan Result of Vigilante_Security_Analyzer::get_last_scan(). |
| 2423 |
* @param array $history Score history (oldest first). |
| 2424 |
* @param array $categories_def Category metadata (slug => label/max). |
| 2425 |
* @param array $analyzer_settings Settings subsection for weekly cron + email. |
| 2426 |
*/ |
| 2427 |
private function render_analyzer_widget( $last_scan, $history, $categories_def, $analyzer_settings ) { |
| 2428 |
$has_data = ! empty( $last_scan['ran_at'] ); |
| 2429 |
$score = isset( $last_scan['score'] ) ? (int) $last_scan['score'] : 0; |
| 2430 |
$grade = isset( $last_scan['grade'] ) ? (string) $last_scan['grade'] : ''; |
| 2431 |
$counts = isset( $last_scan['counts'] ) && is_array( $last_scan['counts'] ) ? $last_scan['counts'] : array(); |
| 2432 |
$ran_at_human = $has_data |
| 2433 |
? sprintf( |
| 2434 |
/* translators: %s: relative time like "2 hours" */ |
| 2435 |
__( 'Last scan %s ago', 'vigilante' ), |
| 2436 |
human_time_diff( (int) $last_scan['ran_at'], time() ) |
| 2437 |
) |
| 2438 |
: __( 'Never scanned', 'vigilante' ); |
| 2439 |
$categories = isset( $last_scan['categories'] ) && is_array( $last_scan['categories'] ) ? $last_scan['categories'] : array(); |
| 2440 |
$weekly_enabled = ! isset( $analyzer_settings['weekly_scan_enabled'] ) || ! empty( $analyzer_settings['weekly_scan_enabled'] ); |
| 2441 |
$email_enabled = ! empty( $analyzer_settings['email_on_regression'] ); |
| 2442 |
|
| 2443 |
$quality = self::analyzer_quality_tag( $score ); |
| 2444 |
?> |
| 2445 |
<div class="vigilante-analyzer" id="vigilante-analyzer" |
| 2446 |
data-has-data="<?php echo $has_data ? '1' : '0'; ?>"> |
| 2447 |
<div class="vigilante-analyzer-header"> |
| 2448 |
<div class="vigilante-analyzer-title"> |
| 2449 |
<h2> |
| 2450 |
<span class="dashicons dashicons-shield-alt" aria-hidden="true"></span> |
| 2451 |
<?php esc_html_e( 'Security Check', 'vigilante' ); ?> |
| 2452 |
</h2> |
| 2453 |
<p class="description"> |
| 2454 |
<?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?> |
| 2455 |
</p> |
| 2456 |
<p class="vigilante-analyzer-last-scan" data-role="ran-at"> |
| 2457 |
<span class="dashicons dashicons-clock" aria-hidden="true"></span> |
| 2458 |
<?php echo esc_html( $ran_at_human ); ?> |
| 2459 |
</p> |
| 2460 |
</div> |
| 2461 |
<div class="vigilante-analyzer-actions"> |
| 2462 |
<button type="button" class="button button-primary" id="vigilante-analyzer-scan"> |
| 2463 |
<span class="dashicons dashicons-update" aria-hidden="true"></span> |
| 2464 |
<?php esc_html_e( 'Scan now', 'vigilante' ); ?> |
| 2465 |
</button> |
| 2466 |
</div> |
| 2467 |
</div> |
| 2468 |
|
| 2469 |
<div class="vigilante-analyzer-summary"> |
| 2470 |
<div class="vigilante-analyzer-score-card"> |
| 2471 |
<?php if ( $has_data && $grade ) : ?> |
| 2472 |
<div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>"> |
| 2473 |
<span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span> |
| 2474 |
<span class="vigilante-score-text"><?php echo esc_html( $score ); ?>%</span> |
| 2475 |
</div> |
| 2476 |
<?php else : ?> |
| 2477 |
<div class="vigilante-score-circle vigilante-grade-empty"> |
| 2478 |
<span class="vigilante-grade">—</span> |
| 2479 |
<span class="vigilante-score-text"><?php esc_html_e( 'N/A', 'vigilante' ); ?></span> |
| 2480 |
</div> |
| 2481 |
<?php endif; ?> |
| 2482 |
<div class="vigilante-analyzer-score-meta"> |
| 2483 |
<p class="vigilante-analyzer-score-label"> |
| 2484 |
<?php esc_html_e( 'Security Score', 'vigilante' ); ?> |
| 2485 |
</p> |
| 2486 |
<span class="vigilante-analyzer-quality-tag vigilante-analyzer-quality-<?php echo esc_attr( $quality['slug'] ); ?>" |
| 2487 |
data-role="quality-tag"> |
| 2488 |
<?php echo esc_html( $quality['label'] ); ?> |
| 2489 |
</span> |
| 2490 |
</div> |
| 2491 |
</div> |
| 2492 |
|
| 2493 |
<div class="vigilante-analyzer-counts"> |
| 2494 |
<span class="vigilante-analyzer-count vigilante-analyzer-count--pass"> |
| 2495 |
<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span> |
| 2496 |
<strong data-role="pass"><?php echo esc_html( isset( $counts['pass'] ) ? $counts['pass'] : 0 ); ?></strong> |
| 2497 |
<span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Passed', 'vigilante' ); ?></span> |
| 2498 |
</span> |
| 2499 |
<span class="vigilante-analyzer-count vigilante-analyzer-count--warn"> |
| 2500 |
<span class="dashicons dashicons-warning" aria-hidden="true"></span> |
| 2501 |
<strong data-role="warn"><?php echo esc_html( isset( $counts['warn'] ) ? $counts['warn'] : 0 ); ?></strong> |
| 2502 |
<span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Warnings', 'vigilante' ); ?></span> |
| 2503 |
</span> |
| 2504 |
<span class="vigilante-analyzer-count vigilante-analyzer-count--fail"> |
| 2505 |
<span class="dashicons dashicons-dismiss" aria-hidden="true"></span> |
| 2506 |
<strong data-role="fail"><?php echo esc_html( isset( $counts['fail'] ) ? $counts['fail'] : 0 ); ?></strong> |
| 2507 |
<span class="vigilante-analyzer-count-label"><?php esc_html_e( 'Failing', 'vigilante' ); ?></span> |
| 2508 |
</span> |
| 2509 |
</div> |
| 2510 |
|
| 2511 |
<?php |
| 2512 |
// Sparkline of recent scores. Require at least 3 data points so the trend |
| 2513 |
// is meaningful (2 points is just a line between dots, no real trend). |
| 2514 |
$hist_points = array(); |
| 2515 |
foreach ( $history as $h ) { |
| 2516 |
$hist_points[] = (int) $h['score']; |
| 2517 |
} |
| 2518 |
$hist_count = count( $hist_points ); |
| 2519 |
if ( $hist_count >= 3 ) : |
| 2520 |
$current_score = (int) end( $hist_points ); |
| 2521 |
$previous_score = (int) $hist_points[ $hist_count - 2 ]; |
| 2522 |
$delta = $current_score - $previous_score; |
| 2523 |
$delta_class = $delta > 0 ? 'vigilante-analyzer-delta--up' : ( $delta < 0 ? 'vigilante-analyzer-delta--down' : 'vigilante-analyzer-delta--flat' ); |
| 2524 |
$delta_icon = $delta > 0 ? 'arrow-up-alt' : ( $delta < 0 ? 'arrow-down-alt' : 'minus' ); |
| 2525 |
if ( 0 === $delta ) { |
| 2526 |
$delta_text = __( 'No change', 'vigilante' ); |
| 2527 |
} else { |
| 2528 |
$delta_text = sprintf( |
| 2529 |
/* translators: %s: signed delta, e.g. "+3" or "-5" */ |
| 2530 |
_n( '%s pt vs. previous scan', '%s pts vs. previous scan', abs( $delta ), 'vigilante' ), |
| 2531 |
( $delta > 0 ? '+' : '' ) . (int) $delta |
| 2532 |
); |
| 2533 |
} |
| 2534 |
?> |
| 2535 |
<div class="vigilante-analyzer-sparkline-wrap"> |
| 2536 |
<div class="vigilante-analyzer-sparkline-head"> |
| 2537 |
<span class="vigilante-analyzer-sparkline-label"> |
| 2538 |
<?php |
| 2539 |
echo esc_html( sprintf( |
| 2540 |
/* translators: %d: number of scans */ |
| 2541 |
_n( 'Score trend (last %d scan)', 'Score trend (last %d scans)', $hist_count, 'vigilante' ), |
| 2542 |
$hist_count |
| 2543 |
) ); |
| 2544 |
?> |
| 2545 |
</span> |
| 2546 |
<span class="vigilante-analyzer-delta <?php echo esc_attr( $delta_class ); ?>"> |
| 2547 |
<span class="dashicons dashicons-<?php echo esc_attr( $delta_icon ); ?>" aria-hidden="true"></span> |
| 2548 |
<?php echo esc_html( $delta_text ); ?> |
| 2549 |
</span> |
| 2550 |
</div> |
| 2551 |
<div class="vigilante-analyzer-sparkline" data-role="sparkline" |
| 2552 |
data-points="<?php echo esc_attr( wp_json_encode( $hist_points ) ); ?>"> |
| 2553 |
<?php echo self::sparkline_svg( $hist_points ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe SVG from helper ?> |
| 2554 |
</div> |
| 2555 |
</div> |
| 2556 |
<?php elseif ( $has_data ) : ?> |
| 2557 |
<div class="vigilante-analyzer-sparkline-wrap vigilante-analyzer-sparkline-wrap--placeholder"> |
| 2558 |
<span class="vigilante-analyzer-sparkline-label"> |
| 2559 |
<?php esc_html_e( 'Score trend', 'vigilante' ); ?> |
| 2560 |
</span> |
| 2561 |
<p class="vigilante-analyzer-sparkline-hint"> |
| 2562 |
<span class="dashicons dashicons-chart-line" aria-hidden="true"></span> |
| 2563 |
<?php |
| 2564 |
$needed = 3 - $hist_count; |
| 2565 |
echo esc_html( sprintf( |
| 2566 |
/* translators: %d: number of additional scans needed */ |
| 2567 |
_n( '%d more scan needed to show a trend.', '%d more scans needed to show a trend.', $needed, 'vigilante' ), |
| 2568 |
$needed |
| 2569 |
) ); |
| 2570 |
?> |
| 2571 |
</p> |
| 2572 |
</div> |
| 2573 |
<?php endif; ?> |
| 2574 |
</div> |
| 2575 |
|
| 2576 |
<div class="vigilante-analyzer-toggle-row"> |
| 2577 |
<button type="button" class="button-link vigilante-analyzer-toggle" aria-expanded="false"> |
| 2578 |
<?php esc_html_e( 'Show detailed breakdown', 'vigilante' ); ?> |
| 2579 |
<span class="vigilante-analyzer-toggle-chevron" aria-hidden="true"></span> |
| 2580 |
</button> |
| 2581 |
</div> |
| 2582 |
|
| 2583 |
<div class="vigilante-analyzer-details" hidden> |
| 2584 |
<div class="vigilante-analyzer-categories" data-role="categories"> |
| 2585 |
<?php foreach ( $categories_def as $slug => $meta ) : |
| 2586 |
$cat = isset( $categories[ $slug ] ) ? $categories[ $slug ] : array(); |
| 2587 |
$earned = isset( $cat['earned'] ) ? (int) $cat['earned'] : 0; |
| 2588 |
// Always use the declared meta as the source of truth for the maximum. |
| 2589 |
// The cached scan may carry an old max if a check was added/removed |
| 2590 |
// between releases (see 2.6.1: closed_plugins raised internal from 22 to 28 |
| 2591 |
// but cached scans still reported max=22 until reset). |
| 2592 |
$cat_max = (int) $meta['max']; |
| 2593 |
$info_only = ! empty( $meta['info_only'] ) || 0 === (int) $meta['max']; |
| 2594 |
$cat_pct = $cat_max > 0 ? (int) round( ( $earned / $cat_max ) * 100 ) : 0; |
| 2595 |
$checks = isset( $cat['checks'] ) ? (array) $cat['checks'] : array(); |
| 2596 |
$cat_counts = isset( $cat['counts'] ) && is_array( $cat['counts'] ) ? $cat['counts'] : array( 'pass' => 0, 'warn' => 0, 'fail' => 0, 'info' => 0 ); |
| 2597 |
$cat_quality = self::analyzer_quality_tag( $cat_pct ); |
| 2598 |
$info_count = isset( $cat_counts['info'] ) ? (int) $cat_counts['info'] : 0; |
| 2599 |
?> |
| 2600 |
<details class="vigilante-analyzer-category<?php echo $info_only ? ' vigilante-analyzer-category--info' : ''; ?>" |
| 2601 |
data-category="<?php echo esc_attr( $slug ); ?>" |
| 2602 |
data-info-only="<?php echo $info_only ? '1' : '0'; ?>"> |
| 2603 |
<summary class="vigilante-analyzer-category-summary"> |
| 2604 |
<span class="vigilante-analyzer-category-chevron" aria-hidden="true"></span> |
| 2605 |
<span class="vigilante-analyzer-category-label"><?php echo esc_html( $meta['label'] ); ?></span> |
| 2606 |
<?php if ( $info_only ) : ?> |
| 2607 |
<span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-info" |
| 2608 |
data-role="category-quality" |
| 2609 |
title="<?php esc_attr_e( 'Informational — does not affect the security score.', 'vigilante' ); ?>"> |
| 2610 |
<span class="dashicons dashicons-info-outline" aria-hidden="true"></span> |
| 2611 |
<?php esc_html_e( 'Informational', 'vigilante' ); ?> |
| 2612 |
</span> |
| 2613 |
<?php else : |
| 2614 |
$passed_count = (int) ( $cat_counts['pass'] ?? 0 ); |
| 2615 |
$scored_total = $passed_count |
| 2616 |
+ (int) ( $cat_counts['warn'] ?? 0 ) |
| 2617 |
+ (int) ( $cat_counts['fail'] ?? 0 ); |
| 2618 |
?> |
| 2619 |
<span class="vigilante-analyzer-category-quality vigilante-analyzer-quality-<?php echo esc_attr( $cat_quality['slug'] ); ?>" |
| 2620 |
data-role="category-quality"> |
| 2621 |
<span data-role="category-quality-label"><?php echo esc_html( $cat_quality['label'] ); ?></span> |
| 2622 |
<span class="vigilante-analyzer-category-quality-sep" aria-hidden="true">·</span> |
| 2623 |
<span class="vigilante-analyzer-category-tests" data-role="category-tests" |
| 2624 |
data-passed="<?php echo esc_attr( $passed_count ); ?>" |
| 2625 |
data-total="<?php echo esc_attr( $scored_total ); ?>"> |
| 2626 |
<?php |
| 2627 |
echo esc_html( sprintf( |
| 2628 |
/* translators: 1: tests passed, 2: total tests in this category */ |
| 2629 |
__( '%1$d/%2$d tests', 'vigilante' ), |
| 2630 |
$passed_count, |
| 2631 |
$scored_total |
| 2632 |
) ); |
| 2633 |
?> |
| 2634 |
</span> |
| 2635 |
</span> |
| 2636 |
<?php endif; ?> |
| 2637 |
<?php if ( $info_only ) : ?> |
| 2638 |
<span class="vigilante-analyzer-category-states" data-role="category-states"> |
| 2639 |
<?php if ( $info_count > 0 ) : ?> |
| 2640 |
<span class="vigilante-analyzer-category-state vigilante-analyzer-category-state--info" title="<?php esc_attr_e( 'Informational', 'vigilante' ); ?>"> |
| 2641 |
<span data-role="state-info"><?php echo esc_html( $info_count ); ?></span><span class="dashicons dashicons-info-outline" aria-hidden="true"></span> |
| 2642 |
</span> |
| 2643 |
<?php endif; ?> |
| 2644 |
</span> |
| 2645 |
<?php endif; ?> |
| 2646 |
<?php if ( ! $info_only ) : ?> |
| 2647 |
<span class="vigilante-analyzer-category-score"> |
| 2648 |
<span data-role="earned"><?php echo esc_html( $earned ); ?></span><span class="vigilante-analyzer-category-score-sep">/</span><?php echo esc_html( $cat_max ); ?> |
| 2649 |
<span class="vigilante-analyzer-category-score-unit"><?php esc_html_e( 'pts', 'vigilante' ); ?></span> |
| 2650 |
</span> |
| 2651 |
<span class="vigilante-analyzer-category-bar" aria-hidden="true"> |
| 2652 |
<span class="vigilante-analyzer-category-bar-fill vigilante-analyzer-category-bar-fill--<?php echo esc_attr( $cat_quality['slug'] ); ?>" |
| 2653 |
style="width: <?php echo esc_attr( $cat_pct ); ?>%" |
| 2654 |
data-role="category-bar"></span> |
| 2655 |
</span> |
| 2656 |
<?php else : |
| 2657 |
$info_warn = isset( $cat_counts['warn'] ) ? (int) $cat_counts['warn'] : 0; |
| 2658 |
$info_fail = isset( $cat_counts['fail'] ) ? (int) $cat_counts['fail'] : 0; |
| 2659 |
$info_issues = $info_warn + $info_fail; |
| 2660 |
if ( $info_issues > 0 ) : ?> |
| 2661 |
<span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--attention" data-role="info-status"> |
| 2662 |
<span class="dashicons dashicons-warning" aria-hidden="true"></span> |
| 2663 |
<?php |
| 2664 |
echo esc_html( sprintf( |
| 2665 |
/* translators: %d: number of findings */ |
| 2666 |
_n( '%d finding', '%d findings', $info_issues, 'vigilante' ), |
| 2667 |
$info_issues |
| 2668 |
) ); |
| 2669 |
?> |
| 2670 |
</span> |
| 2671 |
<?php else : ?> |
| 2672 |
<span class="vigilante-analyzer-category-status vigilante-analyzer-category-status--clear" data-role="info-status"> |
| 2673 |
<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span> |
| 2674 |
<?php esc_html_e( 'All clear', 'vigilante' ); ?> |
| 2675 |
</span> |
| 2676 |
<?php endif; ?> |
| 2677 |
<?php endif; ?> |
| 2678 |
</summary> |
| 2679 |
<ul class="vigilante-analyzer-check-list" data-role="check-list"> |
| 2680 |
<?php if ( empty( $checks ) ) : ?> |
| 2681 |
<li class="vigilante-analyzer-check-empty"> |
| 2682 |
<?php esc_html_e( 'No data yet — run a scan to populate this category.', 'vigilante' ); ?> |
| 2683 |
</li> |
| 2684 |
<?php else : ?> |
| 2685 |
<?php foreach ( $checks as $c ) : ?> |
| 2686 |
<?php echo self::render_analyzer_check_row( $c ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped inside helper ?> |
| 2687 |
<?php endforeach; ?> |
| 2688 |
<?php endif; ?> |
| 2689 |
</ul> |
| 2690 |
</details> |
| 2691 |
<?php endforeach; ?> |
| 2692 |
</div> |
| 2693 |
|
| 2694 |
<div class="vigilante-analyzer-weekly"> |
| 2695 |
<h3><?php esc_html_e( 'Automatic weekly scan', 'vigilante' ); ?></h3> |
| 2696 |
<p class="description"> |
| 2697 |
<?php esc_html_e( 'Vigilante runs this check once a week in the background. Enable email alerts to be notified if the score drops by 10 points or more, or if a new critical check starts failing.', 'vigilante' ); ?> |
| 2698 |
</p> |
| 2699 |
<label class="vigilante-analyzer-toggle-option"> |
| 2700 |
<input type="checkbox" |
| 2701 |
name="security_analyzer[weekly_scan_enabled]" |
| 2702 |
value="1" |
| 2703 |
<?php checked( $weekly_enabled ); ?>> |
| 2704 |
<?php esc_html_e( 'Run a weekly automatic scan', 'vigilante' ); ?> |
| 2705 |
</label> |
| 2706 |
<label class="vigilante-analyzer-toggle-option"> |
| 2707 |
<input type="checkbox" |
| 2708 |
name="security_analyzer[email_on_regression]" |
| 2709 |
value="1" |
| 2710 |
<?php checked( $email_enabled ); ?>> |
| 2711 |
<?php esc_html_e( 'Email me when the score drops significantly', 'vigilante' ); ?> |
| 2712 |
</label> |
| 2713 |
</div> |
| 2714 |
</div> |
| 2715 |
</div> |
| 2716 |
<?php |
| 2717 |
} |
| 2718 |
|
| 2719 |
/** |
| 2720 |
* Map a 0-100 percentage to a quality tag { label, slug } aligned with the |
| 2721 |
* Dashboard grade palette (a/b/c/d/e). |
| 2722 |
* |
| 2723 |
* @param int $pct 0..100. |
| 2724 |
* @return array{label:string,slug:string} |
| 2725 |
*/ |
| 2726 |
public static function analyzer_quality_tag( $pct ) { |
| 2727 |
$pct = max( 0, min( 100, (int) $pct ) ); |
| 2728 |
// "Excellent" is reserved for a perfect score — a single missing point drops to Good. |
| 2729 |
if ( 100 === $pct ) { |
| 2730 |
return array( 'label' => __( 'Excellent', 'vigilante' ), 'slug' => 'a' ); |
| 2731 |
} |
| 2732 |
if ( $pct >= 70 ) { |
| 2733 |
return array( 'label' => __( 'Good', 'vigilante' ), 'slug' => 'b' ); |
| 2734 |
} |
| 2735 |
if ( $pct >= 50 ) { |
| 2736 |
return array( 'label' => __( 'Fair', 'vigilante' ), 'slug' => 'c' ); |
| 2737 |
} |
| 2738 |
if ( $pct >= 30 ) { |
| 2739 |
return array( 'label' => __( 'Poor', 'vigilante' ), 'slug' => 'd' ); |
| 2740 |
} |
| 2741 |
return array( 'label' => __( 'Critical', 'vigilante' ), 'slug' => 'e' ); |
| 2742 |
} |
| 2743 |
|
| 2744 |
/** |
| 2745 |
* Render a single analyzer check row (used both server-side and via JS template). |
| 2746 |
* |
| 2747 |
* @param array $check Check result array (from Vigilante_SA_Check_Result::to_array()). |
| 2748 |
* @return string HTML (escaped). |
| 2749 |
*/ |
| 2750 |
private static function render_analyzer_check_row( $check ) { |
| 2751 |
$id = isset( $check['id'] ) ? $check['id'] : ''; |
| 2752 |
$state = isset( $check['state'] ) ? $check['state'] : 'skip'; |
| 2753 |
$label = isset( $check['label'] ) ? $check['label'] : ''; |
| 2754 |
$detail = isset( $check['detail'] ) ? $check['detail'] : ''; |
| 2755 |
$score = isset( $check['score'] ) ? (int) $check['score'] : 0; |
| 2756 |
$max = isset( $check['max'] ) ? (int) $check['max'] : 0; |
| 2757 |
$fix_link = isset( $check['fix_link'] ) ? $check['fix_link'] : ''; |
| 2758 |
|
| 2759 |
$icons = array( |
| 2760 |
'pass' => 'yes-alt', |
| 2761 |
'warn' => 'warning', |
| 2762 |
'fail' => 'dismiss', |
| 2763 |
'info' => 'info', |
| 2764 |
'skip' => 'minus', |
| 2765 |
); |
| 2766 |
$icon = isset( $icons[ $state ] ) ? $icons[ $state ] : 'minus'; |
| 2767 |
|
| 2768 |
$html = '<li class="vigilante-analyzer-check vigilante-analyzer-check--' . esc_attr( $state ) . '"'; |
| 2769 |
$html .= ' data-check-id="' . esc_attr( $id ) . '">'; |
| 2770 |
$html .= '<span class="vigilante-analyzer-check-icon dashicons dashicons-' . esc_attr( $icon ) . '" aria-hidden="true"></span>'; |
| 2771 |
$html .= '<div class="vigilante-analyzer-check-body">'; |
| 2772 |
$html .= '<div class="vigilante-analyzer-check-label">'; |
| 2773 |
$html .= '<span>' . esc_html( $label ) . '</span>'; |
| 2774 |
if ( $max > 0 && 'info' !== $state && 'skip' !== $state ) { |
| 2775 |
$html .= '<span class="vigilante-analyzer-check-score">' |
| 2776 |
. esc_html( $score . '/' . $max ) |
| 2777 |
. ' <span class="vigilante-analyzer-check-score-unit">' . esc_html__( 'pts', 'vigilante' ) . '</span>' |
| 2778 |
. '</span>'; |
| 2779 |
} |
| 2780 |
$html .= '</div>'; |
| 2781 |
if ( $detail ) { |
| 2782 |
$html .= '<p class="vigilante-analyzer-check-detail">' . esc_html( $detail ) . '</p>'; |
| 2783 |
} |
| 2784 |
if ( $fix_link && in_array( $state, array( 'fail', 'warn' ), true ) ) { |
| 2785 |
$html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link">' |
| 2786 |
. esc_html__( 'Go to setting', 'vigilante' ) |
| 2787 |
. '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">→</span></a>'; |
| 2788 |
} elseif ( $fix_link && 'info' === $state ) { |
| 2789 |
// Info rows (e.g. DNSBL lookups) get an external "Learn more" link instead. |
| 2790 |
$is_external = 0 === strpos( $fix_link, 'http' ); |
| 2791 |
$html .= '<a href="' . esc_url( $fix_link ) . '" class="vigilante-analyzer-fix-link"' |
| 2792 |
. ( $is_external ? ' target="_blank" rel="noopener noreferrer"' : '' ) . '>' |
| 2793 |
. esc_html__( 'Learn more', 'vigilante' ) |
| 2794 |
. '<span class="vigilante-analyzer-fix-arrow" aria-hidden="true">→</span></a>'; |
| 2795 |
} |
| 2796 |
$html .= '</div>'; |
| 2797 |
$html .= '</li>'; |
| 2798 |
return $html; |
| 2799 |
} |
| 2800 |
|
| 2801 |
/** |
| 2802 |
* Build a minimal SVG sparkline for the score history. |
| 2803 |
* |
| 2804 |
* @param int[] $points Score values (0..100), oldest to newest. |
| 2805 |
* @return string SVG markup. |
| 2806 |
*/ |
| 2807 |
private static function sparkline_svg( $points ) { |
| 2808 |
$points = array_map( 'intval', (array) $points ); |
| 2809 |
$count = count( $points ); |
| 2810 |
if ( $count < 2 ) { |
| 2811 |
return ''; |
| 2812 |
} |
| 2813 |
$width = 280; |
| 2814 |
$height = 60; |
| 2815 |
$padding = 4; |
| 2816 |
|
| 2817 |
$usable_w = $width - ( $padding * 2 ); |
| 2818 |
$usable_h = $height - ( $padding * 2 ); |
| 2819 |
|
| 2820 |
$step = $usable_w / max( 1, $count - 1 ); |
| 2821 |
$max = 100; // Fixed scale — scores are 0..100. |
| 2822 |
|
| 2823 |
$coords = array(); |
| 2824 |
foreach ( $points as $i => $v ) { |
| 2825 |
$x = $padding + ( $i * $step ); |
| 2826 |
$y = $padding + ( $usable_h - ( ( $v / $max ) * $usable_h ) ); |
| 2827 |
$coords[] = round( $x, 2 ) . ',' . round( $y, 2 ); |
| 2828 |
} |
| 2829 |
$path = 'M ' . implode( ' L ', $coords ); |
| 2830 |
$last = end( $points ); |
| 2831 |
$last_x = $padding + ( ( $count - 1 ) * $step ); |
| 2832 |
$last_y = $padding + ( $usable_h - ( ( $last / $max ) * $usable_h ) ); |
| 2833 |
|
| 2834 |
$svg = '<svg viewBox="0 0 ' . $width . ' ' . $height . '" preserveAspectRatio="none" role="img" aria-label="' . esc_attr__( 'Security Score history', 'vigilante' ) . '" focusable="false">'; |
| 2835 |
$svg .= '<path d="' . esc_attr( $path ) . '" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'; |
| 2836 |
$svg .= '<circle cx="' . esc_attr( round( $last_x, 2 ) ) . '" cy="' . esc_attr( round( $last_y, 2 ) ) . '" r="3" fill="currentColor"/>'; |
| 2837 |
$svg .= '</svg>'; |
| 2838 |
return $svg; |
| 2839 |
} |
| 2840 |
|
| 2841 |
/** |
| 2842 |
* Render dashboard tab |
| 2843 |
*/ |
| 2844 |
private function render_tab_dashboard() { |
| 2845 |
$options = $this->settings->get_all_options(); |
| 2846 |
$module_labels = $this->settings->get_module_labels(); |
| 2847 |
$module_descriptions = $this->settings->get_module_descriptions(); |
| 2848 |
$presets = $this->settings->get_presets(); |
| 2849 |
$active_preset = get_option( 'vigilante_active_preset', '' ); |
| 2850 |
|
| 2851 |
// Calculate security score with more factors |
| 2852 |
$security_score = $this->calculate_security_score( $options ); |
| 2853 |
|
| 2854 |
// Security Analyzer (v2.1.0) — hydrate widget with the last persisted scan, if any. |
| 2855 |
if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) { |
| 2856 |
require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php'; |
| 2857 |
} |
| 2858 |
$analyzer_instance = new Vigilante_Security_Analyzer( $this->settings, $this->activity_log ); |
| 2859 |
$analyzer_last_scan = $analyzer_instance->get_last_scan(); |
| 2860 |
$analyzer_history = $analyzer_instance->get_score_history(); |
| 2861 |
$analyzer_categories_def = Vigilante_Security_Analyzer::get_categories(); |
| 2862 |
$analyzer_settings = isset( $options['security_analyzer'] ) ? $options['security_analyzer'] : array(); |
| 2863 |
?> |
| 2864 |
<div class="vigilante-dashboard"> |
| 2865 |
<div class="vigilante-status-card"> |
| 2866 |
<h2><?php esc_html_e( 'Configuration Score', 'vigilante' ); ?></h2> |
| 2867 |
<p class="vigilante-score-kind description"> |
| 2868 |
<?php esc_html_e( 'How well Vigilante is configured right now. Pair it with the Security Check below to see the real-world result.', 'vigilante' ); ?> |
| 2869 |
</p> |
| 2870 |
<div class="vigilante-security-score"> |
| 2871 |
<?php |
| 2872 |
// Grade thresholds: A (90+), B (70-89), C (50-69), D (30-49), E (0-29) |
| 2873 |
if ( $security_score >= 90 ) { |
| 2874 |
$grade = 'A'; |
| 2875 |
} elseif ( $security_score >= 70 ) { |
| 2876 |
$grade = 'B'; |
| 2877 |
} elseif ( $security_score >= 50 ) { |
| 2878 |
$grade = 'C'; |
| 2879 |
} elseif ( $security_score >= 30 ) { |
| 2880 |
$grade = 'D'; |
| 2881 |
} else { |
| 2882 |
$grade = 'E'; |
| 2883 |
} |
| 2884 |
?> |
| 2885 |
<div class="vigilante-score-circle vigilante-grade-<?php echo esc_attr( strtolower( $grade ) ); ?>"> |
| 2886 |
<span class="vigilante-grade"><?php echo esc_html( $grade ); ?></span> |
| 2887 |
<span class="vigilante-score-text"><?php echo esc_html( $security_score ); ?>%</span> |
| 2888 |
</div> |
| 2889 |
<div class="vigilante-config-status"> |
| 2890 |
<?php if ( $active_preset && isset( $presets[ $active_preset ] ) ) : ?> |
| 2891 |
<span class="vigilante-preset-badge vigilante-preset-<?php echo esc_attr( $active_preset ); ?>"> |
| 2892 |
<?php echo esc_html( $presets[ $active_preset ]['name'] ); ?> |
| 2893 |
</span> |
| 2894 |
<?php else : ?> |
| 2895 |
<span class="vigilante-preset-badge vigilante-preset-custom"> |
| 2896 |
<?php esc_html_e( 'Custom Configuration', 'vigilante' ); ?> |
| 2897 |
</span> |
| 2898 |
<?php endif; ?> |
| 2899 |
</div> |
| 2900 |
</div> |
| 2901 |
|
| 2902 |
<?php |
| 2903 |
$recommendations = $this->get_security_recommendations( $options ); |
| 2904 |
if ( ! empty( $recommendations ) ) : |
| 2905 |
?> |
| 2906 |
<div class="vigilante-recommendations"> |
| 2907 |
<h4><?php esc_html_e( 'Recommendations', 'vigilante' ); ?></h4> |
| 2908 |
<ul class="vigilante-recommendations-grid"> |
| 2909 |
<?php foreach ( $recommendations as $rec ) : ?> |
| 2910 |
<li> |
| 2911 |
<span class="dashicons dashicons-<?php echo esc_attr( $rec['icon'] ); ?> vigilante-priority-<?php echo esc_attr( $rec['priority'] ); ?>"></span> |
| 2912 |
<?php echo esc_html( $rec['message'] ); ?> |
| 2913 |
<?php if ( ! empty( $rec['tab'] ) ) : ?> |
| 2914 |
<a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $rec['tab'] ) ); ?>" class="vigilante-rec-link" title="<?php esc_attr_e( 'Go to settings', 'vigilante' ); ?>"><span class="dashicons dashicons-arrow-right-alt2"></span></a> |
| 2915 |
<?php endif; ?> |
| 2916 |
</li> |
| 2917 |
<?php endforeach; ?> |
| 2918 |
</ul> |
| 2919 |
</div> |
| 2920 |
<?php endif; ?> |
| 2921 |
</div> |
| 2922 |
|
| 2923 |
<?php $this->render_analyzer_widget( $analyzer_last_scan, $analyzer_history, $analyzer_categories_def, $analyzer_settings ); ?> |
| 2924 |
|
| 2925 |
<div class="vigilante-modules-grid"> |
| 2926 |
<h2 id="vigilante-section-dashboard-modules"><?php esc_html_e( 'Security Modules', 'vigilante' ); ?></h2> |
| 2927 |
<p class="description"><?php esc_html_e( 'Enable or disable security modules. Each module controls a tab with detailed settings.', 'vigilante' ); ?></p> |
| 2928 |
<div class="vigilante-modules-list"> |
| 2929 |
<?php foreach ( $options['modules'] as $module => $enabled ) : |
| 2930 |
$label = isset( $module_labels[ $module ] ) ? $module_labels[ $module ] : ucwords( str_replace( '_', ' ', $module ) ); |
| 2931 |
$description = isset( $module_descriptions[ $module ] ) ? $module_descriptions[ $module ] : ''; |
| 2932 |
$vg_module_locked = $this->main_site_files_locked() && in_array( $module, Vigilante_Settings::get_main_site_file_settings()['modules'], true ); |
| 2933 |
?> |
| 2934 |
<div class="vigilante-module-item <?php echo $enabled ? 'enabled' : 'disabled'; ?>"> |
| 2935 |
<div class="vigilante-module-header"> |
| 2936 |
<span class="vigilante-module-status"></span> |
| 2937 |
<span class="vigilante-module-name"><?php echo esc_html( $label ); ?></span> |
| 2938 |
<label class="vigilante-toggle"> |
| 2939 |
<?php |
| 2940 |
/* translators: %s: Security module name, for example Firewall. */ |
| 2941 |
$toggle_label = sprintf( __( 'Enable %s', 'vigilante' ), $label ); |
| 2942 |
?> |
| 2943 |
<input type="checkbox" |
| 2944 |
name="modules[<?php echo esc_attr( $module ); ?>]" |
| 2945 |
value="1" |
| 2946 |
<?php checked( $enabled ); ?> |
| 2947 |
<?php disabled( $vg_module_locked ); ?> |
| 2948 |
aria-label="<?php echo esc_attr( $toggle_label ); ?>" |
| 2949 |
data-module="<?php echo esc_attr( $module ); ?>"> |
| 2950 |
<span class="vigilante-toggle-slider"></span> |
| 2951 |
</label> |
| 2952 |
</div> |
| 2953 |
<?php if ( $description ) : ?> |
| 2954 |
<p class="vigilante-module-desc"><?php echo esc_html( $description ); ?></p> |
| 2955 |
<?php endif; ?> |
| 2956 |
<?php if ( $vg_module_locked ) : ?> |
| 2957 |
<p class="vigilante-module-desc"><?php esc_html_e( 'On the main site of a network this module also writes files every site shares, so only a network administrator can switch it.', 'vigilante' ); ?></p> |
| 2958 |
<?php endif; ?> |
| 2959 |
</div> |
| 2960 |
<?php endforeach; ?> |
| 2961 |
</div> |
| 2962 |
</div> |
| 2963 |
|
| 2964 |
<div class="vigilante-presets-card"> |
| 2965 |
<h2><?php esc_html_e( 'Quick Configuration Presets', 'vigilante' ); ?></h2> |
| 2966 |
<p><?php esc_html_e( 'Apply a preset to quickly set up recommended settings for standard or maximum security level.', 'vigilante' ); ?></p> |
| 2967 |
<div class="vigilante-presets-grid"> |
| 2968 |
<?php foreach ( $presets as $preset_id => $preset ) : |
| 2969 |
$is_active = ( $active_preset === $preset_id ); |
| 2970 |
?> |
| 2971 |
<div class="vigilante-preset-card <?php echo $is_active ? 'vigilante-preset-active' : ''; ?>"> |
| 2972 |
<?php if ( $is_active ) : ?> |
| 2973 |
<span class="vigilante-active-indicator"><?php esc_html_e( 'Active', 'vigilante' ); ?></span> |
| 2974 |
<?php endif; ?> |
| 2975 |
<h3><?php echo esc_html( $preset['name'] ); ?></h3> |
| 2976 |
<p><?php echo esc_html( $preset['description'] ); ?></p> |
| 2977 |
<button type="button" class="button vigilante-preset-btn <?php echo $is_active ? 'button-primary' : ''; ?>" data-preset="<?php echo esc_attr( $preset_id ); ?>"> |
| 2978 |
<?php esc_html_e( 'Apply Preset', 'vigilante' ); ?> |
| 2979 |
</button> |
| 2980 |
</div> |
| 2981 |
<?php endforeach; ?> |
| 2982 |
|
| 2983 |
<?php |
| 2984 |
// Under Attack mode card |
| 2985 |
$under_attack = new Vigilante_Under_Attack( $this->settings, $this->activity_log ); |
| 2986 |
$ua_active = $under_attack->is_active(); |
| 2987 |
$ua_remaining = $under_attack->get_remaining_time(); |
| 2988 |
$ua_remaining_hours = floor( $ua_remaining / 3600 ); |
| 2989 |
$ua_remaining_mins = floor( ( $ua_remaining % 3600 ) / 60 ); |
| 2990 |
?> |
| 2991 |
<div class="vigilante-preset-card vigilante-under-attack-card <?php echo $ua_active ? 'vigilante-under-attack-active' : ''; ?>"> |
| 2992 |
<h3 id="vigilante-section-dashboard-under-attack"> |
| 2993 |
<span class="dashicons dashicons-shield"></span> |
| 2994 |
<?php esc_html_e( 'Under Attack', 'vigilante' ); ?> |
| 2995 |
</h3> |
| 2996 |
<p><?php esc_html_e( 'Emergency mode. JavaScript challenge for all visitors, aggressive rate limiting, and restricted access. Auto-deactivates after 4 hours.', 'vigilante' ); ?></p> |
| 2997 |
<?php if ( $ua_active ) : ?> |
| 2998 |
<div class="vigilante-ua-countdown" data-expires="<?php echo esc_attr( $under_attack->get_status()['activated_at'] + $under_attack->get_status()['duration'] ); ?>"> |
| 2999 |
<span class="dashicons dashicons-clock"></span> |
| 3000 |
<span class="vigilante-ua-time"> |
| 3001 |
<?php |
| 3002 |
printf( |
| 3003 |
/* translators: 1: Hours, 2: Minutes */ |
| 3004 |
esc_html__( '%1$dh %2$dm remaining', 'vigilante' ), |
| 3005 |
absint( $ua_remaining_hours ), |
| 3006 |
absint( $ua_remaining_mins ) |
| 3007 |
); |
| 3008 |
?> |
| 3009 |
</span> |
| 3010 |
</div> |
| 3011 |
<button type="button" class="button vigilante-ua-btn vigilante-ua-deactivate"> |
| 3012 |
<?php esc_html_e( 'Deactivate', 'vigilante' ); ?> |
| 3013 |
</button> |
| 3014 |
<?php else : ?> |
| 3015 |
<button type="button" class="button vigilante-ua-btn vigilante-ua-activate"> |
| 3016 |
<?php esc_html_e( 'Activate for 4 hours', 'vigilante' ); ?> |
| 3017 |
</button> |
| 3018 |
<?php endif; ?> |
| 3019 |
</div> |
| 3020 |
</div> |
| 3021 |
</div> |
| 3022 |
</div> |
| 3023 |
<?php |
| 3024 |
} |
| 3025 |
|
| 3026 |
/** |
| 3027 |
* Render tools tab |
| 3028 |
*/ |
| 3029 |
private function render_tab_tools() { |
| 3030 |
// Get current settings for notification summary |
| 3031 |
$email_options = $this->settings->get_section( 'email' ); |
| 3032 |
$login_options = $this->settings->get_section( 'login_security' ); |
| 3033 |
$user_options = $this->settings->get_section( 'user_security' ); |
| 3034 |
$fi_options = $this->settings->get_section( 'file_integrity' ); |
| 3035 |
$alerts_options = $this->settings->get_section( 'audit_alerts' ); |
| 3036 |
$monitoring = $user_options['admin_monitoring'] ?? array(); |
| 3037 |
$registration = $user_options['registration_approval'] ?? array(); |
| 3038 |
$current_admin = get_option( 'admin_email' ); |
| 3039 |
$send_to_admin = ! isset( $email_options['send_to_admin_email'] ) || ! empty( $email_options['send_to_admin_email'] ); |
| 3040 |
$additional_raw = $email_options['additional_recipients'] ?? array(); |
| 3041 |
$additional = is_array( $additional_raw ) ? implode( "\n", $additional_raw ) : trim( $additional_raw ); |
| 3042 |
?> |
| 3043 |
|
| 3044 |
<!-- Notification Settings --> |
| 3045 |
<div id="vigilante-section-tools-notifications" class="vigilante-settings-section vigilante-notification-section"> |
| 3046 |
<h2><?php esc_html_e( 'Notification settings', 'vigilante' ); ?></h2> |
| 3047 |
<p><?php esc_html_e( 'Configure who receives all administrative email notifications from Vigilant. Individual notifications are enabled in their respective tabs.', 'vigilante' ); ?></p> |
| 3048 |
|
| 3049 |
<div class="vigilante-notification-layout"> |
| 3050 |
|
| 3051 |
<!-- Left column: Recipients settings --> |
| 3052 |
<div class="vigilante-notification-settings"> |
| 3053 |
<form class="vigilante-settings-form" data-section="email"> |
| 3054 |
|
| 3055 |
<table class="form-table vigilante-compact-form"> |
| 3056 |
<tr> |
| 3057 |
<th scope="row"><?php esc_html_e( 'WordPress Admin Email', 'vigilante' ); ?></th> |
| 3058 |
<td> |
| 3059 |
<label> |
| 3060 |
<input type="checkbox" name="email[send_to_admin_email]" value="1" <?php checked( $send_to_admin ); ?>> |
| 3061 |
<?php |
| 3062 |
printf( |
| 3063 |
/* translators: %s: Admin email address */ |
| 3064 |
esc_html__( 'Send to admin email (%s)', 'vigilante' ), |
| 3065 |
'<code>' . esc_html( $current_admin ) . '</code>' |
| 3066 |
); |
| 3067 |
?> |
| 3068 |
</label> |
| 3069 |
</td> |
| 3070 |
</tr> |
| 3071 |
<tr> |
| 3072 |
<th scope="row"><label for="vigilante-f-email-additional-recipients"><?php esc_html_e( 'Additional Recipients', 'vigilante' ); ?></label></th> |
| 3073 |
<td> |
| 3074 |
<textarea id="vigilante-f-email-additional-recipients" name="email[additional_recipients]" rows="3" class="large-text code" placeholder="maintenance@example.com security@example.com"><?php echo esc_textarea( $additional ); ?></textarea> |
| 3075 |
<p class="description"><?php esc_html_e( 'One email per line.', 'vigilante' ); ?></p> |
| 3076 |
</td> |
| 3077 |
</tr> |
| 3078 |
<tr> |
| 3079 |
<th scope="row"><?php esc_html_e( 'Plugin Deactivation', 'vigilante' ); ?></th> |
| 3080 |
<td> |
| 3081 |
<label> |
| 3082 |
<input type="checkbox" name="email[send_deactivation_email]" value="1" <?php checked( ! empty( $email_options['send_deactivation_email'] ) ); ?>> |
| 3083 |
<?php esc_html_e( 'Send email when Vigilant is deactivated', 'vigilante' ); ?> |
| 3084 |
</label> |
| 3085 |
</td> |
| 3086 |
</tr> |
| 3087 |
</table> |
| 3088 |
|
| 3089 |
<p class="submit vigilante-notification-submit"> |
| 3090 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 3091 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 3092 |
</button> |
| 3093 |
<button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>"> |
| 3094 |
<?php esc_html_e( 'Send test email', 'vigilante' ); ?> |
| 3095 |
</button> |
| 3096 |
<span class="vigilante-test-email-result" style="margin-left:8px;vertical-align:middle;"></span> |
| 3097 |
</p> |
| 3098 |
<p class="description"><?php esc_html_e( 'The test goes to the recipients above and confirms that email delivery works for every Vigilant notification.', 'vigilante' ); ?></p> |
| 3099 |
|
| 3100 |
</form> |
| 3101 |
</div> |
| 3102 |
|
| 3103 |
<!-- Right column: Active notifications summary --> |
| 3104 |
<div class="vigilante-notification-summary"> |
| 3105 |
<h4><?php esc_html_e( 'Active notifications', 'vigilante' ); ?></h4> |
| 3106 |
|
| 3107 |
<table class="widefat striped"> |
| 3108 |
<thead> |
| 3109 |
<tr> |
| 3110 |
<th><?php esc_html_e( 'Notification', 'vigilante' ); ?></th> |
| 3111 |
<th style="width: 1%; white-space: nowrap; text-align: center;"><?php esc_html_e( 'Status', 'vigilante' ); ?></th> |
| 3112 |
<th style="width: 50px; text-align: center;"></th> |
| 3113 |
</tr> |
| 3114 |
</thead> |
| 3115 |
<tbody> |
| 3116 |
<?php |
| 3117 |
$notifications = array( |
| 3118 |
array( |
| 3119 |
'label' => __( 'Login lockout', 'vigilante' ), |
| 3120 |
'active' => ! empty( $login_options['notify_on_lockout'] ), |
| 3121 |
'tab' => 'login', |
| 3122 |
), |
| 3123 |
array( |
| 3124 |
'label' => __( 'Administrator login', 'vigilante' ), |
| 3125 |
'active' => ! empty( $login_options['notify_on_admin_login'] ), |
| 3126 |
'tab' => 'login', |
| 3127 |
), |
| 3128 |
array( |
| 3129 |
'label' => __( 'New administrator created', 'vigilante' ), |
| 3130 |
'active' => ! empty( $monitoring['alert_new_admin'] ), |
| 3131 |
'tab' => 'users', |
| 3132 |
), |
| 3133 |
array( |
| 3134 |
'label' => __( 'Administrator email changed', 'vigilante' ), |
| 3135 |
'active' => ! empty( $monitoring['alert_admin_email_change'] ), |
| 3136 |
'tab' => 'users', |
| 3137 |
), |
| 3138 |
array( |
| 3139 |
'label' => __( 'Permission elevation', 'vigilante' ), |
| 3140 |
'active' => ! empty( $monitoring['alert_permission_elevation'] ), |
| 3141 |
'tab' => 'users', |
| 3142 |
), |
| 3143 |
array( |
| 3144 |
'label' => __( 'Admin password changed', 'vigilante' ), |
| 3145 |
'active' => ! empty( $monitoring['alert_admin_password_change'] ), |
| 3146 |
'tab' => 'users', |
| 3147 |
), |
| 3148 |
array( |
| 3149 |
'label' => __( 'Registration pending approval', 'vigilante' ), |
| 3150 |
'active' => ! empty( $registration['enabled'] ) && ! empty( $registration['notify_admin'] ), |
| 3151 |
'tab' => 'users', |
| 3152 |
), |
| 3153 |
array( |
| 3154 |
'label' => __( 'File integrity scan report', 'vigilante' ), |
| 3155 |
'active' => ( $fi_options['notify_level'] ?? 'disabled' ) !== 'disabled', |
| 3156 |
'tab' => 'file-integrity', |
| 3157 |
), |
| 3158 |
array( |
| 3159 |
'label' => __( 'File integrity instant alert', 'vigilante' ), |
| 3160 |
'active' => ! empty( $fi_options['instant_alert'] ), |
| 3161 |
'tab' => 'file-integrity', |
| 3162 |
), |
| 3163 |
array( |
| 3164 |
'label' => __( 'Audit alert: immediate', 'vigilante' ), |
| 3165 |
'active' => Vigilante_Audit_Alerts::immediate_is_active( $alerts_options ), |
| 3166 |
'tab' => 'activity-log', |
| 3167 |
'anchor' => 'vigilante-section-audit-alerts', |
| 3168 |
), |
| 3169 |
array( |
| 3170 |
'label' => __( 'Audit alert: threshold', 'vigilante' ), |
| 3171 |
'active' => Vigilante_Audit_Alerts::threshold_is_active( $alerts_options ), |
| 3172 |
'tab' => 'activity-log', |
| 3173 |
'anchor' => 'vigilante-section-audit-alerts', |
| 3174 |
), |
| 3175 |
array( |
| 3176 |
'label' => __( 'Under Attack mode', 'vigilante' ), |
| 3177 |
'active' => true, |
| 3178 |
'tab' => '', |
| 3179 |
'note' => __( 'Always active', 'vigilante' ), |
| 3180 |
), |
| 3181 |
array( |
| 3182 |
'label' => __( 'Plugin deactivation', 'vigilante' ), |
| 3183 |
'active' => ! empty( $email_options['send_deactivation_email'] ), |
| 3184 |
'tab' => 'tools', |
| 3185 |
), |
| 3186 |
); |
| 3187 |
|
| 3188 |
foreach ( $notifications as $notif ) : |
| 3189 |
$status_class = $notif['active'] ? 'vigilante-status-active' : 'vigilante-status-inactive'; |
| 3190 |
$status_label = $notif['active'] ? __( 'Active', 'vigilante' ) : __( 'Inactive', 'vigilante' ); |
| 3191 |
if ( ! empty( $notif['note'] ) ) { |
| 3192 |
$status_label = $notif['note']; |
| 3193 |
} |
| 3194 |
?> |
| 3195 |
<tr> |
| 3196 |
<td><?php echo esc_html( $notif['label'] ); ?></td> |
| 3197 |
<td style="text-align: center; white-space: nowrap;"> |
| 3198 |
<span class="<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span> |
| 3199 |
</td> |
| 3200 |
<td style="text-align: center;"> |
| 3201 |
<?php if ( ! empty( $notif['tab'] ) ) : ?> |
| 3202 |
<a href="<?php echo esc_url( admin_url( 'admin.php?page=vigilante&tab=' . $notif['tab'] ) . ( ! empty( $notif['anchor'] ) ? '#' . $notif['anchor'] : '' ) ); ?>" class="button button-small"> |
| 3203 |
<span class="dashicons dashicons-admin-generic" style="font-size: 14px; line-height: 1.8;"></span> |
| 3204 |
</a> |
| 3205 |
<?php else : ?> |
| 3206 |
— |
| 3207 |
<?php endif; ?> |
| 3208 |
</td> |
| 3209 |
</tr> |
| 3210 |
<?php endforeach; ?> |
| 3211 |
</tbody> |
| 3212 |
</table> |
| 3213 |
</div> |
| 3214 |
|
| 3215 |
</div> |
| 3216 |
</div> |
| 3217 |
|
| 3218 |
<h2 id="vigilante-section-tools-main" class="vigilante-tools-heading"><?php esc_html_e( 'Tools', 'vigilante' ); ?></h2> |
| 3219 |
|
| 3220 |
<?php |
| 3221 |
// Warn that during Under Attack mode the export/import operate against |
| 3222 |
// the temporary hardened config and any imported changes will be |
| 3223 |
// reverted when the mode ends. |
| 3224 |
$ua_status = get_option( Vigilante_Under_Attack::OPTION_NAME, array() ); |
| 3225 |
if ( ! empty( $ua_status['active'] ) ) : |
| 3226 |
?> |
| 3227 |
<div class="vigilante-ua-tools-notice-wrap"> |
| 3228 |
<div class="notice notice-warning inline vigilante-ua-tools-notice"> |
| 3229 |
<p> |
| 3230 |
<strong><?php esc_html_e( 'Under Attack mode is active.', 'vigilante' ); ?></strong> |
| 3231 |
<?php esc_html_e( 'Exports will reflect the temporary hardened configuration, not your saved one. Imports will apply on top of the hardened config and will be reverted when the mode ends. Consider waiting until you deactivate Under Attack before exporting or importing settings.', 'vigilante' ); ?> |
| 3232 |
</p> |
| 3233 |
</div> |
| 3234 |
</div> |
| 3235 |
<?php |
| 3236 |
endif; |
| 3237 |
?> |
| 3238 |
|
| 3239 |
<div class="vigilante-tools-grid"> |
| 3240 |
<div class="vigilante-tool-card"> |
| 3241 |
<h3><?php esc_html_e( 'Export Settings', 'vigilante' ); ?></h3> |
| 3242 |
<p><?php esc_html_e( 'Download your current security settings as a JSON file.', 'vigilante' ); ?></p> |
| 3243 |
<button type="button" class="button vigilante-export-settings"> |
| 3244 |
<?php esc_html_e( 'Export Settings', 'vigilante' ); ?> |
| 3245 |
</button> |
| 3246 |
</div> |
| 3247 |
|
| 3248 |
<div class="vigilante-tool-card"> |
| 3249 |
<h3><?php esc_html_e( 'Import Settings', 'vigilante' ); ?></h3> |
| 3250 |
<p><?php esc_html_e( 'Import settings from a previously exported JSON file.', 'vigilante' ); ?></p> |
| 3251 |
<input type="file" id="vigilante-import-file" aria-label="<?php esc_attr_e( 'Configuration file to import', 'vigilante' ); ?>" accept=".json" style="display: none;"> |
| 3252 |
<button type="button" class="button vigilante-import-settings"> |
| 3253 |
<?php esc_html_e( 'Import Settings', 'vigilante' ); ?> |
| 3254 |
</button> |
| 3255 |
</div> |
| 3256 |
|
| 3257 |
<div class="vigilante-tool-card"> |
| 3258 |
<h3><?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?></h3> |
| 3259 |
<p><?php esc_html_e( 'Reset all the Vigilant security settings to default values. Your IP lists, custom login address, two-factor setup, scan exclusions and extra alert recipients are kept.', 'vigilante' ); ?></p> |
| 3260 |
<button type="button" class="button vigilante-reset-settings" style="color: #a00;"> |
| 3261 |
<?php esc_html_e( 'Reset All Settings', 'vigilante' ); ?> |
| 3262 |
</button> |
| 3263 |
</div> |
| 3264 |
|
| 3265 |
<div class="vigilante-tool-card"> |
| 3266 |
<h3><?php esc_html_e( 'Download Config Backup', 'vigilante' ); ?></h3> |
| 3267 |
<p><?php esc_html_e( 'Download a ZIP backup of your wp-config.php and .htaccess (plus robots.txt if present) before making security changes. The archive is built on the fly and sent to your browser, so nothing is left on the server.', 'vigilante' ); ?></p> |
| 3268 |
<?php if ( $this->shared_files_locked() ) : ?> |
| 3269 |
<p class="description"><?php esc_html_e( 'Both files belong to the whole network, and wp-config.php carries the database credentials and the authentication salts of every site. The copy is taken from the main site.', 'vigilante' ); ?></p> |
| 3270 |
<?php else : ?> |
| 3271 |
<button type="button" class="button vigilante-create-backup"> |
| 3272 |
<?php esc_html_e( 'Download Backup', 'vigilante' ); ?> |
| 3273 |
</button> |
| 3274 |
<?php endif; ?> |
| 3275 |
</div> |
| 3276 |
|
| 3277 |
<?php if ( ! $this->shared_files_locked() ) : ?> |
| 3278 |
|
| 3279 |
<div class="vigilante-tool-card vigilante-tool-card-wide"> |
| 3280 |
<h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3> |
| 3281 |
<p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p> |
| 3282 |
<button type="button" class="button vigilante-db-backup-toggle"> |
| 3283 |
<?php esc_html_e( 'Download Database Backup', 'vigilante' ); ?> |
| 3284 |
</button> |
| 3285 |
|
| 3286 |
<div class="vigilante-db-backup-panel" style="display: none;"> |
| 3287 |
<div class="vigilante-db-tables-loading"> |
| 3288 |
<span class="spinner is-active"></span> |
| 3289 |
<?php esc_html_e( 'Loading tables...', 'vigilante' ); ?> |
| 3290 |
</div> |
| 3291 |
|
| 3292 |
<div class="vigilante-db-tables-content" style="display: none;"> |
| 3293 |
<div class="vigilante-db-tables-controls"> |
| 3294 |
<label> |
| 3295 |
<input type="checkbox" id="vigilante-db-select-all" checked> |
| 3296 |
<strong><?php esc_html_e( 'Select / deselect all', 'vigilante' ); ?></strong> |
| 3297 |
</label> |
| 3298 |
<span class="vigilante-db-tables-info"></span> |
| 3299 |
</div> |
| 3300 |
|
| 3301 |
<div class="vigilante-db-tables-group"> |
| 3302 |
<h4><?php esc_html_e( 'WordPress core tables', 'vigilante' ); ?></h4> |
| 3303 |
<div class="vigilante-db-tables-list" id="vigilante-db-core-tables"></div> |
| 3304 |
</div> |
| 3305 |
|
| 3306 |
<div class="vigilante-db-tables-group" id="vigilante-db-other-group" style="display: none;"> |
| 3307 |
<h4><?php esc_html_e( 'Plugin and custom tables', 'vigilante' ); ?></h4> |
| 3308 |
<div class="vigilante-db-tables-list" id="vigilante-db-other-tables"></div> |
| 3309 |
</div> |
| 3310 |
|
| 3311 |
<div class="vigilante-db-backup-actions"> |
| 3312 |
<button type="button" class="button button-primary vigilante-db-backup-download"> |
| 3313 |
<?php esc_html_e( 'Download Backup (.zip)', 'vigilante' ); ?> |
| 3314 |
</button> |
| 3315 |
</div> |
| 3316 |
</div> |
| 3317 |
</div> |
| 3318 |
</div> |
| 3319 |
<?php else : ?> |
| 3320 |
<div class="vigilante-tool-card vigilante-tool-card-wide"> |
| 3321 |
<h3><?php esc_html_e( 'Database Backup', 'vigilante' ); ?></h3> |
| 3322 |
<p><?php esc_html_e( 'Download a backup of your database as a ZIP file. Select which tables to include.', 'vigilante' ); ?></p> |
| 3323 |
<p class="description"><?php esc_html_e( 'The database is shared by the whole network, so a backup taken here would carry every other site and all of the network users. The copy is taken from the main site.', 'vigilante' ); ?></p> |
| 3324 |
</div> |
| 3325 |
<?php endif; ?> |
| 3326 |
</div> |
| 3327 |
<?php |
| 3328 |
} |
| 3329 |
|
| 3330 |
/** |
| 3331 |
* Render firewall tab |
| 3332 |
*/ |
| 3333 |
private function render_tab_firewall() { |
| 3334 |
$is_disabled = $this->render_module_disabled_notice( 'firewall' ); |
| 3335 |
$options = $this->settings->get_section( 'firewall' ); |
| 3336 |
?> |
| 3337 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="firewall" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 3338 |
<div id="vigilante-section-firewall-main" class="vigilante-settings-section"> |
| 3339 |
<h2> |
| 3340 |
<?php esc_html_e( 'Firewall Protection', 'vigilante' ); ?> |
| 3341 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 3342 |
</h2> |
| 3343 |
<p><?php esc_html_e( 'PHP-based request filtering. Analyzes each request before WordPress loads.', 'vigilante' ); ?></p> |
| 3344 |
<div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 3345 |
<p style="margin:0;"> |
| 3346 |
<?php esc_html_e( 'Full page caching systems that serve cached pages before PHP executes (Varnish, LiteSpeed Cache, NGINX FastCGI Cache, Cloudflare APO) may bypass PHP-level firewall rules for cached requests. The .htaccess rules will still apply on Apache/LiteSpeed servers.', 'vigilante' ); ?> |
| 3347 |
</p> |
| 3348 |
</div> |
| 3349 |
|
| 3350 |
<?php $vg_main_locked = $this->main_site_files_locked(); ?> |
| 3351 |
<?php if ( $vg_main_locked ) : ?> |
| 3352 |
<div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 3353 |
<p style="margin:0;"><?php esc_html_e( 'On the main site of a network, blocking bad bots and bad query strings, the visitor IP detection and the two whitelists also build the .htaccess rules every site shares, so only a network administrator can change them.', 'vigilante' ); ?></p> |
| 3354 |
</div> |
| 3355 |
<?php endif; ?> |
| 3356 |
|
| 3357 |
<table class="form-table"> |
| 3358 |
<tr> |
| 3359 |
<th scope="row"><?php esc_html_e( 'Block Bad Query Strings', 'vigilante' ); ?></th> |
| 3360 |
<td> |
| 3361 |
<label> |
| 3362 |
<input type="checkbox" name="firewall[block_bad_query_strings]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_query_strings'] ) ); ?>> |
| 3363 |
<?php esc_html_e( 'Block malicious query string patterns', 'vigilante' ); ?> |
| 3364 |
</label> |
| 3365 |
</td> |
| 3366 |
</tr> |
| 3367 |
<tr> |
| 3368 |
<th scope="row"><?php esc_html_e( 'SQL Injection Protection', 'vigilante' ); ?></th> |
| 3369 |
<td> |
| 3370 |
<label> |
| 3371 |
<input type="checkbox" name="firewall[block_sql_injection]" value="1" <?php checked( ! empty( $options['block_sql_injection'] ) ); ?>> |
| 3372 |
<?php esc_html_e( 'Block SQL injection attempts', 'vigilante' ); ?> |
| 3373 |
</label> |
| 3374 |
</td> |
| 3375 |
</tr> |
| 3376 |
<tr> |
| 3377 |
<th scope="row"><?php esc_html_e( 'XSS Protection', 'vigilante' ); ?></th> |
| 3378 |
<td> |
| 3379 |
<label> |
| 3380 |
<input type="checkbox" name="firewall[block_xss_attacks]" value="1" <?php checked( ! empty( $options['block_xss_attacks'] ) ); ?>> |
| 3381 |
<?php esc_html_e( 'Block cross-site scripting attacks', 'vigilante' ); ?> |
| 3382 |
</label> |
| 3383 |
</td> |
| 3384 |
</tr> |
| 3385 |
<tr> |
| 3386 |
<th scope="row"><?php esc_html_e( 'File Inclusion Protection', 'vigilante' ); ?></th> |
| 3387 |
<td> |
| 3388 |
<label> |
| 3389 |
<input type="checkbox" name="firewall[block_file_inclusion]" value="1" <?php checked( ! empty( $options['block_file_inclusion'] ) ); ?>> |
| 3390 |
<?php esc_html_e( 'Block local/remote file inclusion attempts', 'vigilante' ); ?> |
| 3391 |
</label> |
| 3392 |
</td> |
| 3393 |
</tr> |
| 3394 |
<tr> |
| 3395 |
<th scope="row"><?php esc_html_e( 'Directory Traversal Protection', 'vigilante' ); ?></th> |
| 3396 |
<td> |
| 3397 |
<label> |
| 3398 |
<input type="checkbox" name="firewall[block_directory_traversal]" value="1" <?php checked( ! empty( $options['block_directory_traversal'] ) ); ?>> |
| 3399 |
<?php esc_html_e( 'Block path traversal attempts', 'vigilante' ); ?> |
| 3400 |
</label> |
| 3401 |
</td> |
| 3402 |
</tr> |
| 3403 |
<tr> |
| 3404 |
<th scope="row"><?php esc_html_e( 'Block Bad Bots', 'vigilante' ); ?></th> |
| 3405 |
<td> |
| 3406 |
<label> |
| 3407 |
<input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>> |
| 3408 |
<?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?> |
| 3409 |
</label> |
| 3410 |
</td> |
| 3411 |
</tr> |
| 3412 |
</table> |
| 3413 |
|
| 3414 |
<h3><?php esc_html_e( 'Rate Limiting', 'vigilante' ); ?></h3> |
| 3415 |
<table class="form-table"> |
| 3416 |
<tr> |
| 3417 |
<th scope="row"><?php esc_html_e( 'Enable Rate Limiting', 'vigilante' ); ?></th> |
| 3418 |
<td> |
| 3419 |
<label> |
| 3420 |
<input type="checkbox" name="firewall[rate_limiting][enabled]" value="1" <?php checked( ! empty( $options['rate_limiting']['enabled'] ) ); ?>> |
| 3421 |
<?php esc_html_e( 'Limit requests per IP address', 'vigilante' ); ?> |
| 3422 |
</label> |
| 3423 |
</td> |
| 3424 |
</tr> |
| 3425 |
<tr> |
| 3426 |
<th scope="row"><label for="vigilante-f-firewall-rate-limiting-requests-per-minute"><?php esc_html_e( 'Requests per Minute', 'vigilante' ); ?></label></th> |
| 3427 |
<td> |
| 3428 |
<input id="vigilante-f-firewall-rate-limiting-requests-per-minute" type="number" name="firewall[rate_limiting][requests_per_minute]" value="<?php echo esc_attr( $options['rate_limiting']['requests_per_minute'] ?? 120 ); ?>" min="10" max="500" class="small-text"> |
| 3429 |
<p class="description"> |
| 3430 |
<?php esc_html_e( 'Counts only PHP requests to WordPress (pages, admin-ajax, REST, login) from a single IP, not static assets like images, CSS or JS. 120/min suits most sites; sustained traffic above that from one IP is usually a bot. To allow a legitimate service, whitelist its IP instead of raising the limit.', 'vigilante' ); ?> |
| 3431 |
</p> |
| 3432 |
</td> |
| 3433 |
</tr> |
| 3434 |
<tr> |
| 3435 |
<th scope="row"><label for="vigilante-f-firewall-rate-limiting-block-duration"><?php esc_html_e( 'Block Duration (seconds)', 'vigilante' ); ?></label></th> |
| 3436 |
<td> |
| 3437 |
<input id="vigilante-f-firewall-rate-limiting-block-duration" type="number" name="firewall[rate_limiting][block_duration]" value="<?php echo esc_attr( $options['rate_limiting']['block_duration'] ?? 300 ); ?>" min="60" max="3600" class="small-text"> |
| 3438 |
</td> |
| 3439 |
</tr> |
| 3440 |
<tr> |
| 3441 |
<th scope="row"><?php esc_html_e( 'Progressive Blocking', 'vigilante' ); ?></th> |
| 3442 |
<td> |
| 3443 |
<label> |
| 3444 |
<input type="checkbox" name="firewall[rate_limiting][progressive]" value="1" <?php checked( ! empty( $options['rate_limiting']['progressive'] ) ); ?>> |
| 3445 |
<?php esc_html_e( 'Double block duration on each repeat offense', 'vigilante' ); ?> |
| 3446 |
</label> |
| 3447 |
<p class="description"> |
| 3448 |
<?php |
| 3449 |
$base = absint( $options['rate_limiting']['block_duration'] ?? 300 ); |
| 3450 |
printf( |
| 3451 |
/* translators: 1: First block duration, 2: Second, 3: Third */ |
| 3452 |
esc_html__( 'Example: %1$s → %2$s → %3$s and so on, up to the maximum.', 'vigilante' ), |
| 3453 |
esc_html( human_time_diff( 0, $base ) ), |
| 3454 |
esc_html( human_time_diff( 0, $base * 2 ) ), |
| 3455 |
esc_html( human_time_diff( 0, $base * 4 ) ) |
| 3456 |
); |
| 3457 |
?> |
| 3458 |
</p> |
| 3459 |
</td> |
| 3460 |
</tr> |
| 3461 |
<tr> |
| 3462 |
<th scope="row"><label for="vigilante-f-firewall-rate-limiting-max-block-duration"><?php esc_html_e( 'Maximum Block Duration', 'vigilante' ); ?></label></th> |
| 3463 |
<td> |
| 3464 |
<select id="vigilante-f-firewall-rate-limiting-max-block-duration" name="firewall[rate_limiting][max_block_duration]"> |
| 3465 |
<?php |
| 3466 |
$max_options = array( |
| 3467 |
3600 => __( '1 hour', 'vigilante' ), |
| 3468 |
21600 => __( '6 hours', 'vigilante' ), |
| 3469 |
43200 => __( '12 hours', 'vigilante' ), |
| 3470 |
86400 => __( '24 hours', 'vigilante' ), |
| 3471 |
604800 => __( '7 days', 'vigilante' ), |
| 3472 |
); |
| 3473 |
$current_max = absint( $options['rate_limiting']['max_block_duration'] ?? 86400 ); |
| 3474 |
foreach ( $max_options as $val => $label ) : |
| 3475 |
?> |
| 3476 |
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $current_max, $val ); ?>> |
| 3477 |
<?php echo esc_html( $label ); ?> |
| 3478 |
</option> |
| 3479 |
<?php endforeach; ?> |
| 3480 |
</select> |
| 3481 |
<p class="description"><?php esc_html_e( 'Upper limit for progressive blocking.', 'vigilante' ); ?></p> |
| 3482 |
</td> |
| 3483 |
</tr> |
| 3484 |
</table> |
| 3485 |
|
| 3486 |
<?php |
| 3487 |
// Currently blocked IPs from rate limiting |
| 3488 |
$active_blocks = Vigilante_Firewall::get_active_blocks(); |
| 3489 |
if ( ! empty( $active_blocks ) ) : |
| 3490 |
?> |
| 3491 |
<div class="vigilante-settings-section vigilante-lockout-section" style="margin-top:20px;"> |
| 3492 |
<h3><?php esc_html_e( 'Currently Blocked IPs', 'vigilante' ); ?></h3> |
| 3493 |
<table class="wp-list-table widefat fixed striped" style="max-width:800px;"> |
| 3494 |
<thead> |
| 3495 |
<tr> |
| 3496 |
<th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th> |
| 3497 |
<th><?php esc_html_e( 'Blocked', 'vigilante' ); ?></th> |
| 3498 |
<th><?php esc_html_e( 'Expires in', 'vigilante' ); ?></th> |
| 3499 |
<th><?php esc_html_e( 'Strikes', 'vigilante' ); ?></th> |
| 3500 |
<th><?php esc_html_e( 'Action', 'vigilante' ); ?></th> |
| 3501 |
</tr> |
| 3502 |
</thead> |
| 3503 |
<tbody> |
| 3504 |
<?php foreach ( $active_blocks as $blocked_ip => $block_data ) : ?> |
| 3505 |
<tr> |
| 3506 |
<td><code><?php echo esc_html( $blocked_ip ); ?></code></td> |
| 3507 |
<td><?php echo esc_html( human_time_diff( $block_data['blocked_at'] ) . ' ' . __( 'ago', 'vigilante' ) ); ?></td> |
| 3508 |
<td><?php echo esc_html( human_time_diff( time(), $block_data['expires'] ) ); ?></td> |
| 3509 |
<td><?php echo esc_html( $block_data['strikes'] ?? 1 ); ?></td> |
| 3510 |
<td> |
| 3511 |
<button type="button" class="button button-small vigilante-unblock-firewall-ip" |
| 3512 |
data-ip="<?php echo esc_attr( $blocked_ip ); ?>"> |
| 3513 |
<?php esc_html_e( 'Unblock', 'vigilante' ); ?> |
| 3514 |
</button> |
| 3515 |
</td> |
| 3516 |
</tr> |
| 3517 |
<?php endforeach; ?> |
| 3518 |
</tbody> |
| 3519 |
</table> |
| 3520 |
</div> |
| 3521 |
<?php endif; ?> |
| 3522 |
|
| 3523 |
<?php |
| 3524 |
// Since 2.11.8 X-Forwarded-For is read from its end, where the proxy |
| 3525 |
// writes. The administrator's own request shows whether that end is |
| 3526 |
// a CDN or a balancer for everybody here. Cross review of 2.11.8. |
| 3527 |
$xff_readings = $this->forwarded_chain_readings(); |
| 3528 |
if ( $xff_readings ) : |
| 3529 |
?> |
| 3530 |
<div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 3531 |
<p style="margin:0;"> |
| 3532 |
<?php |
| 3533 |
printf( |
| 3534 |
/* translators: 1: last address in the header, the one Vigilant reads, 2: first address in the header, which a visitor can write */ |
| 3535 |
esc_html__( 'Your own request reaches the site with more than one public address in X-Forwarded-For. Vigilant reads the last one, %1$s, which is the one your proxy added, and not the first one, %2$s, which a visitor can write. If %1$s belongs to a CDN or a load balancer rather than to you, every visitor shares it for rate limiting, login lockouts and the IP lists: choose the header of that CDN in Visitor IP detection, such as CF-Connecting-IP for Cloudflare.', 'vigilante' ), |
| 3536 |
esc_html( $xff_readings['now'] ), |
| 3537 |
esc_html( $xff_readings['before'] ) |
| 3538 |
); |
| 3539 |
?> |
| 3540 |
</p> |
| 3541 |
</div> |
| 3542 |
<?php endif; ?> |
| 3543 |
|
| 3544 |
<h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3> |
| 3545 |
<p class="description"> |
| 3546 |
<?php |
| 3547 |
printf( |
| 3548 |
/* translators: %s: Current visitor IP address */ |
| 3549 |
esc_html__( 'Your current IP address: %s', 'vigilante' ), |
| 3550 |
'<code>' . esc_html( $this->database->get_client_ip() ) . '</code>' |
| 3551 |
); |
| 3552 |
?> |
| 3553 |
</p> |
| 3554 |
<table class="form-table"> |
| 3555 |
<tr> |
| 3556 |
<th scope="row"><label for="vigilante-f-firewall-trusted-proxy-header"><?php esc_html_e( 'Visitor IP detection', 'vigilante' ); ?></label></th> |
| 3557 |
<td> |
| 3558 |
<?php $proxy_header = $options['trusted_proxy_header'] ?? ''; ?> |
| 3559 |
<select id="vigilante-f-firewall-trusted-proxy-header" name="firewall[trusted_proxy_header]" <?php disabled( $vg_main_locked ); ?>> |
| 3560 |
<option value="" <?php selected( $proxy_header, '' ); ?>><?php esc_html_e( 'Direct connection, only REMOTE_ADDR (recommended)', 'vigilante' ); ?></option> |
| 3561 |
<option value="cf-connecting-ip" <?php selected( $proxy_header, 'cf-connecting-ip' ); ?>><?php esc_html_e( 'Behind Cloudflare (CF-Connecting-IP)', 'vigilante' ); ?></option> |
| 3562 |
<option value="x-forwarded-for" <?php selected( $proxy_header, 'x-forwarded-for' ); ?>><?php esc_html_e( 'Behind a reverse proxy or load balancer (X-Forwarded-For)', 'vigilante' ); ?></option> |
| 3563 |
<option value="x-real-ip" <?php selected( $proxy_header, 'x-real-ip' ); ?>><?php esc_html_e( 'Behind an nginx proxy (X-Real-IP)', 'vigilante' ); ?></option> |
| 3564 |
</select> |
| 3565 |
<p class="description"> |
| 3566 |
<?php esc_html_e( 'Where to read the visitor IP from. Leave on "Direct connection" unless your site really sits behind that proxy or CDN. Trusting a forwarded header on a site that is not behind it lets visitors spoof their IP and bypass the IP lists and rate limiting.', 'vigilante' ); ?> |
| 3567 |
</p> |
| 3568 |
</td> |
| 3569 |
</tr> |
| 3570 |
<tr> |
| 3571 |
<th scope="row"><label for="vigilante-f-firewall-trusted-proxies"><?php esc_html_e( 'Trusted proxy IPs', 'vigilante' ); ?></label></th> |
| 3572 |
<td> |
| 3573 |
<textarea id="vigilante-f-firewall-trusted-proxies" name="firewall[trusted_proxies]" rows="3" class="large-text code" placeholder="10.0.0.0/8 192.168.1.1" <?php disabled( $vg_main_locked ); ?>><?php echo esc_textarea( implode( "\n", $options['trusted_proxies'] ?? array() ) ); ?></textarea> |
| 3574 |
<p class="description"> |
| 3575 |
<?php esc_html_e( 'Only used with a forwarded header selected above. One IP or CIDR range per line: the addresses your proxy or load balancer connects from. The forwarded header is accepted only from these. Left empty, Vigilant accepts it from your own private network, and for Cloudflare from Cloudflare\'s own ranges automatically.', 'vigilante' ); ?> |
| 3576 |
<?php if ( in_array( $proxy_header, array( 'x-forwarded-for', 'x-real-ip' ), true ) && empty( $options['trusted_proxies'] ) ) : ?> |
| 3577 |
<br><strong><?php esc_html_e( 'The header above is trusted but no proxy IPs are set. If your proxy or load balancer connects from a public address, add it here, or the header is ignored for safety and every visitor is seen as that proxy.', 'vigilante' ); ?></strong> |
| 3578 |
<?php endif; ?> |
| 3579 |
</p> |
| 3580 |
</td> |
| 3581 |
</tr> |
| 3582 |
<tr> |
| 3583 |
<th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th> |
| 3584 |
<td> |
| 3585 |
<textarea id="vigilante-f-firewall-ip-whitelist" name="firewall[ip_whitelist]" <?php disabled( $vg_main_locked ); ?> rows="4" class="large-text code" placeholder="192.168.1.50 192.168.1.0/24 192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea> |
| 3586 |
<p class="description"> |
| 3587 |
<?php esc_html_e( 'One IP per line. These IPs bypass the firewall checks, and they also reach wp-admin when the login URL is hidden, so remote managers such as MainWP or ManageWP are not turned away with a 404. The hidden login form itself stays hidden for every IP, this one included.', 'vigilante' ); ?> |
| 3588 |
<br> |
| 3589 |
<?php |
| 3590 |
printf( |
| 3591 |
/* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */ |
| 3592 |
esc_html__( 'Accepts exact IPs (%1$s192.168.1.50%2$s), CIDR ranges (%1$s192.168.1.0/24%2$s, IPv4 and IPv6), and wildcards with %1$s*%2$s (e.g. %1$s192.168.1.*%2$s).', 'vigilante' ), |
| 3593 |
'<code>', |
| 3594 |
'</code>' |
| 3595 |
); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. |
| 3596 |
?> |
| 3597 |
</p> |
| 3598 |
</td> |
| 3599 |
</tr> |
| 3600 |
<tr> |
| 3601 |
<th scope="row"><label for="vigilante-f-firewall-ip-blacklist"><?php esc_html_e( 'IP Blacklist', 'vigilante' ); ?></label></th> |
| 3602 |
<td> |
| 3603 |
<textarea id="vigilante-f-firewall-ip-blacklist" name="firewall[ip_blacklist]" rows="4" class="large-text code" placeholder="203.0.113.42 203.0.113.0/24 203.0.113.*"><?php echo esc_textarea( implode( "\n", $options['ip_blacklist'] ?? array() ) ); ?></textarea> |
| 3604 |
<p class="description"> |
| 3605 |
<?php esc_html_e( 'One IP per line. These IPs will be blocked immediately.', 'vigilante' ); ?> |
| 3606 |
<br> |
| 3607 |
<?php |
| 3608 |
printf( |
| 3609 |
/* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the IP, CIDR and wildcard examples. */ |
| 3610 |
esc_html__( 'Accepts exact IPs (%1$s203.0.113.42%2$s), CIDR ranges (%1$s203.0.113.0/24%2$s, IPv4 and IPv6), and wildcards with %1$s*%2$s (e.g. %1$s203.0.113.*%2$s).', 'vigilante' ), |
| 3611 |
'<code>', |
| 3612 |
'</code>' |
| 3613 |
); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. |
| 3614 |
?> |
| 3615 |
</p> |
| 3616 |
</td> |
| 3617 |
</tr> |
| 3618 |
</table> |
| 3619 |
|
| 3620 |
<h3><?php esc_html_e( 'User-Agent Lists', 'vigilante' ); ?></h3> |
| 3621 |
<p><?php esc_html_e( 'Partial matching: enter a keyword and any User-Agent containing it will be matched.', 'vigilante' ); ?></p> |
| 3622 |
<table class="form-table"> |
| 3623 |
<tr> |
| 3624 |
<th scope="row"><label for="vigilante-f-firewall-ua-whitelist"><?php esc_html_e( 'User-Agent Whitelist', 'vigilante' ); ?></label></th> |
| 3625 |
<td> |
| 3626 |
<textarea id="vigilante-f-firewall-ua-whitelist" name="firewall[ua_whitelist]" <?php disabled( $vg_main_locked ); ?> rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_whitelist'] ?? array() ) ); ?></textarea> |
| 3627 |
<p class="description"><?php esc_html_e( 'One User-Agent per line. These will bypass all firewall checks. Example: ManageWP, MainWP, UptimeRobot.', 'vigilante' ); ?></p> |
| 3628 |
</td> |
| 3629 |
</tr> |
| 3630 |
<tr> |
| 3631 |
<th scope="row"><label for="vigilante-f-firewall-ua-blacklist"><?php esc_html_e( 'User-Agent Blacklist', 'vigilante' ); ?></label></th> |
| 3632 |
<td> |
| 3633 |
<textarea id="vigilante-f-firewall-ua-blacklist" name="firewall[ua_blacklist]" rows="4" class="large-text code"><?php echo esc_textarea( implode( "\n", $options['ua_blacklist'] ?? array() ) ); ?></textarea> |
| 3634 |
<p class="description"><?php esc_html_e( 'One User-Agent per line. These will be blocked immediately.', 'vigilante' ); ?></p> |
| 3635 |
</td> |
| 3636 |
</tr> |
| 3637 |
</table> |
| 3638 |
</div> |
| 3639 |
|
| 3640 |
<?php |
| 3641 |
$vg_shared_locked = $this->shared_files_locked(); |
| 3642 |
// Paint what is actually in force, not this site's unused copy. |
| 3643 |
$vg_local_options = $options; |
| 3644 |
$options = $this->get_section_for_display( 'firewall' ); |
| 3645 |
?> |
| 3646 |
<?php $this->render_shared_files_notice(); ?> |
| 3647 |
<div id="vigilante-section-firewall-server" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>> |
| 3648 |
<h2> |
| 3649 |
<?php esc_html_e( 'Server Protection', 'vigilante' ); ?> |
| 3650 |
<span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span> |
| 3651 |
</h2> |
| 3652 |
<p><?php esc_html_e( 'Server-level rules for Apache/LiteSpeed. These rules are processed before PHP.', 'vigilante' ); ?></p> |
| 3653 |
|
| 3654 |
<table class="form-table"> |
| 3655 |
<tr id="field-disable-directory-browsing"> |
| 3656 |
<th scope="row"><?php esc_html_e( 'Directory Browsing', 'vigilante' ); ?></th> |
| 3657 |
<td> |
| 3658 |
<label> |
| 3659 |
<input type="checkbox" name="firewall[disable_directory_browsing]" value="1" <?php checked( ! empty( $options['disable_directory_browsing'] ) ); ?>> |
| 3660 |
<?php esc_html_e( 'Disable directory listing (Options -Indexes)', 'vigilante' ); ?> |
| 3661 |
</label> |
| 3662 |
</td> |
| 3663 |
</tr> |
| 3664 |
<tr> |
| 3665 |
<th scope="row"><?php esc_html_e( 'Protect wp-config.php', 'vigilante' ); ?></th> |
| 3666 |
<td> |
| 3667 |
<label> |
| 3668 |
<input type="checkbox" name="firewall[protect_wp_config]" value="1" <?php checked( ! empty( $options['protect_wp_config'] ) ); ?>> |
| 3669 |
<?php esc_html_e( 'Block direct HTTP access to wp-config.php', 'vigilante' ); ?> |
| 3670 |
</label> |
| 3671 |
</td> |
| 3672 |
</tr> |
| 3673 |
<tr id="field-protect-wp-cron"> |
| 3674 |
<th scope="row"><?php esc_html_e( 'Protect wp-cron.php', 'vigilante' ); ?></th> |
| 3675 |
<td> |
| 3676 |
<label> |
| 3677 |
<input type="checkbox" name="firewall[protect_wp_cron]" value="1" <?php checked( ! empty( $options['protect_wp_cron'] ) ); ?>> |
| 3678 |
<?php esc_html_e( 'Block direct HTTP access to wp-cron.php (prevents cron-spam DoS abuse)', 'vigilante' ); ?> |
| 3679 |
</label> |
| 3680 |
<p class="description"><?php |
| 3681 |
printf( |
| 3682 |
/* translators: 1: opening <strong>, 2: closing </strong> */ |
| 3683 |
esc_html__( '%1$sWarning:%2$s Only enable if your host runs a real server-side cron job calling wp-cron.php (most managed WordPress hosts do; check with your provider). Otherwise scheduled tasks — publishing, updates, emails, backups — stop running. Pair with %3$sDISABLE_WP_CRON%4$s in WP Hardening for full coverage.', 'vigilante' ), |
| 3684 |
'<strong>', |
| 3685 |
'</strong>', |
| 3686 |
'<code>', |
| 3687 |
'</code>' |
| 3688 |
); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. |
| 3689 |
?></p> |
| 3690 |
</td> |
| 3691 |
</tr> |
| 3692 |
<tr> |
| 3693 |
<th scope="row"><?php esc_html_e( 'Protect wp-includes', 'vigilante' ); ?></th> |
| 3694 |
<td> |
| 3695 |
<label> |
| 3696 |
<input type="checkbox" name="firewall[protect_wp_includes]" value="1" <?php checked( ! empty( $options['protect_wp_includes'] ) ); ?>> |
| 3697 |
<?php esc_html_e( 'Block direct access to PHP files in wp-includes', 'vigilante' ); ?> |
| 3698 |
</label> |
| 3699 |
</td> |
| 3700 |
</tr> |
| 3701 |
<tr> |
| 3702 |
<th scope="row"><?php esc_html_e( 'PHP in Uploads', 'vigilante' ); ?></th> |
| 3703 |
<td> |
| 3704 |
<label> |
| 3705 |
<input type="checkbox" name="firewall[protect_uploads_php]" value="1" <?php checked( ! empty( $options['protect_uploads_php'] ) ); ?>> |
| 3706 |
<?php esc_html_e( 'Block PHP execution in wp-content/uploads', 'vigilante' ); ?> |
| 3707 |
</label> |
| 3708 |
</td> |
| 3709 |
</tr> |
| 3710 |
<tr> |
| 3711 |
<th scope="row"><?php esc_html_e( 'Sensitive Files', 'vigilante' ); ?></th> |
| 3712 |
<td> |
| 3713 |
<label> |
| 3714 |
<input type="checkbox" name="firewall[protect_sensitive_files]" value="1" <?php checked( ! empty( $options['protect_sensitive_files'] ) ); ?>> |
| 3715 |
<?php esc_html_e( 'Block access to .sql, .bak, .log, .ini, readme.html, license.txt, licencia.txt', 'vigilante' ); ?> |
| 3716 |
</label> |
| 3717 |
</td> |
| 3718 |
</tr> |
| 3719 |
<tr> |
| 3720 |
<th scope="row"><?php esc_html_e( 'Limit HTTP Methods', 'vigilante' ); ?></th> |
| 3721 |
<td> |
| 3722 |
<label> |
| 3723 |
<input type="checkbox" name="firewall[limit_http_methods]" value="1" <?php checked( ! empty( $options['limit_http_methods'] ) ); ?>> |
| 3724 |
<?php esc_html_e( 'Allow only GET, POST, HEAD (blocks PUT, DELETE, TRACE, etc.)', 'vigilante' ); ?> |
| 3725 |
</label> |
| 3726 |
</td> |
| 3727 |
</tr> |
| 3728 |
</table> |
| 3729 |
</div> |
| 3730 |
<?php $options = $vg_local_options; ?> |
| 3731 |
|
| 3732 |
<p class="submit vigilante-submit-buttons"> |
| 3733 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 3734 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 3735 |
</button> |
| 3736 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 3737 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 3738 |
</button> |
| 3739 |
</p> |
| 3740 |
</form> |
| 3741 |
<?php |
| 3742 |
} |
| 3743 |
|
| 3744 |
/** |
| 3745 |
* Render login security tab |
| 3746 |
*/ |
| 3747 |
private function render_tab_login() { |
| 3748 |
$is_disabled = $this->render_module_disabled_notice( 'login_security' ); |
| 3749 |
$options = $this->settings->get_section( 'login_security' ); |
| 3750 |
$lockouts = $this->database->get_active_lockouts(); |
| 3751 |
?> |
| 3752 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="login_security" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 3753 |
<div id="vigilante-section-login-main" class="vigilante-settings-section"> |
| 3754 |
<h2> |
| 3755 |
<?php esc_html_e( 'Login Protection', 'vigilante' ); ?> |
| 3756 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 3757 |
<span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span> |
| 3758 |
</h2> |
| 3759 |
<p><?php esc_html_e( 'Brute force protection and WordPress login hardening.', 'vigilante' ); ?></p> |
| 3760 |
|
| 3761 |
<table class="form-table"> |
| 3762 |
<tr id="field-max-attempts"> |
| 3763 |
<th scope="row"><label for="vigilante-f-login-security-max-attempts"><?php esc_html_e( 'Max Login Attempts', 'vigilante' ); ?></label></th> |
| 3764 |
<td> |
| 3765 |
<input id="vigilante-f-login-security-max-attempts" type="number" name="login_security[max_attempts]" value="<?php echo esc_attr( $options['max_attempts'] ?? 5 ); ?>" min="1" max="20" class="small-text"> |
| 3766 |
<p class="description"><?php esc_html_e( 'Number of failed attempts before lockout.', 'vigilante' ); ?></p> |
| 3767 |
</td> |
| 3768 |
</tr> |
| 3769 |
<tr> |
| 3770 |
<th scope="row"><label for="vigilante-f-login-security-lockout-duration"><?php esc_html_e( 'Lockout Duration', 'vigilante' ); ?></label></th> |
| 3771 |
<td> |
| 3772 |
<input id="vigilante-f-login-security-lockout-duration" type="number" name="login_security[lockout_duration]" value="<?php echo esc_attr( ( $options['lockout_duration'] ?? 1800 ) / 60 ); ?>" min="1" max="1440" class="small-text"> |
| 3773 |
<?php esc_html_e( 'minutes', 'vigilante' ); ?> |
| 3774 |
</td> |
| 3775 |
</tr> |
| 3776 |
<tr> |
| 3777 |
<th scope="row"><?php esc_html_e( 'Progressive Lockout', 'vigilante' ); ?></th> |
| 3778 |
<td> |
| 3779 |
<label> |
| 3780 |
<input type="checkbox" name="login_security[lockout_increment]" value="1" <?php checked( ! empty( $options['lockout_increment'] ) ); ?>> |
| 3781 |
<?php esc_html_e( 'Double lockout duration for repeat offenders', 'vigilante' ); ?> |
| 3782 |
</label> |
| 3783 |
</td> |
| 3784 |
</tr> |
| 3785 |
<tr> |
| 3786 |
<th scope="row"><?php esc_html_e( 'Hide Login Errors', 'vigilante' ); ?></th> |
| 3787 |
<td> |
| 3788 |
<label> |
| 3789 |
<input type="checkbox" name="login_security[hide_login_errors]" value="1" <?php checked( ! empty( $options['hide_login_errors'] ) ); ?>> |
| 3790 |
<?php esc_html_e( 'Show generic error message instead of specific errors', 'vigilante' ); ?> |
| 3791 |
</label> |
| 3792 |
</td> |
| 3793 |
</tr> |
| 3794 |
<tr> |
| 3795 |
<th scope="row"><?php esc_html_e( 'Disable Application Passwords', 'vigilante' ); ?></th> |
| 3796 |
<td> |
| 3797 |
<label> |
| 3798 |
<input type="checkbox" name="login_security[disable_application_passwords]" value="1" <?php checked( ! empty( $options['disable_application_passwords'] ) ); ?>> |
| 3799 |
<?php esc_html_e( 'Disable WordPress application passwords feature', 'vigilante' ); ?> |
| 3800 |
</label> |
| 3801 |
</td> |
| 3802 |
</tr> |
| 3803 |
</table> |
| 3804 |
|
| 3805 |
<h3><?php esc_html_e( 'Custom Login URL', 'vigilante' ); ?></h3> |
| 3806 |
<table class="form-table"> |
| 3807 |
<tr id="field-custom-login-url"> |
| 3808 |
<th scope="row"><?php esc_html_e( 'Login URL Slug', 'vigilante' ); ?></th> |
| 3809 |
<td> |
| 3810 |
<code><?php echo esc_url( home_url( '/' ) ); ?></code> |
| 3811 |
<input type="text" name="login_security[custom_login_url]" id="vigilante_custom_login_url" value="<?php echo esc_attr( $options['custom_login_url'] ?? '' ); ?>" class="regular-text" placeholder="<?php esc_attr_e( 'my-secret-login', 'vigilante' ); ?>"> |
| 3812 |
<p class="description"> |
| 3813 |
<?php esc_html_e( 'ⓘ Leave empty to use default wp-login.php. Use only lowercase letters, numbers and hyphens.', 'vigilante' ); ?> |
| 3814 |
</p> |
| 3815 |
<div class="vigilante-login-url-preview" <?php echo empty( $options['custom_login_url'] ) ? 'style="display:none;"' : ''; ?>> |
| 3816 |
<p class="description"> |
| 3817 |
<strong><?php esc_html_e( 'Your login URL:', 'vigilante' ); ?></strong> |
| 3818 |
<code class="vigilante-login-url-display"><?php echo esc_url( home_url( sanitize_title( $options['custom_login_url'] ?? '' ) . '/' ) ); ?></code> |
| 3819 |
</p> |
| 3820 |
<p class="description"> |
| 3821 |
<?php esc_html_e( 'Direct access to wp-login.php and wp-admin will return a 404 error for non-logged users.', 'vigilante' ); ?> |
| 3822 |
</p> |
| 3823 |
<p class="description"> |
| 3824 |
<?php esc_html_e( 'An IP in the firewall whitelist is still allowed into wp-admin, so remote managers keep working, but it does not get the login form: the hidden URL is the only way in for everyone.', 'vigilante' ); ?> |
| 3825 |
</p> |
| 3826 |
</div> |
| 3827 |
</td> |
| 3828 |
</tr> |
| 3829 |
</table> |
| 3830 |
|
| 3831 |
<div class="vigilante-login-url-notify-wrapper <?php echo empty( $options['custom_login_url'] ) ? 'vigilante-login-url-notify-disabled' : ''; ?>"> |
| 3832 |
<table class="form-table"> |
| 3833 |
<tr> |
| 3834 |
<th scope="row"><?php esc_html_e( 'Notify users', 'vigilante' ); ?></th> |
| 3835 |
<td> |
| 3836 |
<label> |
| 3837 |
<input type="checkbox" name="login_security[notify_on_login_url_change]" id="vigilante_notify_login_url_change" value="1" <?php checked( $options['notify_on_login_url_change'] ?? true ); ?>> |
| 3838 |
<?php esc_html_e( 'Notify affected users when the login URL changes', 'vigilante' ); ?> |
| 3839 |
</label> |
| 3840 |
<div class="vigilante-login-url-send-notification" style="margin-top:12px;"> |
| 3841 |
<button type="button" id="vigilante_notify_login_url" class="button"> |
| 3842 |
<?php esc_html_e( 'Send notification now', 'vigilante' ); ?> |
| 3843 |
</button> |
| 3844 |
<span class="vigilante-login-url-notify-status"></span> |
| 3845 |
<p class="description"><?php esc_html_e( 'Sends the new URL to administrators, editors, authors, and contributors.', 'vigilante' ); ?></p> |
| 3846 |
</div> |
| 3847 |
</td> |
| 3848 |
</tr> |
| 3849 |
</table> |
| 3850 |
</div> |
| 3851 |
|
| 3852 |
<?php $this->render_2fa_settings( $options ); ?> |
| 3853 |
|
| 3854 |
<h3><?php esc_html_e( 'Notifications', 'vigilante' ); ?></h3> |
| 3855 |
<table class="form-table"> |
| 3856 |
<tr> |
| 3857 |
<th scope="row"><?php esc_html_e( 'Notify on Lockout', 'vigilante' ); ?></th> |
| 3858 |
<td> |
| 3859 |
<label> |
| 3860 |
<input type="checkbox" name="login_security[notify_on_lockout]" value="1" <?php checked( ! empty( $options['notify_on_lockout'] ) ); ?>> |
| 3861 |
<?php esc_html_e( 'Send email when an IP is locked out', 'vigilante' ); ?> |
| 3862 |
</label> |
| 3863 |
</td> |
| 3864 |
</tr> |
| 3865 |
<tr> |
| 3866 |
<th scope="row"><?php esc_html_e( 'Notify on Admin Login', 'vigilante' ); ?></th> |
| 3867 |
<td> |
| 3868 |
<label> |
| 3869 |
<input type="checkbox" name="login_security[notify_on_admin_login]" value="1" <?php checked( ! empty( $options['notify_on_admin_login'] ) ); ?>> |
| 3870 |
<?php esc_html_e( 'Send email when an administrator logs in', 'vigilante' ); ?> |
| 3871 |
</label> |
| 3872 |
</td> |
| 3873 |
</tr> |
| 3874 |
</table> |
| 3875 |
|
| 3876 |
<p class="description"> |
| 3877 |
<?php |
| 3878 |
printf( |
| 3879 |
/* translators: %s: Link to notification settings */ |
| 3880 |
esc_html__( 'ⓘ Notifications are sent to the recipients configured in %s.', 'vigilante' ), |
| 3881 |
'<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>' |
| 3882 |
); |
| 3883 |
?> |
| 3884 |
</p> |
| 3885 |
</div> |
| 3886 |
|
| 3887 |
<p class="submit vigilante-submit-buttons"> |
| 3888 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 3889 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 3890 |
</button> |
| 3891 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 3892 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 3893 |
</button> |
| 3894 |
</p> |
| 3895 |
</form> |
| 3896 |
|
| 3897 |
<?php $this->render_lockout_info_section( $options, $lockouts ); ?> |
| 3898 |
<?php |
| 3899 |
} |
| 3900 |
|
| 3901 |
/** |
| 3902 |
* Render lockout information and blocked IPs section |
| 3903 |
* |
| 3904 |
* @param array $options Login security options. |
| 3905 |
* @param array $lockouts Currently locked out IPs. |
| 3906 |
*/ |
| 3907 |
private function render_lockout_info_section( $options, $lockouts ) { |
| 3908 |
$max_attempts = absint( $options['max_attempts'] ?? 5 ); |
| 3909 |
$lockout_duration = absint( $options['lockout_duration'] ?? 1800 ); |
| 3910 |
$lockout_increment = ! empty( $options['lockout_increment'] ); |
| 3911 |
$max_lockout = absint( $options['max_lockout_duration'] ?? 86400 ); |
| 3912 |
$two_factor = $options['two_factor'] ?? array(); |
| 3913 |
$two_factor_enabled = ! empty( $two_factor['enabled'] ); |
| 3914 |
?> |
| 3915 |
<div class="vigilante-settings-section vigilante-lockout-section"> |
| 3916 |
<h2 id="vigilante-section-login-status"><?php esc_html_e( 'Login Protection Status', 'vigilante' ); ?></h2> |
| 3917 |
|
| 3918 |
<table class="form-table"> |
| 3919 |
<tr> |
| 3920 |
<th scope="row"><?php esc_html_e( 'Current settings', 'vigilante' ); ?></th> |
| 3921 |
<td> |
| 3922 |
<?php |
| 3923 |
printf( |
| 3924 |
/* translators: 1: Maximum login attempts, 2: Lockout duration in minutes */ |
| 3925 |
esc_html__( 'After %1$d failed login attempts, the IP address is blocked for %2$d minutes.', 'vigilante' ), |
| 3926 |
absint( $max_attempts ), |
| 3927 |
absint( ceil( $lockout_duration / 60 ) ) |
| 3928 |
); |
| 3929 |
|
| 3930 |
if ( $lockout_increment ) { |
| 3931 |
echo '<br>'; |
| 3932 |
printf( |
| 3933 |
/* translators: %d: Maximum lockout duration in hours */ |
| 3934 |
esc_html__( 'Progressive lockout enabled (max: %d hours).', 'vigilante' ), |
| 3935 |
absint( ceil( $max_lockout / 3600 ) ) |
| 3936 |
); |
| 3937 |
} |
| 3938 |
|
| 3939 |
if ( $two_factor_enabled ) { |
| 3940 |
echo '<br>'; |
| 3941 |
esc_html_e( 'Failed 2FA codes also count toward the lockout limit.', 'vigilante' ); |
| 3942 |
} |
| 3943 |
?> |
| 3944 |
</td> |
| 3945 |
</tr> |
| 3946 |
<tr> |
| 3947 |
<th scope="row"><?php esc_html_e( 'Blocked IPs', 'vigilante' ); ?></th> |
| 3948 |
<td> |
| 3949 |
<?php if ( ! empty( $lockouts ) ) : ?> |
| 3950 |
<div class="vigilante-paginated-section"> |
| 3951 |
<div class="vigilante-fi-pagination-wrap"></div> |
| 3952 |
<table class="wp-list-table widefat fixed striped vigilante-fi-paginated"> |
| 3953 |
<thead> |
| 3954 |
<tr> |
| 3955 |
<th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th> |
| 3956 |
<th><?php esc_html_e( 'Attempts', 'vigilante' ); ?></th> |
| 3957 |
<th><?php esc_html_e( 'Blocked Until', 'vigilante' ); ?></th> |
| 3958 |
<th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 3959 |
</tr> |
| 3960 |
</thead> |
| 3961 |
<tbody> |
| 3962 |
<?php foreach ( $lockouts as $lockout ) : |
| 3963 |
$lockout_time = strtotime( $lockout->locked_until ); |
| 3964 |
$remaining_seconds = $lockout_time - time(); |
| 3965 |
$remaining_text = $this->format_remaining_time( $remaining_seconds ); |
| 3966 |
?> |
| 3967 |
<tr> |
| 3968 |
<td><code><?php echo esc_html( $lockout->ip_address ); ?></code></td> |
| 3969 |
<td><?php echo esc_html( $lockout->attempts ); ?></td> |
| 3970 |
<td> |
| 3971 |
<?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lockout_time ) ); ?> |
| 3972 |
<br> |
| 3973 |
<small class="description"> |
| 3974 |
<?php |
| 3975 |
printf( |
| 3976 |
/* translators: %s: Remaining time */ |
| 3977 |
esc_html__( '%s remaining', 'vigilante' ), |
| 3978 |
esc_html( $remaining_text ) |
| 3979 |
); |
| 3980 |
?> |
| 3981 |
</small> |
| 3982 |
</td> |
| 3983 |
<td> |
| 3984 |
<button type="button" class="button button-small vigilante-clear-lockout" data-ip="<?php echo esc_attr( $lockout->ip_address ); ?>"> |
| 3985 |
<?php esc_html_e( 'Unblock', 'vigilante' ); ?> |
| 3986 |
</button> |
| 3987 |
</td> |
| 3988 |
</tr> |
| 3989 |
<?php endforeach; ?> |
| 3990 |
</tbody> |
| 3991 |
</table> |
| 3992 |
</div> |
| 3993 |
<p class="description" style="margin-top: 10px;"> |
| 3994 |
<button type="button" class="button vigilante-clear-all-lockouts"> |
| 3995 |
<?php esc_html_e( 'Unblock All IPs', 'vigilante' ); ?> |
| 3996 |
</button> |
| 3997 |
<span style="margin-left: 10px;"> |
| 3998 |
<?php |
| 3999 |
printf( |
| 4000 |
/* translators: %d: Number of blocked IPs */ |
| 4001 |
esc_html( _n( '%d IP currently blocked', '%d IPs currently blocked', count( $lockouts ), 'vigilante' ) ), |
| 4002 |
count( $lockouts ) |
| 4003 |
); |
| 4004 |
?> |
| 4005 |
</span> |
| 4006 |
</p> |
| 4007 |
<?php else : ?> |
| 4008 |
<span class="dashicons dashicons-yes-alt" style="color: #00a32a; font-size: 20px; width: 20px; height: 20px; vertical-align: middle;"></span> |
| 4009 |
<span style="vertical-align: middle; margin-left: 5px;"><?php esc_html_e( 'No IPs are currently blocked. All clear!', 'vigilante' ); ?></span> |
| 4010 |
<?php endif; ?> |
| 4011 |
</td> |
| 4012 |
</tr> |
| 4013 |
</table> |
| 4014 |
</div> |
| 4015 |
<?php |
| 4016 |
} |
| 4017 |
|
| 4018 |
/** |
| 4019 |
* Format remaining lockout time in human readable format |
| 4020 |
* |
| 4021 |
* @param int $seconds Remaining seconds. |
| 4022 |
* @return string Formatted time string. |
| 4023 |
*/ |
| 4024 |
private function format_remaining_time( $seconds ) { |
| 4025 |
if ( $seconds <= 0 ) { |
| 4026 |
return __( 'expired', 'vigilante' ); |
| 4027 |
} |
| 4028 |
|
| 4029 |
if ( $seconds < 60 ) { |
| 4030 |
return sprintf( |
| 4031 |
/* translators: %d: Number of seconds */ |
| 4032 |
_n( '%d second', '%d seconds', $seconds, 'vigilante' ), |
| 4033 |
$seconds |
| 4034 |
); |
| 4035 |
} |
| 4036 |
|
| 4037 |
if ( $seconds < 3600 ) { |
| 4038 |
$minutes = ceil( $seconds / 60 ); |
| 4039 |
return sprintf( |
| 4040 |
/* translators: %d: Number of minutes */ |
| 4041 |
_n( '%d minute', '%d minutes', $minutes, 'vigilante' ), |
| 4042 |
$minutes |
| 4043 |
); |
| 4044 |
} |
| 4045 |
|
| 4046 |
$hours = floor( $seconds / 3600 ); |
| 4047 |
$remaining_minutes = ceil( ( $seconds % 3600 ) / 60 ); |
| 4048 |
|
| 4049 |
if ( $remaining_minutes > 0 ) { |
| 4050 |
return sprintf( |
| 4051 |
/* translators: 1: Number of hours, 2: Number of minutes */ |
| 4052 |
__( '%1$d hours %2$d minutes', 'vigilante' ), |
| 4053 |
$hours, |
| 4054 |
$remaining_minutes |
| 4055 |
); |
| 4056 |
} |
| 4057 |
|
| 4058 |
return sprintf( |
| 4059 |
/* translators: %d: Number of hours */ |
| 4060 |
_n( '%d hour', '%d hours', $hours, 'vigilante' ), |
| 4061 |
$hours |
| 4062 |
); |
| 4063 |
} |
| 4064 |
|
| 4065 |
/** |
| 4066 |
* Render 2FA settings section |
| 4067 |
* |
| 4068 |
* @param array $options Login security options. |
| 4069 |
*/ |
| 4070 |
private function render_2fa_settings( $options ) { |
| 4071 |
$two_factor = $options['two_factor'] ?? array(); |
| 4072 |
$all_roles = wp_roles()->roles; |
| 4073 |
$enforced = $two_factor['enforced_roles'] ?? array( 'administrator', 'editor' ); |
| 4074 |
$excluded = $two_factor['excluded_users'] ?? array(); |
| 4075 |
$method = $two_factor['method'] ?? 'email'; |
| 4076 |
$grace_days = $two_factor['grace_period_days'] ?? 3; |
| 4077 |
?> |
| 4078 |
<h3 id="vigilante-section-login-2fa"> |
| 4079 |
<?php esc_html_e( 'Two-Factor Authentication (2FA)', 'vigilante' ); ?> |
| 4080 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 4081 |
<span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span> |
| 4082 |
</h3> |
| 4083 |
<p class="description"><?php esc_html_e( 'Require a second verification step after password. Choose between email codes or an authenticator app (TOTP).', 'vigilante' ); ?></p> |
| 4084 |
|
| 4085 |
<table class="form-table"> |
| 4086 |
<tr> |
| 4087 |
<th scope="row"><?php esc_html_e( 'Enable 2FA', 'vigilante' ); ?></th> |
| 4088 |
<td> |
| 4089 |
<label> |
| 4090 |
<input type="checkbox" name="login_security[two_factor][enabled]" id="vigilante_2fa_enabled" value="1" <?php checked( ! empty( $two_factor['enabled'] ) ); ?>> |
| 4091 |
<?php esc_html_e( 'Enable two-factor authentication', 'vigilante' ); ?> |
| 4092 |
</label> |
| 4093 |
</td> |
| 4094 |
</tr> |
| 4095 |
</table> |
| 4096 |
|
| 4097 |
<div class="vigilante-2fa-settings-wrapper <?php echo empty( $two_factor['enabled'] ) ? 'vigilante-2fa-settings-disabled' : ''; ?>"> |
| 4098 |
<table class="form-table"> |
| 4099 |
<tr> |
| 4100 |
<th scope="row"><?php esc_html_e( 'Verification method', 'vigilante' ); ?></th> |
| 4101 |
<td> |
| 4102 |
<fieldset class="vigilante-2fa-method-selector"> |
| 4103 |
<label class="vigilante-2fa-method-option <?php echo 'email' === $method ? 'selected' : ''; ?>"> |
| 4104 |
<input type="radio" name="login_security[two_factor][method]" value="email" <?php checked( $method, 'email' ); ?>> |
| 4105 |
<span class="dashicons dashicons-email"></span> |
| 4106 |
<span class="method-info"> |
| 4107 |
<strong><?php esc_html_e( 'Email code', 'vigilante' ); ?></strong> |
| 4108 |
<span><?php esc_html_e( 'Users receive a 6-digit code via email after entering their password.', 'vigilante' ); ?></span> |
| 4109 |
</span> |
| 4110 |
</label> |
| 4111 |
<label class="vigilante-2fa-method-option <?php echo 'totp' === $method ? 'selected' : ''; ?>"> |
| 4112 |
<input type="radio" name="login_security[two_factor][method]" value="totp" <?php checked( $method, 'totp' ); ?>> |
| 4113 |
<span class="dashicons dashicons-smartphone"></span> |
| 4114 |
<span class="method-info"> |
| 4115 |
<strong><?php esc_html_e( 'Authenticator app (TOTP)', 'vigilante' ); ?></strong> |
| 4116 |
<span><?php esc_html_e( 'Users verify with a time-based code from Google Authenticator, Authy, etc.', 'vigilante' ); ?></span> |
| 4117 |
</span> |
| 4118 |
</label> |
| 4119 |
</fieldset> |
| 4120 |
</td> |
| 4121 |
</tr> |
| 4122 |
<tr> |
| 4123 |
<th scope="row"><?php esc_html_e( 'Enforce for roles', 'vigilante' ); ?></th> |
| 4124 |
<td> |
| 4125 |
<div class="vigilante-2fa-roles"> |
| 4126 |
<?php foreach ( $all_roles as $role_slug => $role_data ) : |
| 4127 |
$user_count = count( get_users( array( 'role' => $role_slug, 'fields' => 'ID' ) ) ); |
| 4128 |
?> |
| 4129 |
<label> |
| 4130 |
<input type="checkbox" |
| 4131 |
name="login_security[two_factor][enforced_roles][]" |
| 4132 |
value="<?php echo esc_attr( $role_slug ); ?>" |
| 4133 |
<?php checked( in_array( $role_slug, $enforced, true ) ); ?>> |
| 4134 |
<span class="role-name"><?php echo esc_html( translate_user_role( $role_data['name'] ) ); ?></span> |
| 4135 |
<span class="role-count">(<?php echo esc_html( $user_count ); ?>)</span> |
| 4136 |
</label> |
| 4137 |
<?php endforeach; ?> |
| 4138 |
</div> |
| 4139 |
<p class="description"><?php esc_html_e( 'Users with these roles will be required to verify with the selected method.', 'vigilante' ); ?></p> |
| 4140 |
</td> |
| 4141 |
</tr> |
| 4142 |
<tr> |
| 4143 |
<th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th> |
| 4144 |
<td> |
| 4145 |
<div class="vigilante-2fa-user-search-container"> |
| 4146 |
<div class="vigilante-2fa-user-search"> |
| 4147 |
<span class="search-icon"></span> |
| 4148 |
<input type="text" |
| 4149 |
id="vigilante_2fa_user_search" |
| 4150 |
placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>" |
| 4151 |
autocomplete="off"> |
| 4152 |
<div class="vigilante-2fa-search-results"></div> |
| 4153 |
</div> |
| 4154 |
<div class="vigilante-2fa-excluded-users"> |
| 4155 |
<?php |
| 4156 |
foreach ( $excluded as $user_id ) : |
| 4157 |
$user = get_user_by( 'ID', $user_id ); |
| 4158 |
if ( ! $user ) continue; |
| 4159 |
?> |
| 4160 |
<div class="vigilante-2fa-excluded-user" data-user-id="<?php echo esc_attr( $user_id ); ?>"> |
| 4161 |
<span class="user-display"><?php echo esc_html( $user->display_name . ' (' . $user->user_email . ')' ); ?></span> |
| 4162 |
<button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">×</button> |
| 4163 |
<input type="hidden" name="login_security[two_factor][excluded_users][]" value="<?php echo esc_attr( $user_id ); ?>"> |
| 4164 |
</div> |
| 4165 |
<?php endforeach; ?> |
| 4166 |
</div> |
| 4167 |
</div> |
| 4168 |
<p class="description"><?php esc_html_e( 'These users will not be required to use 2FA regardless of their role.', 'vigilante' ); ?></p> |
| 4169 |
</td> |
| 4170 |
</tr> |
| 4171 |
|
| 4172 |
<!-- Remember device option --> |
| 4173 |
<tr> |
| 4174 |
<th scope="row"><?php esc_html_e( 'Remember device', 'vigilante' ); ?></th> |
| 4175 |
<td> |
| 4176 |
<label> |
| 4177 |
<input type="checkbox" name="login_security[two_factor][allow_remember_device]" value="1" <?php checked( ! empty( $two_factor['allow_remember_device'] ) ); ?>> |
| 4178 |
<?php esc_html_e( 'Allow users to skip 2FA verification on trusted devices', 'vigilante' ); ?> |
| 4179 |
</label> |
| 4180 |
<p class="description"> |
| 4181 |
<?php |
| 4182 |
printf( |
| 4183 |
/* translators: %d: Number of days */ |
| 4184 |
esc_html__( 'When enabled, users can check "Remember this device" on the verification screen to skip 2FA for %d days.', 'vigilante' ), |
| 4185 |
absint( $two_factor['remember_device_days'] ?? 30 ) |
| 4186 |
); |
| 4187 |
?> |
| 4188 |
</p> |
| 4189 |
</td> |
| 4190 |
</tr> |
| 4191 |
|
| 4192 |
<!-- TOTP-specific: Grace period --> |
| 4193 |
<tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>> |
| 4194 |
<th scope="row"><label for="vigilante-f-login-security-two-factor-grace-period-days"><?php esc_html_e( 'Grace period', 'vigilante' ); ?></label></th> |
| 4195 |
<td> |
| 4196 |
<input id="vigilante-f-login-security-two-factor-grace-period-days" type="number" |
| 4197 |
name="login_security[two_factor][grace_period_days]" |
| 4198 |
value="<?php echo esc_attr( $grace_days ); ?>" |
| 4199 |
min="0" max="30" class="small-text"> |
| 4200 |
<?php esc_html_e( 'days', 'vigilante' ); ?> |
| 4201 |
<p class="description"><?php esc_html_e( 'Days users have to set up their authenticator app. During this period they can log in without TOTP. Set to 0 for immediate enforcement.', 'vigilante' ); ?></p> |
| 4202 |
</td> |
| 4203 |
</tr> |
| 4204 |
|
| 4205 |
<!-- Email-specific: Sender name --> |
| 4206 |
<tr class="vigilante-2fa-email-only" <?php echo 'email' !== $method ? 'style="display:none;"' : ''; ?>> |
| 4207 |
<th scope="row"><label for="vigilante-f-login-security-two-factor-email-from-name"><?php esc_html_e( 'Email sender name', 'vigilante' ); ?></label></th> |
| 4208 |
<td> |
| 4209 |
<input id="vigilante-f-login-security-two-factor-email-from-name" type="text" |
| 4210 |
name="login_security[two_factor][email_from_name]" |
| 4211 |
value="<?php echo esc_attr( $two_factor['email_from_name'] ?? '' ); ?>" |
| 4212 |
class="regular-text vigilante-2fa-email-from" |
| 4213 |
placeholder="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>"> |
| 4214 |
<p class="description"><?php esc_html_e( 'Name shown in verification emails. Leave empty to use site name.', 'vigilante' ); ?></p> |
| 4215 |
</td> |
| 4216 |
</tr> |
| 4217 |
|
| 4218 |
<!-- TOTP-specific: Reset users --> |
| 4219 |
<tr class="vigilante-2fa-totp-only" <?php echo 'totp' !== $method ? 'style="display:none;"' : ''; ?>> |
| 4220 |
<th scope="row"><?php esc_html_e( 'Reset user TOTP', 'vigilante' ); ?></th> |
| 4221 |
<td> |
| 4222 |
<div class="vigilante-totp-reset-container"> |
| 4223 |
<div class="vigilante-2fa-user-search"> |
| 4224 |
<span class="search-icon"></span> |
| 4225 |
<input type="text" |
| 4226 |
id="vigilante_totp_reset_search" |
| 4227 |
placeholder="<?php esc_attr_e( 'Search users with TOTP configured...', 'vigilante' ); ?>" |
| 4228 |
autocomplete="off"> |
| 4229 |
<div class="vigilante-totp-reset-results"></div> |
| 4230 |
</div> |
| 4231 |
<div class="vigilante-totp-reset-selected"></div> |
| 4232 |
<button type="button" id="vigilante_totp_reset_btn" class="button" style="display:none;"> |
| 4233 |
<?php esc_html_e( 'Reset selected', 'vigilante' ); ?> |
| 4234 |
</button> |
| 4235 |
<span class="vigilante-totp-reset-status"></span> |
| 4236 |
</div> |
| 4237 |
<p class="description"><?php esc_html_e( 'Reset TOTP for users who lost access to their authenticator app. They will need to set up again.', 'vigilante' ); ?></p> |
| 4238 |
</td> |
| 4239 |
</tr> |
| 4240 |
</table> |
| 4241 |
|
| 4242 |
<h3><?php esc_html_e( 'User notification', 'vigilante' ); ?></h3> |
| 4243 |
<table class="form-table"> |
| 4244 |
<tr> |
| 4245 |
<th scope="row"><?php esc_html_e( 'Notify on enable', 'vigilante' ); ?></th> |
| 4246 |
<td> |
| 4247 |
<label> |
| 4248 |
<input type="checkbox" name="login_security[two_factor][notify_on_enable]" value="1" <?php checked( $two_factor['notify_on_enable'] ?? true ); ?>> |
| 4249 |
<?php esc_html_e( 'Send notification email to affected users when 2FA is enabled', 'vigilante' ); ?> |
| 4250 |
</label> |
| 4251 |
|
| 4252 |
<div class="vigilante-2fa-notification-options"> |
| 4253 |
<label> |
| 4254 |
<input type="radio" name="vigilante_2fa_notify_mode" value="all" checked> |
| 4255 |
<?php esc_html_e( 'Send to all affected users', 'vigilante' ); ?> |
| 4256 |
</label> |
| 4257 |
<label> |
| 4258 |
<input type="radio" name="vigilante_2fa_notify_mode" value="new"> |
| 4259 |
<?php esc_html_e( 'Send only to users not previously notified', 'vigilante' ); ?> |
| 4260 |
</label> |
| 4261 |
</div> |
| 4262 |
|
| 4263 |
<div class="vigilante-2fa-send-notification"> |
| 4264 |
<button type="button" id="vigilante_2fa_send_notification" class="button"> |
| 4265 |
<?php esc_html_e( 'Send notification now', 'vigilante' ); ?> |
| 4266 |
</button> |
| 4267 |
<span class="vigilante-2fa-notification-status"></span> |
| 4268 |
</div> |
| 4269 |
</td> |
| 4270 |
</tr> |
| 4271 |
</table> |
| 4272 |
</div> |
| 4273 |
<?php |
| 4274 |
} |
| 4275 |
|
| 4276 |
/** |
| 4277 |
* Render security headers tab |
| 4278 |
*/ |
| 4279 |
/** |
| 4280 |
* Offer back the header settings the 2.9.8 migration wiped. |
| 4281 |
* |
| 4282 |
* Rendered outside the settings form on purpose, so its buttons can never |
| 4283 |
* submit it, and only when there is something to actually change. Shows the |
| 4284 |
* difference before anything is written: nothing is applied that the owner |
| 4285 |
* has not seen first. |
| 4286 |
* |
| 4287 |
* @since 2.10.0 |
| 4288 |
*/ |
| 4289 |
private function render_headers_recovery_offer() { |
| 4290 |
/* |
| 4291 |
* On a network the .htaccess belongs to every site and only the main one |
| 4292 |
* writes it, so this is not a decision a subsite gets to make. Its own |
| 4293 |
* security_headers options are inert anyway: what the network serves |
| 4294 |
* comes from the file the main site owns. Without this gate a subsite |
| 4295 |
* administrator was shown a Restore button that could only ever answer |
| 4296 |
* with a permission error, which is worse than showing nothing. |
| 4297 |
*/ |
| 4298 |
if ( ! Vigilante_Settings::can_write_shared_files() ) { |
| 4299 |
return; |
| 4300 |
} |
| 4301 |
|
| 4302 |
if ( ! Vigilante_Htaccess_Recovery::is_available() ) { |
| 4303 |
/* |
| 4304 |
* Already restored. Offer to take it back for as long as the previous |
| 4305 |
* section is still stored: a restore that cannot be undone is a second |
| 4306 |
* irreversible change on top of the one being repaired. |
| 4307 |
*/ |
| 4308 |
if ( Vigilante_Htaccess_Recovery::has_undo() ) { |
| 4309 |
?> |
| 4310 |
<div class="notice notice-info inline" id="vigilante-headers-recovery-undo"> |
| 4311 |
<p> |
| 4312 |
<?php esc_html_e( 'The Security Headers settings were restored from the copy Vigilant had kept of your .htaccess.', 'vigilante' ); ?> |
| 4313 |
<button type="button" class="button button-small" id="vigilante-recovery-undo"> |
| 4314 |
<?php esc_html_e( 'Undo the restore', 'vigilante' ); ?> |
| 4315 |
</button> |
| 4316 |
</p> |
| 4317 |
</div> |
| 4318 |
<?php |
| 4319 |
} |
| 4320 |
|
| 4321 |
return; |
| 4322 |
} |
| 4323 |
|
| 4324 |
$rows = Vigilante_Htaccess_Recovery::get_diff( $this->settings ); |
| 4325 |
|
| 4326 |
if ( empty( $rows ) ) { |
| 4327 |
return; |
| 4328 |
} |
| 4329 |
|
| 4330 |
$snapshot = Vigilante_Htaccess_Recovery::get_snapshot(); |
| 4331 |
$taken = isset( $snapshot['time'] ) ? (int) $snapshot['time'] : 0; |
| 4332 |
$block = Vigilante_Htaccess_Recovery::get_raw_block(); |
| 4333 |
?> |
| 4334 |
<div class="vigilante-settings-section" id="vigilante-headers-recovery"> |
| 4335 |
<h2><?php esc_html_e( 'Recover your previous header settings', 'vigilante' ); ?></h2> |
| 4336 |
<p> |
| 4337 |
<?php esc_html_e( 'An earlier update reset this tab to factory values: the migration replaced the whole section instead of merging into it. Your server kept sending the right headers, because the .htaccess had not been rewritten yet, so Vigilant saved a copy of that file before touching it. These are the settings it found in that copy.', 'vigilante' ); ?> |
| 4338 |
</p> |
| 4339 |
<?php if ( $taken ) : ?> |
| 4340 |
<p class="description"> |
| 4341 |
<?php |
| 4342 |
printf( |
| 4343 |
/* translators: %s: date and time the .htaccess copy was taken. */ |
| 4344 |
esc_html__( 'Copy taken on %s.', 'vigilante' ), |
| 4345 |
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $taken ) ) |
| 4346 |
); |
| 4347 |
?> |
| 4348 |
</p> |
| 4349 |
<?php endif; ?> |
| 4350 |
|
| 4351 |
<table class="widefat striped"> |
| 4352 |
<thead> |
| 4353 |
<tr> |
| 4354 |
<th scope="col"><?php esc_html_e( 'Setting', 'vigilante' ); ?></th> |
| 4355 |
<th scope="col"><?php esc_html_e( 'Now', 'vigilante' ); ?></th> |
| 4356 |
<th scope="col"><?php esc_html_e( 'Would be restored to', 'vigilante' ); ?></th> |
| 4357 |
</tr> |
| 4358 |
</thead> |
| 4359 |
<tbody> |
| 4360 |
<?php foreach ( $rows as $row ) : ?> |
| 4361 |
<tr> |
| 4362 |
<th scope="row"><?php echo esc_html( $row['label'] ); ?></th> |
| 4363 |
<td><?php echo esc_html( $row['current'] ); ?></td> |
| 4364 |
<td> |
| 4365 |
<?php echo esc_html( $row['recovered'] ); ?> |
| 4366 |
<?php if ( ! empty( $row['detail'] ) ) : ?> |
| 4367 |
<br><span class="description"><?php echo esc_html( $row['detail'] ); ?></span> |
| 4368 |
<?php endif; ?> |
| 4369 |
</td> |
| 4370 |
</tr> |
| 4371 |
<?php endforeach; ?> |
| 4372 |
</tbody> |
| 4373 |
</table> |
| 4374 |
|
| 4375 |
<p class="description"> |
| 4376 |
<?php esc_html_e( 'Only these settings are written. The .htaccess is then rebuilt from them, the same way saving this tab rebuilds it. The stored copy of the file is never written back, so nothing your host, your cache plugin or your CDN added to it is touched.', 'vigilante' ); ?> |
| 4377 |
</p> |
| 4378 |
|
| 4379 |
<?php if ( '' !== $block ) : ?> |
| 4380 |
<details> |
| 4381 |
<summary><?php esc_html_e( 'Show the saved .htaccess block', 'vigilante' ); ?></summary> |
| 4382 |
<textarea readonly rows="12" class="large-text code" onclick="this.select();"><?php echo esc_textarea( $block ); ?></textarea> |
| 4383 |
</details> |
| 4384 |
<?php endif; ?> |
| 4385 |
|
| 4386 |
<p class="submit vigilante-submit-buttons"> |
| 4387 |
<button type="button" class="button button-primary" id="vigilante-recovery-restore"> |
| 4388 |
<?php esc_html_e( 'Restore these settings', 'vigilante' ); ?> |
| 4389 |
</button> |
| 4390 |
<button type="button" class="button" id="vigilante-recovery-dismiss"> |
| 4391 |
<?php esc_html_e( 'No thanks, keep what I have', 'vigilante' ); ?> |
| 4392 |
</button> |
| 4393 |
</p> |
| 4394 |
<div id="vigilante-recovery-result"></div> |
| 4395 |
</div> |
| 4396 |
<?php |
| 4397 |
} |
| 4398 |
|
| 4399 |
private function render_tab_headers() { |
| 4400 |
$is_disabled = $this->render_module_disabled_notice( 'security_headers' ); |
| 4401 |
// Every setting on this tab ends up in .htaccess, so on a subsite the |
| 4402 |
// whole tab is somebody else's, values included. |
| 4403 |
$vg_shared_locked = $this->shared_files_locked(); |
| 4404 |
$options = $this->get_section_for_display( 'security_headers' ); |
| 4405 |
?> |
| 4406 |
<?php $this->render_headers_recovery_offer(); ?> |
| 4407 |
|
| 4408 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="security_headers" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 4409 |
<?php $this->render_shared_files_notice(); ?> |
| 4410 |
<div id="vigilante-section-headers-main" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>> |
| 4411 |
<h2> |
| 4412 |
<?php esc_html_e( 'Security Headers', 'vigilante' ); ?> |
| 4413 |
<span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span> |
| 4414 |
</h2> |
| 4415 |
<p><?php esc_html_e( 'HTTP headers sent with every response via .htaccess (mod_headers).', 'vigilante' ); ?></p> |
| 4416 |
|
| 4417 |
<table class="form-table"> |
| 4418 |
<tr> |
| 4419 |
<th scope="row"><label for="vigilante-f-security-headers-x-frame-options"><?php esc_html_e( 'X-Frame-Options', 'vigilante' ); ?></label></th> |
| 4420 |
<td> |
| 4421 |
<select id="vigilante-f-security-headers-x-frame-options" name="security_headers[x_frame_options]"> |
| 4422 |
<option value="" <?php selected( empty( $options['x_frame_options'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option> |
| 4423 |
<option value="SAMEORIGIN" <?php selected( $options['x_frame_options'] ?? '', 'SAMEORIGIN' ); ?>>SAMEORIGIN</option> |
| 4424 |
<option value="DENY" <?php selected( $options['x_frame_options'] ?? '', 'DENY' ); ?>>DENY</option> |
| 4425 |
</select> |
| 4426 |
<p class="description"><?php esc_html_e( 'ⓘ Prevents clickjacking attacks. Also sets CSP frame-ancestors automatically.', 'vigilante' ); ?></p> |
| 4427 |
</td> |
| 4428 |
</tr> |
| 4429 |
<tr> |
| 4430 |
<th scope="row"><?php esc_html_e( 'X-Content-Type-Options', 'vigilante' ); ?></th> |
| 4431 |
<td> |
| 4432 |
<label> |
| 4433 |
<input type="checkbox" name="security_headers[x_content_type_options]" value="1" <?php checked( ! empty( $options['x_content_type_options'] ) ); ?>> |
| 4434 |
<?php esc_html_e( 'Add nosniff header to prevent MIME type sniffing', 'vigilante' ); ?> |
| 4435 |
</label> |
| 4436 |
</td> |
| 4437 |
</tr> |
| 4438 |
<tr> |
| 4439 |
<th scope="row"><label for="vigilante-f-security-headers-referrer-policy"><?php esc_html_e( 'Referrer-Policy', 'vigilante' ); ?></label></th> |
| 4440 |
<td> |
| 4441 |
<select id="vigilante-f-security-headers-referrer-policy" name="security_headers[referrer_policy]"> |
| 4442 |
<option value="" <?php selected( empty( $options['referrer_policy'] ) ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option> |
| 4443 |
<option value="no-referrer" <?php selected( $options['referrer_policy'] ?? '', 'no-referrer' ); ?>>no-referrer</option> |
| 4444 |
<option value="strict-origin-when-cross-origin" <?php selected( $options['referrer_policy'] ?? '', 'strict-origin-when-cross-origin' ); ?>>strict-origin-when-cross-origin</option> |
| 4445 |
<option value="same-origin" <?php selected( $options['referrer_policy'] ?? '', 'same-origin' ); ?>>same-origin</option> |
| 4446 |
</select> |
| 4447 |
</td> |
| 4448 |
</tr> |
| 4449 |
</table> |
| 4450 |
|
| 4451 |
<h3 id="vigilante-section-headers-csp"><?php esc_html_e( 'Content Security Policy', 'vigilante' ); ?></h3> |
| 4452 |
<table class="form-table"> |
| 4453 |
<tr> |
| 4454 |
<th scope="row"><?php esc_html_e( 'Enable CSP', 'vigilante' ); ?></th> |
| 4455 |
<td> |
| 4456 |
<label> |
| 4457 |
<input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>> |
| 4458 |
<?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?> |
| 4459 |
</label> |
| 4460 |
</td> |
| 4461 |
</tr> |
| 4462 |
<tr> |
| 4463 |
<th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th> |
| 4464 |
<td> |
| 4465 |
<label> |
| 4466 |
<input type="checkbox" name="security_headers[csp][report_only]" value="1" <?php checked( ! empty( $options['csp']['report_only'] ) ); ?>> |
| 4467 |
<?php esc_html_e( 'Report violations without blocking (for testing)', 'vigilante' ); ?> |
| 4468 |
</label> |
| 4469 |
</td> |
| 4470 |
</tr> |
| 4471 |
</table> |
| 4472 |
|
| 4473 |
<h3 id="vigilante-section-headers-force-https"><?php esc_html_e( 'HTTPS', 'vigilante' ); ?></h3> |
| 4474 |
<p class="description"><?php esc_html_e( 'HTTPS is strongly recommended, but Vigilant will not impose it. Enable only what your site already supports.', 'vigilante' ); ?></p> |
| 4475 |
<table class="form-table"> |
| 4476 |
<tr> |
| 4477 |
<th scope="row"><?php esc_html_e( 'Redirect HTTP to HTTPS', 'vigilante' ); ?></th> |
| 4478 |
<td> |
| 4479 |
<label> |
| 4480 |
<input type="checkbox" name="security_headers[redirect_http_to_https]" value="1" <?php checked( ! empty( $options['redirect_http_to_https'] ) ); ?>> |
| 4481 |
<?php esc_html_e( 'Send visitors arriving over HTTP to the HTTPS address', 'vigilante' ); ?> |
| 4482 |
</label> |
| 4483 |
<p class="description"><?php esc_html_e( 'Only applies when the site address is already an https:// one. On a site still published over HTTP it does nothing, so it cannot leave the site unreachable.', 'vigilante' ); ?></p> |
| 4484 |
</td> |
| 4485 |
</tr> |
| 4486 |
<tr> |
| 4487 |
<th scope="row"><?php esc_html_e( 'Fix Mixed Content', 'vigilante' ); ?></th> |
| 4488 |
<td> |
| 4489 |
<label> |
| 4490 |
<input type="checkbox" name="security_headers[fix_mixed_content]" value="1" <?php checked( ! empty( $options['fix_mixed_content'] ) ); ?>> |
| 4491 |
<?php esc_html_e( 'Rewrite this site http:// resources to https://', 'vigilante' ); ?> |
| 4492 |
</label> |
| 4493 |
<p class="description"><?php esc_html_e( 'Off by default. Only touches addresses of this same site, and only when the site is already served over HTTPS, so it cannot break an external resource. Useful right after moving a site to HTTPS, when old content still points at http:// addresses.', 'vigilante' ); ?></p> |
| 4494 |
</td> |
| 4495 |
</tr> |
| 4496 |
<tr id="field-upgrade-insecure-requests"> |
| 4497 |
<th scope="row"><?php esc_html_e( 'Upgrade Insecure Requests', 'vigilante' ); ?></th> |
| 4498 |
<td> |
| 4499 |
<label> |
| 4500 |
<input type="checkbox" name="security_headers[upgrade_insecure_requests]" value="1" <?php checked( ! empty( $options['upgrade_insecure_requests'] ) ); ?>> |
| 4501 |
<?php esc_html_e( 'Ask browsers to upgrade every http:// request to https://', 'vigilante' ); ?> |
| 4502 |
</label> |
| 4503 |
<p class="description"><?php esc_html_e( '⚠ Off by default. This one also covers resources hosted elsewhere: anything served from a domain with no HTTPS stops loading instead of loading insecurely. Turn it on once you know every external resource the site uses is available over HTTPS.', 'vigilante' ); ?></p> |
| 4504 |
</td> |
| 4505 |
</tr> |
| 4506 |
<tr> |
| 4507 |
<th scope="row"><?php esc_html_e( 'Rewrite Site Address on Activation', 'vigilante' ); ?></th> |
| 4508 |
<td> |
| 4509 |
<label> |
| 4510 |
<input type="checkbox" name="security_headers[force_https]" value="1" <?php checked( ! empty( $options['force_https'] ) ); ?>> |
| 4511 |
<?php esc_html_e( 'Change the WordPress and site addresses to https:// when the plugin is activated', 'vigilante' ); ?> |
| 4512 |
</label> |
| 4513 |
<p class="description"><?php esc_html_e( '⚠ Off by default. This writes to the WordPress Address and Site Address settings, and turning the plugin off later does not undo it. It only runs on activation, and only when the site answers over HTTPS.', 'vigilante' ); ?></p> |
| 4514 |
</td> |
| 4515 |
</tr> |
| 4516 |
</table> |
| 4517 |
|
| 4518 |
<h3 id="vigilante-section-headers-hsts"><?php esc_html_e( 'HSTS (HTTP Strict Transport Security)', 'vigilante' ); ?></h3> |
| 4519 |
<?php $vig_home_https = ( 0 === strpos( (string) get_option( 'home' ), 'https://' ) ); ?> |
| 4520 |
<p class="description"><?php esc_html_e( 'Tells browsers to reach this site over HTTPS and never over HTTP, for as long as the max age below.', 'vigilante' ); ?></p> |
| 4521 |
<?php if ( ! $vig_home_https ) : ?> |
| 4522 |
<p class="description" style="color:#b32d2e"><strong><?php esc_html_e( 'Unavailable: the site address still starts with http://. Enabling HSTS on a site not published over HTTPS would make it unreachable in any browser that honours it.', 'vigilante' ); ?></strong></p> |
| 4523 |
<?php endif; ?> |
| 4524 |
<table class="form-table"> |
| 4525 |
<tr> |
| 4526 |
<th scope="row"><?php esc_html_e( 'Enable HSTS', 'vigilante' ); ?></th> |
| 4527 |
<td> |
| 4528 |
<?php if ( ! $vig_home_https ) : ?> |
| 4529 |
<?php /* A disabled checkbox is not submitted, and a boolean missing from the post is treated as unticked, so saving the tab would silently switch HSTS off. Carry the stored value instead. */ ?> |
| 4530 |
<input type="hidden" name="security_headers[hsts][enabled]" value="<?php echo ! empty( $options['hsts']['enabled'] ) ? '1' : '0'; ?>"> |
| 4531 |
<?php endif; ?> |
| 4532 |
<label> |
| 4533 |
<input type="checkbox" name="security_headers[hsts][enabled]" value="1" <?php checked( ! empty( $options['hsts']['enabled'] ) ); ?> <?php disabled( ! $vig_home_https ); ?>> |
| 4534 |
<?php esc_html_e( 'Send the Strict-Transport-Security header', 'vigilante' ); ?> |
| 4535 |
</label> |
| 4536 |
<p class="description"><?php esc_html_e( '⚠ Hard to undo: browsers remember it for the whole max age even if you turn it off later, so a site that loses its certificate stays unreachable until it expires. Start with a short max age.', 'vigilante' ); ?></p> |
| 4537 |
</td> |
| 4538 |
</tr> |
| 4539 |
<tr> |
| 4540 |
<th scope="row"><label for="vigilante-f-security-headers-hsts-max-age"><?php esc_html_e( 'Max Age', 'vigilante' ); ?></label></th> |
| 4541 |
<td> |
| 4542 |
<select id="vigilante-f-security-headers-hsts-max-age" name="security_headers[hsts][max_age]"> |
| 4543 |
<option value="86400" <?php selected( $options['hsts']['max_age'] ?? 31536000, 86400 ); ?>><?php esc_html_e( '1 day (testing)', 'vigilante' ); ?></option> |
| 4544 |
<option value="2592000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 2592000 ); ?>><?php esc_html_e( '30 days', 'vigilante' ); ?></option> |
| 4545 |
<option value="31536000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 31536000 ); ?>><?php esc_html_e( '1 year (recommended)', 'vigilante' ); ?></option> |
| 4546 |
<option value="63072000" <?php selected( $options['hsts']['max_age'] ?? 31536000, 63072000 ); ?>><?php esc_html_e( '2 years', 'vigilante' ); ?></option> |
| 4547 |
</select> |
| 4548 |
</td> |
| 4549 |
</tr> |
| 4550 |
<tr> |
| 4551 |
<th scope="row"><?php esc_html_e( 'Include Subdomains', 'vigilante' ); ?></th> |
| 4552 |
<td> |
| 4553 |
<label> |
| 4554 |
<input type="checkbox" name="security_headers[hsts][include_subdomains]" value="1" <?php checked( ! empty( $options['hsts']['include_subdomains'] ) ); ?>> |
| 4555 |
<?php esc_html_e( 'Apply HSTS to all subdomains', 'vigilante' ); ?> |
| 4556 |
</label> |
| 4557 |
</td> |
| 4558 |
</tr> |
| 4559 |
</table> |
| 4560 |
|
| 4561 |
<h3 id="vigilante-section-headers-fingerprint"><?php esc_html_e( 'Server Identity', 'vigilante' ); ?></h3> |
| 4562 |
<p class="description"><?php esc_html_e( 'Hide identifying information that servers expose in responses.', 'vigilante' ); ?></p> |
| 4563 |
<table class="form-table"> |
| 4564 |
<tr> |
| 4565 |
<th scope="row"><?php esc_html_e( 'Server Signature', 'vigilante' ); ?></th> |
| 4566 |
<td> |
| 4567 |
<label> |
| 4568 |
<input type="checkbox" name="security_headers[hide_server_signature]" value="1" <?php checked( ! empty( $options['hide_server_signature'] ) ); ?>> |
| 4569 |
<?php esc_html_e( 'Hide server signature (ServerSignature Off)', 'vigilante' ); ?> |
| 4570 |
</label> |
| 4571 |
</td> |
| 4572 |
</tr> |
| 4573 |
<tr> |
| 4574 |
<th scope="row"><?php esc_html_e( 'Remove Fingerprinting Headers', 'vigilante' ); ?></th> |
| 4575 |
<td> |
| 4576 |
<label> |
| 4577 |
<input type="checkbox" name="security_headers[remove_fingerprinting_headers]" value="1" <?php checked( ! empty( $options['remove_fingerprinting_headers'] ) ); ?>> |
| 4578 |
<?php esc_html_e( 'Remove X-Powered-By and Server headers', 'vigilante' ); ?> |
| 4579 |
</label> |
| 4580 |
</td> |
| 4581 |
</tr> |
| 4582 |
</table> |
| 4583 |
</div> |
| 4584 |
|
| 4585 |
<?php $vg_cop = ( isset( $options['cross_origin_policies'] ) && is_array( $options['cross_origin_policies'] ) ) ? $options['cross_origin_policies'] : array(); ?> |
| 4586 |
<div id="vigilante-section-headers-cross-origin" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>> |
| 4587 |
<h2> |
| 4588 |
<?php esc_html_e( 'Cross-Origin Policies', 'vigilante' ); ?> |
| 4589 |
<span class="vigilante-method-badge htaccess"><?php esc_html_e( 'HTACCESS', 'vigilante' ); ?></span> |
| 4590 |
</h2> |
| 4591 |
<p><?php esc_html_e( 'Control how other origins may open, embed or fetch your site. Vigilant already sends these headers with the values below.', 'vigilante' ); ?></p> |
| 4592 |
|
| 4593 |
<table class="form-table"> |
| 4594 |
<tr> |
| 4595 |
<th scope="row"><label for="vigilante-f-security-headers-coop"><?php esc_html_e( 'Cross-Origin-Opener-Policy (COOP)', 'vigilante' ); ?></label></th> |
| 4596 |
<td> |
| 4597 |
<select id="vigilante-f-security-headers-coop" name="security_headers[cross_origin_policies][opener_policy]"> |
| 4598 |
<option value="" <?php selected( empty( $vg_cop['opener_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option> |
| 4599 |
<option value="unsafe-none" <?php selected( $vg_cop['opener_policy'] ?? '', 'unsafe-none' ); ?>>unsafe-none</option> |
| 4600 |
<option value="same-origin-allow-popups" <?php selected( $vg_cop['opener_policy'] ?? '', 'same-origin-allow-popups' ); ?>><?php esc_html_e( 'same-origin-allow-popups (recommended)', 'vigilante' ); ?></option> |
| 4601 |
<option value="same-origin" <?php selected( $vg_cop['opener_policy'] ?? '', 'same-origin' ); ?>>same-origin</option> |
| 4602 |
</select> |
| 4603 |
<p class="description"><?php esc_html_e( 'ⓘ Cuts the link between your site and a window from another origin that opened it. Side effect: external tools that open your site in a new tab and talk to it through window.opener, such as Google Tag Assistant, will report that they cannot connect. Pick unsafe-none or Disabled if you need those tools.', 'vigilante' ); ?></p> |
| 4604 |
</td> |
| 4605 |
</tr> |
| 4606 |
<tr> |
| 4607 |
<th scope="row"><label for="vigilante-f-security-headers-coep"><?php esc_html_e( 'Cross-Origin-Embedder-Policy (COEP)', 'vigilante' ); ?></label></th> |
| 4608 |
<td> |
| 4609 |
<select id="vigilante-f-security-headers-coep" name="security_headers[cross_origin_policies][embedder_policy]"> |
| 4610 |
<option value="unsafe-none" <?php selected( ( $vg_cop['embedder_policy'] ?? 'unsafe-none' ), 'unsafe-none' ); ?>><?php esc_html_e( 'unsafe-none (header not sent)', 'vigilante' ); ?></option> |
| 4611 |
<option value="credentialless" <?php selected( $vg_cop['embedder_policy'] ?? '', 'credentialless' ); ?>>credentialless</option> |
| 4612 |
<option value="require-corp" <?php selected( $vg_cop['embedder_policy'] ?? '', 'require-corp' ); ?>>require-corp</option> |
| 4613 |
</select> |
| 4614 |
<p class="description"><?php esc_html_e( 'ⓘ Requires every cross-origin resource to opt in. require-corp can block third-party images, fonts, videos and embeds that do not send their own CORP or CORS headers.', 'vigilante' ); ?></p> |
| 4615 |
</td> |
| 4616 |
</tr> |
| 4617 |
<tr> |
| 4618 |
<th scope="row"><label for="vigilante-f-security-headers-corp"><?php esc_html_e( 'Cross-Origin-Resource-Policy (CORP)', 'vigilante' ); ?></label></th> |
| 4619 |
<td> |
| 4620 |
<select id="vigilante-f-security-headers-corp" name="security_headers[cross_origin_policies][resource_policy]"> |
| 4621 |
<option value="" <?php selected( empty( $vg_cop['resource_policy'] ) ); ?>><?php esc_html_e( 'Disabled (header not sent)', 'vigilante' ); ?></option> |
| 4622 |
<option value="same-site" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-site' ); ?>>same-site</option> |
| 4623 |
<option value="same-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'same-origin' ); ?>>same-origin</option> |
| 4624 |
<option value="cross-origin" <?php selected( $vg_cop['resource_policy'] ?? '', 'cross-origin' ); ?>><?php esc_html_e( 'cross-origin (recommended)', 'vigilante' ); ?></option> |
| 4625 |
</select> |
| 4626 |
<p class="description"><?php esc_html_e( 'ⓘ Declares who may load resources from this site. same-origin stops hotlinking, but it also breaks CDNs, feed readers and any external service that fetches your images or files.', 'vigilante' ); ?></p> |
| 4627 |
</td> |
| 4628 |
</tr> |
| 4629 |
</table> |
| 4630 |
</div> |
| 4631 |
|
| 4632 |
<p class="submit vigilante-submit-buttons"> |
| 4633 |
<?php if ( ! $vg_shared_locked ) : ?> |
| 4634 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 4635 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 4636 |
</button> |
| 4637 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 4638 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 4639 |
</button> |
| 4640 |
<?php endif; ?> |
| 4641 |
<?php /* Testing what the server actually sends is read-only and useful from any site of a network. */ ?> |
| 4642 |
<button type="button" class="button vigilante-test-headers"> |
| 4643 |
<?php esc_html_e( 'Test Headers', 'vigilante' ); ?> |
| 4644 |
</button> |
| 4645 |
</p> |
| 4646 |
</form> |
| 4647 |
|
| 4648 |
<div id="vigilante-headers-result"></div> |
| 4649 |
<?php |
| 4650 |
} |
| 4651 |
|
| 4652 |
/** |
| 4653 |
* Render REST API tab |
| 4654 |
*/ |
| 4655 |
private function render_tab_rest_api() { |
| 4656 |
$is_disabled = $this->render_module_disabled_notice( 'rest_api_security' ); |
| 4657 |
$options = $this->settings->get_section( 'rest_api_security' ); |
| 4658 |
?> |
| 4659 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="rest_api_security" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 4660 |
<div id="vigilante-section-rest-api-main" class="vigilante-settings-section"> |
| 4661 |
<h2> |
| 4662 |
<?php esc_html_e( 'REST API Security', 'vigilante' ); ?> |
| 4663 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 4664 |
</h2> |
| 4665 |
<p><?php esc_html_e( 'Control access to WordPress REST API endpoints.', 'vigilante' ); ?></p> |
| 4666 |
|
| 4667 |
<table class="form-table"> |
| 4668 |
<tr> |
| 4669 |
<th scope="row"><label for="vigilante-f-rest-api-security-mode"><?php esc_html_e( 'Access Mode', 'vigilante' ); ?></label></th> |
| 4670 |
<td> |
| 4671 |
<select id="vigilante-f-rest-api-security-mode" name="rest_api_security[mode]"> |
| 4672 |
<option value="open" <?php selected( $options['mode'] ?? 'selective', 'open' ); ?>><?php esc_html_e( 'Open - Allow all requests', 'vigilante' ); ?></option> |
| 4673 |
<option value="selective" <?php selected( $options['mode'] ?? 'selective', 'selective' ); ?>><?php esc_html_e( 'Selective - Protect sensitive endpoints', 'vigilante' ); ?></option> |
| 4674 |
<option value="authenticated_only" <?php selected( $options['mode'] ?? 'selective', 'authenticated_only' ); ?>><?php esc_html_e( 'Authenticated - Require login for all', 'vigilante' ); ?></option> |
| 4675 |
</select> |
| 4676 |
<p class="description"><?php esc_html_e( 'Selective mode is recommended.', 'vigilante' ); ?></p> |
| 4677 |
</td> |
| 4678 |
</tr> |
| 4679 |
<tr id="field-block-user-enumeration"> |
| 4680 |
<th scope="row"><?php esc_html_e( 'Block User Enumeration', 'vigilante' ); ?></th> |
| 4681 |
<td> |
| 4682 |
<label> |
| 4683 |
<input type="checkbox" name="rest_api_security[block_user_enumeration]" value="1" <?php checked( ! empty( $options['block_user_enumeration'] ) ); ?>> |
| 4684 |
<?php esc_html_e( 'Protect /wp/v2/users endpoint for unauthenticated users', 'vigilante' ); ?> |
| 4685 |
</label> |
| 4686 |
</td> |
| 4687 |
</tr> |
| 4688 |
<tr> |
| 4689 |
<th scope="row"><?php esc_html_e( 'Disable JSONP', 'vigilante' ); ?></th> |
| 4690 |
<td> |
| 4691 |
<label> |
| 4692 |
<input type="checkbox" name="rest_api_security[disable_jsonp]" value="1" <?php checked( ! empty( $options['disable_jsonp'] ) ); ?>> |
| 4693 |
<?php esc_html_e( 'Disable JSONP support in REST API', 'vigilante' ); ?> |
| 4694 |
</label> |
| 4695 |
</td> |
| 4696 |
</tr> |
| 4697 |
</table> |
| 4698 |
</div> |
| 4699 |
|
| 4700 |
<p class="submit vigilante-submit-buttons"> |
| 4701 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 4702 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 4703 |
</button> |
| 4704 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 4705 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 4706 |
</button> |
| 4707 |
</p> |
| 4708 |
</form> |
| 4709 |
<?php |
| 4710 |
} |
| 4711 |
|
| 4712 |
/** |
| 4713 |
* Render User Security tab |
| 4714 |
*/ |
| 4715 |
private function render_tab_users() { |
| 4716 |
$is_disabled = $this->render_module_disabled_notice( 'user_security' ); |
| 4717 |
$options = $this->settings->get_section( 'user_security' ); |
| 4718 |
$monitoring = $options['admin_monitoring'] ?? array(); |
| 4719 |
$registration = $options['registration_approval'] ?? array(); |
| 4720 |
$session_limits = $options['session_limits'] ?? array(); |
| 4721 |
$password_exp = $options['password_expiration'] ?? array(); |
| 4722 |
$email_verify = $options['email_verification'] ?? array(); |
| 4723 |
?> |
| 4724 |
|
| 4725 |
<!-- ============================================================ |
| 4726 |
SETTINGS SECTION - Single form for all configuration |
| 4727 |
============================================================ --> |
| 4728 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="user_security" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 4729 |
|
| 4730 |
<!-- Username & Password Protection --> |
| 4731 |
<div id="vigilante-section-users-password" class="vigilante-settings-section"> |
| 4732 |
<h2> |
| 4733 |
<?php esc_html_e( 'Username & password protection', 'vigilante' ); ?> |
| 4734 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 4735 |
</h2> |
| 4736 |
<p><?php esc_html_e( 'Enforce secure username and password policies.', 'vigilante' ); ?></p> |
| 4737 |
|
| 4738 |
<table class="form-table"> |
| 4739 |
<tr> |
| 4740 |
<th scope="row"><?php esc_html_e( 'Block Insecure Usernames', 'vigilante' ); ?></th> |
| 4741 |
<td> |
| 4742 |
<label> |
| 4743 |
<input type="checkbox" name="user_security[block_insecure_usernames]" value="1" <?php checked( ! empty( $options['block_insecure_usernames'] ) ); ?>> |
| 4744 |
<?php esc_html_e( 'Prevent creation of users with common usernames (admin, administrator, etc.)', 'vigilante' ); ?> |
| 4745 |
</label> |
| 4746 |
</td> |
| 4747 |
</tr> |
| 4748 |
<?php |
| 4749 |
$pw_policy = wp_parse_args( |
| 4750 |
( isset( $options['password_policy'] ) && is_array( $options['password_policy'] ) ) ? $options['password_policy'] : array(), |
| 4751 |
array( |
| 4752 |
'require_uppercase' => false, |
| 4753 |
'require_lowercase' => false, |
| 4754 |
'require_number' => false, |
| 4755 |
'require_special' => false, |
| 4756 |
'block_common' => true, |
| 4757 |
'block_username' => true, |
| 4758 |
'affected_roles' => array(), |
| 4759 |
) |
| 4760 |
); |
| 4761 |
$pw_policy_roles = (array) $pw_policy['affected_roles']; |
| 4762 |
?> |
| 4763 |
<tr> |
| 4764 |
<th scope="row"><?php esc_html_e( 'Enforce Strong Passwords', 'vigilante' ); ?></th> |
| 4765 |
<td> |
| 4766 |
<label> |
| 4767 |
<input type="checkbox" name="user_security[force_strong_passwords]" value="1" <?php checked( ! empty( $options['force_strong_passwords'] ) ); ?>> |
| 4768 |
<?php esc_html_e( 'Check passwords against the requirements below when a user sets or changes one', 'vigilante' ); ?> |
| 4769 |
</label> |
| 4770 |
</td> |
| 4771 |
</tr> |
| 4772 |
<tr> |
| 4773 |
<th scope="row"><label for="vigilante-f-user-security-min-password-length"><?php esc_html_e( 'Minimum Password Length', 'vigilante' ); ?></label></th> |
| 4774 |
<td> |
| 4775 |
<input id="vigilante-f-user-security-min-password-length" type="number" name="user_security[min_password_length]" value="<?php echo esc_attr( $options['min_password_length'] ?? 12 ); ?>" min="6" max="32" class="small-text"> |
| 4776 |
<?php esc_html_e( 'characters', 'vigilante' ); ?> |
| 4777 |
</td> |
| 4778 |
</tr> |
| 4779 |
<tr> |
| 4780 |
<th scope="row"><?php esc_html_e( 'Password Requirements', 'vigilante' ); ?></th> |
| 4781 |
<td> |
| 4782 |
<label style="display:block;margin-bottom:5px;"> |
| 4783 |
<input type="checkbox" name="user_security[password_policy][require_uppercase]" value="1" <?php checked( ! empty( $pw_policy['require_uppercase'] ) ); ?>> |
| 4784 |
<?php esc_html_e( 'Require an uppercase letter (A-Z)', 'vigilante' ); ?> |
| 4785 |
</label> |
| 4786 |
<label style="display:block;margin-bottom:5px;"> |
| 4787 |
<input type="checkbox" name="user_security[password_policy][require_lowercase]" value="1" <?php checked( ! empty( $pw_policy['require_lowercase'] ) ); ?>> |
| 4788 |
<?php esc_html_e( 'Require a lowercase letter (a-z)', 'vigilante' ); ?> |
| 4789 |
</label> |
| 4790 |
<label style="display:block;margin-bottom:5px;"> |
| 4791 |
<input type="checkbox" name="user_security[password_policy][require_number]" value="1" <?php checked( ! empty( $pw_policy['require_number'] ) ); ?>> |
| 4792 |
<?php esc_html_e( 'Require a number (0-9)', 'vigilante' ); ?> |
| 4793 |
</label> |
| 4794 |
<label style="display:block;margin-bottom:5px;"> |
| 4795 |
<input type="checkbox" name="user_security[password_policy][require_special]" value="1" <?php checked( ! empty( $pw_policy['require_special'] ) ); ?>> |
| 4796 |
<?php esc_html_e( 'Require a special character (!, @, #, ...)', 'vigilante' ); ?> |
| 4797 |
</label> |
| 4798 |
<label style="display:block;margin-bottom:5px;"> |
| 4799 |
<input type="checkbox" name="user_security[password_policy][block_common]" value="1" <?php checked( ! empty( $pw_policy['block_common'] ) ); ?>> |
| 4800 |
<?php esc_html_e( 'Reject well-known common passwords', 'vigilante' ); ?> |
| 4801 |
</label> |
| 4802 |
<label style="display:block;margin-bottom:5px;"> |
| 4803 |
<input type="checkbox" name="user_security[password_policy][block_username]" value="1" <?php checked( ! empty( $pw_policy['block_username'] ) ); ?>> |
| 4804 |
<?php esc_html_e( 'Do not allow the username inside the password', 'vigilante' ); ?> |
| 4805 |
</label> |
| 4806 |
<p class="description"><?php esc_html_e( 'These rules apply only while "Enforce Strong Passwords" is on. Turn off individual rules to allow, for example, long passphrases without numbers or symbols.', 'vigilante' ); ?></p> |
| 4807 |
</td> |
| 4808 |
</tr> |
| 4809 |
<tr> |
| 4810 |
<th scope="row"><?php esc_html_e( 'Apply Password Rules To', 'vigilante' ); ?></th> |
| 4811 |
<td> |
| 4812 |
<?php foreach ( wp_roles()->get_names() as $role_slug => $role_name ) : ?> |
| 4813 |
<label style="display:block;margin-bottom:5px;"> |
| 4814 |
<input type="checkbox" name="user_security[password_policy][affected_roles][]" value="<?php echo esc_attr( $role_slug ); ?>" <?php checked( empty( $pw_policy_roles ) || in_array( $role_slug, $pw_policy_roles, true ) ); ?>> |
| 4815 |
<?php echo esc_html( translate_user_role( $role_name ) ); ?> |
| 4816 |
</label> |
| 4817 |
<?php endforeach; ?> |
| 4818 |
<p class="description"><?php esc_html_e( 'All roles are covered by default. Uncheck a role to exclude it from the password policy.', 'vigilante' ); ?></p> |
| 4819 |
</td> |
| 4820 |
</tr> |
| 4821 |
<tr id="field-block-author-scanning"> |
| 4822 |
<th scope="row"><?php esc_html_e( 'Block Author Scanning', 'vigilante' ); ?></th> |
| 4823 |
<td> |
| 4824 |
<label> |
| 4825 |
<input type="checkbox" name="user_security[block_author_scanning]" value="1" <?php checked( ! empty( $options['block_author_scanning'] ) ); ?>> |
| 4826 |
<?php esc_html_e( 'Prevent username discovery via ?author=N URLs', 'vigilante' ); ?> |
| 4827 |
</label> |
| 4828 |
</td> |
| 4829 |
</tr> |
| 4830 |
<tr> |
| 4831 |
<th scope="row"><?php esc_html_e( 'Display Name Protection', 'vigilante' ); ?></th> |
| 4832 |
<td> |
| 4833 |
<label> |
| 4834 |
<input type="checkbox" name="user_security[prevent_display_name_login_match]" value="1" <?php checked( ! empty( $options['prevent_display_name_login_match'] ) ); ?>> |
| 4835 |
<?php esc_html_e( 'Prevent users from saving a display name that matches their login username', 'vigilante' ); ?> |
| 4836 |
</label> |
| 4837 |
<p class="description"><?php esc_html_e( 'The display name is publicly visible and should not reveal the login username.', 'vigilante' ); ?></p> |
| 4838 |
</td> |
| 4839 |
</tr> |
| 4840 |
</table> |
| 4841 |
</div> |
| 4842 |
|
| 4843 |
<!-- Admin Monitoring --> |
| 4844 |
<div id="vigilante-section-users-admin-monitoring" class="vigilante-settings-section"> |
| 4845 |
<h2> |
| 4846 |
<?php esc_html_e( 'Admin monitoring', 'vigilante' ); ?> |
| 4847 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 4848 |
</h2> |
| 4849 |
<p><?php esc_html_e( 'Receive email alerts when administrator accounts are modified. All events are always logged to the Security Audit.', 'vigilante' ); ?></p> |
| 4850 |
|
| 4851 |
<table class="form-table"> |
| 4852 |
<tr> |
| 4853 |
<th scope="row"><?php esc_html_e( 'New Administrator Alert', 'vigilante' ); ?></th> |
| 4854 |
<td> |
| 4855 |
<label> |
| 4856 |
<input type="checkbox" name="user_security[admin_monitoring][alert_new_admin]" value="1" <?php checked( ! empty( $monitoring['alert_new_admin'] ) ); ?>> |
| 4857 |
<?php esc_html_e( 'Send email alert when a new administrator account is created', 'vigilante' ); ?> |
| 4858 |
</label> |
| 4859 |
</td> |
| 4860 |
</tr> |
| 4861 |
<tr> |
| 4862 |
<th scope="row"><?php esc_html_e( 'Admin Email Change Alert', 'vigilante' ); ?></th> |
| 4863 |
<td> |
| 4864 |
<label> |
| 4865 |
<input type="checkbox" name="user_security[admin_monitoring][alert_admin_email_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_email_change'] ) ); ?>> |
| 4866 |
<?php esc_html_e( 'Send email alert when an administrator email address is changed', 'vigilante' ); ?> |
| 4867 |
</label> |
| 4868 |
</td> |
| 4869 |
</tr> |
| 4870 |
<tr> |
| 4871 |
<th scope="row"><?php esc_html_e( 'Permission Elevation Alert', 'vigilante' ); ?></th> |
| 4872 |
<td> |
| 4873 |
<label> |
| 4874 |
<input type="checkbox" name="user_security[admin_monitoring][alert_permission_elevation]" value="1" <?php checked( ! empty( $monitoring['alert_permission_elevation'] ) ); ?>> |
| 4875 |
<?php esc_html_e( 'Send email alert when a user is elevated to administrator role', 'vigilante' ); ?> |
| 4876 |
</label> |
| 4877 |
</td> |
| 4878 |
</tr> |
| 4879 |
<tr> |
| 4880 |
<th scope="row"><?php esc_html_e( 'Admin Password Change Alert', 'vigilante' ); ?></th> |
| 4881 |
<td> |
| 4882 |
<label> |
| 4883 |
<input type="checkbox" name="user_security[admin_monitoring][alert_admin_password_change]" value="1" <?php checked( ! empty( $monitoring['alert_admin_password_change'] ) ); ?>> |
| 4884 |
<?php esc_html_e( 'Send email alert when an administrator password is changed', 'vigilante' ); ?> |
| 4885 |
</label> |
| 4886 |
</td> |
| 4887 |
</tr> |
| 4888 |
</table> |
| 4889 |
|
| 4890 |
<p class="description"> |
| 4891 |
<?php |
| 4892 |
printf( |
| 4893 |
/* translators: %s: Link to notification settings */ |
| 4894 |
esc_html__( 'ⓘ Notifications are sent to the recipients configured in %s.', 'vigilante' ), |
| 4895 |
'<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>' |
| 4896 |
); |
| 4897 |
?> |
| 4898 |
</p> |
| 4899 |
</div> |
| 4900 |
|
| 4901 |
<!-- Registration Approval --> |
| 4902 |
<div id="vigilante-section-users-registration" class="vigilante-settings-section"> |
| 4903 |
<h2> |
| 4904 |
<?php esc_html_e( 'Registration approval', 'vigilante' ); ?> |
| 4905 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 4906 |
</h2> |
| 4907 |
<p><?php esc_html_e( 'Require manual approval for new user registrations.', 'vigilante' ); ?></p> |
| 4908 |
|
| 4909 |
<table class="form-table"> |
| 4910 |
<tr> |
| 4911 |
<th scope="row"><?php esc_html_e( 'Enable Registration Approval', 'vigilante' ); ?></th> |
| 4912 |
<td> |
| 4913 |
<label> |
| 4914 |
<input type="checkbox" name="user_security[registration_approval][enabled]" value="1" <?php checked( ! empty( $registration['enabled'] ) ); ?>> |
| 4915 |
<?php esc_html_e( 'New users must be approved by an administrator before they can log in', 'vigilante' ); ?> |
| 4916 |
</label> |
| 4917 |
</td> |
| 4918 |
</tr> |
| 4919 |
<tr> |
| 4920 |
<th scope="row"><?php esc_html_e( 'Notify Admin', 'vigilante' ); ?></th> |
| 4921 |
<td> |
| 4922 |
<label> |
| 4923 |
<input type="checkbox" name="user_security[registration_approval][notify_admin]" value="1" <?php checked( ! empty( $registration['notify_admin'] ) ); ?>> |
| 4924 |
<?php esc_html_e( 'Send email notification when a new user registers', 'vigilante' ); ?> |
| 4925 |
</label> |
| 4926 |
<p class="description"><?php esc_html_e( 'Disable on high-traffic sites to avoid email overload.', 'vigilante' ); ?></p> |
| 4927 |
</td> |
| 4928 |
</tr> |
| 4929 |
<tr> |
| 4930 |
<th scope="row"><label for="vigilante-f-user-security-registration-approval-auto-reject-days"><?php esc_html_e( 'Auto-reject After', 'vigilante' ); ?></label></th> |
| 4931 |
<td> |
| 4932 |
<input id="vigilante-f-user-security-registration-approval-auto-reject-days" type="number" name="user_security[registration_approval][auto_reject_days]" value="<?php echo esc_attr( $registration['auto_reject_days'] ?? 0 ); ?>" min="0" max="365" class="small-text"> |
| 4933 |
<?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?> |
| 4934 |
<p class="description"><?php esc_html_e( 'Automatically reject pending registrations after this many days.', 'vigilante' ); ?></p> |
| 4935 |
</td> |
| 4936 |
</tr> |
| 4937 |
</table> |
| 4938 |
</div> |
| 4939 |
|
| 4940 |
<!-- Session Limits --> |
| 4941 |
<div id="vigilante-section-users-sessions" class="vigilante-settings-section"> |
| 4942 |
<h2> |
| 4943 |
<?php esc_html_e( 'Session limits', 'vigilante' ); ?> |
| 4944 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 4945 |
</h2> |
| 4946 |
<p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p> |
| 4947 |
|
| 4948 |
<?php if ( Vigilante_User_Security::session_limit_is_network_wide() ) : ?> |
| 4949 |
<div class="notice notice-warning inline"> |
| 4950 |
<p> |
| 4951 |
<?php esc_html_e( 'This limit does not apply on a network. WordPress keeps the sessions of an account for the whole network, not per site, so a limit set here would count and close the sessions that person opened on other sites, including an administrator session elsewhere. A network-wide session policy is planned; until then these settings are saved but not enforced.', 'vigilante' ); ?> |
| 4952 |
</p> |
| 4953 |
</div> |
| 4954 |
<?php endif; ?> |
| 4955 |
|
| 4956 |
<table class="form-table"> |
| 4957 |
<tr> |
| 4958 |
<th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th> |
| 4959 |
<td> |
| 4960 |
<label> |
| 4961 |
<input type="checkbox" name="user_security[session_limits][enabled]" value="1" <?php checked( ! empty( $session_limits['enabled'] ) ); ?>> |
| 4962 |
<?php esc_html_e( 'Limit the number of active sessions per user', 'vigilante' ); ?> |
| 4963 |
</label> |
| 4964 |
</td> |
| 4965 |
</tr> |
| 4966 |
<tr> |
| 4967 |
<th scope="row"><label for="vigilante-f-user-security-session-limits-max-sessions"><?php esc_html_e( 'Maximum Sessions', 'vigilante' ); ?></label></th> |
| 4968 |
<td> |
| 4969 |
<input id="vigilante-f-user-security-session-limits-max-sessions" type="number" name="user_security[session_limits][max_sessions]" value="<?php echo esc_attr( $session_limits['max_sessions'] ?? 3 ); ?>" min="1" max="10" class="small-text"> |
| 4970 |
<?php esc_html_e( 'sessions per user', 'vigilante' ); ?> |
| 4971 |
</td> |
| 4972 |
</tr> |
| 4973 |
<tr> |
| 4974 |
<th scope="row"><label for="vigilante-f-user-security-session-limits-behavior"><?php esc_html_e( 'When Limit Exceeded', 'vigilante' ); ?></label></th> |
| 4975 |
<td> |
| 4976 |
<select id="vigilante-f-user-security-session-limits-behavior" name="user_security[session_limits][behavior]"> |
| 4977 |
<option value="block_new" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'block_new' ); ?>><?php esc_html_e( 'Block new login', 'vigilante' ); ?></option> |
| 4978 |
<option value="close_oldest" <?php selected( ( $session_limits['behavior'] ?? 'close_oldest' ), 'close_oldest' ); ?>><?php esc_html_e( 'Close oldest session', 'vigilante' ); ?></option> |
| 4979 |
</select> |
| 4980 |
<p class="description"><?php esc_html_e( '"Close oldest" is recommended for security - ensures attackers cannot lock out legitimate users.', 'vigilante' ); ?></p> |
| 4981 |
</td> |
| 4982 |
</tr> |
| 4983 |
<tr> |
| 4984 |
<th scope="row"><?php esc_html_e( 'Exclude Administrators', 'vigilante' ); ?></th> |
| 4985 |
<td> |
| 4986 |
<label> |
| 4987 |
<input type="checkbox" name="user_security[session_limits][exclude_admins]" value="1" <?php checked( ! empty( $session_limits['exclude_admins'] ) ); ?>> |
| 4988 |
<?php esc_html_e( 'Do not apply session limits to administrators', 'vigilante' ); ?> |
| 4989 |
</label> |
| 4990 |
</td> |
| 4991 |
</tr> |
| 4992 |
</table> |
| 4993 |
</div> |
| 4994 |
|
| 4995 |
<!-- Password Expiration --> |
| 4996 |
<div id="vigilante-section-users-password-exp" class="vigilante-settings-section"> |
| 4997 |
<h2> |
| 4998 |
<?php esc_html_e( 'Password expiration', 'vigilante' ); ?> |
| 4999 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5000 |
</h2> |
| 5001 |
<p><?php esc_html_e( 'Force users to change their password periodically.', 'vigilante' ); ?></p> |
| 5002 |
|
| 5003 |
<table class="form-table"> |
| 5004 |
<tr> |
| 5005 |
<th scope="row"><?php esc_html_e( 'Enable Password Expiration', 'vigilante' ); ?></th> |
| 5006 |
<td> |
| 5007 |
<label> |
| 5008 |
<input type="checkbox" name="user_security[password_expiration][enabled]" value="1" <?php checked( ! empty( $password_exp['enabled'] ) ); ?>> |
| 5009 |
<?php esc_html_e( 'Force password change after a set number of days', 'vigilante' ); ?> |
| 5010 |
</label> |
| 5011 |
</td> |
| 5012 |
</tr> |
| 5013 |
<tr> |
| 5014 |
<th scope="row"><label for="vigilante-f-user-security-password-expiration-expire-days"><?php esc_html_e( 'Expire After', 'vigilante' ); ?></label></th> |
| 5015 |
<td> |
| 5016 |
<input id="vigilante-f-user-security-password-expiration-expire-days" type="number" name="user_security[password_expiration][expire_days]" value="<?php echo esc_attr( $password_exp['expire_days'] ?? 90 ); ?>" min="7" max="365" class="small-text"> |
| 5017 |
<?php esc_html_e( 'days', 'vigilante' ); ?> |
| 5018 |
<p class="description"><?php esc_html_e( 'PCI-DSS recommends 90 days.', 'vigilante' ); ?></p> |
| 5019 |
</td> |
| 5020 |
</tr> |
| 5021 |
<tr> |
| 5022 |
<th scope="row"><label for="vigilante-f-user-security-password-expiration-warning-days"><?php esc_html_e( 'Warning Period', 'vigilante' ); ?></label></th> |
| 5023 |
<td> |
| 5024 |
<input id="vigilante-f-user-security-password-expiration-warning-days" type="number" name="user_security[password_expiration][warning_days]" value="<?php echo esc_attr( $password_exp['warning_days'] ?? 14 ); ?>" min="1" max="30" class="small-text"> |
| 5025 |
<?php esc_html_e( 'days before expiration', 'vigilante' ); ?> |
| 5026 |
<p class="description"><?php esc_html_e( 'Show warning notice this many days before password expires.', 'vigilante' ); ?></p> |
| 5027 |
</td> |
| 5028 |
</tr> |
| 5029 |
<tr> |
| 5030 |
<th scope="row"><label for="vigilante-f-user-security-password-expiration-password-history"><?php esc_html_e( 'Password History', 'vigilante' ); ?></label></th> |
| 5031 |
<td> |
| 5032 |
<input id="vigilante-f-user-security-password-expiration-password-history" type="number" name="user_security[password_expiration][password_history]" value="<?php echo esc_attr( $password_exp['password_history'] ?? 3 ); ?>" min="0" max="24" class="small-text"> |
| 5033 |
<?php esc_html_e( 'passwords to remember', 'vigilante' ); ?> |
| 5034 |
<p class="description"><?php esc_html_e( 'Prevent reusing recent passwords. Set to 0 to disable.', 'vigilante' ); ?></p> |
| 5035 |
</td> |
| 5036 |
</tr> |
| 5037 |
<tr> |
| 5038 |
<th scope="row"><?php esc_html_e( 'Email Reminder', 'vigilante' ); ?></th> |
| 5039 |
<td> |
| 5040 |
<label> |
| 5041 |
<input type="checkbox" name="user_security[password_expiration][send_reminder]" value="1" <?php checked( ! empty( $password_exp['send_reminder'] ) ); ?>> |
| 5042 |
<?php esc_html_e( 'Send email reminder when password is about to expire', 'vigilante' ); ?> |
| 5043 |
</label> |
| 5044 |
<p class="description"><?php esc_html_e( 'The reminder is sent once when the warning period starts, using the same number of days configured above.', 'vigilante' ); ?></p> |
| 5045 |
</td> |
| 5046 |
</tr> |
| 5047 |
<tr> |
| 5048 |
<th scope="row"><?php esc_html_e( 'Affected Roles', 'vigilante' ); ?></th> |
| 5049 |
<td> |
| 5050 |
<?php |
| 5051 |
$affected_roles = $password_exp['affected_roles'] ?? array( 'administrator', 'editor' ); |
| 5052 |
$all_roles = wp_roles()->get_names(); |
| 5053 |
foreach ( $all_roles as $role_slug => $role_name ) : |
| 5054 |
?> |
| 5055 |
<label style="display: block; margin-bottom: 5px;"> |
| 5056 |
<input type="checkbox" name="user_security[password_expiration][affected_roles][]" value="<?php echo esc_attr( $role_slug ); ?>" <?php checked( in_array( $role_slug, $affected_roles, true ) ); ?>> |
| 5057 |
<?php echo esc_html( translate_user_role( $role_name ) ); ?> |
| 5058 |
</label> |
| 5059 |
<?php endforeach; ?> |
| 5060 |
</td> |
| 5061 |
</tr> |
| 5062 |
<tr> |
| 5063 |
<th scope="row"><?php esc_html_e( 'Exclude specific users', 'vigilante' ); ?></th> |
| 5064 |
<td> |
| 5065 |
<?php $pwexp_excluded = $password_exp['excluded_users'] ?? array(); ?> |
| 5066 |
<div class="vigilante-2fa-user-search-container"> |
| 5067 |
<div class="vigilante-2fa-user-search"> |
| 5068 |
<span class="search-icon"></span> |
| 5069 |
<input type="text" |
| 5070 |
id="vigilante_pwexp_user_search" |
| 5071 |
placeholder="<?php esc_attr_e( 'Search users by name or email...', 'vigilante' ); ?>" |
| 5072 |
autocomplete="off"> |
| 5073 |
<div class="vigilante-pwexp-search-results"></div> |
| 5074 |
</div> |
| 5075 |
<div class="vigilante-pwexp-excluded-users"> |
| 5076 |
<?php |
| 5077 |
foreach ( $pwexp_excluded as $pwexp_excluded_id ) : |
| 5078 |
$excluded_user = get_user_by( 'ID', $pwexp_excluded_id ); |
| 5079 |
if ( ! $excluded_user ) { |
| 5080 |
continue; |
| 5081 |
} |
| 5082 |
?> |
| 5083 |
<div class="vigilante-pwexp-excluded-user" data-user-id="<?php echo esc_attr( $pwexp_excluded_id ); ?>"> |
| 5084 |
<span class="user-display"><?php echo esc_html( $excluded_user->display_name . ' (' . $excluded_user->user_email . ')' ); ?></span> |
| 5085 |
<button type="button" class="remove-user" aria-label="<?php esc_attr_e( 'Remove', 'vigilante' ); ?>">×</button> |
| 5086 |
<input type="hidden" name="user_security[password_expiration][excluded_users][]" value="<?php echo esc_attr( $pwexp_excluded_id ); ?>"> |
| 5087 |
</div> |
| 5088 |
<?php endforeach; ?> |
| 5089 |
</div> |
| 5090 |
</div> |
| 5091 |
<p class="description"><?php esc_html_e( 'Listed users will be excluded from password expiration regardless of their role.', 'vigilante' ); ?></p> |
| 5092 |
</td> |
| 5093 |
</tr> |
| 5094 |
</table> |
| 5095 |
</div> |
| 5096 |
|
| 5097 |
<!-- Email Verification --> |
| 5098 |
<div id="vigilante-section-users-email-verify" class="vigilante-settings-section"> |
| 5099 |
<h2> |
| 5100 |
<?php esc_html_e( 'Email verification', 'vigilante' ); ?> |
| 5101 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5102 |
</h2> |
| 5103 |
<p><?php esc_html_e( 'Require new users to verify their email address before logging in.', 'vigilante' ); ?></p> |
| 5104 |
|
| 5105 |
<table class="form-table"> |
| 5106 |
<tr> |
| 5107 |
<th scope="row"><?php esc_html_e( 'Enable Email Verification', 'vigilante' ); ?></th> |
| 5108 |
<td> |
| 5109 |
<label> |
| 5110 |
<input type="checkbox" name="user_security[email_verification][enabled]" value="1" <?php checked( ! empty( $email_verify['enabled'] ) ); ?>> |
| 5111 |
<?php esc_html_e( 'New users must verify their email before logging in', 'vigilante' ); ?> |
| 5112 |
</label> |
| 5113 |
</td> |
| 5114 |
</tr> |
| 5115 |
<tr> |
| 5116 |
<th scope="row"><label for="vigilante-f-user-security-email-verification-token-expiry-hours"><?php esc_html_e( 'Link Expiration', 'vigilante' ); ?></label></th> |
| 5117 |
<td> |
| 5118 |
<input id="vigilante-f-user-security-email-verification-token-expiry-hours" type="number" name="user_security[email_verification][token_expiry_hours]" value="<?php echo esc_attr( $email_verify['token_expiry_hours'] ?? 24 ); ?>" min="1" max="168" class="small-text"> |
| 5119 |
<?php esc_html_e( 'hours', 'vigilante' ); ?> |
| 5120 |
</td> |
| 5121 |
</tr> |
| 5122 |
<tr> |
| 5123 |
<th scope="row"><?php esc_html_e( 'Allow Resend', 'vigilante' ); ?></th> |
| 5124 |
<td> |
| 5125 |
<label> |
| 5126 |
<input type="checkbox" name="user_security[email_verification][allow_resend]" value="1" <?php checked( ! empty( $email_verify['allow_resend'] ) ); ?>> |
| 5127 |
<?php esc_html_e( 'Allow users to request a new verification email', 'vigilante' ); ?> |
| 5128 |
</label> |
| 5129 |
</td> |
| 5130 |
</tr> |
| 5131 |
<tr> |
| 5132 |
<th scope="row"><label for="vigilante-f-user-security-email-verification-auto-delete-days"><?php esc_html_e( 'Auto-delete Unverified', 'vigilante' ); ?></label></th> |
| 5133 |
<td> |
| 5134 |
<input id="vigilante-f-user-security-email-verification-auto-delete-days" type="number" name="user_security[email_verification][auto_delete_days]" value="<?php echo esc_attr( $email_verify['auto_delete_days'] ?? 7 ); ?>" min="0" max="365" class="small-text"> |
| 5135 |
<?php esc_html_e( 'days (0 = never)', 'vigilante' ); ?> |
| 5136 |
<p class="description"><?php esc_html_e( 'Automatically delete users who never verify their email.', 'vigilante' ); ?></p> |
| 5137 |
</td> |
| 5138 |
</tr> |
| 5139 |
</table> |
| 5140 |
</div> |
| 5141 |
|
| 5142 |
<p class="submit vigilante-submit-buttons"> |
| 5143 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 5144 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 5145 |
</button> |
| 5146 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 5147 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 5148 |
</button> |
| 5149 |
</p> |
| 5150 |
</form> |
| 5151 |
|
| 5152 |
<!-- ============================================================ |
| 5153 |
TOOLS SECTION - Actions and utilities (no save button) |
| 5154 |
============================================================ --> |
| 5155 |
<div class="vigilante-tools-section"> |
| 5156 |
<h2 class="vigilante-tools-header"> |
| 5157 |
<?php esc_html_e( 'User security tools', 'vigilante' ); ?> |
| 5158 |
</h2> |
| 5159 |
|
| 5160 |
<?php $this->render_user_actions_notice(); ?> |
| 5161 |
<?php if ( ! $this->user_actions_locked() ) : ?> |
| 5162 |
|
| 5163 |
<!-- Force Password Reset --> |
| 5164 |
<div class="vigilante-tool-box"> |
| 5165 |
<h3><?php esc_html_e( 'Force password reset', 'vigilante' ); ?></h3> |
| 5166 |
<p class="description"><?php esc_html_e( 'Force users to reset their password. Useful after a security incident. Users will receive an email with a reset link.', 'vigilante' ); ?></p> |
| 5167 |
|
| 5168 |
<!-- Reset Specific Users --> |
| 5169 |
<div class="vigilante-password-reset-box"> |
| 5170 |
<h4><?php esc_html_e( 'Reset specific users', 'vigilante' ); ?></h4> |
| 5171 |
|
| 5172 |
<div class="vigilante-user-search-wrapper"> |
| 5173 |
<input type="text" id="vigilante-password-reset-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username, email, or display name...', 'vigilante' ); ?>"> |
| 5174 |
<div id="vigilante-password-reset-results" class="vigilante-user-search-results" style="display: none;"></div> |
| 5175 |
</div> |
| 5176 |
|
| 5177 |
<div id="vigilante-password-reset-selected" class="vigilante-selected-users" style="display: none;"> |
| 5178 |
<strong><?php esc_html_e( 'Selected Users:', 'vigilante' ); ?></strong> |
| 5179 |
<ul class="vigilante-selected-users-list"></ul> |
| 5180 |
</div> |
| 5181 |
|
| 5182 |
<p class="description" style="margin-top: 15px;"> |
| 5183 |
<span class="dashicons dashicons-email-alt" style="color: #2271b1;"></span> |
| 5184 |
<?php esc_html_e( 'Selected users will receive an email with a password reset link.', 'vigilante' ); ?> |
| 5185 |
</p> |
| 5186 |
|
| 5187 |
<p class="submit"> |
| 5188 |
<button type="button" id="vigilante-reset-selected-users" class="button button-primary" disabled> |
| 5189 |
<?php esc_html_e( 'Force Reset for Selected Users', 'vigilante' ); ?> |
| 5190 |
</button> |
| 5191 |
</p> |
| 5192 |
</div> |
| 5193 |
|
| 5194 |
<!-- Reset by Role --> |
| 5195 |
<div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;"> |
| 5196 |
<h4><?php esc_html_e( 'Reset by role', 'vigilante' ); ?></h4> |
| 5197 |
<p class="description"><?php esc_html_e( 'Select one or more roles to force a password reset for all users with those roles. Ideal for security incidents where you need to reset access quickly.', 'vigilante' ); ?></p> |
| 5198 |
|
| 5199 |
<?php |
| 5200 |
$wp_roles = wp_roles(); |
| 5201 |
$user_counts = count_users(); |
| 5202 |
$avail_roles = $user_counts['avail_roles'] ?? array(); |
| 5203 |
$current_user = wp_get_current_user(); |
| 5204 |
$current_roles = $current_user->roles; |
| 5205 |
?> |
| 5206 |
|
| 5207 |
<fieldset class="vigilante-role-checkboxes" style="margin-top: 10px;"> |
| 5208 |
<?php foreach ( $wp_roles->roles as $role_slug => $role_data ) : |
| 5209 |
$count = $avail_roles[ $role_slug ] ?? 0; |
| 5210 |
if ( 0 === $count ) { |
| 5211 |
continue; |
| 5212 |
} |
| 5213 |
$role_name = translate_user_role( $role_data['name'] ); |
| 5214 |
?> |
| 5215 |
<label style="display: block; margin-bottom: 6px;"> |
| 5216 |
<input type="checkbox" |
| 5217 |
class="vigilante-reset-role-checkbox" |
| 5218 |
value="<?php echo esc_attr( $role_slug ); ?>" |
| 5219 |
data-count="<?php echo absint( $count ); ?>"> |
| 5220 |
<?php |
| 5221 |
printf( |
| 5222 |
/* translators: 1: Role name, 2: Number of users */ |
| 5223 |
'%1$s <span class="description">(%2$d)</span>', |
| 5224 |
esc_html( $role_name ), |
| 5225 |
absint( $count ) |
| 5226 |
); |
| 5227 |
?> |
| 5228 |
<?php if ( in_array( $role_slug, $current_roles, true ) ) : ?> |
| 5229 |
<em class="description"><?php esc_html_e( '(includes you)', 'vigilante' ); ?></em> |
| 5230 |
<?php endif; ?> |
| 5231 |
</label> |
| 5232 |
<?php endforeach; ?> |
| 5233 |
</fieldset> |
| 5234 |
|
| 5235 |
<div id="vigilante-reset-role-summary" style="display: none; margin-top: 10px;"> |
| 5236 |
<p> |
| 5237 |
<span class="dashicons dashicons-groups" style="color: #2271b1;"></span> |
| 5238 |
<strong id="vigilante-reset-role-count">0</strong> |
| 5239 |
<?php esc_html_e( 'user(s) will be affected.', 'vigilante' ); ?> |
| 5240 |
</p> |
| 5241 |
</div> |
| 5242 |
|
| 5243 |
<div class="vigilante-password-reset-options" id="vigilante-reset-role-self-option" style="display: none; margin-top: 10px;"> |
| 5244 |
<label> |
| 5245 |
<input type="checkbox" id="vigilante-reset-role-include-self" value="1"> |
| 5246 |
<?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?> |
| 5247 |
</label> |
| 5248 |
</div> |
| 5249 |
|
| 5250 |
<p class="submit"> |
| 5251 |
<button type="button" id="vigilante-reset-by-role" class="button button-primary" disabled> |
| 5252 |
<?php esc_html_e( 'Force Reset for Selected Roles', 'vigilante' ); ?> |
| 5253 |
</button> |
| 5254 |
</p> |
| 5255 |
</div> |
| 5256 |
|
| 5257 |
<!-- Reset All Users --> |
| 5258 |
<div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;"> |
| 5259 |
<h4><?php esc_html_e( 'Reset all users', 'vigilante' ); ?></h4> |
| 5260 |
|
| 5261 |
<?php |
| 5262 |
$total_users = count_users(); |
| 5263 |
$total_count = $total_users['total_users']; |
| 5264 |
?> |
| 5265 |
<p> |
| 5266 |
<?php |
| 5267 |
printf( |
| 5268 |
/* translators: %d: Number of users */ |
| 5269 |
esc_html__( 'This will affect %d user(s).', 'vigilante' ), |
| 5270 |
absint( $total_count ) |
| 5271 |
); |
| 5272 |
?> |
| 5273 |
</p> |
| 5274 |
|
| 5275 |
<p class="description" style="color: #d63638;"> |
| 5276 |
<span class="dashicons dashicons-warning"></span> |
| 5277 |
<?php esc_html_e( 'Warning: All users will receive a password reset email. On sites with many users, this could overwhelm your mail server.', 'vigilante' ); ?> |
| 5278 |
</p> |
| 5279 |
|
| 5280 |
<div class="vigilante-password-reset-options" style="margin-top: 10px;"> |
| 5281 |
<label> |
| 5282 |
<input type="checkbox" id="vigilante-reset-all-include-self" value="1"> |
| 5283 |
<?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?> |
| 5284 |
</label> |
| 5285 |
</div> |
| 5286 |
|
| 5287 |
<p class="submit"> |
| 5288 |
<button type="button" id="vigilante-reset-all-users" class="button" style="color: #d63638; border-color: #d63638;"> |
| 5289 |
<?php esc_html_e( 'Force Reset for ALL Users', 'vigilante' ); ?> |
| 5290 |
</button> |
| 5291 |
</p> |
| 5292 |
</div> |
| 5293 |
</div> |
| 5294 |
|
| 5295 |
<!-- Pending Registrations --> |
| 5296 |
<?php |
| 5297 |
// Enforcement-only: this instance exists to read the queue, and the |
| 5298 |
// flag keeps it from registering the module's own hooks a second |
| 5299 |
// time. It is not inert, and saying it was would be a false comment: |
| 5300 |
// init_enforcement_hooks() does add its three filters again, on top |
| 5301 |
// of the ones already registered. They are idempotent (the same |
| 5302 |
// methods of an equivalent instance, deciding on the same user meta), |
| 5303 |
// so running them twice in an admin request changes nothing, which is |
| 5304 |
// why this is accepted rather than worked around. |
| 5305 |
$user_security = new Vigilante_User_Security( $this->settings, $this->activity_log, true ); |
| 5306 |
$pending_users = $user_security->get_pending_users(); |
| 5307 |
?> |
| 5308 |
<div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section"> |
| 5309 |
<h3> |
| 5310 |
<?php esc_html_e( 'Pending registrations', 'vigilante' ); ?> |
| 5311 |
<?php if ( count( $pending_users ) > 0 ) : ?> |
| 5312 |
<span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span> |
| 5313 |
<?php endif; ?> |
| 5314 |
</h3> |
| 5315 |
|
| 5316 |
<?php |
| 5317 |
/* |
| 5318 |
* The queue is shown whenever there is somebody in it, even with |
| 5319 |
* the feature off. Since 2.11.10 an account already waiting stays |
| 5320 |
* blocked when the feature is switched off, which is the point: |
| 5321 |
* turning a setting off must not quietly let in people an |
| 5322 |
* administrator decided not to approve. But hiding the table then |
| 5323 |
* left them locked out with no button anywhere to approve or |
| 5324 |
* reject them. Found by the cross review of 2.11.10. |
| 5325 |
*/ |
| 5326 |
?> |
| 5327 |
<?php if ( empty( $registration['enabled'] ) && empty( $pending_users ) ) : ?> |
| 5328 |
<p class="description"> |
| 5329 |
<span class="dashicons dashicons-info" style="color: #72aee6;"></span> |
| 5330 |
<?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?> |
| 5331 |
</p> |
| 5332 |
<?php elseif ( empty( $pending_users ) ) : ?> |
| 5333 |
<div class="vigilante-no-lockouts"> |
| 5334 |
<span class="dashicons dashicons-yes-alt"></span> |
| 5335 |
<p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p> |
| 5336 |
</div> |
| 5337 |
<?php else : ?> |
| 5338 |
<?php $this->render_user_actions_notice(); ?> |
| 5339 |
<table class="wp-list-table widefat fixed striped vigilante-pending-users-table"> |
| 5340 |
<thead> |
| 5341 |
<tr> |
| 5342 |
<th><?php esc_html_e( 'User', 'vigilante' ); ?></th> |
| 5343 |
<th><?php esc_html_e( 'Email', 'vigilante' ); ?></th> |
| 5344 |
<th><?php esc_html_e( 'Registered', 'vigilante' ); ?></th> |
| 5345 |
<th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 5346 |
</tr> |
| 5347 |
</thead> |
| 5348 |
<tbody> |
| 5349 |
<?php foreach ( $pending_users as $pending_user ) : |
| 5350 |
$pending_since = get_user_meta( $pending_user->ID, Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_since' ), true ); |
| 5351 |
?> |
| 5352 |
<tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>"> |
| 5353 |
<td> |
| 5354 |
<?php echo get_avatar( $pending_user->ID, 32 ); ?> |
| 5355 |
<strong><?php echo esc_html( $pending_user->user_login ); ?></strong> |
| 5356 |
</td> |
| 5357 |
<td><?php echo esc_html( $pending_user->user_email ); ?></td> |
| 5358 |
<td> |
| 5359 |
<?php |
| 5360 |
if ( $pending_since ) { |
| 5361 |
/* translators: %s: Time ago */ |
| 5362 |
printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $pending_since ) ) ); |
| 5363 |
} else { |
| 5364 |
echo esc_html( $pending_user->user_registered ); |
| 5365 |
} |
| 5366 |
?> |
| 5367 |
</td> |
| 5368 |
<td> |
| 5369 |
<button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" <?php disabled( $this->user_actions_locked() ); ?>> |
| 5370 |
<?php esc_html_e( 'Approve', 'vigilante' ); ?> |
| 5371 |
</button> |
| 5372 |
<button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;" <?php disabled( $this->user_actions_locked() ); ?>> |
| 5373 |
<?php esc_html_e( 'Reject', 'vigilante' ); ?> |
| 5374 |
</button> |
| 5375 |
</td> |
| 5376 |
</tr> |
| 5377 |
<?php endforeach; ?> |
| 5378 |
</tbody> |
| 5379 |
</table> |
| 5380 |
<?php endif; ?> |
| 5381 |
</div> |
| 5382 |
|
| 5383 |
<!-- Active Sessions Management --> |
| 5384 |
<div class="vigilante-tool-box vigilante-session-management-section"> |
| 5385 |
<h3><?php esc_html_e( 'Active sessions', 'vigilante' ); ?></h3> |
| 5386 |
<p class="description"><?php esc_html_e( 'View and manage active login sessions. You can revoke sessions to force users to log in again.', 'vigilante' ); ?></p> |
| 5387 |
|
| 5388 |
<!-- Current user sessions --> |
| 5389 |
<h4><?php esc_html_e( 'Your sessions', 'vigilante' ); ?></h4> |
| 5390 |
<?php |
| 5391 |
$current_user_id = get_current_user_id(); |
| 5392 |
$my_sessions = $user_security->get_user_sessions( $current_user_id ); |
| 5393 |
$has_corrupted = $user_security->has_corrupted_sessions( $current_user_id ); |
| 5394 |
$raw_count = $user_security->get_raw_session_count( $current_user_id ); |
| 5395 |
?> |
| 5396 |
|
| 5397 |
<?php if ( $has_corrupted && $raw_count > 0 ) : ?> |
| 5398 |
<div class="notice notice-warning inline" style="margin: 10px 0;"> |
| 5399 |
<p> |
| 5400 |
<span class="dashicons dashicons-warning" style="color: #dba617;"></span> |
| 5401 |
<?php esc_html_e( 'Some session data is corrupted and cannot be displayed. Use "Revoke All Other Sessions" to clean up, then log out and log in again to fix this.', 'vigilante' ); ?> |
| 5402 |
</p> |
| 5403 |
</div> |
| 5404 |
<?php endif; ?> |
| 5405 |
|
| 5406 |
<?php if ( empty( $my_sessions ) ) : ?> |
| 5407 |
<p class="description"><?php esc_html_e( 'No active sessions found.', 'vigilante' ); ?></p> |
| 5408 |
<?php if ( $has_corrupted ) : ?> |
| 5409 |
<p style="margin-top: 10px;"> |
| 5410 |
<button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>"> |
| 5411 |
<?php esc_html_e( 'Clean Up Corrupted Sessions', 'vigilante' ); ?> |
| 5412 |
</button> |
| 5413 |
</p> |
| 5414 |
<?php endif; ?> |
| 5415 |
<?php else : ?> |
| 5416 |
<div class="vigilante-paginated-section"> |
| 5417 |
<div class="vigilante-fi-pagination-wrap"></div> |
| 5418 |
<table class="wp-list-table widefat fixed striped vigilante-sessions-table vigilante-fi-paginated"> |
| 5419 |
<thead> |
| 5420 |
<tr> |
| 5421 |
<th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th> |
| 5422 |
<th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th> |
| 5423 |
<th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th> |
| 5424 |
<th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 5425 |
</tr> |
| 5426 |
</thead> |
| 5427 |
<tbody> |
| 5428 |
<?php foreach ( $my_sessions as $session ) : ?> |
| 5429 |
<tr data-token="<?php echo esc_attr( $session['token_hash'] ); ?>"> |
| 5430 |
<td><?php echo esc_html( $session['browser'] ); ?></td> |
| 5431 |
<td><code><?php echo esc_html( $session['ip'] ); ?></code></td> |
| 5432 |
<td> |
| 5433 |
<?php |
| 5434 |
if ( $session['login'] ) { |
| 5435 |
/* translators: %s: Time ago */ |
| 5436 |
printf( esc_html__( '%s ago', 'vigilante' ), esc_html( human_time_diff( $session['login'] ) ) ); |
| 5437 |
} else { |
| 5438 |
esc_html_e( 'Unknown', 'vigilante' ); |
| 5439 |
} |
| 5440 |
?> |
| 5441 |
</td> |
| 5442 |
<td> |
| 5443 |
<?php if ( ! $session['is_current'] ) : ?> |
| 5444 |
<button type="button" class="button button-small vigilante-revoke-session" data-user-id="<?php echo esc_attr( $current_user_id ); ?>" data-token="<?php echo esc_attr( $session['token_hash'] ); ?>"> |
| 5445 |
<?php esc_html_e( 'Revoke', 'vigilante' ); ?> |
| 5446 |
</button> |
| 5447 |
<?php else : ?> |
| 5448 |
<span class="description"><?php esc_html_e( 'Current session', 'vigilante' ); ?></span> |
| 5449 |
<?php endif; ?> |
| 5450 |
</td> |
| 5451 |
</tr> |
| 5452 |
<?php endforeach; ?> |
| 5453 |
</tbody> |
| 5454 |
</table> |
| 5455 |
</div> |
| 5456 |
|
| 5457 |
<?php if ( count( $my_sessions ) > 1 || $has_corrupted ) : ?> |
| 5458 |
<p style="margin-top: 10px;"> |
| 5459 |
<button type="button" class="button vigilante-revoke-other-sessions" data-user-id="<?php echo esc_attr( $current_user_id ); ?>"> |
| 5460 |
<?php esc_html_e( 'Revoke All Other Sessions', 'vigilante' ); ?> |
| 5461 |
</button> |
| 5462 |
</p> |
| 5463 |
<?php endif; ?> |
| 5464 |
<?php endif; ?> |
| 5465 |
|
| 5466 |
<!-- Search user sessions (admin only) --> |
| 5467 |
<h4 style="margin-top: 30px;"><?php esc_html_e( 'Manage user sessions', 'vigilante' ); ?></h4> |
| 5468 |
<p class="description"><?php esc_html_e( 'Search for a user to view and manage their sessions.', 'vigilante' ); ?></p> |
| 5469 |
|
| 5470 |
<div class="vigilante-user-search-wrapper" style="margin-top: 10px;"> |
| 5471 |
<input type="text" id="vigilante-session-user-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search by username or email...', 'vigilante' ); ?>"> |
| 5472 |
<div id="vigilante-session-search-results" class="vigilante-user-search-results" style="display: none;"></div> |
| 5473 |
</div> |
| 5474 |
|
| 5475 |
<div id="vigilante-user-sessions-container" style="display: none; margin-top: 20px;"> |
| 5476 |
<h4 id="vigilante-sessions-user-name"></h4> |
| 5477 |
<table class="wp-list-table widefat fixed striped vigilante-sessions-table"> |
| 5478 |
<thead> |
| 5479 |
<tr> |
| 5480 |
<th><?php esc_html_e( 'Browser', 'vigilante' ); ?></th> |
| 5481 |
<th><?php esc_html_e( 'IP Address', 'vigilante' ); ?></th> |
| 5482 |
<th><?php esc_html_e( 'Login Time', 'vigilante' ); ?></th> |
| 5483 |
<th><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 5484 |
</tr> |
| 5485 |
</thead> |
| 5486 |
<tbody id="vigilante-user-sessions-list"> |
| 5487 |
</tbody> |
| 5488 |
</table> |
| 5489 |
<p style="margin-top: 10px;"> |
| 5490 |
<button type="button" class="button vigilante-revoke-all-user-sessions" style="color: #d63638;"> |
| 5491 |
<?php esc_html_e( 'Revoke All Sessions', 'vigilante' ); ?> |
| 5492 |
</button> |
| 5493 |
</p> |
| 5494 |
</div> |
| 5495 |
</div> |
| 5496 |
|
| 5497 |
<?php endif; ?> |
| 5498 |
</div> |
| 5499 |
<?php |
| 5500 |
} |
| 5501 |
|
| 5502 |
/** |
| 5503 |
* Render WordPress Hardening tab |
| 5504 |
*/ |
| 5505 |
private function render_tab_wp_hardening() { |
| 5506 |
$is_disabled = $this->render_module_disabled_notice( 'wp_hardening' ); |
| 5507 |
$options = $this->settings->get_section( 'wp_hardening' ); |
| 5508 |
?> |
| 5509 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="wp_hardening" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 5510 |
<!-- Database Hardening (outside form save flow - uses its own AJAX action) --> |
| 5511 |
<?php $vg_shared_locked = $this->shared_files_locked(); ?> |
| 5512 |
<?php $this->render_shared_files_notice(); ?> |
| 5513 |
<div id="vigilante-section-hardening-database" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>> |
| 5514 |
<h2> |
| 5515 |
<?php esc_html_e( 'Database Hardening', 'vigilante' ); ?> |
| 5516 |
<span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span> |
| 5517 |
<span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span> |
| 5518 |
</h2> |
| 5519 |
<p><?php esc_html_e( 'Change the database table prefix to prevent SQL injection attacks that target default WordPress tables.', 'vigilante' ); ?></p> |
| 5520 |
|
| 5521 |
<?php |
| 5522 |
$db_prefix = new Vigilante_Database_Prefix(); |
| 5523 |
$current_prefix = $db_prefix->get_current_prefix(); |
| 5524 |
$is_default = $db_prefix->is_default_prefix(); |
| 5525 |
?> |
| 5526 |
|
| 5527 |
<?php if ( is_multisite() && ! $vg_shared_locked ) : ?> |
| 5528 |
<div class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 5529 |
<p style="margin:0;"><?php esc_html_e( 'Network-wide operation: it renames the tables of every site in the network and rewrites the wp-config.php they all share. Back up the whole database first, not just the main site.', 'vigilante' ); ?></p> |
| 5530 |
</div> |
| 5531 |
<?php endif; ?> |
| 5532 |
|
| 5533 |
<table class="form-table"> |
| 5534 |
<tr> |
| 5535 |
<th scope="row"><?php esc_html_e( 'Current prefix', 'vigilante' ); ?></th> |
| 5536 |
<td> |
| 5537 |
<code class="vigilante-db-current-prefix"><?php echo esc_html( $current_prefix ); ?></code> |
| 5538 |
<?php if ( $is_default ) : ?> |
| 5539 |
<span class="vigilante-inline-warning"> |
| 5540 |
<span class="dashicons dashicons-warning"></span> |
| 5541 |
<?php esc_html_e( 'Default prefix detected. Changing it adds a layer of protection against automated SQL injection attacks.', 'vigilante' ); ?> |
| 5542 |
</span> |
| 5543 |
<?php else : ?> |
| 5544 |
<span class="vigilante-inline-ok"> |
| 5545 |
<span class="dashicons dashicons-yes-alt"></span> |
| 5546 |
<?php esc_html_e( 'Custom prefix in use.', 'vigilante' ); ?> |
| 5547 |
</span> |
| 5548 |
<?php endif; ?> |
| 5549 |
</td> |
| 5550 |
</tr> |
| 5551 |
<tr> |
| 5552 |
<th scope="row"><?php esc_html_e( 'New prefix', 'vigilante' ); ?></th> |
| 5553 |
<td> |
| 5554 |
<div class="vigilante-db-prefix-row"> |
| 5555 |
<code class="vigilante-db-new-prefix" id="vigilante-new-prefix"><?php echo esc_html( $db_prefix->generate_prefix() ); ?></code> |
| 5556 |
<button type="button" class="button button-small vigilante-db-regenerate-prefix" title="<?php esc_attr_e( 'Generate new prefix', 'vigilante' ); ?>"> |
| 5557 |
<span class="dashicons dashicons-update"></span> |
| 5558 |
</button> |
| 5559 |
</div> |
| 5560 |
</td> |
| 5561 |
</tr> |
| 5562 |
<tr> |
| 5563 |
<th scope="row"></th> |
| 5564 |
<td> |
| 5565 |
<div class="vigilante-db-prefix-confirm"> |
| 5566 |
<label> |
| 5567 |
<input type="checkbox" id="vigilante-prefix-backup-confirm"> |
| 5568 |
<?php esc_html_e( 'I understand this operation is irreversible and I have a current database backup', 'vigilante' ); ?> |
| 5569 |
</label> |
| 5570 |
<p class="description"> |
| 5571 |
<?php |
| 5572 |
printf( |
| 5573 |
/* translators: %s: Link to tools tab */ |
| 5574 |
esc_html__( 'Need a backup? %s first.', 'vigilante' ), |
| 5575 |
'<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Download a database backup', 'vigilante' ) . '</a>' |
| 5576 |
); |
| 5577 |
?> |
| 5578 |
</p> |
| 5579 |
</div> |
| 5580 |
<button type="button" class="button button-primary vigilante-db-change-prefix" disabled data-original-text="<?php esc_attr_e( 'Change Database Prefix', 'vigilante' ); ?>"> |
| 5581 |
<?php esc_html_e( 'Change Database Prefix', 'vigilante' ); ?> |
| 5582 |
</button> |
| 5583 |
</td> |
| 5584 |
</tr> |
| 5585 |
</table> |
| 5586 |
</div> |
| 5587 |
|
| 5588 |
<!-- wp-config Security --> |
| 5589 |
<?php |
| 5590 |
$vg_shared_locked = $this->shared_files_locked(); |
| 5591 |
// Paint what is actually in force, not this site's unused copy. |
| 5592 |
$vg_local_options = $options; |
| 5593 |
$options = $this->get_section_for_display( 'wp_hardening' ); |
| 5594 |
?> |
| 5595 |
<?php $this->render_shared_files_notice(); ?> |
| 5596 |
<div id="vigilante-section-hardening-wpconfig" class="vigilante-settings-section <?php echo $vg_shared_locked ? 'vigilante-form-disabled' : ''; ?>" <?php echo $vg_shared_locked ? 'inert' : ''; ?>> |
| 5597 |
<h2> |
| 5598 |
<?php esc_html_e( 'wp-config.php Security', 'vigilante' ); ?> |
| 5599 |
<span class="vigilante-method-badge config"><?php esc_html_e( 'WP-CONFIG', 'vigilante' ); ?></span> |
| 5600 |
</h2> |
| 5601 |
<p><?php esc_html_e( 'Security constants added directly to wp-config.php file.', 'vigilante' ); ?></p> |
| 5602 |
|
| 5603 |
<table class="form-table"> |
| 5604 |
<tr> |
| 5605 |
<th scope="row"><?php esc_html_e( 'Disable File Editor', 'vigilante' ); ?></th> |
| 5606 |
<td> |
| 5607 |
<label> |
| 5608 |
<input type="checkbox" name="wp_hardening[disallow_file_edit]" value="1" <?php checked( ! empty( $options['disallow_file_edit'] ) ); ?>> |
| 5609 |
<?php esc_html_e( 'Disable plugin and theme editor in admin (DISALLOW_FILE_EDIT)', 'vigilante' ); ?> |
| 5610 |
</label> |
| 5611 |
</td> |
| 5612 |
</tr> |
| 5613 |
<tr> |
| 5614 |
<th scope="row"><?php esc_html_e( 'Disable File Modifications', 'vigilante' ); ?></th> |
| 5615 |
<td> |
| 5616 |
<label> |
| 5617 |
<input type="checkbox" name="wp_hardening[disallow_file_mods]" value="1" <?php checked( ! empty( $options['disallow_file_mods'] ) ); ?>> |
| 5618 |
<?php esc_html_e( 'Disable all file modifications including updates (DISALLOW_FILE_MODS)', 'vigilante' ); ?> |
| 5619 |
</label> |
| 5620 |
<p class="description"><?php esc_html_e( '⚠ Warning: This prevents automatic updates.', 'vigilante' ); ?></p> |
| 5621 |
</td> |
| 5622 |
</tr> |
| 5623 |
<tr id="field-force-ssl-admin"> |
| 5624 |
<th scope="row"><?php esc_html_e( 'Force SSL Admin', 'vigilante' ); ?></th> |
| 5625 |
<td> |
| 5626 |
<label> |
| 5627 |
<input type="checkbox" name="wp_hardening[force_ssl_admin]" value="1" <?php checked( ! empty( $options['force_ssl_admin'] ) ); ?>> |
| 5628 |
<?php esc_html_e( 'Force HTTPS for admin area (FORCE_SSL_ADMIN)', 'vigilante' ); ?> |
| 5629 |
</label> |
| 5630 |
<p class="description"><?php esc_html_e( '⚠ Warning: Only enable if your site fully supports HTTPS.', 'vigilante' ); ?></p> |
| 5631 |
</td> |
| 5632 |
</tr> |
| 5633 |
<tr id="field-wp-debug"> |
| 5634 |
<th scope="row"><?php esc_html_e( 'Hide PHP errors from visitors', 'vigilante' ); ?></th> |
| 5635 |
<td> |
| 5636 |
<label> |
| 5637 |
<input type="checkbox" name="wp_hardening[wp_debug]" value="1" <?php checked( ! empty( $options['wp_debug'] ) ); ?>> |
| 5638 |
<?php esc_html_e( 'Prevents PHP errors and warnings from being displayed publicly. Also avoids exposing a debug.log file in wp-content/ that could leak paths and code. Uncheck only on development or staging sites.', 'vigilante' ); ?> |
| 5639 |
</label> |
| 5640 |
</td> |
| 5641 |
</tr> |
| 5642 |
<tr id="field-disable-wp-cron"> |
| 5643 |
<th scope="row"><?php esc_html_e( 'Disable WP Cron', 'vigilante' ); ?></th> |
| 5644 |
<td> |
| 5645 |
<label> |
| 5646 |
<input type="checkbox" name="wp_hardening[disable_wp_cron]" value="1" <?php checked( ! empty( $options['disable_wp_cron'] ) ); ?>> |
| 5647 |
<?php esc_html_e( 'Disable WordPress\'s page-view cron trigger (DISABLE_WP_CRON)', 'vigilante' ); ?> |
| 5648 |
</label> |
| 5649 |
<p class="description"><?php |
| 5650 |
printf( |
| 5651 |
/* translators: 1: opening <strong>, 2: closing </strong>, 3: opening <code>, 4: closing </code> */ |
| 5652 |
esc_html__( '%1$sWarning:%2$s Only enable if your host runs a real server-side cron job calling wp-cron.php. Otherwise scheduled tasks stop running. This constant only stops the page-view auto-spawn — to also block external HTTP abuse, enable %3$sProtect wp-cron.php%4$s in Firewall → File Protection.', 'vigilante' ), |
| 5653 |
'<strong>', |
| 5654 |
'</strong>', |
| 5655 |
'<code>', |
| 5656 |
'</code>' |
| 5657 |
); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. |
| 5658 |
?></p> |
| 5659 |
</td> |
| 5660 |
</tr> |
| 5661 |
</table> |
| 5662 |
</div> |
| 5663 |
<?php $options = $vg_local_options; ?> |
| 5664 |
|
| 5665 |
<!-- Comment Security --> |
| 5666 |
<div id="vigilante-section-hardening-xmlrpc" class="vigilante-settings-section"> |
| 5667 |
<h2> |
| 5668 |
<?php esc_html_e( 'XML-RPC', 'vigilante' ); ?> |
| 5669 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5670 |
</h2> |
| 5671 |
<p><?php esc_html_e( 'The legacy remote interface. It is what the WordPress mobile app, Jetpack and remote managers talk to, and also the way pingback amplification and password guessing reach a site.', 'vigilante' ); ?></p> |
| 5672 |
|
| 5673 |
<table class="form-table"> |
| 5674 |
<tr id="field-disable-xmlrpc"> |
| 5675 |
<th scope="row"><label for="vigilante-f-wp-hardening-xmlrpc-mode"><?php esc_html_e( 'XML-RPC access', 'vigilante' ); ?></label></th> |
| 5676 |
<td> |
| 5677 |
<?php $vig_xmlrpc_mode = Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); ?> |
| 5678 |
<select id="vigilante-f-wp-hardening-xmlrpc-mode" name="wp_hardening[xmlrpc_mode]"> |
| 5679 |
<option value="none" <?php selected( $vig_xmlrpc_mode, 'none' ); ?>> |
| 5680 |
<?php esc_html_e( 'Leave XML-RPC enabled', 'vigilante' ); ?> |
| 5681 |
</option> |
| 5682 |
<option value="pingback" <?php selected( $vig_xmlrpc_mode, 'pingback' ); ?>> |
| 5683 |
<?php esc_html_e( 'Block the pingback methods only', 'vigilante' ); ?> |
| 5684 |
</option> |
| 5685 |
<option value="full" <?php selected( $vig_xmlrpc_mode, 'full' ); ?>> |
| 5686 |
<?php esc_html_e( 'Disable XML-RPC completely (recommended)', 'vigilante' ); ?> |
| 5687 |
</option> |
| 5688 |
</select> |
| 5689 |
<p class="description"><?php esc_html_e( 'Disable it completely unless something still needs it, such as the WordPress mobile app, Jetpack or a remote manager; in that case block only the pingback methods, which closes the amplification vector and leaves the rest working. Pingbacks are also covered by the Comment Security setting just below, which additionally closes them for comments.', 'vigilante' ); ?></p> |
| 5690 |
</td> |
| 5691 |
</tr> |
| 5692 |
</table> |
| 5693 |
</div> |
| 5694 |
|
| 5695 |
<div id="vigilante-section-hardening-comments" class="vigilante-settings-section"> |
| 5696 |
<h2> |
| 5697 |
<?php esc_html_e( 'Comment Security', 'vigilante' ); ?> |
| 5698 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5699 |
<span class="vigilante-method-badge settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></span> |
| 5700 |
</h2> |
| 5701 |
<p><?php esc_html_e( 'Comment protection using WordPress settings and PHP hooks.', 'vigilante' ); ?></p> |
| 5702 |
|
| 5703 |
<table class="form-table"> |
| 5704 |
<tr> |
| 5705 |
<th scope="row"><?php esc_html_e( 'Disable Pingbacks', 'vigilante' ); ?></th> |
| 5706 |
<td> |
| 5707 |
<label> |
| 5708 |
<input type="checkbox" name="wp_hardening[disable_pingbacks]" value="1" <?php checked( ! empty( $options['disable_pingbacks'] ) ); ?>> |
| 5709 |
<?php esc_html_e( 'Disable pingbacks (commonly exploited for DDoS)', 'vigilante' ); ?> |
| 5710 |
</label> |
| 5711 |
</td> |
| 5712 |
</tr> |
| 5713 |
<tr> |
| 5714 |
<th scope="row"><?php esc_html_e( 'Disable Trackbacks', 'vigilante' ); ?></th> |
| 5715 |
<td> |
| 5716 |
<label> |
| 5717 |
<input type="checkbox" name="wp_hardening[disable_trackbacks]" value="1" <?php checked( ! empty( $options['disable_trackbacks'] ) ); ?>> |
| 5718 |
<?php esc_html_e( 'Disable trackbacks (rarely used legitimately)', 'vigilante' ); ?> |
| 5719 |
</label> |
| 5720 |
</td> |
| 5721 |
</tr> |
| 5722 |
<tr> |
| 5723 |
<th scope="row"><?php esc_html_e( 'Require Moderation', 'vigilante' ); ?></th> |
| 5724 |
<td> |
| 5725 |
<label> |
| 5726 |
<input type="checkbox" name="wp_hardening[require_comment_moderation]" value="1" <?php checked( ! empty( $options['require_comment_moderation'] ) ); ?>> |
| 5727 |
<?php esc_html_e( 'All comments must be manually approved', 'vigilante' ); ?> |
| 5728 |
</label> |
| 5729 |
</td> |
| 5730 |
</tr> |
| 5731 |
<tr> |
| 5732 |
<th scope="row"><?php esc_html_e( 'Close Old Comments', 'vigilante' ); ?></th> |
| 5733 |
<td> |
| 5734 |
<label> |
| 5735 |
<input type="checkbox" name="wp_hardening[close_old_comments]" value="1" <?php checked( ! empty( $options['close_old_comments'] ) ); ?>> |
| 5736 |
<?php esc_html_e( 'Automatically close comments on old posts after', 'vigilante' ); ?> |
| 5737 |
</label> |
| 5738 |
<input type="number" id="vigilante-f-wp-hardening-close-comments-after-days" name="wp_hardening[close_comments_after_days]" value="<?php echo esc_attr( $options['close_comments_after_days'] ?? 30 ); ?>" min="1" max="365" class="small-text"> |
| 5739 |
<label for="vigilante-f-wp-hardening-close-comments-after-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label> |
| 5740 |
</td> |
| 5741 |
</tr> |
| 5742 |
<tr> |
| 5743 |
<th scope="row"><?php esc_html_e( 'Honeypot Protection', 'vigilante' ); ?></th> |
| 5744 |
<td> |
| 5745 |
<label> |
| 5746 |
<input type="checkbox" name="wp_hardening[honeypot_comments]" value="1" <?php checked( ! empty( $options['honeypot_comments'] ) ); ?>> |
| 5747 |
<?php esc_html_e( 'Add hidden honeypot field to catch bots', 'vigilante' ); ?> |
| 5748 |
</label> |
| 5749 |
</td> |
| 5750 |
</tr> |
| 5751 |
</table> |
| 5752 |
</div> |
| 5753 |
|
| 5754 |
<!-- Head Cleaner --> |
| 5755 |
<div id="vigilante-section-hardening-headers" class="vigilante-settings-section"> |
| 5756 |
<h2> |
| 5757 |
<?php esc_html_e( 'Header Cleanup', 'vigilante' ); ?> |
| 5758 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5759 |
</h2> |
| 5760 |
<p><?php esc_html_e( 'Remove meta tags from HTML head.', 'vigilante' ); ?></p> |
| 5761 |
|
| 5762 |
<table class="form-table"> |
| 5763 |
<tr> |
| 5764 |
<th scope="row"><?php esc_html_e( 'Remove Generator', 'vigilante' ); ?></th> |
| 5765 |
<td> |
| 5766 |
<label> |
| 5767 |
<input type="checkbox" name="wp_hardening[remove_wp_generator]" value="1" <?php checked( ! empty( $options['remove_wp_generator'] ) ); ?>> |
| 5768 |
<?php esc_html_e( 'Remove WordPress version from HTML head', 'vigilante' ); ?> |
| 5769 |
</label> |
| 5770 |
</td> |
| 5771 |
</tr> |
| 5772 |
<tr id="field-remove-wp-version-assets"> |
| 5773 |
<th scope="row"><?php esc_html_e( 'Remove version from assets', 'vigilante' ); ?></th> |
| 5774 |
<td> |
| 5775 |
<label> |
| 5776 |
<input type="checkbox" name="wp_hardening[remove_wp_version_assets]" value="1" <?php checked( ! empty( $options['remove_wp_version_assets'] ) ); ?>> |
| 5777 |
<?php esc_html_e( 'Remove WordPress version from script/style URLs (?ver=)', 'vigilante' ); ?> |
| 5778 |
</label> |
| 5779 |
<p class="description"><?php esc_html_e( 'Hides the exact WordPress version that would otherwise leak in every enqueued asset URL. Versions added by plugins or themes are kept untouched.', 'vigilante' ); ?></p> |
| 5780 |
</td> |
| 5781 |
</tr> |
| 5782 |
<tr> |
| 5783 |
<th scope="row"><?php esc_html_e( 'Remove RSD Link', 'vigilante' ); ?></th> |
| 5784 |
<td> |
| 5785 |
<label> |
| 5786 |
<input type="checkbox" name="wp_hardening[remove_rsd_link]" value="1" <?php checked( ! empty( $options['remove_rsd_link'] ) ); ?>> |
| 5787 |
<?php esc_html_e( 'Remove Really Simple Discovery link', 'vigilante' ); ?> |
| 5788 |
</label> |
| 5789 |
</td> |
| 5790 |
</tr> |
| 5791 |
<tr> |
| 5792 |
<th scope="row"><?php esc_html_e( 'Remove WLW Manifest', 'vigilante' ); ?></th> |
| 5793 |
<td> |
| 5794 |
<label> |
| 5795 |
<input type="checkbox" name="wp_hardening[remove_wlw_manifest]" value="1" <?php checked( ! empty( $options['remove_wlw_manifest'] ) ); ?>> |
| 5796 |
<?php esc_html_e( 'Remove Windows Live Writer manifest link', 'vigilante' ); ?> |
| 5797 |
</label> |
| 5798 |
</td> |
| 5799 |
</tr> |
| 5800 |
<tr> |
| 5801 |
<th scope="row"><?php esc_html_e( 'Remove Shortlink', 'vigilante' ); ?></th> |
| 5802 |
<td> |
| 5803 |
<label> |
| 5804 |
<input type="checkbox" name="wp_hardening[remove_shortlink]" value="1" <?php checked( ! empty( $options['remove_shortlink'] ) ); ?>> |
| 5805 |
<?php esc_html_e( 'Remove shortlink tag from header', 'vigilante' ); ?> |
| 5806 |
</label> |
| 5807 |
</td> |
| 5808 |
</tr> |
| 5809 |
<tr> |
| 5810 |
<th scope="row"><?php esc_html_e( 'Remove REST API Link', 'vigilante' ); ?></th> |
| 5811 |
<td> |
| 5812 |
<label> |
| 5813 |
<input type="checkbox" name="wp_hardening[remove_rest_api_link]" value="1" <?php checked( ! empty( $options['remove_rest_api_link'] ) ); ?>> |
| 5814 |
<?php esc_html_e( 'Remove REST API discovery link from header', 'vigilante' ); ?> |
| 5815 |
</label> |
| 5816 |
<p class="description"><?php esc_html_e( '⚠ Notice: Some plugins may need this link.', 'vigilante' ); ?></p> |
| 5817 |
</td> |
| 5818 |
</tr> |
| 5819 |
</table> |
| 5820 |
</div> |
| 5821 |
|
| 5822 |
<!-- Feed Manager --> |
| 5823 |
<div id="vigilante-section-hardening-rss" class="vigilante-settings-section"> |
| 5824 |
<h2> |
| 5825 |
<?php esc_html_e( 'RSS Feed Settings', 'vigilante' ); ?> |
| 5826 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5827 |
</h2> |
| 5828 |
<p><?php esc_html_e( 'Control RSS/Atom feeds.', 'vigilante' ); ?></p> |
| 5829 |
|
| 5830 |
<table class="form-table"> |
| 5831 |
<tr> |
| 5832 |
<th scope="row"><?php esc_html_e( 'Disable Feeds', 'vigilante' ); ?></th> |
| 5833 |
<td> |
| 5834 |
<label> |
| 5835 |
<input type="checkbox" name="wp_hardening[disable_feeds]" value="1" <?php checked( ! empty( $options['disable_feeds'] ) ); ?>> |
| 5836 |
<?php esc_html_e( 'Completely disable RSS/Atom feeds', 'vigilante' ); ?> |
| 5837 |
</label> |
| 5838 |
</td> |
| 5839 |
</tr> |
| 5840 |
<tr> |
| 5841 |
<th scope="row"><?php esc_html_e( 'Disable If No Content', 'vigilante' ); ?></th> |
| 5842 |
<td> |
| 5843 |
<label> |
| 5844 |
<input type="checkbox" name="wp_hardening[disable_if_no_content]" value="1" <?php checked( ! empty( $options['disable_if_no_content'] ) ); ?>> |
| 5845 |
<?php esc_html_e( 'Only disable feeds if site has no published posts', 'vigilante' ); ?> |
| 5846 |
</label> |
| 5847 |
</td> |
| 5848 |
</tr> |
| 5849 |
<tr> |
| 5850 |
<th scope="row"><?php esc_html_e( 'Remove Feed Version', 'vigilante' ); ?></th> |
| 5851 |
<td> |
| 5852 |
<label> |
| 5853 |
<input type="checkbox" name="wp_hardening[remove_feed_version]" value="1" <?php checked( ! empty( $options['remove_feed_version'] ) ); ?>> |
| 5854 |
<?php esc_html_e( 'Remove WordPress version from feed generator tag', 'vigilante' ); ?> |
| 5855 |
</label> |
| 5856 |
</td> |
| 5857 |
</tr> |
| 5858 |
</table> |
| 5859 |
</div> |
| 5860 |
|
| 5861 |
<p class="submit vigilante-submit-buttons"> |
| 5862 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 5863 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 5864 |
</button> |
| 5865 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 5866 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 5867 |
</button> |
| 5868 |
</p> |
| 5869 |
</form> |
| 5870 |
<?php |
| 5871 |
} |
| 5872 |
|
| 5873 |
/** |
| 5874 |
* Render activity log tab |
| 5875 |
*/ |
| 5876 |
private function render_tab_activity_log() { |
| 5877 |
$is_disabled = $this->render_module_disabled_notice( 'activity_log' ); |
| 5878 |
$options = $this->settings->get_section( 'activity_log' ); |
| 5879 |
?> |
| 5880 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="activity_log" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 5881 |
<div id="vigilante-section-audit-settings" class="vigilante-settings-section"> |
| 5882 |
<h2> |
| 5883 |
<?php esc_html_e( 'Security Audit Settings', 'vigilante' ); ?> |
| 5884 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5885 |
<span class="vigilante-method-badge database"><?php esc_html_e( 'Database', 'vigilante' ); ?></span> |
| 5886 |
</h2> |
| 5887 |
<p><?php esc_html_e( 'Security event logging and auditing.', 'vigilante' ); ?></p> |
| 5888 |
|
| 5889 |
<table class="form-table"> |
| 5890 |
<tr> |
| 5891 |
<th scope="row"><?php esc_html_e( 'Retention', 'vigilante' ); ?></th> |
| 5892 |
<td> |
| 5893 |
<input type="number" id="vigilante-f-activity-log-retention-days" name="activity_log[retention_days]" value="<?php echo esc_attr( $options['retention_days'] ?? 30 ); ?>" min="7" max="365" class="small-text"> |
| 5894 |
<label for="vigilante-f-activity-log-retention-days"><?php esc_html_e( 'days', 'vigilante' ); ?></label> |
| 5895 |
|
| 5896 |
<input type="number" id="vigilante-f-activity-log-max-entries" name="activity_log[max_entries]" value="<?php echo esc_attr( $options['max_entries'] ?? 10000 ); ?>" min="100" max="100000" step="100" class="small-text"> |
| 5897 |
<label for="vigilante-f-activity-log-max-entries"><?php esc_html_e( 'max entries', 'vigilante' ); ?></label> |
| 5898 |
<p class="description"><?php esc_html_e( 'Whichever limit is reached first takes effect. Changes apply immediately on save; daily maintenance also enforces these limits automatically.', 'vigilante' ); ?></p> |
| 5899 |
</td> |
| 5900 |
</tr> |
| 5901 |
<tr> |
| 5902 |
<th scope="row"><?php esc_html_e( 'Events to Log', 'vigilante' ); ?></th> |
| 5903 |
<td> |
| 5904 |
<fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:6px 24px; max-width:600px;"> |
| 5905 |
<label><input type="checkbox" name="activity_log[log_logins]" value="1" <?php checked( ! empty( $options['log_logins'] ) ); ?>> <?php esc_html_e( 'Successful logins', 'vigilante' ); ?></label> |
| 5906 |
<label><input type="checkbox" name="activity_log[log_failed_logins]" value="1" <?php checked( ! empty( $options['log_failed_logins'] ) ); ?>> <?php esc_html_e( 'Failed login attempts', 'vigilante' ); ?></label> |
| 5907 |
<label><input type="checkbox" name="activity_log[log_user_changes]" value="1" <?php checked( ! empty( $options['log_user_changes'] ) ); ?>> <?php esc_html_e( 'User changes', 'vigilante' ); ?></label> |
| 5908 |
<label><input type="checkbox" name="activity_log[log_post_changes]" value="1" <?php checked( ! empty( $options['log_post_changes'] ) ); ?>> <?php esc_html_e( 'Content changes', 'vigilante' ); ?></label> |
| 5909 |
<label><input type="checkbox" name="activity_log[log_plugin_changes]" value="1" <?php checked( ! empty( $options['log_plugin_changes'] ) ); ?>> <?php esc_html_e( 'Plugin changes', 'vigilante' ); ?></label> |
| 5910 |
<label><input type="checkbox" name="activity_log[log_theme_changes]" value="1" <?php checked( ! empty( $options['log_theme_changes'] ) ); ?>> <?php esc_html_e( 'Theme changes', 'vigilante' ); ?></label> |
| 5911 |
<label><input type="checkbox" name="activity_log[log_comments]" value="1" <?php checked( ! empty( $options['log_comments'] ) ); ?>> <?php esc_html_e( 'Comment changes', 'vigilante' ); ?></label> |
| 5912 |
<label><input type="checkbox" name="activity_log[log_media]" value="1" <?php checked( ! empty( $options['log_media'] ) ); ?>> <?php esc_html_e( 'Media uploads/deletions', 'vigilante' ); ?></label> |
| 5913 |
<label><input type="checkbox" name="activity_log[log_file_changes]" value="1" <?php checked( ! empty( $options['log_file_changes'] ) ); ?>> <?php esc_html_e( 'File integrity events', 'vigilante' ); ?></label> |
| 5914 |
<label><input type="checkbox" name="activity_log[log_option_changes]" value="1" <?php checked( ! empty( $options['log_option_changes'] ) ); ?>> <?php esc_html_e( 'WordPress option changes', 'vigilante' ); ?></label> |
| 5915 |
</fieldset> |
| 5916 |
<div class="notice notice-info inline" style="margin:10px 0 0;padding:8px 12px;"> |
| 5917 |
<p style="margin:0;"> |
| 5918 |
<?php esc_html_e( 'Firewall blocks, security events, and Vigilant settings changes are always logged regardless of the above selections.', 'vigilante' ); ?> |
| 5919 |
</p> |
| 5920 |
</div> |
| 5921 |
</td> |
| 5922 |
</tr> |
| 5923 |
<tr> |
| 5924 |
<th scope="row"><label for="vigilante-f-activity-log-tracked-options"><?php esc_html_e( 'Option Tracking', 'vigilante' ); ?></label></th> |
| 5925 |
<td> |
| 5926 |
<p class="description" style="margin-top:0;"><?php esc_html_e( 'When "WordPress option changes" is enabled, Vigilant tracks ~30 core WordPress settings (site URL, admin email, registration, active plugins, theme, comments, privacy, etc.). Use the field below to track additional options from other plugins.', 'vigilante' ); ?></p> |
| 5927 |
<br> |
| 5928 |
<label><?php esc_html_e( 'Additional options to track:', 'vigilante' ); ?></label><br> |
| 5929 |
<textarea id="vigilante-f-activity-log-tracked-options" name="activity_log[tracked_options]" rows="3" cols="50" class="regular-text code" placeholder="woocommerce_ seopress_ wpforms_"><?php echo esc_textarea( implode( "\n", $options['tracked_options'] ?? array() ) ); ?></textarea> |
| 5930 |
<p class="description"><?php esc_html_e( 'One option name per line. Use a trailing underscore to match all options with that prefix (e.g. "woocommerce_" tracks all WooCommerce settings).', 'vigilante' ); ?></p> |
| 5931 |
</td> |
| 5932 |
</tr> |
| 5933 |
<tr> |
| 5934 |
<th scope="row"><?php esc_html_e( 'Exclusions', 'vigilante' ); ?></th> |
| 5935 |
<td> |
| 5936 |
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; max-width:600px;"> |
| 5937 |
<div> |
| 5938 |
<label for="vigilante-f-activity-log-excluded-users"><?php esc_html_e( 'Excluded user IDs:', 'vigilante' ); ?></label><br> |
| 5939 |
<textarea id="vigilante-f-activity-log-excluded-users" name="activity_log[excluded_users]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_users'] ?? array() ) ); ?></textarea> |
| 5940 |
<p class="description"><?php esc_html_e( 'One user ID per line. Actions by these users will not be logged.', 'vigilante' ); ?></p> |
| 5941 |
</div> |
| 5942 |
<div> |
| 5943 |
<label for="vigilante-f-activity-log-excluded-ips"><?php esc_html_e( 'Excluded IPs:', 'vigilante' ); ?></label><br> |
| 5944 |
<textarea id="vigilante-f-activity-log-excluded-ips" name="activity_log[excluded_ips]" rows="3" cols="25"><?php echo esc_textarea( implode( "\n", $options['excluded_ips'] ?? array() ) ); ?></textarea> |
| 5945 |
<p class="description"><?php esc_html_e( 'One IP per line. Requests from these IPs will not be logged.', 'vigilante' ); ?></p> |
| 5946 |
</div> |
| 5947 |
</div> |
| 5948 |
</td> |
| 5949 |
</tr> |
| 5950 |
</table> |
| 5951 |
</div> |
| 5952 |
|
| 5953 |
<p class="submit vigilante-submit-buttons"> |
| 5954 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 5955 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 5956 |
</button> |
| 5957 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 5958 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 5959 |
</button> |
| 5960 |
</p> |
| 5961 |
</form> |
| 5962 |
|
| 5963 |
<?php |
| 5964 |
// Audit Alerts — alerting layer on top of Security Audit. Its own |
| 5965 |
// settings section and form, rendered right after the logging settings |
| 5966 |
// so the tab reads as "log this, exclude that, and alert me about this". |
| 5967 |
$alerts = $this->settings->get_section( 'audit_alerts' ); |
| 5968 |
$immediate = isset( $alerts['immediate'] ) ? $alerts['immediate'] : array(); |
| 5969 |
$threshold = isset( $alerts['threshold'] ) ? $alerts['threshold'] : array(); |
| 5970 |
$alert_severity = isset( $immediate['min_severity'] ) ? $immediate['min_severity'] : 'critical'; |
| 5971 |
$alert_window = isset( $threshold['window'] ) ? $threshold['window'] : '1h'; |
| 5972 |
$threshold_cats = isset( $threshold['categories'] ) ? (array) $threshold['categories'] : array(); |
| 5973 |
$cat_labels = Vigilante_Audit_Alerts::category_labels(); |
| 5974 |
?> |
| 5975 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="audit_alerts" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 5976 |
<div id="vigilante-section-audit-alerts" class="vigilante-settings-section"> |
| 5977 |
<h2> |
| 5978 |
<?php esc_html_e( 'Audit Alerts', 'vigilante' ); ?> |
| 5979 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 5980 |
</h2> |
| 5981 |
<p><?php esc_html_e( 'Get an email when the events above point to something worth your attention. Both alert types are off by default.', 'vigilante' ); ?></p> |
| 5982 |
|
| 5983 |
<table class="form-table"> |
| 5984 |
<tr id="field-audit-alerts-immediate"> |
| 5985 |
<th scope="row"><?php esc_html_e( 'Immediate alerts', 'vigilante' ); ?></th> |
| 5986 |
<td> |
| 5987 |
<label> |
| 5988 |
<input type="checkbox" name="audit_alerts[immediate][enabled]" value="1" <?php checked( ! empty( $immediate['enabled'] ) ); ?>> |
| 5989 |
<?php esc_html_e( 'Email me as soon as a serious event is logged', 'vigilante' ); ?> |
| 5990 |
</label> |
| 5991 |
<p class="description"><?php esc_html_e( 'Sends one email per event type, then waits for the cooldown below before repeating, so a burst of the same event is a single notice.', 'vigilante' ); ?></p> |
| 5992 |
</td> |
| 5993 |
</tr> |
| 5994 |
<tr> |
| 5995 |
<th scope="row"><label for="vigilante-f-audit-alerts-immediate-min-severity"><?php esc_html_e( 'Alert on severity', 'vigilante' ); ?></label></th> |
| 5996 |
<td> |
| 5997 |
<select id="vigilante-f-audit-alerts-immediate-min-severity" name="audit_alerts[immediate][min_severity]"> |
| 5998 |
<option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option> |
| 5999 |
<option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option> |
| 6000 |
</select> |
| 6001 |
<p class="description"><?php esc_html_e( 'A new administrator, a closed plugin or a privilege escalation are all logged as Critical, so "Critical only" already covers them.', 'vigilante' ); ?></p> |
| 6002 |
</td> |
| 6003 |
</tr> |
| 6004 |
<tr id="field-audit-alerts-threshold"> |
| 6005 |
<th scope="row"><?php esc_html_e( 'Threshold alerts', 'vigilante' ); ?></th> |
| 6006 |
<td> |
| 6007 |
<label> |
| 6008 |
<input type="checkbox" name="audit_alerts[threshold][enabled]" value="1" <?php checked( ! empty( $threshold['enabled'] ) ); ?>> |
| 6009 |
<?php esc_html_e( 'Email me when a category spikes within a time window', 'vigilante' ); ?> |
| 6010 |
</label> |
| 6011 |
<p class="description"><?php esc_html_e( 'Catches an attack in progress, e.g. hundreds of firewall blocks or login failures in an hour.', 'vigilante' ); ?></p> |
| 6012 |
</td> |
| 6013 |
</tr> |
| 6014 |
<tr> |
| 6015 |
<th scope="row"><label for="vigilante-f-audit-alerts-threshold-window"><?php esc_html_e( 'Time window', 'vigilante' ); ?></label></th> |
| 6016 |
<td> |
| 6017 |
<select id="vigilante-f-audit-alerts-threshold-window" name="audit_alerts[threshold][window]"> |
| 6018 |
<option value="30m" <?php selected( $alert_window, '30m' ); ?>><?php esc_html_e( '30 minutes', 'vigilante' ); ?></option> |
| 6019 |
<option value="1h" <?php selected( $alert_window, '1h' ); ?>><?php esc_html_e( '1 hour', 'vigilante' ); ?></option> |
| 6020 |
<option value="6h" <?php selected( $alert_window, '6h' ); ?>><?php esc_html_e( '6 hours', 'vigilante' ); ?></option> |
| 6021 |
<option value="24h" <?php selected( $alert_window, '24h' ); ?>><?php esc_html_e( '24 hours', 'vigilante' ); ?></option> |
| 6022 |
</select> |
| 6023 |
<p class="description"><?php esc_html_e( 'How far back Vigilant looks when counting events. For example, "1 hour" means "more than the number below within the last hour".', 'vigilante' ); ?></p> |
| 6024 |
</td> |
| 6025 |
</tr> |
| 6026 |
<tr> |
| 6027 |
<th scope="row"><?php esc_html_e( 'Thresholds per category', 'vigilante' ); ?></th> |
| 6028 |
<td> |
| 6029 |
<fieldset style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:8px 24px; max-width:760px;"> |
| 6030 |
<?php |
| 6031 |
foreach ( $cat_labels as $cat_slug => $cat_label ) : |
| 6032 |
$cat_value = isset( $threshold_cats[ $cat_slug ] ) ? (int) $threshold_cats[ $cat_slug ] : 0; |
| 6033 |
?> |
| 6034 |
<label style="display:flex;align-items:center;gap:8px;justify-content:space-between;"> |
| 6035 |
<span><?php echo esc_html( $cat_label ); ?></span> |
| 6036 |
<input type="number" name="audit_alerts[threshold][categories][<?php echo esc_attr( $cat_slug ); ?>]" value="<?php echo esc_attr( $cat_value ); ?>" min="0" max="100000" step="1" class="small-text"> |
| 6037 |
</label> |
| 6038 |
<?php endforeach; ?> |
| 6039 |
</fieldset> |
| 6040 |
<p class="description"><?php esc_html_e( 'Number of warning/critical events in the window that triggers an alert. 0 disables that category. Routine info-level activity (normal logins, edits) is not counted.', 'vigilante' ); ?></p> |
| 6041 |
</td> |
| 6042 |
</tr> |
| 6043 |
<tr> |
| 6044 |
<th scope="row"><?php esc_html_e( "Don't repeat alerts", 'vigilante' ); ?></th> |
| 6045 |
<td> |
| 6046 |
<input type="number" id="vigilante-f-audit-alerts-cooldown-minutes" name="audit_alerts[cooldown_minutes]" value="<?php echo esc_attr( isset( $alerts['cooldown_minutes'] ) ? (int) $alerts['cooldown_minutes'] : 60 ); ?>" min="0" max="1440" class="small-text"> |
| 6047 |
<label for="vigilante-f-audit-alerts-cooldown-minutes"><?php esc_html_e( 'minutes', 'vigilante' ); ?></label> |
| 6048 |
<p class="description"><?php esc_html_e( 'After an alert, Vigilant waits this long before sending another about the same thing: the same event type for immediate alerts, or the same category for threshold alerts. This prevents a flood during a sustained attack. Applies to both alert types above.', 'vigilante' ); ?></p> |
| 6049 |
</td> |
| 6050 |
</tr> |
| 6051 |
|
| 6052 |
<tr> |
| 6053 |
<th scope="row"><?php esc_html_e( 'Recipients', 'vigilante' ); ?></th> |
| 6054 |
<td> |
| 6055 |
<p class="description" style="margin-top:0;"> |
| 6056 |
<?php |
| 6057 |
printf( |
| 6058 |
/* translators: %s: Link to notification settings */ |
| 6059 |
esc_html__( 'Alerts go to the recipients configured in %s.', 'vigilante' ), |
| 6060 |
'<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>' |
| 6061 |
); |
| 6062 |
?> |
| 6063 |
</p> |
| 6064 |
<p style="margin:8px 0 0;"> |
| 6065 |
<button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>"> |
| 6066 |
<?php esc_html_e( 'Send test email', 'vigilante' ); ?> |
| 6067 |
</button> |
| 6068 |
<span class="vigilante-test-email-result" style="margin-left:8px;"></span> |
| 6069 |
</p> |
| 6070 |
<p class="description"><?php esc_html_e( 'Heads up: some events (a new admin, a closed plugin) already send their own email from other modules. Enabling alerts for them here too may produce two notices until notifications are unified.', 'vigilante' ); ?></p> |
| 6071 |
</td> |
| 6072 |
</tr> |
| 6073 |
</table> |
| 6074 |
</div> |
| 6075 |
|
| 6076 |
<p class="submit vigilante-submit-buttons"> |
| 6077 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 6078 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 6079 |
</button> |
| 6080 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 6081 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 6082 |
</button> |
| 6083 |
</p> |
| 6084 |
</form> |
| 6085 |
|
| 6086 |
<div id="vigilante-section-audit-recent" class="vigilante-settings-section"> |
| 6087 |
<h2><?php esc_html_e( 'Recent Activity', 'vigilante' ); ?></h2> |
| 6088 |
|
| 6089 |
<?php |
| 6090 |
$logs = $this->activity_log->get_logs( array( 'per_page' => 20 ) ); |
| 6091 |
$total_logs = $this->activity_log->get_logs_count(); |
| 6092 |
|
| 6093 |
// Label maps for translated display |
| 6094 |
$type_labels = array( |
| 6095 |
'login' => __( 'Login', 'vigilante' ), |
| 6096 |
'user' => __( 'User', 'vigilante' ), |
| 6097 |
'content' => __( 'Content', 'vigilante' ), |
| 6098 |
'plugin' => __( 'Plugin', 'vigilante' ), |
| 6099 |
'theme' => __( 'Theme', 'vigilante' ), |
| 6100 |
'settings' => __( 'Settings', 'vigilante' ), |
| 6101 |
'comment' => __( 'Comment', 'vigilante' ), |
| 6102 |
'media' => __( 'Media', 'vigilante' ), |
| 6103 |
'firewall' => __( 'Firewall', 'vigilante' ), |
| 6104 |
'file' => __( 'File', 'vigilante' ), |
| 6105 |
'security' => __( 'Security', 'vigilante' ), |
| 6106 |
'system' => __( 'System', 'vigilante' ), |
| 6107 |
); |
| 6108 |
$severity_labels = array( |
| 6109 |
'info' => __( 'Info', 'vigilante' ), |
| 6110 |
'warning' => __( 'Warning', 'vigilante' ), |
| 6111 |
'critical' => __( 'Critical', 'vigilante' ), |
| 6112 |
); |
| 6113 |
|
| 6114 |
$firewall_options = $this->settings->get_section( 'firewall' ); |
| 6115 |
$ip_whitelist = $firewall_options['ip_whitelist'] ?? array(); |
| 6116 |
$ip_blacklist = $firewall_options['ip_blacklist'] ?? array(); |
| 6117 |
$ua_whitelist = $firewall_options['ua_whitelist'] ?? array(); |
| 6118 |
$ua_blacklist = $firewall_options['ua_blacklist'] ?? array(); |
| 6119 |
?> |
| 6120 |
|
| 6121 |
<div class="vigilante-log-filters"> |
| 6122 |
<input type="text" id="vigilante-log-search" aria-label="<?php esc_attr_e( 'Search the activity log', 'vigilante' ); ?>" size="1" placeholder="<?php esc_attr_e( 'Search logs (min. 3 characters)...', 'vigilante' ); ?>" class="vigilante-log-search-input"> |
| 6123 |
<select id="vigilante-log-type-filter" aria-label="<?php esc_attr_e( 'Filter the log by event type', 'vigilante' ); ?>"> |
| 6124 |
<option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option> |
| 6125 |
<option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option> |
| 6126 |
<option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option> |
| 6127 |
<option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option> |
| 6128 |
<option value="plugin"><?php esc_html_e( 'Plugin', 'vigilante' ); ?></option> |
| 6129 |
<option value="theme"><?php esc_html_e( 'Theme', 'vigilante' ); ?></option> |
| 6130 |
<option value="settings"><?php esc_html_e( 'Settings', 'vigilante' ); ?></option> |
| 6131 |
<option value="comment"><?php esc_html_e( 'Comment', 'vigilante' ); ?></option> |
| 6132 |
<option value="media"><?php esc_html_e( 'Media', 'vigilante' ); ?></option> |
| 6133 |
<option value="firewall"><?php esc_html_e( 'Firewall', 'vigilante' ); ?></option> |
| 6134 |
<option value="file"><?php esc_html_e( 'File', 'vigilante' ); ?></option> |
| 6135 |
<option value="security"><?php esc_html_e( 'Security', 'vigilante' ); ?></option> |
| 6136 |
<option value="system"><?php esc_html_e( 'System', 'vigilante' ); ?></option> |
| 6137 |
</select> |
| 6138 |
<select id="vigilante-log-severity-filter" aria-label="<?php esc_attr_e( 'Filter the log by severity', 'vigilante' ); ?>"> |
| 6139 |
<option value=""><?php esc_html_e( 'All Severities', 'vigilante' ); ?></option> |
| 6140 |
<option value="info"><?php esc_html_e( 'Info', 'vigilante' ); ?></option> |
| 6141 |
<option value="warning"><?php esc_html_e( 'Warning', 'vigilante' ); ?></option> |
| 6142 |
<option value="critical"><?php esc_html_e( 'Critical', 'vigilante' ); ?></option> |
| 6143 |
</select> |
| 6144 |
<select id="vigilante-log-method-filter" aria-label="<?php esc_attr_e( 'Filter the log by HTTP method', 'vigilante' ); ?>"> |
| 6145 |
<option value=""><?php esc_html_e( 'All Methods', 'vigilante' ); ?></option> |
| 6146 |
<option value="GET">GET</option> |
| 6147 |
<option value="POST">POST</option> |
| 6148 |
<option value="PUT">PUT</option> |
| 6149 |
<option value="DELETE">DELETE</option> |
| 6150 |
<option value="PATCH">PATCH</option> |
| 6151 |
<option value="OPTIONS">OPTIONS</option> |
| 6152 |
<option value="HEAD">HEAD</option> |
| 6153 |
</select> |
| 6154 |
<button type="button" id="vigilante-log-refresh" class="button"><?php esc_html_e( 'Refresh', 'vigilante' ); ?></button> |
| 6155 |
<span class="vigilante-pagination" id="vigilante-log-pagination" data-total="<?php echo esc_attr( $total_logs ); ?>" data-per-page="20" data-page="1"> |
| 6156 |
<?php if ( $total_logs > 20 ) : ?> |
| 6157 |
<button type="button" class="vigilante-page-first" title="<?php esc_attr_e( 'First page', 'vigilante' ); ?>" disabled>«</button> |
| 6158 |
<button type="button" class="vigilante-page-prev" title="<?php esc_attr_e( 'Previous page', 'vigilante' ); ?>" disabled>‹</button> |
| 6159 |
<?php endif; ?> |
| 6160 |
<span class="vigilante-page-info"> |
| 6161 |
<?php |
| 6162 |
$showing = min( 20, $total_logs ); |
| 6163 |
printf( |
| 6164 |
/* translators: 1: first item, 2: last item, 3: total items */ |
| 6165 |
esc_html__( '%1$d–%2$d of %3$d', 'vigilante' ), |
| 6166 |
$total_logs > 0 ? 1 : 0, |
| 6167 |
absint( $showing ), |
| 6168 |
absint( $total_logs ) |
| 6169 |
); |
| 6170 |
?> |
| 6171 |
</span> |
| 6172 |
<?php if ( $total_logs > 20 ) : ?> |
| 6173 |
<button type="button" class="vigilante-page-next" title="<?php esc_attr_e( 'Next page', 'vigilante' ); ?>">›</button> |
| 6174 |
<button type="button" class="vigilante-page-last" title="<?php esc_attr_e( 'Last page', 'vigilante' ); ?>">»</button> |
| 6175 |
<?php endif; ?> |
| 6176 |
</span> |
| 6177 |
</div> |
| 6178 |
|
| 6179 |
<div class="vigilante-log-table-wrap"> |
| 6180 |
<table id="vigilante-activity-log-table" class="wp-list-table widefat striped"> |
| 6181 |
<thead> |
| 6182 |
<tr> |
| 6183 |
<th class="column-date"><?php esc_html_e( 'Date', 'vigilante' ); ?></th> |
| 6184 |
<th class="column-type"><?php esc_html_e( 'Type', 'vigilante' ); ?></th> |
| 6185 |
<th class="column-method"><?php esc_html_e( 'Method', 'vigilante' ); ?></th> |
| 6186 |
<th class="column-severity"><?php esc_html_e( 'Severity', 'vigilante' ); ?></th> |
| 6187 |
<th class="column-message"><?php esc_html_e( 'Message', 'vigilante' ); ?></th> |
| 6188 |
<th class="column-user"><?php esc_html_e( 'User', 'vigilante' ); ?></th> |
| 6189 |
<th class="column-ip"><?php esc_html_e( 'IP', 'vigilante' ); ?></th> |
| 6190 |
<th class="column-details"><?php esc_html_e( 'Details', 'vigilante' ); ?></th> |
| 6191 |
</tr> |
| 6192 |
</thead> |
| 6193 |
<tbody> |
| 6194 |
<?php |
| 6195 |
if ( empty( $logs ) ) : |
| 6196 |
?> |
| 6197 |
<tr><td colspan="8"><?php esc_html_e( 'No log entries found.', 'vigilante' ); ?></td></tr> |
| 6198 |
<?php else : ?> |
| 6199 |
<?php foreach ( $logs as $log ) : |
| 6200 |
$request_method = isset( $log->request_method ) ? $log->request_method : ''; |
| 6201 |
// Prepare details as a simple object |
| 6202 |
$ip_val = (string) ( $log->ip_address ?? '' ); |
| 6203 |
$ua_val = (string) ( $log->user_agent ?? '' ); |
| 6204 |
$details = array( |
| 6205 |
'id' => (int) $log->id, |
| 6206 |
'type' => (string) ( $log->event_type ?? '' ), |
| 6207 |
'action' => (string) ( $log->event_action ?? '' ), |
| 6208 |
'message' => (string) ( $log->event_message ?? '' ), |
| 6209 |
'user' => (string) ( $log->user_login ?? '' ), |
| 6210 |
'ip' => $ip_val, |
| 6211 |
'user_agent' => $ua_val, |
| 6212 |
'request_method' => (string) $request_method, |
| 6213 |
'request_uri' => Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' ), |
| 6214 |
'date' => (string) ( $log->created_at ?? '' ), |
| 6215 |
'severity' => (string) ( $log->severity ?? 'info' ), |
| 6216 |
'is_ip_whitelisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) ), |
| 6217 |
'is_ip_blacklisted' => ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) ), |
| 6218 |
'is_ua_whitelisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) ), |
| 6219 |
'is_ua_blacklisted' => ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) ), |
| 6220 |
); |
| 6221 |
$display_type = isset( $type_labels[ $log->event_type ] ) ? $type_labels[ $log->event_type ] : $log->event_type; |
| 6222 |
$display_severity = isset( $severity_labels[ $log->severity ] ) ? $severity_labels[ $log->severity ] : $log->severity; |
| 6223 |
?> |
| 6224 |
<tr class="vigilante-severity-<?php echo esc_attr( $log->severity ); ?>"> |
| 6225 |
<td><?php echo esc_html( $log->created_at ); ?></td> |
| 6226 |
<td><?php echo esc_html( $display_type ); ?></td> |
| 6227 |
<td><?php if ( ! empty( $request_method ) ) : ?><span class="vigilante-method-label vigilante-method-<?php echo esc_attr( strtolower( $request_method ) ); ?>"><?php echo esc_html( $request_method ); ?></span><?php else : ?>-<?php endif; ?></td> |
| 6228 |
<td><span class="vigilante-badge vigilante-badge-<?php echo esc_attr( $log->severity ); ?>"><?php echo esc_html( $display_severity ); ?></span></td> |
| 6229 |
<td><?php echo esc_html( $log->event_message ); ?></td> |
| 6230 |
<td><?php echo esc_html( $log->user_login ?? '-' ); ?></td> |
| 6231 |
<td><code><?php echo esc_html( $log->ip_address ); ?></code></td> |
| 6232 |
<td> |
| 6233 |
<button type="button" class="button button-small vigilante-view-log-details" |
| 6234 |
data-details='<?php echo esc_attr( wp_json_encode( $details, JSON_HEX_APOS | JSON_HEX_QUOT ) ); ?>'> |
| 6235 |
<?php esc_html_e( 'View', 'vigilante' ); ?> |
| 6236 |
</button> |
| 6237 |
</td> |
| 6238 |
</tr> |
| 6239 |
<?php endforeach; ?> |
| 6240 |
<?php endif; ?> |
| 6241 |
</tbody> |
| 6242 |
</table> |
| 6243 |
</div> |
| 6244 |
|
| 6245 |
<!-- Log Details Modal --> |
| 6246 |
<div id="vigilante-log-details-modal" class="vigilante-modal" style="display: none;"> |
| 6247 |
<div class="vigilante-modal-content"> |
| 6248 |
<span class="vigilante-modal-close">×</span> |
| 6249 |
<h3><?php esc_html_e( 'Log Entry Details', 'vigilante' ); ?></h3> |
| 6250 |
<div id="vigilante-log-details-content"></div> |
| 6251 |
</div> |
| 6252 |
</div> |
| 6253 |
|
| 6254 |
<p> |
| 6255 |
<button type="button" class="button vigilante-export-logs"><?php esc_html_e( 'Export Audit Log', 'vigilante' ); ?></button> |
| 6256 |
<button type="button" class="button vigilante-clear-logs" style="color: #a00;"><?php esc_html_e( 'Clear All Logs', 'vigilante' ); ?></button> |
| 6257 |
</p> |
| 6258 |
</div> |
| 6259 |
<?php |
| 6260 |
} |
| 6261 |
|
| 6262 |
/** |
| 6263 |
* Render File Integrity tab |
| 6264 |
*/ |
| 6265 |
private function render_tab_file_integrity() { |
| 6266 |
$is_disabled = $this->render_module_disabled_notice( 'file_integrity' ); |
| 6267 |
$options = $this->settings->get_section( 'file_integrity' ); |
| 6268 |
// On the main site of a network the critical-file scan is the network's |
| 6269 |
// canary for a change to wp-config.php or the root .htaccess, so a |
| 6270 |
// main-site admin without network rights cannot turn it off. Since |
| 6271 |
// 2.11.8; see Vigilante_Settings::get_main_site_file_settings(). |
| 6272 |
$vg_main_locked = $this->main_site_files_locked(); |
| 6273 |
$last_scan = get_option( 'vigilante_last_integrity_scan' ); |
| 6274 |
$last_results = get_option( 'vigilante_last_integrity_results' ); |
| 6275 |
$ignored_files = get_option( 'vigilante_ignored_files', array() ); |
| 6276 |
|
| 6277 |
// Backward compat: convert old notify_on_changes to notify_level |
| 6278 |
$notify_level = $options['notify_level'] ?? ''; |
| 6279 |
if ( empty( $notify_level ) ) { |
| 6280 |
$notify_level = ! empty( $options['notify_on_changes'] ) ? 'all' : 'disabled'; |
| 6281 |
} |
| 6282 |
|
| 6283 |
// Closed + Removed plugins data. Surfaced inside Last Scan Results so the |
| 6284 |
// user sees file findings and plugin closures together (same tier of risk, |
| 6285 |
// same UI), and as the trigger to keep Last Scan Results open even when no |
| 6286 |
// file scan has run yet (the daily cron may have populated this section). |
| 6287 |
// |
| 6288 |
// Gating by last_check_time > 0 is how "Clear Previous Results" visually |
| 6289 |
// resets this block: the option vigilante_plugin_status_last_check is |
| 6290 |
// deleted on Clear, but the state map and the ignored list survive so the |
| 6291 |
// next scan reconstructs without degrading a 'removed' slug. While |
| 6292 |
// last_check is 0, we treat the plugin_status data as if it didn't exist. |
| 6293 |
if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { |
| 6294 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 6295 |
} |
| 6296 |
$closed_checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); |
| 6297 |
$closed_last_check = $closed_checker->get_last_check_time(); |
| 6298 |
if ( $closed_last_check > 0 ) { |
| 6299 |
$closed_plugins = $closed_checker->get_closed_plugins(); |
| 6300 |
$ignored_closed_plugins = $closed_checker->get_ignored_closed_plugins(); |
| 6301 |
} else { |
| 6302 |
$closed_plugins = array(); |
| 6303 |
$ignored_closed_plugins = array(); |
| 6304 |
} |
| 6305 |
$has_closed = ! empty( $closed_plugins ); |
| 6306 |
$datetime_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' ); |
| 6307 |
?> |
| 6308 |
<form class="vigilante-settings-form <?php echo $is_disabled ? 'vigilante-form-disabled' : ''; ?>" data-section="file_integrity" <?php echo $is_disabled ? 'inert' : ''; ?>> |
| 6309 |
<div id="vigilante-section-fi-monitoring" class="vigilante-settings-section"> |
| 6310 |
<h2> |
| 6311 |
<?php esc_html_e( 'File Integrity Monitoring', 'vigilante' ); ?> |
| 6312 |
<span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 6313 |
</h2> |
| 6314 |
<p><?php esc_html_e( 'Detects file modifications using WordPress.org checksums.', 'vigilante' ); ?></p> |
| 6315 |
|
| 6316 |
<table class="form-table"> |
| 6317 |
<tr> |
| 6318 |
<th scope="row"><?php esc_html_e( 'Automatic Scans', 'vigilante' ); ?></th> |
| 6319 |
<td> |
| 6320 |
<label> |
| 6321 |
<input type="checkbox" name="file_integrity[auto_scan]" value="1" <?php checked( ! empty( $options['auto_scan'] ) ); ?>> |
| 6322 |
<?php esc_html_e( 'Enable scheduled file integrity scans', 'vigilante' ); ?> |
| 6323 |
</label> |
| 6324 |
</td> |
| 6325 |
</tr> |
| 6326 |
<tr> |
| 6327 |
<th scope="row"><label for="vigilante-f-file-integrity-scan-frequency"><?php esc_html_e( 'Scan Frequency', 'vigilante' ); ?></label></th> |
| 6328 |
<td> |
| 6329 |
<select id="vigilante-f-file-integrity-scan-frequency" name="file_integrity[scan_frequency]"> |
| 6330 |
<option value="daily" <?php selected( $options['scan_frequency'] ?? 'daily', 'daily' ); ?>><?php esc_html_e( 'Daily', 'vigilante' ); ?></option> |
| 6331 |
<option value="weekly" <?php selected( $options['scan_frequency'] ?? 'daily', 'weekly' ); ?>><?php esc_html_e( 'Weekly', 'vigilante' ); ?></option> |
| 6332 |
</select> |
| 6333 |
</td> |
| 6334 |
</tr> |
| 6335 |
<tr> |
| 6336 |
<th scope="row"><label for="vigilante-f-file-integrity-notify-level"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></label></th> |
| 6337 |
<td> |
| 6338 |
<select id="vigilante-f-file-integrity-notify-level" name="file_integrity[notify_level]"> |
| 6339 |
<option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option> |
| 6340 |
<option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option> |
| 6341 |
<option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option> |
| 6342 |
</select> |
| 6343 |
<p class="description"><?php esc_html_e( '"Suspicious files only" reduces noise by skipping modified file notifications. Recommended for most sites.', 'vigilante' ); ?></p> |
| 6344 |
</td> |
| 6345 |
</tr> |
| 6346 |
<tr> |
| 6347 |
<th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th> |
| 6348 |
<td> |
| 6349 |
<label> |
| 6350 |
<input type="checkbox" name="file_integrity[instant_alert]" value="1" <?php checked( ! empty( $options['instant_alert'] ) ); ?>> |
| 6351 |
<?php esc_html_e( 'Send immediate alert when modified, suspicious or additional files are detected, or when a closed plugin is found', 'vigilante' ); ?> |
| 6352 |
</label> |
| 6353 |
<p class="description"><?php esc_html_e( 'Fires even if the Email Notifications setting above is set to Disabled.', 'vigilante' ); ?></p> |
| 6354 |
<p class="description"> |
| 6355 |
<?php |
| 6356 |
printf( |
| 6357 |
/* translators: %s: Link to notification settings */ |
| 6358 |
esc_html__( 'ⓘ Notifications are sent to the recipients configured in %s.', 'vigilante' ), |
| 6359 |
'<a href="' . esc_url( admin_url( 'admin.php?page=vigilante&tab=tools' ) ) . '">' . esc_html__( 'Settings & Tools', 'vigilante' ) . '</a>' |
| 6360 |
); |
| 6361 |
?> |
| 6362 |
</p> |
| 6363 |
</td> |
| 6364 |
</tr> |
| 6365 |
<tr> |
| 6366 |
<th scope="row"><?php esc_html_e( 'Test email', 'vigilante' ); ?></th> |
| 6367 |
<td> |
| 6368 |
<button type="button" class="button vigilante-test-email-btn" data-original-text="<?php esc_attr_e( 'Send test email', 'vigilante' ); ?>"> |
| 6369 |
<?php esc_html_e( 'Send test email', 'vigilante' ); ?> |
| 6370 |
</button> |
| 6371 |
<span class="vigilante-test-email-result" style="margin-left:8px;"></span> |
| 6372 |
<p class="description"><?php esc_html_e( 'Sends a test message to the configured recipients to confirm email delivery works.', 'vigilante' ); ?></p> |
| 6373 |
</td> |
| 6374 |
</tr> |
| 6375 |
<tr> |
| 6376 |
<th scope="row"><?php esc_html_e( 'Scan Scope', 'vigilante' ); ?></th> |
| 6377 |
<td> |
| 6378 |
<fieldset> |
| 6379 |
<label> |
| 6380 |
<input type="checkbox" name="file_integrity[scan_core]" value="1" <?php checked( $options['scan_core'] ?? true ); ?>> |
| 6381 |
<?php esc_html_e( 'Core files (compare against WordPress.org checksums)', 'vigilante' ); ?> |
| 6382 |
</label> |
| 6383 |
<br> |
| 6384 |
<label> |
| 6385 |
<input type="checkbox" name="file_integrity[scan_plugins]" value="1" <?php checked( $options['scan_plugins'] ?? true ); ?>> |
| 6386 |
<?php esc_html_e( 'Plugins (WordPress.org repository plugins)', 'vigilante' ); ?> |
| 6387 |
</label> |
| 6388 |
<br> |
| 6389 |
<label> |
| 6390 |
<input type="checkbox" name="file_integrity[scan_themes]" value="1" <?php checked( $options['scan_themes'] ?? true ); ?>> |
| 6391 |
<?php esc_html_e( 'Themes (WordPress.org repository themes)', 'vigilante' ); ?> |
| 6392 |
</label> |
| 6393 |
<br> |
| 6394 |
<label> |
| 6395 |
<input type="checkbox" name="file_integrity[scan_uploads]" value="1" <?php checked( $options['scan_uploads'] ?? true ); ?>> |
| 6396 |
<?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?> |
| 6397 |
</label> |
| 6398 |
<br> |
| 6399 |
<label> |
| 6400 |
<input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>> |
| 6401 |
<?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?> |
| 6402 |
<?php if ( $vg_main_locked ) : ?> |
| 6403 |
<span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span> |
| 6404 |
<?php endif; ?> |
| 6405 |
</label> |
| 6406 |
<br> |
| 6407 |
<label> |
| 6408 |
<input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>> |
| 6409 |
<?php esc_html_e( 'Closed plugins (daily check against the WordPress.org repository)', 'vigilante' ); ?> |
| 6410 |
</label> |
| 6411 |
</fieldset> |
| 6412 |
</td> |
| 6413 |
</tr> |
| 6414 |
<tr> |
| 6415 |
<th scope="row"><label for="vigilante-f-file-integrity-excluded-paths"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></label></th> |
| 6416 |
<td> |
| 6417 |
<textarea id="vigilante-f-file-integrity-excluded-paths" name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/cache wp-content/languages"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea> |
| 6418 |
<p class="description"><?php esc_html_e( 'One path per line, relative to the WordPress root. A path such as wp-content/cache excludes exactly that folder and everything under it. A name on its own, such as cache, excludes any folder called exactly that, wherever it is.', 'vigilante' ); ?></p> |
| 6419 |
</td> |
| 6420 |
</tr> |
| 6421 |
<tr> |
| 6422 |
<th scope="row"><label for="vigilante-f-file-integrity-excluded-extensions"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></label></th> |
| 6423 |
<td> |
| 6424 |
<textarea id="vigilante-f-file-integrity-excluded-extensions" name="file_integrity[excluded_extensions]" rows="3" class="large-text code" placeholder=".log .po .mo .pot"><?php echo esc_textarea( implode( "\n", $options['excluded_extensions'] ?? array() ) ); ?></textarea> |
| 6425 |
<p class="description"> |
| 6426 |
<?php esc_html_e( 'One extension per line (e.g. .log, .po, .mo). Files with these extensions will be skipped. Useful to avoid false positives from translation or log files.', 'vigilante' ); ?> |
| 6427 |
<br> |
| 6428 |
<?php |
| 6429 |
printf( |
| 6430 |
/* translators: 1: opening <code>, 2: closing </code>. Placeholders wrap the scoped-extension example. */ |
| 6431 |
esc_html__( 'An extension on its own applies to the whole site. To limit it to one folder, write it as %1$swp-content/languages/*.json%2$s, which leaves the same extension watched everywhere else.', 'vigilante' ), |
| 6432 |
'<code>', |
| 6433 |
'</code>' |
| 6434 |
); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML tags are hardcoded. |
| 6435 |
?> |
| 6436 |
</p> |
| 6437 |
</td> |
| 6438 |
</tr> |
| 6439 |
</table> |
| 6440 |
</div> |
| 6441 |
|
| 6442 |
<p class="submit vigilante-submit-buttons"> |
| 6443 |
<button type="submit" class="button button-primary vigilante-save-btn" data-original-text="<?php esc_attr_e( 'Save Settings', 'vigilante' ); ?>"> |
| 6444 |
<?php esc_html_e( 'Save Settings', 'vigilante' ); ?> |
| 6445 |
</button> |
| 6446 |
<button type="button" class="button vigilante-reset-section-btn" data-original-text="<?php esc_attr_e( 'Reset to Defaults', 'vigilante' ); ?>"> |
| 6447 |
<?php esc_html_e( 'Reset to Defaults', 'vigilante' ); ?> |
| 6448 |
</button> |
| 6449 |
<span class="vigilante-buttons-separator"></span> |
| 6450 |
<button type="button" class="button button-primary vigilante-run-scan"> |
| 6451 |
<?php esc_html_e( 'Run Scan Now', 'vigilante' ); ?> |
| 6452 |
</button> |
| 6453 |
<button type="button" class="button vigilante-clear-scan-btn vigilante-clear-scan"> |
| 6454 |
<?php esc_html_e( 'Clear Previous Results', 'vigilante' ); ?> |
| 6455 |
</button> |
| 6456 |
</p> |
| 6457 |
</form> |
| 6458 |
|
| 6459 |
<div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div> |
| 6460 |
|
| 6461 |
<?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?> |
| 6462 |
<div id="vigilante-section-fi-last-scan" class="vigilante-settings-section"> |
| 6463 |
<h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2> |
| 6464 |
<?php if ( $last_scan ) : ?> |
| 6465 |
<p> |
| 6466 |
<?php |
| 6467 |
// Build the "X files scanned" hint inline with the date so it doesn't |
| 6468 |
// need its own stat box (keeps the row compact when closed plugins are |
| 6469 |
// present). |
| 6470 |
$scanned_total = 0; |
| 6471 |
if ( $last_results ) { |
| 6472 |
$scanned_total = (int) ( $last_results['ok'] ?? 0 ) |
| 6473 |
+ count( $last_results['modified'] ?? array() ) |
| 6474 |
+ count( $last_results['suspicious'] ?? array() ) |
| 6475 |
+ count( $last_results['extra'] ?? array() ) |
| 6476 |
+ count( $ignored_files ); |
| 6477 |
} |
| 6478 |
if ( $scanned_total > 0 ) { |
| 6479 |
printf( |
| 6480 |
/* translators: 1: date and time of last scan, 2: formatted file count */ |
| 6481 |
esc_html__( 'Last scan: %1$s (%2$s files scanned)', 'vigilante' ), |
| 6482 |
esc_html( wp_date( $datetime_format, $last_scan ) ), |
| 6483 |
esc_html( number_format_i18n( $scanned_total ) ) |
| 6484 |
); |
| 6485 |
} else { |
| 6486 |
printf( |
| 6487 |
/* translators: %s: date and time of last scan */ |
| 6488 |
esc_html__( 'Last scan: %s', 'vigilante' ), |
| 6489 |
esc_html( wp_date( $datetime_format, $last_scan ) ) |
| 6490 |
); |
| 6491 |
} |
| 6492 |
if ( $closed_last_check > 0 && $closed_last_check !== (int) $last_scan ) { |
| 6493 |
echo ' · '; |
| 6494 |
printf( |
| 6495 |
/* translators: %s: date and time of last closed plugins check */ |
| 6496 |
esc_html__( 'Closed plugins last checked: %s', 'vigilante' ), |
| 6497 |
esc_html( wp_date( $datetime_format, $closed_last_check ) ) |
| 6498 |
); |
| 6499 |
} |
| 6500 |
?> |
| 6501 |
</p> |
| 6502 |
<?php elseif ( $closed_last_check > 0 ) : ?> |
| 6503 |
<p> |
| 6504 |
<?php |
| 6505 |
printf( |
| 6506 |
/* translators: %s: date and time of last closed plugins check */ |
| 6507 |
esc_html__( 'Closed plugins last checked: %s · the daily cron is running, no full integrity scan yet.', 'vigilante' ), |
| 6508 |
esc_html( wp_date( $datetime_format, $closed_last_check ) ) |
| 6509 |
); |
| 6510 |
?> |
| 6511 |
</p> |
| 6512 |
<?php endif; ?> |
| 6513 |
<div id="vigilante-last-scan-results"> |
| 6514 |
<?php if ( $last_results || $has_closed ) : ?> |
| 6515 |
<div class="vigilante-scan-summary"> |
| 6516 |
<?php if ( $last_results ) : ?> |
| 6517 |
<div class="vigilante-scan-stat vigilante-stat-ok"> |
| 6518 |
<span class="vigilante-stat-number"><?php echo esc_html( $last_results['ok'] ?? 0 ); ?></span> |
| 6519 |
<span class="vigilante-stat-label"><?php esc_html_e( 'OK', 'vigilante' ); ?></span> |
| 6520 |
</div> |
| 6521 |
<div class="vigilante-scan-stat vigilante-stat-modified"> |
| 6522 |
<span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['modified'] ?? array() ) ); ?></span> |
| 6523 |
<span class="vigilante-stat-label"><?php esc_html_e( 'Modified', 'vigilante' ); ?></span> |
| 6524 |
</div> |
| 6525 |
<div class="vigilante-scan-stat vigilante-stat-suspicious"> |
| 6526 |
<span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['suspicious'] ?? array() ) ); ?></span> |
| 6527 |
<span class="vigilante-stat-label"><?php esc_html_e( 'Suspicious', 'vigilante' ); ?></span> |
| 6528 |
</div> |
| 6529 |
<div class="vigilante-scan-stat vigilante-stat-extra"> |
| 6530 |
<span class="vigilante-stat-number"><?php echo esc_html( count( $last_results['extra'] ?? array() ) ); ?></span> |
| 6531 |
<span class="vigilante-stat-label"><?php esc_html_e( 'Extra', 'vigilante' ); ?></span> |
| 6532 |
</div> |
| 6533 |
<?php endif; ?> |
| 6534 |
<?php if ( $has_closed ) : ?> |
| 6535 |
<div class="vigilante-scan-stat vigilante-stat-suspicious"> |
| 6536 |
<span class="vigilante-stat-number" style="color: #d63638;"><?php echo (int) count( $closed_plugins ); ?></span> |
| 6537 |
<span class="vigilante-stat-label"><?php esc_html_e( 'Closed/Removed', 'vigilante' ); ?></span> |
| 6538 |
</div> |
| 6539 |
<?php endif; ?> |
| 6540 |
<?php if ( ! empty( $ignored_files ) ) : ?> |
| 6541 |
<div class="vigilante-scan-stat vigilante-stat-ignored"> |
| 6542 |
<span class="vigilante-stat-number"><?php echo esc_html( count( $ignored_files ) ); ?></span> |
| 6543 |
<span class="vigilante-stat-label"><?php esc_html_e( 'Ignored', 'vigilante' ); ?></span> |
| 6544 |
</div> |
| 6545 |
<?php endif; ?> |
| 6546 |
</div> |
| 6547 |
|
| 6548 |
<?php if ( ! empty( $last_results['suspicious'] ) ) : ?> |
| 6549 |
<div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore"> |
| 6550 |
<h3 style="color: #d63638;"><?php esc_html_e( 'Suspicious Files', 'vigilante' ); ?></h3> |
| 6551 |
<p class="description" style="color: #d63638;"><?php esc_html_e( '⚠ Warning: These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ); ?></p> |
| 6552 |
<div class="vigilante-fi-bulk-bar"> |
| 6553 |
<button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 6554 |
<span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 6555 |
</div> |
| 6556 |
<div class="vigilante-fi-pagination-wrap"></div> |
| 6557 |
<table class="wp-list-table widefat fixed striped vigilante-fi-paginated"> |
| 6558 |
<thead> |
| 6559 |
<tr> |
| 6560 |
<td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td> |
| 6561 |
<th><?php esc_html_e( 'File', 'vigilante' ); ?></th> |
| 6562 |
<th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th> |
| 6563 |
<th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th> |
| 6564 |
<th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6565 |
</tr> |
| 6566 |
</thead> |
| 6567 |
<tbody> |
| 6568 |
<?php |
| 6569 |
foreach ( $last_results['suspicious'] as $item ) { |
| 6570 |
$file_path = ''; |
| 6571 |
$file_reason = __( 'Unknown', 'vigilante' ); |
| 6572 |
$file_type = 'unknown'; |
| 6573 |
|
| 6574 |
if ( is_array( $item ) ) { |
| 6575 |
if ( isset( $item['file'] ) ) { |
| 6576 |
$file_path = $item['file']; |
| 6577 |
} |
| 6578 |
if ( isset( $item['reason'] ) ) { |
| 6579 |
$file_reason = $item['reason']; |
| 6580 |
} |
| 6581 |
if ( isset( $item['type'] ) ) { |
| 6582 |
$file_type = $item['type']; |
| 6583 |
} |
| 6584 |
} else { |
| 6585 |
$file_path = (string) $item; |
| 6586 |
} |
| 6587 |
?> |
| 6588 |
<tr> |
| 6589 |
<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| 6590 |
<td><code style="color: #d63638;"><?php echo esc_html( $file_path ); ?></code></td> |
| 6591 |
<td><?php echo esc_html( $file_reason ); ?></td> |
| 6592 |
<td><?php echo esc_html( $file_type ); ?></td> |
| 6593 |
<td><button type="button" class="button button-small vigilante-ignore-file" data-file="<?php echo esc_attr( $file_path ); ?>"><?php esc_html_e( 'Ignore', 'vigilante' ); ?></button></td> |
| 6594 |
</tr> |
| 6595 |
<?php |
| 6596 |
} |
| 6597 |
?> |
| 6598 |
</tbody> |
| 6599 |
</table> |
| 6600 |
</div> |
| 6601 |
<?php endif; ?> |
| 6602 |
|
| 6603 |
<?php if ( ! empty( $last_results['extra'] ) ) : ?> |
| 6604 |
<div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore"> |
| 6605 |
<h3 style="color: #b32d2e;"><?php esc_html_e( 'Extra Files', 'vigilante' ); ?></h3> |
| 6606 |
<p class="description"><?php esc_html_e( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org. May be legitimate customizations or injected backdoors.', 'vigilante' ); ?></p> |
| 6607 |
<div class="vigilante-fi-bulk-bar"> |
| 6608 |
<button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 6609 |
<span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 6610 |
</div> |
| 6611 |
<div class="vigilante-fi-pagination-wrap"></div> |
| 6612 |
<table class="wp-list-table widefat fixed striped vigilante-fi-paginated"> |
| 6613 |
<thead> |
| 6614 |
<tr> |
| 6615 |
<td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td> |
| 6616 |
<th><?php esc_html_e( 'File', 'vigilante' ); ?></th> |
| 6617 |
<th style="width: 250px;"><?php esc_html_e( 'Reason', 'vigilante' ); ?></th> |
| 6618 |
<th style="width: 120px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th> |
| 6619 |
<th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6620 |
</tr> |
| 6621 |
</thead> |
| 6622 |
<tbody> |
| 6623 |
<?php |
| 6624 |
foreach ( $last_results['extra'] as $item ) { |
| 6625 |
$file_path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item; |
| 6626 |
$file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' ); |
| 6627 |
$file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown'; |
| 6628 |
?> |
| 6629 |
<tr> |
| 6630 |
<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| 6631 |
<td><code style="color: #b32d2e;"><?php echo esc_html( $file_path ); ?></code></td> |
| 6632 |
<td><?php echo esc_html( $file_reason ); ?></td> |
| 6633 |
<td><?php echo esc_html( $file_type ); ?></td> |
| 6634 |
<td><button type="button" class="button button-small vigilante-ignore-file" data-file="<?php echo esc_attr( $file_path ); ?>"><?php esc_html_e( 'Ignore', 'vigilante' ); ?></button></td> |
| 6635 |
</tr> |
| 6636 |
<?php |
| 6637 |
} |
| 6638 |
?> |
| 6639 |
</tbody> |
| 6640 |
</table> |
| 6641 |
</div> |
| 6642 |
<?php endif; ?> |
| 6643 |
|
| 6644 |
<?php |
| 6645 |
// Split critical config files from regular modified files. |
| 6646 |
// Computed unconditionally so the three sub-sections that consume |
| 6647 |
// these arrays (Critical Config, Closed + Removed, Modified Files) |
| 6648 |
// can render independently and in the order the team picked. |
| 6649 |
$critical_modified = array(); |
| 6650 |
$regular_modified = array(); |
| 6651 |
if ( $last_results && ! empty( $last_results['modified'] ) ) { |
| 6652 |
foreach ( $last_results['modified'] as $item ) { |
| 6653 |
if ( is_array( $item ) && isset( $item['type'] ) && 'critical_config' === $item['type'] ) { |
| 6654 |
$critical_modified[] = $item; |
| 6655 |
} else { |
| 6656 |
$regular_modified[] = $item; |
| 6657 |
} |
| 6658 |
} |
| 6659 |
} |
| 6660 |
?> |
| 6661 |
|
| 6662 |
<?php if ( ! empty( $critical_modified ) ) : ?> |
| 6663 |
<div class="vigilante-file-list vigilante-critical-config-files"> |
| 6664 |
<h3 style="color: #e36210;"><?php esc_html_e( 'Critical config files modified', 'vigilante' ); ?></h3> |
| 6665 |
<p class="description"> |
| 6666 |
<?php esc_html_e( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ); ?> |
| 6667 |
</p> |
| 6668 |
<table class="wp-list-table widefat fixed striped"> |
| 6669 |
<thead> |
| 6670 |
<tr> |
| 6671 |
<th><?php esc_html_e( 'File', 'vigilante' ); ?></th> |
| 6672 |
<th style="width: 200px;"><?php esc_html_e( 'Changes', 'vigilante' ); ?></th> |
| 6673 |
<th style="width: 220px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6674 |
</tr> |
| 6675 |
</thead> |
| 6676 |
<tbody> |
| 6677 |
<?php foreach ( $critical_modified as $crit_item ) : |
| 6678 |
$crit_file = $crit_item['file'] ?? ''; |
| 6679 |
$crit_baseline_size = $crit_item['baseline_size'] ?? 0; |
| 6680 |
$crit_current_size = $crit_item['current_size'] ?? 0; |
| 6681 |
$crit_diff = $crit_item['diff'] ?? array(); |
| 6682 |
$crit_id = sanitize_html_class( $crit_file ); |
| 6683 |
$added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0; |
| 6684 |
$removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0; |
| 6685 |
// The lines of a shared file are for whoever approves it. Results |
| 6686 |
// stored before 2.11.8 on the main site still carry them, so the |
| 6687 |
// screen asks too, not only the scan that wrote them. |
| 6688 |
$diff_network = ( is_array( $crit_diff ) && ! empty( $crit_diff['network'] ) ) || $this->critical_approval_locked(); |
| 6689 |
$diff_rescan = is_array( $crit_diff ) && ! empty( $crit_diff['rescan'] ); |
| 6690 |
$diff_redaction = is_array( $crit_diff ) && ! empty( $crit_diff['redaction'] ); |
| 6691 |
$diff_unavailable = $diff_network || ( is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ) ); |
| 6692 |
?> |
| 6693 |
<tr> |
| 6694 |
<td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td> |
| 6695 |
<td> |
| 6696 |
<?php if ( ! $diff_unavailable ) : ?> |
| 6697 |
<span style="color: #007017;">+<?php echo (int) $added_count; ?></span> |
| 6698 |
<span style="color: #b32d2e;">-<?php echo (int) $removed_count; ?></span> |
| 6699 |
<?php esc_html_e( 'lines', 'vigilante' ); ?><br> |
| 6700 |
<?php endif; ?> |
| 6701 |
<small style="color: #50575e;"> |
| 6702 |
<?php |
| 6703 |
printf( |
| 6704 |
/* translators: 1: baseline size, 2: current size */ |
| 6705 |
esc_html__( '%1$s → %2$s bytes', 'vigilante' ), |
| 6706 |
esc_html( number_format_i18n( $crit_baseline_size ) ), |
| 6707 |
esc_html( number_format_i18n( $crit_current_size ) ) |
| 6708 |
); |
| 6709 |
?> |
| 6710 |
</small> |
| 6711 |
</td> |
| 6712 |
<td> |
| 6713 |
<button type="button" class="button button-small vigilante-toggle-critical-content" data-target="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" data-label-show="<?php esc_attr_e( 'Review changes', 'vigilante' ); ?>" data-label-hide="<?php esc_attr_e( 'Hide changes', 'vigilante' ); ?>"> |
| 6714 |
<?php esc_html_e( 'Review changes', 'vigilante' ); ?> |
| 6715 |
</button> |
| 6716 |
<?php if ( $this->critical_approval_locked() ) : ?> |
| 6717 |
<span class="description" style="display:block;margin-top:4px;"> |
| 6718 |
<?php echo esc_html( $this->critical_approval_notice() ); ?> |
| 6719 |
</span> |
| 6720 |
<?php else : ?> |
| 6721 |
<button type="button" class="button button-small button-primary vigilante-approve-critical-file" data-file="<?php echo esc_attr( $crit_file ); ?>"> |
| 6722 |
<?php esc_html_e( 'Approve', 'vigilante' ); ?> |
| 6723 |
</button> |
| 6724 |
<?php endif; ?> |
| 6725 |
</td> |
| 6726 |
</tr> |
| 6727 |
<tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;"> |
| 6728 |
<td colspan="3" style="padding: 0;"> |
| 6729 |
<div class="vigilante-critical-content" style="max-height: 400px; overflow: auto; background: #fff; padding: 10px; font-size: 12px; line-height: 1.5; font-family: Consolas, Monaco, monospace; border-top: 1px solid #c3c4c7;"> |
| 6730 |
<?php if ( $diff_network ) : ?> |
| 6731 |
<p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6732 |
<?php esc_html_e( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ); ?> |
| 6733 |
</p> |
| 6734 |
<?php elseif ( $diff_rescan ) : ?> |
| 6735 |
<p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6736 |
<?php esc_html_e( 'Run a new scan to see the line changes of this file.', 'vigilante' ); ?> |
| 6737 |
</p> |
| 6738 |
<?php elseif ( $diff_redaction ) : ?> |
| 6739 |
<p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6740 |
<?php esc_html_e( 'The line changes of this file are not shown because a value in it could not be hidden safely. The change itself is still detected.', 'vigilante' ); ?> |
| 6741 |
</p> |
| 6742 |
<?php elseif ( $diff_unavailable ) : ?> |
| 6743 |
<p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6744 |
<?php esc_html_e( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ); ?> |
| 6745 |
</p> |
| 6746 |
<?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?> |
| 6747 |
<p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6748 |
<?php esc_html_e( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ); ?> |
| 6749 |
</p> |
| 6750 |
<?php else : ?> |
| 6751 |
<?php if ( ! empty( $crit_diff['removed'] ) ) : ?> |
| 6752 |
<?php foreach ( $crit_diff['removed'] as $rline ) : ?> |
| 6753 |
<div style="background: #fbeaea; color: #b32d2e; padding: 1px 4px; white-space: pre-wrap; word-wrap: break-word;"><span style="display: inline-block; width: 50px; color: #999; user-select: none;"><?php echo (int) $rline['line']; ?></span>- <?php echo esc_html( $rline['content'] ); ?></div> |
| 6754 |
<?php endforeach; ?> |
| 6755 |
<?php endif; ?> |
| 6756 |
<?php if ( ! empty( $crit_diff['added'] ) ) : ?> |
| 6757 |
<?php foreach ( $crit_diff['added'] as $aline ) : ?> |
| 6758 |
<div style="background: #e6f4e9; color: #007017; padding: 1px 4px; white-space: pre-wrap; word-wrap: break-word;"><span style="display: inline-block; width: 50px; color: #999; user-select: none;"><?php echo (int) $aline['line']; ?></span>+ <?php echo esc_html( $aline['content'] ); ?></div> |
| 6759 |
<?php endforeach; ?> |
| 6760 |
<?php endif; ?> |
| 6761 |
<?php endif; ?> |
| 6762 |
</div> |
| 6763 |
</td> |
| 6764 |
</tr> |
| 6765 |
<?php endforeach; ?> |
| 6766 |
</tbody> |
| 6767 |
</table> |
| 6768 |
</div> |
| 6769 |
<?php endif; ?> |
| 6770 |
|
| 6771 |
<?php if ( $has_closed ) : ?> |
| 6772 |
<div class="vigilante-file-list vigilante-closed-plugins"> |
| 6773 |
<h3 id="vigilante-section-fi-closed-plugins" style="color: #d63638;"><?php esc_html_e( 'Closed + Removed Plugins', 'vigilante' ); ?></h3> |
| 6774 |
<p class="description" style="color: #d63638;"> |
| 6775 |
<?php esc_html_e( '⚠ Warning: These plugins have been closed in the WordPress.org repository. Closures usually indicate malware, security issues, guideline violations, or supply chain attacks. Uninstall and replace as soon as possible.', 'vigilante' ); ?> |
| 6776 |
</p> |
| 6777 |
<table class="wp-list-table widefat striped"> |
| 6778 |
<thead> |
| 6779 |
<tr> |
| 6780 |
<th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th> |
| 6781 |
<th style="width: 70px;"><?php esc_html_e( 'Version', 'vigilante' ); ?></th> |
| 6782 |
<th style="width: 90px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th> |
| 6783 |
<th style="width: 110px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th> |
| 6784 |
<th><?php esc_html_e( 'Reason', 'vigilante' ); ?></th> |
| 6785 |
<th style="width: 130px;"><?php esc_html_e( 'Detected', 'vigilante' ); ?></th> |
| 6786 |
<th style="width: 90px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6787 |
</tr> |
| 6788 |
</thead> |
| 6789 |
<tbody> |
| 6790 |
<?php foreach ( $closed_plugins as $cp_slug => $cp_entry ) : |
| 6791 |
$cp_state = $cp_entry['state'] ?? ''; |
| 6792 |
$cp_state_label = 'closed' === $cp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' ); |
| 6793 |
$cp_state_color = 'closed' === $cp_state ? '#d63638' : '#b32d2e'; |
| 6794 |
$cp_reason = ''; |
| 6795 |
if ( ! empty( $cp_entry['closed_reason_text'] ) ) { |
| 6796 |
$cp_reason = $cp_entry['closed_reason_text']; |
| 6797 |
} elseif ( 'removed' === $cp_state ) { |
| 6798 |
$cp_reason = __( 'Removed from repository (metadata hidden, typical of Security Issue closures)', 'vigilante' ); |
| 6799 |
} |
| 6800 |
$cp_detected = isset( $cp_entry['first_detected'] ) ? (int) $cp_entry['first_detected'] : 0; |
| 6801 |
?> |
| 6802 |
<tr> |
| 6803 |
<td> |
| 6804 |
<strong><?php echo esc_html( $cp_entry['name'] ?? $cp_slug ); ?></strong><br> |
| 6805 |
<a href="<?php echo esc_url( 'https://wordpress.org/plugins/' . $cp_slug . '/' ); ?>" target="_blank" rel="noopener noreferrer"><code style="color: #50575e;"><?php echo esc_html( $cp_slug ); ?></code></a> |
| 6806 |
</td> |
| 6807 |
<td><?php echo esc_html( $cp_entry['version'] ?? '' ); ?></td> |
| 6808 |
<td><span style="color: <?php echo esc_attr( $cp_state_color ); ?>; font-weight: 600;"><?php echo esc_html( $cp_state_label ); ?></span></td> |
| 6809 |
<td><?php echo esc_html( $cp_entry['closed_date'] ?? '' ); ?></td> |
| 6810 |
<td><?php echo esc_html( $cp_reason ); ?></td> |
| 6811 |
<td> |
| 6812 |
<?php echo $cp_detected > 0 ? esc_html( wp_date( $datetime_format, $cp_detected ) ) : '—'; ?> |
| 6813 |
</td> |
| 6814 |
<td> |
| 6815 |
<button type="button" class="button button-small vigilante-ignore-closed-plugin" data-slug="<?php echo esc_attr( $cp_slug ); ?>"> |
| 6816 |
<?php esc_html_e( 'Ignore', 'vigilante' ); ?> |
| 6817 |
</button> |
| 6818 |
</td> |
| 6819 |
</tr> |
| 6820 |
<?php endforeach; ?> |
| 6821 |
</tbody> |
| 6822 |
</table> |
| 6823 |
</div> |
| 6824 |
<?php endif; ?> |
| 6825 |
|
| 6826 |
<?php if ( ! empty( $regular_modified ) ) : ?> |
| 6827 |
<div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore"> |
| 6828 |
<h3><?php esc_html_e( 'Modified Files', 'vigilante' ); ?></h3> |
| 6829 |
<p class="description"><?php esc_html_e( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ); ?></p> |
| 6830 |
<div class="vigilante-fi-bulk-bar"> |
| 6831 |
<button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 6832 |
<span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 6833 |
</div> |
| 6834 |
<div class="vigilante-fi-pagination-wrap"></div> |
| 6835 |
<table class="wp-list-table widefat fixed striped vigilante-fi-paginated"> |
| 6836 |
<thead> |
| 6837 |
<tr> |
| 6838 |
<td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td> |
| 6839 |
<th><?php esc_html_e( 'File', 'vigilante' ); ?></th> |
| 6840 |
<th style="width: 100px;"><?php esc_html_e( 'Type', 'vigilante' ); ?></th> |
| 6841 |
<th style="width: 80px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6842 |
</tr> |
| 6843 |
</thead> |
| 6844 |
<tbody> |
| 6845 |
<?php |
| 6846 |
foreach ( $regular_modified as $item ) { |
| 6847 |
$file_path = ''; |
| 6848 |
$file_type = 'unknown'; |
| 6849 |
|
| 6850 |
if ( is_array( $item ) ) { |
| 6851 |
if ( isset( $item['file'] ) ) { |
| 6852 |
$file_path = $item['file']; |
| 6853 |
} |
| 6854 |
if ( isset( $item['type'] ) ) { |
| 6855 |
$file_type = $item['type']; |
| 6856 |
} |
| 6857 |
} else { |
| 6858 |
$file_path = (string) $item; |
| 6859 |
} |
| 6860 |
?> |
| 6861 |
<tr> |
| 6862 |
<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| 6863 |
<td><code><?php echo esc_html( $file_path ); ?></code></td> |
| 6864 |
<td><?php echo esc_html( $file_type ); ?></td> |
| 6865 |
<td><button type="button" class="button button-small vigilante-ignore-file" data-file="<?php echo esc_attr( $file_path ); ?>"><?php esc_html_e( 'Ignore', 'vigilante' ); ?></button></td> |
| 6866 |
</tr> |
| 6867 |
<?php |
| 6868 |
} |
| 6869 |
?> |
| 6870 |
</tbody> |
| 6871 |
</table> |
| 6872 |
</div> |
| 6873 |
<?php endif; ?> |
| 6874 |
|
| 6875 |
<?php if ( $last_results && empty( $last_results['modified'] ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?> |
| 6876 |
<p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;"> |
| 6877 |
<?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?> |
| 6878 |
</p> |
| 6879 |
<?php endif; ?> |
| 6880 |
<?php endif; ?> |
| 6881 |
</div> |
| 6882 |
</div> |
| 6883 |
<?php endif; ?> |
| 6884 |
|
| 6885 |
<?php if ( ! empty( $ignored_closed_plugins ) ) : ?> |
| 6886 |
<div id="vigilante-section-fi-ignored-closed" class="vigilante-settings-section"> |
| 6887 |
<h2><?php esc_html_e( 'Ignored Closed + Removed Plugins', 'vigilante' ); ?></h2> |
| 6888 |
<p class="description"><?php esc_html_e( 'These plugins remain closed/removed in WordPress.org but you have chosen to hide them from the main list and from email alerts. They are still installed on the site and still running their code — the silencing is purely cosmetic.', 'vigilante' ); ?></p> |
| 6889 |
<table class="wp-list-table widefat fixed striped"> |
| 6890 |
<thead> |
| 6891 |
<tr> |
| 6892 |
<th><?php esc_html_e( 'Plugin', 'vigilante' ); ?></th> |
| 6893 |
<th style="width: 110px;"><?php esc_html_e( 'State', 'vigilante' ); ?></th> |
| 6894 |
<th style="width: 120px;"><?php esc_html_e( 'Closed date', 'vigilante' ); ?></th> |
| 6895 |
<th style="width: 130px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6896 |
</tr> |
| 6897 |
</thead> |
| 6898 |
<tbody> |
| 6899 |
<?php foreach ( $ignored_closed_plugins as $icp_slug => $icp_entry ) : |
| 6900 |
$icp_state = $icp_entry['state'] ?? ''; |
| 6901 |
$icp_state_label = 'closed' === $icp_state ? __( 'Closed', 'vigilante' ) : __( 'Removed', 'vigilante' ); |
| 6902 |
?> |
| 6903 |
<tr> |
| 6904 |
<td> |
| 6905 |
<strong><?php echo esc_html( $icp_entry['name'] ?? $icp_slug ); ?></strong><br> |
| 6906 |
<a href="<?php echo esc_url( 'https://wordpress.org/plugins/' . $icp_slug . '/' ); ?>" target="_blank" rel="noopener noreferrer"><code style="color: #50575e;"><?php echo esc_html( $icp_slug ); ?></code></a> |
| 6907 |
</td> |
| 6908 |
<td><?php echo esc_html( $icp_state_label ); ?></td> |
| 6909 |
<td><?php echo esc_html( $icp_entry['closed_date'] ?? '' ); ?></td> |
| 6910 |
<td> |
| 6911 |
<button type="button" class="button button-small vigilante-unignore-closed-plugin" data-slug="<?php echo esc_attr( $icp_slug ); ?>"> |
| 6912 |
<?php esc_html_e( 'Stop ignoring', 'vigilante' ); ?> |
| 6913 |
</button> |
| 6914 |
</td> |
| 6915 |
</tr> |
| 6916 |
<?php endforeach; ?> |
| 6917 |
</tbody> |
| 6918 |
</table> |
| 6919 |
<p style="margin-top: 10px;"> |
| 6920 |
<button type="button" class="button vigilante-clear-ignored-closed-plugins"><?php esc_html_e( 'Clear All Ignored Closed + Removed Plugins', 'vigilante' ); ?></button> |
| 6921 |
</p> |
| 6922 |
</div> |
| 6923 |
<?php endif; ?> |
| 6924 |
|
| 6925 |
<?php if ( ! empty( $ignored_files ) ) : ?> |
| 6926 |
<div id="vigilante-section-fi-ignored" class="vigilante-settings-section"> |
| 6927 |
<h2><?php esc_html_e( 'Ignored Files', 'vigilante' ); ?></h2> |
| 6928 |
<p class="description"><?php esc_html_e( 'These files are excluded from scan results and email notifications. They will still be scanned but any findings will be hidden.', 'vigilante' ); ?></p> |
| 6929 |
<div class="vigilante-paginated-section" data-bulk-mode="unignore"> |
| 6930 |
<div class="vigilante-fi-bulk-bar"> |
| 6931 |
<button type="button" class="button vigilante-bulk-unignore" disabled><?php esc_html_e( 'Stop ignoring selected', 'vigilante' ); ?></button> |
| 6932 |
<span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 6933 |
</div> |
| 6934 |
<div class="vigilante-fi-pagination-wrap"></div> |
| 6935 |
<table class="wp-list-table widefat fixed striped vigilante-fi-paginated"> |
| 6936 |
<thead> |
| 6937 |
<tr> |
| 6938 |
<td class="manage-column column-cb check-column"><input type="checkbox" class="vigilante-fi-cb-all" aria-label="<?php esc_attr_e( 'Select all', 'vigilante' ); ?>"></td> |
| 6939 |
<th><?php esc_html_e( 'File', 'vigilante' ); ?></th> |
| 6940 |
<th style="width: 120px;"><?php esc_html_e( 'Actions', 'vigilante' ); ?></th> |
| 6941 |
</tr> |
| 6942 |
</thead> |
| 6943 |
<tbody> |
| 6944 |
<?php foreach ( $ignored_files as $file ) : ?> |
| 6945 |
<tr> |
| 6946 |
<th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file ); ?>"></th> |
| 6947 |
<td><code><?php echo esc_html( $file ); ?></code></td> |
| 6948 |
<td><button type="button" class="button button-small vigilante-unignore-file" data-file="<?php echo esc_attr( $file ); ?>"><?php esc_html_e( 'Stop ignoring', 'vigilante' ); ?></button></td> |
| 6949 |
</tr> |
| 6950 |
<?php endforeach; ?> |
| 6951 |
</tbody> |
| 6952 |
</table> |
| 6953 |
</div> |
| 6954 |
<p style="margin-top: 10px;"> |
| 6955 |
<button type="button" class="button vigilante-clear-ignored"><?php esc_html_e( 'Clear All Ignored Files', 'vigilante' ); ?></button> |
| 6956 |
</p> |
| 6957 |
</div> |
| 6958 |
<?php endif; ?> |
| 6959 |
<?php |
| 6960 |
} |
| 6961 |
|
| 6962 |
/** |
| 6963 |
* Render sidebar with promotional widgets |
| 6964 |
*/ |
| 6965 |
private function render_sidebar() { |
| 6966 |
$promo_banner = new Vigilante_Promo_Banner( 'vigilante' ); |
| 6967 |
$promo_banner->render(); |
| 6968 |
} |
| 6969 |
|
| 6970 |
/** |
| 6971 |
* AJAX: Download a ZIP backup of the critical config files. |
| 6972 |
* |
| 6973 |
* Streams wp-config.php and .htaccess (and robots.txt if present) as a |
| 6974 |
* downloadable archive. Config backups are no longer left as files under the |
| 6975 |
* web root, so this hands the admin the archive directly. |
| 6976 |
*/ |
| 6977 |
public function ajax_download_files_backup() { |
| 6978 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 6979 |
|
| 6980 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 6981 |
wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 ); |
| 6982 |
} |
| 6983 |
|
| 6984 |
// The archive carries wp-config.php, which a whole network shares. On a |
| 6985 |
// network manage_options is held by every subsite administrator, so the |
| 6986 |
// same gate the writers use applies here. |
| 6987 |
if ( ! Vigilante_Settings::can_write_shared_files() ) { |
| 6988 |
wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 ); |
| 6989 |
} |
| 6990 |
|
| 6991 |
$backup_manager = new Vigilante_Backup_Manager(); |
| 6992 |
$result = $backup_manager->stream_files_zip(); |
| 6993 |
|
| 6994 |
// stream_files_zip() exits on success; only a WP_Error returns here. |
| 6995 |
if ( is_wp_error( $result ) ) { |
| 6996 |
wp_die( esc_html( $result->get_error_message() ), 500 ); |
| 6997 |
} |
| 6998 |
} |
| 6999 |
|
| 7000 |
/** |
| 7001 |
* AJAX: Save settings |
| 7002 |
*/ |
| 7003 |
public function ajax_save_settings() { |
| 7004 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7005 |
|
| 7006 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7007 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7008 |
} |
| 7009 |
|
| 7010 |
$section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : ''; |
| 7011 |
|
| 7012 |
// Handle $_POST['data'] based on type |
| 7013 |
if ( isset( $_POST['data'] ) && is_array( $_POST['data'] ) ) { |
| 7014 |
$data = map_deep( wp_unslash( $_POST['data'] ), 'sanitize_text_field' ); |
| 7015 |
} elseif ( isset( $_POST['data'] ) ) { |
| 7016 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 7017 |
$raw_data = wp_unslash( $_POST['data'] ); |
| 7018 |
parse_str( $raw_data, $data ); |
| 7019 |
$data = map_deep( $data, 'sanitize_textarea_field' ); |
| 7020 |
} else { |
| 7021 |
$data = array(); |
| 7022 |
} |
| 7023 |
|
| 7024 |
if ( empty( $section ) ) { |
| 7025 |
wp_send_json_error( __( 'Invalid section.', 'vigilante' ) ); |
| 7026 |
} |
| 7027 |
|
| 7028 |
// Check if 2FA is being enabled or method changed (for notification sending) |
| 7029 |
$send_2fa_notification = false; |
| 7030 |
$send_login_url_notification = false; |
| 7031 |
|
| 7032 |
if ( 'login_security' === $section ) { |
| 7033 |
// Read old state from DB |
| 7034 |
$db_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 7035 |
$old_2fa_enabled = ! empty( $db_options['login_security']['two_factor']['enabled'] ); |
| 7036 |
$old_method = $db_options['login_security']['two_factor']['method'] ?? 'email'; |
| 7037 |
|
| 7038 |
// Check new state from submitted data |
| 7039 |
$new_2fa_enabled = false; |
| 7040 |
$notify_on_enable = false; |
| 7041 |
$new_method = isset( $data['login_security']['two_factor']['method'] ) |
| 7042 |
? sanitize_key( $data['login_security']['two_factor']['method'] ) |
| 7043 |
: 'email'; |
| 7044 |
|
| 7045 |
if ( isset( $data['login_security']['two_factor']['enabled'] ) ) { |
| 7046 |
$new_2fa_enabled = filter_var( $data['login_security']['two_factor']['enabled'], FILTER_VALIDATE_BOOLEAN ); |
| 7047 |
} |
| 7048 |
if ( isset( $data['login_security']['two_factor']['notify_on_enable'] ) ) { |
| 7049 |
$notify_on_enable = filter_var( $data['login_security']['two_factor']['notify_on_enable'], FILTER_VALIDATE_BOOLEAN ); |
| 7050 |
} |
| 7051 |
|
| 7052 |
// Send notification if: |
| 7053 |
// 1. 2FA is being enabled (was off, now on) OR |
| 7054 |
// 2. Method changed while 2FA is enabled |
| 7055 |
if ( $notify_on_enable && $new_2fa_enabled ) { |
| 7056 |
if ( ! $old_2fa_enabled || ( $old_2fa_enabled && $old_method !== $new_method ) ) { |
| 7057 |
$send_2fa_notification = true; |
| 7058 |
} |
| 7059 |
} |
| 7060 |
|
| 7061 |
// Check if login URL changed and notification is enabled |
| 7062 |
$old_login_url = $db_options['login_security']['custom_login_url'] ?? ''; |
| 7063 |
$new_login_url = isset( $data['login_security']['custom_login_url'] ) |
| 7064 |
? sanitize_title( $data['login_security']['custom_login_url'] ) |
| 7065 |
: ''; |
| 7066 |
$notify_on_url_change = isset( $data['login_security']['notify_on_login_url_change'] ) |
| 7067 |
? filter_var( $data['login_security']['notify_on_login_url_change'], FILTER_VALIDATE_BOOLEAN ) |
| 7068 |
: false; |
| 7069 |
|
| 7070 |
if ( $notify_on_url_change && ! empty( $new_login_url ) && $new_login_url !== $old_login_url ) { |
| 7071 |
$send_login_url_notification = true; |
| 7072 |
} |
| 7073 |
} |
| 7074 |
|
| 7075 |
// Get defaults |
| 7076 |
$defaults = $this->settings->get_default_options(); |
| 7077 |
|
| 7078 |
// Read ONLY saved options from database (not merged with defaults) |
| 7079 |
$saved_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 7080 |
|
| 7081 |
// What is stored before this request changes anything: the shared file |
| 7082 |
// settings this user may not change are put back from here (2.11.6). |
| 7083 |
$stored_options = $saved_options; |
| 7084 |
$locked = Vigilante_Settings::get_locked_file_settings(); |
| 7085 |
|
| 7086 |
if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) { |
| 7087 |
wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); |
| 7088 |
} |
| 7089 |
|
| 7090 |
// A module switch is a single key, so refusing says more than a success |
| 7091 |
// that changed nothing, and the dashboard puts the toggle back. |
| 7092 |
if ( 'modules' === $section && isset( $locked['modules'], $data['modules'] ) && is_array( $locked['modules'] ) && is_array( $data['modules'] ) ) { |
| 7093 |
foreach ( array_keys( $data['modules'] ) as $vg_module ) { |
| 7094 |
if ( in_array( sanitize_key( $vg_module ), $locked['modules'], true ) ) { |
| 7095 |
wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); |
| 7096 |
} |
| 7097 |
} |
| 7098 |
} |
| 7099 |
|
| 7100 |
$rejected_ips = array(); |
| 7101 |
$rejected_proxies = array(); |
| 7102 |
|
| 7103 |
// Handle modules |
| 7104 |
if ( 'modules' === $section && isset( $data['modules'] ) ) { |
| 7105 |
if ( ! isset( $saved_options['modules'] ) ) { |
| 7106 |
$saved_options['modules'] = array(); |
| 7107 |
} |
| 7108 |
foreach ( $data['modules'] as $module => $enabled ) { |
| 7109 |
$module = sanitize_key( $module ); |
| 7110 |
$saved_options['modules'][ $module ] = in_array( $enabled, array( '1', 1, 'true', true ), true ); |
| 7111 |
} |
| 7112 |
// Clear active preset when modules change |
| 7113 |
update_option( 'vigilante_active_preset', '' ); |
| 7114 |
} else { |
| 7115 |
// Process primary section |
| 7116 |
if ( isset( $data[ $section ] ) && is_array( $data[ $section ] ) ) { |
| 7117 |
$section_defaults = isset( $defaults[ $section ] ) ? $defaults[ $section ] : array(); |
| 7118 |
$current_section = isset( $saved_options[ $section ] ) ? $saved_options[ $section ] : array(); |
| 7119 |
|
| 7120 |
// Process the submitted data |
| 7121 |
$processed = $this->process_section_data( $data[ $section ], $section_defaults, $current_section ); |
| 7122 |
|
| 7123 |
// The IP boxes are free text and, until 2.9.9, whatever was typed |
| 7124 |
// went straight into the option. An entry the matcher can never |
| 7125 |
// match still sits in a security list looking like protection, |
| 7126 |
// so the ones that cannot match are dropped and reported back |
| 7127 |
// instead of being stored in silence. |
| 7128 |
$rejected_ips = $this->filter_ip_lists( $section, $processed, $rejected_proxies ); |
| 7129 |
|
| 7130 |
// Save the processed section |
| 7131 |
$saved_options[ $section ] = $processed; |
| 7132 |
|
| 7133 |
// Clear active preset when any section settings change |
| 7134 |
update_option( 'vigilante_active_preset', '' ); |
| 7135 |
} |
| 7136 |
} |
| 7137 |
|
| 7138 |
// Clear cache before saving |
| 7139 |
wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' ); |
| 7140 |
|
| 7141 |
$saved_options = Vigilante_Settings::keep_locked_file_settings( $saved_options, $stored_options ); |
| 7142 |
|
| 7143 |
// Save to database |
| 7144 |
update_option( Vigilante_Settings::OPTION_NAME, $saved_options ); |
| 7145 |
|
| 7146 |
// Clear the settings cache |
| 7147 |
$this->settings->clear_cache(); |
| 7148 |
|
| 7149 |
// Apply changes based on section |
| 7150 |
$this->apply_section_changes( $section, $saved_options ); |
| 7151 |
|
| 7152 |
// Send 2FA notifications after settings are saved |
| 7153 |
$notification_result = null; |
| 7154 |
if ( $send_2fa_notification ) { |
| 7155 |
$notification_result = $this->send_2fa_enable_notifications(); |
| 7156 |
} |
| 7157 |
|
| 7158 |
// Send login URL notifications after settings are saved |
| 7159 |
$login_url_result = null; |
| 7160 |
if ( $send_login_url_notification ) { |
| 7161 |
$login_url_result = $this->send_login_url_notifications(); |
| 7162 |
} |
| 7163 |
|
| 7164 |
// Build success message |
| 7165 |
$message = __( 'Settings saved successfully.', 'vigilante' ); |
| 7166 |
|
| 7167 |
if ( $notification_result && $notification_result['sent'] > 0 ) { |
| 7168 |
$message .= ' ' . sprintf( |
| 7169 |
/* translators: %d: Number of emails sent */ |
| 7170 |
_n( |
| 7171 |
'2FA notification sent to %d user.', |
| 7172 |
'2FA notifications sent to %d users.', |
| 7173 |
$notification_result['sent'], |
| 7174 |
'vigilante' |
| 7175 |
), |
| 7176 |
$notification_result['sent'] |
| 7177 |
); |
| 7178 |
} |
| 7179 |
|
| 7180 |
if ( $login_url_result && $login_url_result['sent'] > 0 ) { |
| 7181 |
$message .= ' ' . sprintf( |
| 7182 |
/* translators: %d: Number of emails sent */ |
| 7183 |
_n( |
| 7184 |
'Login URL notification sent to %d user.', |
| 7185 |
'Login URL notifications sent to %d users.', |
| 7186 |
$login_url_result['sent'], |
| 7187 |
'vigilante' |
| 7188 |
), |
| 7189 |
$login_url_result['sent'] |
| 7190 |
); |
| 7191 |
} |
| 7192 |
|
| 7193 |
if ( ! empty( $rejected_ips ) ) { |
| 7194 |
$message .= ' ' . sprintf( |
| 7195 |
/* translators: %s: comma separated list of the entries that were not saved. */ |
| 7196 |
_n( |
| 7197 |
'This entry is not a valid IP, CIDR range or wildcard, so it was not saved: %s', |
| 7198 |
'These entries are not valid IPs, CIDR ranges or wildcards, so they were not saved: %s', |
| 7199 |
count( $rejected_ips ), |
| 7200 |
'vigilante' |
| 7201 |
), |
| 7202 |
implode( ', ', array_map( 'esc_html', $rejected_ips ) ) |
| 7203 |
); |
| 7204 |
} |
| 7205 |
|
| 7206 |
if ( ! empty( $rejected_proxies ) ) { |
| 7207 |
$message .= ' ' . sprintf( |
| 7208 |
/* translators: %s: comma separated list of the trusted proxy entries that were not saved. */ |
| 7209 |
_n( |
| 7210 |
'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so this entry was not saved: %s', |
| 7211 |
'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so these entries were not saved: %s', |
| 7212 |
count( $rejected_proxies ), |
| 7213 |
'vigilante' |
| 7214 |
), |
| 7215 |
implode( ', ', array_map( 'esc_html', $rejected_proxies ) ) |
| 7216 |
); |
| 7217 |
} |
| 7218 |
|
| 7219 |
wp_send_json_success( $message ); |
| 7220 |
} |
| 7221 |
|
| 7222 |
/** |
| 7223 |
* Keep only the IP patterns the matcher can actually match |
| 7224 |
* |
| 7225 |
* @since 2.9.9 |
| 7226 |
* |
| 7227 |
* @param string $section Section being saved. |
| 7228 |
* @param array $processed Section data, edited in place. |
| 7229 |
* @return array Entries that were dropped, for the message back to the user. |
| 7230 |
*/ |
| 7231 |
private function filter_ip_lists( $section, &$processed, &$rejected_proxies = array() ) { |
| 7232 |
$rejected_proxies = array(); |
| 7233 |
|
| 7234 |
// Trusted proxies feed an identity decision, so only exact addresses and |
| 7235 |
// CIDR ranges belong there: a wildcard is stripped with its own message, |
| 7236 |
// never stored looking effective. The matcher ignores it anyway (see |
| 7237 |
// Vigilante_IP_Utils::in_list_ip_or_cidr), this stops it persisting. |
| 7238 |
if ( 'firewall' === $section && isset( $processed['trusted_proxies'] ) && is_array( $processed['trusted_proxies'] ) ) { |
| 7239 |
$split = Vigilante_IP_Utils::split_list_ip_or_cidr( $processed['trusted_proxies'] ); |
| 7240 |
$processed['trusted_proxies'] = $split['valid']; |
| 7241 |
$rejected_proxies = $split['rejected']; |
| 7242 |
} |
| 7243 |
|
| 7244 |
$lists = array( |
| 7245 |
'firewall' => array( 'ip_whitelist', 'ip_blacklist' ), |
| 7246 |
'login_security' => array( 'ip_whitelist' ), |
| 7247 |
); |
| 7248 |
|
| 7249 |
if ( ! isset( $lists[ $section ] ) ) { |
| 7250 |
return array(); |
| 7251 |
} |
| 7252 |
|
| 7253 |
$rejected = array(); |
| 7254 |
|
| 7255 |
foreach ( $lists[ $section ] as $key ) { |
| 7256 |
if ( ! isset( $processed[ $key ] ) || ! is_array( $processed[ $key ] ) ) { |
| 7257 |
continue; |
| 7258 |
} |
| 7259 |
|
| 7260 |
$split = Vigilante_IP_Utils::split_list( $processed[ $key ] ); |
| 7261 |
$processed[ $key ] = $split['valid']; |
| 7262 |
$rejected = array_merge( $rejected, $split['rejected'] ); |
| 7263 |
} |
| 7264 |
|
| 7265 |
return array_values( array_unique( $rejected ) ); |
| 7266 |
} |
| 7267 |
|
| 7268 |
/** |
| 7269 |
* Send 2FA enable notifications to users |
| 7270 |
* |
| 7271 |
* @return array Result with 'sent' and 'failed' counts. |
| 7272 |
*/ |
| 7273 |
private function send_2fa_enable_notifications() { |
| 7274 |
$result = array( |
| 7275 |
'sent' => 0, |
| 7276 |
'failed' => 0, |
| 7277 |
); |
| 7278 |
|
| 7279 |
// Get settings for roles and method |
| 7280 |
$login_security = $this->settings->get_section( 'login_security' ); |
| 7281 |
$two_factor = isset( $login_security['two_factor'] ) ? $login_security['two_factor'] : array(); |
| 7282 |
$roles = isset( $two_factor['enforced_roles'] ) ? $two_factor['enforced_roles'] : array( 'administrator' ); |
| 7283 |
$method = isset( $two_factor['method'] ) ? $two_factor['method'] : 'email'; |
| 7284 |
|
| 7285 |
if ( empty( $roles ) ) { |
| 7286 |
$roles = array( 'administrator' ); |
| 7287 |
} |
| 7288 |
|
| 7289 |
$excluded = isset( $two_factor['excluded_users'] ) ? array_map( 'absint', $two_factor['excluded_users'] ) : array(); |
| 7290 |
|
| 7291 |
// Get users with these roles |
| 7292 |
$args = array( |
| 7293 |
'role__in' => $roles, |
| 7294 |
); |
| 7295 |
if ( ! empty( $excluded ) ) { |
| 7296 |
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Small excluded users list from settings. |
| 7297 |
$args['exclude'] = $excluded; |
| 7298 |
} |
| 7299 |
$users = get_users( $args ); |
| 7300 |
|
| 7301 |
if ( empty( $users ) ) { |
| 7302 |
return $result; |
| 7303 |
} |
| 7304 |
|
| 7305 |
$site_name = get_bloginfo( 'name' ); |
| 7306 |
$from_name = ! empty( $two_factor['email_from_name'] ) ? $two_factor['email_from_name'] : $site_name; |
| 7307 |
|
| 7308 |
if ( 'totp' === $method ) { |
| 7309 |
// Use TOTP class for styled HTML emails |
| 7310 |
if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) { |
| 7311 |
require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php'; |
| 7312 |
} |
| 7313 |
$totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log ); |
| 7314 |
|
| 7315 |
foreach ( $users as $user ) { |
| 7316 |
// Skip users who already have TOTP configured |
| 7317 |
$totp_data = $this->database->get_totp_data( $user->ID ); |
| 7318 |
if ( $totp_data && ! empty( $totp_data['is_configured'] ) ) { |
| 7319 |
continue; |
| 7320 |
} |
| 7321 |
|
| 7322 |
$sent = $totp->send_activation_email( $user, $site_name, $from_name ); |
| 7323 |
|
| 7324 |
if ( $sent ) { |
| 7325 |
$this->database->mark_2fa_notified( $user->ID ); |
| 7326 |
$result['sent']++; |
| 7327 |
} else { |
| 7328 |
$result['failed']++; |
| 7329 |
} |
| 7330 |
} |
| 7331 |
} else { |
| 7332 |
// Email method - use existing email 2FA class |
| 7333 |
if ( ! class_exists( 'Vigilante_Two_Factor_Email' ) ) { |
| 7334 |
require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php'; |
| 7335 |
} |
| 7336 |
$email_2fa = new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log ); |
| 7337 |
return $email_2fa->send_activation_notifications( false ); |
| 7338 |
} |
| 7339 |
|
| 7340 |
return $result; |
| 7341 |
} |
| 7342 |
|
| 7343 |
/** |
| 7344 |
* Send login URL change notifications to users with admin access |
| 7345 |
* |
| 7346 |
* @return array Result with 'sent' and 'failed' counts. |
| 7347 |
*/ |
| 7348 |
private function send_login_url_notifications() { |
| 7349 |
$login_options = $this->settings->get_section( 'login_security' ); |
| 7350 |
$custom_url = ! empty( $login_options['custom_login_url'] ) ? sanitize_title( $login_options['custom_login_url'] ) : ''; |
| 7351 |
|
| 7352 |
if ( empty( $custom_url ) ) { |
| 7353 |
return array( 'sent' => 0, 'failed' => 0 ); |
| 7354 |
} |
| 7355 |
|
| 7356 |
$login_url = home_url( $custom_url . '/' ); |
| 7357 |
$site_name = get_bloginfo( 'name' ); |
| 7358 |
|
| 7359 |
$admin_roles = array( 'administrator', 'editor', 'author', 'contributor' ); |
| 7360 |
$users = get_users( array( 'role__in' => $admin_roles ) ); |
| 7361 |
|
| 7362 |
if ( empty( $users ) ) { |
| 7363 |
return array( 'sent' => 0, 'failed' => 0 ); |
| 7364 |
} |
| 7365 |
|
| 7366 |
$subject = sprintf( |
| 7367 |
/* translators: %s: Site name */ |
| 7368 |
__( '[%s] Your login URL has changed', 'vigilante' ), |
| 7369 |
$site_name |
| 7370 |
); |
| 7371 |
|
| 7372 |
$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' ) ); |
| 7373 |
$body .= Vigilante_Email_Template::url_box( $login_url, __( 'Your new login URL:', 'vigilante' ) ); |
| 7374 |
$body .= Vigilante_Email_Template::alert_box( __( 'The old login address (wp-login.php) will no longer work.', 'vigilante' ) ); |
| 7375 |
$body .= Vigilante_Email_Template::button( $login_url, __( 'Go to login', 'vigilante' ) ); |
| 7376 |
|
| 7377 |
$sent = 0; |
| 7378 |
$failed = 0; |
| 7379 |
|
| 7380 |
foreach ( $users as $user ) { |
| 7381 |
$result = Vigilante_Email_Template::send( |
| 7382 |
$user->user_email, |
| 7383 |
$subject, |
| 7384 |
__( 'Login URL changed', 'vigilante' ), |
| 7385 |
$body |
| 7386 |
); |
| 7387 |
if ( $result ) { |
| 7388 |
$sent++; |
| 7389 |
} else { |
| 7390 |
$failed++; |
| 7391 |
} |
| 7392 |
} |
| 7393 |
|
| 7394 |
if ( $this->activity_log ) { |
| 7395 |
$this->activity_log->log( |
| 7396 |
'login', |
| 7397 |
'login_url_notified', |
| 7398 |
sprintf( |
| 7399 |
/* translators: 1: Sent count, 2: Failed count */ |
| 7400 |
__( 'Login URL notification sent on save: %1$d sent, %2$d failed', 'vigilante' ), |
| 7401 |
$sent, |
| 7402 |
$failed |
| 7403 |
) |
| 7404 |
); |
| 7405 |
} |
| 7406 |
|
| 7407 |
return array( 'sent' => $sent, 'failed' => $failed ); |
| 7408 |
} |
| 7409 |
|
| 7410 |
/** |
| 7411 |
* Process section data maintaining proper types from defaults |
| 7412 |
* |
| 7413 |
* @param array $submitted_data Data submitted from form. |
| 7414 |
* @param array $defaults Default values for this section. |
| 7415 |
* @param array $current Current saved values. |
| 7416 |
* @param string $section_name Section name for special handling. |
| 7417 |
* @return array Processed data. |
| 7418 |
*/ |
| 7419 |
private function process_section_data( $submitted_data, $defaults, $current, $section_name = '' ) { |
| 7420 |
// Start with defaults, then merge current saved values |
| 7421 |
$result = array_replace_recursive( $defaults, $current ); |
| 7422 |
|
| 7423 |
// Process each submitted value |
| 7424 |
foreach ( $submitted_data as $key => $value ) { |
| 7425 |
$key = sanitize_key( $key ); |
| 7426 |
|
| 7427 |
if ( is_array( $value ) ) { |
| 7428 |
// Nested array (like rate_limiting) |
| 7429 |
$nested_defaults = isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) ? $defaults[ $key ] : array(); |
| 7430 |
$nested_current = isset( $result[ $key ] ) && is_array( $result[ $key ] ) ? $result[ $key ] : array(); |
| 7431 |
$result[ $key ] = $this->process_section_data( $value, $nested_defaults, $nested_current, $key ); |
| 7432 |
} else { |
| 7433 |
// Determine type from default value |
| 7434 |
$default_value = isset( $defaults[ $key ] ) ? $defaults[ $key ] : null; |
| 7435 |
|
| 7436 |
if ( null === $default_value ) { |
| 7437 |
// No default, check current value type or use as string |
| 7438 |
$current_value = isset( $current[ $key ] ) ? $current[ $key ] : null; |
| 7439 |
if ( is_bool( $current_value ) ) { |
| 7440 |
$result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true ); |
| 7441 |
} elseif ( is_int( $current_value ) ) { |
| 7442 |
$result[ $key ] = intval( $value ); |
| 7443 |
} elseif ( is_array( $current_value ) ) { |
| 7444 |
$result[ $key ] = is_string( $value ) ? array_filter( array_map( 'trim', explode( "\n", $value ) ) ) : (array) $value; |
| 7445 |
} else { |
| 7446 |
$result[ $key ] = sanitize_text_field( $value ); |
| 7447 |
} |
| 7448 |
} elseif ( is_bool( $default_value ) ) { |
| 7449 |
// Boolean: '1', 1, 'true' become true; '0', 0, '', 'false' become false |
| 7450 |
$result[ $key ] = in_array( $value, array( '1', 1, 'true', true ), true ); |
| 7451 |
} elseif ( is_int( $default_value ) ) { |
| 7452 |
// Integer - with special handling for time fields shown in minutes |
| 7453 |
$int_value = intval( $value ); |
| 7454 |
|
| 7455 |
// Convert minutes to seconds for login_security duration fields |
| 7456 |
// These are displayed as minutes in the form but stored as seconds |
| 7457 |
if ( in_array( $key, array( 'lockout_duration', 'max_lockout_duration' ), true ) ) { |
| 7458 |
$int_value = $int_value * 60; |
| 7459 |
} |
| 7460 |
|
| 7461 |
$result[ $key ] = $int_value; |
| 7462 |
} elseif ( is_array( $default_value ) ) { |
| 7463 |
// Array from textarea (e.g., IP lists) |
| 7464 |
if ( is_string( $value ) ) { |
| 7465 |
$result[ $key ] = array_filter( array_map( 'trim', explode( "\n", $value ) ) ); |
| 7466 |
} else { |
| 7467 |
$result[ $key ] = (array) $value; |
| 7468 |
} |
| 7469 |
} else { |
| 7470 |
// String - preserve newlines for textarea fields |
| 7471 |
if ( is_string( $value ) && ( strpos( $value, "\n" ) !== false || strpos( $value, "\r" ) !== false ) ) { |
| 7472 |
$result[ $key ] = sanitize_textarea_field( $value ); |
| 7473 |
} else { |
| 7474 |
$result[ $key ] = sanitize_text_field( $value ); |
| 7475 |
} |
| 7476 |
} |
| 7477 |
} |
| 7478 |
} |
| 7479 |
|
| 7480 |
// Handle unchecked checkboxes: HTML forms don't submit unchecked boxes |
| 7481 |
// If a boolean field exists in defaults but NOT in submitted_data, set it to false |
| 7482 |
// |
| 7483 |
// EXCEPTION: the top-level 'enabled' flag of every section is the |
| 7484 |
// module's master switch and is controlled by the Dashboard module |
| 7485 |
// toggle, NOT by a checkbox inside the section's form. Treating it |
| 7486 |
// like a regular checkbox here would silently switch the module off |
| 7487 |
// every time the user saves the tab — see the REST API enabled=false |
| 7488 |
// regression. We only skip it at the top level (when section_name is |
| 7489 |
// empty); nested 'enabled' fields like security_headers.csp.enabled |
| 7490 |
// are real checkboxes and must keep the auto-unset behaviour. |
| 7491 |
$preserve_top_level = array( 'enabled' ); |
| 7492 |
|
| 7493 |
foreach ( $defaults as $key => $default_value ) { |
| 7494 |
if ( '' === $section_name && in_array( $key, $preserve_top_level, true ) ) { |
| 7495 |
continue; |
| 7496 |
} |
| 7497 |
if ( is_bool( $default_value ) && ! array_key_exists( $key, $submitted_data ) ) { |
| 7498 |
$result[ $key ] = false; |
| 7499 |
} elseif ( is_array( $default_value ) && ! isset( $submitted_data[ $key ] ) ) { |
| 7500 |
// Check if this is a flat value list (like excluded_users, enforced_roles, ip_whitelist) |
| 7501 |
// vs a nested settings group (like rate_limiting, two_factor) |
| 7502 |
// Flat lists: default is empty array OR all values are scalar |
| 7503 |
$is_value_list = empty( $default_value ); |
| 7504 |
if ( ! $is_value_list ) { |
| 7505 |
$is_value_list = true; |
| 7506 |
foreach ( $default_value as $dv ) { |
| 7507 |
if ( ! is_scalar( $dv ) ) { |
| 7508 |
$is_value_list = false; |
| 7509 |
break; |
| 7510 |
} |
| 7511 |
} |
| 7512 |
} |
| 7513 |
|
| 7514 |
if ( $is_value_list ) { |
| 7515 |
// All items removed - reset to empty array |
| 7516 |
$result[ $key ] = array(); |
| 7517 |
} else { |
| 7518 |
// Nested settings group - handle boolean children |
| 7519 |
foreach ( $default_value as $nested_key => $nested_default ) { |
| 7520 |
if ( is_bool( $nested_default ) && isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) { |
| 7521 |
$nested_submitted = isset( $submitted_data[ $key ] ) && is_array( $submitted_data[ $key ] ) |
| 7522 |
? $submitted_data[ $key ] |
| 7523 |
: array(); |
| 7524 |
if ( ! array_key_exists( $nested_key, $nested_submitted ) ) { |
| 7525 |
$result[ $key ][ $nested_key ] = false; |
| 7526 |
} |
| 7527 |
} |
| 7528 |
} |
| 7529 |
} |
| 7530 |
} |
| 7531 |
} |
| 7532 |
|
| 7533 |
return $result; |
| 7534 |
} |
| 7535 |
|
| 7536 |
/** |
| 7537 |
* AJAX: Export settings |
| 7538 |
*/ |
| 7539 |
public function ajax_export_settings() { |
| 7540 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7541 |
|
| 7542 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7543 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7544 |
} |
| 7545 |
|
| 7546 |
$options = $this->settings->get_all_options(); |
| 7547 |
$filename = 'vigilante-settings-' . gmdate( 'Y-m-d-His' ) . '.json'; |
| 7548 |
|
| 7549 |
wp_send_json_success( array( |
| 7550 |
'content' => wp_json_encode( $options, JSON_PRETTY_PRINT ), |
| 7551 |
'filename' => $filename, |
| 7552 |
) ); |
| 7553 |
} |
| 7554 |
|
| 7555 |
/** |
| 7556 |
* AJAX: Import settings |
| 7557 |
*/ |
| 7558 |
public function ajax_import_settings() { |
| 7559 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7560 |
|
| 7561 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7562 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7563 |
} |
| 7564 |
|
| 7565 |
// Accept both 'settings' (from JS) and 'content' (legacy). |
| 7566 |
// Do NOT run sanitize_text_field() on the raw payload: it calls |
| 7567 |
// wp_strip_all_tags() internally, which removes any "<...>" substring |
| 7568 |
// and turns a valid export JSON into garbage if any stored value |
| 7569 |
// contains < or > (htaccess snippets, email templates, etc.). The |
| 7570 |
// real sanitization happens after json_decode(), via map_deep() on |
| 7571 |
// the parsed array. |
| 7572 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash |
| 7573 |
$content = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; |
| 7574 |
if ( empty( $content ) ) { |
| 7575 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash |
| 7576 |
$content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; |
| 7577 |
} |
| 7578 |
|
| 7579 |
if ( empty( $content ) || ! is_string( $content ) ) { |
| 7580 |
wp_send_json_error( __( 'No content provided.', 'vigilante' ) ); |
| 7581 |
} |
| 7582 |
|
| 7583 |
$imported = json_decode( $content, true ); |
| 7584 |
|
| 7585 |
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $imported ) ) { |
| 7586 |
wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) ); |
| 7587 |
} |
| 7588 |
|
| 7589 |
// Sanitize imported data recursively |
| 7590 |
$imported = map_deep( $imported, 'sanitize_text_field' ); |
| 7591 |
|
| 7592 |
// Validate structure: only sections and keys of the schema survive, and |
| 7593 |
// every value takes the type of its default. Until 2.11.0 this was an |
| 7594 |
// array_replace_recursive() of the file over the defaults, so any key in |
| 7595 |
// the file, known or not, landed in vigilante_options (S7). Sections |
| 7596 |
// the file does not carry keep their defaults; a section it does carry |
| 7597 |
// replaces the default one whole, because validate_options() has |
| 7598 |
// already filled in whatever the file left out. |
| 7599 |
$defaults = $this->settings->get_default_options(); |
| 7600 |
$validated = $this->settings->validate_options( $imported ); |
| 7601 |
$merged = $defaults; |
| 7602 |
|
| 7603 |
foreach ( $validated as $section => $data ) { |
| 7604 |
if ( is_array( $data ) ) { |
| 7605 |
$merged[ $section ] = $data; |
| 7606 |
} |
| 7607 |
} |
| 7608 |
|
| 7609 |
// Save |
| 7610 |
$merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); |
| 7611 |
update_option( Vigilante_Settings::OPTION_NAME, $merged ); |
| 7612 |
$this->settings->clear_cache(); |
| 7613 |
|
| 7614 |
// Re-evaluate the active preset marker. The imported config may match |
| 7615 |
// a known preset exactly, partially, or not at all — without this step |
| 7616 |
// the dashboard would keep showing whatever preset was active before |
| 7617 |
// the import even if the new config no longer matches it. |
| 7618 |
$matched_preset = $this->detect_matching_preset( $merged ); |
| 7619 |
if ( null === $matched_preset ) { |
| 7620 |
delete_option( 'vigilante_active_preset' ); |
| 7621 |
} else { |
| 7622 |
update_option( 'vigilante_active_preset', $matched_preset ); |
| 7623 |
} |
| 7624 |
|
| 7625 |
// Apply file changes after import |
| 7626 |
$this->apply_all_file_changes( $merged ); |
| 7627 |
|
| 7628 |
// Refresh the Security Analyzer score so the dashboard widget reflects |
| 7629 |
// the imported config rather than the pre-import scan. Reuse the |
| 7630 |
// post-Under-Attack scan hook (same job: full scan, async). |
| 7631 |
if ( ! wp_next_scheduled( 'vigilante_under_attack_post_scan' ) ) { |
| 7632 |
wp_schedule_single_event( time() + 5, 'vigilante_under_attack_post_scan' ); |
| 7633 |
} |
| 7634 |
|
| 7635 |
wp_send_json_success( __( 'Settings imported successfully.', 'vigilante' ) . $this->locked_file_settings_message() ); |
| 7636 |
} |
| 7637 |
|
| 7638 |
/** |
| 7639 |
* Detect whether a vigilante_options array matches a known preset. |
| 7640 |
* |
| 7641 |
* A preset matches when every field the preset explicitly declares is |
| 7642 |
* present in the config with the same (normalised) value. Fields outside |
| 7643 |
* the preset are ignored — they may have been modified by the user before |
| 7644 |
* applying the preset and do not invalidate the match. This mirrors how |
| 7645 |
* apply_preset() now layers presets on top of the user's existing config. |
| 7646 |
* |
| 7647 |
* @param array $options The current/imported vigilante_options. |
| 7648 |
* @return string|null Preset id ('standard', 'maximum') or null if custom. |
| 7649 |
*/ |
| 7650 |
private function detect_matching_preset( $options ) { |
| 7651 |
if ( ! is_array( $options ) ) { |
| 7652 |
return null; |
| 7653 |
} |
| 7654 |
|
| 7655 |
$presets = $this->settings->get_presets(); |
| 7656 |
|
| 7657 |
foreach ( $presets as $preset_id => $preset_data ) { |
| 7658 |
unset( $preset_data['name'], $preset_data['description'] ); |
| 7659 |
if ( $this->preset_subset_matches( $preset_data, $options ) ) { |
| 7660 |
return $preset_id; |
| 7661 |
} |
| 7662 |
} |
| 7663 |
|
| 7664 |
return null; |
| 7665 |
} |
| 7666 |
|
| 7667 |
/** |
| 7668 |
* Check whether every leaf value inside $preset_subset exists with the |
| 7669 |
* same (normalised) value at the same path inside $config. |
| 7670 |
* |
| 7671 |
* @param mixed $preset_subset Branch of the preset definition. |
| 7672 |
* @param mixed $config Same branch in the live/imported config. |
| 7673 |
* @return bool |
| 7674 |
*/ |
| 7675 |
private function preset_subset_matches( $preset_subset, $config ) { |
| 7676 |
if ( is_array( $preset_subset ) ) { |
| 7677 |
if ( ! is_array( $config ) ) { |
| 7678 |
return false; |
| 7679 |
} |
| 7680 |
foreach ( $preset_subset as $key => $value ) { |
| 7681 |
if ( ! array_key_exists( $key, $config ) ) { |
| 7682 |
return false; |
| 7683 |
} |
| 7684 |
if ( ! $this->preset_subset_matches( $value, $config[ $key ] ) ) { |
| 7685 |
return false; |
| 7686 |
} |
| 7687 |
} |
| 7688 |
return true; |
| 7689 |
} |
| 7690 |
|
| 7691 |
return $this->normalise_scalar_for_compare( $preset_subset ) === $this->normalise_scalar_for_compare( $config ); |
| 7692 |
} |
| 7693 |
|
| 7694 |
/** |
| 7695 |
* Normalise a scalar value so that the variants WordPress and the form |
| 7696 |
* layer routinely produce ('1' / 1 / true → "1"; '' / '0' / 0 / false / |
| 7697 |
* null → "") compare equal. Other values become strings unchanged. |
| 7698 |
* |
| 7699 |
* @param mixed $value |
| 7700 |
* @return string |
| 7701 |
*/ |
| 7702 |
private function normalise_scalar_for_compare( $value ) { |
| 7703 |
if ( is_bool( $value ) ) { |
| 7704 |
return $value ? '1' : ''; |
| 7705 |
} |
| 7706 |
if ( null === $value ) { |
| 7707 |
return ''; |
| 7708 |
} |
| 7709 |
if ( is_int( $value ) || is_float( $value ) ) { |
| 7710 |
return (string) $value; |
| 7711 |
} |
| 7712 |
if ( is_string( $value ) ) { |
| 7713 |
if ( 'true' === $value ) { |
| 7714 |
return '1'; |
| 7715 |
} |
| 7716 |
if ( 'false' === $value ) { |
| 7717 |
return ''; |
| 7718 |
} |
| 7719 |
return $value; |
| 7720 |
} |
| 7721 |
// Arrays and objects shouldn't reach here (handled by recursion above), |
| 7722 |
// but if they do, fall back to a stable comparable representation. |
| 7723 |
return wp_json_encode( $value ); |
| 7724 |
} |
| 7725 |
|
| 7726 |
/** |
| 7727 |
* AJAX: Apply preset |
| 7728 |
*/ |
| 7729 |
public function ajax_apply_preset() { |
| 7730 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7731 |
|
| 7732 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7733 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7734 |
} |
| 7735 |
|
| 7736 |
$preset = isset( $_POST['preset'] ) ? sanitize_key( $_POST['preset'] ) : ''; |
| 7737 |
|
| 7738 |
// Handle reset to defaults |
| 7739 |
if ( 'reset' === $preset ) { |
| 7740 |
$stored_options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 7741 |
$defaults = Vigilante_Settings::get_defaults_preserving_user_data( $stored_options ); |
| 7742 |
$defaults = Vigilante_Settings::keep_locked_file_settings( $defaults, $stored_options ); |
| 7743 |
update_option( Vigilante_Settings::OPTION_NAME, $defaults ); |
| 7744 |
$this->settings->clear_cache(); |
| 7745 |
|
| 7746 |
// Clear active preset |
| 7747 |
update_option( 'vigilante_active_preset', '' ); |
| 7748 |
|
| 7749 |
// Apply file changes after reset |
| 7750 |
$this->apply_all_file_changes( $defaults ); |
| 7751 |
|
| 7752 |
wp_send_json_success( __( 'Settings reset to defaults.', 'vigilante' ) . $this->locked_file_settings_message() ); |
| 7753 |
return; |
| 7754 |
} |
| 7755 |
|
| 7756 |
$presets = $this->settings->get_presets(); |
| 7757 |
|
| 7758 |
if ( ! isset( $presets[ $preset ] ) ) { |
| 7759 |
wp_send_json_error( __( 'Invalid preset.', 'vigilante' ) ); |
| 7760 |
} |
| 7761 |
|
| 7762 |
$preset_options = $presets[ $preset ]; |
| 7763 |
unset( $preset_options['name'], $preset_options['description'] ); |
| 7764 |
|
| 7765 |
// Layer the preset on top of the user's CURRENT configuration, not on |
| 7766 |
// top of defaults. This way applying a preset only changes the fields |
| 7767 |
// the preset explicitly mentions; everything else stays as the user |
| 7768 |
// had it. For example, applying Maximum will not flip HSTS off if the |
| 7769 |
// user had it on — Maximum doesn't touch HSTS, so it's left alone. |
| 7770 |
// Use "Reset to Defaults" if a clean slate is needed. |
| 7771 |
$current = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 7772 |
if ( ! is_array( $current ) ) { |
| 7773 |
$current = array(); |
| 7774 |
} |
| 7775 |
// Make sure all known keys exist before merging — the merge does not |
| 7776 |
// invent keys that are missing on both sides. |
| 7777 |
$current = Vigilante_Settings::merge_preset( $this->settings->get_default_options(), $current ); |
| 7778 |
|
| 7779 |
$merged = Vigilante_Settings::merge_preset( $current, $preset_options ); |
| 7780 |
$merged = Vigilante_Settings::keep_locked_file_settings( $merged, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); |
| 7781 |
|
| 7782 |
update_option( Vigilante_Settings::OPTION_NAME, $merged ); |
| 7783 |
$this->settings->clear_cache(); |
| 7784 |
|
| 7785 |
// Save active preset |
| 7786 |
update_option( 'vigilante_active_preset', $preset ); |
| 7787 |
|
| 7788 |
// Apply file changes after preset |
| 7789 |
$this->apply_all_file_changes( $merged ); |
| 7790 |
|
| 7791 |
wp_send_json_success( __( 'Preset applied successfully.', 'vigilante' ) . $this->locked_file_settings_message() ); |
| 7792 |
} |
| 7793 |
|
| 7794 |
/** |
| 7795 |
* AJAX: Reset a specific section to defaults |
| 7796 |
*/ |
| 7797 |
public function ajax_reset_section() { |
| 7798 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7799 |
|
| 7800 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7801 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7802 |
} |
| 7803 |
|
| 7804 |
$section = isset( $_POST['section'] ) ? sanitize_key( $_POST['section'] ) : ''; |
| 7805 |
|
| 7806 |
if ( empty( $section ) ) { |
| 7807 |
wp_send_json_error( __( 'No section specified.', 'vigilante' ) ); |
| 7808 |
} |
| 7809 |
|
| 7810 |
// Get current options and defaults. get_defaults_preserving_user_data() |
| 7811 |
// applies the tweaks a fresh installation gets, so the button and a new |
| 7812 |
// install agree, and keeps whatever the owner typed in. |
| 7813 |
$current_options = $this->settings->get_all_options(); |
| 7814 |
$defaults = Vigilante_Settings::get_defaults_preserving_user_data( $current_options ); |
| 7815 |
|
| 7816 |
// Check if section exists in defaults |
| 7817 |
if ( ! isset( $defaults[ $section ] ) ) { |
| 7818 |
wp_send_json_error( __( 'Invalid section.', 'vigilante' ) ); |
| 7819 |
} |
| 7820 |
|
| 7821 |
$new_values = $defaults[ $section ]; |
| 7822 |
|
| 7823 |
/* |
| 7824 |
* On a subsite, the settings written to wp-config.php and .htaccess are |
| 7825 |
* the main site's business. Resetting the local copy of those would only |
| 7826 |
* make this screen disagree with the file, so they are carried over |
| 7827 |
* untouched, and a section that is nothing but shared settings is not |
| 7828 |
* reset at all. On the main site, a user without network rights keeps |
| 7829 |
* the ones the shared files are built from as well (2.11.6). |
| 7830 |
*/ |
| 7831 |
$locked = Vigilante_Settings::get_locked_file_settings(); |
| 7832 |
|
| 7833 |
if ( isset( $locked[ $section ] ) && true === $locked[ $section ] ) { |
| 7834 |
wp_send_json_error( Vigilante_Settings::get_shared_files_notice() ); |
| 7835 |
} |
| 7836 |
|
| 7837 |
$current_options[ $section ] = $new_values; |
| 7838 |
$current_options = Vigilante_Settings::keep_locked_file_settings( $current_options, get_option( Vigilante_Settings::OPTION_NAME, array() ) ); |
| 7839 |
|
| 7840 |
// Save |
| 7841 |
update_option( Vigilante_Settings::OPTION_NAME, $current_options ); |
| 7842 |
$this->settings->clear_cache(); |
| 7843 |
|
| 7844 |
// Apply file changes if needed |
| 7845 |
$this->apply_section_changes( $section, $current_options ); |
| 7846 |
|
| 7847 |
wp_send_json_success( array( |
| 7848 |
'message' => __( 'Section reset to defaults.', 'vigilante' ), |
| 7849 |
'section' => $section, |
| 7850 |
'reload' => true, |
| 7851 |
) ); |
| 7852 |
} |
| 7853 |
|
| 7854 |
/** |
| 7855 |
* AJAX: Clear lockouts |
| 7856 |
*/ |
| 7857 |
public function ajax_clear_lockouts() { |
| 7858 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7859 |
|
| 7860 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7861 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7862 |
} |
| 7863 |
|
| 7864 |
$ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : ''; |
| 7865 |
|
| 7866 |
if ( ! empty( $ip ) ) { |
| 7867 |
$this->database->clear_lockout( $ip ); |
| 7868 |
} else { |
| 7869 |
$this->database->clear_all_lockouts(); |
| 7870 |
} |
| 7871 |
|
| 7872 |
wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) ); |
| 7873 |
} |
| 7874 |
|
| 7875 |
/** |
| 7876 |
* AJAX: Clear logs |
| 7877 |
*/ |
| 7878 |
public function ajax_clear_logs() { |
| 7879 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7880 |
|
| 7881 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7882 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7883 |
} |
| 7884 |
|
| 7885 |
if ( $this->activity_log ) { |
| 7886 |
$result = $this->activity_log->clear_all_logs(); |
| 7887 |
if ( $result ) { |
| 7888 |
wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) ); |
| 7889 |
} else { |
| 7890 |
wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) ); |
| 7891 |
} |
| 7892 |
} else { |
| 7893 |
wp_send_json_error( __( 'Activity log not available.', 'vigilante' ) ); |
| 7894 |
} |
| 7895 |
} |
| 7896 |
|
| 7897 |
/** |
| 7898 |
* AJAX: Run file integrity scan. |
| 7899 |
* |
| 7900 |
* Triggers the file integrity scan, which now also runs the closed plugins |
| 7901 |
* check at the end when the `check_closed_plugins` toggle is on. Activity |
| 7902 |
* log is passed through so Security Audit entries (both file-level and |
| 7903 |
* plugin-status) are recorded from this entry point. |
| 7904 |
*/ |
| 7905 |
public function ajax_run_scan() { |
| 7906 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7907 |
|
| 7908 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7909 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7910 |
} |
| 7911 |
|
| 7912 |
// Clear previous results before running new scan |
| 7913 |
delete_option( 'vigilante_last_integrity_results' ); |
| 7914 |
delete_option( 'vigilante_last_integrity_scan' ); |
| 7915 |
|
| 7916 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); |
| 7917 |
$results = $file_integrity->run_scan(); |
| 7918 |
|
| 7919 |
// Save new results |
| 7920 |
update_option( 'vigilante_last_integrity_scan', time() ); |
| 7921 |
update_option( 'vigilante_last_integrity_results', $results ); |
| 7922 |
|
| 7923 |
// On the main site the scan does compute the lines of wp-config.php and |
| 7924 |
// .htaccess, for the network administrator. Somebody without network |
| 7925 |
// rights gets the change and its sizes, not the lines. |
| 7926 |
if ( $this->critical_approval_locked() && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) { |
| 7927 |
foreach ( $results['modified'] as $index => $item ) { |
| 7928 |
if ( is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) { |
| 7929 |
$results['modified'][ $index ]['diff'] = Vigilante_File_Integrity::network_only_diff(); |
| 7930 |
} |
| 7931 |
} |
| 7932 |
} |
| 7933 |
|
| 7934 |
wp_send_json_success( array( |
| 7935 |
'message' => __( 'Scan completed.', 'vigilante' ), |
| 7936 |
'results' => $results, |
| 7937 |
'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ), |
| 7938 |
) ); |
| 7939 |
} |
| 7940 |
|
| 7941 |
/** |
| 7942 |
* AJAX: Clear scan results. |
| 7943 |
* |
| 7944 |
* Wipes visually everything inside "Last Scan Results": file scan findings |
| 7945 |
* (Suspicious / Modified / Extra / Critical Config), file hashes and the |
| 7946 |
* Closed + Removed Plugins block. |
| 7947 |
* |
| 7948 |
* Implementation detail to keep persistence intact: |
| 7949 |
* - We delete the file scan options + hashes outright. |
| 7950 |
* - For plugin status we ONLY delete the last_check timestamp — the state |
| 7951 |
* map and the ignore list are preserved in DB. The render gates the |
| 7952 |
* plugin_status subsections on last_check > 0, so they hide after |
| 7953 |
* Clear (visual reset) and reappear on the next Run Scan Now with the |
| 7954 |
* state intact. This avoids degrading a 'removed' slug (404 without |
| 7955 |
* metadata) back to 'not_in_repo' on the next scan, which would |
| 7956 |
* silently lose the alert. |
| 7957 |
* |
| 7958 |
* The Ignored Files list and the Ignored Closed + Removed Plugins list |
| 7959 |
* are preserved on purpose; each has its own explicit "Clear All …" |
| 7960 |
* button. |
| 7961 |
*/ |
| 7962 |
public function ajax_clear_scan() { |
| 7963 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 7964 |
|
| 7965 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 7966 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7967 |
} |
| 7968 |
|
| 7969 |
$results = get_option( 'vigilante_last_integrity_results' ); |
| 7970 |
$scanned_at = get_option( 'vigilante_last_integrity_scan' ); |
| 7971 |
|
| 7972 |
delete_option( 'vigilante_last_integrity_results' ); |
| 7973 |
delete_option( 'vigilante_last_integrity_scan' ); |
| 7974 |
|
| 7975 |
/* |
| 7976 |
* A pending change to wp-config.php or the root .htaccess is closed by |
| 7977 |
* approving it, which takes the network. Clearing the results was one |
| 7978 |
* more way to close it without, until the next scan: the ignore list was |
| 7979 |
* shut in 2.11.8 and this button was left open, found by the cross |
| 7980 |
* review of 2.11.8. So for somebody who cannot approve, those entries |
| 7981 |
* stay and everything else goes. |
| 7982 |
*/ |
| 7983 |
if ( $this->critical_approval_locked() && is_array( $results ) && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) { |
| 7984 |
$critical = array_values( |
| 7985 |
array_filter( |
| 7986 |
$results['modified'], |
| 7987 |
function ( $item ) { |
| 7988 |
return is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ); |
| 7989 |
} |
| 7990 |
) |
| 7991 |
); |
| 7992 |
|
| 7993 |
if ( $critical ) { |
| 7994 |
$results['modified'] = $critical; |
| 7995 |
$results['suspicious'] = array(); |
| 7996 |
$results['extra'] = array(); |
| 7997 |
update_option( 'vigilante_last_integrity_results', $results ); |
| 7998 |
update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() ); |
| 7999 |
} |
| 8000 |
} |
| 8001 |
|
| 8002 |
if ( $this->database ) { |
| 8003 |
$this->database->clear_file_hashes(); |
| 8004 |
} |
| 8005 |
|
| 8006 |
// Visual reset of plugin_status block without touching the state map |
| 8007 |
// or the ignored list. See PHPDoc above for the rationale. |
| 8008 |
delete_option( 'vigilante_plugin_status_last_check' ); |
| 8009 |
|
| 8010 |
wp_send_json_success( __( 'Scan results cleared.', 'vigilante' ) ); |
| 8011 |
} |
| 8012 |
|
| 8013 |
/** |
| 8014 |
* AJAX: Ignore a file from scan results |
| 8015 |
*/ |
| 8016 |
public function ajax_ignore_file() { |
| 8017 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8018 |
|
| 8019 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8020 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8021 |
} |
| 8022 |
|
| 8023 |
$file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; |
| 8024 |
|
| 8025 |
if ( empty( $file ) ) { |
| 8026 |
wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 8027 |
} |
| 8028 |
|
| 8029 |
// A change to a shared file is closed by approving it, and approving it |
| 8030 |
// takes the network. Ignoring it would close the same warning without. |
| 8031 |
if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) { |
| 8032 |
wp_send_json_error( $this->critical_approval_notice() ); |
| 8033 |
} |
| 8034 |
|
| 8035 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 8036 |
$file_integrity->ignore_file( $file ); |
| 8037 |
|
| 8038 |
// Also remove the file from stored scan results so UI updates |
| 8039 |
$results = get_option( 'vigilante_last_integrity_results' ); |
| 8040 |
if ( $results ) { |
| 8041 |
foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) { |
| 8042 |
if ( ! empty( $results[ $category ] ) ) { |
| 8043 |
$results[ $category ] = array_values( |
| 8044 |
array_filter( |
| 8045 |
$results[ $category ], |
| 8046 |
function ( $item ) use ( $file ) { |
| 8047 |
return ( is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item ) !== $file; |
| 8048 |
} |
| 8049 |
) |
| 8050 |
); |
| 8051 |
} |
| 8052 |
} |
| 8053 |
update_option( 'vigilante_last_integrity_results', $results ); |
| 8054 |
} |
| 8055 |
|
| 8056 |
wp_send_json_success( __( 'File added to ignored list.', 'vigilante' ) ); |
| 8057 |
} |
| 8058 |
|
| 8059 |
/** |
| 8060 |
* AJAX: Stop ignoring a file |
| 8061 |
*/ |
| 8062 |
public function ajax_unignore_file() { |
| 8063 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8064 |
|
| 8065 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8066 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8067 |
} |
| 8068 |
|
| 8069 |
$file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; |
| 8070 |
|
| 8071 |
if ( empty( $file ) ) { |
| 8072 |
wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 8073 |
} |
| 8074 |
|
| 8075 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 8076 |
$file_integrity->unignore_file( $file ); |
| 8077 |
|
| 8078 |
wp_send_json_success( __( 'File removed from ignored list.', 'vigilante' ) ); |
| 8079 |
} |
| 8080 |
|
| 8081 |
/** |
| 8082 |
* AJAX: Bulk ignore multiple files at once |
| 8083 |
* |
| 8084 |
* Processes a single batch into ignored list and prunes them from the |
| 8085 |
* stored scan results so the UI updates without a re-scan. |
| 8086 |
*/ |
| 8087 |
public function ajax_bulk_ignore_files() { |
| 8088 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8089 |
|
| 8090 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8091 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8092 |
} |
| 8093 |
|
| 8094 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item. |
| 8095 |
$raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array(); |
| 8096 |
if ( ! is_array( $raw_files ) ) { |
| 8097 |
wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); |
| 8098 |
} |
| 8099 |
|
| 8100 |
$files = array(); |
| 8101 |
$shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array(); |
| 8102 |
foreach ( $raw_files as $f ) { |
| 8103 |
$clean = sanitize_text_field( $f ); |
| 8104 |
// Same rule as ajax_ignore_file() for the two shared files. |
| 8105 |
if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) { |
| 8106 |
$files[] = $clean; |
| 8107 |
} |
| 8108 |
} |
| 8109 |
|
| 8110 |
if ( empty( $files ) ) { |
| 8111 |
wp_send_json_error( __( 'No files selected.', 'vigilante' ) ); |
| 8112 |
} |
| 8113 |
|
| 8114 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 8115 |
$count = 0; |
| 8116 |
foreach ( $files as $file ) { |
| 8117 |
$file_integrity->ignore_file( $file ); |
| 8118 |
$count++; |
| 8119 |
} |
| 8120 |
|
| 8121 |
// Also prune the stored scan results so the UI matches the new ignore list. |
| 8122 |
$results = get_option( 'vigilante_last_integrity_results' ); |
| 8123 |
if ( $results ) { |
| 8124 |
$files_set = array_flip( $files ); |
| 8125 |
foreach ( array( 'modified', 'suspicious', 'extra' ) as $category ) { |
| 8126 |
if ( ! empty( $results[ $category ] ) ) { |
| 8127 |
$results[ $category ] = array_values( |
| 8128 |
array_filter( |
| 8129 |
$results[ $category ], |
| 8130 |
function ( $item ) use ( $files_set ) { |
| 8131 |
$path = is_array( $item ) ? ( $item['file'] ?? '' ) : (string) $item; |
| 8132 |
return ! isset( $files_set[ $path ] ); |
| 8133 |
} |
| 8134 |
) |
| 8135 |
); |
| 8136 |
} |
| 8137 |
} |
| 8138 |
update_option( 'vigilante_last_integrity_results', $results ); |
| 8139 |
} |
| 8140 |
|
| 8141 |
wp_send_json_success( |
| 8142 |
array( |
| 8143 |
'count' => $count, |
| 8144 |
'message' => sprintf( |
| 8145 |
/* translators: %d: number of files added to the ignored list */ |
| 8146 |
_n( '%d file added to ignored list.', '%d files added to ignored list.', $count, 'vigilante' ), |
| 8147 |
$count |
| 8148 |
), |
| 8149 |
) |
| 8150 |
); |
| 8151 |
} |
| 8152 |
|
| 8153 |
/** |
| 8154 |
* AJAX: Bulk un-ignore multiple files at once |
| 8155 |
*/ |
| 8156 |
public function ajax_bulk_unignore_files() { |
| 8157 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8158 |
|
| 8159 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8160 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8161 |
} |
| 8162 |
|
| 8163 |
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below per-item. |
| 8164 |
$raw_files = isset( $_POST['files'] ) ? wp_unslash( $_POST['files'] ) : array(); |
| 8165 |
if ( ! is_array( $raw_files ) ) { |
| 8166 |
wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); |
| 8167 |
} |
| 8168 |
|
| 8169 |
$files = array(); |
| 8170 |
foreach ( $raw_files as $f ) { |
| 8171 |
$clean = sanitize_text_field( $f ); |
| 8172 |
if ( '' !== $clean ) { |
| 8173 |
$files[] = $clean; |
| 8174 |
} |
| 8175 |
} |
| 8176 |
|
| 8177 |
if ( empty( $files ) ) { |
| 8178 |
wp_send_json_error( __( 'No files selected.', 'vigilante' ) ); |
| 8179 |
} |
| 8180 |
|
| 8181 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 8182 |
$count = 0; |
| 8183 |
foreach ( $files as $file ) { |
| 8184 |
$file_integrity->unignore_file( $file ); |
| 8185 |
$count++; |
| 8186 |
} |
| 8187 |
|
| 8188 |
wp_send_json_success( |
| 8189 |
array( |
| 8190 |
'count' => $count, |
| 8191 |
'message' => sprintf( |
| 8192 |
/* translators: %d: number of files removed from the ignored list */ |
| 8193 |
_n( '%d file removed from ignored list.', '%d files removed from ignored list.', $count, 'vigilante' ), |
| 8194 |
$count |
| 8195 |
), |
| 8196 |
) |
| 8197 |
); |
| 8198 |
} |
| 8199 |
|
| 8200 |
/** |
| 8201 |
* AJAX: Clear all ignored files |
| 8202 |
*/ |
| 8203 |
public function ajax_clear_ignored() { |
| 8204 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8205 |
|
| 8206 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8207 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8208 |
} |
| 8209 |
|
| 8210 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 8211 |
$file_integrity->clear_ignored_files(); |
| 8212 |
|
| 8213 |
wp_send_json_success( __( 'Ignored files list cleared.', 'vigilante' ) ); |
| 8214 |
} |
| 8215 |
|
| 8216 |
/** |
| 8217 |
* AJAX: Ignore a closed/removed plugin slug so it stops appearing in the |
| 8218 |
* main list and email digests. The plugin keeps running on the site; |
| 8219 |
* silencing is purely cosmetic and reversible. |
| 8220 |
*/ |
| 8221 |
public function ajax_ignore_closed_plugin() { |
| 8222 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8223 |
|
| 8224 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8225 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8226 |
} |
| 8227 |
|
| 8228 |
$slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : ''; |
| 8229 |
if ( '' === $slug ) { |
| 8230 |
wp_send_json_error( __( 'No slug specified.', 'vigilante' ) ); |
| 8231 |
} |
| 8232 |
|
| 8233 |
if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { |
| 8234 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 8235 |
} |
| 8236 |
$checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); |
| 8237 |
$checker->ignore_slug( $slug ); |
| 8238 |
|
| 8239 |
wp_send_json_success( __( 'Plugin added to the ignored list.', 'vigilante' ) ); |
| 8240 |
} |
| 8241 |
|
| 8242 |
/** |
| 8243 |
* AJAX: Stop ignoring a previously-ignored closed/removed plugin slug. |
| 8244 |
*/ |
| 8245 |
public function ajax_unignore_closed_plugin() { |
| 8246 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8247 |
|
| 8248 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8249 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8250 |
} |
| 8251 |
|
| 8252 |
$slug = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : ''; |
| 8253 |
if ( '' === $slug ) { |
| 8254 |
wp_send_json_error( __( 'No slug specified.', 'vigilante' ) ); |
| 8255 |
} |
| 8256 |
|
| 8257 |
if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { |
| 8258 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 8259 |
} |
| 8260 |
$checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); |
| 8261 |
$checker->unignore_slug( $slug ); |
| 8262 |
|
| 8263 |
wp_send_json_success( __( 'Plugin removed from the ignored list.', 'vigilante' ) ); |
| 8264 |
} |
| 8265 |
|
| 8266 |
/** |
| 8267 |
* AJAX: Clear the entire ignored-closed-plugins list. |
| 8268 |
*/ |
| 8269 |
public function ajax_clear_ignored_closed_plugins() { |
| 8270 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8271 |
|
| 8272 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8273 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8274 |
} |
| 8275 |
|
| 8276 |
if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { |
| 8277 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 8278 |
} |
| 8279 |
$checker = new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); |
| 8280 |
$checker->clear_ignored(); |
| 8281 |
|
| 8282 |
wp_send_json_success( __( 'Ignored closed plugins list cleared.', 'vigilante' ) ); |
| 8283 |
} |
| 8284 |
|
| 8285 |
/** |
| 8286 |
* AJAX: Test security headers |
| 8287 |
*/ |
| 8288 |
public function ajax_test_headers() { |
| 8289 |
check_ajax_referer( 'vigilante_admin_nonce', 'nonce' ); |
| 8290 |
|
| 8291 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 8292 |
wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8293 |
} |
| 8294 |
|
| 8295 |
// Get security grade from settings (not from actual HTTP request) |
| 8296 |
$security_headers = new Vigilante_Security_Headers( $this->settings ); |
| 8297 |
$results = $security_headers->get_security_grade(); |
| 8298 |
|
| 8299 |
wp_send_json_success( $results ); |
| 8300 |
} |
| 8301 |
|
| 8302 |
/** |
| 8303 |
* Sanitize section data (kept for compatibility) |
| 8304 |
* |
| 8305 |
* @param string $section Section name. |
| 8306 |
* @param array $data Section data. |
| 8307 |
* @return array |
| 8308 |
*/ |
| 8309 |
private function sanitize_section_data( $section, $data ) { |
| 8310 |
$sanitized = array(); |
| 8311 |
|
| 8312 |
foreach ( $data as $key => $value ) { |
| 8313 |
$key = sanitize_key( $key ); |
| 8314 |
|
| 8315 |
if ( is_array( $value ) ) { |
| 8316 |
$sanitized[ $key ] = $this->sanitize_section_data( $key, $value ); |
| 8317 |
} elseif ( is_numeric( $value ) ) { |
| 8318 |
$sanitized[ $key ] = intval( $value ); |
| 8319 |
} else { |
| 8320 |
$sanitized[ $key ] = sanitize_text_field( $value ); |
| 8321 |
} |
| 8322 |
} |
| 8323 |
|
| 8324 |
return $sanitized; |
| 8325 |
} |
| 8326 |
|
| 8327 |
/** |
| 8328 |
* Apply changes after saving settings |
| 8329 |
* |
| 8330 |
* @param string $section Section that was updated. |
| 8331 |
* @param array $all_options All options. |
| 8332 |
*/ |
| 8333 |
private function apply_section_changes( $section, $all_options ) { |
| 8334 |
// Create fresh settings instance to ensure we have the latest data |
| 8335 |
$fresh_settings = new Vigilante_Settings(); |
| 8336 |
|
| 8337 |
// The shared Vigilant .htaccess block carries BOTH the firewall's |
| 8338 |
// file-protection rules and the server-signature / fingerprinting rules |
| 8339 |
// that are configured under Security Headers. So it must be regenerated |
| 8340 |
// whenever either section (or the module toggles) changes, not only on |
| 8341 |
// the firewall save — otherwise toggling "Hide server signature" or |
| 8342 |
// "Remove fingerprinting headers" never reaches the .htaccess. |
| 8343 |
if ( in_array( $section, array( 'firewall', 'security_headers', 'modules' ), true ) ) { |
| 8344 |
$htaccess = new Vigilante_Htaccess_Protection( $fresh_settings ); |
| 8345 |
$sh = $fresh_settings->get_section( 'security_headers' ); |
| 8346 |
$needs_htaccess_block = ! empty( $all_options['modules']['firewall'] ) |
| 8347 |
|| ! empty( $sh['hide_server_signature'] ) |
| 8348 |
|| ! empty( $sh['remove_fingerprinting_headers'] ); |
| 8349 |
|
| 8350 |
if ( $needs_htaccess_block ) { |
| 8351 |
$htaccess->apply_rules(); |
| 8352 |
} else { |
| 8353 |
$htaccess->remove_rules(); |
| 8354 |
} |
| 8355 |
} |
| 8356 |
|
| 8357 |
// Regenerate the HTTP security headers (sent by PHP) for the headers section. |
| 8358 |
if ( 'security_headers' === $section || 'modules' === $section ) { |
| 8359 |
$security_headers = new Vigilante_Security_Headers( $fresh_settings ); |
| 8360 |
$headers_enabled = ! empty( $all_options['modules']['security_headers'] ); |
| 8361 |
|
| 8362 |
if ( $headers_enabled ) { |
| 8363 |
$security_headers->apply_rules(); |
| 8364 |
} else { |
| 8365 |
$security_headers->remove_rules(); |
| 8366 |
} |
| 8367 |
} |
| 8368 |
|
| 8369 |
// Regenerate wp-config for wp_hardening section |
| 8370 |
if ( 'wp_hardening' === $section || 'modules' === $section ) { |
| 8371 |
$wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings ); |
| 8372 |
$hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] ); |
| 8373 |
|
| 8374 |
if ( $hardening_enabled ) { |
| 8375 |
$wpconfig->apply_security_constants(); |
| 8376 |
} else { |
| 8377 |
$wpconfig->remove_constants(); |
| 8378 |
} |
| 8379 |
|
| 8380 |
// Apply WordPress options for comments/pingbacks |
| 8381 |
$hardening_options = $all_options['wp_hardening'] ?? array(); |
| 8382 |
|
| 8383 |
if ( $hardening_enabled ) { |
| 8384 |
// Pingbacks |
| 8385 |
if ( ! empty( $hardening_options['disable_pingbacks'] ) ) { |
| 8386 |
update_option( 'default_pingback_flag', 0 ); |
| 8387 |
} else { |
| 8388 |
// Restore default: pingbacks enabled |
| 8389 |
update_option( 'default_pingback_flag', 1 ); |
| 8390 |
} |
| 8391 |
|
| 8392 |
// Trackbacks and ping status |
| 8393 |
// Only close if either pingbacks OR trackbacks are disabled |
| 8394 |
if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) { |
| 8395 |
update_option( 'default_ping_status', 'closed' ); |
| 8396 |
} else { |
| 8397 |
// Restore default: pings open |
| 8398 |
update_option( 'default_ping_status', 'open' ); |
| 8399 |
} |
| 8400 |
|
| 8401 |
// Comment moderation |
| 8402 |
if ( ! empty( $hardening_options['require_comment_moderation'] ) ) { |
| 8403 |
update_option( 'comment_moderation', 1 ); |
| 8404 |
} else { |
| 8405 |
// Restore default: no moderation required |
| 8406 |
update_option( 'comment_moderation', 0 ); |
| 8407 |
} |
| 8408 |
} |
| 8409 |
} |
| 8410 |
|
| 8411 |
// Trim activity log entries immediately when limits change |
| 8412 |
if ( 'activity_log' === $section && $this->activity_log ) { |
| 8413 |
$this->activity_log->cleanup_old_logs(); |
| 8414 |
} |
| 8415 |
|
| 8416 |
// Flush rewrite rules if login URL changed |
| 8417 |
if ( 'login_security' === $section ) { |
| 8418 |
$login_options = $all_options['login_security'] ?? array(); |
| 8419 |
if ( ! empty( $login_options['custom_login_url'] ) ) { |
| 8420 |
delete_option( 'vigilante_login_rules_version' ); |
| 8421 |
} |
| 8422 |
} |
| 8423 |
|
| 8424 |
// Log the settings change with readable section name |
| 8425 |
if ( $this->activity_log ) { |
| 8426 |
$section_names = array( |
| 8427 |
'firewall' => __( 'Firewall', 'vigilante' ), |
| 8428 |
'login_security' => __( 'Login Security', 'vigilante' ), |
| 8429 |
'security_headers' => __( 'Security Headers', 'vigilante' ), |
| 8430 |
'rest_api_security'=> __( 'REST API Security', 'vigilante' ), |
| 8431 |
'user_security' => __( 'User Security', 'vigilante' ), |
| 8432 |
'wp_hardening' => __( 'WP Hardening', 'vigilante' ), |
| 8433 |
'activity_log' => __( 'Security Audit', 'vigilante' ), |
| 8434 |
'file_integrity' => __( 'File Integrity', 'vigilante' ), |
| 8435 |
'email' => __( 'Notification Settings', 'vigilante' ), |
| 8436 |
'backup' => __( 'Backup', 'vigilante' ), |
| 8437 |
'advanced' => __( 'Advanced', 'vigilante' ), |
| 8438 |
'modules' => __( 'Modules', 'vigilante' ), |
| 8439 |
); |
| 8440 |
$display_name = isset( $section_names[ $section ] ) ? $section_names[ $section ] : $section; |
| 8441 |
|
| 8442 |
$this->activity_log->log( |
| 8443 |
'settings', |
| 8444 |
'settings_updated', |
| 8445 |
sprintf( |
| 8446 |
/* translators: %s: Section name */ |
| 8447 |
__( 'Settings updated: %s', 'vigilante' ), |
| 8448 |
$display_name |
| 8449 |
), |
| 8450 |
array( 'section' => $section ), |
| 8451 |
'info' |
| 8452 |
); |
| 8453 |
} |
| 8454 |
} |
| 8455 |
|
| 8456 |
/** |
| 8457 |
* Apply all file changes (htaccess, wp-config) based on current options |
| 8458 |
* |
| 8459 |
* Used after preset, import, or reset operations |
| 8460 |
* |
| 8461 |
* @param array $all_options All plugin options. |
| 8462 |
*/ |
| 8463 |
private function apply_all_file_changes( $all_options ) { |
| 8464 |
// Refresh settings cache first |
| 8465 |
$this->settings->clear_cache(); |
| 8466 |
|
| 8467 |
// Create fresh settings instance |
| 8468 |
$fresh_settings = new Vigilante_Settings(); |
| 8469 |
|
| 8470 |
// Apply firewall htaccess changes |
| 8471 |
$htaccess = new Vigilante_Htaccess_Protection( $fresh_settings ); |
| 8472 |
$firewall_enabled = ! empty( $all_options['modules']['firewall'] ); |
| 8473 |
|
| 8474 |
if ( $firewall_enabled ) { |
| 8475 |
$htaccess->apply_rules(); |
| 8476 |
} else { |
| 8477 |
$htaccess->remove_rules(); |
| 8478 |
} |
| 8479 |
|
| 8480 |
// Apply security headers htaccess changes |
| 8481 |
$security_headers = new Vigilante_Security_Headers( $fresh_settings ); |
| 8482 |
$headers_enabled = ! empty( $all_options['modules']['security_headers'] ); |
| 8483 |
|
| 8484 |
if ( $headers_enabled ) { |
| 8485 |
$security_headers->apply_rules(); |
| 8486 |
} else { |
| 8487 |
$security_headers->remove_rules(); |
| 8488 |
} |
| 8489 |
|
| 8490 |
// Apply wp-config changes |
| 8491 |
$wpconfig = new Vigilante_Wpconfig_Security( $fresh_settings ); |
| 8492 |
$hardening_enabled = ! empty( $all_options['modules']['wp_hardening'] ); |
| 8493 |
|
| 8494 |
if ( $hardening_enabled ) { |
| 8495 |
$wpconfig->apply_security_constants(); |
| 8496 |
} else { |
| 8497 |
$wpconfig->remove_constants(); |
| 8498 |
} |
| 8499 |
|
| 8500 |
// Apply WordPress options for comments/pingbacks |
| 8501 |
$hardening_options = $all_options['wp_hardening'] ?? array(); |
| 8502 |
|
| 8503 |
if ( $hardening_enabled ) { |
| 8504 |
// Pingbacks |
| 8505 |
if ( ! empty( $hardening_options['disable_pingbacks'] ) ) { |
| 8506 |
update_option( 'default_pingback_flag', 0 ); |
| 8507 |
} else { |
| 8508 |
// Restore default: pingbacks enabled |
| 8509 |
update_option( 'default_pingback_flag', 1 ); |
| 8510 |
} |
| 8511 |
|
| 8512 |
// Trackbacks and ping status |
| 8513 |
// Only close if either pingbacks OR trackbacks are disabled |
| 8514 |
if ( ! empty( $hardening_options['disable_pingbacks'] ) || ! empty( $hardening_options['disable_trackbacks'] ) ) { |
| 8515 |
update_option( 'default_ping_status', 'closed' ); |
| 8516 |
} else { |
| 8517 |
// Restore default: pings open |
| 8518 |
update_option( 'default_ping_status', 'open' ); |
| 8519 |
} |
| 8520 |
|
| 8521 |
// Comment moderation |
| 8522 |
if ( ! empty( $hardening_options['require_comment_moderation'] ) ) { |
| 8523 |
update_option( 'comment_moderation', 1 ); |
| 8524 |
} else { |
| 8525 |
// Restore default: no moderation required |
| 8526 |
update_option( 'comment_moderation', 0 ); |
| 8527 |
} |
| 8528 |
} |
| 8529 |
|
| 8530 |
// Log the change |
| 8531 |
if ( $this->activity_log ) { |
| 8532 |
$this->activity_log->log( |
| 8533 |
'settings', |
| 8534 |
'bulk_settings_applied', |
| 8535 |
__( 'Bulk settings applied (preset/import/reset)', 'vigilante' ), |
| 8536 |
array(), |
| 8537 |
'info' |
| 8538 |
); |
| 8539 |
} |
| 8540 |
} |
| 8541 |
} |