[\w \-+:\/]{6,50}?)\]\ )? (?P (?:(?:(?PPHP|WordPress\ database|REST\ API|WP_Error)\ )?(?P[\w+ ]{3,40}?):?\ )? .+)$ /x'; if ( preg_match( $pattern, $line, $matches ) ) { $message = $matches['message']; if ( ! empty( $matches['timestamp'] ) ) { // Attempt to parse the timestamp, if any. Timestamp format can vary by server. $parsed_timestamp = strtotime( $matches['timestamp'] ); if ( ! empty( $parsed_timestamp ) ) { $timestamp = $parsed_timestamp; if ( $timestamp > self::$last_timestamp ) { self::$last_timestamp = $timestamp; } if ( (int) self::get_last_parsed_timestamp() < $timestamp ) { ++self::$newer_lines; } } } if ( ! empty( $matches['severity'] ) && ! empty( $matches['source'] ) ) { // Parse the severity level. $level = strtolower( trim( $matches['severity'] ) ); } if ( ! empty( $matches['source'] ) ) { // Parse the severity level. $source = strtolower( trim( $matches['source'] ) ); } /* Attempt to extract the file name and line number from the message. * * spprintf(&log_buffer, 0, "PHP %s: %s in %s on line %" PRIu32, error_type_str, buffer, error_filename, error_lineno); php_log_err_with_severity(log_buffer, syslog_type_int); zend_error_va(severity, (file && ZSTR_LEN(file) > 0) ? ZSTR_VAL(file) : NULL, line, "Uncaught %s\n thrown", ZSTR_VAL(str)); */ if ( preg_match( '/(?:in\s+([^\s]+(?:[\\\\\/][^\s]+)*)\s+on\s+line\s+(\d+)|([^\s]+(?:[\\\\\/][^\s:]+)*)[:](\d+))/', $message, $matches ) ) { $error_file = $matches[1] ? $matches[1] : $matches[3]; /** * If file is outside web root or somewhere where restrictions are in place, that will trigger warnings in PHP, so lets suppress it. */ if ( @\is_file( $error_file ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged // If the file exists, we can use it. $error_line = $matches[2] ? $matches[2] : $matches[4]; } else { $error_file = null; } } // Does this line contain contextual data for another error? $context_prefix = '[ELM_context_'; $trimmed_message = trim( $message ); if ( substr( $trimmed_message, 0, strlen( $context_prefix ) ) === $context_prefix ) { $context = self::parse_context_line( $trimmed_message ); } } return array( 'message' => $message, 'timestamp' => $timestamp, 'severity' => $level, 'source' => $source, 'isContext' => ( null !== $context ), 'contextPayload' => $context, 'error_line' => $error_line, 'error_file' => $error_file, ); } /** * Parses the context line from the log message. * * @param string $message - The log message. * * @return array|null - The parsed context or null if not found. * * @since 1.1.0 */ private static function parse_context_line( $message ) { if ( ! preg_match( '@^\[(ELM_context_\d{1,8}?)\]@', $message, $matches ) ) { return null; } $end_tag = '[/' . $matches[1] . ']'; $end_tag_position = strrpos( $message, $end_tag ); if ( false === $end_tag_position ) { return null; } $serialized_context = substr( $message, strlen( $matches[0] ), $end_tag_position - strlen( $matches[0] ) ); $context = @json_decode( $serialized_context, true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( ! is_array( $context ) ) { return null; } if ( ! isset( $context['parentEntryPosition'] ) ) { $context['parentEntryPosition'] = 'next'; } return $context; } /** * Parses a line from the PHP error log with stack trace. * * @param string $message - The line to parse. * @param bool $is_last_line - Whether this is the last line of a stack trace. * * @return array An associative array containing the parsed data. * * @since 1.1.0 */ public static function parse_php_error_log_stack_line( $message, $is_last_line = false ) { // It's usually "#123 C:\path\to\plugin.php(456): functionCallHere()". // The last line of a very long entry can be truncated. if ( preg_match( '@(?: \#(?P\d+)\s+ # Stack frame index line starting with (?: (?P \[internal\sfunction\] | (?P (?:phar://)? # PHAR archive prefix (optional) (?:[a-zA-Z]:)? # Drive letter (optional) [^:?*<>{}]+? # File path ) \((?P\d{1,6})?\) # Line number (optional) ) : | (?P
{main})\s*?$ ) | ((?P Previous\s+[^:]+:\s+ # “Previous Error:” or similar prefix .*? # The error message (non-greedy) \s+in\s+) (?P (?:phar://)? # Optional PHAR prefix (?:[a-zA-Z]:)? # Optional drive letter [^:()]+ # File path ) \( (?P\d{1,6}) # Line number \) ) )@x', $message, $matches ) ) { $item = array(); if ( ! empty( $matches['source'] ) && ! empty( $matches[0] ) ) { $item['call'] = ltrim( substr( $message, strlen( $matches[0] ) ) ); } elseif ( ! empty( $matches['main'] ) ) { $item['call'] = $matches['main']; } if ( ! empty( $matches['file'] ) ) { $item['file'] = $matches['file']; } elseif ( ! empty( $matches['source'] ) ) { $item['file'] = $matches['source']; } if ( ! empty( $matches['line'] ) ) { $item['line'] = $matches['line']; } elseif ( empty( $matches['main'] ) ) { // Line is missing from log (unknown reason). $item['line'] = 'Line Unknown'; } if ( ! empty( $matches['previous'] ) ) { $item['call'] = $matches['previous']; } if ( ! empty( $matches['prev_line'] ) ) { $item['line'] = $matches['prev_line']; } if ( ! empty( $matches['prev_file'] ) ) { $item['file'] = $matches['prev_file']; } return $item; } elseif ( // Simplified parsing for truncated stack trace entries. $is_last_line && preg_match( '@^\#(?P\d++)\s@', $message, $matches ) && preg_match( '@\son\sline\s\d++$@', $message ) ) { $item = array( 'call' => trim( substr( $message, strlen( $matches[0] ) ) ) ); return $item; } elseif ( false === \str_starts_with( $message, '[' ) ) { // Some modules are writing in error log with new lines - this logic tries to cover that case. $item = array( 'call' => trim( $message ) . "\n" ); return $item; } else { return self::parse_php_error_log_line( (string) $message ); } } /** * Parses a line from the PHP error log with stack trace. * * @param string $line - The line to parse. * * @return bool|null * * @since 1.1.0 */ public static function parse_entry_with_stack_trace( string $line ) { if ( false !== \strpos( $line, 'throw' ) || false !== \strpos( $line, 'Stack trace' ) ) { return \null; } return true; } /** * Stores the last known Timestamp as transient * * @return void * * @since 1.1.0 */ public static function store_last_parsed_timestamp() { if ( null !== self::$last_timestamp ) { $previous_timestamp = self::get_last_parsed_timestamp(); if ( false === $previous_timestamp || (int) self::$last_timestamp > (int) $previous_timestamp ) { self::$last_parsed_timestamp = (int) self::$last_timestamp; \set_transient( self::TIMESTAMP_TRANSIENT, (int) self::$last_timestamp, 0 ); } // Only propagate new line counts when there was a valid previous timestamp. // When no previous timestamp exists (first run or cleared), just establish the baseline. if ( false !== $previous_timestamp ) { self::get_lines_to_show_interface(); } else { // Consume without storing — first-time baseline. self::get_newer_lines(); } } } /** * Returns the last known Timestamp transient. * * @return bool|string * * @since 1.1.0 */ public static function get_last_parsed_timestamp() { if ( null === self::$last_parsed_timestamp ) { self::$last_parsed_timestamp = \get_transient( self::TIMESTAMP_TRANSIENT ); if ( false === self::$last_parsed_timestamp ) { return self::$last_parsed_timestamp; } } return self::$last_parsed_timestamp; } /** * Clears the variable and deletes the transient. * * @return void * * @since 1.1.0 */ public static function delete_last_parsed_timestamp() { self::$last_parsed_timestamp = null; \delete_transient( self::TIMESTAMP_TRANSIENT ); \delete_transient( self::LINES_TRANSIENT ); } /** * Returns newer errors from last log parsing. * * @return int * * @since 1.1.0 */ public static function get_newer_lines(): int { $lines = (int) self::$newer_lines; self::$newer_lines = 0; return $lines; } /** * Checks newer lines stored in the class var, if negative - returns the lines parsed from the log file, last time the log was parsed. If empty - extracts the data from transient, otherwise stores the newly lines in the transient and returns them * * @return int * * @since 1.7.5 */ public static function get_lines_to_show_interface(): int { $lines = self::get_newer_lines(); if ( 0 >= $lines ) { $lines = (int) \get_transient( self::LINES_TRANSIENT ); } else { // Accumulate with any existing count. $existing = (int) \get_transient( self::LINES_TRANSIENT ); $lines = $existing + $lines; \set_transient( self::LINES_TRANSIENT, $lines, 0 ); } return $lines; } } }