log_dir = $upload['basedir'] . '/ai-schema-gen-logs'; $this->log_file = $this->log_dir . '/ai-schema-gen.log'; $this->ensure_log_directory(); } /** * Log a message at a given severity level. * * @param string $message The log message. * @param string $level One of: debug, info, warning, error. Default 'info'. * @param array $context Optional associative array of contextual data. */ public function log( $message, $level = 'info', $context = array() ) { $level = strtolower( $level ); if ( ! in_array( $level, self::LEVELS, true ) ) { $level = 'info'; } $formatted = $this->format( $message, $level, $context ); // Write to WordPress debug.log via error_log. error_log( '[Schema AI] ' . $formatted ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log // Write to custom log file. $this->write( $formatted ); } /** * Log an error-level message. * * @param string $message The log message. * @param array $context Optional contextual data. */ public function error( $message, $context = array() ) { $this->log( $message, 'error', $context ); } /** * Log a warning-level message. * * @param string $message The log message. * @param array $context Optional contextual data. */ public function warning( $message, $context = array() ) { $this->log( $message, 'warning', $context ); } /** * Log an info-level message. * * @param string $message The log message. * @param array $context Optional contextual data. */ public function info( $message, $context = array() ) { $this->log( $message, 'info', $context ); } /** * Log a debug-level message. * * @param string $message The log message. * @param array $context Optional contextual data. */ public function debug( $message, $context = array() ) { $this->log( $message, 'debug', $context ); } /** * Retrieve log entries, optionally filtered by level. * * Reads the log file from the end so the most recent entries come first. * * @param string|null $level Filter by level (debug, info, warning, error) or null for all. * @param int $limit Maximum number of entries to return. Default 100. * @return array List of log line strings. */ public function get_logs( $level = null, $limit = 100 ) { if ( ! file_exists( $this->log_file ) ) { return array(); } $lines = file( $this->log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); if ( false === $lines ) { return array(); } // Most recent first. $lines = array_reverse( $lines ); // Filter by level when requested. if ( null !== $level ) { $level = strtoupper( $level ); $lines = array_filter( $lines, function ( $line ) use ( $level ) { return false !== strpos( $line, '[' . $level . ']' ); } ); // Re-index after filtering. $lines = array_values( $lines ); } return array_slice( $lines, 0, (int) $limit ); } /** * Format a log entry. * * Format: [YYYY-MM-DD HH:mm:ss] [LEVEL] Message | Context JSON * * @param string $message The message. * @param string $level The severity level. * @param array $context Contextual data. * @return string Formatted log line. */ private function format( $message, $level, $context ) { $timestamp = current_time( 'Y-m-d H:i:s' ); $level_tag = strtoupper( $level ); $entry = sprintf( '[%s] [%s] %s', $timestamp, $level_tag, $message ); if ( ! empty( $context ) ) { $entry .= ' | ' . wp_json_encode( $context ); } return $entry; } /** * Write a formatted line to the custom log file. * * Handles auto-rotation when the file exceeds MAX_FILE_SIZE. * * @param string $line The formatted log entry. */ private function write( $line ) { $this->maybe_rotate(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $this->log_file, $line . PHP_EOL, FILE_APPEND | LOCK_EX ); } /** * Rotate the log file if it exceeds 5 MB. * * The current file is renamed with a timestamp suffix and a new empty * file is created automatically on the next write. */ private function maybe_rotate() { if ( ! file_exists( $this->log_file ) ) { return; } $size = filesize( $this->log_file ); if ( false === $size || $size < self::MAX_FILE_SIZE ) { return; } $rotated_name = $this->log_dir . '/ai-schema-gen-' . gmdate( 'Y-m-d-His' ) . '.log'; // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename rename( $this->log_file, $rotated_name ); } /** * Create the log directory if it does not exist and protect it. * * Writes an .htaccess file that denies all HTTP access to the logs. */ private function ensure_log_directory() { if ( ! is_dir( $this->log_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir wp_mkdir_p( $this->log_dir ); } // Add .htaccess protection. $htaccess = $this->log_dir . '/.htaccess'; if ( ! file_exists( $htaccess ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $htaccess, "Order deny,allow\nDeny from all\n" ); } // Add an index.php for extra safety. $index = $this->log_dir . '/index.php'; if ( ! file_exists( $index ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents( $index, "