PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
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 2.9.4 All 87 releases
vigilante / includes / class-deactivator.php

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

240 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Deactivator Class
4 *
5 * Handles plugin deactivation tasks
6 *
7 * @package Vigilante
8 */
9
10 // Prevent direct access
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class Vigilante_Deactivator
17 *
18 * Fired during plugin deactivation
19 */
20 class Vigilante_Deactivator {
21
22 /**
23 * Run deactivation tasks
24 *
25 * @param bool $network_wide Whether core is deactivating the plugin for the
26 * whole network, as it passes it to the hook.
27 */
28 public static function deactivate( $network_wide = false ) {
29 /*
30 * wp-config.php and the root .htaccess belong to the main site of a
31 * network, and the gate that protects them asks whether the request is
32 * on the main site. A network-wide deactivation can arrive from any site,
33 * though: the REST plugins endpoint answers on every site's URL, and
34 * WP-CLI takes --network together with the --url of a subsite. Asked
35 * there, the gate said no and the blocks stayed behind, with no plugin
36 * left to remove them. So the question is asked on the main site. Only
37 * the site changes: the capability is still checked against whoever is
38 * deactivating, so a subsite administrator gains nothing from it.
39 */
40 $switched = false;
41 if ( $network_wide && is_multisite() && ! is_main_site() ) {
42 switch_to_blog( get_main_site_id() );
43 $switched = true;
44 }
45
46 try {
47 /*
48 * Activating network-wide does not clear a site's own activation, so
49 * a Vigilant that was active on the main site before it was activated
50 * for the network is still running there after a network
51 * deactivation. Its blocks are still in use, and removing them would
52 * leave that copy running without the protections it wrote.
53 */
54 if ( ! self::still_active_here( $network_wide ) ) {
55 // ALWAYS remove htaccess rules using the centralized manager
56 self::remove_htaccess_rules();
57
58 // ALWAYS remove wp-config security constants and restore originals
59 self::remove_wpconfig_security();
60 }
61 } finally {
62 if ( $switched ) {
63 restore_current_blog();
64 }
65 }
66
67 /*
68 * The same question for the site the request is on: a copy that keeps
69 * running here keeps its schedule, and since it has not been deactivated
70 * it is not the one to announce that protection is off. The transients
71 * below are cleared either way; for a copy that keeps running that only
72 * resets the alert engine's counters and cooldowns, so an alert can come
73 * back sooner than it would have.
74 */
75 if ( ! self::still_active_here( $network_wide ) ) {
76 // Clear scheduled events
77 self::clear_scheduled_events();
78
79 // Send deactivation email
80 self::send_deactivation_email();
81 }
82
83 // Clear transients
84 delete_transient( 'vigilante_activated' );
85 delete_transient( 'vigilante_restore_on_deactivate' );
86 delete_transient( 'vigilante_backup_error' );
87
88 // Audit Alerts: clear every engine transient (per-category counters and
89 // cooldowns, plus the immediate anti-duplicate keys). Names are dynamic
90 // (one per category, md5 per event), so a prefix sweep is the only way.
91 global $wpdb;
92 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off cleanup on deactivate; dynamic transient names cannot be enumerated individually.
93 $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_vigilante\\_aa\\_%' OR option_name LIKE '\\_transient\\_timeout\\_vigilante\\_aa\\_%'" );
94
95 // Flush rewrite rules
96 flush_rewrite_rules();
97 }
98
99 /**
100 * Remove htaccess rules using the centralized manager
101 */
102 private static function remove_htaccess_rules() {
103 // Use the centralized manager
104 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-manager.php';
105
106 $manager = Vigilante_Htaccess_Manager::get_instance();
107
108 // Remove our blocks
109 $manager->remove_block( '# BEGIN Vigilante Protection', '# END Vigilante Protection' );
110 $manager->remove_block( '# BEGIN Vigilante Security Headers', '# END Vigilante Security Headers' );
111 }
112
113 /**
114 * Remove wp-config security constants and restore original values
115 */
116 private static function remove_wpconfig_security() {
117 /*
118 * One removal for the whole plugin. This used to be a copy of
119 * Vigilante_Wpconfig_Security::remove_constants(), and the copy had
120 * drifted from it in three ways. Until 2.11.5 it did not ask the network
121 * gate, so the administrator of a subsite who deactivated a per-site
122 * Vigilant stripped the constants for every site of the network (reported
123 * by the wordpress.org automated security review). It cut the block line
124 * by line, so a block that had lost its END marker took everything after
125 * it, the require of wp-settings.php included. And it wrote through
126 * WP_Filesystem with FS_CHMOD_FILE, which is "permissions of index.php |
127 * 0644", so a wp-config.php kept at 0600 or 0640 was left at 0644; a
128 * direct write keeps the permissions the file already has.
129 */
130 $wpconfig_path = ABSPATH . 'wp-config.php';
131
132 // remove_constants() writes the file directly, so there is nothing to do
133 // when PHP cannot.
134 if ( ! file_exists( $wpconfig_path ) || ! wp_is_writable( $wpconfig_path ) ) {
135 return;
136 }
137
138 require_once VIGILANTE_INCLUDES_DIR . 'class-wpconfig-security.php';
139
140 // remove_constants() asks the network gate and leaves alone a block that
141 // has lost one of its markers.
142 $wpconfig = new Vigilante_Wpconfig_Security( new Vigilante_Settings() );
143 $wpconfig->remove_constants();
144 }
145
146 /**
147 * Whether a per-site Vigilant keeps running on the current site
148 *
149 * Only a network-wide deactivation can leave one behind, and only then is
150 * the site's own list final when the hook runs: core saves the lists after
151 * the hook, so for a per-site deactivation the list still names the plugin.
152 * Code that calls deactivate_plugins() without saying whether it is
153 * network-wide on a plugin active both ways drops the site's entry too,
154 * after the hook, and then this answers yes for a copy that is going away;
155 * core itself never makes that call.
156 *
157 * @since 2.11.6
158 *
159 * @param bool $network_wide Whether the deactivation is network-wide.
160 * @return bool
161 */
162 private static function still_active_here( $network_wide ) {
163 return $network_wide && is_multisite()
164 && in_array( VIGILANTE_PLUGIN_BASENAME, (array) get_option( 'active_plugins', array() ), true );
165 }
166
167 /**
168 * Clear all scheduled cron events
169 */
170 private static function clear_scheduled_events() {
171 $events = array(
172 'vigilante_daily_maintenance',
173 'vigilante_hourly_checks',
174 'vigilante_file_integrity_scan',
175 'vigilante_password_expiry_reminder',
176 'vigilante_analyzer_weekly_scan',
177 'vigilante_plugin_status_check',
178 );
179
180 foreach ( $events as $event ) {
181 $timestamp = wp_next_scheduled( $event );
182 if ( $timestamp ) {
183 wp_unschedule_event( $timestamp, $event );
184 }
185 }
186
187 // Clear all events with our prefix
188 wp_unschedule_hook( 'vigilante_daily_maintenance' );
189 wp_unschedule_hook( 'vigilante_hourly_checks' );
190 wp_unschedule_hook( 'vigilante_file_integrity_scan' );
191 wp_unschedule_hook( 'vigilante_password_expiry_reminder' );
192 wp_unschedule_hook( 'vigilante_analyzer_weekly_scan' );
193 wp_unschedule_hook( 'vigilante_plugin_status_check' );
194 // Post-update verification single events (scheduled with per-update args).
195 wp_unschedule_hook( 'vigilante_fi_postupdate_verify' );
196 }
197
198 /**
199 * Send deactivation notification email
200 */
201 private static function send_deactivation_email() {
202 $settings = new Vigilante_Settings();
203 $email_settings = $settings->get_section( 'email' );
204
205 if ( empty( $email_settings['send_deactivation_email'] ) ) {
206 return;
207 }
208
209 if ( ! class_exists( 'Vigilante_Email_Template' ) ) {
210 require_once VIGILANTE_INCLUDES_DIR . 'class-email-template.php';
211 }
212
213 $to = Vigilante_Email_Template::get_admin_recipients();
214
215 $site_name = get_bloginfo( 'name' );
216 $site_url = get_site_url();
217
218 // Get current user info
219 $current_user = wp_get_current_user();
220 $user_info = $current_user->ID > 0
221 ? $current_user->user_login . ' (' . $current_user->user_email . ')'
222 : __( 'Unknown', 'vigilante' );
223
224 $subject = sprintf(
225 /* translators: %s: Site name */
226 __( '[%s] Vigilant Deactivated', 'vigilante' ),
227 $site_name
228 );
229
230 $body = Vigilante_Email_Template::warning_box( __( 'Security protection has been disabled. Please ensure you have alternative security measures in place.', 'vigilante' ) );
231 $body .= Vigilante_Email_Template::data_table( array(
232 __( 'Site', 'vigilante' ) => $site_name,
233 __( 'URL', 'vigilante' ) => $site_url,
234 __( 'Date', 'vigilante' ) => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
235 __( 'Deactivated by', 'vigilante' ) => $user_info,
236 ) );
237
238 Vigilante_Email_Template::send( $to, $subject, __( 'Plugin deactivated', 'vigilante' ), $body );
239 }
240 }