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