PluginProbe
Disable All WordPress Updates / trunk
Disable All WordPress Updates vtrunk
2.0.2 2.0.1 2.0.0 trunk 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.5 1.6.6 1.6.7 1.6.8 1.7.0 1.7.1 1.8.0 1.9.0 1.9.1
disable-wordpress-updates / includes / class-osdwp-security-mode.php

class-osdwp-security-mode.php in Disable All WordPress Updates trunk, at includes/class-osdwp-security-mode.php

368 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Optional "Security Mode" for WordPress core auto-updates.
4 *
5 * Adds a Settings-API-powered settings page that lets a site admin restrict
6 * WordPress core automatic updates to minor / security releases only, blocking
7 * major (and development) version updates.
8 *
9 * The effect is achieved entirely with WordPress filters registered from the
10 * plugin itself — no wp-config.php edits and no runtime file writes:
11 *
12 * add_filter( 'allow_major_auto_core_updates', '__return_false' );
13 * add_filter( 'allow_minor_auto_core_updates', '__return_true' );
14 * add_filter( 'allow_dev_auto_core_updates', '__return_false' );
15 *
16 * Disabling the toggle simply stops the filters from being registered on the
17 * next load; the only thing persisted is the boolean plugin option.
18 *
19 * @package WordPress_Plugins
20 * @subpackage OS_Disable_WordPress_Updates
21 * @since 2.0.0
22 */
23
24 // Don't load directly.
25 if ( ! defined( 'ABSPATH' ) ) {
26 die( '-1' );
27 }
28
29 /**
30 * Class OSDWP_Security_Mode
31 *
32 * Implements the optional "Security Mode" for core auto-updates.
33 *
34 * @since 2.0.0
35 */
36 class OSDWP_Security_Mode {
37
38 /**
39 * Option name used to store the toggle state (boolean).
40 */
41 const OPTION_NAME = 'osdwp_security_mode';
42
43 /**
44 * Settings page slug.
45 */
46 const PAGE_SLUG = 'osdwp-security-mode';
47
48 /**
49 * Settings option-group name (drives nonce action + allowed_options).
50 */
51 const OPTION_GROUP = 'osdwp_security_mode_group';
52
53 /**
54 * Whether WP_AUTO_UPDATE_CORE was already defined before this plugin loaded.
55 *
56 * Captured in the constructor (this class is instantiated *before* the main
57 * plugin class) so we can warn the admin when the constant is defined by
58 * something else (wp-config.php, a host panel, an mu-plugin, another plugin).
59 *
60 * @var bool
61 */
62 protected $auto_update_core_defined_externally = false;
63
64 /**
65 * The externally-defined value of WP_AUTO_UPDATE_CORE, if any.
66 *
67 * @var mixed
68 */
69 protected $auto_update_core_external_value = null;
70
71 /**
72 * Constructor.
73 */
74 public function __construct() {
75 // 1) Detect an externally-defined WP_AUTO_UPDATE_CORE BEFORE the main
76 // plugin class defines it. Must run first (see disable-updates.php).
77 $this->auto_update_core_defined_externally = defined( 'WP_AUTO_UPDATE_CORE' );
78 if ( $this->auto_update_core_defined_externally ) {
79 $this->auto_update_core_external_value = WP_AUTO_UPDATE_CORE;
80 }
81
82 // 2) Register the core auto-update filters only when enabled.
83 // This runs on every request (admin, front-end and wp-cron) at
84 // plugin-load time, which is before WP_Automatic_Updater decides
85 // what to install.
86 if ( $this->is_enabled() ) {
87 $this->register_core_filters();
88 }
89
90 // 3) Settings API (page + setting/section/field).
91 add_action( 'admin_menu', [$this, 'add_settings_page'] );
92 add_action( 'admin_init', [$this, 'register_settings'] );
93
94 // 4) Optional cosmetic annotation on the WordPress Updates screen.
95 add_action( 'admin_notices', [$this, 'maybe_annotate_updates_screen'] );
96
97 // 5) Recolor the admin-bar notice from red to orange while active.
98 add_action( 'admin_enqueue_scripts', [$this, 'admin_bar_color_override'], 20 );
99 }
100
101 /**
102 * Whether Security Mode is currently enabled.
103 *
104 * @return bool
105 */
106 public function is_enabled() {
107 return (bool) get_option( self::OPTION_NAME, false );
108 }
109
110 /**
111 * Register the core auto-update filters that implement Security Mode.
112 *
113 * Priority 20 ensures these win over the base plugin's own allow_* filters
114 * (registered at the default priority 10) and over the defaults derived from
115 * the WP_AUTO_UPDATE_CORE constant.
116 *
117 * Note: this class is instantiated *before* the base plugin class, so it must
118 * not try to remove_filter() things the base plugin registers in its own
119 * constructor (that would be a no-op). Instead, the base plugin checks
120 * is_security_mode() itself and skips registering its core-update-blocking
121 * filters/actions entirely while Security Mode is active.
122 *
123 * Verification: when enabled, has_filter( 'allow_minor_auto_core_updates',
124 * '__return_true' ) is truthy; when disabled it is false.
125 */
126 public function register_core_filters() {
127 add_filter( 'allow_minor_auto_core_updates', '__return_true', 20 );
128 add_filter( 'allow_major_auto_core_updates', '__return_false', 20 );
129 add_filter( 'allow_dev_auto_core_updates', '__return_false', 20 );
130 add_filter( 'auto_update_core', '__return_true', 20 );
131 add_filter( 'wp_auto_update_core', '__return_true', 20 );
132 add_filter( 'auto_core_update_send_email', '__return_true', 20 );
133 add_filter( 'send_core_update_notification_email', '__return_true', 20 );
134 add_filter( 'automatic_updates_is_vcs_checkout', '__return_false', 20 );
135 }
136
137 /**
138 * Register the Settings API page under "Settings".
139 */
140 public function add_settings_page() {
141 add_submenu_page(
142 'options-general.php',
143 __( 'Disable Updates', 'disable-wordpress-updates' ),
144 __( 'Disable Updates', 'disable-wordpress-updates' ),
145 'manage_options',
146 self::PAGE_SLUG,
147 [$this, 'render_settings_page']
148 );
149 }
150
151 /**
152 * Register the setting, section and field via the Settings API.
153 *
154 * Nonce and capability checks on save are handled by the API itself:
155 * settings_fields() emits the nonce, and options.php enforces both the
156 * nonce (check_admin_referer) and the manage_options capability.
157 */
158 public function register_settings() {
159 register_setting(
160 self::OPTION_GROUP,
161 self::OPTION_NAME,
162 [
163 'type' => 'boolean',
164 'sanitize_callback' => [$this, 'sanitize_security_mode'],
165 'default' => false,
166 ]
167 );
168
169 add_settings_section(
170 'osdwp_security_mode_section',
171 __( 'Security Mode', 'disable-wordpress-updates' ),
172 [$this, 'render_section_intro'],
173 self::PAGE_SLUG
174 );
175
176 add_settings_field(
177 'osdwp_security_mode_field',
178 __( 'Status', 'disable-wordpress-updates' ),
179 [$this, 'render_security_mode_field'],
180 self::PAGE_SLUG,
181 'osdwp_security_mode_section'
182 );
183 }
184
185 /**
186 * Sanitize the toggle value to a strict boolean.
187 *
188 * options.php passes null when the checkbox is unchecked, so this correctly
189 * stores false when the admin turns Security Mode off — no persistence
190 * tricks beyond the stored option.
191 *
192 * @param mixed $value Raw value submitted for the option (or null).
193 * @return bool
194 */
195 public function sanitize_security_mode( $value ) {
196 return (bool) $value;
197 }
198
199 /**
200 * Render the section description.
201 */
202 public function render_section_intro() {
203 ?>
204 <p>
205 <?php
206 esc_html_e( 'Security Mode restricts WordPress core automatic updates to minor and security releases. Major version and development (nightly) core updates are blocked from being applied automatically.', 'disable-wordpress-updates' );
207 ?>
208 </p>
209 <p class="description">
210 <?php
211 esc_html_e( 'Implemented with WordPress filters only — no wp-config.php edits. Plugin and theme auto-updates are not affected by this setting.', 'disable-wordpress-updates' );
212 ?>
213 </p>
214 <p class="description">
215 <?php
216 esc_html_e( 'Note: this setting controls which core releases are eligible for automatic updates. If the automatic updater is disabled site-wide (for example via the AUTOMATIC_UPDATER_DISABLED constant/filter, or by this plugin\'s own update-blocking features), that takes precedence and no core update will be applied automatically regardless of this toggle.', 'disable-wordpress-updates' );
217 ?>
218 </p>
219 <?php
220 }
221
222 /**
223 * Render the toggle checkbox field.
224 */
225 public function render_security_mode_field() {
226 $enabled = $this->is_enabled();
227 ?>
228 <label for="osdwp_security_mode">
229 <input
230 type="checkbox"
231 id="osdwp_security_mode"
232 name="<?php echo esc_attr( self::OPTION_NAME ); ?>"
233 value="1"
234 <?php checked( $enabled ); ?>
235 />
236 <?php esc_html_e( 'Enable security updates only (block major updates)', 'disable-wordpress-updates' ); ?>
237 </label>
238 <?php
239 }
240
241 /**
242 * Render the settings page.
243 */
244 public function render_settings_page() {
245 if ( ! current_user_can( 'manage_options' ) ) {
246 return;
247 }
248 ?>
249 <div class="wrap">
250 <h1><?php echo esc_html__( 'Disable Updates', 'disable-wordpress-updates' ); ?></h1>
251
252 <?php $this->render_constant_warning(); ?>
253
254 <form action="options.php" method="post">
255 <?php
256 // settings_fields() emits the nonce + option_page hidden fields;
257 // options.php verifies the nonce and the capability on save.
258 settings_fields( self::OPTION_GROUP );
259 do_settings_sections( self::PAGE_SLUG );
260 submit_button();
261 ?>
262 </form>
263 </div>
264 <?php
265 }
266
267 /**
268 * Render a warning notice when WP_AUTO_UPDATE_CORE is defined by something
269 * other than this plugin, as that constant overrides the filters and the
270 * toggle may have no effect.
271 */
272 public function render_constant_warning() {
273 if ( ! $this->auto_update_core_defined_externally ) {
274 return;
275 }
276
277 $value_label = $this->describe_auto_update_core_value( $this->auto_update_core_external_value );
278 ?>
279 <div class="notice notice-warning inline">
280 <p>
281 <strong><?php esc_html_e( 'WP_AUTO_UPDATE_CORE is defined elsewhere', 'disable-wordpress-updates' ); ?></strong>
282 </p>
283 <p>
284 <?php
285 printf(
286 /* translators: %s: WP_AUTO_UPDATE_CORE constant name wrapped in a <code> tag. */
287 esc_html__( 'The %s constant is defined outside of this plugin (for example in wp-config.php, a host panel, an mu-plugin or another plugin) with the following value:', 'disable-wordpress-updates' ),
288 '<code>WP_AUTO_UPDATE_CORE</code>'
289 );
290 ?>
291 </p>
292 <p><code><?php echo esc_html( $value_label ); ?></code></p>
293 <p>
294 <?php esc_html_e( 'This constant always overrides this plugin\'s filters, so the Security Mode toggle may have no effect. To rely on Security Mode, remove or adjust that constant definition.', 'disable-wordpress-updates' ); ?>
295 </p>
296 </div>
297 <?php
298 }
299
300 /**
301 * Produce a human-readable label for a WP_AUTO_UPDATE_CORE value.
302 *
303 * @param mixed $value Constant value (true|false|'minor'|other).
304 * @return string Safe for HTML output.
305 */
306 protected function describe_auto_update_core_value( $value ) {
307 if ( true === $value ) {
308 return 'true (' . __( 'all core updates', 'disable-wordpress-updates' ) . ')';
309 }
310 if ( false === $value ) {
311 return 'false (' . __( 'no core updates', 'disable-wordpress-updates' ) . ')';
312 }
313 if ( 'minor' === $value ) {
314 return "'minor' (" . __( 'minor / security only', 'disable-wordpress-updates' ) . ')';
315 }
316
317 return var_export( $value, true );
318 }
319
320 /**
321 * Optional cosmetic annotation: when Security Mode is enabled, show a note on
322 * the WordPress Updates screen so an admin understands why major core updates
323 * are not being offered/applied automatically.
324 */
325 public function maybe_annotate_updates_screen() {
326 if ( ! $this->is_enabled() ) {
327 return;
328 }
329 if ( ! function_exists( 'get_current_screen' ) ) {
330 return;
331 }
332 $screen = get_current_screen();
333 if ( ! $screen || 'update-core' !== $screen->id ) {
334 return;
335 }
336 ?>
337 <div class="notice notice-info">
338 <p>
339 <?php
340 esc_html_e( 'Security Mode is enabled: WordPress core automatic updates are restricted to minor and security releases. Major version updates are intentionally blocked.', 'disable-wordpress-updates' );
341 ?>
342 </p>
343 </div>
344 <?php
345 }
346
347 /**
348 * Recolor the admin-bar notice icon from red to orange while Security Mode
349 * is active, so admins can tell at a glance that core updates are being
350 * restricted to security releases rather than fully disabled.
351 *
352 * The base plugin registers the red background via wp_add_inline_style on the
353 * 'admin-bar' handle at the default priority (10). This override runs later
354 * (priority 20) and uses a higher-specificity selector (#wpadminbar …) so the
355 * orange rule reliably wins regardless of source order.
356 */
357 public function admin_bar_color_override() {
358 if ( ! $this->is_enabled() ) {
359 return;
360 }
361
362 wp_add_inline_style(
363 'admin-bar',
364 '#wpadminbar .wp-admin-bar-dwuos-notice { background-color: rgba(255, 140, 0, 0.5) !important; }'
365 );
366 }
367 }
368