MediaWiki master
MWDebug.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Debug;
22
23use LogicException;
33use ReflectionMethod;
34use UtfNormal;
35use Wikimedia\WrappedString;
36use Wikimedia\WrappedStringList;
37
49class MWDebug {
55 protected static $log = [];
56
62 protected static $debug = [];
63
69 protected static $query = [];
70
76 protected static $enabled = false;
77
84 protected static $deprecationWarnings = [];
85
89 protected static $deprecationFilters = [];
90
94 public static function setup() {
95 global $wgDebugToolbar,
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 ||
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
121 public static function init() {
122 self::$enabled = true;
123 }
124
130 public static function deinit() {
131 self::$enabled = false;
132 }
133
141 public static function addModules( OutputPage $out ) {
142 if ( self::$enabled ) {
143 $out->addModules( 'mediawiki.debug' );
144 }
145 }
146
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
172 public static function getLog() {
173 return self::$log;
174 }
175
180 public static function clearLog() {
181 self::$log = [];
182 self::$deprecationWarnings = [];
183 }
184
196 public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
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
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
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
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 ) {
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
357 $rawMsg,
358 $sendToLog,
359 $callerFunc
360 );
361 }
362
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
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
409 public static function clearDeprecationFilters() {
410 self::$deprecationFilters = [];
411 }
412
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
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
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
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
515 public static function debugMsg( $str, $context = [] ) {
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
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
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
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.
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
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
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
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
788class_alias( MWDebug::class, 'MWDebug' );
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:37
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:43
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:49
static init()
Enabled the debugger and load resource module.
Definition MWDebug.php:121
static parseCallerDescription( $msg)
Append a caller description to an error message.
Definition MWDebug.php:474
static clearDeprecationFilters()
Clear all deprecation filters.
Definition MWDebug.php:409
static query( $sql, $function, $runTime, $dbhost)
Begins profiling on a database query.
Definition MWDebug.php:579
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition MWDebug.php:715
static getLog()
Returns internal log array.
Definition MWDebug.php:172
static log( $str)
Adds a line to the log.
Definition MWDebug.php:153
static array $log
Log lines.
Definition MWDebug.php:55
static warning( $msg, $callerOffset=1, $level=E_USER_NOTICE, $log='auto')
Adds a warning entry to the log.
Definition MWDebug.php:196
static debugMsg( $str, $context=[])
This method receives messages from LoggerFactory, wfDebugLog, and MWExceptionHandler.
Definition MWDebug.php:515
static array $deprecationWarnings
Array of functions that have already been warned, formatted function-caller to prevent a buttload of ...
Definition MWDebug.php:84
static getFilesIncluded(IContextSource $context)
Returns a list of files included, along with their size.
Definition MWDebug.php:624
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition MWDebug.php:141
static array $deprecationFilters
Keys are regexes, values are optional callbacks to call if the filter is hit.
Definition MWDebug.php:89
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:263
static filterDeprecationForTest(string $regex, ?callable $callback=null)
Deprecation messages matching the supplied regex will be suppressed.
Definition MWDebug.php:397
static deprecated( $function, $version=false, $component=false, $callerOffset=2)
Show a warning that $function is deprecated.
Definition MWDebug.php:229
static getDebugInfo(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:749
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:375
static array $debug
Debug messages from wfDebug().
Definition MWDebug.php:62
static deprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition MWDebug.php:311
static getDebugHTML(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:652
static bool $enabled
Is the debugger enabled?
Definition MWDebug.php:76
static clearLog()
Clears internal log array and deprecation tracking.
Definition MWDebug.php:180
static array $query
SQL statements of the database queries.
Definition MWDebug.php:69
static deinit()
Disable the debugger.
Definition MWDebug.php:130
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition MWDebug.php:689
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
JSON formatter wrapper class.
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.
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: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.