1<?php declare(strict_types=1);
2
3/*
4 * This file is part of the Monolog package.
5 *
6 * (c) Jordi Boggiano <j.boggiano@seld.be>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Monolog\Handler;
13
14use Monolog\Formatter\WildfireFormatter;
15use Monolog\Formatter\FormatterInterface;
16
17/**
18 * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol.
19 *
20 * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
21 *
22 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
23 */
24class FirePHPHandler extends AbstractProcessingHandler
25{
26    use WebRequestRecognizerTrait;
27
28    /**
29     * WildFire JSON header message format
30     */
31    protected const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2';
32
33    /**
34     * FirePHP structure for parsing messages & their presentation
35     */
36    protected const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1';
37
38    /**
39     * Must reference a "known" plugin, otherwise headers won't display in FirePHP
40     */
41    protected const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3';
42
43    /**
44     * Header prefix for Wildfire to recognize & parse headers
45     */
46    protected const HEADER_PREFIX = 'X-Wf';
47
48    /**
49     * Whether or not Wildfire vendor-specific headers have been generated & sent yet
50     * @var bool
51     */
52    protected static $initialized = false;
53
54    /**
55     * Shared static message index between potentially multiple handlers
56     * @var int
57     */
58    protected static $messageIndex = 1;
59
60    /** @var bool */
61    protected static $sendHeaders = true;
62
63    /**
64     * Base header creation function used by init headers & record headers
65     *
66     * @param array<int|string> $meta    Wildfire Plugin, Protocol & Structure Indexes
67     * @param string            $message Log message
68     *
69     * @return array<string, string> Complete header string ready for the client as key and message as value
70     *
71     * @phpstan-return non-empty-array<string, string>
72     */
73    protected function createHeader(array $meta, string $message): array
74    {
75        $header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta));
76
77        return [$header => $message];
78    }
79
80    /**
81     * Creates message header from record
82     *
83     * @return array<string, string>
84     *
85     * @phpstan-return non-empty-array<string, string>
86     *
87     * @see createHeader()
88     *
89     * @phpstan-param FormattedRecord $record
90     */
91    protected function createRecordHeader(array $record): array
92    {
93        // Wildfire is extensible to support multiple protocols & plugins in a single request,
94        // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake.
95        return $this->createHeader(
96            [1, 1, 1, self::$messageIndex++],
97            $record['formatted']
98        );
99    }
100
101    /**
102     * {@inheritDoc}
103     */
104    protected function getDefaultFormatter(): FormatterInterface
105    {
106        return new WildfireFormatter();
107    }
108
109    /**
110     * Wildfire initialization headers to enable message parsing
111     *
112     * @see createHeader()
113     * @see sendHeader()
114     *
115     * @return array<string, string>
116     */
117    protected function getInitHeaders(): array
118    {
119        // Initial payload consists of required headers for Wildfire
120        return array_merge(
121            $this->createHeader(['Protocol', 1], static::PROTOCOL_URI),
122            $this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI),
123            $this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI)
124        );
125    }
126
127    /**
128     * Send header string to the client
129     */
130    protected function sendHeader(string $header, string $content): void
131    {
132        if (!headers_sent() && self::$sendHeaders) {
133            header(sprintf('%s: %s', $header, $content));
134        }
135    }
136
137    /**
138     * Creates & sends header for a record, ensuring init headers have been sent prior
139     *
140     * @see sendHeader()
141     * @see sendInitHeaders()
142     */
143    protected function write(array $record): void
144    {
145        if (!self::$sendHeaders || !$this->isWebRequest()) {
146            return;
147        }
148
149        // WildFire-specific headers must be sent prior to any messages
150        if (!self::$initialized) {
151            self::$initialized = true;
152
153            self::$sendHeaders = $this->headersAccepted();
154            if (!self::$sendHeaders) {
155                return;
156            }
157
158            foreach ($this->getInitHeaders() as $header => $content) {
159                $this->sendHeader($header, $content);
160            }
161        }
162
163        $header = $this->createRecordHeader($record);
164        if (trim(current($header)) !== '') {
165            $this->sendHeader(key($header), current($header));
166        }
167    }
168
169    /**
170     * Verifies if the headers are accepted by the current user agent
171     */
172    protected function headersAccepted(): bool
173    {
174        if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) {
175            return true;
176        }
177
178        return isset($_SERVER['HTTP_X_FIREPHP_VERSION']);
179    }
180}
181