MediaWiki master
MWDebug.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Debug;
22
23use ApiResult;
24use LogicException;
32use ReflectionMethod;
33use UtfNormal;
34use Wikimedia\WrappedString;
35use Wikimedia\WrappedStringList;
36
48class MWDebug {
54 protected static $log = [];
55
61 protected static $debug = [];
62
68 protected static $query = [];
69
75 protected static $enabled = false;
76
83 protected static $deprecationWarnings = [];
84
88 protected static $deprecationFilters = [];
89
93 public static function setup() {
94 global $wgDebugToolbar,
96
97 if (
98 // Easy to forget to falsify $wgDebugToolbar for static caches.
99 // If file cache or CDN cache is on, just disable this (DWIMD).
100 $wgUseCdn ||
102 // Keep MWDebug off on CLI. This prevents MWDebug from eating up
103 // all the memory for logging SQL queries in maintenance scripts.
104 MW_ENTRY_POINT === 'cli'
105 ) {
106 return;
107 }
108
109 if ( $wgDebugToolbar ) {
110 self::init();
111 }
112 }
113
120 public static function init() {
121 self::$enabled = true;
122 }
123
129 public static function deinit() {
130 self::$enabled = false;
131 }
132
140 public static function addModules( OutputPage $out ) {
141 if ( self::$enabled ) {
142 $out->addModules( 'mediawiki.debug' );
143 }
144 }
145
152 public static function log( $str ) {
153 if ( !self::$enabled ) {
154 return;
155 }
156 if ( !is_string( $str ) ) {
157 $str = print_r( $str, true );
158 }
159 self::$log[] = [
160 'msg' => htmlspecialchars( $str ),
161 'type' => 'log',
162 'caller' => wfGetCaller(),
163 ];
164 }
165
171 public static function getLog() {
172 return self::$log;
173 }
174
179 public static function clearLog() {
180 self::$log = [];
181 self::$deprecationWarnings = [];
182 }
183
195 public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
197
198 if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
199 $log = 'debug';
200 }
201
202 if ( $log === 'debug' ) {
203 $level = false;
204 }
205
206 $callerDescription = self::getCallerDescription( $callerOffset );
207
208 self::sendMessage(
209 self::formatCallerDescription( $msg, $callerDescription ),
210 'warning',
211 $level );
212 }
213
228 public static function deprecated( $function, $version = false,
229 $component = false, $callerOffset = 2
230 ) {
231 if ( $version ) {
232 $component = $component ?: 'MediaWiki';
233 $msg = "Use of $function was deprecated in $component $version.";
234 } else {
235 $msg = "Use of $function is deprecated.";
236 }
237 self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
238 }
239
262 public static function detectDeprecatedOverride( $instance, $class, $method, $version = false,
263 $component = false, $callerOffset = 2
264 ) {
265 $reflectionMethod = new ReflectionMethod( $instance, $method );
266 $declaringClass = $reflectionMethod->getDeclaringClass()->getName();
267
268 if ( $declaringClass === $class ) {
269 // not overridden, nothing to do
270 return false;
271 }
272
273 if ( $version ) {
274 $component = $component ?: 'MediaWiki';
275 $msg = "$declaringClass overrides $method which was deprecated in $component $version.";
276 self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
277 }
278
279 return true;
280 }
281
310 public static function deprecatedMsg( $msg, $version = false,
311 $component = false, $callerOffset = 2
312 ) {
313 if ( $callerOffset === false ) {
314 $callerFunc = '';
315 $rawMsg = $msg;
316 } else {
317 $callerDescription = self::getCallerDescription( $callerOffset );
318 $callerFunc = $callerDescription['func'];
319 $rawMsg = self::formatCallerDescription( $msg, $callerDescription );
320 }
321
322 $sendToLog = true;
323
324 // Check to see if there already was a warning about this function
325 if ( isset( self::$deprecationWarnings[$msg][$callerFunc] ) ) {
326 return;
327 } elseif ( isset( self::$deprecationWarnings[$msg] ) ) {
328 if ( self::$enabled ) {
329 $sendToLog = false;
330 } else {
331 return;
332 }
333 }
334
335 self::$deprecationWarnings[$msg][$callerFunc] = true;
336
337 if ( $version ) {
339
340 $component = $component ?: 'MediaWiki';
341 if ( $wgDeprecationReleaseLimit && $component === 'MediaWiki' ) {
342 # Strip -* off the end of $version so that branches can use the
343 # format #.##-branchname to avoid issues if the branch is merged into
344 # a version of MediaWiki later than what it was branched from
345 $comparableVersion = preg_replace( '/-.*$/', '', $version );
346
347 # If the comparableVersion is larger than our release limit then
348 # skip the warning message for the deprecation
349 if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
350 $sendToLog = false;
351 }
352 }
353 }
354
356 $rawMsg,
357 $sendToLog,
358 $callerFunc
359 );
360 }
361
374 public static function sendRawDeprecated( $msg, $sendToLog = true, $callerFunc = '' ) {
375 foreach ( self::$deprecationFilters as $filter => $callback ) {
376 if ( preg_match( $filter, $msg ) ) {
377 if ( is_callable( $callback ) ) {
378 $callback();
379 }
380 return;
381 }
382 }
383
384 if ( $sendToLog ) {
385 trigger_error( $msg, E_USER_DEPRECATED );
386 }
387 }
388
396 public static function filterDeprecationForTest(
397 string $regex, ?callable $callback = null
398 ): void {
399 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
400 throw new LogicException( __METHOD__ . ' can only be used in tests' );
401 }
402 self::$deprecationFilters[$regex] = $callback;
403 }
404
408 public static function clearDeprecationFilters() {
409 self::$deprecationFilters = [];
410 }
411
419 private static function getCallerDescription( $callerOffset ) {
420 $callers = wfDebugBacktrace();
421
422 if ( isset( $callers[$callerOffset] ) ) {
423 $callerfile = $callers[$callerOffset];
424 if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
425 $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
426 } else {
427 $file = '(internal function)';
428 }
429 } else {
430 $file = '(unknown location)';
431 }
432
433 if ( isset( $callers[$callerOffset + 1] ) ) {
434 $callerfunc = $callers[$callerOffset + 1];
435 $func = '';
436 if ( isset( $callerfunc['class'] ) ) {
437 $func .= $callerfunc['class'] . '::';
438 }
439 if ( isset( $callerfunc['function'] ) ) {
440 $func .= $callerfunc['function'];
441 }
442 } else {
443 $func = 'unknown';
444 }
445
446 return [ 'file' => $file, 'func' => $func ];
447 }
448
456 private static function formatCallerDescription( $msg, $caller ) {
457 // When changing this, update the below parseCallerDescription() method as well.
458 return $msg . ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
459 }
460
473 public static function parseCallerDescription( $msg ) {
474 $match = null;
475 preg_match( '/(.*) \[Called from ([^ ]+) in ([^ ]+) at line (\d+)\]$/', $msg, $match );
476 if ( $match ) {
477 return [
478 'message' => "{$match[1]} [Called from {$match[2]}]",
479 'func' => $match[2],
480 'file' => $match[3],
481 'line' => $match[4],
482 ];
483 } else {
484 return null;
485 }
486 }
487
496 private static function sendMessage( $msg, $group, $level ) {
497 if ( $level !== false ) {
498 trigger_error( $msg, $level );
499 }
500
501 wfDebugLog( $group, $msg );
502 }
503
514 public static function debugMsg( $str, $context = [] ) {
516
517 if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
518 if ( $context ) {
519 $prefix = '';
520 if ( isset( $context['prefix'] ) ) {
521 $prefix = $context['prefix'];
522 } elseif ( isset( $context['channel'] ) && $context['channel'] !== 'wfDebug' ) {
523 $prefix = "[{$context['channel']}] ";
524 }
525 if ( isset( $context['seconds_elapsed'] ) && isset( $context['memory_used'] ) ) {
526 $prefix .= "{$context['seconds_elapsed']} {$context['memory_used']} ";
527 }
528 $str = LegacyLogger::interpolate( $str, $context );
529 $str = $prefix . $str;
530 }
531 $str = rtrim( UtfNormal\Validator::cleanUp( $str ) );
532 self::$debug[] = $str;
533 if ( isset( $context['channel'] ) && $context['channel'] === 'error' ) {
534 $message = isset( $context['exception'] )
535 ? $context['exception']->getMessage()
536 : $str;
537 $real = self::parseCallerDescription( $message );
538 if ( $real ) {
539 // from wfLogWarning()
540 $message = $real['message'];
541 $caller = $real['func'];
542 } else {
543 $trace = isset( $context['exception'] ) ? $context['exception']->getTrace() : [];
544 if ( ( $trace[5]['function'] ?? null ) === 'wfDeprecated' ) {
545 // from MWExceptionHandler/trigger_error/MWDebug/MWDebug/MWDebug/wfDeprecated()
546 $offset = 6;
547 } elseif ( ( $trace[1]['function'] ?? null ) === 'trigger_error' ) {
548 // from trigger_error
549 $offset = 2;
550 } else {
551 // built-in PHP error
552 $offset = 1;
553 }
554 $frame = $trace[$offset] ?? $trace[0];
555 $caller = ( isset( $frame['class'] ) ? $frame['class'] . '::' : '' )
556 . $frame['function'];
557 }
558
559 self::$log[] = [
560 'msg' => htmlspecialchars( $message ),
561 'type' => 'warn',
562 'caller' => $caller,
563 ];
564 }
565 }
566 }
567
578 public static function query( $sql, $function, $runTime, $dbhost ) {
579 if ( !self::$enabled ) {
580 return false;
581 }
582
583 // Replace invalid UTF-8 chars with a square UTF-8 character
584 // This prevents json_encode from erroring out due to binary SQL data
585 $sql = preg_replace(
586 '/(
587 [\xC0-\xC1] # Invalid UTF-8 Bytes
588 | [\xF5-\xFF] # Invalid UTF-8 Bytes
589 | \xE0[\x80-\x9F] # Overlong encoding of prior code point
590 | \xF0[\x80-\x8F] # Overlong encoding of prior code point
591 | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
592 | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
593 | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
594 | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
595 | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]
596 | [\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
597 | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
598 | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
599 | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
600 )/x',
601 '■',
602 $sql
603 );
604
605 // last check for invalid utf8
606 $sql = UtfNormal\Validator::cleanUp( $sql );
607
608 self::$query[] = [
609 'sql' => "$dbhost: $sql",
610 'function' => $function,
611 'time' => $runTime,
612 ];
613
614 return true;
615 }
616
623 protected static function getFilesIncluded( IContextSource $context ) {
624 $files = get_included_files();
625 $fileList = [];
626 foreach ( $files as $file ) {
627 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
628 $size = @filesize( $file );
629 if ( $size === false ) {
630 // Certain files that have been included might then be deleted. This is especially likely to happen
631 // in tests, see T351986.
632 // Just use a size of 0, but include these files here to try and be as useful as possible.
633 $size = 0;
634 }
635 $fileList[] = [
636 'name' => $file,
637 'size' => $context->getLanguage()->formatSize( $size ),
638 ];
639 }
640
641 return $fileList;
642 }
643
651 public static function getDebugHTML( IContextSource $context ) {
652 global $wgDebugComments;
653
654 $html = [];
655
656 if ( self::$enabled ) {
657 self::log( 'MWDebug output complete' );
658 $debugInfo = self::getDebugInfo( $context );
659
660 // Cannot use OutputPage::addJsConfigVars because those are already outputted
661 // by the time this method is called.
663 ResourceLoader::makeConfigSetScript( [ 'debugInfo' => $debugInfo ] )
664 );
665 }
666
667 if ( $wgDebugComments ) {
668 $html[] = '<!-- Debug output:';
669 foreach ( self::$debug as $line ) {
670 $html[] = htmlspecialchars( $line, ENT_NOQUOTES );
671 }
672 $html[] = '-->';
673 }
674
675 return WrappedString::join( "\n", $html );
676 }
677
686 public static function getHTMLDebugLog() {
687 global $wgShowDebug;
688
689 $html = [];
690 if ( $wgShowDebug ) {
691 $html[] = Html::openElement( 'div', [ 'id' => 'mw-html-debug-log' ] );
692 $html[] = "<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">";
693
694 foreach ( self::$debug as $line ) {
695 $display = nl2br( htmlspecialchars( trim( $line ) ) );
696
697 $html[] = "<li><code>$display</code></li>";
698 }
699
700 $html[] = '</ul>';
701 $html[] = '</div>';
702 }
703 return WrappedString::join( "\n", $html );
704 }
705
712 public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
713 if ( !self::$enabled ) {
714 return;
715 }
716
717 // output errors as debug info, when display_errors is on
718 // this is necessary for all non html output of the api, because that clears all errors first
719 $obContents = ob_get_contents();
720 if ( $obContents ) {
721 $obContentArray = explode( '<br />', $obContents );
722 foreach ( $obContentArray as $obContent ) {
723 if ( trim( $obContent ) ) {
724 self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
725 }
726 }
727 }
728
729 self::log( 'MWDebug output complete' );
730 $debugInfo = self::getDebugInfo( $context );
731
732 ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' );
733 ApiResult::setIndexedTagName( $debugInfo['log'], 'line' );
734 ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' );
735 ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' );
736 ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' );
737 $result->addValue( null, 'debuginfo', $debugInfo );
738 }
739
746 public static function getDebugInfo( IContextSource $context ) {
747 if ( !self::$enabled ) {
748 return [];
749 }
750
751 $request = $context->getRequest();
752
753 $branch = GitInfo::currentBranch();
754 if ( GitInfo::isSHA1( $branch ) ) {
755 // If it's a detached HEAD, the SHA1 will already be
756 // included in the MW version, so don't show it.
757 $branch = false;
758 }
759
760 return [
761 'mwVersion' => MW_VERSION,
762 'phpEngine' => 'PHP',
763 'phpVersion' => PHP_VERSION,
764 'gitRevision' => GitInfo::headSHA1(),
765 'gitBranch' => $branch,
766 'gitViewUrl' => GitInfo::headViewUrl(),
767 'time' => $request->getElapsedTime(),
768 'log' => self::$log,
769 'debugLog' => self::$debug,
770 'queries' => self::$query,
771 'request' => [
772 'method' => $request->getMethod(),
773 'url' => $request->getRequestURL(),
774 'headers' => $request->getAllHeaders(),
775 'params' => $request->getValues(),
776 ],
777 'memory' => $context->getLanguage()->formatSize( memory_get_usage() ),
778 'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ),
779 'includes' => self::getFilesIncluded( $context ),
780 ];
781 }
782}
783
785class_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:35
addValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Debug toolbar.
Definition MWDebug.php:48
static init()
Enabled the debugger and load resource module.
Definition MWDebug.php:120
static parseCallerDescription( $msg)
Append a caller description to an error message.
Definition MWDebug.php:473
static clearDeprecationFilters()
Clear all deprecation filters.
Definition MWDebug.php:408
static query( $sql, $function, $runTime, $dbhost)
Begins profiling on a database query.
Definition MWDebug.php:578
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition MWDebug.php:712
static getLog()
Returns internal log array.
Definition MWDebug.php:171
static log( $str)
Adds a line to the log.
Definition MWDebug.php:152
static array $log
Log lines.
Definition MWDebug.php:54
static warning( $msg, $callerOffset=1, $level=E_USER_NOTICE, $log='auto')
Adds a warning entry to the log.
Definition MWDebug.php:195
static debugMsg( $str, $context=[])
This method receives messages from LoggerFactory, wfDebugLog, and MWExceptionHandler.
Definition MWDebug.php:514
static array $deprecationWarnings
Array of functions that have already been warned, formatted function-caller to prevent a buttload of ...
Definition MWDebug.php:83
static getFilesIncluded(IContextSource $context)
Returns a list of files included, along with their size.
Definition MWDebug.php:623
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition MWDebug.php:140
static array $deprecationFilters
Keys are regexes, values are optional callbacks to call if the filter is hit.
Definition MWDebug.php:88
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:262
static filterDeprecationForTest(string $regex, ?callable $callback=null)
Deprecation messages matching the supplied regex will be suppressed.
Definition MWDebug.php:396
static deprecated( $function, $version=false, $component=false, $callerOffset=2)
Show a warning that $function is deprecated.
Definition MWDebug.php:228
static getDebugInfo(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:746
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:374
static array $debug
Debug messages from wfDebug().
Definition MWDebug.php:61
static deprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition MWDebug.php:310
static getDebugHTML(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:651
static bool $enabled
Is the debugger enabled?
Definition MWDebug.php:75
static clearLog()
Clears internal log array and deprecation tracking.
Definition MWDebug.php:179
static array $query
SQL statements of the database queries.
Definition MWDebug.php:68
static deinit()
Disable the debugger.
Definition MWDebug.php:129
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition MWDebug.php:686
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.
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
static makeConfigSetScript(array $configuration)
Return JS code which will set the MediaWiki configuration array to the given value.
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.