| 1 |
<?php |
| 2 |
/** |
| 3 |
* Plugin Name: Vigilant - 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… |
| 4 |
* Plugin URI: https://servicios.ayudawp.com |
| 5 |
* Description: Complete security solution for WordPress. Firewall, 2FA, security headers, login protection, file integrity monitoring, activity logging and more. |
| 6 |
* Version: 2.11.10 |
| 7 |
* Author: Fernando Tellado |
| 8 |
* Author URI: https://ayudawp.com |
| 9 |
* Text Domain: vigilante |
| 10 |
* Requires at least: 6.2 |
| 11 |
* Tested up to: 7.1 |
| 12 |
* Requires PHP: 7.4 |
| 13 |
* License: GPL v2 or later |
| 14 |
* License URI: https://www.gnu.org/licenses/gpl-2.0.html |
| 15 |
* |
| 16 |
* @package Vigilante |
| 17 |
*/ |
| 18 |
|
| 19 |
// Prevent direct access |
| 20 |
if ( ! defined( 'ABSPATH' ) ) { |
| 21 |
exit; |
| 22 |
} |
| 23 |
|
| 24 |
/** |
| 25 |
* Plugin constants |
| 26 |
*/ |
| 27 |
define( 'VIGILANTE_VERSION', '2.11.10' ); |
| 28 |
define( 'VIGILANTE_PLUGIN_FILE', __FILE__ ); |
| 29 |
define( 'VIGILANTE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); |
| 30 |
define( 'VIGILANTE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); |
| 31 |
define( 'VIGILANTE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); |
| 32 |
define( 'VIGILANTE_INCLUDES_DIR', VIGILANTE_PLUGIN_DIR . 'includes/' ); |
| 33 |
define( 'VIGILANTE_ADMIN_DIR', VIGILANTE_PLUGIN_DIR . 'admin/' ); |
| 34 |
define( 'VIGILANTE_ASSETS_URL', VIGILANTE_PLUGIN_URL . 'assets/' ); |
| 35 |
|
| 36 |
// Backup directory outside plugin folder (persists through updates) |
| 37 |
define( 'VIGILANTE_BACKUP_DIR', WP_CONTENT_DIR . '/vigilante-backups/' ); |
| 38 |
|
| 39 |
// Minimum requirements |
| 40 |
define( 'VIGILANTE_MIN_PHP_VERSION', '7.4' ); |
| 41 |
define( 'VIGILANTE_MIN_WP_VERSION', '5.0' ); |
| 42 |
|
| 43 |
/** |
| 44 |
* Check minimum requirements before loading |
| 45 |
* |
| 46 |
* @return bool True if requirements are met |
| 47 |
*/ |
| 48 |
function vigilante_check_requirements() { |
| 49 |
$meets_requirements = true; |
| 50 |
|
| 51 |
// Check PHP version |
| 52 |
if ( version_compare( PHP_VERSION, VIGILANTE_MIN_PHP_VERSION, '<' ) ) { |
| 53 |
$meets_requirements = false; |
| 54 |
} |
| 55 |
|
| 56 |
// Check WordPress version |
| 57 |
global $wp_version; |
| 58 |
if ( version_compare( $wp_version, VIGILANTE_MIN_WP_VERSION, '<' ) ) { |
| 59 |
$meets_requirements = false; |
| 60 |
} |
| 61 |
|
| 62 |
if ( ! $meets_requirements ) { |
| 63 |
add_action( 'admin_notices', 'vigilante_requirements_notice' ); |
| 64 |
} |
| 65 |
|
| 66 |
return $meets_requirements; |
| 67 |
} |
| 68 |
|
| 69 |
/** |
| 70 |
* Display requirements notice - called at admin_notices (after init) |
| 71 |
*/ |
| 72 |
function vigilante_requirements_notice() { |
| 73 |
global $wp_version; |
| 74 |
$errors = array(); |
| 75 |
|
| 76 |
if ( version_compare( PHP_VERSION, VIGILANTE_MIN_PHP_VERSION, '<' ) ) { |
| 77 |
$errors[] = sprintf( |
| 78 |
/* translators: 1: Current PHP version, 2: Required PHP version */ |
| 79 |
__( 'Vigilant requires PHP %2$s or higher. You are running PHP %1$s.', 'vigilante' ), |
| 80 |
PHP_VERSION, |
| 81 |
VIGILANTE_MIN_PHP_VERSION |
| 82 |
); |
| 83 |
} |
| 84 |
|
| 85 |
if ( version_compare( $wp_version, VIGILANTE_MIN_WP_VERSION, '<' ) ) { |
| 86 |
$errors[] = sprintf( |
| 87 |
/* translators: 1: Current WordPress version, 2: Required WordPress version */ |
| 88 |
__( 'Vigilant requires WordPress %2$s or higher. You are running WordPress %1$s.', 'vigilante' ), |
| 89 |
$wp_version, |
| 90 |
VIGILANTE_MIN_WP_VERSION |
| 91 |
); |
| 92 |
} |
| 93 |
|
| 94 |
foreach ( $errors as $error ) { |
| 95 |
printf( |
| 96 |
'<div class="notice notice-error"><p>%s</p></div>', |
| 97 |
esc_html( $error ) |
| 98 |
); |
| 99 |
} |
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Load plugin files |
| 104 |
*/ |
| 105 |
function vigilante_load_plugin() { |
| 106 |
// Check requirements first |
| 107 |
if ( ! vigilante_check_requirements() ) { |
| 108 |
return; |
| 109 |
} |
| 110 |
|
| 111 |
// Load core classes (no translations used in these) |
| 112 |
require_once VIGILANTE_INCLUDES_DIR . 'class-database.php'; |
| 113 |
require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php'; |
| 114 |
require_once VIGILANTE_INCLUDES_DIR . 'class-ip-utils.php'; |
| 115 |
require_once VIGILANTE_INCLUDES_DIR . 'class-backup-manager.php'; |
| 116 |
require_once VIGILANTE_INCLUDES_DIR . 'class-activator.php'; |
| 117 |
require_once VIGILANTE_INCLUDES_DIR . 'class-deactivator.php'; |
| 118 |
|
| 119 |
// Load security module files (just loading, not initializing) |
| 120 |
require_once VIGILANTE_INCLUDES_DIR . 'class-firewall.php'; |
| 121 |
require_once VIGILANTE_INCLUDES_DIR . 'class-security-headers.php'; |
| 122 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php'; |
| 123 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php'; |
| 124 |
require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php'; |
| 125 |
require_once VIGILANTE_INCLUDES_DIR . 'class-https-enforcer.php'; |
| 126 |
require_once VIGILANTE_INCLUDES_DIR . 'class-rest-api-security.php'; |
| 127 |
require_once VIGILANTE_INCLUDES_DIR . 'class-user-security.php'; |
| 128 |
require_once VIGILANTE_INCLUDES_DIR . 'class-login-security.php'; |
| 129 |
require_once VIGILANTE_INCLUDES_DIR . 'trait-two-factor-session.php'; |
| 130 |
require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-email.php'; |
| 131 |
require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php'; |
| 132 |
require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php'; |
| 133 |
require_once VIGILANTE_INCLUDES_DIR . 'class-comment-security.php'; |
| 134 |
require_once VIGILANTE_INCLUDES_DIR . 'class-head-cleaner.php'; |
| 135 |
require_once VIGILANTE_INCLUDES_DIR . 'class-feed-manager.php'; |
| 136 |
require_once VIGILANTE_INCLUDES_DIR . 'class-activity-log.php'; |
| 137 |
require_once VIGILANTE_INCLUDES_DIR . 'class-audit-alerts.php'; |
| 138 |
require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php'; |
| 139 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 140 |
require_once VIGILANTE_INCLUDES_DIR . 'class-under-attack.php'; |
| 141 |
require_once VIGILANTE_INCLUDES_DIR . 'class-database-backup.php'; |
| 142 |
require_once VIGILANTE_INCLUDES_DIR . 'class-database-prefix.php'; |
| 143 |
require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php'; |
| 144 |
|
| 145 |
// Load admin classes |
| 146 |
if ( is_admin() ) { |
| 147 |
require_once VIGILANTE_ADMIN_DIR . 'class-admin-analyzer-ajax.php'; |
| 148 |
require_once VIGILANTE_ADMIN_DIR . 'class-admin-recovery-ajax.php'; |
| 149 |
require_once VIGILANTE_ADMIN_DIR . 'class-admin-audit-alerts-ajax.php'; |
| 150 |
require_once VIGILANTE_ADMIN_DIR . 'class-admin.php'; |
| 151 |
} |
| 152 |
|
| 153 |
// Weekly Security Analyzer cron (registered even outside admin so it fires on cron hit). |
| 154 |
add_action( 'vigilante_analyzer_weekly_scan', 'vigilante_run_analyzer_cron' ); |
| 155 |
|
| 156 |
// Daily plugin status check (closed-in-wp.org detection). |
| 157 |
add_action( 'vigilante_plugin_status_check', 'vigilante_run_plugin_status_check' ); |
| 158 |
|
| 159 |
// Post-Under Attack scan (one-shot, scheduled by Vigilante_Under_Attack::deactivate). |
| 160 |
add_action( 'vigilante_under_attack_post_scan', 'vigilante_run_post_under_attack_scan' ); |
| 161 |
|
| 162 |
// Initialize core components only - modules will be initialized at init |
| 163 |
add_action( 'init', 'vigilante_init_plugin', 1 ); |
| 164 |
} |
| 165 |
|
| 166 |
/** |
| 167 |
* Initialize plugin at init hook (translations are ready) |
| 168 |
*/ |
| 169 |
function vigilante_init_plugin() { |
| 170 |
Vigilante_Main::get_instance(); |
| 171 |
} |
| 172 |
|
| 173 |
/** |
| 174 |
* Main plugin class - Singleton pattern |
| 175 |
*/ |
| 176 |
final class Vigilante_Main { |
| 177 |
|
| 178 |
/** |
| 179 |
* Single instance of the class |
| 180 |
* |
| 181 |
* @var Vigilante_Main|null |
| 182 |
*/ |
| 183 |
private static $instance = null; |
| 184 |
|
| 185 |
/** |
| 186 |
* Settings instance |
| 187 |
* |
| 188 |
* @var Vigilante_Settings |
| 189 |
*/ |
| 190 |
public $settings; |
| 191 |
|
| 192 |
/** |
| 193 |
* Database instance |
| 194 |
* |
| 195 |
* @var Vigilante_Database |
| 196 |
*/ |
| 197 |
public $database; |
| 198 |
|
| 199 |
/** |
| 200 |
* Activity log instance |
| 201 |
* |
| 202 |
* @var Vigilante_Activity_Log |
| 203 |
*/ |
| 204 |
public $activity_log; |
| 205 |
|
| 206 |
/** |
| 207 |
* Get single instance of the class |
| 208 |
* |
| 209 |
* @return Vigilante_Main |
| 210 |
*/ |
| 211 |
public static function get_instance() { |
| 212 |
if ( null === self::$instance ) { |
| 213 |
self::$instance = new self(); |
| 214 |
} |
| 215 |
return self::$instance; |
| 216 |
} |
| 217 |
|
| 218 |
/** |
| 219 |
* Constructor - private to enforce singleton |
| 220 |
*/ |
| 221 |
private function __construct() { |
| 222 |
$this->init_core(); |
| 223 |
$this->init_modules(); |
| 224 |
$this->init_hooks(); |
| 225 |
} |
| 226 |
|
| 227 |
/** |
| 228 |
* Prevent cloning |
| 229 |
*/ |
| 230 |
private function __clone() {} |
| 231 |
|
| 232 |
/** |
| 233 |
* Prevent unserializing |
| 234 |
* |
| 235 |
* @throws Exception Always throws exception. |
| 236 |
*/ |
| 237 |
public function __wakeup() { |
| 238 |
throw new Exception( 'Cannot unserialize singleton' ); |
| 239 |
} |
| 240 |
|
| 241 |
/** |
| 242 |
* Initialize core components |
| 243 |
*/ |
| 244 |
private function init_core() { |
| 245 |
$this->database = new Vigilante_Database(); |
| 246 |
$this->settings = new Vigilante_Settings(); |
| 247 |
$this->activity_log = new Vigilante_Activity_Log( $this->settings, $this->database ); |
| 248 |
|
| 249 |
// Auto-create/update tables when DB version is outdated (handles file-only updates) |
| 250 |
if ( $this->database->needs_update() ) { |
| 251 |
$this->database->create_tables(); |
| 252 |
} |
| 253 |
|
| 254 |
// One-time cleanup: versions before 2.7.0 wrote config backups (including |
| 255 |
// wp-config.php) as files under wp-content/vigilante-backups/. Those now |
| 256 |
// live in the database, so remove anything left on disk. |
| 257 |
if ( ! get_option( 'vigilante_legacy_backups_cleaned' ) ) { |
| 258 |
Vigilante_Backup_Manager::cleanup_legacy_files(); |
| 259 |
update_option( 'vigilante_legacy_backups_cleaned', 1, false ); |
| 260 |
} |
| 261 |
|
| 262 |
// Once (2.11.6): the copies of wp-config.php, .htaccess and robots.txt |
| 263 |
// that earlier versions kept in the options table, on this site and, on |
| 264 |
// a network, on every site of it. |
| 265 |
Vigilante_Backup_Manager::maybe_purge_stored_copies(); |
| 266 |
|
| 267 |
// One-time migration (2.9.0): add '.css' to File Integrity's excluded |
| 268 |
// extensions on existing installs. Stylesheets are rewritten so often by |
| 269 |
// themes and optimizer plugins that they were the main post-update false |
| 270 |
// positive. New installs get it from the defaults; this brings existing |
| 271 |
// sites in line without touching any other setting. Additive, idempotent. |
| 272 |
if ( ! get_option( 'vigilante_css_exclusion_migrated' ) ) { |
| 273 |
$fi = $this->settings->get_section( 'file_integrity' ); |
| 274 |
if ( is_array( $fi ) ) { |
| 275 |
$ext = ( isset( $fi['excluded_extensions'] ) && is_array( $fi['excluded_extensions'] ) ) |
| 276 |
? $fi['excluded_extensions'] |
| 277 |
: array(); |
| 278 |
if ( ! in_array( '.css', $ext, true ) ) { |
| 279 |
$ext[] = '.css'; |
| 280 |
$fi['excluded_extensions'] = $ext; |
| 281 |
$this->settings->update_section( 'file_integrity', $fi ); |
| 282 |
} |
| 283 |
} |
| 284 |
update_option( 'vigilante_css_exclusion_migrated', 1, false ); |
| 285 |
} |
| 286 |
|
| 287 |
// One-time on upgrade to 2.9.0: drop any cached WordPress.org checksum |
| 288 |
// manifests. The new comparison is array-aware and self-corrects a cached |
| 289 |
// array-md5 value, but a manifest cached by an older version while wp.org |
| 290 |
// was still propagating a new release could otherwise keep producing |
| 291 |
// false "modified" results until it expires (up to 24h). Flushing on |
| 292 |
// upgrade guarantees a clean slate on the very release that fixes them; |
| 293 |
// the next scan refetches fresh manifests. One-time, bulk, no caching. |
| 294 |
if ( ! get_option( 'vigilante_checksum_cache_flushed_290' ) ) { |
| 295 |
global $wpdb; |
| 296 |
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one-time 2.9.0 migration dropping stale checksum transients so the new comparison starts clean. |
| 297 |
"DELETE FROM {$wpdb->options} |
| 298 |
WHERE option_name LIKE '\\_transient\\_vigilante\\_plugin\\_checksums\\_%' |
| 299 |
OR option_name LIKE '\\_transient\\_timeout\\_vigilante\\_plugin\\_checksums\\_%' |
| 300 |
OR option_name LIKE '\\_transient\\_vigilante\\_theme\\_checksums\\_%' |
| 301 |
OR option_name LIKE '\\_transient\\_timeout\\_vigilante\\_theme\\_checksums\\_%' |
| 302 |
OR option_name LIKE '\\_transient\\_vigilante\\_core\\_checksums\\_%' |
| 303 |
OR option_name LIKE '\\_transient\\_timeout\\_vigilante\\_core\\_checksums\\_%'" |
| 304 |
); |
| 305 |
update_option( 'vigilante_checksum_cache_flushed_290', 1, false ); |
| 306 |
} |
| 307 |
} |
| 308 |
|
| 309 |
/** |
| 310 |
* Initialize security modules based on settings |
| 311 |
*/ |
| 312 |
private function init_modules() { |
| 313 |
$options = $this->settings->get_all_options(); |
| 314 |
|
| 315 |
// Self-heal: a UI bug in earlier 2.4.x betas could leave a section's |
| 316 |
// top-level 'enabled' flag set to false because the section forms do |
| 317 |
// not render a checkbox for that field — saving any tab caused the |
| 318 |
// generic save handler to treat the missing field as "unchecked" and |
| 319 |
// store it as false. If the master module toggle on the Dashboard is |
| 320 |
// on but the section flag is off, restore it here so the module's |
| 321 |
// hooks can attach. Idempotent: noop on healthy installs. |
| 322 |
$sections = array( |
| 323 |
'firewall', |
| 324 |
'security_headers', |
| 325 |
'login_security', |
| 326 |
'rest_api_security', |
| 327 |
'user_security', |
| 328 |
'wp_hardening', |
| 329 |
'file_integrity', |
| 330 |
'activity_log', |
| 331 |
); |
| 332 |
$heal_changed = false; |
| 333 |
foreach ( $sections as $section_name ) { |
| 334 |
if ( ! empty( $options['modules'][ $section_name ] ) |
| 335 |
&& isset( $options[ $section_name ] ) |
| 336 |
&& is_array( $options[ $section_name ] ) |
| 337 |
&& array_key_exists( 'enabled', $options[ $section_name ] ) |
| 338 |
&& empty( $options[ $section_name ]['enabled'] ) ) { |
| 339 |
$options[ $section_name ]['enabled'] = true; |
| 340 |
$heal_changed = true; |
| 341 |
} |
| 342 |
} |
| 343 |
if ( $heal_changed ) { |
| 344 |
update_option( Vigilante_Settings::OPTION_NAME, $options ); |
| 345 |
$this->settings->clear_cache(); |
| 346 |
$options = $this->settings->get_all_options(); |
| 347 |
} |
| 348 |
|
| 349 |
// Firewall - runs early to block threats |
| 350 |
if ( ! empty( $options['modules']['firewall'] ) ) { |
| 351 |
new Vigilante_Firewall( $this->settings, $this->activity_log ); |
| 352 |
} |
| 353 |
|
| 354 |
// Security Headers - rules are applied via .htaccess, no runtime hooks needed |
| 355 |
// HTTPS Enforcer still needs runtime hooks |
| 356 |
if ( ! empty( $options['modules']['security_headers'] ) ) { |
| 357 |
new Vigilante_Https_Enforcer( $this->settings ); |
| 358 |
} |
| 359 |
|
| 360 |
// REST API Security |
| 361 |
if ( ! empty( $options['modules']['rest_api_security'] ) ) { |
| 362 |
new Vigilante_Rest_Api_Security( $this->settings ); |
| 363 |
} |
| 364 |
|
| 365 |
// User Security |
| 366 |
if ( ! empty( $options['modules']['user_security'] ) ) { |
| 367 |
new Vigilante_User_Security( $this->settings, $this->activity_log ); |
| 368 |
} else { |
| 369 |
/* |
| 370 |
* Turning the module off must not quietly unlock the accounts it |
| 371 |
* already locked. A forced password reset and a registration waiting |
| 372 |
* for approval are marks written on somebody's account, with their |
| 373 |
* sessions already destroyed and the activity log saying they cannot |
| 374 |
* get in; until 2.11.10 both stopped being enforced the moment this |
| 375 |
* toggle went off and every one of those accounts logged in again |
| 376 |
* with its old password. Only the enforcing half is registered here. |
| 377 |
*/ |
| 378 |
new Vigilante_User_Security( $this->settings, $this->activity_log, true ); |
| 379 |
} |
| 380 |
|
| 381 |
// Login Security |
| 382 |
$login_security = null; |
| 383 |
|
| 384 |
if ( ! empty( $options['modules']['login_security'] ) ) { |
| 385 |
$login_security = new Vigilante_Login_Security( $this->settings, $this->database, $this->activity_log ); |
| 386 |
} |
| 387 |
|
| 388 |
/* |
| 389 |
* Two factor on a network is decided for the whole network, so it has to |
| 390 |
* be ENFORCED on the whole network too. Gating these two on this site's |
| 391 |
* module toggle left the last leg of the bypass open: the administrator |
| 392 |
* of any subsite can turn Login Security off on their own site, which |
| 393 |
* takes them out of the picture but not out of the network, and the |
| 394 |
* session cookie WordPress issues there is valid on every host of it. So |
| 395 |
* a login sent to that subsite registered no second factor check at all |
| 396 |
* and the cookie it handed back opened the main site. Reproduced over |
| 397 |
* HTTP by the second cross review of 2.11.10. |
| 398 |
* |
| 399 |
* Same reasoning, and the same shape, as the enforcement-only User |
| 400 |
* Security above: on a network the enforcing half is registered whatever |
| 401 |
* this site says. On a single site there is no other site to protect and |
| 402 |
* the toggle means what it says. |
| 403 |
*/ |
| 404 |
if ( is_multisite() || null !== $login_security ) { |
| 405 |
new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log, $login_security ); |
| 406 |
new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log, $login_security ); |
| 407 |
} |
| 408 |
|
| 409 |
// WordPress Hardening (includes comments, head cleaner, feeds) |
| 410 |
if ( ! empty( $options['modules']['wp_hardening'] ) ) { |
| 411 |
new Vigilante_Comment_Security( $this->settings ); |
| 412 |
new Vigilante_Head_Cleaner( $this->settings ); |
| 413 |
new Vigilante_Feed_Manager( $this->settings ); |
| 414 |
} |
| 415 |
|
| 416 |
// File Integrity Scanner |
| 417 |
if ( ! empty( $options['modules']['file_integrity'] ) ) { |
| 418 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); |
| 419 |
$file_integrity->init_hooks(); |
| 420 |
$file_integrity->init_cleanup_hooks(); |
| 421 |
|
| 422 |
new Vigilante_Plugin_Status( $this->settings, $this->activity_log ); |
| 423 |
} elseif ( is_admin() ) { |
| 424 |
/* |
| 425 |
* The module is off, and the cleanup goes on anyway. It is not |
| 426 |
* integrity monitoring: it takes out of the database the copy of |
| 427 |
* wp-config.php that earlier versions stored, credentials and all. |
| 428 |
* Turning the module off is not a decision to keep them. |
| 429 |
* |
| 430 |
* Two holes closed here, both reported by @calzbert after reading the |
| 431 |
* 2.11.3 diff. A site with the module off cleaned itself by neither |
| 432 |
* of its own two paths, because both hang off this class. And with |
| 433 |
* the module off on the MAIN site, the network sweep was not |
| 434 |
* registered either, which is what would have reached every other |
| 435 |
* site: the sweep removes each site's option without asking whether |
| 436 |
* the module is on over there. |
| 437 |
* |
| 438 |
* Only in the admin, because both hooks are admin_init and there is |
| 439 |
* nothing to gain from building this on a front-end request. Note |
| 440 |
* that admin-ajax.php fires admin_init too (wp-admin/admin-ajax.php |
| 441 |
* :45), so this also runs on wp_ajax_nopriv_* requests from |
| 442 |
* visitors with no session. That is deliberate and it is what the |
| 443 |
* module has been doing since 2.11.2: the cleanup asks for no |
| 444 |
* capability because it also runs under wp-cron with nobody logged |
| 445 |
* in, and all it does is take the plugin's own copy out of the |
| 446 |
* database. The network sweep, which does reach across sites, is |
| 447 |
* the one that demands manage_network_options. |
| 448 |
*/ |
| 449 |
$file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); |
| 450 |
$file_integrity->init_cleanup_hooks(); |
| 451 |
} |
| 452 |
|
| 453 |
// Activity Log is always initialized (core component) |
| 454 |
// Logging is gated by the modules.activity_log toggle and per-type flags |
| 455 |
|
| 456 |
// Audit Alerts engine - an alerting layer on top of Security Audit. |
| 457 |
// Only instantiated when Security Audit is on, because it reacts to the |
| 458 |
// events the activity log records (a passive subscriber, no per-module |
| 459 |
// coupling). Both alert legs are opt-in, off by default. |
| 460 |
if ( ! empty( $options['modules']['activity_log'] ) ) { |
| 461 |
new Vigilante_Audit_Alerts( $this->settings, $this->activity_log ); |
| 462 |
} |
| 463 |
|
| 464 |
// Under Attack mode - always loaded (independent of modules) |
| 465 |
new Vigilante_Under_Attack( $this->settings, $this->activity_log ); |
| 466 |
|
| 467 |
// Admin interface |
| 468 |
if ( is_admin() ) { |
| 469 |
new Vigilante_Admin( $this->settings, $this->database, $this->activity_log ); |
| 470 |
} |
| 471 |
} |
| 472 |
|
| 473 |
/** |
| 474 |
* Initialize WordPress hooks |
| 475 |
*/ |
| 476 |
private function init_hooks() { |
| 477 |
// Plugin action links |
| 478 |
add_filter( 'plugin_action_links_' . VIGILANTE_PLUGIN_BASENAME, array( $this, 'add_action_links' ) ); |
| 479 |
|
| 480 |
// Scheduled tasks |
| 481 |
add_action( 'vigilante_daily_maintenance', array( $this, 'daily_maintenance' ) ); |
| 482 |
add_action( 'vigilante_hourly_checks', array( $this, 'hourly_checks' ) ); |
| 483 |
|
| 484 |
// AJAX handlers |
| 485 |
add_action( 'wp_ajax_vigilante_dismiss_notice', array( $this, 'ajax_dismiss_notice' ) ); |
| 486 |
|
| 487 |
// Regenerate critical file baseline after Vigilante modifies wp-config.php or .htaccess |
| 488 |
add_action( 'vigilante_critical_file_written', array( $this, 'on_critical_file_written' ) ); |
| 489 |
|
| 490 |
// Keep the server layer in step with the installed version. |
| 491 |
add_action( 'init', array( $this, 'maybe_sync_server_files' ), 20 ); |
| 492 |
} |
| 493 |
|
| 494 |
/** |
| 495 |
* Rewrite the .htaccess block when the installed version has moved on |
| 496 |
* |
| 497 |
* Updating the plugin did not touch the file: the block was only rewritten |
| 498 |
* on activation or when the Headers or Firewall tab was saved. So a fix |
| 499 |
* that lives inside those rules never reached a site that merely updated, |
| 500 |
* which is exactly what happened with the connect-src of 2.9.6: the browser |
| 501 |
* kept receiving the old policy, and image uploads kept failing on |
| 502 |
* WordPress 7.1 until someone pressed Save. This rewrites the block once |
| 503 |
* per version, and picks up the rules that an activation from WP-CLI had to |
| 504 |
* leave pending because it could not tell what server it was on. |
| 505 |
* |
| 506 |
* Only the content between the plugin markers is rewritten, the same part |
| 507 |
* any save has always rewritten. |
| 508 |
* |
| 509 |
* @since 2.9.9 |
| 510 |
*/ |
| 511 |
public function maybe_sync_server_files() { |
| 512 |
$pending = (bool) get_option( 'vigilante_server_files_pending' ); |
| 513 |
|
| 514 |
if ( ! $pending && VIGILANTE_VERSION === get_option( 'vigilante_server_files_version' ) ) { |
| 515 |
return; |
| 516 |
} |
| 517 |
|
| 518 |
// A failed write is not retried on every request. |
| 519 |
if ( (int) get_option( 'vigilante_server_files_retry_after' ) > time() ) { |
| 520 |
return; |
| 521 |
} |
| 522 |
|
| 523 |
/* |
| 524 |
* A subsite has nothing to do here, ever: the file belongs to the main |
| 525 |
* site. Marking it done keeps every request from re-checking. |
| 526 |
* |
| 527 |
* 2.10.0 asked the wrong question at this point and it cost the whole |
| 528 |
* feature on networks. can_write_shared_files() ends in a capability |
| 529 |
* check, and this runs on init for every request, so on a network the |
| 530 |
* branch below was the one nearly every visitor took: it retired the job |
| 531 |
* without having written a thing. The .htaccess was never refreshed after |
| 532 |
* an update, and the one-shot snapshot behind it was consumed without |
| 533 |
* being taken, so not even a network administrator visiting afterwards |
| 534 |
* retried, because the version had already been marked. Reported by |
| 535 |
* @calzbert, who found it reading the code. |
| 536 |
*/ |
| 537 |
if ( ! Vigilante_Settings::owns_shared_files() ) { |
| 538 |
$this->mark_server_files_synced(); |
| 539 |
return; |
| 540 |
} |
| 541 |
|
| 542 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php'; |
| 543 |
$manager = Vigilante_Htaccess_Manager::get_instance(); |
| 544 |
|
| 545 |
if ( ! $manager->is_apache() ) { |
| 546 |
// Still on the command line with nothing to learn from: stay pending. |
| 547 |
if ( $manager->server_is_unknown() ) { |
| 548 |
return; |
| 549 |
} |
| 550 |
|
| 551 |
// Not Apache: there is no block to keep in step. |
| 552 |
$this->mark_server_files_synced(); |
| 553 |
return; |
| 554 |
} |
| 555 |
|
| 556 |
$options = get_option( Vigilante_Settings::OPTION_NAME, array() ); |
| 557 |
$headers = isset( $options['security_headers'] ) ? (array) $options['security_headers'] : array(); |
| 558 |
$failed = false; |
| 559 |
$rewrote = false; |
| 560 |
|
| 561 |
/* |
| 562 |
* Last chance to keep what the file still says. The rewrites below are |
| 563 |
* precisely what overwrites it, and on a site whose header settings the |
| 564 |
* 2.9.8 migration reset, this file is the only remaining copy of what the |
| 565 |
* owner had actually chosen. Captured here rather than inside the write |
| 566 |
* path so it only ever happens on a version change: an ordinary save also |
| 567 |
* leaves the file describing the previous values for an instant, and |
| 568 |
* capturing there would spend the single slot on a difference the owner |
| 569 |
* made deliberately. |
| 570 |
*/ |
| 571 |
$wrote_last = (string) get_option( 'vigilante_server_files_version' ); |
| 572 |
|
| 573 |
/* |
| 574 |
* And only on the very first sync that arrives from a version older than |
| 575 |
* this one. That is the whole window: the file still describes what the |
| 576 |
* owner chose, and the rewrite below is what ends it. Gating on the |
| 577 |
* version also keeps a future release, one that legitimately changes what |
| 578 |
* the block contains, from reading its own improvement as damage and |
| 579 |
* offering to undo it. |
| 580 |
*/ |
| 581 |
/* |
| 582 |
* 2.10.1 and not 2.10.0, deliberately: it gives the networks a second |
| 583 |
* chance. On a network 2.10.0 marked this done without writing anything, |
| 584 |
* so the window closed with the snapshot untaken. But nothing was |
| 585 |
* written, which means the .htaccess on those sites still describes the |
| 586 |
* configuration its owner actually chose. Reopening the window one |
| 587 |
* version wide is what lets them be recovered after all. |
| 588 |
* |
| 589 |
* Harmless where it already worked: a site that took a snapshot is |
| 590 |
* skipped because one exists, and a site that found nothing to take has |
| 591 |
* had its file rewritten to match its settings, so there is still no |
| 592 |
* difference to find. |
| 593 |
*/ |
| 594 |
if ( '' === $wrote_last || version_compare( $wrote_last, '2.10.1', '<' ) ) { |
| 595 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php'; |
| 596 |
Vigilante_Htaccess_Recovery::maybe_capture( $manager->get_content(), $this->settings ); |
| 597 |
} |
| 598 |
|
| 599 |
$needs_protection_block = ! empty( $options['modules']['firewall'] ) |
| 600 |
|| ! empty( $headers['hide_server_signature'] ) |
| 601 |
|| ! empty( $headers['remove_fingerprinting_headers'] ); |
| 602 |
|
| 603 |
/* |
| 604 |
* A 'locked' result is not a failure: another request is doing this very |
| 605 |
* work right now. Returning without marking anything leaves the pending |
| 606 |
* state alone, so whichever request wins finishes the job and this one |
| 607 |
* stays out of the way. Treating it as a failure would arm the one hour |
| 608 |
* backoff for something that is already being handled. |
| 609 |
*/ |
| 610 |
$locked = false; |
| 611 |
$incomplete = false; |
| 612 |
$unreadable = false; |
| 613 |
$settled = array( 'locked', 'block_incomplete', 'read_failed' ); |
| 614 |
|
| 615 |
if ( $needs_protection_block ) { |
| 616 |
require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php'; |
| 617 |
$result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true ); |
| 618 |
$code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' ); |
| 619 |
$locked = $locked || 'locked' === $code; |
| 620 |
$incomplete = $incomplete || 'block_incomplete' === $code; |
| 621 |
$unreadable = $unreadable || 'read_failed' === $code; |
| 622 |
$failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) ); |
| 623 |
$rewrote = true; |
| 624 |
} |
| 625 |
|
| 626 |
if ( ! $locked && ! empty( $options['modules']['security_headers'] ) ) { |
| 627 |
require_once VIGILANTE_INCLUDES_DIR . 'class-security-headers.php'; |
| 628 |
$result = ( new Vigilante_Security_Headers( $this->settings ) )->apply_rules( true ); |
| 629 |
$code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' ); |
| 630 |
$locked = $locked || 'locked' === $code; |
| 631 |
$incomplete = $incomplete || 'block_incomplete' === $code; |
| 632 |
$unreadable = $unreadable || 'read_failed' === $code; |
| 633 |
$failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) ); |
| 634 |
$rewrote = true; |
| 635 |
} |
| 636 |
|
| 637 |
if ( $locked ) { |
| 638 |
return; |
| 639 |
} |
| 640 |
|
| 641 |
if ( $failed ) { |
| 642 |
update_option( 'vigilante_server_files_retry_after', time() + HOUR_IN_SECONDS ); |
| 643 |
|
| 644 |
// A refusal to write the server rules is exactly the kind of thing |
| 645 |
// that used to happen in silence, so it is recorded and retried in |
| 646 |
// an hour instead of being forgotten. |
| 647 |
if ( $this->activity_log ) { |
| 648 |
$this->activity_log->log( |
| 649 |
'system', |
| 650 |
'server_rules_write_failed', |
| 651 |
__( 'The .htaccess rules could not be rewritten after the update. Vigilant will try again in an hour; if the file is read only, fix its permissions or save the Firewall or Headers tab once.', 'vigilante' ), |
| 652 |
array( 'version' => VIGILANTE_VERSION ), |
| 653 |
'warning' |
| 654 |
); |
| 655 |
} |
| 656 |
|
| 657 |
return; |
| 658 |
} |
| 659 |
|
| 660 |
/* |
| 661 |
* A block with a BEGIN line and no END is not going to mend itself, so |
| 662 |
* retrying every hour would only repeat the refusal: it is recorded once |
| 663 |
* for this version, with what to do about it, and the job is marked done. |
| 664 |
* Saving the Firewall or Headers tab after fixing the file writes the |
| 665 |
* rules again. |
| 666 |
*/ |
| 667 |
if ( $incomplete && $this->activity_log ) { |
| 668 |
$this->activity_log->log( |
| 669 |
'system', |
| 670 |
'server_rules_block_incomplete', |
| 671 |
__( 'The .htaccess rules were not rewritten after the update because a Vigilant block in that file has a BEGIN line without its END, and rewriting it would have cut everything below it. Remove the broken block by hand, then save the Firewall or Headers tab.', 'vigilante' ), |
| 672 |
array( 'version' => VIGILANTE_VERSION ), |
| 673 |
'warning' |
| 674 |
); |
| 675 |
} |
| 676 |
|
| 677 |
/* |
| 678 |
* Same for a .htaccess that PHP can write but not read: since 2.11.8 it is |
| 679 |
* left as it is rather than replaced by the Vigilant rules alone, and |
| 680 |
* that does not mend itself either. The first version of that fix left |
| 681 |
* it to the hourly retry, with a message about read only files; found by |
| 682 |
* the cross review of 2.11.8. |
| 683 |
*/ |
| 684 |
if ( $unreadable && $this->activity_log ) { |
| 685 |
$this->activity_log->log( |
| 686 |
'system', |
| 687 |
'server_rules_read_failed', |
| 688 |
__( 'The .htaccess rules were not rewritten after the update because PHP can write that file but cannot read it, and writing it without reading it would have removed every other rule in it. Let PHP read the file, then save the Firewall or Headers tab.', 'vigilante' ), |
| 689 |
array( 'version' => VIGILANTE_VERSION ), |
| 690 |
'warning' |
| 691 |
); |
| 692 |
} |
| 693 |
|
| 694 |
$this->mark_server_files_synced(); |
| 695 |
|
| 696 |
if ( $rewrote && ! $incomplete && ! $unreadable && $this->activity_log ) { |
| 697 |
$this->activity_log->log( |
| 698 |
'system', |
| 699 |
'server_rules_refreshed', |
| 700 |
sprintf( |
| 701 |
/* translators: %s: plugin version. */ |
| 702 |
__( 'The .htaccess rules were rewritten to match Vigilant %s.', 'vigilante' ), |
| 703 |
VIGILANTE_VERSION |
| 704 |
), |
| 705 |
array( 'version' => VIGILANTE_VERSION ), |
| 706 |
'info' |
| 707 |
); |
| 708 |
} |
| 709 |
} |
| 710 |
|
| 711 |
/** |
| 712 |
* Record that the server layer matches the installed version |
| 713 |
* |
| 714 |
* @since 2.9.9 |
| 715 |
*/ |
| 716 |
private function mark_server_files_synced() { |
| 717 |
update_option( 'vigilante_server_files_version', VIGILANTE_VERSION ); |
| 718 |
delete_option( 'vigilante_server_files_pending' ); |
| 719 |
delete_option( 'vigilante_server_files_retry_after' ); |
| 720 |
} |
| 721 |
|
| 722 |
/** |
| 723 |
* Update the critical file baseline after Vigilante writes to a monitored file |
| 724 |
* |
| 725 |
* @param string $filename File that was modified (e.g. 'wp-config.php'). |
| 726 |
*/ |
| 727 |
public function on_critical_file_written( $filename ) { |
| 728 |
if ( ! class_exists( 'Vigilante_File_Integrity' ) ) { |
| 729 |
require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php'; |
| 730 |
} |
| 731 |
|
| 732 |
$fi = new Vigilante_File_Integrity( $this->settings, $this->database, $this->activity_log ); |
| 733 |
$fi->update_critical_file_baseline( $filename ); |
| 734 |
} |
| 735 |
|
| 736 |
/** |
| 737 |
* Add plugin action links |
| 738 |
* |
| 739 |
* @param array $links Existing links. |
| 740 |
* @return array Modified links. |
| 741 |
*/ |
| 742 |
public function add_action_links( $links ) { |
| 743 |
$plugin_links = array( |
| 744 |
'<a href="' . esc_url( admin_url( 'admin.php?page=vigilante' ) ) . '">' . esc_html__( 'Security Settings', 'vigilante' ) . '</a>', |
| 745 |
); |
| 746 |
return array_merge( $plugin_links, $links ); |
| 747 |
} |
| 748 |
|
| 749 |
/** |
| 750 |
* Daily maintenance tasks |
| 751 |
*/ |
| 752 |
public function daily_maintenance() { |
| 753 |
// Clean old activity logs |
| 754 |
$this->activity_log->cleanup_old_logs(); |
| 755 |
|
| 756 |
// Clean old login attempts |
| 757 |
$this->database->cleanup_old_login_attempts(); |
| 758 |
|
| 759 |
// Clean expired 2FA codes and trusted devices |
| 760 |
$this->database->cleanup_expired_2fa_codes(); |
| 761 |
$this->database->cleanup_expired_trusted_devices(); |
| 762 |
|
| 763 |
// Remove sensitive files (readme.html, license.txt, licencia.txt) |
| 764 |
// WordPress core updates recreate these files, so we clean them daily. |
| 765 |
// They sit in the root every site of a network shares, so only the main |
| 766 |
// site removes them, from its own settings; until 2.11.6 the daily |
| 767 |
// maintenance of any site did. |
| 768 |
$advanced = Vigilante_Settings::owns_shared_files() ? $this->settings->get_section( 'advanced' ) : array(); |
| 769 |
if ( ! empty( $advanced['remove_readme'] ) ) { |
| 770 |
$readme_path = ABSPATH . 'readme.html'; |
| 771 |
if ( file_exists( $readme_path ) ) { |
| 772 |
wp_delete_file( $readme_path ); |
| 773 |
} |
| 774 |
} |
| 775 |
if ( ! empty( $advanced['remove_license'] ) ) { |
| 776 |
$license_files = array( 'license.txt', 'licencia.txt' ); |
| 777 |
foreach ( $license_files as $license_file ) { |
| 778 |
$license_path = ABSPATH . $license_file; |
| 779 |
if ( file_exists( $license_path ) ) { |
| 780 |
wp_delete_file( $license_path ); |
| 781 |
} |
| 782 |
} |
| 783 |
} |
| 784 |
|
| 785 |
// Log maintenance |
| 786 |
$this->activity_log->log( 'system', 'maintenance', __( 'Daily maintenance completed', 'vigilante' ) ); |
| 787 |
} |
| 788 |
|
| 789 |
/** |
| 790 |
* Hourly checks |
| 791 |
*/ |
| 792 |
public function hourly_checks() { |
| 793 |
// File integrity scans are handled by the File_Integrity class own cron schedule |
| 794 |
// based on the configured scan_frequency (daily/weekly). |
| 795 |
} |
| 796 |
|
| 797 |
/** |
| 798 |
* AJAX handler for dismissing notices |
| 799 |
*/ |
| 800 |
public function ajax_dismiss_notice() { |
| 801 |
check_ajax_referer( 'vigilante_dismiss_notice', 'nonce' ); |
| 802 |
|
| 803 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 804 |
wp_die( -1 ); |
| 805 |
} |
| 806 |
|
| 807 |
$notice_id = isset( $_POST['notice_id'] ) ? sanitize_key( $_POST['notice_id'] ) : ''; |
| 808 |
|
| 809 |
if ( $notice_id ) { |
| 810 |
$dismissed = get_option( 'vigilante_dismissed_notices', array() ); |
| 811 |
$dismissed[ $notice_id ] = time(); |
| 812 |
update_option( 'vigilante_dismissed_notices', $dismissed ); |
| 813 |
} |
| 814 |
|
| 815 |
wp_send_json_success(); |
| 816 |
} |
| 817 |
} |
| 818 |
|
| 819 |
/** |
| 820 |
* Cron handler for the weekly Security Analyzer scan. |
| 821 |
* |
| 822 |
* Resolves the shared Vigilante_Security_Analyzer (lazily; no cost when the |
| 823 |
* cron is not firing) and lets it run the scan + regression email logic. |
| 824 |
*/ |
| 825 |
function vigilante_run_analyzer_cron() { |
| 826 |
if ( ! class_exists( 'Vigilante_Security_Analyzer' ) ) { |
| 827 |
require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php'; |
| 828 |
} |
| 829 |
if ( ! class_exists( 'Vigilante_Settings' ) ) { |
| 830 |
require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php'; |
| 831 |
} |
| 832 |
|
| 833 |
$settings = new Vigilante_Settings(); |
| 834 |
$activity_log = null; |
| 835 |
if ( class_exists( 'Vigilante_Activity_Log' ) && class_exists( 'Vigilante_Database' ) ) { |
| 836 |
$database = new Vigilante_Database(); |
| 837 |
$activity_log = new Vigilante_Activity_Log( $settings, $database ); |
| 838 |
} |
| 839 |
|
| 840 |
$analyzer = new Vigilante_Security_Analyzer( $settings, $activity_log ); |
| 841 |
$analyzer->cron_weekly_scan(); |
| 842 |
} |
| 843 |
|
| 844 |
/** |
| 845 |
* Cron handler for the daily plugin status check. |
| 846 |
* |
| 847 |
* Resolves the shared Vigilante_Plugin_Status lazily so the daily cron has no |
| 848 |
* cost while it is not firing. |
| 849 |
*/ |
| 850 |
function vigilante_run_plugin_status_check() { |
| 851 |
if ( ! class_exists( 'Vigilante_Plugin_Status' ) ) { |
| 852 |
require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php'; |
| 853 |
} |
| 854 |
if ( ! class_exists( 'Vigilante_Settings' ) ) { |
| 855 |
require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php'; |
| 856 |
} |
| 857 |
|
| 858 |
$settings = new Vigilante_Settings(); |
| 859 |
$activity_log = null; |
| 860 |
if ( class_exists( 'Vigilante_Activity_Log' ) && class_exists( 'Vigilante_Database' ) ) { |
| 861 |
$database = new Vigilante_Database(); |
| 862 |
$activity_log = new Vigilante_Activity_Log( $settings, $database ); |
| 863 |
} |
| 864 |
|
| 865 |
$checker = new Vigilante_Plugin_Status( $settings, $activity_log ); |
| 866 |
$checker->run_scheduled_check(); |
| 867 |
} |
| 868 |
|
| 869 |
/** |
| 870 |
* Run a Security Analyzer full scan after Under Attack mode deactivates. |
| 871 |
* |
| 872 |
* Scheduled one-shot from Vigilante_Under_Attack::deactivate() so the dashboard |
| 873 |
* reflects the restored configuration with the slow HTTP/header probes the |
| 874 |
* mode prevented from running safely while it was active. |
| 875 |
*/ |
| 876 |
function vigilante_run_post_under_attack_scan() { |
| 877 |
if ( ! class_exists( 'Vigilante_Under_Attack' ) ) { |
| 878 |
require_once VIGILANTE_INCLUDES_DIR . 'class-under-attack.php'; |
| 879 |
} |
| 880 |
if ( ! class_exists( 'Vigilante_Settings' ) ) { |
| 881 |
require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php'; |
| 882 |
} |
| 883 |
|
| 884 |
$settings = new Vigilante_Settings(); |
| 885 |
$activity_log = null; |
| 886 |
if ( class_exists( 'Vigilante_Activity_Log' ) && class_exists( 'Vigilante_Database' ) ) { |
| 887 |
$database = new Vigilante_Database(); |
| 888 |
$activity_log = new Vigilante_Activity_Log( $settings, $database ); |
| 889 |
} |
| 890 |
|
| 891 |
$under_attack = new Vigilante_Under_Attack( $settings, $activity_log ); |
| 892 |
$under_attack->run_analyzer_scan( 'all' ); |
| 893 |
} |
| 894 |
|
| 895 |
/** |
| 896 |
* Plugin activation hook |
| 897 |
*/ |
| 898 |
function vigilante_activate() { |
| 899 |
require_once VIGILANTE_INCLUDES_DIR . 'class-database.php'; |
| 900 |
require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php'; |
| 901 |
require_once VIGILANTE_INCLUDES_DIR . 'class-backup-manager.php'; |
| 902 |
require_once VIGILANTE_INCLUDES_DIR . 'class-activator.php'; |
| 903 |
|
| 904 |
Vigilante_Activator::activate(); |
| 905 |
} |
| 906 |
register_activation_hook( __FILE__, 'vigilante_activate' ); |
| 907 |
|
| 908 |
/** |
| 909 |
* Plugin deactivation hook |
| 910 |
* |
| 911 |
* @param bool $network_wide Whether core is deactivating the plugin for the whole network. |
| 912 |
*/ |
| 913 |
function vigilante_deactivate( $network_wide = false ) { |
| 914 |
require_once VIGILANTE_INCLUDES_DIR . 'class-database.php'; |
| 915 |
require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php'; |
| 916 |
require_once VIGILANTE_INCLUDES_DIR . 'class-backup-manager.php'; |
| 917 |
require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php'; |
| 918 |
require_once VIGILANTE_INCLUDES_DIR . 'class-deactivator.php'; |
| 919 |
|
| 920 |
Vigilante_Deactivator::deactivate( (bool) $network_wide ); |
| 921 |
} |
| 922 |
register_deactivation_hook( __FILE__, 'vigilante_deactivate' ); |
| 923 |
|
| 924 |
/** |
| 925 |
* Initialize plugin after WordPress loads |
| 926 |
*/ |
| 927 |
add_action( 'plugins_loaded', 'vigilante_load_plugin' ); |
| 928 |
|
| 929 |
/* |
| 930 |
* The hidden wp-admin is answered as early as the request can be judged with |
| 931 |
* certainty, before the theme and the other plugins load. The modules are built |
| 932 |
* on init priority 1, so until 2.9.9 a request that was going to be refused had |
| 933 |
* already paid for the whole boot. |
| 934 |
*/ |
| 935 |
add_action( 'plugins_loaded', 'vigilante_block_hidden_admin_early', 1 ); |
| 936 |
|
| 937 |
/** |
| 938 |
* Cheap gate for the early hidden wp-admin rejection |
| 939 |
* |
| 940 |
* Everything that can be decided without loading a single plugin class is |
| 941 |
* decided here, so the usual request pays nothing more than a couple of |
| 942 |
* comparisons and one option read that WordPress has already cached. |
| 943 |
* |
| 944 |
* @since 2.9.9 |
| 945 |
*/ |
| 946 |
function vigilante_block_hidden_admin_early() { |
| 947 |
if ( ! is_admin() ) { |
| 948 |
return; |
| 949 |
} |
| 950 |
|
| 951 |
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET'; |
| 952 |
|
| 953 |
// POST is how remote managers authenticate, and the later path lets it through too. |
| 954 |
if ( 'GET' !== $method ) { |
| 955 |
return; |
| 956 |
} |
| 957 |
|
| 958 |
$options = get_option( 'vigilante_options', array() ); |
| 959 |
|
| 960 |
if ( ! is_array( $options ) |
| 961 |
|| empty( $options['modules']['login_security'] ) |
| 962 |
|| empty( $options['login_security']['custom_login_url'] ) ) { |
| 963 |
return; |
| 964 |
} |
| 965 |
|
| 966 |
require_once VIGILANTE_INCLUDES_DIR . 'class-ip-utils.php'; |
| 967 |
require_once VIGILANTE_INCLUDES_DIR . 'class-login-security.php'; |
| 968 |
|
| 969 |
Vigilante_Login_Security::maybe_block_hidden_admin_early( $options ); |
| 970 |
} |
| 971 |
|
| 972 |
/** |
| 973 |
* Helper function to get plugin instance |
| 974 |
* |
| 975 |
* @return Vigilante_Main |
| 976 |
*/ |
| 977 |
function vigilante() { |
| 978 |
return Vigilante_Main::get_instance(); |
| 979 |
} |