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 +133 -20 2.6.24 → 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,9 +438,9 @@
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
@@ -478,20 +525,57 @@
478 525 /**
479 526 * Update WP_DEBUG constants via ConfigController
480 527 *
481 528 * @param bool $enable Whether to enable or disable
482 - * @return void
529 + * @return string One of self::WRITE_OK, self::WRITE_PARTIAL or self::WRITE_FAILED.
483 530 */
484 531 private function update_wp_debug_constants($enable)
485 532 {
533 + $previousEnabled = get_option('wp_debug_enabled', 'false');
534 + $previousLog = get_option('wp_debug_log_enabled', 'false');
535 +
486 536 try {
487 537 update_option('wp_debug_enabled', $enable ? 'true' : 'false');
488 538 update_option('wp_debug_log_enabled', $enable ? 'true' : 'false');
489 539
490 540 $config_controller = new ConfigControllerMetaSync();
491 - $config_controller->store();
492 - } catch (Exception $e) {
493 - 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;
494 578 }
495 579 }
496 580
497 581 /**
@@ -507,13 +591,42 @@
507 591 if (!is_array($notices)) {
508 592 $notices = array();
509 593 }
510 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 +
511 606 $notices[] = array(
512 607 'message' => $message,
513 608 'type' => $type,
514 609 'timestamp' => current_time('timestamp')
515 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 + }
516 629
517 630 set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS);
518 631 }
519 632