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

876 lines 23.9 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 * Option key for debug mode settings
69 *
70 * @var string
71 */
72 const OPTION_KEY = 'metasync_debug_mode_settings';
73
74 /**
75 * Transient key for admin notices
76 *
77 * @var string
78 */
79 const NOTICE_TRANSIENT = 'metasync_debug_mode_notices';
80
81 /**
82 * Debug log file path
83 *
84 * @var string
85 */
86 private $log_file_path;
87
88 /**
89 * Get singleton instance
90 *
91 * @return Metasync_Debug_Mode_Manager
92 */
93 public static function get_instance()
94 {
95 if (self::$instance === null) {
96 self::$instance = new self();
97 }
98 return self::$instance;
99 }
100
101 /**
102 * Constructor - Initialize hooks and settings
103 */
104 private function __construct()
105 {
106 $this->log_file_path = WP_CONTENT_DIR . '/debug.log';
107 $this->init_hooks();
108 }
109
110 /**
111 * Initialize WordPress hooks
112 *
113 * @return void
114 */
115 private function init_hooks()
116 {
117 // Register cron schedules
118 add_filter('cron_schedules', array($this, 'register_cron_schedules'));
119
120 // Schedule cron job on activation
121 add_action('init', array($this, 'maybe_schedule_cron'));
122
123 // Cron job handler
124 add_action(self::CRON_HOOK, array($this, 'check_debug_limits'));
125
126 // Admin notices
127 add_action('admin_notices', array($this, 'display_admin_notices'));
128
129 // Dashboard widget
130 add_action('wp_dashboard_setup', array($this, 'register_dashboard_widget'));
131
132 // REST API endpoints
133 add_action('rest_api_init', array($this, 'register_rest_routes'));
134
135 // Enqueue admin scripts
136 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
137 }
138
139 /**
140 * Register custom cron schedules
141 *
142 * @param array $schedules Existing schedules
143 * @return array Modified schedules
144 */
145 public function register_cron_schedules($schedules)
146 {
147 // Ensure we don't override existing schedule
148 if (!isset($schedules['hourly'])) {
149 $schedules['hourly'] = array(
150 'interval' => 3600,
151 'display' => __('Once Hourly', 'metasync')
152 );
153 }
154 return $schedules;
155 }
156
157 /**
158 * Schedule cron job if not already scheduled
159 *
160 * @return void
161 */
162 public function maybe_schedule_cron()
163 {
164 if (!wp_next_scheduled(self::CRON_HOOK)) {
165 wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
166 }
167 }
168
169 /**
170 * Enable debug mode
171 *
172 * @param bool $indefinite Whether to enable indefinitely
173 * @return bool Success status
174 */
175 public function enable_debug_mode($indefinite = false)
176 {
177 $settings = array(
178 'enabled' => true,
179 'enabled_at' => current_time('timestamp'),
180 'indefinite' => $indefinite,
181 'extended_count' => 0
182 );
183
184 $result = update_option(self::OPTION_KEY, $settings);
185
186 if ($result) {
187 // Enable WP_DEBUG constants via ConfigController
188 $this->update_wp_debug_constants(true);
189
190 // Ensure cron job is scheduled (safety net)
191 if (!wp_next_scheduled(self::CRON_HOOK)) {
192 wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
193 }
194
195 // Clear any previous notices
196 delete_transient(self::NOTICE_TRANSIENT);
197
198 // Log the action
199 error_log('MetaSync: Debug mode enabled' . ($indefinite ? ' (indefinite)' : ' (24 hours)'));
200 }
201
202 return $result;
203 }
204
205 /**
206 * Disable debug mode
207 *
208 * @param string $reason Reason for disabling
209 * @return bool Success status
210 */
211 public function disable_debug_mode($reason = 'manual')
212 {
213 $settings = $this->get_settings();
214 $settings['enabled'] = false;
215 $settings['disabled_at'] = current_time('timestamp');
216 $settings['disabled_reason'] = $reason;
217
218 $result = update_option(self::OPTION_KEY, $settings);
219
220 if ($result) {
221 // Disable WP_DEBUG constants via ConfigController
222 $this->update_wp_debug_constants(false);
223
224 // Add admin notice
225 $this->add_notice(
226 'Debug mode has been disabled (' . $reason . ').',
227 'info'
228 );
229
230 // Log the action
231 error_log('MetaSync: Debug mode disabled - ' . $reason);
232 }
233
234 return $result;
235 }
236
237 /**
238 * Extend debug mode for another 24 hours
239 *
240 * @return bool Success status
241 */
242 public function extend_debug_mode()
243 {
244 $settings = $this->get_settings();
245
246 if (!$settings['enabled']) {
247 return false;
248 }
249
250 $settings['enabled_at'] = current_time('timestamp');
251 $settings['extended_count'] = ($settings['extended_count'] ?? 0) + 1;
252
253 $result = update_option(self::OPTION_KEY, $settings);
254
255 if ($result) {
256 $this->add_notice(
257 'Debug mode extended for another 24 hours.',
258 'success'
259 );
260 }
261
262 return $result;
263 }
264
265 /**
266 * Toggle indefinite mode
267 *
268 * @param bool $enable Whether to enable indefinite mode
269 * @return bool Success status
270 */
271 public function toggle_indefinite_mode($enable)
272 {
273 $settings = $this->get_settings();
274 $settings['indefinite'] = $enable;
275
276 return update_option(self::OPTION_KEY, $settings);
277 }
278
279 /**
280 * Check debug limits (called by cron)
281 *
282 * @return void
283 */
284 public function check_debug_limits()
285 {
286 $this->check_time_limit();
287 $this->check_file_size_limit();
288 }
289
290 /**
291 * Check if debug mode has exceeded time limit
292 *
293 * @return void
294 */
295 private function check_time_limit()
296 {
297 $settings = $this->get_settings();
298
299 // Skip if debug mode is disabled or in indefinite mode
300 if (!$settings['enabled'] || $settings['indefinite']) {
301 return;
302 }
303
304 $enabled_at = $settings['enabled_at'];
305 $current_time = current_time('timestamp');
306 $elapsed_time = $current_time - $enabled_at;
307
308 // Check if 24 hours have passed
309 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 );
315 }
316 }
317
318 /**
319 * Check if debug log file has exceeded size limit
320 *
321 * @return void
322 */
323 private function check_file_size_limit()
324 {
325 if (!file_exists($this->log_file_path)) {
326 return;
327 }
328
329 $file_size = filesize($this->log_file_path);
330
331 if ($file_size >= self::MAX_LOG_SIZE) {
332 $this->rotate_log_file();
333 $this->add_notice(
334 sprintf('Debug log rotated due to size limit (%s).', $this->format_bytes(self::MAX_LOG_SIZE)),
335 'info'
336 );
337 }
338 }
339
340 /**
341 * Rotate debug log file
342 *
343 * @return bool Success status
344 */
345 private function rotate_log_file()
346 {
347 if (!file_exists($this->log_file_path)) {
348 return false;
349 }
350
351 $backup_path = $this->log_file_path . '.old';
352
353 // Remove existing .old file if it exists
354 if (file_exists($backup_path)) {
355 @unlink($backup_path);
356 }
357
358 // Rename current log to .old
359 $result = @rename($this->log_file_path, $backup_path);
360
361 if ($result) {
362 // Create new empty log file
363 @file_put_contents($this->log_file_path, '');
364 error_log('MetaSync: Debug log rotated - exceeded 10MB limit');
365 }
366
367 // Cleanup old rotations (keep only MAX_ROTATED_LOGS)
368 $this->cleanup_old_rotations();
369
370 return $result;
371 }
372
373 /**
374 * Cleanup old log rotations
375 *
376 * @return void
377 */
378 private function cleanup_old_rotations()
379 {
380 $log_dir = dirname($this->log_file_path);
381 $log_basename = basename($this->log_file_path);
382 $pattern = $log_dir . '/' . $log_basename . '.old*';
383
384 $old_logs = glob($pattern);
385
386 if (count($old_logs) > self::MAX_ROTATED_LOGS) {
387 // Sort by modification time (oldest first)
388 usort($old_logs, function ($a, $b) {
389 return filemtime($a) - filemtime($b);
390 });
391
392 // Delete oldest files, keep only MAX_ROTATED_LOGS
393 $to_delete = array_slice($old_logs, 0, count($old_logs) - self::MAX_ROTATED_LOGS);
394 foreach ($to_delete as $old_log) {
395 @unlink($old_log);
396 }
397 }
398 }
399
400 /**
401 * Check whether debug mode is currently enabled
402 *
403 * Static and side-effect free: reads the option directly rather than going
404 * through get_instance(), which registers hooks as part of construction.
405 * Safe to call from front-end request paths, including ones that run before
406 * the singleton is initialized on 'init'.
407 *
408 * @return bool True when debug mode is on
409 */
410 public static function is_enabled()
411 {
412 $settings = get_option(self::OPTION_KEY, array());
413
414 return is_array($settings) && !empty($settings['enabled']);
415 }
416
417 /**
418 * Get current debug mode settings
419 *
420 * @return array Debug mode settings
421 */
422 public function get_settings()
423 {
424 $defaults = array(
425 'enabled' => false,
426 'enabled_at' => 0,
427 'indefinite' => false,
428 'extended_count' => 0,
429 'disabled_at' => 0,
430 'disabled_reason' => ''
431 );
432
433 $settings = get_option(self::OPTION_KEY, $defaults);
434
435 return wp_parse_args($settings, $defaults);
436 }
437
438 /**
439 * Get debug mode status for dashboard widget
440 *
441 * @return array Status information
442 */
443 public function get_status()
444 {
445 $settings = $this->get_settings();
446 $file_size = file_exists($this->log_file_path) ? filesize($this->log_file_path) : 0;
447
448 $status = array(
449 'enabled' => $settings['enabled'],
450 'indefinite' => $settings['indefinite'],
451 'enabled_at' => $settings['enabled_at'],
452 'time_remaining' => 0,
453 'time_remaining_formatted' => 'N/A',
454 'log_file_size' => $file_size,
455 'log_file_size_formatted' => $this->format_bytes($file_size),
456 'log_file_path' => $this->log_file_path,
457 'max_log_size' => self::MAX_LOG_SIZE,
458 'max_log_size_formatted' => $this->format_bytes(self::MAX_LOG_SIZE),
459 'percentage_used' => 0
460 );
461
462 if ($settings['enabled'] && !$settings['indefinite']) {
463 $elapsed_time = current_time('timestamp') - $settings['enabled_at'];
464 $time_remaining = max(0, self::DEBUG_DURATION - $elapsed_time);
465 $status['time_remaining'] = $time_remaining;
466 $status['time_remaining_formatted'] = $this->format_time_remaining($time_remaining);
467 } elseif ($settings['enabled'] && $settings['indefinite']) {
468 $status['time_remaining_formatted'] = 'Indefinite';
469 }
470
471 if (self::MAX_LOG_SIZE > 0) {
472 $status['percentage_used'] = min(100, ($file_size / self::MAX_LOG_SIZE) * 100);
473 }
474
475 return $status;
476 }
477
478 /**
479 * Update WP_DEBUG constants via ConfigController
480 *
481 * @param bool $enable Whether to enable or disable
482 * @return void
483 */
484 private function update_wp_debug_constants($enable)
485 {
486 try {
487 update_option('wp_debug_enabled', $enable ? 'true' : 'false');
488 update_option('wp_debug_log_enabled', $enable ? 'true' : 'false');
489
490 $config_controller = new ConfigControllerMetaSync();
491 $config_controller->store();
492 } catch (Exception $e) {
493 error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage());
494 }
495 }
496
497 /**
498 * Add admin notice
499 *
500 * @param string $message Notice message
501 * @param string $type Notice type (success, warning, error, info)
502 * @return void
503 */
504 private function add_notice($message, $type = 'info')
505 {
506 $notices = get_transient(self::NOTICE_TRANSIENT);
507 if (!is_array($notices)) {
508 $notices = array();
509 }
510
511 $notices[] = array(
512 'message' => $message,
513 'type' => $type,
514 'timestamp' => current_time('timestamp')
515 );
516
517 set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS);
518 }
519
520 /**
521 * Display admin notices
522 *
523 * @return void
524 */
525 public function display_admin_notices()
526 {
527 $notices = get_transient(self::NOTICE_TRANSIENT);
528
529 if (!is_array($notices) || empty($notices)) {
530 return;
531 }
532
533 foreach ($notices as $notice) {
534 $class = 'notice notice-' . esc_attr($notice['type']) . ' is-dismissible';
535 printf(
536 '<div class="%1$s"><p><strong>MetaSync Debug Mode:</strong> %2$s</p></div>',
537 $class,
538 esc_html($notice['message'])
539 );
540 }
541
542 // Clear notices after displaying
543 delete_transient(self::NOTICE_TRANSIENT);
544 }
545
546 /**
547 * Register dashboard widget
548 *
549 * @return void
550 */
551 public function register_dashboard_widget()
552 {
553 // Only show to users with manage_options capability
554 if (!current_user_can('manage_options')) {
555 return;
556 }
557
558 $status = $this->get_status();
559
560 // Only show widget if debug mode is enabled
561 if (!$status['enabled']) {
562 return;
563 }
564
565 wp_add_dashboard_widget(
566 'metasync_debug_mode_widget',
567 'MetaSync Debug Mode',
568 array($this, 'render_dashboard_widget')
569 );
570 }
571
572 /**
573 * Render dashboard widget
574 *
575 * @return void
576 */
577 public function render_dashboard_widget()
578 {
579 $status = $this->get_status();
580 ?>
581 <div class="metasync-debug-widget">
582 <div class="debug-status">
583 <span class="status-indicator <?php echo $status['enabled'] ? 'active' : 'inactive'; ?>">
584 <?php echo $status['enabled'] ? '⚠️ Active' : '✓ Inactive'; ?>
585 </span>
586 </div>
587
588 <?php if ($status['enabled']): ?>
589 <div class="debug-info">
590 <p>
591 <strong>Auto-disable in:</strong>
592 <span class="time-remaining"><?php echo esc_html($status['time_remaining_formatted']); ?></span>
593 </p>
594 <p>
595 <strong>Log file size:</strong>
596 <span class="file-size">
597 <?php echo esc_html($status['log_file_size_formatted']); ?> /
598 <?php echo esc_html($status['max_log_size_formatted']); ?>
599 </span>
600 </p>
601 <div class="progress-bar">
602 <div class="progress-fill" style="width: <?php echo esc_attr($status['percentage_used']); ?>%"></div>
603 </div>
604 </div>
605
606 <div class="debug-actions">
607 <?php if (!$status['indefinite']): ?>
608 <button type="button" class="button button-secondary" id="metasync-extend-debug">
609 Extend for 24 Hours
610 </button>
611 <?php endif; ?>
612 <button type="button" class="button button-primary" id="metasync-disable-debug">
613 Disable Now
614 </button>
615 </div>
616 <?php endif; ?>
617 </div>
618
619 <style>
620 .metasync-debug-widget {
621 padding: 10px 0;
622 }
623 .debug-status {
624 margin-bottom: 15px;
625 font-size: 16px;
626 }
627 .status-indicator {
628 display: inline-block;
629 padding: 5px 10px;
630 border-radius: 3px;
631 font-weight: 600;
632 }
633 .status-indicator.active {
634 background: #fff3cd;
635 color: #856404;
636 }
637 .status-indicator.inactive {
638 background: #d4edda;
639 color: #155724;
640 }
641 .debug-info p {
642 margin: 8px 0;
643 }
644 .progress-bar {
645 width: 100%;
646 height: 20px;
647 background: #f0f0f0;
648 border-radius: 3px;
649 overflow: hidden;
650 margin: 10px 0;
651 }
652 .progress-fill {
653 height: 100%;
654 background: linear-gradient(90deg, #46b450 0%, #ffb900 70%, #dc3232 100%);
655 transition: width 0.3s ease;
656 }
657 .debug-actions {
658 margin-top: 15px;
659 display: flex;
660 gap: 10px;
661 }
662 .debug-actions button {
663 flex: 1;
664 }
665 </style>
666 <?php
667 }
668
669 /**
670 * Enqueue admin scripts
671 *
672 * @param string $hook Current admin page hook
673 * @return void
674 */
675 public function enqueue_admin_scripts($hook)
676 {
677 // Only enqueue on dashboard
678 if ($hook !== 'index.php') {
679 return;
680 }
681
682 wp_enqueue_script(
683 'metasync-debug-widget',
684 plugin_dir_url(dirname(__FILE__)) . 'admin/js/debug-widget.js',
685 array('jquery'),
686 METASYNC_VERSION,
687 true
688 );
689
690 wp_localize_script('metasync-debug-widget', 'metasyncDebug', array(
691 'ajaxurl' => admin_url('admin-ajax.php'),
692 'nonce' => wp_create_nonce('metasync_debug_mode'),
693 'restUrl' => rest_url('metasync/v1/debug-mode/'),
694 'restNonce' => wp_create_nonce('wp_rest')
695 ));
696 }
697
698 /**
699 * Register REST API routes
700 *
701 * @return void
702 */
703 public function register_rest_routes()
704 {
705 register_rest_route('metasync/v1', '/debug-mode/status', array(
706 'methods' => 'GET',
707 'callback' => array($this, 'rest_get_status'),
708 'permission_callback' => array($this, 'rest_permission_check')
709 ));
710
711 register_rest_route('metasync/v1', '/debug-mode/enable', array(
712 'methods' => 'POST',
713 'callback' => array($this, 'rest_enable_debug'),
714 'permission_callback' => array($this, 'rest_permission_check')
715 ));
716
717 register_rest_route('metasync/v1', '/debug-mode/disable', array(
718 'methods' => 'POST',
719 'callback' => array($this, 'rest_disable_debug'),
720 'permission_callback' => array($this, 'rest_permission_check')
721 ));
722
723 register_rest_route('metasync/v1', '/debug-mode/extend', array(
724 'methods' => 'POST',
725 'callback' => array($this, 'rest_extend_debug'),
726 'permission_callback' => array($this, 'rest_permission_check')
727 ));
728 }
729
730 /**
731 * REST API permission check
732 *
733 * @return bool
734 */
735 public function rest_permission_check()
736 {
737 return current_user_can('manage_options');
738 }
739
740 /**
741 * REST API: Get debug mode status
742 *
743 * @return WP_REST_Response
744 */
745 public function rest_get_status()
746 {
747 return new WP_REST_Response($this->get_status(), 200);
748 }
749
750 /**
751 * REST API: Enable debug mode
752 *
753 * @param WP_REST_Request $request
754 * @return WP_REST_Response
755 */
756 public function rest_enable_debug($request)
757 {
758 $indefinite = $request->get_param('indefinite') === true;
759 $result = $this->enable_debug_mode($indefinite);
760
761 if ($result) {
762 return new WP_REST_Response(array(
763 'success' => true,
764 'message' => 'Debug mode enabled',
765 'status' => $this->get_status()
766 ), 200);
767 }
768
769 return new WP_REST_Response(array(
770 'success' => false,
771 'message' => 'Failed to enable debug mode'
772 ), 500);
773 }
774
775 /**
776 * REST API: Disable debug mode
777 *
778 * @return WP_REST_Response
779 */
780 public function rest_disable_debug()
781 {
782 $result = $this->disable_debug_mode('manual');
783
784 if ($result) {
785 return new WP_REST_Response(array(
786 'success' => true,
787 'message' => 'Debug mode disabled',
788 'status' => $this->get_status()
789 ), 200);
790 }
791
792 return new WP_REST_Response(array(
793 'success' => false,
794 'message' => 'Failed to disable debug mode'
795 ), 500);
796 }
797
798 /**
799 * REST API: Extend debug mode
800 *
801 * @return WP_REST_Response
802 */
803 public function rest_extend_debug()
804 {
805 $result = $this->extend_debug_mode();
806
807 if ($result) {
808 return new WP_REST_Response(array(
809 'success' => true,
810 'message' => 'Debug mode extended for 24 hours',
811 'status' => $this->get_status()
812 ), 200);
813 }
814
815 return new WP_REST_Response(array(
816 'success' => false,
817 'message' => 'Failed to extend debug mode'
818 ), 500);
819 }
820
821 /**
822 * Format bytes to human-readable format
823 *
824 * @param int $bytes File size in bytes
825 * @return string Formatted size
826 */
827 private function format_bytes($bytes)
828 {
829 if ($bytes == 0) {
830 return '0 B';
831 }
832
833 $units = array('B', 'KB', 'MB', 'GB');
834 $factor = floor((strlen($bytes) - 1) / 3);
835
836 return sprintf('%.2f %s', $bytes / pow(1024, $factor), $units[$factor]);
837 }
838
839 /**
840 * Format time remaining to human-readable format
841 *
842 * @param int $seconds Time in seconds
843 * @return string Formatted time
844 */
845 private function format_time_remaining($seconds)
846 {
847 if ($seconds <= 0) {
848 return 'Expired';
849 }
850
851 $hours = floor($seconds / 3600);
852 $minutes = floor(($seconds % 3600) / 60);
853
854 if ($hours > 0) {
855 return sprintf('%d hours %d minutes', $hours, $minutes);
856 }
857
858 return sprintf('%d minutes', $minutes);
859 }
860
861 /**
862 * Uninstall - Clean up options and cron jobs
863 *
864 * @return void
865 */
866 public static function uninstall()
867 {
868 // Remove scheduled cron
869 wp_clear_scheduled_hook(self::CRON_HOOK);
870
871 // Remove options
872 delete_option(self::OPTION_KEY);
873 delete_transient(self::NOTICE_TRANSIENT);
874 }
875 }
876