log_file_path = WP_CONTENT_DIR . '/debug.log'; $this->init_hooks(); } /** * Initialize WordPress hooks * * @return void */ private function init_hooks() { // Register cron schedules add_filter('cron_schedules', array($this, 'register_cron_schedules')); // Schedule cron job on activation add_action('init', array($this, 'maybe_schedule_cron')); // Cron job handler add_action(self::CRON_HOOK, array($this, 'check_debug_limits')); // Admin notices add_action('admin_notices', array($this, 'display_admin_notices')); // Dashboard widget add_action('wp_dashboard_setup', array($this, 'register_dashboard_widget')); // REST API endpoints add_action('rest_api_init', array($this, 'register_rest_routes')); // Enqueue admin scripts add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } /** * Register custom cron schedules * * @param array $schedules Existing schedules * @return array Modified schedules */ public function register_cron_schedules($schedules) { // Ensure we don't override existing schedule if (!isset($schedules['hourly'])) { $schedules['hourly'] = array( 'interval' => 3600, 'display' => __('Once Hourly', 'metasync') ); } return $schedules; } /** * Schedule cron job if not already scheduled * * @return void */ public function maybe_schedule_cron() { if (!wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_event(time(), 'hourly', self::CRON_HOOK); } } /** * Enable debug mode * * @param bool $indefinite Whether to enable indefinitely * @return bool Success status */ public function enable_debug_mode($indefinite = false) { $settings = array( 'enabled' => true, 'enabled_at' => current_time('timestamp'), 'indefinite' => $indefinite, 'extended_count' => 0 ); $result = update_option(self::OPTION_KEY, $settings); if ($result) { // Enable WP_DEBUG constants via ConfigController $this->update_wp_debug_constants(true); // Ensure cron job is scheduled (safety net) if (!wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_event(time(), 'hourly', self::CRON_HOOK); } // Clear any previous notices delete_transient(self::NOTICE_TRANSIENT); // Log the action error_log('MetaSync: Debug mode enabled' . ($indefinite ? ' (indefinite)' : ' (24 hours)')); } return $result; } /** * Disable debug mode * * @param string $reason Reason for disabling * @return bool Success status */ public function disable_debug_mode($reason = 'manual') { $settings = $this->get_settings(); $settings['enabled'] = false; $settings['disabled_at'] = current_time('timestamp'); $settings['disabled_reason'] = $reason; $result = update_option(self::OPTION_KEY, $settings); if ($result) { // Disable WP_DEBUG constants via ConfigController $this->update_wp_debug_constants(false); // Add admin notice $this->add_notice( 'Debug mode has been disabled (' . $reason . ').', 'info' ); // Log the action error_log('MetaSync: Debug mode disabled - ' . $reason); } return $result; } /** * Extend debug mode for another 24 hours * * @return bool Success status */ public function extend_debug_mode() { $settings = $this->get_settings(); if (!$settings['enabled']) { return false; } $settings['enabled_at'] = current_time('timestamp'); $settings['extended_count'] = ($settings['extended_count'] ?? 0) + 1; $result = update_option(self::OPTION_KEY, $settings); if ($result) { $this->add_notice( 'Debug mode extended for another 24 hours.', 'success' ); } return $result; } /** * Toggle indefinite mode * * @param bool $enable Whether to enable indefinite mode * @return bool Success status */ public function toggle_indefinite_mode($enable) { $settings = $this->get_settings(); $settings['indefinite'] = $enable; return update_option(self::OPTION_KEY, $settings); } /** * Check debug limits (called by cron) * * @return void */ public function check_debug_limits() { $this->check_time_limit(); $this->check_file_size_limit(); } /** * Check if debug mode has exceeded time limit * * @return void */ private function check_time_limit() { $settings = $this->get_settings(); // Skip if debug mode is disabled or in indefinite mode if (!$settings['enabled'] || $settings['indefinite']) { return; } $enabled_at = $settings['enabled_at']; $current_time = current_time('timestamp'); $elapsed_time = $current_time - $enabled_at; // Check if 24 hours have passed if ($elapsed_time >= self::DEBUG_DURATION) { $this->disable_debug_mode('auto_expired'); $this->add_notice( 'Debug mode auto-disabled after 24 hours.', 'warning' ); } } /** * Check if debug log file has exceeded size limit * * @return void */ private function check_file_size_limit() { if (!file_exists($this->log_file_path)) { return; } $file_size = filesize($this->log_file_path); if ($file_size >= self::MAX_LOG_SIZE) { $this->rotate_log_file(); $this->add_notice( sprintf('Debug log rotated due to size limit (%s).', $this->format_bytes(self::MAX_LOG_SIZE)), 'info' ); } } /** * Rotate debug log file * * @return bool Success status */ private function rotate_log_file() { if (!file_exists($this->log_file_path)) { return false; } $backup_path = $this->log_file_path . '.old'; // Remove existing .old file if it exists if (file_exists($backup_path)) { @unlink($backup_path); } // Rename current log to .old $result = @rename($this->log_file_path, $backup_path); if ($result) { // Create new empty log file @file_put_contents($this->log_file_path, ''); error_log('MetaSync: Debug log rotated - exceeded 10MB limit'); } // Cleanup old rotations (keep only MAX_ROTATED_LOGS) $this->cleanup_old_rotations(); return $result; } /** * Cleanup old log rotations * * @return void */ private function cleanup_old_rotations() { $log_dir = dirname($this->log_file_path); $log_basename = basename($this->log_file_path); $pattern = $log_dir . '/' . $log_basename . '.old*'; $old_logs = glob($pattern); if (count($old_logs) > self::MAX_ROTATED_LOGS) { // Sort by modification time (oldest first) usort($old_logs, function ($a, $b) { return filemtime($a) - filemtime($b); }); // Delete oldest files, keep only MAX_ROTATED_LOGS $to_delete = array_slice($old_logs, 0, count($old_logs) - self::MAX_ROTATED_LOGS); foreach ($to_delete as $old_log) { @unlink($old_log); } } } /** * Check whether debug mode is currently enabled * * Static and side-effect free: reads the option directly rather than going * through get_instance(), which registers hooks as part of construction. * Safe to call from front-end request paths, including ones that run before * the singleton is initialized on 'init'. * * @return bool True when debug mode is on */ public static function is_enabled() { $settings = get_option(self::OPTION_KEY, array()); return is_array($settings) && !empty($settings['enabled']); } /** * Get current debug mode settings * * @return array Debug mode settings */ public function get_settings() { $defaults = array( 'enabled' => false, 'enabled_at' => 0, 'indefinite' => false, 'extended_count' => 0, 'disabled_at' => 0, 'disabled_reason' => '' ); $settings = get_option(self::OPTION_KEY, $defaults); return wp_parse_args($settings, $defaults); } /** * Get debug mode status for dashboard widget * * @return array Status information */ public function get_status() { $settings = $this->get_settings(); $file_size = file_exists($this->log_file_path) ? filesize($this->log_file_path) : 0; $status = array( 'enabled' => $settings['enabled'], 'indefinite' => $settings['indefinite'], 'enabled_at' => $settings['enabled_at'], 'time_remaining' => 0, 'time_remaining_formatted' => 'N/A', 'log_file_size' => $file_size, 'log_file_size_formatted' => $this->format_bytes($file_size), 'log_file_path' => $this->log_file_path, 'max_log_size' => self::MAX_LOG_SIZE, 'max_log_size_formatted' => $this->format_bytes(self::MAX_LOG_SIZE), 'percentage_used' => 0 ); if ($settings['enabled'] && !$settings['indefinite']) { $elapsed_time = current_time('timestamp') - $settings['enabled_at']; $time_remaining = max(0, self::DEBUG_DURATION - $elapsed_time); $status['time_remaining'] = $time_remaining; $status['time_remaining_formatted'] = $this->format_time_remaining($time_remaining); } elseif ($settings['enabled'] && $settings['indefinite']) { $status['time_remaining_formatted'] = 'Indefinite'; } if (self::MAX_LOG_SIZE > 0) { $status['percentage_used'] = min(100, ($file_size / self::MAX_LOG_SIZE) * 100); } return $status; } /** * Update WP_DEBUG constants via ConfigController * * @param bool $enable Whether to enable or disable * @return void */ private function update_wp_debug_constants($enable) { try { update_option('wp_debug_enabled', $enable ? 'true' : 'false'); update_option('wp_debug_log_enabled', $enable ? 'true' : 'false'); $config_controller = new ConfigControllerMetaSync(); $config_controller->store(); } catch (Exception $e) { error_log('MetaSync: Error updating wp-config.php - ' . $e->getMessage()); } } /** * Add admin notice * * @param string $message Notice message * @param string $type Notice type (success, warning, error, info) * @return void */ private function add_notice($message, $type = 'info') { $notices = get_transient(self::NOTICE_TRANSIENT); if (!is_array($notices)) { $notices = array(); } $notices[] = array( 'message' => $message, 'type' => $type, 'timestamp' => current_time('timestamp') ); set_transient(self::NOTICE_TRANSIENT, $notices, DAY_IN_SECONDS); } /** * Display admin notices * * @return void */ public function display_admin_notices() { $notices = get_transient(self::NOTICE_TRANSIENT); if (!is_array($notices) || empty($notices)) { return; } foreach ($notices as $notice) { $class = 'notice notice-' . esc_attr($notice['type']) . ' is-dismissible'; printf( '
MetaSync Debug Mode: %2$s