PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 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 All 141 releases
← All changes | includes/class-metasync-debug-mode-manager.php +150 -20 2.6.21 → 2.7.1 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,20 +201,36 @@
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 - // Log the action
199 - error_log('MetaSync: Debug mode enabled' . ($indefinite ? ' (indefinite)' : ' (24 hours)'));
200 233 }
201 234
202 235 return $result;
203 236 }
@@ -209,8 +242,10 @@
209 242 * @return bool Success status
210 243 */
211 244 public function disable_debug_mode($reason = 'manual')
212 245 {
246 + $previousSettings = get_option(self::OPTION_KEY);
247 +
213 248 $settings = $this->get_settings();
214 249 $settings['enabled'] = false;
215 250 $settings['disabled_at'] = current_time('timestamp');
216 251 $settings['disabled_reason'] = $reason;
@@ -218,10 +253,20 @@
218 253 $result = update_option(self::OPTION_KEY, $settings);
219 254
220 255 if ($result) {
221 256 // Disable WP_DEBUG constants via ConfigController
222 - $this->update_wp_debug_constants(false);
257 + if (self::WRITE_OK !== $this->update_wp_debug_constants(false)) {
258 + // Debug is still on in wp-config.php, wholly or partly. Restore the
259 + // enabled state so it matches the file: that keeps the dashboard widget
260 + // visible and the auto-disable cron running, which is what will eventually
261 + // clear it. Reporting "disabled" here would hide both.
262 + if (false !== $previousSettings) {
263 + update_option(self::OPTION_KEY, $previousSettings);
264 + }
223 265
266 + return false;
267 + }
268 +
224 269 // Add admin notice
225 270 $this->add_notice(
226 271 'Debug mode has been disabled (' . $reason . ').',
227 272 'info'
@@ -226,10 +271,8 @@
226 271 'Debug mode has been disabled (' . $reason . ').',
227 272 'info'
228 273 );
229 274
230 - // Log the action
231 - error_log('MetaSync: Debug mode disabled - ' . $reason);
232 275 }
233 276
234 277 return $result;
235 278 }
@@ -306,13 +349,17 @@
306 349 $elapsed_time = $current_time - $enabled_at;
307 350
308 351 // Check if 24 hours have passed
309 352 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 - );
353 + // Only announce the auto-disable if it actually succeeded. This runs hourly,
354 + // so claiming it unconditionally would repeat the message every hour on a site
355 + // where wp-config.php cannot be written.
356 + if ($this->disable_debug_mode('auto_expired')) {
357 + $this->add_notice(
358 + 'Debug mode auto-disabled after 24 hours.',
359 + 'warning'
360 + );
361 + }
315 362 }
316 363 }
317 364
318 365 /**
@@ -351,18 +398,18 @@
351 398 $backup_path = $this->log_file_path . '.old';
352 399
353 400 // Remove existing .old file if it exists
354 401 if (file_exists($backup_path)) {
355 - @unlink($backup_path);
402 + @wp_delete_file($backup_path);
356 403 }
357 404
358 405 // Rename current log to .old
359 - $result = @rename($this->log_file_path, $backup_path);
406 + $result = @rename($this->log_file_path, $backup_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- atomic same-filesystem replace; WP_Filesystem::move() needs credential bootstrapping on non-direct hosts
360 407
361 408 if ($result) {
362 409 // Create new empty log file
363 410 @file_put_contents($this->log_file_path, '');
364 - error_log('MetaSync: Debug log rotated - exceeded 10MB limit');
411 + error_log('MetaSync: Debug log rotated - exceeded 10MB limit'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- plugin logger sink / last-resort fallback
365 412 }
366 413
367 414 // Cleanup old rotations (keep only MAX_ROTATED_LOGS)
368 415 $this->cleanup_old_rotations();
@@ -391,14 +438,31 @@
391 438
392 439 // Delete oldest files, keep only MAX_ROTATED_LOGS
393 440 $to_delete = array_slice($old_logs, 0, count($old_logs) - self::MAX_ROTATED_LOGS);
394 441 foreach ($to_delete as $old_log) {
395 - @unlink($old_log);
442 + @wp_delete_file($old_log);
396 443 }
397 444 }
398 445 }
399 446
400 447 /**
448 + * Check whether debug mode is currently enabled
449 + *
450 + * Static and side-effect free: reads the option directly rather than going
451 + * through get_instance(), which registers hooks as part of construction.
452 + * Safe to call from front-end request paths, including ones that run before
453 + * the singleton is initialized on 'init'.
454 + *
455 + * @return bool True when debug mode is on
456 + */
457 + public static function is_enabled()
458 + {
459 + $settings = get_option(self::OPTION_KEY, array());
460 +
461 + return is_array($settings) && !empty($settings['enabled']);
462 + }
463 +
464 + /**
401 465 * Get current debug mode settings
402 466 *
403 467 * @return array Debug mode settings
404 468 */
@@ -461,20 +525,57 @@
461 525 /**
462 526 * Update WP_DEBUG constants via ConfigController
463 527 *
464 528 * @param bool $enable Whether to enable or disable
465 - * @return void
529 + * @return string One of self::WRITE_OK, self::WRITE_PARTIAL or self::WRITE_FAILED.
466 530 */
467 531 private function update_wp_debug_constants($enable)
468 532 {
533 + $previousEnabled = get_option('wp_debug_enabled', 'false');
534 + $previousLog = get_option('wp_debug_log_enabled', 'false');
535 +
469 536 try {
470 537 update_option('wp_debug_enabled', $enable ? 'true' : 'false');
471 538 update_option('wp_debug_log_enabled', $enable ? 'true' : 'false');
472 539
473 540 $config_controller = new ConfigControllerMetaSync();
474 - $config_controller->store();
475 - } catch (Exception $e) {
476 - error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage());
541 +
542 + $reason = '';
543 + $partial = false;
544 + if (!$config_controller->isReady()) {
545 + $reason = $config_controller->getConfigError() !== ''
546 + ? $config_controller->getConfigError()
547 + : 'the file is not writable.';
548 + } elseif (!$config_controller->store()) {
549 + $reason = $config_controller->getConfigError() !== ''
550 + ? $config_controller->getConfigError()
551 + : 'the write did not complete.';
552 + $partial = $config_controller->hadPartialWrite();
553 + }
554 +
555 + if ('' !== $reason) {
556 + if (!$partial) {
557 + // Nothing reached the file, so the flags can safely go back.
558 + update_option('wp_debug_enabled', $previousEnabled);
559 + update_option('wp_debug_log_enabled', $previousLog);
560 + }
561 +
562 + // admin_notices never fires during a REST request, so the controller's own
563 + // notice would be discarded - use the transient notices the admin reads.
564 + $this->add_notice('wp-config.php could not be updated: ' . $reason, 'error');
565 + error_log('MetaSync: wp-config.php not updated - ' . $reason); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
566 +
567 + return $partial ? self::WRITE_PARTIAL : self::WRITE_FAILED;
568 + }
569 +
570 + return self::WRITE_OK;
571 + } catch (\Throwable $e) {
572 + // Throwable rather than Exception: an Error here would be a fatal on what is
573 + // an ordinary REST request.
574 + update_option('wp_debug_enabled', $previousEnabled);
575 + update_option('wp_debug_log_enabled', $previousLog);
576 + error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
577 + return self::WRITE_FAILED;
477 578 }
478 579 }
479 580
480 581 /**
@@ -490,13 +591,42 @@
490 591 if (!is_array($notices)) {
491 592 $notices = array();
492 593 }
493 594
595 + // Skip an identical message that is already queued. The limit checks run hourly,
596 + // so a persistent failure would otherwise append the same notice every hour -
597 + // and set_transient() refreshes the TTL each time, so the queue never expires.
598 + foreach ($notices as $existing) {
599 + if (isset($existing['message'], $existing['type'])
600 + && $existing['message'] === $message
601 + && $existing['type'] === $type) {
602 + return;
603 + }
604 + }
605 +
494 606 $notices[] = array(
495 607 'message' => $message,
496 608 'type' => $type,
497 609 'timestamp' => current_time('timestamp')
498 610 );
611 +
612 + // Keep the queue bounded so the admin screen cannot be flooded. Drop informational
613 + // notices first: evicting oldest-first would discard an error about wp-config.php
614 + // not being writable in favour of routine status messages.
615 + if (count($notices) > self::MAX_NOTICES) {
616 + $errors = array_values(array_filter($notices, function ($notice) {
617 + return isset($notice['type']) && 'error' === $notice['type'];
618 + }));
619 + $others = array_values(array_filter($notices, function ($notice) {
620 + return !isset($notice['type']) || 'error' !== $notice['type'];
621 + }));
622 +
623 + $keepOthers = max(0, self::MAX_NOTICES - count($errors));
624 + $notices = array_merge(
625 + array_slice($errors, -self::MAX_NOTICES),
626 + array_slice($others, -$keepOthers)
627 + );
628 + }
499 629
500 630 set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS);
501 631 }
502 632