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

522 lines 19.3 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 self::mark_server_files_pending();
255 return;
256 }
257
258 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
259
260 $htaccess = new Vigilante_Htaccess_Protection( $settings );
261 $htaccess->apply_rules();
262 }
263
264 /**
265 * Apply security headers to htaccess
266 *
267 * @param Vigilante_Settings $settings Settings instance.
268 */
269 private static function apply_security_headers( $settings ) {
270 // Only apply if Apache server
271 if ( ! self::is_apache() ) {
272 self::mark_server_files_pending();
273 return;
274 }
275
276 require_once VIGILANTE_INCLUDES_DIR . 'class-security-headers.php';
277
278 $security_headers = new Vigilante_Security_Headers( $settings );
279 $security_headers->apply_rules();
280 }
281
282 /**
283 * Apply wp-config security
284 *
285 * @param Vigilante_Settings $settings Settings instance.
286 */
287 private static function apply_wpconfig_security( $settings ) {
288 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
289
290 $wpconfig = new Vigilante_Wpconfig_Security( $settings );
291 $wpconfig->apply_security_constants();
292 }
293
294 /**
295 * Enforce HTTPS in WordPress settings
296 *
297 * @param Vigilante_Settings $settings Settings instance.
298 */
299 private static function enforce_https( $settings ) {
300 $options = $settings->get_section( 'security_headers' );
301
302 if ( empty( $options['force_https'] ) ) {
303 return;
304 }
305
306 /*
307 * Only rewrite the URLs when the request doing the activation is itself
308 * running over HTTPS, which proves the site answers over it. Without this
309 * check, activating on an HTTP-only site pointed it at an address that
310 * may not respond, locking the owner out of their own admin. is_ssl() is
311 * also false under WP-CLI, where there is no request to learn from, so a
312 * command-line activation leaves the URLs alone as well.
313 */
314 if ( ! is_ssl() ) {
315 return;
316 }
317
318 // Check if already HTTPS
319 $site_url = get_option( 'siteurl' );
320 $home_url = get_option( 'home' );
321
322 // Update to HTTPS if not already
323 if ( strpos( $site_url, 'https://' ) === false ) {
324 update_option( 'siteurl', str_replace( 'http://', 'https://', $site_url ) );
325 }
326
327 if ( strpos( $home_url, 'https://' ) === false ) {
328 update_option( 'home', str_replace( 'http://', 'https://', $home_url ) );
329 }
330 }
331
332 /**
333 * Remove sensitive files from WordPress root
334 *
335 * @param Vigilante_Settings $settings Settings instance.
336 */
337 private static function remove_sensitive_files( $settings ) {
338 $advanced = $settings->get_section( 'advanced' );
339
340 // Remove readme.html
341 if ( ! empty( $advanced['remove_readme'] ) ) {
342 $readme_path = ABSPATH . 'readme.html';
343 if ( file_exists( $readme_path ) ) {
344 wp_delete_file( $readme_path );
345 }
346 }
347
348 // Remove license.txt / licencia.txt (Spanish locale)
349 if ( ! empty( $advanced['remove_license'] ) ) {
350 $license_files = array( 'license.txt', 'licencia.txt' );
351 foreach ( $license_files as $license_file ) {
352 $license_path = ABSPATH . $license_file;
353 if ( file_exists( $license_path ) ) {
354 wp_delete_file( $license_path );
355 }
356 }
357 }
358 }
359
360 /**
361 * Generate initial baseline hashes for critical config files
362 *
363 * Called once during activation, after Vigilante has written its own
364 * blocks to wp-config.php and .htaccess. The baseline stores the
365 * normalized hash (excluding Vigilante blocks) so that subsequent
366 * scans can detect unauthorized external modifications.
367 *
368 * @param Vigilante_Settings $settings Settings instance.
369 */
370 private static function generate_critical_baseline( $settings ) {
371 if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
372 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
373 }
374
375 $database = new Vigilante_Database();
376 $activity_log = null; // Not needed for baseline generation
377
378 $fi = new Vigilante_File_Integrity( $settings, $database, $activity_log );
379 $fi->regenerate_all_baselines();
380 }
381
382 /**
383 * Schedule cron events
384 */
385 private static function schedule_events() {
386 // Daily maintenance
387 if ( ! wp_next_scheduled( 'vigilante_daily_maintenance' ) ) {
388 wp_schedule_event( time(), 'daily', 'vigilante_daily_maintenance' );
389 }
390
391 // Hourly checks
392 if ( ! wp_next_scheduled( 'vigilante_hourly_checks' ) ) {
393 wp_schedule_event( time(), 'hourly', 'vigilante_hourly_checks' );
394 }
395
396 // Weekly security analyzer scan
397 if ( ! wp_next_scheduled( 'vigilante_analyzer_weekly_scan' ) ) {
398 wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'vigilante_analyzer_weekly_scan' );
399 }
400
401 // Daily plugin status check (closed-in-wp.org detection)
402 if ( ! wp_next_scheduled( 'vigilante_plugin_status_check' ) ) {
403 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'vigilante_plugin_status_check' );
404 }
405 }
406
407 /**
408 * Send activation notification email
409 *
410 * @param Vigilante_Settings $settings Settings instance.
411 */
412 private static function send_activation_email( $settings ) {
413 $email_settings = $settings->get_section( 'email' );
414
415 if ( empty( $email_settings['send_activation_email'] ) ) {
416 return;
417 }
418
419 if ( ! class_exists( 'Vigilante_Email_Template' ) ) {
420 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
421 }
422
423 $to = Vigilante_Email_Template::get_admin_recipients();
424
425 $site_name = get_bloginfo( 'name' );
426 $site_url = get_site_url();
427
428 $subject = sprintf(
429 /* translators: %s: Site name */
430 __( '[%s] Vigilant Activated', 'vigilante' ),
431 $site_name
432 );
433
434 $body = Vigilante_Email_Template::p( __( 'Vigilant has been activated on your website. All security modules are now enabled with default settings.', 'vigilante' ) );
435 $body .= Vigilante_Email_Template::data_table( array(
436 __( 'Site', 'vigilante' ) => $site_name,
437 __( 'URL', 'vigilante' ) => $site_url,
438 __( 'Date', 'vigilante' ) => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
439 ) );
440 $body .= Vigilante_Email_Template::info_box( __( 'Please review the settings in your WordPress admin panel.', 'vigilante' ) );
441 $body .= Vigilante_Email_Template::button( admin_url( 'admin.php?page=vigilante' ), __( 'Go to Vigilant', 'vigilante' ) );
442
443 Vigilante_Email_Template::send( $to, $subject, __( 'Plugin activated', 'vigilante' ), $body );
444 }
445
446 /**
447 * Apply comment security settings to WordPress options
448 *
449 * @param Vigilante_Settings $settings Settings instance.
450 */
451 private static function apply_comment_security( $settings ) {
452 $options = $settings->get_section( 'wp_hardening' );
453
454 // Disable pingbacks
455 if ( ! empty( $options['disable_pingbacks'] ) ) {
456 update_option( 'default_pingback_flag', 0 );
457 update_option( 'default_ping_status', 'closed' );
458 }
459
460 // Disable trackbacks
461 if ( ! empty( $options['disable_trackbacks'] ) ) {
462 update_option( 'default_ping_status', 'closed' );
463 }
464
465 // Require comment moderation
466 if ( ! empty( $options['require_comment_moderation'] ) ) {
467 update_option( 'comment_moderation', 1 );
468 }
469 }
470
471 /**
472 * Check if server is Apache
473 *
474 * @return bool
475 */
476 private static function is_apache() {
477 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php';
478
479 // One detection for the whole plugin. This used to be a second copy of
480 // the same logic, so fixing one never fixed the other.
481 return Vigilante_Htaccess_Manager::get_instance()->is_apache();
482 }
483
484 /**
485 * Leave the server layer pending when the server could not be identified
486 *
487 * An activation from WP-CLI has no request to read the server software
488 * from, so before 2.9.9 the two apply_* guards below simply returned and
489 * the site was left without the .htaccess layer, with every switch showing
490 * as on. Now it is written down, so the first web request applies it, and
491 * it is logged, so it is visible that it happened.
492 *
493 * @since 2.9.9
494 */
495 private static function mark_server_files_pending() {
496 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php';
497
498 // On a server known not to be Apache there is nothing to write, ever.
499 if ( ! Vigilante_Htaccess_Manager::get_instance()->server_is_unknown() ) {
500 return;
501 }
502
503 update_option( 'vigilante_server_files_pending', 1 );
504
505 // The activation runs before the plugin has loaded its own files, so
506 // every link of the chain has to be pulled in: the log asks the database
507 // for the client IP, and that resolves it through the IP helper.
508 require_once VIGILANTE_INCLUDES_DIR . 'class-ip-utils.php';
509 require_once VIGILANTE_INCLUDES_DIR . 'class-database.php';
510 require_once VIGILANTE_INCLUDES_DIR . 'class-activity-log.php';
511
512 $settings = new Vigilante_Settings();
513 $activity_log = new Vigilante_Activity_Log( $settings, new Vigilante_Database() );
514 $activity_log->log(
515 'system',
516 'server_rules_pending',
517 __( 'The server type could not be identified from this request, so the .htaccess rules were left pending and will be written on the first web request.', 'vigilante' ),
518 array( 'sapi' => PHP_SAPI ),
519 'warning'
520 );
521 }
522 }