Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.93% covered (warning)
67.93%
161 / 237
32.00% covered (danger)
32.00%
8 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
MWDebug
67.93% covered (warning)
67.93%
161 / 237
32.00% covered (danger)
32.00%
8 / 25
385.37
0.00% covered (danger)
0.00%
0 / 1
 setup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deinit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addModules
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 log
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 getLog
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearLog
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 warning
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 deprecated
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 detectDeprecatedOverride
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 deprecatedMsg
68.00% covered (warning)
68.00%
17 / 25
0.00% covered (danger)
0.00%
0 / 1
13.28
 sendRawDeprecated
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
9.66
 filterDeprecationForTest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 clearDeprecationFilters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCallerDescription
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
7.10
 formatCallerDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseCallerDescription
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
2.86
 sendMessage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 debugMsg
82.86% covered (warning)
82.86%
29 / 35
0.00% covered (danger)
0.00%
0 / 1
19.63
 query
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getFilesIncluded
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 getDebugHTML
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getHTMLDebugLog
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 appendDebugInfoToApiResult
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
5.76
 getDebugInfo
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
3
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
21use MediaWiki\Context\IContextSource;
22use MediaWiki\Html\Html;
23use MediaWiki\Logger\LegacyLogger;
24use MediaWiki\Output\OutputPage;
25use MediaWiki\Parser\Sanitizer;
26use MediaWiki\ResourceLoader\ResourceLoader;
27use MediaWiki\Utils\GitInfo;
28use Wikimedia\WrappedString;
29use Wikimedia\WrappedStringList;
30
31/**
32 * Debug toolbar.
33 *
34 * By default most of these methods do nothing, as enforced by self::$enabled = false.
35 *
36 * To enable the debug toolbar, use $wgDebugToolbar = true in LocalSettings.php.
37 * That ensures MWDebug::init() is called from Setup.php.
38 *
39 * @since 1.19
40 * @ingroup Debug
41 */
42class MWDebug {
43    /**
44     * Log lines
45     *
46     * @var array
47     */
48    protected static $log = [];
49
50    /**
51     * Debug messages from wfDebug().
52     *
53     * @var array
54     */
55    protected static $debug = [];
56
57    /**
58     * SQL statements of the database queries.
59     *
60     * @var array
61     */
62    protected static $query = [];
63
64    /**
65     * Is the debugger enabled?
66     *
67     * @var bool
68     */
69    protected static $enabled = false;
70
71    /**
72     * Array of functions that have already been warned, formatted
73     * function-caller to prevent a buttload of warnings
74     *
75     * @var array
76     */
77    protected static $deprecationWarnings = [];
78
79    /**
80     * @var array Keys are regexes, values are optional callbacks to call if the filter is hit
81     */
82    protected static $deprecationFilters = [];
83
84    /**
85     * @internal For use by Setup.php only.
86     */
87    public static function setup() {
88        global $wgDebugToolbar,
89            $wgUseCdn, $wgUseFileCache;
90
91        if (
92            // Easy to forget to falsify $wgDebugToolbar for static caches.
93            // If file cache or CDN cache is on, just disable this (DWIMD).
94            $wgUseCdn ||
95            $wgUseFileCache ||
96            // Keep MWDebug off on CLI. This prevents MWDebug from eating up
97            // all the memory for logging SQL queries in maintenance scripts.
98            MW_ENTRY_POINT === 'cli'
99        ) {
100            return;
101        }
102
103        if ( $wgDebugToolbar ) {
104            self::init();
105        }
106    }
107
108    /**
109     * Enabled the debugger and load resource module.
110     * This is called by Setup.php when $wgDebugToolbar is true.
111     *
112     * @since 1.19
113     */
114    public static function init() {
115        self::$enabled = true;
116    }
117
118    /**
119     * Disable the debugger.
120     *
121     * @since 1.28
122     */
123    public static function deinit() {
124        self::$enabled = false;
125    }
126
127    /**
128     * Add ResourceLoader modules to the OutputPage object if debugging is
129     * enabled.
130     *
131     * @since 1.19
132     * @param OutputPage $out
133     */
134    public static function addModules( OutputPage $out ) {
135        if ( self::$enabled ) {
136            $out->addModules( 'mediawiki.debug' );
137        }
138    }
139
140    /**
141     * Adds a line to the log
142     *
143     * @since 1.19
144     * @param mixed $str
145     */
146    public static function log( $str ) {
147        if ( !self::$enabled ) {
148            return;
149        }
150        if ( !is_string( $str ) ) {
151            $str = print_r( $str, true );
152        }
153        self::$log[] = [
154            'msg' => htmlspecialchars( $str ),
155            'type' => 'log',
156            'caller' => wfGetCaller(),
157        ];
158    }
159
160    /**
161     * Returns internal log array
162     * @since 1.19
163     * @return array
164     */
165    public static function getLog() {
166        return self::$log;
167    }
168
169    /**
170     * Clears internal log array and deprecation tracking
171     * @since 1.19
172     */
173    public static function clearLog() {
174        self::$log = [];
175        self::$deprecationWarnings = [];
176    }
177
178    /**
179     * Adds a warning entry to the log
180     *
181     * @since 1.19
182     * @param string $msg
183     * @param int $callerOffset
184     * @param int $level A PHP error level. See sendMessage()
185     * @param string $log 'production' will always trigger a php error, 'auto'
186     *    will trigger an error if $wgDevelopmentWarnings is true, and 'debug'
187     *    will only write to the debug log(s).
188     */
189    public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
190        global $wgDevelopmentWarnings;
191
192        if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
193            $log = 'debug';
194        }
195
196        if ( $log === 'debug' ) {
197            $level = false;
198        }
199
200        $callerDescription = self::getCallerDescription( $callerOffset );
201
202        self::sendMessage(
203            self::formatCallerDescription( $msg, $callerDescription ),
204            'warning',
205            $level );
206    }
207
208    /**
209     * Show a warning that $function is deprecated.
210     *
211     * @see deprecatedMsg()
212     * @since 1.19
213     *
214     * @param string $function Function that is deprecated.
215     * @param string|false $version Version in which the function was deprecated.
216     * @param string|bool $component Component to which the function belongs.
217     *    If false, it is assumed the function is in MediaWiki core.
218     * @param int $callerOffset How far up the callstack is the original
219     *    caller. 2 = function that called the function that called
220     *    MWDebug::deprecated() (Added in 1.20).
221     */
222    public static function deprecated( $function, $version = false,
223        $component = false, $callerOffset = 2
224    ) {
225        if ( $version ) {
226            $component = $component ?: 'MediaWiki';
227            $msg = "Use of $function was deprecated in $component $version.";
228        } else {
229            $msg = "Use of $function is deprecated.";
230        }
231        self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
232    }
233
234    /**
235     * Show a warning if $method declared in $class is overridden in $instance.
236     *
237     * @since 1.36
238     * @see deprecatedMsg()
239     *
240     * phpcs:ignore MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam
241     * @param object $instance Object on which to detect deprecated overrides (typically $this).
242     * @param string $class Class declaring the deprecated method (typically __CLASS__ )
243     * @param string $method The name of the deprecated method.
244     * @param string|false $version Version in which the method was deprecated.
245     *   Does not issue deprecation warnings if false.
246     * @param string|bool $component Component to which the class belongs.
247     *    If false, it is assumed the class is in MediaWiki core.
248     * @param int $callerOffset How far up the callstack is the original
249     *    caller. 2 = function that called the function that called
250     *    MWDebug::detectDeprecatedOverride()
251     *
252     * @return bool True if the method was overridden, false otherwise. If the method
253     *         was overridden, it should be called. The deprecated method's default
254     *         implementation should call MWDebug::deprecated().
255     */
256    public static function detectDeprecatedOverride( $instance, $class, $method, $version = false,
257        $component = false, $callerOffset = 2
258    ) {
259        $reflectionMethod = new ReflectionMethod( $instance, $method );
260        $declaringClass = $reflectionMethod->getDeclaringClass()->getName();
261
262        if ( $declaringClass === $class ) {
263            // not overridden, nothing to do
264            return false;
265        }
266
267        if ( $version ) {
268            $component = $component ?: 'MediaWiki';
269            $msg = "$declaringClass overrides $method which was deprecated in $component $version.";
270            self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
271        }
272
273        return true;
274    }
275
276    /**
277     * Log a deprecation warning with arbitrary message text. A caller
278     * description will be appended. If the message has already been sent for
279     * this caller, it won't be sent again.
280     *
281     * Although there are component and version parameters, they are not
282     * automatically appended to the message. The message text should include
283     * information about when the thing was deprecated.
284     *
285     * The warning will be sent to the following locations:
286     * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar
287     *   is set to true.
288     * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings
289     *   is set to true. This is the case in phpunit tests per default, and will
290     *   cause tests to fail.
291     * - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false.
292     *
293     * @since 1.35
294     *
295     * @param string $msg The message
296     * @param string|false $version Version of MediaWiki that the function
297     *    was deprecated in.
298     * @param string|bool $component Component to which the function belongs.
299     *    If false, it is assumed the function is in MediaWiki core.
300     * @param int|false $callerOffset How far up the call stack is the original
301     *    caller. 2 = function that called the function that called us. If false,
302     *    the caller description will not be appended.
303     */
304    public static function deprecatedMsg( $msg, $version = false,
305        $component = false, $callerOffset = 2
306    ) {
307        if ( $callerOffset === false ) {
308            $callerFunc = '';
309            $rawMsg = $msg;
310        } else {
311            $callerDescription = self::getCallerDescription( $callerOffset );
312            $callerFunc = $callerDescription['func'];
313            $rawMsg = self::formatCallerDescription( $msg, $callerDescription );
314        }
315
316        $sendToLog = true;
317
318        // Check to see if there already was a warning about this function
319        if ( isset( self::$deprecationWarnings[$msg][$callerFunc] ) ) {
320            return;
321        } elseif ( isset( self::$deprecationWarnings[$msg] ) ) {
322            if ( self::$enabled ) {
323                $sendToLog = false;
324            } else {
325                return;
326            }
327        }
328
329        self::$deprecationWarnings[$msg][$callerFunc] = true;
330
331        if ( $version ) {
332            global $wgDeprecationReleaseLimit;
333
334            $component = $component ?: 'MediaWiki';
335            if ( $wgDeprecationReleaseLimit && $component === 'MediaWiki' ) {
336                # Strip -* off the end of $version so that branches can use the
337                # format #.##-branchname to avoid issues if the branch is merged into
338                # a version of MediaWiki later than what it was branched from
339                $comparableVersion = preg_replace( '/-.*$/', '', $version );
340
341                # If the comparableVersion is larger than our release limit then
342                # skip the warning message for the deprecation
343                if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
344                    $sendToLog = false;
345                }
346            }
347        }
348
349        self::sendRawDeprecated(
350            $rawMsg,
351            $sendToLog,
352            $callerFunc
353        );
354    }
355
356    /**
357     * Send a raw deprecation message to the log and the debug toolbar,
358     * without filtering of duplicate messages. A caller description will
359     * not be appended.
360     *
361     * @param string $msg The complete message including relevant caller information.
362     * @param bool $sendToLog If true, the message will be sent to the debug
363     *   toolbar, the debug log, and raised as a warning to PHP. If false, the message
364     *   will only be sent to the debug toolbar.
365     * @param string $callerFunc The caller, for display in the debug toolbar's
366     *   caller column.
367     */
368    public static function sendRawDeprecated( $msg, $sendToLog = true, $callerFunc = '' ) {
369        foreach ( self::$deprecationFilters as $filter => $callback ) {
370            if ( preg_match( $filter, $msg ) ) {
371                if ( is_callable( $callback ) ) {
372                    $callback();
373                }
374                return;
375            }
376        }
377
378        if ( $sendToLog ) {
379            trigger_error( $msg, E_USER_DEPRECATED );
380        }
381    }
382
383    /**
384     * Deprecation messages matching the supplied regex will be suppressed.
385     * Use this to filter deprecation warnings when testing deprecated code.
386     *
387     * @param string $regex
388     * @param ?callable $callback To call if $regex is hit
389     */
390    public static function filterDeprecationForTest(
391        string $regex, ?callable $callback = null
392    ): void {
393        if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
394            throw new LogicException( __METHOD__ . ' can only be used in tests' );
395        }
396        self::$deprecationFilters[$regex] = $callback;
397    }
398
399    /**
400     * Clear all deprecation filters.
401     */
402    public static function clearDeprecationFilters() {
403        self::$deprecationFilters = [];
404    }
405
406    /**
407     * Get an array describing the calling function at a specified offset.
408     *
409     * @param int $callerOffset How far up the callstack is the original
410     *    caller. 0 = function that called getCallerDescription()
411     * @return array Array with two keys: 'file' and 'func'
412     */
413    private static function getCallerDescription( $callerOffset ) {
414        $callers = wfDebugBacktrace();
415
416        if ( isset( $callers[$callerOffset] ) ) {
417            $callerfile = $callers[$callerOffset];
418            if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
419                $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
420            } else {
421                $file = '(internal function)';
422            }
423        } else {
424            $file = '(unknown location)';
425        }
426
427        if ( isset( $callers[$callerOffset + 1] ) ) {
428            $callerfunc = $callers[$callerOffset + 1];
429            $func = '';
430            if ( isset( $callerfunc['class'] ) ) {
431                $func .= $callerfunc['class'] . '::';
432            }
433            if ( isset( $callerfunc['function'] ) ) {
434                $func .= $callerfunc['function'];
435            }
436        } else {
437            $func = 'unknown';
438        }
439
440        return [ 'file' => $file, 'func' => $func ];
441    }
442
443    /**
444     * Append a caller description to an error message
445     *
446     * @param string $msg
447     * @param array $caller Caller description from getCallerDescription()
448     * @return string
449     */
450    private static function formatCallerDescription( $msg, $caller ) {
451        // When changing this, update the below parseCallerDescription() method  as well.
452        return $msg . ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
453    }
454
455    /**
456     * Append a caller description to an error message
457     *
458     * @internal For use by MWExceptionHandler to override 'exception.file' in error logs.
459     * @param string $msg Formatted message from formatCallerDescription() and getCallerDescription()
460     * @return null|array<string,string> Null if unable to recognise all parts, or array with:
461     *  - 'file': string of file path
462     *  - 'line': string of line number
463     *  - 'func': string of function or method name
464     *  - 'message': Re-formatted version of $msg for use with ErrorException,
465     *  so as to not include file/line twice.
466     */
467    public static function parseCallerDescription( $msg ) {
468        $match = null;
469        preg_match( '/(.*) \[Called from ([^ ]+) in ([^ ]+) at line (\d+)\]$/', $msg, $match );
470        if ( $match ) {
471            return [
472                'message' => "{$match[1]} [Called from {$match[2]}]",
473                'func' => $match[2],
474                'file' => $match[3],
475                'line' => $match[4],
476            ];
477        } else {
478            return null;
479        }
480    }
481
482    /**
483     * Send a message to the debug log and optionally also trigger a PHP
484     * error, depending on the $level argument.
485     *
486     * @param string $msg Message to send
487     * @param string $group Log group on which to send the message
488     * @param int|bool $level Error level to use; set to false to not trigger an error
489     */
490    private static function sendMessage( $msg, $group, $level ) {
491        if ( $level !== false ) {
492            trigger_error( $msg, $level );
493        }
494
495        wfDebugLog( $group, $msg );
496    }
497
498    /**
499     * This method receives messages from LoggerFactory, wfDebugLog, and MWExceptionHandler.
500     *
501     * Do NOT call this method directly.
502     *
503     * @internal For use by MWExceptionHandler and LegacyLogger only
504     * @since 1.19
505     * @param string $str
506     * @param array $context
507     */
508    public static function debugMsg( $str, $context = [] ) {
509        global $wgDebugComments, $wgShowDebug;
510
511        if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
512            if ( $context ) {
513                $prefix = '';
514                if ( isset( $context['prefix'] ) ) {
515                    $prefix = $context['prefix'];
516                } elseif ( isset( $context['channel'] ) && $context['channel'] !== 'wfDebug' ) {
517                    $prefix = "[{$context['channel']}";
518                }
519                if ( isset( $context['seconds_elapsed'] ) && isset( $context['memory_used'] ) ) {
520                    $prefix .= "{$context['seconds_elapsed']} {$context['memory_used']}  ";
521                }
522                $str = LegacyLogger::interpolate( $str, $context );
523                $str = $prefix . $str;
524            }
525            $str = rtrim( UtfNormal\Validator::cleanUp( $str ) );
526            self::$debug[] = $str;
527            if ( isset( $context['channel'] ) && $context['channel'] === 'error' ) {
528                $message = isset( $context['exception'] )
529                    ? $context['exception']->getMessage()
530                    : $str;
531                $real = self::parseCallerDescription( $message );
532                if ( $real ) {
533                    // from wfLogWarning()
534                    $message = $real['message'];
535                    $caller = $real['func'];
536                } else {
537                    $trace = isset( $context['exception'] ) ? $context['exception']->getTrace() : [];
538                    if ( ( $trace[5]['function'] ?? null ) === 'wfDeprecated' ) {
539                        // from MWExceptionHandler/trigger_error/MWDebug/MWDebug/MWDebug/wfDeprecated()
540                        $offset = 6;
541                    } elseif ( ( $trace[1]['function'] ?? null ) === 'trigger_error' ) {
542                        // from trigger_error
543                        $offset = 2;
544                    } else {
545                        // built-in PHP error
546                        $offset = 1;
547                    }
548                    $frame = $trace[$offset] ?? $trace[0];
549                    $caller = ( isset( $frame['class'] ) ? $frame['class'] . '::' : '' )
550                        . $frame['function'];
551                }
552
553                self::$log[] = [
554                    'msg' => htmlspecialchars( $message ),
555                    'type' => 'warn',
556                    'caller' => $caller,
557                ];
558            }
559        }
560    }
561
562    /**
563     * Begins profiling on a database query
564     *
565     * @since 1.19
566     * @param string $sql
567     * @param string $function
568     * @param float $runTime Query run time
569     * @param string $dbhost
570     * @return bool True if debugger is enabled, false otherwise
571     */
572    public static function query( $sql, $function, $runTime, $dbhost ) {
573        if ( !self::$enabled ) {
574            return false;
575        }
576
577        // Replace invalid UTF-8 chars with a square UTF-8 character
578        // This prevents json_encode from erroring out due to binary SQL data
579        $sql = preg_replace(
580            '/(
581                [\xC0-\xC1] # Invalid UTF-8 Bytes
582                | [\xF5-\xFF] # Invalid UTF-8 Bytes
583                | \xE0[\x80-\x9F] # Overlong encoding of prior code point
584                | \xF0[\x80-\x8F] # Overlong encoding of prior code point
585                | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
586                | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
587                | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
588                | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
589                | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]
590                    | [\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
591                | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
592                | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
593                | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
594            )/x',
595            'â– ',
596            $sql
597        );
598
599        // last check for invalid utf8
600        $sql = UtfNormal\Validator::cleanUp( $sql );
601
602        self::$query[] = [
603            'sql' => "$dbhost$sql",
604            'function' => $function,
605            'time' => $runTime,
606        ];
607
608        return true;
609    }
610
611    /**
612     * Returns a list of files included, along with their size
613     *
614     * @param IContextSource $context
615     * @return array
616     */
617    protected static function getFilesIncluded( IContextSource $context ) {
618        $files = get_included_files();
619        $fileList = [];
620        foreach ( $files as $file ) {
621            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
622            $size = @filesize( $file );
623            if ( $size === false ) {
624                // Certain files that have been included might then be deleted. This is especially likely to happen
625                // in tests, see T351986.
626                // Just use a size of 0, but include these files here to try and be as useful as possible.
627                $size = 0;
628            }
629            $fileList[] = [
630                'name' => $file,
631                'size' => $context->getLanguage()->formatSize( $size ),
632            ];
633        }
634
635        return $fileList;
636    }
637
638    /**
639     * Returns the HTML to add to the page for the toolbar
640     *
641     * @since 1.19
642     * @param IContextSource $context
643     * @return WrappedStringList
644     */
645    public static function getDebugHTML( IContextSource $context ) {
646        global $wgDebugComments;
647
648        $html = [];
649
650        if ( self::$enabled ) {
651            self::log( 'MWDebug output complete' );
652            $debugInfo = self::getDebugInfo( $context );
653
654            // Cannot use OutputPage::addJsConfigVars because those are already outputted
655            // by the time this method is called.
656            $html[] = ResourceLoader::makeInlineScript(
657                ResourceLoader::makeConfigSetScript( [ 'debugInfo' => $debugInfo ] )
658            );
659        }
660
661        if ( $wgDebugComments ) {
662            $html[] = '<!-- Debug output:';
663            foreach ( self::$debug as $line ) {
664                $html[] = htmlspecialchars( $line, ENT_NOQUOTES );
665            }
666            $html[] = '-->';
667        }
668
669        return WrappedString::join( "\n", $html );
670    }
671
672    /**
673     * Generate debug log in HTML for displaying at the bottom of the main
674     * content area.
675     * If $wgShowDebug is false, an empty string is always returned.
676     *
677     * @since 1.20
678     * @return WrappedStringList HTML fragment
679     */
680    public static function getHTMLDebugLog() {
681        global $wgShowDebug;
682
683        $html = [];
684        if ( $wgShowDebug ) {
685            $html[] = Html::openElement( 'div', [ 'id' => 'mw-html-debug-log' ] );
686            $html[] = "<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">";
687
688            foreach ( self::$debug as $line ) {
689                $display = nl2br( htmlspecialchars( trim( $line ) ) );
690
691                $html[] = "<li><code>$display</code></li>";
692            }
693
694            $html[] = '</ul>';
695            $html[] = '</div>';
696        }
697        return WrappedString::join( "\n", $html );
698    }
699
700    /**
701     * Append the debug info to given ApiResult
702     *
703     * @param IContextSource $context
704     * @param ApiResult $result
705     */
706    public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
707        if ( !self::$enabled ) {
708            return;
709        }
710
711        // output errors as debug info, when display_errors is on
712        // this is necessary for all non html output of the api, because that clears all errors first
713        $obContents = ob_get_contents();
714        if ( $obContents ) {
715            $obContentArray = explode( '<br />', $obContents );
716            foreach ( $obContentArray as $obContent ) {
717                if ( trim( $obContent ) ) {
718                    self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
719                }
720            }
721        }
722
723        self::log( 'MWDebug output complete' );
724        $debugInfo = self::getDebugInfo( $context );
725
726        ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' );
727        ApiResult::setIndexedTagName( $debugInfo['log'], 'line' );
728        ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' );
729        ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' );
730        ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' );
731        $result->addValue( null, 'debuginfo', $debugInfo );
732    }
733
734    /**
735     * Returns the HTML to add to the page for the toolbar
736     *
737     * @param IContextSource $context
738     * @return array
739     */
740    public static function getDebugInfo( IContextSource $context ) {
741        if ( !self::$enabled ) {
742            return [];
743        }
744
745        $request = $context->getRequest();
746
747        $branch = GitInfo::currentBranch();
748        if ( GitInfo::isSHA1( $branch ) ) {
749            // If it's a detached HEAD, the SHA1 will already be
750            // included in the MW version, so don't show it.
751            $branch = false;
752        }
753
754        return [
755            'mwVersion' => MW_VERSION,
756            'phpEngine' => 'PHP',
757            'phpVersion' => PHP_VERSION,
758            'gitRevision' => GitInfo::headSHA1(),
759            'gitBranch' => $branch,
760            'gitViewUrl' => GitInfo::headViewUrl(),
761            'time' => $request->getElapsedTime(),
762            'log' => self::$log,
763            'debugLog' => self::$debug,
764            'queries' => self::$query,
765            'request' => [
766                'method' => $request->getMethod(),
767                'url' => $request->getRequestURL(),
768                'headers' => $request->getAllHeaders(),
769                'params' => $request->getValues(),
770            ],
771            'memory' => $context->getLanguage()->formatSize( memory_get_usage() ),
772            'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ),
773            'includes' => self::getFilesIncluded( $context ),
774        ];
775    }
776}