PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.4
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.4
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 2.9.4 2.9.3 All 86 releases
vigilante / includes / class-activator.php

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

528 lines 19.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 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
380 // Same care as the migration: reactivating the plugin on a site that
381 // already has an approved baseline must not throw it away and adopt
382 // whatever the files say today.
383 if ( ! $fi->get_critical_files_baseline() ) {
384 $fi->regenerate_all_baselines();
385 }
386 }
387
388 /**
389 * Schedule cron events
390 */
391 private static function schedule_events() {
392 // Daily maintenance
393 if ( ! wp_next_scheduled( 'vigilante_daily_maintenance' ) ) {
394 wp_schedule_event( time(), 'daily', 'vigilante_daily_maintenance' );
395 }
396
397 // Hourly checks
398 if ( ! wp_next_scheduled( 'vigilante_hourly_checks' ) ) {
399 wp_schedule_event( time(), 'hourly', 'vigilante_hourly_checks' );
400 }
401
402 // Weekly security analyzer scan
403 if ( ! wp_next_scheduled( 'vigilante_analyzer_weekly_scan' ) ) {
404 wp_schedule_event( time() + DAY_IN_SECONDS, 'weekly', 'vigilante_analyzer_weekly_scan' );
405 }
406
407 // Daily plugin status check (closed-in-wp.org detection)
408 if ( ! wp_next_scheduled( 'vigilante_plugin_status_check' ) ) {
409 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'vigilante_plugin_status_check' );
410 }
411 }
412
413 /**
414 * Send activation notification email
415 *
416 * @param Vigilante_Settings $settings Settings instance.
417 */
418 private static function send_activation_email( $settings ) {
419 $email_settings = $settings->get_section( 'email' );
420
421 if ( empty( $email_settings['send_activation_email'] ) ) {
422 return;
423 }
424
425 if ( ! class_exists( 'Vigilante_Email_Template' ) ) {
426 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
427 }
428
429 $to = Vigilante_Email_Template::get_admin_recipients();
430
431 $site_name = get_bloginfo( 'name' );
432 $site_url = get_site_url();
433
434 $subject = sprintf(
435 /* translators: %s: Site name */
436 __( '[%s] Vigilant Activated', 'vigilante' ),
437 $site_name
438 );
439
440 $body = Vigilante_Email_Template::p( __( 'Vigilant has been activated on your website. All security modules are now enabled with default settings.', 'vigilante' ) );
441 $body .= Vigilante_Email_Template::data_table( array(
442 __( 'Site', 'vigilante' ) => $site_name,
443 __( 'URL', 'vigilante' ) => $site_url,
444 __( 'Date', 'vigilante' ) => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
445 ) );
446 $body .= Vigilante_Email_Template::info_box( __( 'Please review the settings in your WordPress admin panel.', 'vigilante' ) );
447 $body .= Vigilante_Email_Template::button( admin_url( 'admin.php?page=vigilante' ), __( 'Go to Vigilant', 'vigilante' ) );
448
449 Vigilante_Email_Template::send( $to, $subject, __( 'Plugin activated', 'vigilante' ), $body );
450 }
451
452 /**
453 * Apply comment security settings to WordPress options
454 *
455 * @param Vigilante_Settings $settings Settings instance.
456 */
457 private static function apply_comment_security( $settings ) {
458 $options = $settings->get_section( 'wp_hardening' );
459
460 // Disable pingbacks
461 if ( ! empty( $options['disable_pingbacks'] ) ) {
462 update_option( 'default_pingback_flag', 0 );
463 update_option( 'default_ping_status', 'closed' );
464 }
465
466 // Disable trackbacks
467 if ( ! empty( $options['disable_trackbacks'] ) ) {
468 update_option( 'default_ping_status', 'closed' );
469 }
470
471 // Require comment moderation
472 if ( ! empty( $options['require_comment_moderation'] ) ) {
473 update_option( 'comment_moderation', 1 );
474 }
475 }
476
477 /**
478 * Check if server is Apache
479 *
480 * @return bool
481 */
482 private static function is_apache() {
483 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php';
484
485 // One detection for the whole plugin. This used to be a second copy of
486 // the same logic, so fixing one never fixed the other.
487 return Vigilante_Htaccess_Manager::get_instance()->is_apache();
488 }
489
490 /**
491 * Leave the server layer pending when the server could not be identified
492 *
493 * An activation from WP-CLI has no request to read the server software
494 * from, so before 2.9.9 the two apply_* guards below simply returned and
495 * the site was left without the .htaccess layer, with every switch showing
496 * as on. Now it is written down, so the first web request applies it, and
497 * it is logged, so it is visible that it happened.
498 *
499 * @since 2.9.9
500 */
501 private static function mark_server_files_pending() {
502 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php';
503
504 // On a server known not to be Apache there is nothing to write, ever.
505 if ( ! Vigilante_Htaccess_Manager::get_instance()->server_is_unknown() ) {
506 return;
507 }
508
509 update_option( 'vigilante_server_files_pending', 1 );
510
511 // The activation runs before the plugin has loaded its own files, so
512 // every link of the chain has to be pulled in: the log asks the database
513 // for the client IP, and that resolves it through the IP helper.
514 require_once VIGILANTE_INCLUDES_DIR . 'class-ip-utils.php';
515 require_once VIGILANTE_INCLUDES_DIR . 'class-database.php';
516 require_once VIGILANTE_INCLUDES_DIR . 'class-activity-log.php';
517
518 $settings = new Vigilante_Settings();
519 $activity_log = new Vigilante_Activity_Log( $settings, new Vigilante_Database() );
520 $activity_log->log(
521 'system',
522 'server_rules_pending',
523 __( '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' ),
524 array( 'sapi' => PHP_SAPI ),
525 'warning'
526 );
527 }
528 }