PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / includes / class-activator.php

class-activator.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.9.8, at includes/class-activator.php

482 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Activator Class
4 *
5 * Handles plugin activation tasks
6 *
7 * @package Vigilante
8 */
9
10 // Prevent direct access
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class Vigilante_Activator
17 *
18 * Fired during plugin activation
19 */
20 class Vigilante_Activator {
21
22 /**
23 * Run activation tasks
24 */
25 public static function activate() {
26 // Start output buffering to prevent any accidental output
27 ob_start();
28
29 // Check requirements first
30 if ( ! self::check_requirements() ) {
31 ob_end_clean();
32 return;
33 }
34
35 // Create database tables
36 $database = new Vigilante_Database();
37 $database->create_tables();
38
39 // Initialize default settings
40 $settings = new Vigilante_Settings();
41 $current_options = get_option( Vigilante_Settings::OPTION_NAME );
42
43 if ( false === $current_options ) {
44 // First installation - set defaults
45 $first_run = $settings->get_default_options();
46
47 /*
48 * XML-RPC on a brand new install: block the pingback methods, which is
49 * what gets abused for amplification, and leave the rest reachable so
50 * the WordPress app, Jetpack or a remote manager keep working out of
51 * the box. Disabling it completely is the stricter choice and the one
52 * the settings screen recommends, but it is not imposed on a site that
53 * never asked for it. Brute force through XML-RPC stays covered either
54 * way, because those logins go through wp_authenticate() and the login
55 * lockout hooks into it.
56 *
57 * Only written here, on a first installation. Sites upgrading keep
58 * whatever they had: the activation hook does not run on an update, and
59 * Vigilante_Comment_Security::resolve_xmlrpc_mode() answers 'full' when
60 * nothing is stored, which is what every version since 1.0.0 did.
61 */
62 $first_run = Vigilante_Settings::apply_install_tweaks( $first_run );
63
64 update_option( Vigilante_Settings::OPTION_NAME, $first_run );
65 // Refresh settings instance to get new values
66 $settings->clear_cache();
67 $settings = new Vigilante_Settings();
68 } else {
69 // Existing installation - run idempotent migrations
70 if ( self::run_migrations( $current_options ) ) {
71 $settings->clear_cache();
72 $settings = new Vigilante_Settings();
73 }
74 }
75
76 // Create backup of current files FIRST (before any modifications)
77 self::create_activation_backup( $settings );
78
79 // Apply htaccess protection (part of firewall module)
80 if ( $settings->is_module_enabled( 'firewall' ) ) {
81 self::apply_htaccess_protection( $settings );
82 }
83
84 // Apply security headers to htaccess
85 if ( $settings->is_module_enabled( 'security_headers' ) ) {
86 self::apply_security_headers( $settings );
87 }
88
89 // Apply wp-config security (part of wp_hardening module)
90 if ( $settings->is_module_enabled( 'wp_hardening' ) ) {
91 self::apply_wpconfig_security( $settings );
92 }
93
94 // Update WordPress options for HTTPS (part of security_headers module)
95 if ( $settings->is_module_enabled( 'security_headers' ) ) {
96 self::enforce_https( $settings );
97 }
98
99 // Apply comment security settings (part of wp_hardening module)
100 if ( $settings->is_module_enabled( 'wp_hardening' ) ) {
101 self::apply_comment_security( $settings );
102 }
103
104 // Remove sensitive files
105 self::remove_sensitive_files( $settings );
106
107 // Generate critical config files baseline (after all Vigilante writes above)
108 self::generate_critical_baseline( $settings );
109
110 // Schedule cron events
111 self::schedule_events();
112
113 // Set activation transient for admin notice
114 set_transient( 'vigilante_activated', true, 30 );
115
116 // Store activation time
117 update_option( 'vigilante_activated_time', time() );
118
119 // Send activation email if enabled
120 self::send_activation_email( $settings );
121
122 // Flush rewrite rules
123 flush_rewrite_rules();
124
125 // Clean any output that may have been generated
126 ob_end_clean();
127 }
128
129 /**
130 * Idempotent migrations for existing installations.
131 *
132 * @param array $current_options Current vigilante_options array.
133 * @return bool True if any migration changed the stored option.
134 */
135 private static function run_migrations( $current_options ) {
136 $changed = false;
137
138 // Migration: rest_api_security.mode legacy value 'authenticated'
139 // (UI bug shipped a <select> value that did not match the backend
140 // string 'authenticated_only', so manual saves wrote a value the
141 // module ignored). Normalise so the option is honoured again.
142 if ( isset( $current_options['rest_api_security']['mode'] )
143 && 'authenticated' === $current_options['rest_api_security']['mode'] ) {
144 $current_options['rest_api_security']['mode'] = 'authenticated_only';
145 $changed = true;
146 }
147
148 // Migration: rest_api_security.protected_endpoints used to default to
149 // ['/wp/v2/users'], which duplicated the "Block user enumeration"
150 // toggle and confused users (turning that toggle off didn't unblock
151 // /users because protected_endpoints kept it locked in selective
152 // mode). If the saved list is still the legacy single-element default,
153 // empty it out so there is one knob per behaviour. Custom lists
154 // (anything other than exactly ['/wp/v2/users']) are left untouched.
155 if ( isset( $current_options['rest_api_security']['protected_endpoints'] )
156 && is_array( $current_options['rest_api_security']['protected_endpoints'] )
157 && array( '/wp/v2/users' ) === array_values( $current_options['rest_api_security']['protected_endpoints'] ) ) {
158 $current_options['rest_api_security']['protected_endpoints'] = array();
159 $changed = true;
160 }
161
162 // Migration: section-level 'enabled' flag wrongly stored as false.
163 // Earlier 2.4.x betas had a UI save handler that treated the absence
164 // of a field in the form as "checkbox unchecked" — including the
165 // top-level 'enabled' master flag, which has no checkbox in any
166 // section form. This left modules silently disabled even though the
167 // Dashboard master toggle was on. Restore the flag where it makes
168 // sense (master toggle on + flag false).
169 $sections = array(
170 'firewall',
171 'security_headers',
172 'login_security',
173 'rest_api_security',
174 'user_security',
175 'wp_hardening',
176 'file_integrity',
177 'activity_log',
178 );
179 foreach ( $sections as $section_name ) {
180 if ( ! empty( $current_options['modules'][ $section_name ] )
181 && isset( $current_options[ $section_name ] )
182 && is_array( $current_options[ $section_name ] )
183 && array_key_exists( 'enabled', $current_options[ $section_name ] )
184 && empty( $current_options[ $section_name ]['enabled'] ) ) {
185 $current_options[ $section_name ]['enabled'] = true;
186 $changed = true;
187 }
188 }
189
190 if ( $changed ) {
191 update_option( Vigilante_Settings::OPTION_NAME, $current_options );
192 }
193
194 return $changed;
195 }
196
197 /**
198 * Check minimum requirements
199 *
200 * @return bool
201 */
202 private static function check_requirements() {
203 // PHP version check
204 if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
205 add_action( 'admin_notices', function() {
206 printf(
207 '<div class="notice notice-error"><p>%s</p></div>',
208 esc_html__( 'Vigilant requires PHP 7.4 or higher.', 'vigilante' )
209 );
210 });
211 return false;
212 }
213
214 // WordPress version check
215 global $wp_version;
216 if ( version_compare( $wp_version, '5.0', '<' ) ) {
217 add_action( 'admin_notices', function() {
218 printf(
219 '<div class="notice notice-error"><p>%s</p></div>',
220 esc_html__( 'Vigilant requires WordPress 5.0 or higher.', 'vigilante' )
221 );
222 });
223 return false;
224 }
225
226 return true;
227 }
228
229 /**
230 * Create backup of important files
231 *
232 * @param Vigilante_Settings $settings Settings instance.
233 */
234 private static function create_activation_backup( $settings ) {
235 require_once VIGILANTE_INCLUDES_DIR . 'class-backup-manager.php';
236
237 $backup_manager = new Vigilante_Backup_Manager();
238 $result = $backup_manager->create_backups();
239
240 if ( is_wp_error( $result ) ) {
241 // Store error for admin notice
242 set_transient( 'vigilante_backup_error', $result->get_error_message(), 60 );
243 }
244 }
245
246 /**
247 * Apply htaccess protection
248 *
249 * @param Vigilante_Settings $settings Settings instance.
250 */
251 private static function apply_htaccess_protection( $settings ) {
252 // Only apply if Apache server
253 if ( ! self::is_apache() ) {
254 return;
255 }
256
257 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
258
259 $htaccess = new Vigilante_Htaccess_Protection( $settings );
260 $htaccess->apply_rules();
261 }
262
263 /**
264 * Apply security headers to htaccess
265 *
266 * @param Vigilante_Settings $settings Settings instance.
267 */
268 private static function apply_security_headers( $settings ) {
269 // Only apply if Apache server
270 if ( ! self::is_apache() ) {
271 return;
272 }
273
274 require_once VIGILANTE_INCLUDES_DIR . 'class-security-headers.php';
275
276 $security_headers = new Vigilante_Security_Headers( $settings );
277 $security_headers->apply_rules();
278 }
279
280 /**
281 * Apply wp-config security
282 *
283 * @param Vigilante_Settings $settings Settings instance.
284 */
285 private static function apply_wpconfig_security( $settings ) {
286 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
287
288 $wpconfig = new Vigilante_Wpconfig_Security( $settings );
289 $wpconfig->apply_security_constants();
290 }
291
292 /**
293 * Enforce HTTPS in WordPress settings
294 *
295 * @param Vigilante_Settings $settings Settings instance.
296 */
297 private static function enforce_https( $settings ) {
298 $options = $settings->get_section( 'security_headers' );
299
300 if ( empty( $options['force_https'] ) ) {
301 return;
302 }
303
304 /*
305 * Only rewrite the URLs when the request doing the activation is itself
306 * running over HTTPS, which proves the site answers over it. Without this
307 * check, activating on an HTTP-only site pointed it at an address that
308 * may not respond, locking the owner out of their own admin. is_ssl() is
309 * also false under WP-CLI, where there is no request to learn from, so a
310 * command-line activation leaves the URLs alone as well.
311 */
312 if ( ! is_ssl() ) {
313 return;
314 }
315
316 // Check if already HTTPS
317 $site_url = get_option( 'siteurl' );
318 $home_url = get_option( 'home' );
319
320 // Update to HTTPS if not already
321 if ( strpos( $site_url, 'https://' ) === false ) {
322 update_option( 'siteurl', str_replace( 'http://', 'https://', $site_url ) );
323 }
324
325 if ( strpos( $home_url, 'https://' ) === false ) {
326 update_option( 'home', str_replace( 'http://', 'https://', $home_url ) );
327 }
328 }
329
330 /**
331 * Remove sensitive files from WordPress root
332 *
333 * @param Vigilante_Settings $settings Settings instance.
334 */
335 private static function remove_sensitive_files( $settings ) {
336 $advanced = $settings->get_section( 'advanced' );
337
338 // Remove readme.html
339 if ( ! empty( $advanced['remove_readme'] ) ) {
340 $readme_path = ABSPATH . 'readme.html';
341 if ( file_exists( $readme_path ) ) {
342 wp_delete_file( $readme_path );
343 }
344 }
345
346 // Remove license.txt / licencia.txt (Spanish locale)
347 if ( ! empty( $advanced['remove_license'] ) ) {
348 $license_files = array( 'license.txt', 'licencia.txt' );
349 foreach ( $license_files as $license_file ) {
350 $license_path = ABSPATH . $license_file;
351 if ( file_exists( $license_path ) ) {
352 wp_delete_file( $license_path );
353 }
354 }
355 }
356 }
357
358 /**
359 * Generate initial baseline hashes for critical config files
360 *
361 * Called once during activation, after Vigilante has written its own
362 * blocks to wp-config.php and .htaccess. The baseline stores the
363 * normalized hash (excluding Vigilante blocks) so that subsequent
364 * scans can detect unauthorized external modifications.
365 *
366 * @param Vigilante_Settings $settings Settings instance.
367 */
368 private static function generate_critical_baseline( $settings ) {
369 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
370 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
371 }
372
373 $database = new Vigilante_Database();
374 $activity_log = null; // Not needed for baseline generation
375
376 $fi = new Vigilante_File_Integrity( $settings, $database, $activity_log );
377 $fi->regenerate_all_baselines();
378 }
379
380 /**
381 * Schedule cron events
382 */
383 private static function schedule_events() {
384 // Daily maintenance
385 if ( ! wp_next_scheduled( 'vigilante_daily_maintenance' ) ) {
386 wp_schedule_event( time(), 'daily', 'vigilante_daily_maintenance' );
387 }
388
389 // Hourly checks
390 if ( ! wp_next_scheduled( 'vigilante_hourly_checks' ) ) {
391 wp_schedule_event( time(), 'hourly', 'vigilante_hourly_checks' );
392 }
393
394 // Weekly security analyzer scan
395 if ( ! wp_next_scheduled( 'vigilante_analyzer_weekly_scan' ) ) {
396 wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'vigilante_analyzer_weekly_scan' );
397 }
398
399 // Daily plugin status check (closed-in-wp.org detection)
400 if ( ! wp_next_scheduled( 'vigilante_plugin_status_check' ) ) {
401 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'vigilante_plugin_status_check' );
402 }
403 }
404
405 /**
406 * Send activation notification email
407 *
408 * @param Vigilante_Settings $settings Settings instance.
409 */
410 private static function send_activation_email( $settings ) {
411 $email_settings = $settings->get_section( 'email' );
412
413 if ( empty( $email_settings['send_activation_email'] ) ) {
414 return;
415 }
416
417 if ( ! class_exists( 'Vigilante_Email_Template' ) ) {
418 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
419 }
420
421 $to = Vigilante_Email_Template::get_admin_recipients();
422
423 $site_name = get_bloginfo( 'name' );
424 $site_url = get_site_url();
425
426 $subject = sprintf(
427 /* translators: %s: Site name */
428 __( '[%s] Vigilant Activated', 'vigilante' ),
429 $site_name
430 );
431
432 $body = Vigilante_Email_Template::p( __( 'Vigilant has been activated on your website. All security modules are now enabled with default settings.', 'vigilante' ) );
433 $body .= Vigilante_Email_Template::data_table( array(
434 __( 'Site', 'vigilante' ) => $site_name,
435 __( 'URL', 'vigilante' ) => $site_url,
436 __( 'Date', 'vigilante' ) => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
437 ) );
438 $body .= Vigilante_Email_Template::info_box( __( 'Please review the settings in your WordPress admin panel.', 'vigilante' ) );
439 $body .= Vigilante_Email_Template::button( admin_url( 'admin.php?page=vigilante' ), __( 'Go to Vigilant', 'vigilante' ) );
440
441 Vigilante_Email_Template::send( $to, $subject, __( 'Plugin activated', 'vigilante' ), $body );
442 }
443
444 /**
445 * Apply comment security settings to WordPress options
446 *
447 * @param Vigilante_Settings $settings Settings instance.
448 */
449 private static function apply_comment_security( $settings ) {
450 $options = $settings->get_section( 'wp_hardening' );
451
452 // Disable pingbacks
453 if ( ! empty( $options['disable_pingbacks'] ) ) {
454 update_option( 'default_pingback_flag', 0 );
455 update_option( 'default_ping_status', 'closed' );
456 }
457
458 // Disable trackbacks
459 if ( ! empty( $options['disable_trackbacks'] ) ) {
460 update_option( 'default_ping_status', 'closed' );
461 }
462
463 // Require comment moderation
464 if ( ! empty( $options['require_comment_moderation'] ) ) {
465 update_option( 'comment_moderation', 1 );
466 }
467 }
468
469 /**
470 * Check if server is Apache
471 *
472 * @return bool
473 */
474 private static function is_apache() {
475 if ( ! function_exists( 'apache_get_modules' ) ) {
476 // Check server software
477 $server = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '';
478 return stripos( $server, 'apache' ) !== false || stripos( $server, 'litespeed' ) !== false;
479 }
480 return true;
481 }
482 }