PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 All 139 releases
← All changes | includes/class-metasync-debug-mode-manager.php +145 -11 2.6.10trunk View file →
@@ -64,8 +64,23 @@
64 64 */
65 65 const CRON_HOOK = 'metasync_check_debug_limits';
66 66
67 67 /**
68 + * Outcomes of a wp-config.php constant write.
69 + *
70 + * WRITE_PARTIAL matters: some constants are live on disk, so the caller must not
71 + * report "off" - that hides the dashboard widget and stops the auto-disable cron.
72 + */
73 + const WRITE_OK = 'ok';
74 + const WRITE_PARTIAL = 'partial';
75 + const WRITE_FAILED = 'failed';
76 +
77 + /**
78 + * Maximum queued admin notices.
79 + */
80 + const MAX_NOTICES = 5;
81 +
82 + /**
68 83 * Option key for debug mode settings
69 84 *
70 85 * @var string
71 86 */
@@ -173,8 +188,10 @@
173 188 * @return bool Success status
174 189 */
175 190 public function enable_debug_mode($indefinite = false)
176 191 {
192 + $previousSettings = get_option(self::OPTION_KEY);
193 +
177 194 $settings = array(
178 195 'enabled' => true,
179 196 'enabled_at' => current_time('timestamp'),
180 197 'indefinite' => $indefinite,
@@ -184,15 +201,33 @@
184 201 $result = update_option(self::OPTION_KEY, $settings);
185 202
186 203 if ($result) {
187 204 // Enable WP_DEBUG constants via ConfigController
188 - $this->update_wp_debug_constants(true);
205 + $write = $this->update_wp_debug_constants(true);
189 206
190 - // Ensure cron job is scheduled (safety net)
207 + // Ensure cron job is scheduled (safety net). Scheduled before the write is
208 + // judged, because the partial-write branch below deliberately leaves debug
209 + // enabled on the assumption this cron can still clean it up.
191 210 if (!wp_next_scheduled(self::CRON_HOOK)) {
192 211 wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
193 212 }
194 213
214 + if (self::WRITE_OK !== $write) {
215 + // Only roll the tracking state back when nothing reached wp-config.php.
216 + // After a partial write the constants that landed are live, and a state of
217 + // "off" would hide the dashboard widget and make the auto-disable cron
218 + // skip - leaving debug on with nothing left to turn it off.
219 + if (self::WRITE_FAILED === $write) {
220 + if (false !== $previousSettings) {
221 + update_option(self::OPTION_KEY, $previousSettings);
222 + } else {
223 + delete_option(self::OPTION_KEY);
224 + }
225 + }
226 +
227 + return false;
228 + }
229 +
195 230 // Clear any previous notices
196 231 delete_transient(self::NOTICE_TRANSIENT);
197 232
198 233 // Log the action
@@ -209,8 +244,10 @@
209 244 * @return bool Success status
210 245 */
211 246 public function disable_debug_mode($reason = 'manual')
212 247 {
248 + $previousSettings = get_option(self::OPTION_KEY);
249 +
213 250 $settings = $this->get_settings();
214 251 $settings['enabled'] = false;
215 252 $settings['disabled_at'] = current_time('timestamp');
216 253 $settings['disabled_reason'] = $reason;
@@ -218,10 +255,20 @@
218 255 $result = update_option(self::OPTION_KEY, $settings);
219 256
220 257 if ($result) {
221 258 // Disable WP_DEBUG constants via ConfigController
222 - $this->update_wp_debug_constants(false);
259 + if (self::WRITE_OK !== $this->update_wp_debug_constants(false)) {
260 + // Debug is still on in wp-config.php, wholly or partly. Restore the
261 + // enabled state so it matches the file: that keeps the dashboard widget
262 + // visible and the auto-disable cron running, which is what will eventually
263 + // clear it. Reporting "disabled" here would hide both.
264 + if (false !== $previousSettings) {
265 + update_option(self::OPTION_KEY, $previousSettings);
266 + }
223 267
268 + return false;
269 + }
270 +
224 271 // Add admin notice
225 272 $this->add_notice(
226 273 'Debug mode has been disabled (' . $reason . ').',
227 274 'info'
@@ -306,13 +353,17 @@
306 353 $elapsed_time = $current_time - $enabled_at;
307 354
308 355 // Check if 24 hours have passed
309 356 if ($elapsed_time >= self::DEBUG_DURATION) {
310 - $this->disable_debug_mode('auto_expired');
311 - $this->add_notice(
312 - 'Debug mode auto-disabled after 24 hours.',
313 - 'warning'
314 - );
357 + // Only announce the auto-disable if it actually succeeded. This runs hourly,
358 + // so claiming it unconditionally would repeat the message every hour on a site
359 + // where wp-config.php cannot be written.
360 + if ($this->disable_debug_mode('auto_expired')) {
361 + $this->add_notice(
362 + 'Debug mode auto-disabled after 24 hours.',
363 + 'warning'
364 + );
365 + }
315 366 }
316 367 }
317 368
318 369 /**
@@ -397,8 +448,25 @@
397 448 }
398 449 }
399 450
400 451 /**
452 + * Check whether debug mode is currently enabled
453 + *
454 + * Static and side-effect free: reads the option directly rather than going
455 + * through get_instance(), which registers hooks as part of construction.
456 + * Safe to call from front-end request paths, including ones that run before
457 + * the singleton is initialized on 'init'.
458 + *
459 + * @return bool True when debug mode is on
460 + */
461 + public static function is_enabled()
462 + {
463 + $settings = get_option(self::OPTION_KEY, array());
464 +
465 + return is_array($settings) && !empty($settings['enabled']);
466 + }
467 +
468 + /**
401 469 * Get current debug mode settings
402 470 *
403 471 * @return array Debug mode settings
404 472 */
@@ -461,20 +529,57 @@
461 529 /**
462 530 * Update WP_DEBUG constants via ConfigController
463 531 *
464 532 * @param bool $enable Whether to enable or disable
465 - * @return void
533 + * @return string One of self::WRITE_OK, self::WRITE_PARTIAL or self::WRITE_FAILED.
466 534 */
467 535 private function update_wp_debug_constants($enable)
468 536 {
537 + $previousEnabled = get_option('wp_debug_enabled', 'false');
538 + $previousLog = get_option('wp_debug_log_enabled', 'false');
539 +
469 540 try {
470 541 update_option('wp_debug_enabled', $enable ? 'true' : 'false');
471 542 update_option('wp_debug_log_enabled', $enable ? 'true' : 'false');
472 543
473 544 $config_controller = new ConfigControllerMetaSync();
474 - $config_controller->store();
475 - } catch (Exception $e) {
545 +
546 + $reason = '';
547 + $partial = false;
548 + if (!$config_controller->isReady()) {
549 + $reason = $config_controller->getConfigError() !== ''
550 + ? $config_controller->getConfigError()
551 + : 'the file is not writable.';
552 + } elseif (!$config_controller->store()) {
553 + $reason = $config_controller->getConfigError() !== ''
554 + ? $config_controller->getConfigError()
555 + : 'the write did not complete.';
556 + $partial = $config_controller->hadPartialWrite();
557 + }
558 +
559 + if ('' !== $reason) {
560 + if (!$partial) {
561 + // Nothing reached the file, so the flags can safely go back.
562 + update_option('wp_debug_enabled', $previousEnabled);
563 + update_option('wp_debug_log_enabled', $previousLog);
564 + }
565 +
566 + // admin_notices never fires during a REST request, so the controller's own
567 + // notice would be discarded - use the transient notices the admin reads.
568 + $this->add_notice('wp-config.php could not be updated: ' . $reason, 'error');
569 + error_log('MetaSync: wp-config.php not updated - ' . $reason);
570 +
571 + return $partial ? self::WRITE_PARTIAL : self::WRITE_FAILED;
572 + }
573 +
574 + return self::WRITE_OK;
575 + } catch (\Throwable $e) {
576 + // Throwable rather than Exception: an Error here would be a fatal on what is
577 + // an ordinary REST request.
578 + update_option('wp_debug_enabled', $previousEnabled);
579 + update_option('wp_debug_log_enabled', $previousLog);
476 580 error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage());
581 + return self::WRITE_FAILED;
477 582 }
478 583 }
479 584
480 585 /**
@@ -490,13 +595,42 @@
490 595 if (!is_array($notices)) {
491 596 $notices = array();
492 597 }
493 598
599 + // Skip an identical message that is already queued. The limit checks run hourly,
600 + // so a persistent failure would otherwise append the same notice every hour -
601 + // and set_transient() refreshes the TTL each time, so the queue never expires.
602 + foreach ($notices as $existing) {
603 + if (isset($existing['message'], $existing['type'])
604 + && $existing['message'] === $message
605 + && $existing['type'] === $type) {
606 + return;
607 + }
608 + }
609 +
494 610 $notices[] = array(
495 611 'message' => $message,
496 612 'type' => $type,
497 613 'timestamp' => current_time('timestamp')
498 614 );
615 +
616 + // Keep the queue bounded so the admin screen cannot be flooded. Drop informational
617 + // notices first: evicting oldest-first would discard an error about wp-config.php
618 + // not being writable in favour of routine status messages.
619 + if (count($notices) > self::MAX_NOTICES) {
620 + $errors = array_values(array_filter($notices, function ($notice) {
621 + return isset($notice['type']) && 'error' === $notice['type'];
622 + }));
623 + $others = array_values(array_filter($notices, function ($notice) {
624 + return !isset($notice['type']) || 'error' !== $notice['type'];
625 + }));
626 +
627 + $keepOthers = max(0, self::MAX_NOTICES - count($errors));
628 + $notices = array_merge(
629 + array_slice($errors, -self::MAX_NOTICES),
630 + array_slice($others, -$keepOthers)
631 + );
632 + }
499 633
500 634 set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS);
501 635 }
502 636