PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
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
metasync / includes / class-metasync-debug-mode-manager.php

class-metasync-debug-mode-manager.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at includes/class-metasync-debug-mode-manager.php

989 lines 29.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MetaSync Debug Mode Manager
4 *
5 * Manages debug mode with automatic disable, safety limits, and log rotation.
6 * Implements time-based auto-disable and file size limits to prevent log file growth issues.
7 *
8 * @package MetaSync
9 * @subpackage MetaSync/includes
10 * @since 2.5.15
11 */
12
13 // Prevent direct access
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 /**
19 * Class Metasync_Debug_Mode_Manager
20 *
21 * Handles all debug mode functionality including:
22 * - Time-based auto-disable (24 hours default)
23 * - File size monitoring and rotation (10MB max)
24 * - Manual override for indefinite debug mode
25 * - Admin notifications for state changes
26 * - Dashboard widget for status display
27 *
28 * @since 2.5.15
29 */
30 class Metasync_Debug_Mode_Manager
31 {
32 /**
33 * Singleton instance
34 *
35 * @var Metasync_Debug_Mode_Manager|null
36 */
37 private static $instance = null;
38
39 /**
40 * Maximum debug log file size in bytes (10MB)
41 *
42 * @var int
43 */
44 const MAX_LOG_SIZE = 10485760; // 10MB in bytes
45
46 /**
47 * Debug mode duration in seconds (24 hours)
48 *
49 * @var int
50 */
51 const DEBUG_DURATION = 86400; // 24 hours in seconds
52
53 /**
54 * Maximum number of rotated log files to keep
55 *
56 * @var int
57 */
58 const MAX_ROTATED_LOGS = 1; // Keep current + 1 old
59
60 /**
61 * Cron hook name for checking debug limits
62 *
63 * @var string
64 */
65 const CRON_HOOK = 'metasync_check_debug_limits';
66
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 /**
83 * Option key for debug mode settings
84 *
85 * @var string
86 */
87 const OPTION_KEY = 'metasync_debug_mode_settings';
88
89 /**
90 * Transient key for admin notices
91 *
92 * @var string
93 */
94 const NOTICE_TRANSIENT = 'metasync_debug_mode_notices';
95
96 /**
97 * Debug log file path
98 *
99 * @var string
100 */
101 private $log_file_path;
102
103 /**
104 * Get singleton instance
105 *
106 * @return Metasync_Debug_Mode_Manager
107 */
108 public static function get_instance()
109 {
110 if (self::$instance === null) {
111 self::$instance = new self();
112 }
113 return self::$instance;
114 }
115
116 /**
117 * Constructor - Initialize hooks and settings
118 */
119 private function __construct()
120 {
121 $this->log_file_path = WP_CONTENT_DIR . '/debug.log';
122 $this->init_hooks();
123 }
124
125 /**
126 * Initialize WordPress hooks
127 *
128 * @return void
129 */
130 private function init_hooks()
131 {
132 // Register cron schedules
133 add_filter('cron_schedules', array($this, 'register_cron_schedules'));
134
135 // Schedule cron job on activation
136 add_action('init', array($this, 'maybe_schedule_cron'));
137
138 // Cron job handler
139 add_action(self::CRON_HOOK, array($this, 'check_debug_limits'));
140
141 // Admin notices
142 add_action('admin_notices', array($this, 'display_admin_notices'));
143
144 // Dashboard widget
145 add_action('wp_dashboard_setup', array($this, 'register_dashboard_widget'));
146
147 // REST API endpoints
148 add_action('rest_api_init', array($this, 'register_rest_routes'));
149
150 // Enqueue admin scripts
151 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
152 }
153
154 /**
155 * Register custom cron schedules
156 *
157 * @param array $schedules Existing schedules
158 * @return array Modified schedules
159 */
160 public function register_cron_schedules($schedules)
161 {
162 // Ensure we don't override existing schedule
163 if (!isset($schedules['hourly'])) {
164 $schedules['hourly'] = array(
165 'interval' => 3600,
166 'display' => __('Once Hourly', 'metasync')
167 );
168 }
169 return $schedules;
170 }
171
172 /**
173 * Schedule cron job if not already scheduled
174 *
175 * @return void
176 */
177 public function maybe_schedule_cron()
178 {
179 if (!wp_next_scheduled(self::CRON_HOOK)) {
180 wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
181 }
182 }
183
184 /**
185 * Enable debug mode
186 *
187 * @param bool $indefinite Whether to enable indefinitely
188 * @return bool Success status
189 */
190 public function enable_debug_mode($indefinite = false)
191 {
192 $previousSettings = get_option(self::OPTION_KEY);
193
194 $settings = array(
195 'enabled' => true,
196 'enabled_at' => current_time('timestamp'),
197 'indefinite' => $indefinite,
198 'extended_count' => 0
199 );
200
201 $result = update_option(self::OPTION_KEY, $settings);
202
203 if ($result) {
204 // Enable WP_DEBUG constants via ConfigController
205 $write = $this->update_wp_debug_constants(true);
206
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.
210 if (!wp_next_scheduled(self::CRON_HOOK)) {
211 wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
212 }
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
230 // Clear any previous notices
231 delete_transient(self::NOTICE_TRANSIENT);
232
233 }
234
235 return $result;
236 }
237
238 /**
239 * Disable debug mode
240 *
241 * @param string $reason Reason for disabling
242 * @return bool Success status
243 */
244 public function disable_debug_mode($reason = 'manual')
245 {
246 $previousSettings = get_option(self::OPTION_KEY);
247
248 $settings = $this->get_settings();
249 $settings['enabled'] = false;
250 $settings['disabled_at'] = current_time('timestamp');
251 $settings['disabled_reason'] = $reason;
252
253 $result = update_option(self::OPTION_KEY, $settings);
254
255 if ($result) {
256 // Disable WP_DEBUG constants via ConfigController
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 }
265
266 return false;
267 }
268
269 // Add admin notice
270 $this->add_notice(
271 'Debug mode has been disabled (' . $reason . ').',
272 'info'
273 );
274
275 }
276
277 return $result;
278 }
279
280 /**
281 * Extend debug mode for another 24 hours
282 *
283 * @return bool Success status
284 */
285 public function extend_debug_mode()
286 {
287 $settings = $this->get_settings();
288
289 if (!$settings['enabled']) {
290 return false;
291 }
292
293 $settings['enabled_at'] = current_time('timestamp');
294 $settings['extended_count'] = ($settings['extended_count'] ?? 0) + 1;
295
296 $result = update_option(self::OPTION_KEY, $settings);
297
298 if ($result) {
299 $this->add_notice(
300 'Debug mode extended for another 24 hours.',
301 'success'
302 );
303 }
304
305 return $result;
306 }
307
308 /**
309 * Toggle indefinite mode
310 *
311 * @param bool $enable Whether to enable indefinite mode
312 * @return bool Success status
313 */
314 public function toggle_indefinite_mode($enable)
315 {
316 $settings = $this->get_settings();
317 $settings['indefinite'] = $enable;
318
319 return update_option(self::OPTION_KEY, $settings);
320 }
321
322 /**
323 * Check debug limits (called by cron)
324 *
325 * @return void
326 */
327 public function check_debug_limits()
328 {
329 $this->check_time_limit();
330 $this->check_file_size_limit();
331 }
332
333 /**
334 * Check if debug mode has exceeded time limit
335 *
336 * @return void
337 */
338 private function check_time_limit()
339 {
340 $settings = $this->get_settings();
341
342 // Skip if debug mode is disabled or in indefinite mode
343 if (!$settings['enabled'] || $settings['indefinite']) {
344 return;
345 }
346
347 $enabled_at = $settings['enabled_at'];
348 $current_time = current_time('timestamp');
349 $elapsed_time = $current_time - $enabled_at;
350
351 // Check if 24 hours have passed
352 if ($elapsed_time >= self::DEBUG_DURATION) {
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 }
362 }
363 }
364
365 /**
366 * Check if debug log file has exceeded size limit
367 *
368 * @return void
369 */
370 private function check_file_size_limit()
371 {
372 if (!file_exists($this->log_file_path)) {
373 return;
374 }
375
376 $file_size = filesize($this->log_file_path);
377
378 if ($file_size >= self::MAX_LOG_SIZE) {
379 $this->rotate_log_file();
380 $this->add_notice(
381 sprintf('Debug log rotated due to size limit (%s).', $this->format_bytes(self::MAX_LOG_SIZE)),
382 'info'
383 );
384 }
385 }
386
387 /**
388 * Rotate debug log file
389 *
390 * @return bool Success status
391 */
392 private function rotate_log_file()
393 {
394 if (!file_exists($this->log_file_path)) {
395 return false;
396 }
397
398 $backup_path = $this->log_file_path . '.old';
399
400 // Remove existing .old file if it exists
401 if (file_exists($backup_path)) {
402 @wp_delete_file($backup_path);
403 }
404
405 // Rename current log to .old
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
407
408 if ($result) {
409 // Create new empty log file
410 @file_put_contents($this->log_file_path, '');
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
412 }
413
414 // Cleanup old rotations (keep only MAX_ROTATED_LOGS)
415 $this->cleanup_old_rotations();
416
417 return $result;
418 }
419
420 /**
421 * Cleanup old log rotations
422 *
423 * @return void
424 */
425 private function cleanup_old_rotations()
426 {
427 $log_dir = dirname($this->log_file_path);
428 $log_basename = basename($this->log_file_path);
429 $pattern = $log_dir . '/' . $log_basename . '.old*';
430
431 $old_logs = glob($pattern);
432
433 if (count($old_logs) > self::MAX_ROTATED_LOGS) {
434 // Sort by modification time (oldest first)
435 usort($old_logs, function ($a, $b) {
436 return filemtime($a) - filemtime($b);
437 });
438
439 // Delete oldest files, keep only MAX_ROTATED_LOGS
440 $to_delete = array_slice($old_logs, 0, count($old_logs) - self::MAX_ROTATED_LOGS);
441 foreach ($to_delete as $old_log) {
442 @wp_delete_file($old_log);
443 }
444 }
445 }
446
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 /**
465 * Get current debug mode settings
466 *
467 * @return array Debug mode settings
468 */
469 public function get_settings()
470 {
471 $defaults = array(
472 'enabled' => false,
473 'enabled_at' => 0,
474 'indefinite' => false,
475 'extended_count' => 0,
476 'disabled_at' => 0,
477 'disabled_reason' => ''
478 );
479
480 $settings = get_option(self::OPTION_KEY, $defaults);
481
482 return wp_parse_args($settings, $defaults);
483 }
484
485 /**
486 * Get debug mode status for dashboard widget
487 *
488 * @return array Status information
489 */
490 public function get_status()
491 {
492 $settings = $this->get_settings();
493 $file_size = file_exists($this->log_file_path) ? filesize($this->log_file_path) : 0;
494
495 $status = array(
496 'enabled' => $settings['enabled'],
497 'indefinite' => $settings['indefinite'],
498 'enabled_at' => $settings['enabled_at'],
499 'time_remaining' => 0,
500 'time_remaining_formatted' => 'N/A',
501 'log_file_size' => $file_size,
502 'log_file_size_formatted' => $this->format_bytes($file_size),
503 'log_file_path' => $this->log_file_path,
504 'max_log_size' => self::MAX_LOG_SIZE,
505 'max_log_size_formatted' => $this->format_bytes(self::MAX_LOG_SIZE),
506 'percentage_used' => 0
507 );
508
509 if ($settings['enabled'] && !$settings['indefinite']) {
510 $elapsed_time = current_time('timestamp') - $settings['enabled_at'];
511 $time_remaining = max(0, self::DEBUG_DURATION - $elapsed_time);
512 $status['time_remaining'] = $time_remaining;
513 $status['time_remaining_formatted'] = $this->format_time_remaining($time_remaining);
514 } elseif ($settings['enabled'] && $settings['indefinite']) {
515 $status['time_remaining_formatted'] = 'Indefinite';
516 }
517
518 if (self::MAX_LOG_SIZE > 0) {
519 $status['percentage_used'] = min(100, ($file_size / self::MAX_LOG_SIZE) * 100);
520 }
521
522 return $status;
523 }
524
525 /**
526 * Update WP_DEBUG constants via ConfigController
527 *
528 * @param bool $enable Whether to enable or disable
529 * @return string One of self::WRITE_OK, self::WRITE_PARTIAL or self::WRITE_FAILED.
530 */
531 private function update_wp_debug_constants($enable)
532 {
533 $previousEnabled = get_option('wp_debug_enabled', 'false');
534 $previousLog = get_option('wp_debug_log_enabled', 'false');
535
536 try {
537 update_option('wp_debug_enabled', $enable ? 'true' : 'false');
538 update_option('wp_debug_log_enabled', $enable ? 'true' : 'false');
539
540 $config_controller = new ConfigControllerMetaSync();
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;
578 }
579 }
580
581 /**
582 * Add admin notice
583 *
584 * @param string $message Notice message
585 * @param string $type Notice type (success, warning, error, info)
586 * @return void
587 */
588 private function add_notice($message, $type = 'info')
589 {
590 $notices = get_transient(self::NOTICE_TRANSIENT);
591 if (!is_array($notices)) {
592 $notices = array();
593 }
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
606 $notices[] = array(
607 'message' => $message,
608 'type' => $type,
609 'timestamp' => current_time('timestamp')
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 }
629
630 set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS);
631 }
632
633 /**
634 * Display admin notices
635 *
636 * @return void
637 */
638 public function display_admin_notices()
639 {
640 $notices = get_transient(self::NOTICE_TRANSIENT);
641
642 if (!is_array($notices) || empty($notices)) {
643 return;
644 }
645
646 foreach ($notices as $notice) {
647 $class = 'notice notice-' . $notice['type'] . ' is-dismissible';
648 printf(
649 '<div class="%1$s"><p><strong>MetaSync Debug Mode:</strong> %2$s</p></div>',
650 esc_attr($class),
651 esc_html($notice['message'])
652 );
653 }
654
655 // Clear notices after displaying
656 delete_transient(self::NOTICE_TRANSIENT);
657 }
658
659 /**
660 * Register dashboard widget
661 *
662 * @return void
663 */
664 public function register_dashboard_widget()
665 {
666 // Only show to users with manage_options capability
667 if (!current_user_can('manage_options')) {
668 return;
669 }
670
671 $status = $this->get_status();
672
673 // Only show widget if debug mode is enabled
674 if (!$status['enabled']) {
675 return;
676 }
677
678 wp_add_dashboard_widget(
679 'metasync_debug_mode_widget',
680 'MetaSync Debug Mode',
681 array($this, 'render_dashboard_widget')
682 );
683 }
684
685 /**
686 * Render dashboard widget
687 *
688 * @return void
689 */
690 public function render_dashboard_widget()
691 {
692 $status = $this->get_status();
693 ?>
694 <div class="metasync-debug-widget">
695 <div class="debug-status">
696 <span class="status-indicator <?php echo $status['enabled'] ? 'active' : 'inactive'; ?>">
697 <?php echo $status['enabled'] ? '⚠️ Active' : '✓ Inactive'; ?>
698 </span>
699 </div>
700
701 <?php if ($status['enabled']): ?>
702 <div class="debug-info">
703 <p>
704 <strong>Auto-disable in:</strong>
705 <span class="time-remaining"><?php echo esc_html($status['time_remaining_formatted']); ?></span>
706 </p>
707 <p>
708 <strong>Log file size:</strong>
709 <span class="file-size">
710 <?php echo esc_html($status['log_file_size_formatted']); ?> /
711 <?php echo esc_html($status['max_log_size_formatted']); ?>
712 </span>
713 </p>
714 <div class="progress-bar">
715 <div class="progress-fill" style="width: <?php echo esc_attr($status['percentage_used']); ?>%"></div>
716 </div>
717 </div>
718
719 <div class="debug-actions">
720 <?php if (!$status['indefinite']): ?>
721 <button type="button" class="button button-secondary" id="metasync-extend-debug">
722 Extend for 24 Hours
723 </button>
724 <?php endif; ?>
725 <button type="button" class="button button-primary" id="metasync-disable-debug">
726 Disable Now
727 </button>
728 </div>
729 <?php endif; ?>
730 </div>
731
732 <style>
733 .metasync-debug-widget {
734 padding: 10px 0;
735 }
736 .debug-status {
737 margin-bottom: 15px;
738 font-size: 16px;
739 }
740 .status-indicator {
741 display: inline-block;
742 padding: 5px 10px;
743 border-radius: 3px;
744 font-weight: 600;
745 }
746 .status-indicator.active {
747 background: #fff3cd;
748 color: #856404;
749 }
750 .status-indicator.inactive {
751 background: #d4edda;
752 color: #155724;
753 }
754 .debug-info p {
755 margin: 8px 0;
756 }
757 .progress-bar {
758 width: 100%;
759 height: 20px;
760 background: #f0f0f0;
761 border-radius: 3px;
762 overflow: hidden;
763 margin: 10px 0;
764 }
765 .progress-fill {
766 height: 100%;
767 background: linear-gradient(90deg, #46b450 0%, #ffb900 70%, #dc3232 100%);
768 transition: width 0.3s ease;
769 }
770 .debug-actions {
771 margin-top: 15px;
772 display: flex;
773 gap: 10px;
774 }
775 .debug-actions button {
776 flex: 1;
777 }
778 </style>
779 <?php
780 }
781
782 /**
783 * Enqueue admin scripts
784 *
785 * @param string $hook Current admin page hook
786 * @return void
787 */
788 public function enqueue_admin_scripts($hook)
789 {
790 // Only enqueue on dashboard
791 if ($hook !== 'index.php') {
792 return;
793 }
794
795 wp_enqueue_script(
796 'metasync-debug-widget',
797 plugin_dir_url(dirname(__FILE__)) . 'admin/js/debug-widget.js',
798 array('jquery'),
799 METASYNC_VERSION,
800 true
801 );
802
803 wp_localize_script('metasync-debug-widget', 'metasyncDebug', array(
804 'ajaxurl' => admin_url('admin-ajax.php'),
805 'nonce' => wp_create_nonce('metasync_debug_mode'),
806 'restUrl' => rest_url('metasync/v1/debug-mode/'),
807 'restNonce' => wp_create_nonce('wp_rest')
808 ));
809 }
810
811 /**
812 * Register REST API routes
813 *
814 * @return void
815 */
816 public function register_rest_routes()
817 {
818 register_rest_route('metasync/v1', '/debug-mode/status', array(
819 'methods' => 'GET',
820 'callback' => array($this, 'rest_get_status'),
821 'permission_callback' => array($this, 'rest_permission_check')
822 ));
823
824 register_rest_route('metasync/v1', '/debug-mode/enable', array(
825 'methods' => 'POST',
826 'callback' => array($this, 'rest_enable_debug'),
827 'permission_callback' => array($this, 'rest_permission_check')
828 ));
829
830 register_rest_route('metasync/v1', '/debug-mode/disable', array(
831 'methods' => 'POST',
832 'callback' => array($this, 'rest_disable_debug'),
833 'permission_callback' => array($this, 'rest_permission_check')
834 ));
835
836 register_rest_route('metasync/v1', '/debug-mode/extend', array(
837 'methods' => 'POST',
838 'callback' => array($this, 'rest_extend_debug'),
839 'permission_callback' => array($this, 'rest_permission_check')
840 ));
841 }
842
843 /**
844 * REST API permission check
845 *
846 * @return bool
847 */
848 public function rest_permission_check()
849 {
850 return current_user_can('manage_options');
851 }
852
853 /**
854 * REST API: Get debug mode status
855 *
856 * @return WP_REST_Response
857 */
858 public function rest_get_status()
859 {
860 return new WP_REST_Response($this->get_status(), 200);
861 }
862
863 /**
864 * REST API: Enable debug mode
865 *
866 * @param WP_REST_Request $request
867 * @return WP_REST_Response
868 */
869 public function rest_enable_debug($request)
870 {
871 $indefinite = $request->get_param('indefinite') === true;
872 $result = $this->enable_debug_mode($indefinite);
873
874 if ($result) {
875 return new WP_REST_Response(array(
876 'success' => true,
877 'message' => 'Debug mode enabled',
878 'status' => $this->get_status()
879 ), 200);
880 }
881
882 return new WP_REST_Response(array(
883 'success' => false,
884 'message' => 'Failed to enable debug mode'
885 ), 500);
886 }
887
888 /**
889 * REST API: Disable debug mode
890 *
891 * @return WP_REST_Response
892 */
893 public function rest_disable_debug()
894 {
895 $result = $this->disable_debug_mode('manual');
896
897 if ($result) {
898 return new WP_REST_Response(array(
899 'success' => true,
900 'message' => 'Debug mode disabled',
901 'status' => $this->get_status()
902 ), 200);
903 }
904
905 return new WP_REST_Response(array(
906 'success' => false,
907 'message' => 'Failed to disable debug mode'
908 ), 500);
909 }
910
911 /**
912 * REST API: Extend debug mode
913 *
914 * @return WP_REST_Response
915 */
916 public function rest_extend_debug()
917 {
918 $result = $this->extend_debug_mode();
919
920 if ($result) {
921 return new WP_REST_Response(array(
922 'success' => true,
923 'message' => 'Debug mode extended for 24 hours',
924 'status' => $this->get_status()
925 ), 200);
926 }
927
928 return new WP_REST_Response(array(
929 'success' => false,
930 'message' => 'Failed to extend debug mode'
931 ), 500);
932 }
933
934 /**
935 * Format bytes to human-readable format
936 *
937 * @param int $bytes File size in bytes
938 * @return string Formatted size
939 */
940 private function format_bytes($bytes)
941 {
942 if ($bytes == 0) {
943 return '0 B';
944 }
945
946 $units = array('B', 'KB', 'MB', 'GB');
947 $factor = floor((strlen($bytes) - 1) / 3);
948
949 return sprintf('%.2f %s', $bytes / pow(1024, $factor), $units[$factor]);
950 }
951
952 /**
953 * Format time remaining to human-readable format
954 *
955 * @param int $seconds Time in seconds
956 * @return string Formatted time
957 */
958 private function format_time_remaining($seconds)
959 {
960 if ($seconds <= 0) {
961 return 'Expired';
962 }
963
964 $hours = floor($seconds / 3600);
965 $minutes = floor(($seconds % 3600) / 60);
966
967 if ($hours > 0) {
968 return sprintf('%d hours %d minutes', $hours, $minutes);
969 }
970
971 return sprintf('%d minutes', $minutes);
972 }
973
974 /**
975 * Uninstall - Clean up options and cron jobs
976 *
977 * @return void
978 */
979 public static function uninstall()
980 {
981 // Remove scheduled cron
982 wp_clear_scheduled_hook(self::CRON_HOOK);
983
984 // Remove options
985 delete_option(self::OPTION_KEY);
986 delete_transient(self::NOTICE_TRANSIENT);
987 }
988 }
989