PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.9.7
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.9.7
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.7, at includes/class-activator.php

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