MediaWiki master
MWDebug.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Debug;
8
9use LogicException;
19use ReflectionMethod;
20use UtfNormal;
21use Wikimedia\WrappedString;
22use Wikimedia\WrappedStringList;
23
35class MWDebug {
41 protected static $log = [];
42
48 protected static $debug = [];
49
55 protected static $query = [];
56
62 protected static $enabled = false;
63
70 protected static $deprecationWarnings = [];
71
75 protected static $deprecationFilters = [];
76
80 public static function setup() {
81 global $wgDebugToolbar,
83
84 if (
85 // Easy to forget to falsify $wgDebugToolbar for static caches.
86 // If file cache or CDN cache is on, just disable this (DWIMD).
87 $wgUseCdn ||
89 // Keep MWDebug off on CLI. This prevents MWDebug from eating up
90 // all the memory for logging SQL queries in maintenance scripts.
91 MW_ENTRY_POINT === 'cli'
92 ) {
93 return;
94 }
95
96 if ( $wgDebugToolbar ) {
97 self::init();
98 }
99 }
100
107 public static function init() {
108 self::$enabled = true;
109 }
110
116 public static function deinit() {
117 self::$enabled = false;
118 }
119
127 public static function addModules( OutputPage $out ) {
128 if ( self::$enabled ) {
129 $out->addModules( 'mediawiki.debug' );
130 }
131 }
132
139 public static function log( $str ) {
140 if ( !self::$enabled ) {
141 return;
142 }
143 if ( !is_string( $str ) ) {
144 $str = print_r( $str, true );
145 }
146 self::$log[] = [
147 'msg' => htmlspecialchars( $str ),
148 'type' => 'log',
149 'caller' => wfGetCaller(),
150 ];
151 }
152
158 public static function getLog() {
159 return self::$log;
160 }
161
166 public static function clearLog() {
167 self::$log = [];
168 self::$deprecationWarnings = [];
169 }
170
182 public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
184
185 if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
186 $log = 'debug';
187 }
188
189 if ( $log === 'debug' ) {
190 $level = false;
191 }
192
193 $callerDescription = self::getCallerDescription( $callerOffset );
194
195 self::sendMessage(
196 self::formatCallerDescription( $msg, $callerDescription ),
197 'warning',
198 $level );
199 }
200
215 public static function deprecated( $function, $version = false,
216 $component = false, $callerOffset = 2
217 ) {
218 if ( $version ) {
219 $component = $component ?: 'MediaWiki';
220 $msg = "Use of $function was deprecated in $component $version.";
221 } else {
222 $msg = "Use of $function is deprecated.";
223 }
224 self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
225 }
226
249 public static function detectDeprecatedOverride( $instance, $class, $method, $version = false,
250 $component = false, $callerOffset = 2
251 ) {
252 $reflectionMethod = new ReflectionMethod( $instance, $method );
253 $declaringClass = $reflectionMethod->getDeclaringClass()->getName();
254
255 if ( $declaringClass === $class ) {
256 // not overridden, nothing to do
257 return false;
258 }
259
260 if ( $version ) {
261 $component = $component ?: 'MediaWiki';
262 $msg = "$declaringClass overrides $method which was deprecated in $component $version.";
263 self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
264 }
265
266 return true;
267 }
268
297 public static function deprecatedMsg( $msg, $version = false,
298 $component = false, $callerOffset = 2
299 ) {
300 if ( $callerOffset === false ) {
301 $callerFunc = '';
302 $rawMsg = $msg;
303 } else {
304 $callerDescription = self::getCallerDescription( $callerOffset );
305 $callerFunc = $callerDescription['func'];
306 $rawMsg = self::formatCallerDescription( $msg, $callerDescription );
307 }
308
309 $sendToLog = true;
310
311 // Check to see if there already was a warning about this function
312 if ( isset( self::$deprecationWarnings[$msg][$callerFunc] ) ) {
313 return;
314 } elseif ( isset( self::$deprecationWarnings[$msg] ) ) {
315 if ( self::$enabled ) {
316 $sendToLog = false;
317 } else {
318 return;
319 }
320 }
321
322 self::$deprecationWarnings[$msg][$callerFunc] = true;
323
324 if ( $version ) {
326
327 $component = $component ?: 'MediaWiki';
328 if ( $wgDeprecationReleaseLimit && $component === 'MediaWiki' ) {
329 # Strip -* off the end of $version so that branches can use the
330 # format #.##-branchname to avoid issues if the branch is merged into
331 # a version of MediaWiki later than what it was branched from
332 $comparableVersion = preg_replace( '/-.*$/', '', $version );
333
334 # If the comparableVersion is larger than our release limit then
335 # skip the warning message for the deprecation
336 if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
337 $sendToLog = false;
338 }
339 }
340 }
341
343 $rawMsg,
344 $sendToLog,
345 $callerFunc
346 );
347 }
348
361 public static function sendRawDeprecated( $msg, $sendToLog = true, $callerFunc = '' ) {
362 foreach ( self::$deprecationFilters as $filter => $callback ) {
363 if ( preg_match( $filter, $msg ) ) {
364 if ( is_callable( $callback ) ) {
365 $callback();
366 }
367 return;
368 }
369 }
370
371 if ( $sendToLog ) {
372 trigger_error( $msg, E_USER_DEPRECATED );
373 }
374 }
375
383 public static function filterDeprecationForTest(
384 string $regex, ?callable $callback = null
385 ): void {
386 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
387 throw new LogicException( __METHOD__ . ' can only be used in tests' );
388 }
389 self::$deprecationFilters[$regex] = $callback;
390 }
391
395 public static function clearDeprecationFilters() {
396 self::$deprecationFilters = [];
397 }
398
406 private static function getCallerDescription( $callerOffset ) {
407 $callers = wfDebugBacktrace();
408
409 if ( isset( $callers[$callerOffset] ) ) {
410 $callerfile = $callers[$callerOffset];
411 if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
412 $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
413 } else {
414 $file = '(internal function)';
415 }
416 } else {
417 $file = '(unknown location)';
418 }
419
420 if ( isset( $callers[$callerOffset + 1] ) ) {
421 $callerfunc = $callers[$callerOffset + 1];
422 $func = '';
423 if ( isset( $callerfunc['class'] ) ) {
424 $func .= $callerfunc['class'] . '::';
425 }
426 if ( isset( $callerfunc['function'] ) ) {
427 $func .= $callerfunc['function'];
428 }
429 } else {
430 $func = 'unknown';
431 }
432
433 return [ 'file' => $file, 'func' => $func ];
434 }
435
443 private static function formatCallerDescription( $msg, $caller ) {
444 // When changing this, update the below parseCallerDescription() method as well.
445 return $msg . ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
446 }
447
461 public static function parseCallerDescription( $msg ) {
462 $match = null;
463 preg_match( '/(.*) \[Called from ([^ ]+) in ([^ ]+) at line (\d+)\]$/', $msg, $match );
464 if ( $match ) {
465 return [
466 'message' => "{$match[1]} [Called from {$match[2]}]",
467 'func' => $match[2],
468 'file' => $match[3],
469 'line' => (int)$match[4],
470 ];
471 } else {
472 return null;
473 }
474 }
475
484 private static function sendMessage( $msg, $group, $level ) {
485 if ( $level !== false ) {
486 trigger_error( $msg, $level );
487 }
488
489 wfDebugLog( $group, $msg );
490 }
491
502 public static function debugMsg( $str, $context = [] ) {
504
505 if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
506 if ( $context ) {
507 $prefix = '';
508 if ( isset( $context['prefix'] ) ) {
509 $prefix = $context['prefix'];
510 } elseif ( isset( $context['channel'] ) && $context['channel'] !== 'wfDebug' ) {
511 $prefix = "[{$context['channel']}] ";
512 }
513 if ( isset( $context['seconds_elapsed'] ) && isset( $context['memory_used'] ) ) {
514 $prefix .= "{$context['seconds_elapsed']} {$context['memory_used']} ";
515 }
516 $str = LegacyLogger::interpolate( $str, $context );
517 $str = $prefix . $str;
518 }
519 $str = rtrim( UtfNormal\Validator::cleanUp( $str ) );
520 self::$debug[] = $str;
521 if ( isset( $context['channel'] ) && $context['channel'] === 'error' ) {
522 $message = isset( $context['exception'] )
523 ? $context['exception']->getMessage()
524 : $str;
525 $real = self::parseCallerDescription( $message );
526 if ( $real ) {
527 // from wfLogWarning()
528 $message = $real['message'];
529 $caller = $real['func'];
530 } else {
531 $trace = isset( $context['exception'] ) ? $context['exception']->getTrace() : [];
532 if ( ( $trace[5]['function'] ?? null ) === 'wfDeprecated' ) {
533 // from MWExceptionHandler/trigger_error/MWDebug/MWDebug/MWDebug/wfDeprecated()
534 $offset = 6;
535 } elseif ( ( $trace[1]['function'] ?? null ) === 'trigger_error' ) {
536 // from trigger_error
537 $offset = 2;
538 } else {
539 // built-in PHP error
540 $offset = 1;
541 }
542 $frame = $trace[$offset] ?? $trace[0];
543 $caller = ( isset( $frame['class'] ) ? $frame['class'] . '::' : '' )
544 . $frame['function'];
545 }
546
547 self::$log[] = [
548 'msg' => htmlspecialchars( $message ),
549 'type' => 'warn',
550 'caller' => $caller,
551 ];
552 }
553 }
554 }
555
566 public static function query( $sql, $function, $runTime, $dbhost ) {
567 if ( !self::$enabled ) {
568 return false;
569 }
570
571 // Replace invalid UTF-8 chars with a square UTF-8 character
572 // This prevents json_encode from erroring out due to binary SQL data
573 $sql = preg_replace(
574 '/(
575 [\xC0-\xC1] # Invalid UTF-8 Bytes
576 | [\xF5-\xFF] # Invalid UTF-8 Bytes
577 | \xE0[\x80-\x9F] # Overlong encoding of prior code point
578 | \xF0[\x80-\x8F] # Overlong encoding of prior code point
579 | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
580 | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
581 | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
582 | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
583 | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]
584 | [\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
585 | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
586 | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
587 | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
588 )/x',
589 '■',
590 $sql
591 );
592
593 // last check for invalid utf8
594 $sql = UtfNormal\Validator::cleanUp( $sql );
595
596 self::$query[] = [
597 'sql' => "$dbhost: $sql",
598 'function' => $function,
599 'time' => $runTime,
600 ];
601
602 return true;
603 }
604
611 protected static function getFilesIncluded( IContextSource $context ) {
612 $files = get_included_files();
613 $fileList = [];
614 foreach ( $files as $file ) {
615 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
616 $size = @filesize( $file );
617 if ( $size === false ) {
618 // Certain files that have been included might then be deleted. This is especially likely to happen
619 // in tests, see T351986.
620 // Just use a size of 0, but include these files here to try and be as useful as possible.
621 $size = 0;
622 }
623 $fileList[] = [
624 'name' => $file,
625 'size' => $context->getLanguage()->formatSize( $size ),
626 ];
627 }
628
629 return $fileList;
630 }
631
639 public static function getDebugHTML( IContextSource $context ) {
640 global $wgDebugComments;
641
642 $html = [];
643
644 if ( self::$enabled ) {
645 self::log( 'MWDebug output complete' );
646 $debugInfo = self::getDebugInfo( $context );
647
648 // Cannot use OutputPage::addJsConfigVars because those are already outputted
649 // by the time this method is called.
651 'mw.config.set('
652 . FormatJson::encode( [ 'debugInfo' => $debugInfo ] )
653 . ');'
654 );
655 }
656
657 if ( $wgDebugComments ) {
658 $html[] = '<!-- Debug output:';
659 foreach ( self::$debug as $line ) {
660 $html[] = htmlspecialchars( $line, ENT_NOQUOTES );
661 }
662 $html[] = '-->';
663 }
664
665 return WrappedString::join( "\n", $html );
666 }
667
676 public static function getHTMLDebugLog() {
677 global $wgShowDebug;
678
679 $html = [];
680 if ( $wgShowDebug ) {
681 $html[] = Html::openElement( 'div', [ 'id' => 'mw-html-debug-log' ] );
682 $html[] = "<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">";
683
684 foreach ( self::$debug as $line ) {
685 $display = nl2br( htmlspecialchars( trim( $line ) ) );
686
687 $html[] = "<li><code>$display</code></li>";
688 }
689
690 $html[] = '</ul>';
691 $html[] = '</div>';
692 }
693 return WrappedString::join( "\n", $html );
694 }
695
702 public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
703 if ( !self::$enabled ) {
704 return;
705 }
706
707 // output errors as debug info, when display_errors is on
708 // this is necessary for all non html output of the api, because that clears all errors first
709 $obContents = ob_get_contents();
710 if ( $obContents ) {
711 $obContentArray = explode( '<br />', $obContents );
712 foreach ( $obContentArray as $obContent ) {
713 if ( trim( $obContent ) ) {
714 self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
715 }
716 }
717 }
718
719 self::log( 'MWDebug output complete' );
720 $debugInfo = self::getDebugInfo( $context );
721
722 ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' );
723 ApiResult::setIndexedTagName( $debugInfo['log'], 'line' );
724 ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' );
725 ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' );
726 ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' );
727 $result->addValue( null, 'debuginfo', $debugInfo );
728 }
729
736 public static function getDebugInfo( IContextSource $context ) {
737 if ( !self::$enabled ) {
738 return [];
739 }
740
741 $request = $context->getRequest();
742
743 $branch = GitInfo::currentBranch();
744 if ( GitInfo::isSHA1( $branch ) ) {
745 // If it's a detached HEAD, the SHA1 will already be
746 // included in the MW version, so don't show it.
747 $branch = false;
748 }
749
750 return [
751 'mwVersion' => MW_VERSION,
752 'phpEngine' => 'PHP',
753 'phpVersion' => PHP_VERSION,
754 'gitRevision' => GitInfo::headSHA1(),
755 'gitBranch' => $branch,
756 'gitViewUrl' => GitInfo::headViewUrl(),
757 'time' => $request->getElapsedTime(),
758 'log' => self::$log,
759 'debugLog' => self::$debug,
760 'queries' => self::$query,
761 'request' => [
762 'method' => $request->getMethod(),
763 'url' => $request->getRequestURL(),
764 'headers' => $request->getAllHeaders(),
765 'params' => $request->getValues(),
766 ],
767 'memory' => $context->getLanguage()->formatSize( memory_get_usage() ),
768 'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ),
769 'includes' => self::getFilesIncluded( $context ),
770 ];
771 }
772}
773
775class_alias( MWDebug::class, 'MWDebug' );
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:23
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfDebugBacktrace( $limit=0)
Safety wrapper for debug_backtrace().
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
const MW_ENTRY_POINT
Definition api.php:21
This class represents the result of the API operations.
Definition ApiResult.php:34
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
addValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path.
Debug toolbar.
Definition MWDebug.php:35
static init()
Enabled the debugger and load resource module.
Definition MWDebug.php:107
static parseCallerDescription( $msg)
Append a caller description to an error message.
Definition MWDebug.php:461
static clearDeprecationFilters()
Clear all deprecation filters.
Definition MWDebug.php:395
static query( $sql, $function, $runTime, $dbhost)
Begins profiling on a database query.
Definition MWDebug.php:566
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition MWDebug.php:702
static getLog()
Returns internal log array.
Definition MWDebug.php:158
static log( $str)
Adds a line to the log.
Definition MWDebug.php:139
static array $log
Log lines.
Definition MWDebug.php:41
static warning( $msg, $callerOffset=1, $level=E_USER_NOTICE, $log='auto')
Adds a warning entry to the log.
Definition MWDebug.php:182
static debugMsg( $str, $context=[])
This method receives messages from LoggerFactory, wfDebugLog, and MWExceptionHandler.
Definition MWDebug.php:502
static array $deprecationWarnings
Array of functions that have already been warned, formatted function-caller to prevent a buttload of ...
Definition MWDebug.php:70
static getFilesIncluded(IContextSource $context)
Returns a list of files included, along with their size.
Definition MWDebug.php:611
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition MWDebug.php:127
static array $deprecationFilters
Keys are regexes, values are optional callbacks to call if the filter is hit.
Definition MWDebug.php:75
static detectDeprecatedOverride( $instance, $class, $method, $version=false, $component=false, $callerOffset=2)
Show a warning if $method declared in $class is overridden in $instance.
Definition MWDebug.php:249
static filterDeprecationForTest(string $regex, ?callable $callback=null)
Deprecation messages matching the supplied regex will be suppressed.
Definition MWDebug.php:383
static deprecated( $function, $version=false, $component=false, $callerOffset=2)
Show a warning that $function is deprecated.
Definition MWDebug.php:215
static getDebugInfo(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:736
static sendRawDeprecated( $msg, $sendToLog=true, $callerFunc='')
Send a raw deprecation message to the log and the debug toolbar, without filtering of duplicate messa...
Definition MWDebug.php:361
static array $debug
Debug messages from wfDebug().
Definition MWDebug.php:48
static deprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition MWDebug.php:297
static getDebugHTML(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:639
static bool $enabled
Is the debugger enabled?
Definition MWDebug.php:62
static clearLog()
Clears internal log array and deprecation tracking.
Definition MWDebug.php:166
static array $query
SQL statements of the database queries.
Definition MWDebug.php:55
static deinit()
Disable the debugger.
Definition MWDebug.php:116
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition MWDebug.php:676
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
JSON formatter wrapper class.
PSR-3 logger that mimics the historic implementation of MediaWiki's former wfErrorLog logging impleme...
static interpolate( $message, array $context)
Interpolate placeholders in logging message.
This is one of the Core classes and should be read at least once by any new developers.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:32
ResourceLoader is a loading system for JavaScript and CSS resources.
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
Fetch status information from a local git repository.
Definition GitInfo.php:33
$wgUseCdn
Config variable stub for the UseCdn setting, for use by phpdoc and IDEs.
$wgDeprecationReleaseLimit
Config variable stub for the DeprecationReleaseLimit setting, for use by phpdoc and IDEs.
$wgDebugComments
Config variable stub for the DebugComments setting, for use by phpdoc and IDEs.
$wgShowDebug
Config variable stub for the ShowDebug setting, for use by phpdoc and IDEs.
$wgDebugToolbar
Config variable stub for the DebugToolbar setting, for use by phpdoc and IDEs.
$wgDevelopmentWarnings
Config variable stub for the DevelopmentWarnings setting, for use by phpdoc and IDEs.
$wgUseFileCache
Config variable stub for the UseFileCache setting, for use by phpdoc and IDEs.
Interface for objects which can provide a MediaWiki context on request.