Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LegacyHandler
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 6
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 openSink
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
72
 errorTrap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 useUdp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 write
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 close
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Logger\Monolog;
22
23use LogicException;
24use MediaWiki\Logger\LegacyLogger;
25use Monolog\Handler\AbstractProcessingHandler;
26use Monolog\Logger;
27use Socket;
28use UnexpectedValueException;
29
30/**
31 * Monolog imitation of MediaWiki\Logger\LegacyLogger
32 *
33 * This replicates the behavior of LegacyLogger, which in turn replicates
34 * MediaWiki's former wfErrorLog() function.
35 *
36 * The main use case of the LegacyHandler is to enable adoption of Monolog
37 * features (such as alternate formatters, extra processors, and enabling multiple
38 * destinations/handlers at the same time), where one of the handlers (this one)
39 * essentiallly does what the LegacySpi would do if you hadn't enabled
40 * MonologSpi. In particular: writing to a file like $wgDebugLogFile,
41 * and sending messages to a PHP stream or udp2log server.
42 *
43 * For udp2log output, the stream specification must have the form:
44 * "udp://HOST:PORT[/PREFIX]"
45 * where:
46 *
47 * - HOST: IPv4, IPv6 or hostname
48 * - PORT: server port
49 * - PREFIX: optional (but recommended) prefix telling udp2log how to route
50 *   the log event. The special prefix "{channel}" will use the log event's
51 *   channel as the prefix value.
52 *
53 * When not targeting a udp2log server, this class will act as a drop-in
54 * replacement for \Monolog\Handler\StreamHandler.
55 *
56 * @since 1.25
57 * @ingroup Debug
58 * @copyright © 2013 Wikimedia Foundation and contributors
59 */
60class LegacyHandler extends AbstractProcessingHandler {
61
62    /**
63     * Log sink descriptor
64     * @var string
65     */
66    protected $uri;
67
68    /**
69     * Filter log events using legacy rules
70     * @var bool
71     */
72    protected $useLegacyFilter;
73
74    /**
75     * Log sink
76     * @var Socket|resource|null
77     */
78    protected $sink;
79
80    /**
81     * @var string|null
82     */
83    protected $error;
84
85    /**
86     * @var string
87     */
88    protected $host;
89
90    /**
91     * @var int
92     */
93    protected $port;
94
95    /**
96     * @var string
97     */
98    protected $prefix;
99
100    /**
101     * @param string $stream Stream URI
102     * @param bool $useLegacyFilter Filter log events using legacy rules
103     * @param int $level Minimum logging level that will trigger handler
104     * @param bool $bubble Can handled messages bubble up the handler stack?
105     */
106    public function __construct(
107        $stream,
108        $useLegacyFilter = false,
109        $level = Logger::DEBUG,
110        $bubble = true
111    ) {
112        parent::__construct( $level, $bubble );
113        $this->uri = $stream;
114        $this->useLegacyFilter = $useLegacyFilter;
115    }
116
117    /**
118     * Open the log sink described by our stream URI.
119     */
120    protected function openSink() {
121        if ( !$this->uri ) {
122            throw new LogicException(
123                'Missing stream uri, the stream can not be opened.' );
124        }
125        $this->error = null;
126        set_error_handler( [ $this, 'errorTrap' ] );
127
128        if ( str_starts_with( $this->uri, 'udp:' ) ) {
129            $parsed = parse_url( $this->uri );
130            if ( !isset( $parsed['host'] ) ) {
131                throw new UnexpectedValueException( sprintf(
132                    'Udp transport "%s" must specify a host', $this->uri
133                ) );
134            }
135            if ( !isset( $parsed['port'] ) ) {
136                throw new UnexpectedValueException( sprintf(
137                    'Udp transport "%s" must specify a port', $this->uri
138                ) );
139            }
140
141            $this->host = $parsed['host'];
142            $this->port = $parsed['port'];
143            $this->prefix = '';
144
145            if ( isset( $parsed['path'] ) ) {
146                $this->prefix = ltrim( $parsed['path'], '/' );
147            }
148
149            if ( filter_var( $this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
150                $domain = AF_INET6;
151
152            } else {
153                $domain = AF_INET;
154            }
155
156            $this->sink = socket_create( $domain, SOCK_DGRAM, SOL_UDP );
157
158        } else {
159            $this->sink = fopen( $this->uri, 'a' );
160        }
161        restore_error_handler();
162
163        if ( !$this->sink ) {
164            $this->sink = null;
165            throw new UnexpectedValueException( sprintf(
166                'The stream or file "%s" could not be opened: %s',
167                // @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal Set by error handler
168                $this->uri, $this->error
169            ) );
170        }
171    }
172
173    /**
174     * Custom error handler.
175     * @param int $code Error number
176     * @param string $msg Error message
177     */
178    protected function errorTrap( $code, $msg ) {
179        $this->error = $msg;
180    }
181
182    /**
183     * Should we use UDP to send messages to the sink?
184     * @return bool
185     */
186    protected function useUdp() {
187        return $this->host !== null;
188    }
189
190    protected function write( array $record ): void {
191        if ( $this->useLegacyFilter &&
192            !LegacyLogger::shouldEmit(
193                $record['channel'], $record['message'],
194                $record['level'], $record
195        ) ) {
196            // Do not write record if we are enforcing legacy rules and they
197            // do not pass this message. This used to be done in isHandling(),
198            // but Monolog 1.12.0 made a breaking change that removed access
199            // to the needed channel and context information.
200            return;
201        }
202
203        if ( $this->sink === null ) {
204            $this->openSink();
205        }
206
207        $text = (string)$record['formatted'];
208        if ( $this->useUdp() ) {
209            // Clean it up for the multiplexer
210            if ( $this->prefix !== '' ) {
211                $leader = ( $this->prefix === '{channel}' ) ?
212                    $record['channel'] : $this->prefix;
213                $text = preg_replace( '/^/m', "{$leader} ", $text );
214
215                // Limit to 64 KiB
216                if ( strlen( $text ) > 65506 ) {
217                    $text = substr( $text, 0, 65506 );
218                }
219
220                if ( !str_ends_with( $text, "\n" ) ) {
221                    $text .= "\n";
222                }
223
224            } elseif ( strlen( $text ) > 65507 ) {
225                $text = substr( $text, 0, 65507 );
226            }
227
228            socket_sendto(
229                $this->sink,
230                $text,
231                strlen( $text ),
232                0,
233                $this->host,
234                $this->port
235            );
236
237        } else {
238            fwrite( $this->sink, $text );
239        }
240    }
241
242    public function close(): void {
243        if ( $this->sink ) {
244            if ( $this->useUdp() ) {
245                socket_close( $this->sink );
246            } else {
247                fclose( $this->sink );
248            }
249        }
250        $this->sink = null;
251    }
252}