Completed
Push — master ( cdeef2...f03c61 )
by Justin
03:14
created

BrowserConsoleHandler::write()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Monolog package.
5
 *
6
 * (c) Jordi Boggiano <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Monolog\Handler;
13
14
use Monolog\Formatter\LineFormatter;
15
16
/**
17
 * Handler sending logs to browser's javascript console with no browser extension required
18
 *
19
 * @author Olivier Poitrey <[email protected]>
20
 */
21
class BrowserConsoleHandler extends AbstractProcessingHandler
22
{
23
    protected static $initialized = false;
24
    protected static $records = array();
25
26
    /**
27
     * {@inheritDoc}
28
     *
29
     * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
30
     *
31
     * Example of formatted string:
32
     *
33
     *     You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
34
     */
35
    protected function getDefaultFormatter()
36
    {
37
        return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
38
    }
39
40
    /**
41
     * {@inheritDoc}
42
     */
43
    protected function write(array $record)
44
    {
45
        // Accumulate records
46
        self::$records[] = $record;
47
48
        // Register shutdown handler if not already done
49
        if (!self::$initialized) {
50
            self::$initialized = true;
51
            $this->registerShutdownFunction();
52
        }
53
    }
54
55
    /**
56
     * Convert records to javascript console commands and send it to the browser.
57
     * This method is automatically called on PHP shutdown if output is HTML or Javascript.
58
     */
59
    public static function send()
60
    {
61
        $format = self::getResponseFormat();
62
        if ($format === 'unknown') {
63
            return;
64
        }
65
66
        if (count(self::$records)) {
67
            if ($format === 'html') {
68
                self::writeOutput('<script>' . self::generateScript() . '</script>');
69
            } elseif ($format === 'js') {
70
                self::writeOutput(self::generateScript());
71
            }
72
            self::reset();
73
        }
74
    }
75
76
    /**
77
     * Forget all logged records
78
     */
79
    public static function reset()
80
    {
81
        self::$records = array();
82
    }
83
84
    /**
85
     * Wrapper for register_shutdown_function to allow overriding
86
     */
87
    protected function registerShutdownFunction()
88
    {
89
        if (PHP_SAPI !== 'cli') {
90
            register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
91
        }
92
    }
93
94
    /**
95
     * Wrapper for echo to allow overriding
96
     *
97
     * @param string $str
98
     */
99
    protected static function writeOutput($str)
100
    {
101
        echo $str;
102
    }
103
104
    /**
105
     * Checks the format of the response
106
     *
107
     * If Content-Type is set to application/javascript or text/javascript -> js
108
     * If Content-Type is set to text/html, or is unset -> html
109
     * If Content-Type is anything else -> unknown
110
     *
111
     * @return string One of 'js', 'html' or 'unknown'
112
     */
113
    protected static function getResponseFormat()
114
    {
115
        // Check content type
116
        foreach (headers_list() as $header) {
117
            if (stripos($header, 'content-type:') === 0) {
118
                // This handler only works with HTML and javascript outputs
119
                // text/javascript is obsolete in favour of application/javascript, but still used
120
                if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) {
121
                    return 'js';
122
                }
123
                if (stripos($header, 'text/html') === false) {
124
                    return 'unknown';
125
                }
126
                break;
127
            }
128
        }
129
130
        return 'html';
131
    }
132
133
    private static function generateScript()
134
    {
135
        $script = array();
136
        foreach (self::$records as $record) {
137
            $context = self::dump('Context', $record['context']);
138
            $extra = self::dump('Extra', $record['extra']);
139
140
            if (empty($context) && empty($extra)) {
141
                $script[] = self::call_array('log', self::handleStyles($record['formatted']));
142
            } else {
143
                $script = array_merge($script,
144
                    array(self::call_array('groupCollapsed', self::handleStyles($record['formatted']))),
145
                    $context,
146
                    $extra,
147
                    array(self::call('groupEnd'))
148
                );
149
            }
150
        }
151
152
        return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
153
    }
154
155
    private static function handleStyles($formatted)
156
    {
157
        $args = array(self::quote('font-weight: normal'));
158
        $format = '%c' . $formatted;
159
        preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
160
161
        foreach (array_reverse($matches) as $match) {
162
            $args[] = self::quote(self::handleCustomStyles($match[2][0], $match[1][0]));
163
            $args[] = '"font-weight: normal"';
164
165
            $pos = $match[0][1];
166
            $format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0]));
167
        }
168
169
        array_unshift($args, self::quote($format));
170
171
        return $args;
172
    }
173
174
    private static function handleCustomStyles($style, $string)
175
    {
176
        static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey');
177
        static $labels = array();
178
179
        return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function ($m) use ($string, &$colors, &$labels) {
180
            if (trim($m[1]) === 'autolabel') {
181
                // Format the string as a label with consistent auto assigned background color
182
                if (!isset($labels[$string])) {
183
                    $labels[$string] = $colors[count($labels) % count($colors)];
184
                }
185
                $color = $labels[$string];
186
187
                return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
188
            }
189
190
            return $m[1];
191
        }, $style);
192
    }
193
194
    private static function dump($title, array $dict)
195
    {
196
        $script = array();
197
        $dict = array_filter($dict);
198
        if (empty($dict)) {
199
            return $script;
200
        }
201
        $script[] = self::call('log', self::quote('%c%s'), self::quote('font-weight: bold'), self::quote($title));
202
        foreach ($dict as $key => $value) {
203
            $value = json_encode($value);
204
            if (empty($value)) {
205
                $value = self::quote('');
206
            }
207
            $script[] = self::call('log', self::quote('%s: %o'), self::quote($key), $value);
208
        }
209
210
        return $script;
211
    }
212
213
    private static function quote($arg)
214
    {
215
        return '"' . addcslashes($arg, "\"\n\\") . '"';
216
    }
217
218
    private static function call()
219
    {
220
        $args = func_get_args();
221
        $method = array_shift($args);
222
223
        return self::call_array($method, $args);
224
    }
225
226
    private static function call_array($method, array $args)
227
    {
228
        return 'c.' . $method . '(' . implode(', ', $args) . ');';
229
    }
230
}
231