| 1 |
<?php |
| 2 |
/** |
| 3 |
* BrowserConsole handler for Monolog |
| 4 |
* |
| 5 |
* Handles all features of BrowserConsole handler for Monolog. |
| 6 |
* |
| 7 |
* @package Handlers |
| 8 |
* @author Pierre Lannoy <https://pierre.lannoy.fr/>. |
| 9 |
* @author Olivier Poitrey <rs@dailymotion.com>. |
| 10 |
* @since 3.1.0 |
| 11 |
*/ |
| 12 |
|
| 13 |
namespace Decalog\Handler; |
| 14 |
|
| 15 |
use DLMonolog\Formatter\LineFormatter; |
| 16 |
use DLMonolog\Formatter\FormatterInterface; |
| 17 |
use DLMonolog\Utils; |
| 18 |
use DLMonolog\Handler\AbstractProcessingHandler; |
| 19 |
use Decalog\Plugin\Feature\EventTypes; |
| 20 |
|
| 21 |
/** |
| 22 |
* Define the Monolog BrowserConsole handler. |
| 23 |
* |
| 24 |
* Handles all features of BrowserConsole handler for Monolog. |
| 25 |
* |
| 26 |
* @package Handlers |
| 27 |
* @author Pierre Lannoy <https://pierre.lannoy.fr/>. |
| 28 |
* @author Olivier Poitrey <rs@dailymotion.com>. |
| 29 |
* @since 3.1.0 |
| 30 |
*/ |
| 31 |
class BrowserConsoleHandler extends AbstractProcessingHandler { |
| 32 |
|
| 33 |
protected static $initialized = false; |
| 34 |
protected static $records = []; |
| 35 |
|
| 36 |
/** |
| 37 |
* {@inheritDoc} |
| 38 |
* |
| 39 |
* Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format. |
| 40 |
* |
| 41 |
* Example of formatted string: |
| 42 |
* |
| 43 |
* You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white} |
| 44 |
*/ |
| 45 |
protected function getDefaultFormatter(): FormatterInterface { |
| 46 |
return new LineFormatter( '[[%level_name%]]{font-weight: bold} %message%' ); |
| 47 |
} |
| 48 |
|
| 49 |
/** |
| 50 |
* {@inheritDoc} |
| 51 |
*/ |
| 52 |
protected function write( array $record ): void { |
| 53 |
static::$records[] = $record; |
| 54 |
if ( ! static::$initialized ) { |
| 55 |
static::$initialized = true; |
| 56 |
$this->registerShutdownFunction(); |
| 57 |
} |
| 58 |
} |
| 59 |
|
| 60 |
/** |
| 61 |
* Convert records to javascript console commands and send it to the browser. |
| 62 |
* This method is automatically called on PHP shutdown if output is HTML or Javascript. |
| 63 |
*/ |
| 64 |
public static function send(): void { |
| 65 |
$format = static::getResponseFormat(); |
| 66 |
if ( $format === 'unknown' ) { |
| 67 |
return; |
| 68 |
} |
| 69 |
if ( count( static::$records ) ) { |
| 70 |
if ( $format === 'html' ) { |
| 71 |
static::writeOutput( '<script>' . static::generateScript() . '</script>' ); |
| 72 |
} elseif ( $format === 'js' ) { |
| 73 |
static::writeOutput( static::generateScript() ); |
| 74 |
} |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* Wrapper for register_shutdown_function to allow overriding |
| 80 |
*/ |
| 81 |
protected function registerShutdownFunction(): void { |
| 82 |
if ( PHP_SAPI !== 'cli' ) { |
| 83 |
register_shutdown_function( [ 'Decalog\Handler\BrowserConsoleHandler', 'send' ] ); |
| 84 |
} |
| 85 |
} |
| 86 |
|
| 87 |
/** |
| 88 |
* Wrapper for echo to allow overriding |
| 89 |
*/ |
| 90 |
protected static function writeOutput( string $str ): void { |
| 91 |
echo $str; |
| 92 |
} |
| 93 |
|
| 94 |
/** |
| 95 |
* Checks the format of the response |
| 96 |
* |
| 97 |
* If Content-Type is set to application/javascript or text/javascript -> js |
| 98 |
* If Content-Type is set to text/html, or is unset -> html |
| 99 |
* If Content-Type is anything else -> unknown |
| 100 |
* |
| 101 |
* @return string One of 'js', 'html' or 'unknown' |
| 102 |
*/ |
| 103 |
protected static function getResponseFormat(): string { |
| 104 |
foreach ( headers_list() as $header ) { |
| 105 |
if ( stripos( $header, 'content-type:' ) === 0 ) { |
| 106 |
if ( stripos( $header, 'application/javascript' ) !== false || stripos( $header, 'text/javascript' ) !== false ) { |
| 107 |
return 'js'; |
| 108 |
} |
| 109 |
if ( stripos( $header, 'text/html' ) === false ) { |
| 110 |
return 'unknown'; |
| 111 |
} |
| 112 |
break; |
| 113 |
} |
| 114 |
} |
| 115 |
return 'html'; |
| 116 |
} |
| 117 |
|
| 118 |
private static function generateScript(): string { |
| 119 |
$script = []; |
| 120 |
foreach ( static::$records as $record ) { |
| 121 |
$context = static::dump( 'Context', $record['context'] ); |
| 122 |
$extra = static::dump( 'Extra', $record['extra'] ); |
| 123 |
if ( empty( $context ) && empty( $extra ) ) { |
| 124 |
$script[] = static::call_array( 'log', static::handleStyles( $record['formatted'] ) ); |
| 125 |
} else { |
| 126 |
$script = array_merge( |
| 127 |
$script, |
| 128 |
[ static::call_array( 'groupCollapsed', static::handleStyles( $record['formatted'] ) ) ], |
| 129 |
$context, |
| 130 |
$extra, |
| 131 |
[ static::call( 'groupEnd' ) ] |
| 132 |
); |
| 133 |
} |
| 134 |
} |
| 135 |
return "(function (c) {if (c && c.groupCollapsed) {\n" . implode( "\n", $script ) . "\n}})(console);"; |
| 136 |
} |
| 137 |
|
| 138 |
private static function handleStyles( string $formatted ): array { |
| 139 |
$args = []; |
| 140 |
$format = '%c' . $formatted; |
| 141 |
preg_match_all( '/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER ); |
| 142 |
foreach ( array_reverse( $matches ) as $match ) { |
| 143 |
$args[] = '"font-weight: normal"'; |
| 144 |
$args[] = static::quote( static::handleCustomStyles( $formatted ) ); |
| 145 |
$pos = $match[0][1]; |
| 146 |
$format = Utils::substr( $format, 0, $pos ) . '%c' . $match[1][0] . '%c' . Utils::substr( $format, $pos + strlen( $match[0][0] ) ); |
| 147 |
} |
| 148 |
$args[] = static::quote( 'font-weight: normal' ); |
| 149 |
$args[] = static::quote( $format ); |
| 150 |
return array_reverse( $args ); |
| 151 |
} |
| 152 |
|
| 153 |
private static function handleCustomStyles( string $style ): string { |
| 154 |
return preg_replace_callback( |
| 155 |
'/^\[\[(.*)\]\]/', |
| 156 |
function ( array $m ) { |
| 157 |
$level = strtolower( $m[1] ); |
| 158 |
if ( ! array_key_exists( $level, EventTypes::$levels_colors ) ) { |
| 159 |
$level = 'unknown'; |
| 160 |
} |
| 161 |
return 'background-color:' . EventTypes::$levels_colors[ $level ][1] . ';color:' . EventTypes::$levels_colors[ $level ][0] . ';border-radius: 3px;padding:1px 6px;'; |
| 162 |
}, |
| 163 |
$style |
| 164 |
); |
| 165 |
} |
| 166 |
|
| 167 |
private static function dump( string $title, array $dict ): array { |
| 168 |
$script = []; |
| 169 |
$dict = array_filter( $dict ); |
| 170 |
if ( empty( $dict ) ) { |
| 171 |
return $script; |
| 172 |
} |
| 173 |
$script[] = static::call( 'log', static::quote( '%c%s' ), static::quote( 'font-weight: bold' ), static::quote( $title ) ); |
| 174 |
foreach ( $dict as $key => $value ) { |
| 175 |
$value = wp_json_encode( $value ); |
| 176 |
if ( empty( $value ) ) { |
| 177 |
$value = static::quote( '' ); |
| 178 |
} |
| 179 |
$script[] = static::call( 'log', static::quote( '%s: %o' ), static::quote( (string) $key ), $value ); |
| 180 |
} |
| 181 |
|
| 182 |
return $script; |
| 183 |
} |
| 184 |
|
| 185 |
private static function quote( string $arg ): string { |
| 186 |
return '"' . addcslashes( $arg, "\"\n\\" ) . '"'; |
| 187 |
} |
| 188 |
|
| 189 |
private static function call( ...$args ): string { |
| 190 |
$method = array_shift( $args ); |
| 191 |
return static::call_array( $method, $args ); |
| 192 |
} |
| 193 |
|
| 194 |
private static function call_array( string $method, array $args ): string { |
| 195 |
return 'c.' . $method . '(' . implode( ', ', $args ) . ');'; |
| 196 |
} |
| 197 |
} |
| 198 |
|