MediaWiki master
MWDebug.php
Go to the documentation of this file.
1<?php
27use Wikimedia\WrappedString;
28use Wikimedia\WrappedStringList;
29
41class MWDebug {
47 protected static $log = [];
48
54 protected static $debug = [];
55
61 protected static $query = [];
62
68 protected static $enabled = false;
69
76 protected static $deprecationWarnings = [];
77
81 protected static $deprecationFilters = [];
82
86 public static function setup() {
87 global $wgDebugToolbar,
89
90 if (
91 // Easy to forget to falsify $wgDebugToolbar for static caches.
92 // If file cache or CDN cache is on, just disable this (DWIMD).
93 $wgUseCdn ||
95 // Keep MWDebug off on CLI. This prevents MWDebug from eating up
96 // all the memory for logging SQL queries in maintenance scripts.
98 ) {
99 return;
100 }
101
102 if ( $wgDebugToolbar ) {
103 self::init();
104 }
105 }
106
113 public static function init() {
114 self::$enabled = true;
115 }
116
122 public static function deinit() {
123 self::$enabled = false;
124 }
125
133 public static function addModules( OutputPage $out ) {
134 if ( self::$enabled ) {
135 $out->addModules( 'mediawiki.debug' );
136 }
137 }
138
145 public static function log( $str ) {
146 if ( !self::$enabled ) {
147 return;
148 }
149 if ( !is_string( $str ) ) {
150 $str = print_r( $str, true );
151 }
152 self::$log[] = [
153 'msg' => htmlspecialchars( $str ),
154 'type' => 'log',
155 'caller' => wfGetCaller(),
156 ];
157 }
158
164 public static function getLog() {
165 return self::$log;
166 }
167
172 public static function clearLog() {
173 self::$log = [];
174 self::$deprecationWarnings = [];
175 }
176
188 public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
190
191 if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
192 $log = 'debug';
193 }
194
195 if ( $log === 'debug' ) {
196 $level = false;
197 }
198
199 $callerDescription = self::getCallerDescription( $callerOffset );
200
201 self::sendMessage(
202 self::formatCallerDescription( $msg, $callerDescription ),
203 'warning',
204 $level );
205 }
206
221 public static function deprecated( $function, $version = false,
222 $component = false, $callerOffset = 2
223 ) {
224 if ( $version ) {
225 $component = $component ?: 'MediaWiki';
226 $msg = "Use of $function was deprecated in $component $version.";
227 } else {
228 $msg = "Use of $function is deprecated.";
229 }
230 self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
231 }
232
255 public static function detectDeprecatedOverride( $instance, $class, $method, $version = false,
256 $component = false, $callerOffset = 2
257 ) {
258 $reflectionMethod = new ReflectionMethod( $instance, $method );
259 $declaringClass = $reflectionMethod->getDeclaringClass()->getName();
260
261 if ( $declaringClass === $class ) {
262 // not overridden, nothing to do
263 return false;
264 }
265
266 if ( $version ) {
267 $component = $component ?: 'MediaWiki';
268 $msg = "$declaringClass overrides $method which was deprecated in $component $version.";
269 self::deprecatedMsg( $msg, $version, $component, $callerOffset + 1 );
270 }
271
272 return true;
273 }
274
303 public static function deprecatedMsg( $msg, $version = false,
304 $component = false, $callerOffset = 2
305 ) {
306 if ( $callerOffset === false ) {
307 $callerFunc = '';
308 $rawMsg = $msg;
309 } else {
310 $callerDescription = self::getCallerDescription( $callerOffset );
311 $callerFunc = $callerDescription['func'];
312 $rawMsg = self::formatCallerDescription( $msg, $callerDescription );
313 }
314
315 $sendToLog = true;
316
317 // Check to see if there already was a warning about this function
318 if ( isset( self::$deprecationWarnings[$msg][$callerFunc] ) ) {
319 return;
320 } elseif ( isset( self::$deprecationWarnings[$msg] ) ) {
321 if ( self::$enabled ) {
322 $sendToLog = false;
323 } else {
324 return;
325 }
326 }
327
328 self::$deprecationWarnings[$msg][$callerFunc] = true;
329
330 if ( $version ) {
332
333 $component = $component ?: 'MediaWiki';
334 if ( $wgDeprecationReleaseLimit && $component === 'MediaWiki' ) {
335 # Strip -* off the end of $version so that branches can use the
336 # format #.##-branchname to avoid issues if the branch is merged into
337 # a version of MediaWiki later than what it was branched from
338 $comparableVersion = preg_replace( '/-.*$/', '', $version );
339
340 # If the comparableVersion is larger than our release limit then
341 # skip the warning message for the deprecation
342 if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
343 $sendToLog = false;
344 }
345 }
346 }
347
348 self::sendRawDeprecated(
349 $rawMsg,
350 $sendToLog,
351 $callerFunc
352 );
353 }
354
367 public static function sendRawDeprecated( $msg, $sendToLog = true, $callerFunc = '' ) {
368 foreach ( self::$deprecationFilters as $filter => $callback ) {
369 if ( preg_match( $filter, $msg ) ) {
370 if ( is_callable( $callback ) ) {
371 $callback();
372 }
373 return;
374 }
375 }
376
377 if ( $sendToLog ) {
378 trigger_error( $msg, E_USER_DEPRECATED );
379 }
380 }
381
389 public static function filterDeprecationForTest(
390 string $regex, ?callable $callback = null
391 ): void {
392 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
393 throw new LogicException( __METHOD__ . ' can only be used in tests' );
394 }
395 self::$deprecationFilters[$regex] = $callback;
396 }
397
401 public static function clearDeprecationFilters() {
402 self::$deprecationFilters = [];
403 }
404
412 private static function getCallerDescription( $callerOffset ) {
413 $callers = wfDebugBacktrace();
414
415 if ( isset( $callers[$callerOffset] ) ) {
416 $callerfile = $callers[$callerOffset];
417 if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
418 $file = $callerfile['file'] . ' at line ' . $callerfile['line'];
419 } else {
420 $file = '(internal function)';
421 }
422 } else {
423 $file = '(unknown location)';
424 }
425
426 if ( isset( $callers[$callerOffset + 1] ) ) {
427 $callerfunc = $callers[$callerOffset + 1];
428 $func = '';
429 if ( isset( $callerfunc['class'] ) ) {
430 $func .= $callerfunc['class'] . '::';
431 }
432 if ( isset( $callerfunc['function'] ) ) {
433 $func .= $callerfunc['function'];
434 }
435 } else {
436 $func = 'unknown';
437 }
438
439 return [ 'file' => $file, 'func' => $func ];
440 }
441
449 private static function formatCallerDescription( $msg, $caller ) {
450 // When changing this, update the below parseCallerDescription() method as well.
451 return $msg . ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
452 }
453
466 public static function parseCallerDescription( $msg ) {
467 $match = null;
468 preg_match( '/(.*) \[Called from ([^ ]+) in ([^ ]+) at line (\d+)\]$/', $msg, $match );
469 if ( $match ) {
470 return [
471 'message' => "{$match[1]} [Called from {$match[2]}]",
472 'func' => $match[2],
473 'file' => $match[3],
474 'line' => $match[4],
475 ];
476 } else {
477 return null;
478 }
479 }
480
489 private static function sendMessage( $msg, $group, $level ) {
490 if ( $level !== false ) {
491 trigger_error( $msg, $level );
492 }
493
494 wfDebugLog( $group, $msg );
495 }
496
507 public static function debugMsg( $str, $context = [] ) {
509
510 if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
511 if ( $context ) {
512 $prefix = '';
513 if ( isset( $context['prefix'] ) ) {
514 $prefix = $context['prefix'];
515 } elseif ( isset( $context['channel'] ) && $context['channel'] !== 'wfDebug' ) {
516 $prefix = "[{$context['channel']}] ";
517 }
518 if ( isset( $context['seconds_elapsed'] ) && isset( $context['memory_used'] ) ) {
519 $prefix .= "{$context['seconds_elapsed']} {$context['memory_used']} ";
520 }
521 $str = LegacyLogger::interpolate( $str, $context );
522 $str = $prefix . $str;
523 }
524 $str = rtrim( UtfNormal\Validator::cleanUp( $str ) );
525 self::$debug[] = $str;
526 if ( isset( $context['channel'] ) && $context['channel'] === 'error' ) {
527 $message = isset( $context['exception'] )
528 ? $context['exception']->getMessage()
529 : $str;
530 $real = self::parseCallerDescription( $message );
531 if ( $real ) {
532 // from wfLogWarning()
533 $message = $real['message'];
534 $caller = $real['func'];
535 } else {
536 $trace = isset( $context['exception'] ) ? $context['exception']->getTrace() : [];
537 if ( ( $trace[5]['function'] ?? null ) === 'wfDeprecated' ) {
538 // from MWExceptionHandler/trigger_error/MWDebug/MWDebug/MWDebug/wfDeprecated()
539 $offset = 6;
540 } elseif ( ( $trace[1]['function'] ?? null ) === 'trigger_error' ) {
541 // from trigger_error
542 $offset = 2;
543 } else {
544 // built-in PHP error
545 $offset = 1;
546 }
547 $frame = $trace[$offset] ?? $trace[0];
548 $caller = ( isset( $frame['class'] ) ? $frame['class'] . '::' : '' )
549 . $frame['function'];
550 }
551
552 self::$log[] = [
553 'msg' => htmlspecialchars( $message ),
554 'type' => 'warn',
555 'caller' => $caller,
556 ];
557 }
558 }
559 }
560
571 public static function query( $sql, $function, $runTime, $dbhost ) {
572 if ( !self::$enabled ) {
573 return false;
574 }
575
576 // Replace invalid UTF-8 chars with a square UTF-8 character
577 // This prevents json_encode from erroring out due to binary SQL data
578 $sql = preg_replace(
579 '/(
580 [\xC0-\xC1] # Invalid UTF-8 Bytes
581 | [\xF5-\xFF] # Invalid UTF-8 Bytes
582 | \xE0[\x80-\x9F] # Overlong encoding of prior code point
583 | \xF0[\x80-\x8F] # Overlong encoding of prior code point
584 | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
585 | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
586 | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
587 | (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
588 | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]
589 | [\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
590 | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
591 | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
592 | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
593 )/x',
594 '■',
595 $sql
596 );
597
598 // last check for invalid utf8
599 $sql = UtfNormal\Validator::cleanUp( $sql );
600
601 self::$query[] = [
602 'sql' => "$dbhost: $sql",
603 'function' => $function,
604 'time' => $runTime,
605 ];
606
607 return true;
608 }
609
616 protected static function getFilesIncluded( IContextSource $context ) {
617 $files = get_included_files();
618 $fileList = [];
619 foreach ( $files as $file ) {
620 $size = filesize( $file );
621 $fileList[] = [
622 'name' => $file,
623 'size' => $context->getLanguage()->formatSize( $size ),
624 ];
625 }
626
627 return $fileList;
628 }
629
637 public static function getDebugHTML( IContextSource $context ) {
638 global $wgDebugComments;
639
640 $html = [];
641
642 if ( self::$enabled ) {
643 self::log( 'MWDebug output complete' );
644 $debugInfo = self::getDebugInfo( $context );
645
646 // Cannot use OutputPage::addJsConfigVars because those are already outputted
647 // by the time this method is called.
648 $html[] = ResourceLoader::makeInlineScript(
649 ResourceLoader::makeConfigSetScript( [ 'debugInfo' => $debugInfo ] )
650 );
651 }
652
653 if ( $wgDebugComments ) {
654 $html[] = '<!-- Debug output:';
655 foreach ( self::$debug as $line ) {
656 $html[] = htmlspecialchars( $line, ENT_NOQUOTES );
657 }
658 $html[] = '-->';
659 }
660
661 return WrappedString::join( "\n", $html );
662 }
663
672 public static function getHTMLDebugLog() {
673 global $wgShowDebug;
674
675 $html = [];
676 if ( $wgShowDebug ) {
677 $html[] = Html::openElement( 'div', [ 'id' => 'mw-html-debug-log' ] );
678 $html[] = "<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">";
679
680 foreach ( self::$debug as $line ) {
681 $display = nl2br( htmlspecialchars( trim( $line ) ) );
682
683 $html[] = "<li><code>$display</code></li>";
684 }
685
686 $html[] = '</ul>';
687 $html[] = '</div>';
688 }
689 return WrappedString::join( "\n", $html );
690 }
691
698 public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
699 if ( !self::$enabled ) {
700 return;
701 }
702
703 // output errors as debug info, when display_errors is on
704 // this is necessary for all non html output of the api, because that clears all errors first
705 $obContents = ob_get_contents();
706 if ( $obContents ) {
707 $obContentArray = explode( '<br />', $obContents );
708 foreach ( $obContentArray as $obContent ) {
709 if ( trim( $obContent ) ) {
710 self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
711 }
712 }
713 }
714
715 self::log( 'MWDebug output complete' );
716 $debugInfo = self::getDebugInfo( $context );
717
718 ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' );
719 ApiResult::setIndexedTagName( $debugInfo['log'], 'line' );
720 ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' );
721 ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' );
722 ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' );
723 $result->addValue( null, 'debuginfo', $debugInfo );
724 }
725
732 public static function getDebugInfo( IContextSource $context ) {
733 if ( !self::$enabled ) {
734 return [];
735 }
736
737 $request = $context->getRequest();
738
739 $branch = GitInfo::currentBranch();
740 if ( GitInfo::isSHA1( $branch ) ) {
741 // If it's a detached HEAD, the SHA1 will already be
742 // included in the MW version, so don't show it.
743 $branch = false;
744 }
745
746 return [
747 'mwVersion' => MW_VERSION,
748 'phpEngine' => 'PHP',
749 'phpVersion' => PHP_VERSION,
750 'gitRevision' => GitInfo::headSHA1(),
751 'gitBranch' => $branch,
752 'gitViewUrl' => GitInfo::headViewUrl(),
753 'time' => $request->getElapsedTime(),
754 'log' => self::$log,
755 'debugLog' => self::$debug,
756 'queries' => self::$query,
757 'request' => [
758 'method' => $request->getMethod(),
759 'url' => $request->getRequestURL(),
760 'headers' => $request->getAllHeaders(),
761 'params' => $request->getValues(),
762 ],
763 'memory' => $context->getLanguage()->formatSize( memory_get_usage() ),
764 'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage() ),
765 'includes' => self::getFilesIncluded( $context ),
766 ];
767 }
768}
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
if(MW_ENTRY_POINT==='index') global $wgCommandLineMode
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:88
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:41
static warning( $msg, $callerOffset=1, $level=E_USER_NOTICE, $log='auto')
Adds a warning entry to the log.
Definition MWDebug.php:188
static getFilesIncluded(IContextSource $context)
Returns a list of files included, along with their size.
Definition MWDebug.php:616
static array $deprecationFilters
Keys are regexes, values are optional callbacks to call if the filter is hit.
Definition MWDebug.php:81
static bool $enabled
Is the debugger enabled?
Definition MWDebug.php:68
static setup()
Definition MWDebug.php:86
static filterDeprecationForTest(string $regex, ?callable $callback=null)
Deprecation messages matching the supplied regex will be suppressed.
Definition MWDebug.php:389
static addModules(OutputPage $out)
Add ResourceLoader modules to the OutputPage object if debugging is enabled.
Definition MWDebug.php:133
static parseCallerDescription( $msg)
Append a caller description to an error message.
Definition MWDebug.php:466
static appendDebugInfoToApiResult(IContextSource $context, ApiResult $result)
Append the debug info to given ApiResult.
Definition MWDebug.php:698
static array $deprecationWarnings
Array of functions that have already been warned, formatted function-caller to prevent a buttload of ...
Definition MWDebug.php:76
static clearLog()
Clears internal log array and deprecation tracking.
Definition MWDebug.php:172
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:367
static deinit()
Disable the debugger.
Definition MWDebug.php:122
static query( $sql, $function, $runTime, $dbhost)
Begins profiling on a database query.
Definition MWDebug.php:571
static getLog()
Returns internal log array.
Definition MWDebug.php:164
static array $debug
Debug messages from wfDebug().
Definition MWDebug.php:54
static log( $str)
Adds a line to the log.
Definition MWDebug.php:145
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:255
static getDebugInfo(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:732
static debugMsg( $str, $context=[])
This method receives messages from LoggerFactory, wfDebugLog, and MWExceptionHandler.
Definition MWDebug.php:507
static clearDeprecationFilters()
Clear all deprecation filters.
Definition MWDebug.php:401
static array $log
Log lines.
Definition MWDebug.php:47
static deprecated( $function, $version=false, $component=false, $callerOffset=2)
Show a warning that $function is deprecated.
Definition MWDebug.php:221
static array $query
SQL statements of the database queries.
Definition MWDebug.php:61
static getHTMLDebugLog()
Generate debug log in HTML for displaying at the bottom of the main content area.
Definition MWDebug.php:672
static init()
Enabled the debugger and load resource module.
Definition MWDebug.php:113
static deprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition MWDebug.php:303
static getDebugHTML(IContextSource $context)
Returns the HTML to add to the page for the toolbar.
Definition MWDebug.php:637
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
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.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42