log_file = WP_CONTENT_DIR . '/activetrail-debug.log'; $this->logging_enabled = $this->is_logging_enabled(); } /** * Get instance * * @return ActiveTrail_Logger */ public static function get_instance() { if ( self::$instance === null ) { self::$instance = new self(); } return self::$instance; } /** * Check if logging is enabled * * @return bool */ private function is_logging_enabled() { // Check if WP_DEBUG is enabled. if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return false; } // Check if debug mode is enabled in plugin settings. if ( class_exists( 'ActiveTrail_Settings' ) ) { return ActiveTrail_Settings::get( 'debug_mode', false ); } // Fallback to option. return get_option( 'activetrail_debug_mode', 'no' ) === 'yes'; } /** * Log a message * * @param string $message Log message * @param string $level Log level * @param string $context Log context * @param array $data Additional data to log */ public function log( $message, $level = self::LEVEL_INFO, $context = self::CONTEXT_GENERAL, $data = array() ) { // Re-evaluate logging status on each call so runtime changes are respected. $this->logging_enabled = $this->is_logging_enabled(); if ( ! $this->logging_enabled ) { return; } // Normalize message to string to avoid "Array to string conversion" warnings. if ( is_array( $message ) || is_object( $message ) ) { $message = wp_json_encode( $message, JSON_UNESCAPED_UNICODE ); } $timestamp = current_time( 'Y-m-d H:i:s' ); $formatted_message = sprintf( '[%s] [%s] [%s] %s', $timestamp, $level, $context, $message ); // Add data if provided. if ( ! empty( $data ) ) { $formatted_message .= ' | Data: ' . wp_json_encode( $data, JSON_UNESCAPED_UNICODE ); } // Write to WordPress debug log. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Logger class intentionally uses error_log error_log( $formatted_message ); } // Also write to our custom log file. $this->write_to_custom_log( $formatted_message ); } /** * Write to custom log file * * @param string $message Formatted message */ private function write_to_custom_log( $message ) { try { // Ensure log directory exists. $log_dir = dirname( $this->log_file ); if ( ! file_exists( $log_dir ) ) { wp_mkdir_p( $log_dir ); } // Write to log file. file_put_contents( $this->log_file, $message . PHP_EOL, FILE_APPEND | LOCK_EX ); // Rotate log file if it gets too large (> 10MB). if ( file_exists( $this->log_file ) && filesize( $this->log_file ) > 10 * 1024 * 1024 ) { $this->rotate_log_file(); } } catch ( Exception $e ) { // Fallback to error_log if custom logging fails. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Logger class intentionally uses error_log error_log( '[ActiveTrail Logger] Failed to write to custom log: ' . $e->getMessage() ); } } } /** * Rotate log file */ private function rotate_log_file() { $backup_file = $this->log_file . '.old'; // Remove old backup if exists. if ( file_exists( $backup_file ) ) { wp_delete_file( $backup_file ); } // Move current log to backup. global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } if ( $wp_filesystem ) { $wp_filesystem->move( $this->log_file, $backup_file, true ); } else { // Fallback: try to use wp_delete_file and create new file if WP_Filesystem is not available. // Note: This is a fallback only and should rarely be needed. if ( file_exists( $this->log_file ) ) { // Copy content and delete original as fallback. $log_content = file_get_contents( $this->log_file ); if ( $log_content !== false && file_put_contents( $backup_file, $log_content ) !== false ) { wp_delete_file( $this->log_file ); } } } } /** * Log error message * * @param string $message Error message * @param string $context Context * @param array $data Additional data */ public function error( $message, $context = self::CONTEXT_GENERAL, $data = array() ) { $this->log( $message, self::LEVEL_ERROR, $context, $data ); } /** * Log warning message * * @param string $message Warning message * @param string $context Context * @param array $data Additional data */ public function warning( $message, $context = self::CONTEXT_GENERAL, $data = array() ) { $this->log( $message, self::LEVEL_WARNING, $context, $data ); } /** * Log info message * * @param string $message Info message * @param string $context Context * @param array $data Additional data */ public function info( $message, $context = self::CONTEXT_GENERAL, $data = array() ) { $this->log( $message, self::LEVEL_INFO, $context, $data ); } /** * Log debug message * * @param string $message Debug message * @param string $context Context * @param array $data Additional data */ public function debug( $message, $context = self::CONTEXT_GENERAL, $data = array() ) { $this->log( $message, self::LEVEL_DEBUG, $context, $data ); } /** * Log product feed specific message * * @param string $message Message * @param string $level Log level * @param array $data Additional data */ public function product_feed( $message, $level = self::LEVEL_INFO, $data = array() ) { $this->log( $message, $level, self::CONTEXT_PRODUCT_FEED, $data ); } /** * Log API specific message * * @param string $message Message * @param string $level Log level * @param array $data Additional data */ public function api( $message, $level = self::LEVEL_INFO, $data = array() ) { $this->log( $message, $level, self::CONTEXT_API, $data ); } /** * Get log file path * * @return string */ public function get_log_file_path() { return $this->log_file; } /** * Get recent log entries * * @param int $lines Number of lines to retrieve * @return array */ public function get_recent_logs( $lines = 100 ) { if ( ! file_exists( $this->log_file ) ) { return array(); } $log_content = file_get_contents( $this->log_file ); $log_lines = explode( PHP_EOL, $log_content ); // Get last N lines. $recent_lines = array_slice( $log_lines, -$lines ); // Remove empty lines. return array_filter( $recent_lines ); } /** * Clear log file * * @return bool */ public function clear_logs() { if ( file_exists( $this->log_file ) ) { return wp_delete_file( $this->log_file ); } return true; } /** * Get log file size * * @return int Size in bytes */ public function get_log_file_size() { if ( file_exists( $this->log_file ) ) { return filesize( $this->log_file ); } return 0; } /** * Format file size for display * * @param int $bytes Size in bytes * @return string Formatted size */ public function format_file_size( $bytes ) { $units = array( 'B', 'KB', 'MB', 'GB' ); $bytes = max( $bytes, 0 ); $pow = floor( ( $bytes ? log( $bytes ) : 0 ) / log( 1024 ) ); $pow = min( $pow, count( $units ) - 1 ); $bytes /= pow( 1024, $pow ); return round( $bytes, 2 ) . ' ' . $units[ $pow ]; } /** * Log request details for debugging * * @param string $context Context description */ public function log_request_details( $context = 'Request' ) { if ( ! $this->logging_enabled ) { return; } $request_data = array( 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'Unknown', 'uri' => isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : 'Unknown', 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : 'Unknown', 'ip' => $this->get_client_ip(), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Debug logging only, data is sanitized 'get_params' => $_GET, // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Debug logging only, data is sanitized 'post_params' => $this->sanitize_post_data( $_POST ), 'headers' => $this->get_request_headers(), ); $this->debug( $context . ' Details', self::CONTEXT_GENERAL, $request_data ); } /** * Get client IP address * * @return string */ private function get_client_ip() { $ip_keys = array( 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' ); foreach ( $ip_keys as $key ) { if ( array_key_exists( $key, $_SERVER ) === true ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below $server_value = wp_unslash( $_SERVER[ $key ] ); foreach ( explode( ',', $server_value ) as $ip ) { $ip = trim( sanitize_text_field( $ip ) ); if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) { return $ip; } } } } return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'Unknown'; } /** * Sanitize POST data for logging (remove sensitive information) * * @param array $post_data POST data * @return array Sanitized data */ private function sanitize_post_data( $post_data ) { $sensitive_keys = array( 'password', 'api_key', 'token', 'secret', 'key' ); $sanitized = $post_data; foreach ( $sensitive_keys as $key ) { if ( isset( $sanitized[ $key ] ) ) { $sanitized[ $key ] = '[REDACTED]'; } } return $sanitized; } /** * Get request headers * * @return array */ private function get_request_headers() { $headers = array(); foreach ( $_SERVER as $key => $value ) { if ( strpos( $key, 'HTTP_' ) === 0 ) { $header = str_replace( 'HTTP_', '', $key ); $header = str_replace( '_', '-', $header ); $headers[ $header ] = sanitize_text_field( wp_unslash( $value ) ); } } return $headers; } } /** * Helper function to get logger instance * * @return ActiveTrail_Logger */ function activetrail_logger() { return ActiveTrail_Logger::get_instance(); }