MediaWiki master
MWDebug.php
Go to the documentation of this file.
1<?php
28use Wikimedia\WrappedString;
29use Wikimedia\WrappedStringList;
30
42class MWDebug {
48 protected static $log = [];
49
55 protected static $debug = [];
56
62 protected static $query = [];
63
69 protected static $enabled = false;
70
77 protected static $deprecationWarnings = [];
78
82 protected static $deprecationFilters = [];
83
87 public static function setup() {
88 global $wgDebugToolbar,
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 ||
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
114 public static function init() {
115 self::$enabled = true;
116 }
117
123 public static function deinit() {
124 self::$enabled = false;
125 }
126
134 public static function addModules( OutputPage $out ) {
135 if ( self::$enabled ) {
136 $out->addModules( 'mediawiki.debug' );
137 }
138 }
139
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
165 public static function getLog() {
166 return self::$log;
167 }
168
173 public static function clearLog() {
174 self::$log = [];
175 self::$deprecationWarnings = [];
176 }
177
189 public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
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
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
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
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 ) {
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
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
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
402 public static function clearDeprecationFilters() {
403 self::$deprecationFilters = [];
404 }
405
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
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
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
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
508 public static function debugMsg( $str, $context = [] ) {
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
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
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
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
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
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
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}
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
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:81
const MW_ENTRY_POINT
Definition api.php:35
This class represents the result of the API operations.
Definition ApiResult.php:35
addValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path.
Debug toolbar.
Definition MWDebug.php:42
static warning( $msg, $callerOffset=1, $level=E_USER_NOTICE, $log='auto')
Adds a warning entry to the log.
Definition MWDebug.php:189
static getFilesIncluded(IContextSource $context)
Returns a list of files included, along with their size.
Definition MWDebug.php:617
static array $deprecationFilters
Keys are regexes, values are optional callbacks to call if the filter is hit.
Definition MWDebug.php:82
static bool $enabled
Is the debugger enabled?
Definition MWDebug.php:69
static setup()
Definition MWDebug.php:87
static filterDeprecationForTest(string $regex, ?callable $callback=null)
Deprecation messages matching the supplied regex will be suppressed.
Definition MWDebug.php:390
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition MWDebug.php:134
static parseCallerDescription( $msg)
Append a caller description to an error message.
Definition MWDebug.php:467
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition MWDebug.php:706
static array $deprecationWarnings
Array of functions that have already been warned, formatted function-caller to prevent a buttload of ...
Definition MWDebug.php:77
static clearLog()
Clears internal log array and deprecation tracking.
Definition MWDebug.php:173
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:368
static deinit()
Disable the debugger.
Definition MWDebug.php:123
static query( $sql, $function, $runTime, $dbhost)
Begins profiling on a database query.
Definition MWDebug.php:572
static getLog()
Returns internal log array.
Definition MWDebug.php:165
static array $debug
Debug messages from wfDebug().
Definition MWDebug.php:55
static log( $str)
Adds a line to the log.
Definition MWDebug.php:146
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:256
static getDebugInfo(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:740
static debugMsg( $str, $context=[])
This method receives messages from LoggerFactory, wfDebugLog, and MWExceptionHandler.
Definition MWDebug.php:508
static clearDeprecationFilters()
Clear all deprecation filters.
Definition MWDebug.php:402
static array $log
Log lines.
Definition MWDebug.php:48
static deprecated( $function, $version=false, $component=false, $callerOffset=2)
Show a warning that $function is deprecated.
Definition MWDebug.php:222
static array $query
SQL statements of the database queries.
Definition MWDebug.php:62
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition MWDebug.php:680
static init()
Enabled the debugger and load resource module.
Definition MWDebug.php:114
static deprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition MWDebug.php:304
static getDebugHTML(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:645
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
PSR-3 logger that mimics the historic implementation of MediaWiki's former wfErrorLog logging impleme...
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:46
ResourceLoader is a loading system for JavaScript and CSS resources.
Fetch status information from a local git repository.
Definition GitInfo.php:47
$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.