PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
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 2.5.23 All 138 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 trunk, at includes/class-metasync-debug-mode-manager.php

993 lines 29.4 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 // Log the action
234 error_log('MetaSync: Debug mode enabled' . ($indefinite ? ' (indefinite)' : ' (24 hours)'));
235 }
236
237 return $result;
238 }
239
240 /**
241 * Disable debug mode
242 *
243 * @param string $reason Reason for disabling
244 * @return bool Success status
245 */
246 public function disable_debug_mode($reason = 'manual')
247 {
248 $previousSettings = get_option(self::OPTION_KEY);
249
250 $settings = $this->get_settings();
251 $settings['enabled'] = false;
252 $settings['disabled_at'] = current_time('timestamp');
253 $settings['disabled_reason'] = $reason;
254
255 $result = update_option(self::OPTION_KEY, $settings);
256
257 if ($result) {
258 // Disable WP_DEBUG constants via ConfigController
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 }
267
268 return false;
269 }
270
271 // Add admin notice
272 $this->add_notice(
273 'Debug mode has been disabled (' . $reason . ').',
274 'info'
275 );
276
277 // Log the action
278 error_log('MetaSync: Debug mode disabled - ' . $reason);
279 }
280
281 return $result;
282 }
283
284 /**
285 * Extend debug mode for another 24 hours
286 *
287 * @return bool Success status
288 */
289 public function extend_debug_mode()
290 {
291 $settings = $this->get_settings();
292
293 if (!$settings['enabled']) {
294 return false;
295 }
296
297 $settings['enabled_at'] = current_time('timestamp');
298 $settings['extended_count'] = ($settings['extended_count'] ?? 0) + 1;
299
300 $result = update_option(self::OPTION_KEY, $settings);
301
302 if ($result) {
303 $this->add_notice(
304 'Debug mode extended for another 24 hours.',
305 'success'
306 );
307 }
308
309 return $result;
310 }
311
312 /**
313 * Toggle indefinite mode
314 *
315 * @param bool $enable Whether to enable indefinite mode
316 * @return bool Success status
317 */
318 public function toggle_indefinite_mode($enable)
319 {
320 $settings = $this->get_settings();
321 $settings['indefinite'] = $enable;
322
323 return update_option(self::OPTION_KEY, $settings);
324 }
325
326 /**
327 * Check debug limits (called by cron)
328 *
329 * @return void
330 */
331 public function check_debug_limits()
332 {
333 $this->check_time_limit();
334 $this->check_file_size_limit();
335 }
336
337 /**
338 * Check if debug mode has exceeded time limit
339 *
340 * @return void
341 */
342 private function check_time_limit()
343 {
344 $settings = $this->get_settings();
345
346 // Skip if debug mode is disabled or in indefinite mode
347 if (!$settings['enabled'] || $settings['indefinite']) {
348 return;
349 }
350
351 $enabled_at = $settings['enabled_at'];
352 $current_time = current_time('timestamp');
353 $elapsed_time = $current_time - $enabled_at;
354
355 // Check if 24 hours have passed
356 if ($elapsed_time >= self::DEBUG_DURATION) {
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 }
366 }
367 }
368
369 /**
370 * Check if debug log file has exceeded size limit
371 *
372 * @return void
373 */
374 private function check_file_size_limit()
375 {
376 if (!file_exists($this->log_file_path)) {
377 return;
378 }
379
380 $file_size = filesize($this->log_file_path);
381
382 if ($file_size >= self::MAX_LOG_SIZE) {
383 $this->rotate_log_file();
384 $this->add_notice(
385 sprintf('Debug log rotated due to size limit (%s).', $this->format_bytes(self::MAX_LOG_SIZE)),
386 'info'
387 );
388 }
389 }
390
391 /**
392 * Rotate debug log file
393 *
394 * @return bool Success status
395 */
396 private function rotate_log_file()
397 {
398 if (!file_exists($this->log_file_path)) {
399 return false;
400 }
401
402 $backup_path = $this->log_file_path . '.old';
403
404 // Remove existing .old file if it exists
405 if (file_exists($backup_path)) {
406 @unlink($backup_path);
407 }
408
409 // Rename current log to .old
410 $result = @rename($this->log_file_path, $backup_path);
411
412 if ($result) {
413 // Create new empty log file
414 @file_put_contents($this->log_file_path, '');
415 error_log('MetaSync: Debug log rotated - exceeded 10MB limit');
416 }
417
418 // Cleanup old rotations (keep only MAX_ROTATED_LOGS)
419 $this->cleanup_old_rotations();
420
421 return $result;
422 }
423
424 /**
425 * Cleanup old log rotations
426 *
427 * @return void
428 */
429 private function cleanup_old_rotations()
430 {
431 $log_dir = dirname($this->log_file_path);
432 $log_basename = basename($this->log_file_path);
433 $pattern = $log_dir . '/' . $log_basename . '.old*';
434
435 $old_logs = glob($pattern);
436
437 if (count($old_logs) > self::MAX_ROTATED_LOGS) {
438 // Sort by modification time (oldest first)
439 usort($old_logs, function ($a, $b) {
440 return filemtime($a) - filemtime($b);
441 });
442
443 // Delete oldest files, keep only MAX_ROTATED_LOGS
444 $to_delete = array_slice($old_logs, 0, count($old_logs) - self::MAX_ROTATED_LOGS);
445 foreach ($to_delete as $old_log) {
446 @unlink($old_log);
447 }
448 }
449 }
450
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 /**
469 * Get current debug mode settings
470 *
471 * @return array Debug mode settings
472 */
473 public function get_settings()
474 {
475 $defaults = array(
476 'enabled' => false,
477 'enabled_at' => 0,
478 'indefinite' => false,
479 'extended_count' => 0,
480 'disabled_at' => 0,
481 'disabled_reason' => ''
482 );
483
484 $settings = get_option(self::OPTION_KEY, $defaults);
485
486 return wp_parse_args($settings, $defaults);
487 }
488
489 /**
490 * Get debug mode status for dashboard widget
491 *
492 * @return array Status information
493 */
494 public function get_status()
495 {
496 $settings = $this->get_settings();
497 $file_size = file_exists($this->log_file_path) ? filesize($this->log_file_path) : 0;
498
499 $status = array(
500 'enabled' => $settings['enabled'],
501 'indefinite' => $settings['indefinite'],
502 'enabled_at' => $settings['enabled_at'],
503 'time_remaining' => 0,
504 'time_remaining_formatted' => 'N/A',
505 'log_file_size' => $file_size,
506 'log_file_size_formatted' => $this->format_bytes($file_size),
507 'log_file_path' => $this->log_file_path,
508 'max_log_size' => self::MAX_LOG_SIZE,
509 'max_log_size_formatted' => $this->format_bytes(self::MAX_LOG_SIZE),
510 'percentage_used' => 0
511 );
512
513 if ($settings['enabled'] && !$settings['indefinite']) {
514 $elapsed_time = current_time('timestamp') - $settings['enabled_at'];
515 $time_remaining = max(0, self::DEBUG_DURATION - $elapsed_time);
516 $status['time_remaining'] = $time_remaining;
517 $status['time_remaining_formatted'] = $this->format_time_remaining($time_remaining);
518 } elseif ($settings['enabled'] && $settings['indefinite']) {
519 $status['time_remaining_formatted'] = 'Indefinite';
520 }
521
522 if (self::MAX_LOG_SIZE > 0) {
523 $status['percentage_used'] = min(100, ($file_size / self::MAX_LOG_SIZE) * 100);
524 }
525
526 return $status;
527 }
528
529 /**
530 * Update WP_DEBUG constants via ConfigController
531 *
532 * @param bool $enable Whether to enable or disable
533 * @return string One of self::WRITE_OK, self::WRITE_PARTIAL or self::WRITE_FAILED.
534 */
535 private function update_wp_debug_constants($enable)
536 {
537 $previousEnabled = get_option('wp_debug_enabled', 'false');
538 $previousLog = get_option('wp_debug_log_enabled', 'false');
539
540 try {
541 update_option('wp_debug_enabled', $enable ? 'true' : 'false');
542 update_option('wp_debug_log_enabled', $enable ? 'true' : 'false');
543
544 $config_controller = new ConfigControllerMetaSync();
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);
580 error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage());
581 return self::WRITE_FAILED;
582 }
583 }
584
585 /**
586 * Add admin notice
587 *
588 * @param string $message Notice message
589 * @param string $type Notice type (success, warning, error, info)
590 * @return void
591 */
592 private function add_notice($message, $type = 'info')
593 {
594 $notices = get_transient(self::NOTICE_TRANSIENT);
595 if (!is_array($notices)) {
596 $notices = array();
597 }
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
610 $notices[] = array(
611 'message' => $message,
612 'type' => $type,
613 'timestamp' => current_time('timestamp')
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 }
633
634 set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS);
635 }
636
637 /**
638 * Display admin notices
639 *
640 * @return void
641 */
642 public function display_admin_notices()
643 {
644 $notices = get_transient(self::NOTICE_TRANSIENT);
645
646 if (!is_array($notices) || empty($notices)) {
647 return;
648 }
649
650 foreach ($notices as $notice) {
651 $class = 'notice notice-' . esc_attr($notice['type']) . ' is-dismissible';
652 printf(
653 '<div class="%1$s"><p><strong>MetaSync Debug Mode:</strong> %2$s</p></div>',
654 $class,
655 esc_html($notice['message'])
656 );
657 }
658
659 // Clear notices after displaying
660 delete_transient(self::NOTICE_TRANSIENT);
661 }
662
663 /**
664 * Register dashboard widget
665 *
666 * @return void
667 */
668 public function register_dashboard_widget()
669 {
670 // Only show to users with manage_options capability
671 if (!current_user_can('manage_options')) {
672 return;
673 }
674
675 $status = $this->get_status();
676
677 // Only show widget if debug mode is enabled
678 if (!$status['enabled']) {
679 return;
680 }
681
682 wp_add_dashboard_widget(
683 'metasync_debug_mode_widget',
684 'MetaSync Debug Mode',
685 array($this, 'render_dashboard_widget')
686 );
687 }
688
689 /**
690 * Render dashboard widget
691 *
692 * @return void
693 */
694 public function render_dashboard_widget()
695 {
696 $status = $this->get_status();
697 ?>
698 <div class="metasync-debug-widget">
699 <div class="debug-status">
700 <span class="status-indicator <?php echo $status['enabled'] ? 'active' : 'inactive'; ?>">
701 <?php echo $status['enabled'] ? '⚠️ Active' : '✓ Inactive'; ?>
702 </span>
703 </div>
704
705 <?php if ($status['enabled']): ?>
706 <div class="debug-info">
707 <p>
708 <strong>Auto-disable in:</strong>
709 <span class="time-remaining"><?php echo esc_html($status['time_remaining_formatted']); ?></span>
710 </p>
711 <p>
712 <strong>Log file size:</strong>
713 <span class="file-size">
714 <?php echo esc_html($status['log_file_size_formatted']); ?> /
715 <?php echo esc_html($status['max_log_size_formatted']); ?>
716 </span>
717 </p>
718 <div class="progress-bar">
719 <div class="progress-fill" style="width: <?php echo esc_attr($status['percentage_used']); ?>%"></div>
720 </div>
721 </div>
722
723 <div class="debug-actions">
724 <?php if (!$status['indefinite']): ?>
725 <button type="button" class="button button-secondary" id="metasync-extend-debug">
726 Extend for 24 Hours
727 </button>
728 <?php endif; ?>
729 <button type="button" class="button button-primary" id="metasync-disable-debug">
730 Disable Now
731 </button>
732 </div>
733 <?php endif; ?>
734 </div>
735
736 <style>
737 .metasync-debug-widget {
738 padding: 10px 0;
739 }
740 .debug-status {
741 margin-bottom: 15px;
742 font-size: 16px;
743 }
744 .status-indicator {
745 display: inline-block;
746 padding: 5px 10px;
747 border-radius: 3px;
748 font-weight: 600;
749 }
750 .status-indicator.active {
751 background: #fff3cd;
752 color: #856404;
753 }
754 .status-indicator.inactive {
755 background: #d4edda;
756 color: #155724;
757 }
758 .debug-info p {
759 margin: 8px 0;
760 }
761 .progress-bar {
762 width: 100%;
763 height: 20px;
764 background: #f0f0f0;
765 border-radius: 3px;
766 overflow: hidden;
767 margin: 10px 0;
768 }
769 .progress-fill {
770 height: 100%;
771 background: linear-gradient(90deg, #46b450 0%, #ffb900 70%, #dc3232 100%);
772 transition: width 0.3s ease;
773 }
774 .debug-actions {
775 margin-top: 15px;
776 display: flex;
777 gap: 10px;
778 }
779 .debug-actions button {
780 flex: 1;
781 }
782 </style>
783 <?php
784 }
785
786 /**
787 * Enqueue admin scripts
788 *
789 * @param string $hook Current admin page hook
790 * @return void
791 */
792 public function enqueue_admin_scripts($hook)
793 {
794 // Only enqueue on dashboard
795 if ($hook !== 'index.php') {
796 return;
797 }
798
799 wp_enqueue_script(
800 'metasync-debug-widget',
801 plugin_dir_url(dirname(__FILE__)) . 'admin/js/debug-widget.js',
802 array('jquery'),
803 METASYNC_VERSION,
804 true
805 );
806
807 wp_localize_script('metasync-debug-widget', 'metasyncDebug', array(
808 'ajaxurl' => admin_url('admin-ajax.php'),
809 'nonce' => wp_create_nonce('metasync_debug_mode'),
810 'restUrl' => rest_url('metasync/v1/debug-mode/'),
811 'restNonce' => wp_create_nonce('wp_rest')
812 ));
813 }
814
815 /**
816 * Register REST API routes
817 *
818 * @return void
819 */
820 public function register_rest_routes()
821 {
822 register_rest_route('metasync/v1', '/debug-mode/status', array(
823 'methods' => 'GET',
824 'callback' => array($this, 'rest_get_status'),
825 'permission_callback' => array($this, 'rest_permission_check')
826 ));
827
828 register_rest_route('metasync/v1', '/debug-mode/enable', array(
829 'methods' => 'POST',
830 'callback' => array($this, 'rest_enable_debug'),
831 'permission_callback' => array($this, 'rest_permission_check')
832 ));
833
834 register_rest_route('metasync/v1', '/debug-mode/disable', array(
835 'methods' => 'POST',
836 'callback' => array($this, 'rest_disable_debug'),
837 'permission_callback' => array($this, 'rest_permission_check')
838 ));
839
840 register_rest_route('metasync/v1', '/debug-mode/extend', array(
841 'methods' => 'POST',
842 'callback' => array($this, 'rest_extend_debug'),
843 'permission_callback' => array($this, 'rest_permission_check')
844 ));
845 }
846
847 /**
848 * REST API permission check
849 *
850 * @return bool
851 */
852 public function rest_permission_check()
853 {
854 return current_user_can('manage_options');
855 }
856
857 /**
858 * REST API: Get debug mode status
859 *
860 * @return WP_REST_Response
861 */
862 public function rest_get_status()
863 {
864 return new WP_REST_Response($this->get_status(), 200);
865 }
866
867 /**
868 * REST API: Enable debug mode
869 *
870 * @param WP_REST_Request $request
871 * @return WP_REST_Response
872 */
873 public function rest_enable_debug($request)
874 {
875 $indefinite = $request->get_param('indefinite') === true;
876 $result = $this->enable_debug_mode($indefinite);
877
878 if ($result) {
879 return new WP_REST_Response(array(
880 'success' => true,
881 'message' => 'Debug mode enabled',
882 'status' => $this->get_status()
883 ), 200);
884 }
885
886 return new WP_REST_Response(array(
887 'success' => false,
888 'message' => 'Failed to enable debug mode'
889 ), 500);
890 }
891
892 /**
893 * REST API: Disable debug mode
894 *
895 * @return WP_REST_Response
896 */
897 public function rest_disable_debug()
898 {
899 $result = $this->disable_debug_mode('manual');
900
901 if ($result) {
902 return new WP_REST_Response(array(
903 'success' => true,
904 'message' => 'Debug mode disabled',
905 'status' => $this->get_status()
906 ), 200);
907 }
908
909 return new WP_REST_Response(array(
910 'success' => false,
911 'message' => 'Failed to disable debug mode'
912 ), 500);
913 }
914
915 /**
916 * REST API: Extend debug mode
917 *
918 * @return WP_REST_Response
919 */
920 public function rest_extend_debug()
921 {
922 $result = $this->extend_debug_mode();
923
924 if ($result) {
925 return new WP_REST_Response(array(
926 'success' => true,
927 'message' => 'Debug mode extended for 24 hours',
928 'status' => $this->get_status()
929 ), 200);
930 }
931
932 return new WP_REST_Response(array(
933 'success' => false,
934 'message' => 'Failed to extend debug mode'
935 ), 500);
936 }
937
938 /**
939 * Format bytes to human-readable format
940 *
941 * @param int $bytes File size in bytes
942 * @return string Formatted size
943 */
944 private function format_bytes($bytes)
945 {
946 if ($bytes == 0) {
947 return '0 B';
948 }
949
950 $units = array('B', 'KB', 'MB', 'GB');
951 $factor = floor((strlen($bytes) - 1) / 3);
952
953 return sprintf('%.2f %s', $bytes / pow(1024, $factor), $units[$factor]);
954 }
955
956 /**
957 * Format time remaining to human-readable format
958 *
959 * @param int $seconds Time in seconds
960 * @return string Formatted time
961 */
962 private function format_time_remaining($seconds)
963 {
964 if ($seconds <= 0) {
965 return 'Expired';
966 }
967
968 $hours = floor($seconds / 3600);
969 $minutes = floor(($seconds % 3600) / 60);
970
971 if ($hours > 0) {
972 return sprintf('%d hours %d minutes', $hours, $minutes);
973 }
974
975 return sprintf('%d minutes', $minutes);
976 }
977
978 /**
979 * Uninstall - Clean up options and cron jobs
980 *
981 * @return void
982 */
983 public static function uninstall()
984 {
985 // Remove scheduled cron
986 wp_clear_scheduled_hook(self::CRON_HOOK);
987
988 // Remove options
989 delete_option(self::OPTION_KEY);
990 delete_transient(self::NOTICE_TRANSIENT);
991 }
992 }
993