plugin_slug = $plugin_slug; $this->log_level = $this->get_log_level_from_settings(); $this->log_dir = $this->initialize_log_directory(); $this->max_file_size = 1024 * 1024; // 1MB per file $this->max_files = 10; // Keep last 10 files $this->enabled = $this->is_logging_enabled(); } /** * Log a debug message. * * @since 1.0.0 * @param string $message Log message. * @param array $context Additional context data. */ public function debug(string $message, array $context = []): void { $this->log(self::LEVEL_DEBUG, $message, $context); } /** * Log an info message. * * @since 1.0.0 * @param string $message Log message. * @param array $context Additional context data. */ public function info(string $message, array $context = []): void { $this->log(self::LEVEL_INFO, $message, $context); } /** * Log a warning message. * * @since 1.0.0 * @param string $message Log message. * @param array $context Additional context data. */ public function warning(string $message, array $context = []): void { $this->log(self::LEVEL_WARNING, $message, $context); } /** * Log an error message. * * @since 1.0.0 * @param string $message Log message. * @param array $context Additional context data. */ public function error(string $message, array $context = []): void { $this->log(self::LEVEL_ERROR, $message, $context); } /** * Log a message with specified level. * * @since 1.0.0 * @param string $level Log level. * @param string $message Log message. * @param array $context Additional context data. */ public function log(string $level, string $message, array $context = []): void { if (!$this->enabled || !$this->should_log_level($level)) { return; } $log_entry = $this->format_log_entry($level, $message, $context); // Write to custom log file with rotation $this->write_to_log_file($log_entry); // Also write to WordPress debug.log if enabled if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { error_log($log_entry); } } /** * Log rule creation event. * * @since 1.0.0 * @param string $rule_id Rule identifier. * @param string $xpath XPath expression. * @param string $description Rule description. */ public function log_rule_created(string $rule_id, string $xpath, string $description = ''): void { $this->info('Rule created', [ 'rule_id' => $rule_id, 'xpath' => $xpath, 'description' => $description, 'user_id' => get_current_user_id(), 'user_login' => wp_get_current_user()->user_login ?? 'unknown', ]); } /** * Log rule update event. * * @since 1.0.0 * @param string $rule_id Rule identifier. * @param array $changes Changes made to the rule. */ public function log_rule_updated(string $rule_id, array $changes): void { $this->info('Rule updated', [ 'rule_id' => $rule_id, 'changes' => $changes, 'user_id' => get_current_user_id(), 'user_login' => wp_get_current_user()->user_login ?? 'unknown', ]); } /** * Log rule deletion event. * * @since 1.0.0 * @param string $rule_id Rule identifier. */ public function log_rule_deleted(string $rule_id): void { $this->info('Rule deleted', [ 'rule_id' => $rule_id, 'user_id' => get_current_user_id(), 'user_login' => wp_get_current_user()->user_login ?? 'unknown', ]); } /** * Log XPath processing event. * * @since 1.0.0 * @param string $xpath XPath expression. * @param int $matches_count Number of elements matched. * @param string $page_url Current page URL. */ public function log_xpath_processed(string $xpath, int $matches_count, string $page_url = ''): void { $this->debug('XPath processed', [ 'xpath' => $xpath, 'matches_count' => $matches_count, 'page_url' => $page_url ?: ($_SERVER['REQUEST_URI'] ?? 'unknown'), 'user_id' => get_current_user_id(), ]); } /** * Log XPath error. * * @since 1.0.0 * @param string $xpath XPath expression. * @param string $error Error message. */ public function log_xpath_error(string $xpath, string $error): void { $this->error('XPath error', [ 'xpath' => $xpath, 'error' => $error, 'user_id' => get_current_user_id(), ]); } /** * Format log entry with timestamp and context. * * @since 1.0.0 * @param string $level Log level. * @param string $message Log message. * @param array $context Additional context data. * @return string Formatted log entry. */ private function format_log_entry(string $level, string $message, array $context): string { $timestamp = current_time('Y-m-d H:i:s'); $context_str = empty($context) ? '' : ' ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE); return sprintf( '[%s] %s [%s] %s%s', $timestamp, strtoupper($level), $this->plugin_slug, $message, $context_str ); } /** * Get log level from settings. * * @since 1.0.0 * @return string Log level. */ private function get_log_level_from_settings(): string { if (!function_exists('get_option')) { return self::LEVEL_ERROR; } return get_option('adsd_log_level', self::LEVEL_ERROR); } /** * Initialize log directory path. * * @since 1.0.0 * @return string Log directory path. */ private function initialize_log_directory(): string { if (!function_exists('wp_upload_dir')) { return sys_get_temp_dir(); } $upload_dir = wp_upload_dir(); $log_dir = $upload_dir['basedir'] . '/ads-destroyer-logs'; // Create directory if it doesn't exist if (!file_exists($log_dir)) { if (function_exists('wp_mkdir_p')) { wp_mkdir_p($log_dir); } else { mkdir($log_dir, 0755, true); } } return $log_dir; } /** * Check if logging is enabled. * * @since 1.0.0 * @return bool True if logging is enabled, false otherwise. */ private function is_logging_enabled(): bool { if (!function_exists('get_option')) { return false; } return (bool) get_option('adsd_logging_enabled', false); } /** * Check if log level should be logged. * * @since 1.0.0 * @param string $level Log level to check. * @return bool True if should log, false otherwise. */ private function should_log_level(string $level): bool { if (!function_exists('get_option')) { return true; // Default to logging if WordPress not available } $enabled_levels = get_option('adsd_log_levels', ['error']); return in_array($level, $enabled_levels, true); } /** * Write log entry to file with rotation. * * @since 1.0.0 * @param string $log_entry Formatted log entry. */ private function write_to_log_file(string $log_entry): void { $log_file = $this->log_dir . '/ads-destroyer-' . date('Y-m-d') . '.log'; // Check if file exists and rotate if needed if (file_exists($log_file) && filesize($log_file) > $this->max_file_size) { $this->rotate_log_file($log_file); } // Write to file file_put_contents($log_file, $log_entry . PHP_EOL, FILE_APPEND | LOCK_EX); // Clean old files $this->cleanup_old_logs(); } /** * Rotate log file when it gets too large. * * @since 1.0.0 * @param string $log_file Current log file path. */ private function rotate_log_file(string $log_file): void { $timestamp = date('Y-m-d_H-i-s'); $rotated_file = $this->log_dir . '/ads-destroyer-' . $timestamp . '.log'; rename($log_file, $rotated_file); } /** * Clean up old log files. * * @since 1.0.0 */ private function cleanup_old_logs(): void { $files = glob($this->log_dir . '/ads-destroyer-*.log'); if (count($files) <= $this->max_files) { return; } // Sort by modification time (newest first) usort($files, function($a, $b) { return filemtime($b) - filemtime($a); }); // Remove oldest files $files_to_remove = array_slice($files, $this->max_files); foreach ($files_to_remove as $file) { if (file_exists($file)) { unlink($file); } } } /** * Enable logging. * * @since 1.0.0 */ public function enable(): void { $this->enabled = true; if (function_exists('update_option')) { update_option('adsd_logging_enabled', true); } } /** * Disable logging. * * @since 1.0.0 */ public function disable(): void { $this->enabled = false; if (function_exists('update_option')) { update_option('adsd_logging_enabled', false); } } /** * Set log level. * * @since 1.0.0 * @param string $level Log level. */ public function set_log_levels(array $levels): void { if (function_exists('update_option')) { update_option('adsd_log_levels', $levels); } } /** * Get current log level. * * @since 1.0.0 * @return string Current log level. */ public function get_log_level(): string { return $this->log_level; } /** * Check if logging is currently enabled. * * @since 1.0.0 * @return bool True if logging is enabled, false otherwise. */ public function is_enabled(): bool { return $this->enabled; } /** * Get log directory path. * * @since 1.0.0 * @return string Log directory path. */ public function get_log_directory(): string { return $this->log_dir; } /** * Get list of log files. * * @since 1.0.0 * @return array Array of log file paths. */ public function get_log_files(): array { $files = glob($this->log_dir . '/ads-destroyer-*.log'); // Sort by modification time (newest first) usort($files, function($a, $b) { return filemtime($b) - filemtime($a); }); return $files; } /** * Get latest log file. * * @since 1.0.0 * @return string|null Latest log file path or null if no files exist. */ public function get_latest_log_file(): ?string { $files = $this->get_log_files(); return $files[0] ?? null; } /** * Read log file content. * * @since 1.0.0 * @param string $file_path Log file path. * @param int $lines Number of lines to read from the end. * @return string Log file content. */ public function read_log_file(string $file_path, int $lines = 1000): string { if (!file_exists($file_path)) { return ''; } $content = file_get_contents($file_path); if ($lines > 0) { $log_lines = explode("\n", $content); $content = implode("\n", array_slice($log_lines, -$lines)); } return $content; } /** * Delete log file. * * @since 1.0.0 * @param string $file_path Log file path. * @return bool True if deleted, false otherwise. */ public function delete_log_file(string $file_path): bool { if (file_exists($file_path) && strpos($file_path, $this->log_dir) === 0) { return unlink($file_path); } return false; } /** * Clear all log files. * * @since 1.0.0 * @return int Number of files deleted. */ public function clear_all_logs(): int { $files = $this->get_log_files(); $deleted = 0; foreach ($files as $file) { if ($this->delete_log_file($file)) { $deleted++; } } return $deleted; } }