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

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