can( $os->env->is_network() ? 'manage_network_options' : 'manage_options' ) && ! empty( $os->preference( 'developerModeEnabled' ) ); /** * Filter whether the current user can see the Code Blue window. * * @param bool $can Default: Developer mode on AND `manage_options` * (`manage_network_options` on a network). */ return (bool) $os->filter( 'openstation_code_blue_user_can_use', $can ); } /** * The PHP error-label → severity map; the single source of truth for * both the parse regex and the label lookup. * * @return array */ function level_map() { return array( 'fatal error' => 'fatal', 'parse error' => 'fatal', 'core error' => 'fatal', 'compile error' => 'fatal', 'recoverable fatal error' => 'fatal', 'user error' => 'error', 'warning' => 'warning', 'core warning' => 'warning', 'compile warning' => 'warning', 'user warning' => 'warning', 'deprecated' => 'deprecated', 'user deprecated' => 'deprecated', 'notice' => 'notice', 'user notice' => 'notice', ); } /** * Grouping key: numbers and hex addresses collapse so two occurrences * of the same problem land together. * * @param string $level Severity slug. * @param string $message Message without the location suffix. * @param string $file File path, may be ''. * @return string */ function signature( $level, $message, $file = '' ) { $norm = preg_replace( '/0x[0-9a-f]+/i', 'N', (string) $message ); $norm = preg_replace( '/\d+/', 'N', (string) $norm ); $norm = preg_replace( '/\s+/', ' ', trim( (string) $norm ) ); return $level . '|' . substr( (string) $norm, 0, 240 ) . '|' . $file; } /** * Build one entry: strip well-formed HTML (`_doing_it_wrong()` logs * markup; a bare `<` from a parse error must survive), pull the * `in /file on line N` suffix out, derive the signature. * * @param int|null $timestamp Unix seconds or null. * @param string $level Severity slug. * @param string $label Human label, e.g. `PHP Fatal error`. * @param string $message Message text. * @return array */ function make_entry( $timestamp, $level, $label, $message ) { $message = trim( (string) preg_replace( '/<\/?[a-zA-Z][^<>]*>/', '', (string) $message ) ); $file = ''; $line = 0; if ( preg_match( '/^(.*?)\s+in\s+(\S+?)(?::(\d+)|\s+on\s+line\s+(\d+))$/s', $message, $m ) ) { $message = trim( $m[1] ); $file = $m[2]; $line = (int) ( '' !== $m[3] ? $m[3] : $m[4] ); } return array( 'timestamp' => $timestamp, 'level' => $level, 'label' => $label, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => '', 'signature' => signature( $level, $message, $file ), ); } /** * Whose code is this? The first question anyone asks of a log line, * and the last one the log itself answers — the path is right there in * every entry, and reading `wp-content/plugins//` off it turns * "some fatal error" into "WooCommerce's fatal error", which is the * difference between triage and archaeology. * * Deliberately conservative: a path that is not clearly under the * content directory or clearly inside core answers `unknown` rather * than guessing, because a wrong attribution sends someone into the * wrong codebase. Single-file plugins and mu-plugins keep their * basename as the slug; there is no directory to name them by. * * This is the ONLY implementation. The window renders what it returns * and the debugging abilities report it verbatim — a second copy of * this classification would let the two disagree about whose bug it is. * * @param string $file Absolute path from a log entry, may be ''. * @param string $content_dir The install's `wp-content` equivalent. * @return array{kind:string,slug:string} `kind` is one of `plugin`, * `mu-plugin`, `theme`, `core`, `unknown`. */ function origin( $file, $content_dir ) { $path = str_replace( '\\', '/', (string) $file ); $content = rtrim( str_replace( '\\', '/', (string) $content_dir ), '/' ); if ( '' !== $content && 0 === strpos( $path, $content . '/' ) ) { $rest = substr( $path, strlen( $content ) + 1 ); if ( preg_match( '#^(plugins|mu-plugins|themes)/([^/]+)#', $rest, $m ) ) { $kinds = array( 'plugins' => 'plugin', 'mu-plugins' => 'mu-plugin', 'themes' => 'theme', ); return array( 'kind' => $kinds[ $m[1] ], 'slug' => preg_replace( '/\.php$/', '', $m[2] ), ); } } if ( preg_match( '#/wp-(admin|includes)/#', $path ) ) { return array( 'kind' => 'core', 'slug' => '', ); } return array( 'kind' => 'unknown', 'slug' => '', ); } /** * Parse `22-Aug-2026 09:14:02 UTC` (or the same without a zone, * read as UTC). * * @param string $raw Text between the brackets. * @return int|null */ function parse_timestamp( $raw ) { $raw = trim( (string) $raw ); $date = \DateTime::createFromFormat( 'd-M-Y H:i:s T', $raw ); if ( false === $date ) { $date = \DateTime::createFromFormat( 'd-M-Y H:i:s', $raw, new \DateTimeZone( 'UTC' ) ); } if ( false === $date ) { $fallback = strtotime( $raw ); return false === $fallback ? null : $fallback; } return $date->getTimestamp(); } /** * Parse raw log text into entries, oldest first. * * Understands `[stamp] PHP