Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.93% |
161 / 237 |
|
32.00% |
8 / 25 |
CRAP | |
0.00% |
0 / 1 |
MWDebug | |
67.93% |
161 / 237 |
|
32.00% |
8 / 25 |
385.37 | |
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 / 13 |
|
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 | use MediaWiki\Context\IContextSource; |
22 | use MediaWiki\Html\Html; |
23 | use MediaWiki\Logger\LegacyLogger; |
24 | use MediaWiki\Output\OutputPage; |
25 | use MediaWiki\Parser\Sanitizer; |
26 | use MediaWiki\ResourceLoader\ResourceLoader; |
27 | use MediaWiki\Utils\GitInfo; |
28 | use Wikimedia\WrappedString; |
29 | use 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 | */ |
42 | class 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 | } |