82 # Flags for Parser::setFunctionHook 86 # Constants needed for external link processing 87 # Everything except bracket, space, or control characters 88 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20 89 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052 90 # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM 91 # uses to replace invalid HTML characters. 93 # Simplified expression to match an IPv4 or IPv6 address, or 94 # at least one character of a host name (embeds EXT_LINK_URL_CLASS) 95 const EXT_LINK_ADDR =
'(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
96 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR) 98 const EXT_IMAGE_REGEX =
'/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+) 99 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
101 # Regular expression for a non-newline space 104 # Flags for preprocessToDom 107 # Allowed values for $this->mOutputType 108 # Parameter to startExternalParse(). 113 const
OT_PLAIN = 4;
# like extractSections() - portions of the original are returned unchanged. 132 const MARKER_SUFFIX =
"-QINU`\"'\x7f";
135 # Markers used for wrapping the table of contents 159 # Initialised by initializeVariables() 177 # Initialised in constructor 180 # Initialized in getPreprocessor() 184 # Cleared with clearState(): 214 public $mUser; #
User object; only used when doing pre-save transform
217 # These are variables reset at least once per parse regardless of $clearState 231 public $mTitle; #
Title context, used
for self-link rendering and similar things
239 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
308 public const CONSTRUCTOR_OPTIONS = [
314 'EnableScaryTranscluding',
315 'ExtraInterlanguageLinkPrefixes',
327 'TranscludeCacheExpiry',
349 $urlProtocols = null,
360 if ( empty( $this->mConf[
'class'] ) ) {
361 $this->mConf[
'class'] = self::class;
363 if ( empty( $this->mConf[
'preprocessorClass'] ) ) {
364 $this->mConf[
'preprocessorClass'] = self::getDefaultPreprocessorClass();
366 $this->svcOptions =
new ServiceOptions( self::CONSTRUCTOR_OPTIONS,
367 $this->mConf, func_num_args() > 6
368 ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
371 $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null;
385 $this->mExtLinkBracketedRegex =
'/\[(((?i)' . $this->mUrlProtocols .
')' .
386 self::EXT_LINK_ADDR .
387 self::EXT_LINK_URL_CLASS .
'*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
390 MediaWikiServices::getInstance()->getMagicWordFactory();
392 $this->contLang =
$contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
394 $this->factory =
$factory ?? MediaWikiServices::getInstance()->getParserFactory();
395 $this->specialPageFactory = $spFactory ??
396 MediaWikiServices::getInstance()->getSpecialPageFactory();
398 MediaWikiServices::getInstance()->getLinkRendererFactory();
399 $this->nsInfo =
$nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
400 $this->logger =
$logger ?:
new NullLogger();
402 MediaWikiServices::getInstance()->getBadFileLookup();
409 if ( isset( $this->mLinkHolders ) ) {
411 unset( $this->mLinkHolders );
414 foreach ( $this as $name => $value ) {
415 unset( $this->$name );
423 $this->mInParse =
false;
431 foreach ( [
'mStripState',
'mVarCache' ] as $k ) {
450 return Preprocessor_Hash::class;
457 if ( !$this->mFirstCall ) {
460 $this->mFirstCall =
false;
468 Hooks::run(
'ParserFirstCallInit', [ &$parser ] );
479 $this->mAutonumber = 0;
480 $this->mIncludeCount = [];
483 $this->mRevisionObject = $this->mRevisionTimestamp =
484 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
485 $this->mVarCache = [];
487 $this->mLangLinkLanguages = [];
488 $this->currentRevisionCache = null;
492 # Clear these on every parse, T6549 493 $this->mTplRedirCache = $this->mTplDomCache = [];
495 $this->mShowToc =
true;
496 $this->mForceTocPosition =
false;
497 $this->mIncludeSizes = [
501 $this->mPPNodeCount = 0;
502 $this->mGeneratedPPNodeCount = 0;
503 $this->mHighestExpansionDepth = 0;
504 $this->mDefaultSort =
false;
505 $this->mHeadings = [];
506 $this->mDoubleUnderscores = [];
507 $this->mExpensiveFunctionCount = 0;
510 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
511 $this->mPreprocessor = null;
518 Hooks::run(
'ParserClearState', [ &$parser ] );
526 $this->mOptions->registerWatcher( [ $this->mOutput,
'recordOption' ] );
548 $linestart =
true, $clearState =
true, $revid = null
553 $text = strtr( $text,
"\x7f",
"?" );
554 $magicScopeVariable = $this->
lock();
557 $text = str_replace(
"\000",
'', $text );
561 $this->currentRevisionCache = null;
562 $this->mInputSize = strlen( $text );
563 if ( $this->mOptions->getEnableLimitReport() ) {
564 $this->mOutput->resetParseStartTime();
572 if ( $revid !== null ) {
573 $this->mRevisionId = $revid;
574 $this->mRevisionObject = null;
575 $this->mRevisionTimestamp = null;
576 $this->mRevisionUser = null;
577 $this->mRevisionSize = null;
582 Hooks::run(
'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
584 Hooks::run(
'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
586 Hooks::run(
'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
598 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] )
599 || isset( $this->mDoubleUnderscores[
'notitleconvert'] )
600 || $this->mOutput->getDisplayTitle() !== false )
603 if ( $convruletitle ) {
604 $this->mOutput->setTitleText( $convruletitle );
607 $this->mOutput->setTitleText( $titleText );
611 # Compute runtime adaptive expiry if set 612 $this->mOutput->finalizeAdaptiveCacheExpiry();
614 # Warn if too many heavyweight parser functions were used 615 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
617 $this->mExpensiveFunctionCount,
618 $this->mOptions->getExpensiveParserFunctionLimit()
622 # Information on limits, for the benefit of users who try to skirt them 623 if ( $this->mOptions->getEnableLimitReport() ) {
627 # Wrap non-interface parser output in a <div> so it can be targeted 629 $class = $this->mOptions->getWrapOutputClass();
630 if ( $class !==
false && !$this->mOptions->getInterfaceMessage() ) {
631 $this->mOutput->addWrapperDivClass( $class );
634 $this->mOutput->setText( $text );
636 $this->mRevisionId = $oldRevisionId;
637 $this->mRevisionObject = $oldRevisionObject;
638 $this->mRevisionTimestamp = $oldRevisionTimestamp;
639 $this->mRevisionUser = $oldRevisionUser;
640 $this->mRevisionSize = $oldRevisionSize;
641 $this->mInputSize =
false;
642 $this->currentRevisionCache = null;
654 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
656 $cpuTime = $this->mOutput->getTimeSinceStart(
'cpu' );
657 if ( $cpuTime !== null ) {
658 $this->mOutput->setLimitReportData(
'limitreport-cputime',
659 sprintf(
"%.3f", $cpuTime )
663 $wallTime = $this->mOutput->getTimeSinceStart(
'wall' );
664 $this->mOutput->setLimitReportData(
'limitreport-walltime',
665 sprintf(
"%.3f", $wallTime )
668 $this->mOutput->setLimitReportData(
'limitreport-ppvisitednodes',
669 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
671 $this->mOutput->setLimitReportData(
'limitreport-ppgeneratednodes',
672 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
674 $this->mOutput->setLimitReportData(
'limitreport-postexpandincludesize',
675 [ $this->mIncludeSizes[
'post-expand'], $maxIncludeSize ]
677 $this->mOutput->setLimitReportData(
'limitreport-templateargumentsize',
678 [ $this->mIncludeSizes[
'arg'], $maxIncludeSize ]
680 $this->mOutput->setLimitReportData(
'limitreport-expansiondepth',
681 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
683 $this->mOutput->setLimitReportData(
'limitreport-expensivefunctioncount',
684 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
687 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
688 $this->mOutput->setLimitReportData( $key, $value );
691 Hooks::run(
'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
693 $limitReport =
"NewPP limit report\n";
694 if ( $this->svcOptions->get(
'ShowHostnames' ) ) {
695 $limitReport .=
'Parsed by ' .
wfHostname() .
"\n";
697 $limitReport .=
'Cached time: ' . $this->mOutput->getCacheTime() .
"\n";
698 $limitReport .=
'Cache expiry: ' . $this->mOutput->getCacheExpiry() .
"\n";
699 $limitReport .=
'Dynamic content: ' .
700 ( $this->mOutput->hasDynamicContent() ?
'true' :
'false' ) .
702 $limitReport .=
'Complications: [' . implode(
', ', $this->mOutput->getAllFlags() ) .
"]\n";
704 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
706 [ $key, &$value, &$limitReport,
false,
false ]
708 $keyMsg =
wfMessage( $key )->inLanguage(
'en' )->useDatabase(
false );
709 $valueMsg =
wfMessage( [
"$key-value-text",
"$key-value" ] )
710 ->inLanguage(
'en' )->useDatabase(
false );
711 if ( !$valueMsg->exists() ) {
714 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
715 $valueMsg->params( $value );
716 $limitReport .=
"{$keyMsg->text()}: {$valueMsg->text()}\n";
722 $limitReport = htmlspecialchars_decode( $limitReport );
726 $limitReport = str_replace( [
'-',
'&' ], [
'‐',
'&' ], $limitReport );
727 $text =
"\n<!-- \n$limitReport-->\n";
730 $dataByFunc = $this->mProfiler->getFunctionStats();
731 uasort( $dataByFunc,
function ( $a, $b ) {
732 return $b[
'real'] <=> $a[
'real'];
735 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
736 $profileReport[] = sprintf(
"%6.2f%% %8.3f %6d %s",
737 $item[
'%real'], $item[
'real'], $item[
'calls'],
738 htmlspecialchars( $item[
'name'] ) );
740 $text .=
"<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
741 $text .= implode(
"\n", $profileReport ) .
"\n-->\n";
743 $this->mOutput->setLimitReportData(
'limitreport-timingprofile', $profileReport );
746 if ( $this->svcOptions->get(
'ShowHostnames' ) ) {
747 $this->mOutput->setLimitReportData(
'cachereport-origin',
wfHostname() );
749 $this->mOutput->setLimitReportData(
'cachereport-timestamp',
750 $this->mOutput->getCacheTime() );
751 $this->mOutput->setLimitReportData(
'cachereport-ttl',
752 $this->mOutput->getCacheExpiry() );
753 $this->mOutput->setLimitReportData(
'cachereport-transientcontent',
754 $this->mOutput->hasDynamicContent() );
756 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
757 wfDebugLog(
'generated-pp-node-count', $this->mGeneratedPPNodeCount .
' ' .
758 $this->
getTitle()->getPrefixedDBkey() );
790 Hooks::run(
'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
791 Hooks::run(
'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
843 Hooks::run(
'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
862 $magicScopeVariable = $this->
lock();
864 if ( $revid !== null ) {
865 $this->mRevisionId = $revid;
869 Hooks::run(
'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
870 Hooks::run(
'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
872 $text = $this->mStripState->unstripBoth( $text );
887 $text = $this->mStripState->unstripBoth( $text );
906 $text = $msg->params( $params )->plain();
908 # Parser (re)initialisation 909 $magicScopeVariable = $this->
lock();
915 $text = $this->mStripState->unstripBoth( $text );
926 $this->mUser = $user;
939 if (
$t->hasFragment() ) {
940 # Strip the fragment to avoid various odd effects 941 $this->mTitle =
$t->createFragmentTarget(
'' );
963 return wfSetVar( $this->mTitle, $x );
972 $this->mOutputType =
$ot;
989 return wfSetVar( $this->mOutputType, $x );
1017 return wfSetVar( $this->mOptions, $x );
1024 return $this->mLinkID++;
1031 $this->mLinkID = $id;
1051 $target = $this->mOptions->getTargetLanguage();
1053 if ( $target !== null ) {
1055 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1056 return $this->mOptions->getUserLangObj();
1059 return $this->
getTitle()->getPageLanguage();
1069 if ( !is_null( $this->mUser ) ) {
1072 return $this->mOptions->getUser();
1081 if ( !isset( $this->mPreprocessor ) ) {
1082 $class = $this->svcOptions->get(
'preprocessorClass' );
1083 $this->mPreprocessor =
new $class( $this );
1096 if ( !$this->mLinkRenderer ) {
1097 $this->mLinkRenderer = $this->linkRendererFactory->create();
1098 $this->mLinkRenderer->setStubThreshold(
1150 $taglist = implode(
'|', $elements );
1151 $start =
"/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1153 while ( $text !=
'' ) {
1154 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1156 if ( count( $p ) < 5 ) {
1159 if ( count( $p ) > 5 ) {
1167 list( , $element, $attributes, $close, $inside ) = $p;
1170 $marker = self::MARKER_PREFIX .
"-$element-" . sprintf(
'%08X', $n++ ) . self::MARKER_SUFFIX;
1171 $stripped .= $marker;
1173 if ( $close ===
'/>' ) {
1174 # Empty element tag, <tag /> 1179 if ( $element ===
'!--' ) {
1182 $end =
"/(<\\/$element\\s*>)/i";
1184 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1186 if ( count( $q ) < 3 ) {
1187 # No end tag -- let it run out to the end of the text. 1191 list( , $tail, $text ) = $q;
1198 "<$element$attributes$close$content$tail" ];
1231 $marker = self::MARKER_PREFIX .
"-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1232 $this->mMarkerIndex++;
1233 $this->mStripState->addGeneral( $marker, $text );
1246 $td_history = []; # Is currently a td tag open?
1247 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1248 $tr_history = []; # Is currently a tr tag open?
1249 $tr_attributes = []; # history of tr attributes
1250 $has_opened_tr = []; # Did
this table open a <tr> element?
1251 $indent_level = 0; # indent level of the table
1253 foreach (
$lines as $outLine ) {
1254 $line = trim( $outLine );
1256 if (
$line ===
'' ) { # empty line, go to next line
1257 $out .= $outLine .
"\n";
1261 $first_character =
$line[0];
1262 $first_two = substr(
$line, 0, 2 );
1265 if ( preg_match(
'/^(:*)\s*\{\|(.*)$/',
$line,
$matches ) ) {
1266 # First check if we are starting a new table 1267 $indent_level = strlen(
$matches[1] );
1269 $attributes = $this->mStripState->unstripBoth(
$matches[2] );
1272 $outLine = str_repeat(
'<dl><dd>', $indent_level ) .
"<table{$attributes}>";
1273 array_push( $td_history,
false );
1274 array_push( $last_tag_history,
'' );
1275 array_push( $tr_history,
false );
1276 array_push( $tr_attributes,
'' );
1277 array_push( $has_opened_tr,
false );
1278 } elseif ( count( $td_history ) == 0 ) {
1279 # Don't do any of the following 1280 $out .= $outLine .
"\n";
1282 } elseif ( $first_two ===
'|}' ) {
1283 # We are ending a table 1285 $last_tag = array_pop( $last_tag_history );
1287 if ( !array_pop( $has_opened_tr ) ) {
1288 $line =
"<tr><td></td></tr>{$line}";
1291 if ( array_pop( $tr_history ) ) {
1292 $line =
"</tr>{$line}";
1295 if ( array_pop( $td_history ) ) {
1296 $line =
"</{$last_tag}>{$line}";
1298 array_pop( $tr_attributes );
1299 if ( $indent_level > 0 ) {
1300 $outLine = rtrim(
$line ) . str_repeat(
'</dd></dl>', $indent_level );
1304 } elseif ( $first_two ===
'|-' ) {
1305 # Now we have a table row 1306 $line = preg_replace(
'#^\|-+#',
'',
$line );
1308 # Whats after the tag is now only attributes 1309 $attributes = $this->mStripState->unstripBoth(
$line );
1311 array_pop( $tr_attributes );
1312 array_push( $tr_attributes, $attributes );
1315 $last_tag = array_pop( $last_tag_history );
1316 array_pop( $has_opened_tr );
1317 array_push( $has_opened_tr,
true );
1319 if ( array_pop( $tr_history ) ) {
1323 if ( array_pop( $td_history ) ) {
1324 $line =
"</{$last_tag}>{$line}";
1328 array_push( $tr_history,
false );
1329 array_push( $td_history,
false );
1330 array_push( $last_tag_history,
'' );
1331 } elseif ( $first_character ===
'|' 1332 || $first_character ===
'!' 1333 || $first_two ===
'|+' 1335 # This might be cell elements, td, th or captions 1336 if ( $first_two ===
'|+' ) {
1337 $first_character =
'+';
1344 if ( $first_character ===
'!' ) {
1348 # Split up multiple cells on the same line. 1349 # FIXME : This can result in improper nesting of tags processed 1350 # by earlier parser steps. 1351 $cells = explode(
'||',
$line );
1355 # Loop through each table cell 1356 foreach ( $cells as $cell ) {
1358 if ( $first_character !==
'+' ) {
1359 $tr_after = array_pop( $tr_attributes );
1360 if ( !array_pop( $tr_history ) ) {
1361 $previous =
"<tr{$tr_after}>\n";
1363 array_push( $tr_history,
true );
1364 array_push( $tr_attributes,
'' );
1365 array_pop( $has_opened_tr );
1366 array_push( $has_opened_tr,
true );
1369 $last_tag = array_pop( $last_tag_history );
1371 if ( array_pop( $td_history ) ) {
1372 $previous =
"</{$last_tag}>\n{$previous}";
1375 if ( $first_character ===
'|' ) {
1377 } elseif ( $first_character ===
'!' ) {
1379 } elseif ( $first_character ===
'+' ) {
1380 $last_tag =
'caption';
1385 array_push( $last_tag_history, $last_tag );
1387 # A cell could contain both parameters and data 1388 $cell_data = explode(
'|', $cell, 2 );
1390 # T2553: Note that a '|' inside an invalid link should not 1391 # be mistaken as delimiting cell parameters 1392 # Bug T153140: Neither should language converter markup. 1393 if ( preg_match(
'/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1394 $cell =
"{$previous}<{$last_tag}>" . trim( $cell );
1395 } elseif ( count( $cell_data ) == 1 ) {
1397 $cell =
"{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1399 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1402 $cell =
"{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1406 array_push( $td_history,
true );
1409 $out .= $outLine .
"\n";
1412 # Closing open td, tr && table 1413 while ( count( $td_history ) > 0 ) {
1414 if ( array_pop( $td_history ) ) {
1417 if ( array_pop( $tr_history ) ) {
1420 if ( !array_pop( $has_opened_tr ) ) {
1421 $out .=
"<tr><td></td></tr>\n";
1424 $out .=
"</table>\n";
1427 # Remove trailing line-ending (b/c) 1428 if ( substr( $out, -1 ) ===
"\n" ) {
1429 $out = substr( $out, 0, -1 );
1432 # special case: don't return empty table 1433 if ( $out ===
"<table>\n<tr><td></td></tr>\n</table>" ) {
1459 # Hook to suspend the parser in this state 1460 if ( !
Hooks::run(
'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1464 # if $frame is provided, then use $frame for replacing any variables 1466 # use frame depth to infer how include/noinclude tags should be handled 1467 # depth=0 means this is the top-level document; otherwise it's an included document 1468 if ( !$frame->depth ) {
1471 $flag = self::PTD_FOR_INCLUSION;
1474 $text = $frame->expand( $dom );
1476 # if $frame is not provided, then use old-style replaceVariables 1480 Hooks::run(
'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1483 [ $this,
'attributeStripCallback' ],
1485 array_keys( $this->mTransparentTagHooks ),
1487 [ $this,
'addTrackingCategory' ]
1489 Hooks::run(
'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1491 # Tables need to come after variable replacement for things to work 1492 # properly; putting them before other transformations should keep 1493 # exciting things like link expansions from showing up in surprising 1497 $text = preg_replace(
'/(^|\n)-----*/',
'\\1<hr />', $text );
1506 # handleInternalLinks may sometimes leave behind 1507 # absolute URLs, which have to be masked to hide them from handleExternalLinks 1508 $text = str_replace( self::MARKER_PREFIX .
'NOPARSE',
'', $text );
1526 $text = $this->mStripState->unstripGeneral( $text );
1532 Hooks::run(
'ParserAfterUnstrip', [ &$parser, &$text ] );
1535 # Clean up special characters, only run once, next-to-last before doBlockLevels 1549 if ( !( $this->mOptions->getDisableContentConversion()
1550 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] ) )
1551 && !$this->mOptions->getInterfaceMessage()
1553 # The position of the convert() call should not be changed. it 1554 # assumes that the links are all replaced and the only thing left 1555 # is the <nowiki> mark. 1559 $text = $this->mStripState->unstripNoWiki( $text );
1562 Hooks::run(
'ParserBeforeTidy', [ &$parser, &$text ] );
1566 $text = $this->mStripState->unstripGeneral( $text );
1571 if ( $this->mOptions->getTidy() ) {
1575 # attempt to sanitize at least some nesting problems 1576 # (T4702 and quite a few others) 1577 # This code path is buggy and deprecated! 1580 # ''Something [http://www.cool.com cool''] --> 1581 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a> 1582 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1583 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1584 # fix up an anchor inside another anchor, only 1585 # at least for a single single nested link (T5695) 1586 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1587 '\\1\\2</a>\\3</a>\\1\\4</a>',
1588 # fix div inside inline elements- doBlockLevels won't wrap a line which 1589 # contains a div, so fix it up here; replace 1590 # div with escaped text 1591 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1592 '\\1\\3<div\\5>\\6</div>\\8\\9',
1593 # remove empty italic or bold tag pairs, some 1594 # introduced by rules above 1595 '/<([bi])><\/\\1>/' =>
'',
1598 $text = preg_replace(
1599 array_keys( $tidyregs ),
1600 array_values( $tidyregs ),
1605 Hooks::run(
'ParserAfterTidy', [ &$parser, &$text ] );
1623 $urlChar = self::EXT_LINK_URL_CLASS;
1624 $addr = self::EXT_LINK_ADDR;
1625 $space = self::SPACE_NOT_NL; # non-newline space
1626 $spdash =
"(?:-|$space)"; # a dash or a non-newline space
1627 $spaces =
"$space++"; # possessive match of 1 or more spaces
1628 $text = preg_replace_callback(
1630 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text 1631 (<.*?>) | # m[2]: Skip stuff inside HTML elements' .
" 1632 (\b # m[3]: Free external links 1634 ($addr$urlChar*) # m[4]: Post-protocol path 1636 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number 1638 \bISBN $spaces ( # m[6]: ISBN, capture number 1639 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix 1640 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters 1641 [0-9Xx] # check digit 1643 )!xu", [ $this,
'magicLinkCallback' ], $text );
1653 if ( isset( $m[1] ) && $m[1] !==
'' ) {
1656 } elseif ( isset( $m[2] ) && $m[2] !==
'' ) {
1659 } elseif ( isset( $m[3] ) && $m[3] !==
'' ) {
1660 # Free external link 1662 } elseif ( isset( $m[5] ) && $m[5] !==
'' ) {
1664 if ( substr( $m[0], 0, 3 ) ===
'RFC' ) {
1665 if ( !$this->mOptions->getMagicRFCLinks() ) {
1670 $cssClass =
'mw-magiclink-rfc';
1671 $trackingCat =
'magiclink-tracking-rfc';
1673 } elseif ( substr( $m[0], 0, 4 ) ===
'PMID' ) {
1674 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1678 $urlmsg =
'pubmedurl';
1679 $cssClass =
'mw-magiclink-pmid';
1680 $trackingCat =
'magiclink-tracking-pmid';
1683 throw new MWException( __METHOD__ .
': unrecognised match type "' .
1684 substr( $m[0], 0, 20 ) .
'"' );
1686 $url =
wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1696 } elseif ( isset( $m[6] ) && $m[6] !==
'' 1697 && $this->mOptions->getMagicISBNLinks()
1701 $space = self::SPACE_NOT_NL; # non-newline space
1702 $isbn = preg_replace(
"/$space/",
' ', $isbn );
1703 $num = strtr( $isbn, [
1713 'class' =>
'internal mw-magiclink-isbn',
1734 # The characters '<' and '>' (which were escaped by 1735 # removeHTMLtags()) should not be included in 1736 # URLs, per RFC 2396. 1737 # Make terminate a URL as well (bug T84937) 1740 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1745 $trail = substr( $url, $m2[0][1] ) . $trail;
1746 $url = substr( $url, 0, $m2[0][1] );
1749 # Move trailing punctuation to $trail 1751 # If there is no left bracket, then consider right brackets fair game too 1752 if ( strpos( $url,
'(' ) ===
false ) {
1756 $urlRev = strrev( $url );
1757 $numSepChars = strspn( $urlRev, $sep );
1758 # Don't break a trailing HTML entity by moving the ; into $trail 1759 # This is in hot code, so use substr_compare to avoid having to 1760 # create a new string object for the comparison 1761 if ( $numSepChars && substr_compare( $url,
";", -$numSepChars, 1 ) === 0 ) {
1762 # more optimization: instead of running preg_match with a $ 1763 # anchor, which can be slow, do the match on the reversed 1764 # string starting at the desired offset. 1765 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i 1766 if ( preg_match(
'/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1770 if ( $numSepChars ) {
1771 $trail = substr( $url, -$numSepChars ) . $trail;
1772 $url = substr( $url, 0, -$numSepChars );
1775 # Verify that we still have a real URL after trail removal, and 1776 # not just lone protocol 1777 if ( strlen( $trail ) >= $numPostProto ) {
1778 return $url . $trail;
1783 # Is this an external image? 1785 if ( $text ===
false ) {
1786 # Not an image, make a link 1791 # Register it in the output object... 1792 $this->mOutput->addExternalLink( $url );
1794 return $text . $trail;
1804 for ( $i = 6; $i >= 1; --$i ) {
1805 $h = str_repeat(
'=', $i );
1808 $text = preg_replace(
"/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m",
"<h$i>\\1</h$i>", $text );
1824 $outtext .= $this->
doQuotes( $line ) .
"\n";
1826 $outtext = substr( $outtext, 0, -1 );
1839 $arr = preg_split(
"/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1840 $countarr = count( $arr );
1841 if ( $countarr == 1 ) {
1850 for ( $i = 1; $i < $countarr; $i += 2 ) {
1851 $thislen = strlen( $arr[$i] );
1855 if ( $thislen == 4 ) {
1856 $arr[$i - 1] .=
"'";
1859 } elseif ( $thislen > 5 ) {
1863 $arr[$i - 1] .= str_repeat(
"'", $thislen - 5 );
1868 if ( $thislen == 2 ) {
1870 } elseif ( $thislen == 3 ) {
1872 } elseif ( $thislen == 5 ) {
1882 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1883 $firstsingleletterword = -1;
1884 $firstmultiletterword = -1;
1886 for ( $i = 1; $i < $countarr; $i += 2 ) {
1887 if ( strlen( $arr[$i] ) == 3 ) {
1888 $x1 = substr( $arr[$i - 1], -1 );
1889 $x2 = substr( $arr[$i - 1], -2, 1 );
1890 if ( $x1 ===
' ' ) {
1891 if ( $firstspace == -1 ) {
1894 } elseif ( $x2 ===
' ' ) {
1895 $firstsingleletterword = $i;
1899 } elseif ( $firstmultiletterword == -1 ) {
1900 $firstmultiletterword = $i;
1906 if ( $firstsingleletterword > -1 ) {
1907 $arr[$firstsingleletterword] =
"''";
1908 $arr[$firstsingleletterword - 1] .=
"'";
1909 } elseif ( $firstmultiletterword > -1 ) {
1911 $arr[$firstmultiletterword] =
"''";
1912 $arr[$firstmultiletterword - 1] .=
"'";
1913 } elseif ( $firstspace > -1 ) {
1917 $arr[$firstspace] =
"''";
1918 $arr[$firstspace - 1] .=
"'";
1927 foreach ( $arr as $r ) {
1928 if ( ( $i % 2 ) == 0 ) {
1929 if ( $state ===
'both' ) {
1935 $thislen = strlen( $r );
1936 if ( $thislen == 2 ) {
1937 if ( $state ===
'i' ) {
1940 } elseif ( $state ===
'bi' ) {
1943 } elseif ( $state ===
'ib' ) {
1944 $output .=
'</b></i><b>';
1946 } elseif ( $state ===
'both' ) {
1947 $output .=
'<b><i>' . $buffer .
'</i>';
1953 } elseif ( $thislen == 3 ) {
1954 if ( $state ===
'b' ) {
1957 } elseif ( $state ===
'bi' ) {
1958 $output .=
'</i></b><i>';
1960 } elseif ( $state ===
'ib' ) {
1963 } elseif ( $state ===
'both' ) {
1964 $output .=
'<i><b>' . $buffer .
'</b>';
1970 } elseif ( $thislen == 5 ) {
1971 if ( $state ===
'b' ) {
1972 $output .=
'</b><i>';
1974 } elseif ( $state ===
'i' ) {
1975 $output .=
'</i><b>';
1977 } elseif ( $state ===
'bi' ) {
1978 $output .=
'</i></b>';
1980 } elseif ( $state ===
'ib' ) {
1981 $output .=
'</b></i>';
1983 } elseif ( $state ===
'both' ) {
1984 $output .=
'<i><b>' . $buffer .
'</b></i>';
1995 if ( $state ===
'b' || $state ===
'ib' ) {
1998 if ( $state ===
'i' || $state ===
'bi' || $state ===
'ib' ) {
2001 if ( $state ===
'bi' ) {
2005 if ( $state ===
'both' && $buffer ) {
2006 $output .=
'<b><i>' . $buffer .
'</i></b>';
2022 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2024 if ( $bits ===
false ) {
2025 throw new MWException(
"PCRE needs to be compiled with " 2026 .
"--enable-unicode-properties in order for MediaWiki to function" );
2028 $s = array_shift( $bits );
2031 while ( $i < count( $bits ) ) {
2034 $text = $bits[$i++];
2035 $trail = $bits[$i++];
2037 # The characters '<' and '>' (which were escaped by 2038 # removeHTMLtags()) should not be included in 2039 # URLs, per RFC 2396. 2041 if ( preg_match(
'/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2042 $text = substr( $url, $m2[0][1] ) .
' ' . $text;
2043 $url = substr( $url, 0, $m2[0][1] );
2046 # If the link text is an image URL, replace it with an <img> tag 2047 # This happened by accident in the original parser, but some people used it extensively 2049 if ( $img !==
false ) {
2055 # Set linktype for CSS 2058 # No link text, e.g. [http://domain.tld/some.link] 2059 if ( $text ==
'' ) {
2062 $text =
'[' . $langObj->formatNum( ++$this->mAutonumber ) .
']';
2063 $linktype =
'autonumber';
2065 # Have link text, e.g. [http://domain.tld/some.link text]s 2077 # Use the encoded URL 2078 # This means that users can paste URLs directly into the text 2079 # Funny characters like ö aren't valid in URLs anyway 2080 # This was changed in August 2004 2084 # Register link in the output object. 2085 $this->mOutput->addExternalLink( $url );
2104 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2125 $rel = self::getExternalLinkRel( $url, $this->
getTitle() );
2127 $target = $this->mOptions->getExternalLinkTarget();
2129 $attribs[
'target'] = $target;
2130 if ( !in_array( $target, [
'_self',
'_parent',
'_top' ] ) ) {
2134 if ( $rel !==
'' ) {
2137 $rel .=
'noreferrer noopener';
2140 $attribs[
'rel'] = $rel;
2155 # Test for RFC 3986 IPv6 syntax 2156 $scheme =
'[a-z][a-z0-9+.-]*:';
2157 $userinfo =
'(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2158 $ipv6Host =
'\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2159 if ( preg_match(
"<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2162 $isIPv6 = rawurldecode( $m[1] );
2167 # Make sure unsafe characters are encoded 2168 $url = preg_replace_callback(
'/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2170 return rawurlencode( $m[0] );
2176 $end = strlen( $url );
2178 # Fragment part - 'fragment' 2179 $start = strpos( $url,
'#' );
2180 if ( $start !==
false && $start < $end ) {
2181 $ret = self::normalizeUrlComponent(
2182 substr( $url, $start, $end - $start ),
'"#%<>[\]^`{|}' ) . $ret;
2186 # Query part - 'query' minus &=+; 2187 $start = strpos( $url,
'?' );
2188 if ( $start !==
false && $start < $end ) {
2189 $ret = self::normalizeUrlComponent(
2190 substr( $url, $start, $end - $start ),
'"#%<>[\]^`{|}&=+;' ) . $ret;
2194 # Scheme and path part - 'pchar' 2195 # (we assume no userinfo or encoded colons in the host) 2196 $ret = self::normalizeUrlComponent(
2197 substr( $url, 0, $end ),
'"#%<>[\]^`{|}/?' ) . $ret;
2200 if ( $isIPv6 !==
false ) {
2201 $ipv6Host =
"%5B({$isIPv6})%5D";
2202 $ret = preg_replace(
2203 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2213 $callback =
function (
$matches ) use ( $unsafe ) {
2215 $ord = ord( $char );
2216 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) ===
false ) {
2220 # Leave it escaped, but use uppercase for a-f 2224 return preg_replace_callback(
'/%[0-9A-Fa-f]{2}/', $callback, $component );
2236 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2237 $imagesexception = !empty( $imagesfrom );
2239 # $imagesfrom could be either a single string or an array of strings, parse out the latter 2240 if ( $imagesexception && is_array( $imagesfrom ) ) {
2241 $imagematch =
false;
2242 foreach ( $imagesfrom as $match ) {
2243 if ( strpos( $url, $match ) === 0 ) {
2248 } elseif ( $imagesexception ) {
2249 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2251 $imagematch =
false;
2254 if ( $this->mOptions->getAllowExternalImages()
2255 || ( $imagesexception && $imagematch )
2257 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2262 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2263 && preg_match( self::EXT_IMAGE_REGEX, $url )
2265 $whitelist = explode(
2267 wfMessage(
'external_image_whitelist' )->inContentLanguage()->text()
2270 foreach ( $whitelist as $entry ) {
2271 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments 2272 if ( strpos( $entry,
'#' ) === 0 || $entry ===
'' ) {
2275 if ( preg_match(
'/' . str_replace(
'/',
'\\/', $entry ) .
'/i', $url ) ) {
2276 # Image matches a whitelist entry 2303 static $tc =
false, $e1, $e1_img;
2304 # the % is needed to support urlencoded titles as well 2307 # Match a link having the form [[namespace:link|alternate]]trail 2308 $e1 =
"/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2309 # Match cases where there is no "]]", which might still be images 2310 $e1_img =
"/^([{$tc}]+)\\|(.*)\$/sD";
2315 # split the entire text string on occurrences of [[ 2317 # get the first element (all text up to first [[), and remove the space we added 2320 $line = $a->current(); # Workaround
for broken ArrayIterator::next() that returns
"void" 2321 $s = substr( $s, 1 );
2323 $nottalk = !$this->
getTitle()->isTalkPage();
2327 if ( $useLinkPrefixExtension ) {
2328 # Match the end of a line for a word that's not followed by whitespace, 2329 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched 2330 $charset = $this->contLang->linkPrefixCharset();
2331 $e2 =
"/^((?>.*[^$charset]|))(.+)$/sDu";
2333 if ( preg_match( $e2, $s, $m ) ) {
2334 $first_prefix = $m[2];
2336 $first_prefix =
false;
2342 # Some namespaces don't allow subpages 2343 $useSubpages = $this->nsInfo->hasSubpages(
2347 # Loop for each link 2348 for ( ;
$line !==
false &&
$line !== null; $a->next(),
$line = $a->current() ) {
2349 # Check for excessive memory usage 2350 if ( $holders->isBig() ) {
2352 # Do the existence check, replace the link holders and clear the array 2353 $holders->replace( $s );
2357 if ( $useLinkPrefixExtension ) {
2358 if ( preg_match( $e2, $s, $m ) ) {
2359 list( , $s, $prefix ) = $m;
2364 if ( $first_prefix ) {
2365 $prefix = $first_prefix;
2366 $first_prefix =
false;
2370 $might_be_img =
false;
2372 if ( preg_match( $e1,
$line, $m ) ) { # page with normal text or alt
2374 # If we get a ] at the beginning of $m[3] that means we have a link that's something like: 2375 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up, 2376 # the real problem is with the $e1 regex 2378 # Still some problems for cases where the ] is meant to be outside punctuation, 2379 # and no image is in sight. See T4095. 2381 && substr( $m[3], 0, 1 ) ===
']' 2382 && strpos( $text,
'[' ) !==
false 2385 $m[3] = substr( $m[3], 1 );
2387 # fix up urlencoded title texts 2388 if ( strpos( $m[1],
'%' ) !==
false ) {
2389 # Should anchors '#' also be rejected? 2390 $m[1] = str_replace( [
'<',
'>' ], [
'<',
'>' ], rawurldecode( $m[1] ) );
2393 } elseif ( preg_match( $e1_img,
$line, $m ) ) {
2394 # Invalid, but might be an image with a link in its caption 2395 $might_be_img =
true;
2397 if ( strpos( $m[1],
'%' ) !==
false ) {
2398 $m[1] = str_replace( [
'<',
'>' ], [
'<',
'>' ], rawurldecode( $m[1] ) );
2401 }
else { # Invalid form; output directly
2402 $s .= $prefix .
'[[' .
$line;
2406 $origLink = ltrim( $m[1],
' ' );
2408 # Don't allow internal links to pages containing 2409 # PROTO: where PROTO is a valid URL protocol; these 2410 # should be external links. 2411 if ( preg_match(
'/^(?i:' . $this->mUrlProtocols .
')/', $origLink ) ) {
2412 $s .= $prefix .
'[[' .
$line;
2416 # Make subpage if necessary 2417 if ( $useSubpages ) {
2419 $this->
getTitle(), $origLink, $text
2429 $unstrip = $this->mStripState->killMarkers( $link );
2430 $noMarkers = ( $unstrip === $link );
2433 if ( $nt === null ) {
2434 $s .= $prefix .
'[[' .
$line;
2438 $ns = $nt->getNamespace();
2439 $iw = $nt->getInterwiki();
2441 $noforce = ( substr( $origLink, 0, 1 ) !==
':' );
2443 if ( $might_be_img ) { #
if this is actually an invalid link
2444 if ( $ns ==
NS_FILE && $noforce ) { # but might be an image
2447 # look at the next 'line' to see if we can close it there 2449 $next_line = $a->current();
2450 if ( $next_line ===
false || $next_line === null ) {
2453 $m = explode(
']]', $next_line, 3 );
2454 if ( count( $m ) == 3 ) {
2455 # the first ]] closes the inner link, the second the image 2457 $text .=
"[[{$m[0]}]]{$m[1]}";
2460 } elseif ( count( $m ) == 2 ) {
2461 # if there's exactly one ]] that's fine, we'll keep looking 2462 $text .=
"[[{$m[0]}]]{$m[1]}";
2464 # if $next_line is invalid too, we need look no further 2465 $text .=
'[[' . $next_line;
2470 # we couldn't find the end of this imageLink, so output it raw 2471 # but don't ignore what might be perfectly normal links in the text we've examined 2473 $s .=
"{$prefix}[[$link|$text";
2474 # note: no $trail, because without an end, there *is* no trail 2477 }
else { # it
's not an image, so output it raw 2478 $s .= "{$prefix}[[$link|$text"; 2479 # note: no $trail, because without an end, there *is* no trail 2484 $wasblank = ( $text == '' ); 2488 # Strip off leading ':
' 2489 $text = substr( $text, 1 ); 2492 # T6598 madness. Handle the quotes only if they come from the alternate part 2493 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a> 2494 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']] 2495 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a> 2496 $text = $this->doQuotes( $text ); 2499 # Link not escaped by : , create the various objects 2500 if ( $noforce && !$nt->wasLocalInterwiki() ) { 2503 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && ( 2504 Language::fetchLanguageName( $iw, null, 'mw
' ) || 2505 in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes
' ) ) 2508 # T26502: filter duplicates 2509 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) { 2510 $this->mLangLinkLanguages[$iw] = true; 2511 $this->mOutput->addLanguageLink( $nt->getFullText() ); 2517 $s = rtrim( $s . $prefix ) . $trail; # T175416 2521 if ( $ns == NS_FILE ) { 2522 if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->getTitle() ) ) { 2524 # if no parameters were passed, $text 2525 # becomes something like "File:Foo.png", 2526 # which we don't want to pass on to the
2530 # recursively parse links inside the image caption 2531 # actually, this will parse them in any other parameters, too, 2532 # but it might be hard to fix that, and it doesn't matter ATM 2536 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them 2538 $this->
makeImage( $nt, $text, $holders ) ) . $trail;
2545 $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2553 $sortkey = str_replace(
"\n",
'', $sortkey );
2555 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2561 # Self-link checking. For some languages, variants of the title are checked in 2562 # LinkHolderArray::doVariants() to allow batching the existence checks necessary 2563 # for linking to a different variant. 2564 if ( $ns !=
NS_SPECIAL && $nt->equals( $this->getTitle() ) && !$nt->hasFragment() ) {
2569 # NS_MEDIA is a pseudo-namespace for linking directly to a file 2570 # @todo FIXME: Should do batch file existence checks, see comment below 2572 # Give extensions a chance to select the file revision for us 2576 [ $this, $nt, &$options, &$descQuery ] );
2577 # Fetch and register the file (file title may be different via hooks) 2579 # Cloak with NOPARSE to avoid replacement in handleExternalLinks 2585 # Some titles, such as valid special pages or files in foreign repos, should 2586 # be shown as bluelinks even though they're not included in the page table 2587 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do 2588 # batch file existence checks for NS_FILE and NS_MEDIA 2589 if ( $iw ==
'' && $nt->isAlwaysKnown() ) {
2590 $this->mOutput->addLink( $nt );
2593 # Links will be added to the output link list after checking 2594 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2616 if ( $text ==
'' ) {
2617 $text = htmlspecialchars( $nt->getPrefixedText() );
2621 $nt,
new HtmlArmor(
"$prefix$text$inside" )
2638 return preg_replace(
'/\b((?i)' . $this->mUrlProtocols .
')/',
2639 self::MARKER_PREFIX .
"NOPARSE$1", $text );
2671 Hooks::run(
'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) &&
2672 isset( $this->mVarCache[$index] )
2674 return $this->mVarCache[$index];
2677 $ts =
wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2678 Hooks::run(
'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2686 case 'currentmonth':
2689 case 'currentmonth1':
2692 case 'currentmonthname':
2695 case 'currentmonthnamegen':
2698 case 'currentmonthabbrev':
2713 case 'localmonthname':
2716 case 'localmonthnamegen':
2719 case 'localmonthabbrev':
2734 case 'fullpagename':
2737 case 'fullpagenamee':
2743 case 'subpagenamee':
2746 case 'rootpagename':
2749 case 'rootpagenamee':
2756 case 'basepagename':
2759 case 'basepagenamee':
2766 case 'talkpagename':
2767 if ( $this->
getTitle()->canHaveTalkPage() ) {
2768 $talkPage = $this->
getTitle()->getTalkPage();
2774 case 'talkpagenamee':
2775 if ( $this->
getTitle()->canHaveTalkPage() ) {
2776 $talkPage = $this->
getTitle()->getTalkPage();
2782 case 'subjectpagename':
2783 $subjPage = $this->
getTitle()->getSubjectPage();
2786 case 'subjectpagenamee':
2787 $subjPage = $this->
getTitle()->getSubjectPage();
2791 # Inform the edit saving system that getting the canonical output 2792 # after page insertion requires a parse that used that exact page ID 2794 $value = $this->
getTitle()->getArticleID();
2796 $value = $this->mOptions->getSpeculativePageId();
2798 $this->mOutput->setSpeculativePageIdUsed( $value );
2804 $this->svcOptions->get(
'MiserMode' ) &&
2805 !$this->mOptions->getInterfaceMessage() &&
2807 $this->nsInfo->isContent( $this->
getTitle()->getNamespace() )
2811 if ( $this->
getRevisionId() || $this->mOptions->getSpeculativeRevId() ) {
2814 $this->
setOutputFlag(
'vary-revision-exists',
'{{REVISIONID}} used' );
2818 # Inform the edit saving system that getting the canonical output after 2819 # revision insertion requires a parse that used that exact revision ID 2820 $this->
setOutputFlag(
'vary-revision-id',
'{{REVISIONID}} used' );
2822 if ( $value === 0 ) {
2824 $value = $rev ? $rev->getId() : $value;
2827 $value = $this->mOptions->getSpeculativeRevId();
2829 $this->mOutput->setSpeculativeRevIdUsed( $value );
2837 case 'revisionday2':
2840 case 'revisionmonth':
2843 case 'revisionmonth1':
2846 case 'revisionyear':
2849 case 'revisiontimestamp':
2852 case 'revisionuser':
2853 # Inform the edit saving system that getting the canonical output after 2854 # revision insertion requires a parse that used the actual user ID 2855 $this->
setOutputFlag(
'vary-user',
'{{REVISIONUSER}} used' );
2858 case 'revisionsize':
2862 $value = str_replace(
'_',
' ',
2863 $this->contLang->getNsText( $this->getTitle()->getNamespace() ) );
2866 $value =
wfUrlencode( $this->contLang->getNsText( $this->getTitle()->getNamespace() ) );
2868 case 'namespacenumber':
2869 $value = $this->
getTitle()->getNamespace();
2872 $value = $this->
getTitle()->canHaveTalkPage()
2873 ? str_replace(
'_',
' ', $this->
getTitle()->getTalkNsText() )
2877 $value = $this->
getTitle()->canHaveTalkPage()
2881 case 'subjectspace':
2882 $value = str_replace(
'_',
' ', $this->
getTitle()->getSubjectNsText() );
2884 case 'subjectspacee':
2887 case 'currentdayname':
2894 $value = $pageLang->time(
wfTimestamp( TS_MW, $ts ),
false,
false );
2900 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to 2901 # int to remove the padding 2907 case 'localdayname':
2908 $value = $pageLang->getWeekdayName(
2916 $value = $pageLang->time(
2926 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to 2927 # int to remove the padding 2933 case 'numberofarticles':
2936 case 'numberoffiles':
2939 case 'numberofusers':
2942 case 'numberofactiveusers':
2945 case 'numberofpages':
2948 case 'numberofadmins':
2951 case 'numberofedits':
2954 case 'currenttimestamp':
2957 case 'localtimestamp':
2960 case 'currentversion':
2964 return $this->svcOptions->get(
'ArticlePath' );
2966 return $this->svcOptions->get(
'Sitename' );
2968 return $this->svcOptions->get(
'Server' );
2970 return $this->svcOptions->get(
'ServerName' );
2972 return $this->svcOptions->get(
'ScriptPath' );
2974 return $this->svcOptions->get(
'StylePath' );
2975 case 'directionmark':
2976 return $pageLang->getDirMark();
2977 case 'contentlanguage':
2978 return $this->svcOptions->get(
'LanguageCode' );
2979 case 'pagelanguage':
2980 $value = $pageLang->getCode();
2982 case 'cascadingsources':
2988 'ParserGetVariableValueSwitch',
2989 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2996 $this->mVarCache[$index] = $value;
3010 # Get the timezone-adjusted timestamp to be used for this revision 3012 # Possibly set vary-revision if there is not yet an associated revision 3014 # Get the timezone-adjusted timestamp $mtts seconds in the future. 3015 # This future is relative to the current time and not that of the 3016 # parser options. The rendered timestamp can be compared to that 3017 # of the timestamp specified by the parser options. 3019 $this->contLang->userAdjust(
wfTimestamp( TS_MW, time() + $mtts ),
'' ),
3024 if ( $resNow !== $resThen ) {
3025 # Inform the edit saving system that getting the canonical output after 3026 # revision insertion requires a parse that used an actual revision timestamp 3027 $this->
setOutputFlag(
'vary-revision-timestamp',
"$variable used" );
3039 $variableIDs = $this->magicWordFactory->getVariableIDs();
3040 $substIDs = $this->magicWordFactory->getSubstIDs();
3042 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
3043 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
3094 # Is there any text? Also, Prevent too big inclusions! 3095 $textSize = strlen( $text );
3096 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3100 if ( $frame ===
false ) {
3102 } elseif ( !( $frame instanceof
PPFrame ) ) {
3103 $this->logger->debug(
3104 __METHOD__ .
" called using plain parameters instead of " .
3105 "a PPFrame instance. Creating custom frame." 3112 $text = $frame->expand( $dom, $flags );
3144 # does no harm if $current and $max are present but are unnecessary for the message 3145 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown 3146 # only during preview, and that would split the parser cache unnecessarily. 3147 $warning =
wfMessage(
"$limitationType-warning" )->numParams( $current, $max )
3149 $this->mOutput->addWarning( $warning );
3176 $forceRawInterwiki =
false;
3178 $isChildObj =
false;
3180 $isLocalObj =
false;
3182 # Title object, where $text came from 3185 # $part1 is the bit before the first |, and must contain only title characters. 3186 # Various prefixes will be stripped from it later. 3187 $titleWithSpaces = $frame->expand( $piece[
'title'] );
3188 $part1 = trim( $titleWithSpaces );
3191 # Original title text preserved for various purposes 3192 $originalTitle = $part1;
3194 # $args is a list of argument nodes, starting from index 0, not including $part1 3195 # @todo FIXME: If piece['parts'] is null then the call to getLength() 3196 # below won't work b/c this $args isn't an object 3197 $args = ( $piece[
'parts'] == null ) ? [] : $piece[
'parts'];
3199 $profileSection = null;
3203 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3205 # Possibilities for substMatch: "subst", "safesubst" or FALSE 3206 # Decide whether to expand template or keep wikitext as-is. 3207 if ( $this->ot[
'wiki'] ) {
3208 if ( $substMatch ===
false ) {
3209 $literal =
true; # literal when in PST with no prefix
3211 $literal =
false; # expand when in PST with subst: or safesubst:
3214 if ( $substMatch ==
'subst' ) {
3215 $literal =
true; # literal when not in PST with plain subst:
3217 $literal =
false; # expand when not in PST with safesubst: or no prefix
3221 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3228 if ( !$found &&
$args->getLength() == 0 ) {
3229 $id = $this->mVariables->matchStartToEnd( $part1 );
3230 if ( $id !==
false ) {
3232 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3233 $this->mOutput->updateCacheExpiry(
3234 $this->magicWordFactory->getCacheTTL( $id ) );
3240 # MSG, MSGNW and RAW 3243 $mwMsgnw = $this->magicWordFactory->get(
'msgnw' );
3244 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3247 # Remove obsolete MSG: 3248 $mwMsg = $this->magicWordFactory->get(
'msg' );
3249 $mwMsg->matchStartAndRemove( $part1 );
3253 $mwRaw = $this->magicWordFactory->get(
'raw' );
3254 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3255 $forceRawInterwiki =
true;
3261 $colonPos = strpos( $part1,
':' );
3262 if ( $colonPos !==
false ) {
3263 $func = substr( $part1, 0, $colonPos );
3264 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3265 $argsLength =
$args->getLength();
3266 for ( $i = 0; $i < $argsLength; $i++ ) {
3267 $funcArgs[] =
$args->item( $i );
3273 if ( isset( $result[
'title'] ) ) {
3274 $title = $result[
'title'];
3276 if ( isset( $result[
'found'] ) ) {
3277 $found = $result[
'found'];
3279 if ( array_key_exists(
'text', $result ) ) {
3281 $text = $result[
'text'];
3283 if ( isset( $result[
'nowiki'] ) ) {
3284 $nowiki = $result[
'nowiki'];
3286 if ( isset( $result[
'isHTML'] ) ) {
3287 $isHTML = $result[
'isHTML'];
3289 if ( isset( $result[
'forceRawInterwiki'] ) ) {
3290 $forceRawInterwiki = $result[
'forceRawInterwiki'];
3292 if ( isset( $result[
'isChildObj'] ) ) {
3293 $isChildObj = $result[
'isChildObj'];
3295 if ( isset( $result[
'isLocalObj'] ) ) {
3296 $isLocalObj = $result[
'isLocalObj'];
3301 # Finish mangling title and then check for loops. 3302 # Set $title to a Title object and $titleText to the PDBK 3305 # Split the title into page and subpage 3308 $this->
getTitle(), $part1, $subpage
3310 if ( $part1 !== $relative ) {
3312 $ns = $this->
getTitle()->getNamespace();
3316 $titleText =
$title->getPrefixedText();
3317 # Check for language variants if the template is not found 3321 # Do recursion depth check 3322 $limit = $this->mOptions->getMaxTemplateDepth();
3323 if ( $frame->depth >= $limit ) {
3325 $text =
'<span class="error">' 3326 .
wfMessage(
'parser-template-recursion-depth-warning' )
3327 ->numParams( $limit )->inContentLanguage()->text()
3333 # Load from database 3334 if ( !$found &&
$title ) {
3335 $profileSection = $this->mProfiler->scopedProfileIn(
$title->getPrefixedDBkey() );
3336 if ( !
$title->isExternal() ) {
3337 if (
$title->isSpecialPage()
3338 && $this->mOptions->getAllowSpecialInclusion()
3339 && $this->ot[
'html']
3341 $specialPage = $this->specialPageFactory->getPage(
$title->getDBkey() );
3346 $argsLength =
$args->getLength();
3347 for ( $i = 0; $i < $argsLength; $i++ ) {
3348 $bits =
$args->item( $i )->splitArg();
3349 if ( strval( $bits[
'index'] ) ===
'' ) {
3351 $value = trim( $frame->expand( $bits[
'value'] ) );
3352 $pageArgs[$name] = $value;
3360 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3366 $context->setLanguage( $this->mOptions->getUserLangObj() );
3369 $text =
$context->getOutput()->getHTML();
3370 $this->mOutput->addOutputPageMetadata(
$context->getOutput() );
3373 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3374 $this->mOutput->updateRuntimeAdaptiveExpiry(
3375 $specialPage->maxIncludeCacheTime()
3379 } elseif ( $this->nsInfo->isNonincludable(
$title->getNamespace() ) ) {
3380 $found =
false; # access denied
3381 $this->logger->debug(
3383 ": template inclusion denied for " .
$title->getPrefixedDBkey()
3387 if ( $text !==
false ) {
3393 # If the title is valid but undisplayable, make a link to it 3394 if ( !$found && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3395 $text =
"[[:$titleText]]";
3398 } elseif (
$title->isTrans() ) {
3399 # Interwiki transclusion 3400 if ( $this->ot[
'html'] && !$forceRawInterwiki ) {
3405 # Preprocess it like a template 3412 # Do infinite loop check 3413 # This has to be done after redirect resolution to avoid infinite loops via redirects 3414 if ( !$frame->loopCheck(
$title ) ) {
3416 $text =
'<span class="error">' 3417 .
wfMessage(
'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3420 $this->mOutput->addWarning(
wfMessage(
'template-loop-warning',
3422 $this->logger->debug( __METHOD__ .
": template loop broken at '$titleText'" );
3426 # If we haven't found text to substitute by now, we're done 3427 # Recover the source wikitext and return it 3429 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3430 if ( $profileSection ) {
3431 $this->mProfiler->scopedProfileOut( $profileSection );
3433 return [
'object' => $text ];
3436 # Expand DOM-style return values in a child frame 3437 if ( $isChildObj ) {
3438 # Clean up argument array 3443 } elseif ( $titleText !==
false && $newFrame->isEmpty() ) {
3444 # Expansion is eligible for the empty-frame cache 3445 $text = $newFrame->cachedExpand( $titleText, $text );
3447 # Uncached expansion 3448 $text = $newFrame->expand( $text );
3451 if ( $isLocalObj && $nowiki ) {
3453 $isLocalObj =
false;
3456 if ( $profileSection ) {
3457 $this->mProfiler->scopedProfileOut( $profileSection );
3460 # Replace raw HTML by a placeholder 3463 } elseif ( $nowiki && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3464 # Escape nowiki-style return values 3466 } elseif ( is_string( $text )
3467 && !$piece[
'lineStart']
3468 && preg_match(
'/^(?:{\\||:|;|#|\*)/', $text )
3470 # T2529: if the template begins with a table or block-level 3471 # element, it should be treated as beginning a new line. 3472 # This behavior is somewhat controversial. 3473 $text =
"\n" . $text;
3477 # Error, oversize inclusion 3478 if ( $titleText !==
false ) {
3479 # Make a working, properly escaped link if possible (T25588) 3480 $text =
"[[:$titleText]]";
3482 # This will probably not be a working link, but at least it may 3483 # provide some hint of where the problem is 3484 preg_replace(
'/^:/',
'', $originalTitle );
3485 $text =
"[[:$originalTitle]]";
3488 .
'post-expand include size too large -->' );
3492 if ( $isLocalObj ) {
3493 $ret = [
'object' => $text ];
3495 $ret = [
'text' => $text ];
3520 # Case sensitive functions 3521 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3522 $function = $this->mFunctionSynonyms[1][$function];
3524 # Case insensitive functions 3525 $function = $this->contLang->lc( $function );
3526 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3527 $function = $this->mFunctionSynonyms[0][$function];
3529 return [
'found' => false ];
3533 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3538 $allArgs = [ &$parser ];
3540 # Convert arguments to PPNodes and collect for appending to $allArgs 3542 foreach (
$args as $k => $v ) {
3543 if ( $v instanceof
PPNode || $k === 0 ) {
3546 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3550 # Add a frame parameter, and pass the arguments as an array 3551 $allArgs[] = $frame;
3552 $allArgs[] = $funcArgs;
3554 # Convert arguments to plain text and append to $allArgs 3555 foreach (
$args as $k => $v ) {
3556 if ( $v instanceof
PPNode ) {
3557 $allArgs[] = trim( $frame->expand( $v ) );
3558 } elseif ( is_int( $k ) && $k >= 0 ) {
3559 $allArgs[] = trim( $v );
3561 $allArgs[] = trim(
"$k=$v" );
3566 $result = $callback( ...$allArgs );
3568 # The interface for function hooks allows them to return a wikitext 3569 # string or an array containing the string and any flags. This mungs 3570 # things around to match what this method should return. 3571 if ( !is_array( $result ) ) {
3577 if ( isset( $result[0] ) && !isset( $result[
'text'] ) ) {
3578 $result[
'text'] = $result[0];
3580 unset( $result[0] );
3587 $preprocessFlags = 0;
3588 if ( isset( $result[
'noparse'] ) ) {
3589 $noparse = $result[
'noparse'];
3591 if ( isset( $result[
'preprocessFlags'] ) ) {
3592 $preprocessFlags = $result[
'preprocessFlags'];
3596 $result[
'text'] = $this->
preprocessToDom( $result[
'text'], $preprocessFlags );
3597 $result[
'isChildObj'] =
true;
3613 $titleText =
$title->getPrefixedDBkey();
3615 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3616 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3618 $titleText =
$title->getPrefixedDBkey();
3620 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3621 return [ $this->mTplDomCache[$titleText],
$title ];
3624 # Cache miss, go to the database 3627 if ( $text ===
false ) {
3628 $this->mTplDomCache[$titleText] =
false;
3629 return [
false,
$title ];
3633 $this->mTplDomCache[$titleText] = $dom;
3635 if ( !
$title->equals( $cacheTitle ) ) {
3636 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3655 $cacheKey =
$title->getPrefixedDBkey();
3656 if ( !$this->currentRevisionCache ) {
3657 $this->currentRevisionCache =
new MapCacheLRU( 100 );
3659 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3660 $this->currentRevisionCache->set( $cacheKey,
3662 call_user_func( $this->mOptions->getCurrentRevisionCallback(),
$title, $this )
3665 return $this->currentRevisionCache->get( $cacheKey );
3675 $this->currentRevisionCache &&
3676 $this->currentRevisionCache->has(
$title->getPrefixedText() )
3702 $templateCb = $this->mOptions->getTemplateCallback();
3703 $stuff = call_user_func( $templateCb,
$title, $this );
3704 $rev = $stuff[
'revision'] ?? null;
3705 $text = $stuff[
'text'];
3706 if ( is_string( $stuff[
'text'] ) ) {
3708 $text = strtr( $text,
"\x7f",
"?" );
3710 $finalTitle = $stuff[
'finalTitle'] ??
$title;
3711 foreach ( ( $stuff[
'deps'] ?? [] ) as $dep ) {
3712 $this->mOutput->addTemplate( $dep[
'title'], $dep[
'page_id'], $dep[
'rev_id'] );
3713 if ( $dep[
'title']->equals( $this->
getTitle() ) && $rev instanceof
Revision ) {
3715 $this->
setOutputFlag(
'vary-revision-sha1',
'Self transclusion' );
3716 $this->
getOutput()->setRevisionUsedSha1Base36( $rev->getSha1() );
3720 return [ $text, $finalTitle ];
3742 $text = $skip =
false;
3747 # Loop to fetch the article, with up to 1 redirect 3748 for ( $i = 0; $i < 2 && is_object(
$title ); $i++ ) {
3749 # Give extensions a chance to select the revision instead 3750 $id =
false; # Assume current
3751 Hooks::run(
'BeforeParserFetchTemplateAndtitle',
3752 [ $parser,
$title, &$skip, &$id ] );
3758 'page_id' =>
$title->getArticleID(),
3766 } elseif ( $parser ) {
3767 $rev = $parser->fetchCurrentRevisionOfTitle(
$title );
3771 $rev_id = $rev ? $rev->getId() : 0;
3772 # If there is no current revision, there is no page 3773 if ( $id ===
false && !$rev ) {
3774 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3775 $linkCache->addBadLinkObj(
$title );
3780 'page_id' =>
$title->getArticleID(),
3783 if ( $rev && !
$title->equals( $rev->getTitle() ) ) {
3784 # We fetched a rev from a different title; register it too... 3786 'title' => $rev->getTitle(),
3787 'page_id' => $rev->getPage(),
3797 [ $parser,
$title, $rev, &$text, &$deps ] );
3799 if ( $text ===
false || $text === null ) {
3805 lcfirst(
$title->getText() ) )->inContentLanguage();
3806 if ( !$message->exists() ) {
3811 $text = $message->plain();
3825 'finalTitle' => $finalTitle,
3840 $time =
$file ?
$file->getTimestamp() :
false;
3842 # Register the file as a dependency... 3843 $this->mOutput->addImage(
$title->getDBkey(), $time, $sha1 );
3845 # Update fetched file title 3847 $this->mOutput->addImage(
$title->getDBkey(), $time, $sha1 );
3863 if ( isset( $options[
'broken'] ) ) {
3865 } elseif ( isset( $options[
'sha1'] ) ) {
3868 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
$title, $options );
3882 if ( !$this->svcOptions->get(
'EnableScaryTranscluding' ) ) {
3883 return wfMessage(
'scarytranscludedisabled' )->inContentLanguage()->text();
3886 $url =
$title->getFullURL( [
'action' => $action ] );
3887 if ( strlen( $url ) > 1024 ) {
3888 return wfMessage(
'scarytranscludetoolong' )->inContentLanguage()->text();
3891 $wikiId =
$title->getTransWikiID();
3893 $fname = __METHOD__;
3894 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3896 $data =
$cache->getWithSetCallback(
3898 'interwiki-transclude',
3899 ( $wikiId !==
false ) ? $wikiId :
'external',
3902 $this->svcOptions->get(
'TranscludeCacheExpiry' ),
3903 function ( $oldValue, &$ttl ) use ( $url, $fname,
$cache ) {
3906 $status = $req->execute();
3907 if ( !$status->isOK() ) {
3908 $ttl = $cache::TTL_UNCACHEABLE;
3909 } elseif ( $req->getResponseHeader(
'X-Database-Lagged' ) !== null ) {
3910 $ttl = min( $cache::TTL_LAGGED, $ttl );
3914 'text' => $status->isOK() ? $req->getContent() : null,
3915 'code' => $req->getStatus()
3919 'checkKeys' => ( $wikiId !== false )
3920 ? [
$cache->makeGlobalKey(
'interwiki-page', $wikiId,
$title->getDBkey() ) ]
3922 'pcGroup' =>
'interwiki-transclude:5',
3923 'pcTTL' => $cache::TTL_PROC_LONG
3927 if ( is_string( $data[
'text'] ) ) {
3928 $text = $data[
'text'];
3929 } elseif ( $data[
'code'] != 200 ) {
3931 $text =
wfMessage(
'scarytranscludefailed-httpstatus' )
3932 ->params( $url, $data[
'code'] )->inContentLanguage()->text();
3934 $text =
wfMessage(
'scarytranscludefailed', $url )->inContentLanguage()->text();
3951 $parts = $piece[
'parts'];
3952 $nameWithSpaces = $frame->expand( $piece[
'title'] );
3953 $argName = trim( $nameWithSpaces );
3955 $text = $frame->getArgument( $argName );
3956 if ( $text ===
false && $parts->getLength() > 0
3957 && ( $this->ot[
'html']
3959 || ( $this->ot[
'wiki'] && $frame->isTemplate() )
3962 # No match in frame, use the supplied default 3963 $object = $parts->item( 0 )->getChildren();
3966 $error =
'<!-- WARNING: argument omitted, expansion size too large -->';
3970 if ( $text ===
false && $object ===
false ) {
3972 $object = $frame->virtualBracketedImplode(
'{{{',
'|',
'}}}', $nameWithSpaces, $parts );
3974 if ( $error !==
false ) {
3977 if ( $object !==
false ) {
3978 $ret = [
'object' => $object ];
3980 $ret = [
'text' => $text ];
4003 static $errorStr =
'<span class="error">';
4004 static $errorLen = 20;
4006 $name = $frame->expand( $params[
'name'] );
4007 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
4013 $attrText = !isset( $params[
'attr'] ) ? null : $frame->expand( $params[
'attr'] );
4014 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
4022 $content = !isset( $params[
'inner'] ) ? null : $frame->expand( $params[
'inner'] );
4024 $marker = self::MARKER_PREFIX .
"-$name-" 4025 . sprintf(
'%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4027 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4028 ( $this->ot[
'html'] || $this->ot[
'pre'] );
4029 if ( $isFunctionTag ) {
4030 $markerType =
'none';
4032 $markerType =
'general';
4034 if ( $this->ot[
'html'] || $isFunctionTag ) {
4035 $name = strtolower( $name );
4037 if ( isset( $params[
'attributes'] ) ) {
4038 $attributes += $params[
'attributes'];
4041 if ( isset( $this->mTagHooks[$name] ) ) {
4042 $output = call_user_func_array( $this->mTagHooks[$name],
4043 [
$content, $attributes, $this, $frame ] );
4044 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4045 list( $callback, ) = $this->mFunctionTagHooks[$name];
4049 $output = call_user_func_array( $callback, [ &$parser, $frame,
$content, $attributes ] );
4051 $output =
'<span class="error">Invalid tag extension name: ' .
4052 htmlspecialchars( $name ) .
'</span>';
4055 if ( is_array( $output ) ) {
4058 $output = $flags[0];
4059 if ( isset( $flags[
'markerType'] ) ) {
4060 $markerType = $flags[
'markerType'];
4064 if ( is_null( $attrText ) ) {
4067 if ( isset( $params[
'attributes'] ) ) {
4068 foreach ( $params[
'attributes'] as $attrName => $attrValue ) {
4069 $attrText .=
' ' . htmlspecialchars( $attrName ) .
'="' .
4070 htmlspecialchars( $attrValue ) .
'"';
4074 $output =
"<$name$attrText/>";
4076 $close = is_null( $params[
'close'] ) ?
'' : $frame->expand( $params[
'close'] );
4077 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4081 $output =
"<$name$attrText>$content$close";
4085 if ( $markerType ===
'none' ) {
4087 } elseif ( $markerType ===
'nowiki' ) {
4088 $this->mStripState->addNoWiki( $marker, $output );
4089 } elseif ( $markerType ===
'general' ) {
4090 $this->mStripState->addGeneral( $marker, $output );
4092 throw new MWException( __METHOD__ .
': invalid marker type' );
4105 if ( $this->mIncludeSizes[
$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4108 $this->mIncludeSizes[
$type] += $size;
4119 $this->mExpensiveFunctionCount++;
4120 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4131 # The position of __TOC__ needs to be recorded 4132 $mw = $this->magicWordFactory->get(
'toc' );
4133 if ( $mw->match( $text ) ) {
4134 $this->mShowToc =
true;
4135 $this->mForceTocPosition =
true;
4137 # Set a placeholder. At the end we'll fill it in with the TOC. 4138 $text = $mw->replace(
'<!--MWTOC\'"-->', $text, 1 );
4140 # Only keep the first one. 4141 $text = $mw->replace(
'', $text );
4144 # Now match and remove the rest of them 4145 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4146 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4148 if ( isset( $this->mDoubleUnderscores[
'nogallery'] ) ) {
4149 $this->mOutput->mNoGallery =
true;
4151 if ( isset( $this->mDoubleUnderscores[
'notoc'] ) && !$this->mForceTocPosition ) {
4152 $this->mShowToc =
false;
4154 if ( isset( $this->mDoubleUnderscores[
'hiddencat'] )
4159 # (T10068) Allow control over whether robots index a page. 4160 # __INDEX__ always overrides __NOINDEX__, see T16899 4161 if ( isset( $this->mDoubleUnderscores[
'noindex'] ) && $this->
getTitle()->canUseNoindex() ) {
4162 $this->mOutput->setIndexPolicy(
'noindex' );
4165 if ( isset( $this->mDoubleUnderscores[
'index'] ) && $this->
getTitle()->canUseNoindex() ) {
4166 $this->mOutput->setIndexPolicy(
'index' );
4170 # Cache all double underscores in the database 4171 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4172 $this->mOutput->setProperty( $key,
'' );
4184 return $this->mOutput->addTrackingCategory( $msg, $this->
getTitle() );
4203 # Inhibit editsection links if requested in the page 4204 if ( isset( $this->mDoubleUnderscores[
'noeditsection'] ) ) {
4205 $maybeShowEditLink =
false;
4207 $maybeShowEditLink =
true;
4210 # Get all headlines for numbering them and adding funky stuff like [edit] 4211 # links - this is for later, but we need the number of headlines right now 4212 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't 4213 # be trimmed here since whitespace in HTML headings is significant. 4215 $numMatches = preg_match_all(
4216 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4221 # if there are fewer than 4 headlines in the article, do not show TOC 4222 # unless it's been explicitly enabled. 4223 $enoughToc = $this->mShowToc &&
4224 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4226 # Allow user to stipulate that a page should have a "new section" 4227 # link added via __NEWSECTIONLINK__ 4228 if ( isset( $this->mDoubleUnderscores[
'newsectionlink'] ) ) {
4229 $this->mOutput->setNewSection(
true );
4232 # Allow user to remove the "new section" 4233 # link via __NONEWSECTIONLINK__ 4234 if ( isset( $this->mDoubleUnderscores[
'nonewsectionlink'] ) ) {
4235 $this->mOutput->hideNewSection(
true );
4238 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, 4239 # override above conditions and always show TOC above first header 4240 if ( isset( $this->mDoubleUnderscores[
'forcetoc'] ) ) {
4241 $this->mShowToc =
true;
4249 # Ugh .. the TOC should have neat indentation levels which can be 4250 # passed to the skin functions. These are determined here 4254 $sublevelCount = [];
4260 $markerRegex = self::MARKER_PREFIX .
"-h-(\d+)-" . self::MARKER_SUFFIX;
4261 $baseTitleText = $this->
getTitle()->getPrefixedDBkey();
4266 $node = $root->getFirstChild();
4271 $headlines = $numMatches !==
false ?
$matches[3] : [];
4273 $maxTocLevel = $this->svcOptions->get(
'MaxTocLevel' );
4274 foreach ( $headlines as $headline ) {
4275 $isTemplate =
false;
4277 $sectionIndex =
false;
4279 $markerMatches = [];
4280 if ( preg_match(
"/^$markerRegex/", $headline, $markerMatches ) ) {
4281 $serial = $markerMatches[1];
4282 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4283 $isTemplate = ( $titleText != $baseTitleText );
4284 $headline = preg_replace(
"/^$markerRegex\\s*/",
"", $headline );
4288 $prevlevel = $level;
4290 $level =
$matches[1][$headlineCount];
4292 if ( $level > $prevlevel ) {
4293 # Increase TOC level 4295 $sublevelCount[$toclevel] = 0;
4296 if ( $toclevel < $maxTocLevel ) {
4297 $prevtoclevel = $toclevel;
4301 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4302 # Decrease TOC level, find level to jump to 4304 for ( $i = $toclevel; $i > 0; $i-- ) {
4305 if ( $levelCount[$i] == $level ) {
4306 # Found last matching level 4309 } elseif ( $levelCount[$i] < $level ) {
4310 # Found first matching level below current level 4318 if ( $toclevel < $maxTocLevel ) {
4319 if ( $prevtoclevel < $maxTocLevel ) {
4320 # Unindent only if the previous toc level was shown :p 4322 $prevtoclevel = $toclevel;
4328 # No change in level, end TOC line 4329 if ( $toclevel < $maxTocLevel ) {
4334 $levelCount[$toclevel] = $level;
4336 # count number of headlines for each level 4337 $sublevelCount[$toclevel]++;
4339 for ( $i = 1; $i <= $toclevel; $i++ ) {
4340 if ( !empty( $sublevelCount[$i] ) ) {
4349 # The safe header is a version of the header text safe to use for links 4351 # Remove link placeholders by the link text. 4352 # <!--LINK number--> 4354 # link text with suffix 4355 # Do this before unstrip since link text can contain strip markers 4358 # Avoid insertion of weird stuff like <math> by expanding the relevant sections 4359 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4361 # Remove any <style> or <script> tags (T198618) 4362 $safeHeadline = preg_replace(
4363 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4368 # Strip out HTML (first regex removes any tag not allowed) 4370 # * <sup> and <sub> (T10393) 4374 # * <span dir="rtl"> and <span dir="ltr"> (T37167) 4375 # * <s> and <strike> (T35715) 4376 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>, 4377 # to allow setting directionality in toc items. 4378 $tocline = preg_replace(
4380 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4381 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#' 4387 # Strip '<span></span>', which is the result from the above if 4388 # <span id="foo"></span> is used to produce an additional anchor 4390 $tocline = str_replace(
'<span></span>',
'', $tocline );
4392 $tocline = trim( $tocline );
4394 # For the anchor, strip out HTML-y stuff period 4395 $safeHeadline = preg_replace(
'/<.*?>/',
'', $safeHeadline );
4398 # Save headline for section edit hint before it's escaped 4399 $headlineHint = $safeHeadline;
4401 # Decode HTML entities 4404 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4409 if ( $fallbackHeadline === $safeHeadline ) {
4410 # No reason to have both (in fact, we can't) 4411 $fallbackHeadline =
false;
4414 # HTML IDs must be case-insensitively unique for IE compatibility (T12721). 4415 # @todo FIXME: We may be changing them depending on the current locale. 4416 $arrayKey = strtolower( $safeHeadline );
4417 if ( $fallbackHeadline ===
false ) {
4418 $fallbackArrayKey =
false;
4420 $fallbackArrayKey = strtolower( $fallbackHeadline );
4423 # Create the anchor for linking from the TOC to the section 4424 $anchor = $safeHeadline;
4425 $fallbackAnchor = $fallbackHeadline;
4426 if ( isset( $refers[$arrayKey] ) ) {
4428 for ( $i = 2; isset( $refers[
"${arrayKey}_$i"] ); ++$i );
4430 $linkAnchor .=
"_$i";
4431 $refers[
"${arrayKey}_$i"] =
true;
4433 $refers[$arrayKey] =
true;
4435 if ( $fallbackHeadline !==
false && isset( $refers[$fallbackArrayKey] ) ) {
4437 for ( $i = 2; isset( $refers[
"${fallbackArrayKey}_$i"] ); ++$i );
4438 $fallbackAnchor .=
"_$i";
4439 $refers[
"${fallbackArrayKey}_$i"] =
true;
4441 $refers[$fallbackArrayKey] =
true;
4444 # Don't number the heading if it is the only one (looks silly) 4445 if ( count(
$matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4446 # the two are different if the line contains a link 4449 [
'class' =>
'mw-headline-number' ],
4451 ) .
' ' . $headline;
4454 if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4456 $numbering, $toclevel, ( $isTemplate ?
false : $sectionIndex ) );
4459 # Add the section to the section tree 4460 # Find the DOM node for this header 4461 $noOffset = ( $isTemplate || $sectionIndex === false );
4462 while ( $node && !$noOffset ) {
4463 if ( $node->getName() ===
'h' ) {
4464 $bits = $node->splitHeading();
4465 if ( $bits[
'i'] == $sectionIndex ) {
4469 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4471 $node = $node->getNextSibling();
4474 'toclevel' => $toclevel,
4477 'number' => $numbering,
4478 'index' => ( $isTemplate ?
'T-' :
'' ) . $sectionIndex,
4479 'fromtitle' => $titleText,
4480 'byteoffset' => ( $noOffset ? null : $byteOffset ),
4481 'anchor' => $anchor,
4484 # give headline the correct <h#> tag 4485 if ( $maybeShowEditLink && $sectionIndex !==
false ) {
4487 if ( $isTemplate ) {
4488 # Put a T flag in the section identifier, to indicate to extractSections() 4489 # that sections inside <includeonly> should be counted. 4490 $editsectionPage = $titleText;
4491 $editsectionSection =
"T-$sectionIndex";
4492 $editsectionContent = null;
4494 $editsectionPage = $this->
getTitle()->getPrefixedText();
4495 $editsectionSection = $sectionIndex;
4496 $editsectionContent = $headlineHint;
4510 $editlink =
'<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4511 $editlink .=
'" section="' . htmlspecialchars( $editsectionSection ) .
'"';
4512 if ( $editsectionContent !== null ) {
4513 $editlink .=
'>' . $editsectionContent .
'</mw:editsection>';
4521 $matches[
'attrib'][$headlineCount], $anchor, $headline,
4522 $editlink, $fallbackAnchor );
4529 # Never ever show TOC if no headers 4530 if ( $numVisible < 1 ) {
4535 if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4539 $this->mOutput->setTOCHTML( $toc );
4540 $toc = self::TOC_START . $toc . self::TOC_END;
4544 $this->mOutput->setSections( $tocraw );
4547 # split up and insert constructed headlines 4548 $blocks = preg_split(
'/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4553 foreach ( $blocks as $block ) {
4555 if ( empty( $head[$i - 1] ) ) {
4556 $sections[$i] = $block;
4558 $sections[$i] = $head[$i - 1] . $block;
4571 Hooks::run(
'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4576 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4579 $sections[0] .= $toc .
"\n";
4582 $full .= implode(
'', $sections );
4584 if ( $this->mForceTocPosition ) {
4585 return str_replace(
'<!--MWTOC\'"-->', $toc, $full );
4605 if ( $clearState ) {
4606 $magicScopeVariable = $this->
lock();
4612 $text = str_replace(
"\000",
'', $text );
4620 $text = $this->
pstPass2( $text, $user );
4622 $text = $this->mStripState->unstripBoth( $text );
4624 $this->
setUser( null ); # Reset
4638 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use 4639 # $this->contLang here in order to give everyone the same signature and use the default one 4640 # rather than the one selected in each user's preferences. (see also T14815) 4641 $ts = $this->mOptions->getTimestamp();
4643 $ts = $timestamp->format(
'YmdHis' );
4644 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4646 $d = $this->contLang->timeanddate( $ts,
false,
false ) .
" ($tzMsg)";
4648 # Variable replacement 4649 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags 4652 # This works almost by chance, as the replaceVariables are done before the getUserSig(), 4653 # which may corrupt this parser instance via its wfMessage()->text() call- 4656 if ( strpos( $text,
'~~~' ) !==
false ) {
4658 $text = strtr( $text, [
4660 '~~~~' =>
"$sigText $d",
4663 # The main two signature forms used above are time-sensitive 4664 $this->
setOutputFlag(
'user-signature',
'User signature detected' );
4667 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]] 4669 $nc =
'[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4672 $p1 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4674 $p4 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4676 $p3 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4678 $p2 =
"/\[\[\\|($tc+)]]/";
4680 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" 4681 $text = preg_replace( $p1,
'[[\\1\\2\\3|\\2]]', $text );
4682 $text = preg_replace( $p4,
'[[\\1\\2\\3|\\2]]', $text );
4683 $text = preg_replace( $p3,
'[[\\1\\2\\3\\4|\\2]]', $text );
4687 if ( preg_match(
"/^($nc+:|)$tc+?( \\($tc+\\))$/",
$t, $m ) ) {
4688 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4689 } elseif ( preg_match(
"/^($nc+:|)$tc+?(, $tc+|)$/",
$t, $m ) &&
"$m[1]$m[2]" !=
'' ) {
4690 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4692 # if there's no context, don't bother duplicating the title 4693 $text = preg_replace( $p2,
'[[\\1]]', $text );
4713 public function getUserSig( &$user, $nickname =
false, $fancySig = null ) {
4714 $username = $user->getName();
4716 # If not given, retrieve from the user object. 4717 if ( $nickname ===
false ) {
4718 $nickname = $user->getOption(
'nickname' );
4721 if ( is_null( $fancySig ) ) {
4722 $fancySig = $user->getBoolOption(
'fancysig' );
4725 if ( $nickname === null || $nickname ===
'' ) {
4726 $nickname = $username;
4727 } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get(
'MaxSigChars' ) ) {
4728 $nickname = $username;
4729 $this->logger->debug( __METHOD__ .
": $username has overlong signature." );
4730 } elseif ( $fancySig !==
false ) {
4731 # Sig. might contain markup; validate this 4732 if ( $this->
validateSig( $nickname ) !==
false ) {
4733 # Validated; clean up (if needed) and return it 4734 return $this->
cleanSig( $nickname,
true );
4736 # Failed to validate; fall back to the default 4737 $nickname = $username;
4738 $this->logger->debug( __METHOD__ .
": $username has bad XML tags in signature." );
4742 # Make sure nickname doesnt get a sig in a sig 4743 $nickname = self::cleanSigInSig( $nickname );
4745 # If we're still here, make it a link to the user page 4748 $msgName = $user->isAnon() ?
'signature-anon' :
'signature';
4750 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4751 ->title( $this->
getTitle() )->text();
4777 $magicScopeVariable = $this->
lock();
4781 # Option to disable this feature 4782 if ( !$this->mOptions->getCleanSignatures() ) {
4786 # @todo FIXME: Regex doesn't respect extension tags or nowiki 4787 # => Move this logic to braceSubstitution() 4788 $substWord = $this->magicWordFactory->get(
'subst' );
4789 $substRegex =
'/\{\{(?!(?:' . $substWord->getBaseRegex() .
'))/x' . $substWord->getRegexCase();
4790 $substText =
'{{' . $substWord->getSynonym( 0 );
4792 $text = preg_replace( $substRegex, $substText, $text );
4793 $text = self::cleanSigInSig( $text );
4796 $text = $frame->expand( $dom );
4799 $text = $this->mStripState->unstripBoth( $text );
4812 $text = preg_replace(
'/~{3,5}/',
'', $text );
4827 $outputType, $clearState =
true, $revId = null
4829 $this->
startParse( $title, $options, $outputType, $clearState );
4830 if ( $revId !== null ) {
4831 $this->mRevisionId = $revId;
4842 $outputType, $clearState =
true 4845 $this->mOptions = $options;
4847 if ( $clearState ) {
4861 static $executing =
false;
4863 # Guard against infinite recursion 4904 public function setHook( $tag, callable $callback ) {
4905 $tag = strtolower( $tag );
4906 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
4907 throw new MWException(
"Invalid character {$m[0]} in setHook('$tag', ...) call" );
4909 $oldVal = $this->mTagHooks[$tag] ?? null;
4910 $this->mTagHooks[$tag] = $callback;
4911 if ( !in_array( $tag, $this->mStripList ) ) {
4912 $this->mStripList[] = $tag;
4936 $tag = strtolower( $tag );
4937 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
4938 throw new MWException(
"Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4940 $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
4941 $this->mTransparentTagHooks[$tag] = $callback;
4950 $this->mTagHooks = [];
4951 $this->mFunctionTagHooks = [];
4999 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5000 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5002 # Add to function cache 5003 $mw = $this->magicWordFactory->get( $id );
5005 throw new MWException( __METHOD__ .
'() expecting a magic word identifier.' );
5008 $synonyms = $mw->getSynonyms();
5009 $sensitive = intval( $mw->isCaseSensitive() );
5011 foreach ( $synonyms as $syn ) {
5013 if ( !$sensitive ) {
5014 $syn = $this->contLang->lc( $syn );
5020 # Remove trailing colon 5021 if ( substr( $syn, -1, 1 ) ===
':' ) {
5022 $syn = substr( $syn, 0, -1 );
5024 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5036 return array_keys( $this->mFunctionHooks );
5050 $tag = strtolower( $tag );
5051 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
5052 throw new MWException(
"Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5054 $old = $this->mFunctionTagHooks[$tag] ?? null;
5055 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5057 if ( !in_array( $tag, $this->mStripList ) ) {
5058 $this->mStripList[] = $tag;
5084 $this->mLinkHolders->replace( $text );
5095 return $this->mLinkHolders->replaceText( $text );
5113 if ( isset( $params[
'mode'] ) ) {
5114 $mode = $params[
'mode'];
5124 $ig->setContextTitle( $this->
getTitle() );
5125 $ig->setShowBytes(
false );
5126 $ig->setShowDimensions(
false );
5127 $ig->setShowFilename(
false );
5128 $ig->setParser( $this );
5129 $ig->setHideBadImages();
5132 if ( isset( $params[
'showfilename'] ) ) {
5133 $ig->setShowFilename(
true );
5135 $ig->setShowFilename(
false );
5137 if ( isset( $params[
'caption'] ) ) {
5142 $ig->setCaptionHtml( $caption );
5144 if ( isset( $params[
'perrow'] ) ) {
5145 $ig->setPerRow( $params[
'perrow'] );
5147 if ( isset( $params[
'widths'] ) ) {
5148 $ig->setWidths( $params[
'widths'] );
5150 if ( isset( $params[
'heights'] ) ) {
5151 $ig->setHeights( $params[
'heights'] );
5153 $ig->setAdditionalOptions( $params );
5157 Hooks::run(
'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5161 # match lines like these: 5162 # Image:someimage.jpg|This is some image 5164 preg_match(
"/^([^|]+)(\\|(.*))?$/", $line,
$matches );
5170 if ( strpos(
$matches[0],
'%' ) !==
false ) {
5174 if ( is_null(
$title ) ) {
5175 # Bogus title. Ignore these so we don't bomb out later. 5179 # We need to get what handler the file uses, to figure out parameters. 5180 # Note, a hook can overide the file name, and chose an entirely different 5181 # file (which potentially could be of a different type and have different handler). 5185 [ $this,
$title, &$options, &$descQuery ] );
5186 # Don't register it now, as TraditionalImageGallery does that later. 5188 $handler =
$file ?
$file->getHandler() :
false;
5191 'img_alt' =>
'gallery-internal-alt',
5192 'img_link' =>
'gallery-internal-link',
5195 $paramMap += $handler->getParamMap();
5198 unset( $paramMap[
'img_width'] );
5201 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5206 $handlerOptions = [];
5220 foreach ( $parameterMatches as $parameterMatch ) {
5221 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5223 $paramName = $paramMap[$magicName];
5225 switch ( $paramName ) {
5226 case 'gallery-internal-alt':
5229 case 'gallery-internal-link':
5231 if ( preg_match(
'/^-{R|(.*)}-$/', $linkValue ) ) {
5234 $linkValue = substr( $linkValue, 4, -2 );
5237 if (
$type ===
'link-url' ) {
5239 $this->mOutput->addExternalLink( $target );
5240 } elseif (
$type ===
'link-title' ) {
5241 $link = $target->getLinkURL();
5242 $this->mOutput->addLink( $target );
5247 if ( $handler->validateParam( $paramName, $match ) ) {
5248 $handlerOptions[$paramName] = $match;
5251 $this->logger->debug(
5252 "$parameterMatch failed parameter validation" );
5253 $label = $parameterMatch;
5259 $label = $parameterMatch;
5264 $ig->add(
$title, $label, $alt, $link, $handlerOptions );
5266 $html = $ig->toHTML();
5267 Hooks::run(
'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5277 $handlerClass = get_class( $handler );
5281 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5282 # Initialise static lists 5283 static $internalParamNames = [
5284 'horizAlign' => [
'left',
'right',
'center',
'none' ],
5285 'vertAlign' => [
'baseline',
'sub',
'super',
'top',
'text-top',
'middle',
5286 'bottom',
'text-bottom' ],
5287 'frame' => [
'thumbnail',
'manualthumb',
'framed',
'frameless',
5288 'upright',
'border',
'link',
'alt',
'class' ],
5290 static $internalParamMap;
5291 if ( !$internalParamMap ) {
5292 $internalParamMap = [];
5293 foreach ( $internalParamNames as
$type => $names ) {
5294 foreach ( $names as $name ) {
5300 $magicName = str_replace(
'-',
'_',
"img_$name" );
5301 $internalParamMap[$magicName] = [
$type, $name ];
5306 # Add handler params 5307 $paramMap = $internalParamMap;
5309 $handlerParamMap = $handler->getParamMap();
5310 foreach ( $handlerParamMap as $magic => $paramName ) {
5311 $paramMap[$magic] = [
'handler', $paramName ];
5314 $this->mImageParams[$handlerClass] = $paramMap;
5315 $this->mImageParamsMagicArray[$handlerClass] =
5316 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5318 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5330 # Check if the options text is of the form "options|alt text" 5332 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang 5333 # * left no resizing, just left align. label is used for alt= only 5334 # * right same, but right aligned 5335 # * none same, but not aligned 5336 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox 5337 # * center center the image 5338 # * frame Keep original image size, no magnify-button. 5339 # * framed Same as "frame" 5340 # * frameless like 'thumb' but without a frame. Keeps user preferences for width 5341 # * upright reduce width for upright images, rounded to full __0 px 5342 # * border draw a 1px border around the image 5343 # * alt Text for HTML alt attribute (defaults to empty) 5344 # * class Set a class for img node 5345 # * link Set the target of the image link. Can be external, interwiki, or local 5346 # vertical-align values (no % or length right now): 5356 # Protect LanguageConverter markup when splitting into parts 5358 '-{',
'}-',
'|', $options,
true 5361 # Give extensions a chance to select the file revision for us 5365 [ $this,
$title, &$options, &$descQuery ] );
5366 # Fetch and register the file (file title may be different via hooks) 5370 $handler =
$file ?
$file->getHandler() :
false;
5378 # Process the input parameters 5380 $params = [
'frame' => [],
'handler' => [],
5381 'horizAlign' => [],
'vertAlign' => [] ];
5382 $seenformat =
false;
5383 foreach ( $parts as $part ) {
5384 $part = trim( $part );
5385 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5387 if ( isset( $paramMap[$magicName] ) ) {
5388 list(
$type, $paramName ) = $paramMap[$magicName];
5390 # Special case; width and height come in one variable together 5391 if (
$type ===
'handler' && $paramName ===
'width' ) {
5392 $parsedWidthParam = self::parseWidthParam( $value );
5393 if ( isset( $parsedWidthParam[
'width'] ) ) {
5394 $width = $parsedWidthParam[
'width'];
5395 if ( $handler->validateParam(
'width', $width ) ) {
5396 $params[
$type][
'width'] = $width;
5400 if ( isset( $parsedWidthParam[
'height'] ) ) {
5401 $height = $parsedWidthParam[
'height'];
5402 if ( $handler->validateParam(
'height', $height ) ) {
5403 $params[
$type][
'height'] = $height;
5407 # else no validation -- T15436 5409 if (
$type ===
'handler' ) {
5410 # Validate handler parameter 5411 $validated = $handler->validateParam( $paramName, $value );
5413 # Validate internal parameters 5414 switch ( $paramName ) {
5418 # @todo FIXME: Possibly check validity here for 5419 # manualthumb? downstream behavior seems odd with 5420 # missing manual thumbs. 5425 list( $paramName, $value ) =
5431 if ( $paramName ===
'no-link' ) {
5434 if ( ( $paramName ===
'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5435 $params[
$type][
'link-target'] = $this->mOptions->getExternalLinkTarget();
5443 $validated = !$seenformat;
5447 # Most other things appear to be empty or numeric... 5448 $validated = ( $value ===
false || is_numeric( trim( $value ) ) );
5453 $params[
$type][$paramName] = $value;
5457 if ( !$validated ) {
5462 # Process alignment parameters 5463 if ( $params[
'horizAlign'] ) {
5464 $params[
'frame'][
'align'] = key( $params[
'horizAlign'] );
5466 if ( $params[
'vertAlign'] ) {
5467 $params[
'frame'][
'valign'] = key( $params[
'vertAlign'] );
5470 $params[
'frame'][
'caption'] = $caption;
5472 # Will the image be presented in a frame, with the caption below? 5473 $imageIsFramed = isset( $params[
'frame'][
'frame'] )
5474 || isset( $params[
'frame'][
'framed'] )
5475 || isset( $params[
'frame'][
'thumbnail'] )
5476 || isset( $params[
'frame'][
'manualthumb'] );
5478 # In the old days, [[Image:Foo|text...]] would set alt text. Later it 5479 # came to also set the caption, ordinary text after the image -- which 5480 # makes no sense, because that just repeats the text multiple times in 5481 # screen readers. It *also* came to set the title attribute. 5482 # Now that we have an alt attribute, we should not set the alt text to 5483 # equal the caption: that's worse than useless, it just repeats the 5484 # text. This is the framed/thumbnail case. If there's no caption, we 5485 # use the unnamed parameter for alt text as well, just for the time be- 5486 # ing, if the unnamed param is set and the alt param is not. 5487 # For the future, we need to figure out if we want to tweak this more, 5488 # e.g., introducing a title= parameter for the title; ignoring the un- 5489 # named parameter entirely for images without a caption; adding an ex- 5490 # plicit caption= parameter and preserving the old magic unnamed para- 5492 if ( $imageIsFramed ) { # Framed image
5493 if ( $caption ===
'' && !isset( $params[
'frame'][
'alt'] ) ) {
5494 # No caption or alt text, add the filename as the alt text so 5495 # that screen readers at least get some description of the image 5496 $params[
'frame'][
'alt'] =
$title->getText();
5498 # Do not set $params['frame']['title'] because tooltips don't make sense 5500 }
else { # Inline image
5501 if ( !isset( $params[
'frame'][
'alt'] ) ) {
5502 # No alt text, use the "caption" for the alt text 5503 if ( $caption !==
'' ) {
5504 $params[
'frame'][
'alt'] = $this->
stripAltText( $caption, $holders );
5506 # No caption, fall back to using the filename for the 5508 $params[
'frame'][
'alt'] =
$title->getText();
5511 # Use the "caption" for the tooltip text 5512 $params[
'frame'][
'title'] = $this->
stripAltText( $caption, $holders );
5518 # Linker does the rest 5519 $time = $options[
'time'] ??
false;
5521 $time, $descQuery, $this->mOptions->getThumbSize() );
5523 # Give the handler a chance to modify the parser object 5525 $handler->parserTransformHook( $this,
$file );
5550 $chars = self::EXT_LINK_URL_CLASS;
5551 $addr = self::EXT_LINK_ADDR;
5555 if ( $value ===
'' ) {
5557 } elseif ( preg_match(
"/^((?i)$prots)/", $value ) ) {
5558 if ( preg_match(
"/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5559 $this->mOutput->addExternalLink( $value );
5566 $this->mOutput->addLink( $linkTitle );
5567 $type =
'link-title';
5568 $target = $linkTitle;
5571 return [
$type, $target ];
5580 # Strip bad stuff out of the title (tooltip). We can't just use 5581 # replaceLinkHoldersText() here, because if this function is called 5582 # from handleInternalLinks2(), mLinkHolders won't be up-to-date. 5584 $tooltip = $holders->replaceText( $caption );
5589 # make sure there are no placeholders in thumbnail attributes 5590 # that are later expanded to html- so expand them now and 5592 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5593 # Compatibility hack! In HTML certain entity references not terminated 5594 # by a semicolon are decoded (but not if we're in an attribute; that's 5595 # how link URLs get away without properly escaping & in queries). 5596 # But wikitext has always required semicolon-termination of entities, 5597 # so encode & where needed to avoid decode of semicolon-less entities. 5599 # https://www.w3.org/TR/html5/syntax.html#named-character-references 5600 # T210437 discusses moving this workaround to Sanitizer::stripAllTags. 5601 $tooltip = preg_replace(
"/ 5602 & # 1. entity prefix 5603 (?= # 2. followed by: 5604 (?: # a. one of the legacy semicolon-less named entities 5605 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)| 5606 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)| 5607 GT|I(?:acute|circ|grave|uml)|LT|Ntilde| 5608 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN| 5609 U(?:acute|circ|grave|uml)|Yacute| 5610 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar| 5611 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg| 5612 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)| 5613 frac(?:1(?:2|4)|34)| 5614 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)| 5615 i(?:acute|circ|excl|grave|quest|uml)|laquo| 5616 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)| 5617 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)| 5618 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)| 5619 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)| 5620 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)| 5621 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)| 5622 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml) 5624 (?:[^;]|$)) # b. and not followed by a semicolon 5625 # S = study, for efficiency 5626 /Sx",
'&', $tooltip );
5642 $text = $this->mStripState->unstripBoth( $text );
5654 array_keys( $this->mTransparentTagHooks ),
5655 array_keys( $this->mTagHooks ),
5656 array_keys( $this->mFunctionTagHooks )
5689 $elements = array_keys( $this->mTransparentTagHooks );
5690 $text = self::extractTagsAndParams( $elements, $text,
$matches );
5693 foreach (
$matches as $marker => $data ) {
5694 list( $element,
$content, $params, $tag ) = $data;
5695 $tagName = strtolower( $element );
5696 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5697 $output = call_user_func_array(
5698 $this->mTransparentTagHooks[$tagName],
5704 $replacements[$marker] = $output;
5706 return strtr( $text, $replacements );
5739 global
$wgTitle; # not generally used but removes an ugly failure mode
5741 $magicScopeVariable = $this->
lock();
5746 # Process section extraction flags 5748 $sectionParts = explode(
'-', $sectionId );
5749 $sectionIndex = array_pop( $sectionParts );
5750 foreach ( $sectionParts as $part ) {
5751 if ( $part ===
'T' ) {
5752 $flags |= self::PTD_FOR_INCLUSION;
5756 # Check for empty input 5757 if ( strval( $text ) ===
'' ) {
5758 # Only sections 0 and T-0 exist in an empty document 5759 if ( $sectionIndex == 0 ) {
5760 if ( $mode ===
'get' ) {
5766 if ( $mode ===
'get' ) {
5774 # Preprocess the text 5777 # <h> nodes indicate section breaks 5778 # They can only occur at the top level, so we can find them by iterating the root's children 5779 $node = $root->getFirstChild();
5781 # Find the target section 5782 if ( $sectionIndex == 0 ) {
5783 # Section zero doesn't nest, level=big 5784 $targetLevel = 1000;
5787 if ( $node->getName() ===
'h' ) {
5788 $bits = $node->splitHeading();
5789 if ( $bits[
'i'] == $sectionIndex ) {
5790 $targetLevel = $bits[
'level'];
5794 if ( $mode ===
'replace' ) {
5797 $node = $node->getNextSibling();
5803 if ( $mode ===
'get' ) {
5810 # Find the end of the section, including nested sections 5812 if ( $node->getName() ===
'h' ) {
5813 $bits = $node->splitHeading();
5814 $curLevel = $bits[
'level'];
5815 if ( $bits[
'i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5819 if ( $mode ===
'get' ) {
5822 $node = $node->getNextSibling();
5825 # Write out the remainder (in replace mode only) 5826 if ( $mode ===
'replace' ) {
5827 # Output the replacement text 5828 # Add two newlines on -- trailing whitespace in $newText is conventionally 5829 # stripped by the editor, so we need both newlines to restore the paragraph gap 5830 # Only add trailing whitespace if there is newText 5831 if ( $newText !=
"" ) {
5832 $outText .= $newText .
"\n\n";
5837 $node = $node->getNextSibling();
5841 if ( is_string( $outText ) ) {
5842 # Re-insert stripped tags 5843 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5863 public function getSection( $text, $sectionId, $defaultText =
'' ) {
5864 return $this->
extractSections( $text, $sectionId,
'get', $defaultText );
5880 return $this->
extractSections( $oldText, $sectionId,
'replace', $newText );
5912 $magicScopeVariable = $this->
lock();
5916 $node = $root->getFirstChild();
5929 if ( $node->getName() ===
'h' ) {
5930 $bits = $node->splitHeading();
5931 $sections[] = $currentSection;
5933 'index' => $bits[
'i'],
5934 'level' => $bits[
'level'],
5935 'offset' => $offset,
5936 'heading' => $nodeText,
5940 $currentSection[
'text'] .= $nodeText;
5942 $offset += strlen( $nodeText );
5943 $node = $node->getNextSibling();
5945 $sections[] = $currentSection;
5970 if ( $this->mRevisionObject ) {
5981 $rev = call_user_func(
5982 $this->mOptions->getCurrentRevisionCallback(),
5987 if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
6003 $this->mRevisionObject = $rev;
6014 if ( $this->mRevisionTimestamp !== null ) {
6018 # Use specified revision timestamp, falling back to the current timestamp 6020 $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
6021 $this->mOutput->setRevisionTimestampUsed( $timestamp );
6023 # The cryptic '' timezone parameter tells to use the site-default 6024 # timezone offset instead of the user settings. 6025 # Since this value will be saved into the parser cache, served 6026 # to other users, and potentially even used inside links and such, 6027 # it needs to be consistent for all visitors. 6028 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp,
'' );
6039 if ( is_null( $this->mRevisionUser ) ) {
6042 # if this template is subst: the revision id will be blank, 6043 # so just use the current user's name 6045 $this->mRevisionUser = $revObject->getUserText();
6046 } elseif ( $this->ot[
'wiki'] || $this->mOptions->getIsPreview() ) {
6047 $this->mRevisionUser = $this->
getUser()->getName();
6059 if ( is_null( $this->mRevisionSize ) ) {
6062 # if this variable is subst: the revision id will be blank, 6063 # so just use the parser input size, because the own substituation 6064 # will change the size. 6066 $this->mRevisionSize = $revObject->getSize();
6080 $this->mDefaultSort = $sort;
6081 $this->mOutput->setProperty(
'defaultsort', $sort );
6095 if ( $this->mDefaultSort !==
false ) {
6115 $text = self::normalizeSectionName( $text );
6124 $fragmentMode = $this->svcOptions->get(
'FragmentMode' );
6125 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] ===
'legacy' ) {
6144 # Strip out wikitext links(they break the anchor) 6146 $sectionName = self::getSectionNameFromStrippedText( $text );
6147 return self::makeAnchor( $sectionName );
6160 # Strip out wikitext links(they break the anchor) 6162 $sectionName = self::getSectionNameFromStrippedText( $text );
6172 $sectionName = self::getSectionNameFromStrippedText( $text );
6173 return self::makeAnchor( $sectionName );
6183 # T90902: ensure the same normalization is applied for IDs as to links 6185 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6186 '@phan-var MediaWikiTitleCodec $titleParser';
6189 $parts = $titleParser->splitTitleString(
"#$text" );
6193 return $parts[
'fragment'];
6211 # Strip internal link markup 6212 $text = preg_replace(
'/\[\[:?([^[|]+)\|([^[]+)\]\]/',
'$2', $text );
6213 $text = preg_replace(
'/\[\[:?([^[]+)\|?\]\]/',
'$1', $text );
6215 # Strip external link markup 6216 # @todo FIXME: Not tolerant to blank link text 6217 # I.E. [https://www.mediawiki.org] will render as [1] or something depending 6218 # on how many empty links there are on the page - need to figure that out. 6219 $text = preg_replace(
'/\[(?i:' . $this->mUrlProtocols .
')([^ ]+?) ([^[]+)\]/',
'$2', $text );
6221 # Parse wikitext quotes (italics & bold) 6242 $magicScopeVariable = $this->
lock();
6243 $this->
startParse( $title, $options, $outputType,
true );
6246 $text = $this->mStripState->unstripBoth( $text );
6290 while ( $i < strlen(
$s ) ) {
6291 $markerStart = strpos(
$s, self::MARKER_PREFIX, $i );
6292 if ( $markerStart ===
false ) {
6293 $out .= call_user_func( $callback, substr(
$s, $i ) );
6296 $out .= call_user_func( $callback, substr(
$s, $i, $markerStart - $i ) );
6297 $markerEnd = strpos(
$s, self::MARKER_SUFFIX, $markerStart );
6298 if ( $markerEnd ===
false ) {
6299 $out .= substr(
$s, $markerStart );
6302 $markerEnd += strlen( self::MARKER_SUFFIX );
6303 $out .= substr(
$s, $markerStart, $markerEnd - $markerStart );
6318 return $this->mStripState->killMarkers( $text );
6331 $parsedWidthParam = [];
6332 if ( $value ===
'' ) {
6333 return $parsedWidthParam;
6336 # (T15500) In both cases (width/height and width only), 6337 # permit trailing "px" for backward compatibility. 6338 if ( $parseHeight && preg_match(
'/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6339 $width = intval( $m[1] );
6340 $height = intval( $m[2] );
6341 $parsedWidthParam[
'width'] = $width;
6342 $parsedWidthParam[
'height'] = $height;
6343 } elseif ( preg_match(
'/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6344 $width = intval( $value );
6345 $parsedWidthParam[
'width'] = $width;
6347 return $parsedWidthParam;
6360 if ( $this->mInParse ) {
6361 throw new MWException(
"Parser state cleared while parsing. " 6362 .
"Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6368 $this->mInParse = $e->getTraceAsString();
6370 $recursiveCheck =
new ScopedCallback(
function () {
6371 $this->mInParse =
false;
6374 return $recursiveCheck;
6389 if ( preg_match(
'/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1],
'</p>' ) ===
false ) {
6407 if ( $this->mInParse ) {
6408 return $this->factory->create();
6422 $this->mOutput->setEnableOOUI(
true );
6430 $this->mOutput->setFlag( $flag );
6431 $name = $this->
getTitle()->getPrefixedText();
6432 $this->logger->debug( __METHOD__ .
": set $flag flag on '$name'; $reason" );
getRevisionObject()
Get the revision object for $this->mRevisionId.
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
static armorFrenchSpaces( $text, $space=' ')
Armor French spaces with a replacement character.
static register( $parser)
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
bool string $mInParse
Recursive call protection.
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
static tocLineEnd()
End a Table Of Contents line.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
cleanSig( $text, $parsing=false)
Clean up signature text.
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
LinkRenderer $mLinkRenderer
getRevisionUser()
Get the name of the user that edited the last revision.
handleMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
static tidy( $text)
Interface with Remex tidy.
SpecialPageFactory $specialPageFactory
killMarkers( $text)
Remove any strip markers found in the given text.
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
MapCacheLRU null $currentRevisionCache
BadFileLookup $badFileLookup
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
handleAllQuotes( $text)
Replace single quotes with HTML markup.
bool $mFirstCall
Whether firstCallInit still needs to be called.
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
getRevisionTimestampSubstring( $start, $len, $mtts, $variable)
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
static normalizeSubpageLink( $contextTitle, $target, &$text)
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated. ...
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
getFunctionHooks()
Get all registered function hook identifiers.
finalizeHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link...
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
wfHostname()
Fetch server name for use in error reporting etc.
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
startParse(?Title $title, ParserOptions $options, $outputType, $clearState=true)
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Title(Title $x=null)
Accessor/mutator for the Title object.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
SectionProfiler $mProfiler
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <...
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
There are three types of nodes:
clearTagHooks()
Remove all tag hooks.
clearState()
Clear Parser state.
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
armorLinks( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
LinkRendererFactory $linkRendererFactory
fuzzTestPst( $text, Title $title, ParserOptions $options)
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
getLinkRenderer()
Get a LinkRenderer instance to make links with.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target...
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
MagicWordArray $mVariables
static setupOOUI( $skinName='default', $dir='ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
getRevisionId()
Get the ID of the revision we are parsing.
limitationWarn( $limitationType, $current='', $max='')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
getPreSaveTransform()
Transform wiki markup when saving the page?
interwikiTransclude( $title, $action)
Transclude an interwiki link.
makeKnownLinkHolder( $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
static stripOuterParagraph( $html)
Strip outer.
const ID_FALLBACK
Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false if no fallback...
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
static getInstance( $ts=false)
Get a timestamp instance in GMT.
static numberingroup( $group)
Find the number of users in a given user group.
stripAltText( $caption, $holders)
setDefaultSort( $sort)
Mutator for $mDefaultSort.
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage()...
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
static validateTagAttributes( $attribs, $element)
Take an array of attribute names and values and normalize or discard illegal values for the given ele...
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed, encoded as plain text.
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
preprocess( $text, ?Title $title, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
__destruct()
Reduce memory usage to reduce the impact of circular references.
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness, which urlencode encodes by default.
getImageParams( $handler)
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
Factory for handling the special page list and generating SpecialPage objects.
static extractTagsAndParams( $elements, $text, &$matches)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
setUser( $user)
Set the current user.
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment...
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way...
Preprocessor $mPreprocessor
getPreprocessor()
Get a preprocessor object.
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores, returns the modified text.
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
static normalizeUrlComponent( $component, $unsafe)
static isValid( $ip)
Validate an IP address.
handleHeadings( $text)
Parse headers and return html.
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise...
setOutputFlag( $flag, $reason)
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
getStripList()
Get a list of strippable XML-like elements.
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
doQuotes( $text)
Helper function for handleAllQuotes()
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text...
setOutputType( $ot)
Set the output type.
getRevisionSize()
Get the size of the revision.
LinkHolderArray $mLinkHolders
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
handleInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
setTitle(Title $t=null)
Set the context title.
getTargetLanguage()
Get the target language for the content being parsed.
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
static decodeTagAttributes( $text)
Return an associative array of attribute names and values from a partial tag string.
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
getUserSig(&$user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext...
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
static makeAnchor( $sectionName)
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object...
static normalizeCharReferences( $text)
Ensure that any entities and character references are legal for XML and XHTML specifically.
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
MagicWordFactory $magicWordFactory
handleTables( $text)
Parse the wiki syntax used to render tables.
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
addTrackingCategory( $msg)
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers...
__clone()
Allow extensions to clean up when the parser is cloned.
Options( $x=null)
Accessor/mutator for the ParserOptions object.
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception, or through the on-wiki whitelist
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
static makeImageLink(Parser $parser, LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
MagicWordArray $mSubstWords
incrementIncludeSize( $type, $size)
Increment an include size counter.
static delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
pstPass2( $text, $user)
Pre-save transform helper function.
ServiceOptions $svcOptions
This is called $svcOptions instead of $options like elsewhere to avoid confusion with $mOptions...
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
resetOutput()
Reset the ParserOutput.
static escapeIdForLink( $id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
Variant of the Message class.
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
handleInternalLinks( $text)
Process [[ ]] wikilinks.
parseExtensionTagAsTopLevelDoc( $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order...
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
getContentLanguage()
Get the content language that this Parser is using.
lock()
Lock the current instance of the parser.
fuzzTestPreprocess( $text, Title $title, ParserOptions $options)
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static tocList( $toc, Language $lang=null)
Wraps the TOC in a table and provides the hide/collapse javascript.
static decodeCharReferences( $text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string...
getStripState()
Get the StripState.
handleExternalLinks( $text)
Replace external links (REL)
fuzzTestSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
getTitle()
Accessor for the Title object.
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
getOutput()
Get the ParserOutput object.
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
expandMagicVariable( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
static tocIndent()
Add another level to the Table of Contents.
static register( $parser)
static legalChars()
Get a regex character class describing the legal characters in a link.
static fixTagAttributes( $text, $element, $sorted=false)
Take a tag soup fragment listing an HTML element's attributes and normalize it to well-formed XML...
isCurrentRevisionOfTitleCached( $title)
makeLegacyAnchor( $sectionName)
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
incrementExpensiveFunctionCount()
Increment the expensive function count.
getDisableTitleConversion()
Whether title conversion should be disabled.
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
static removeHTMLtags( $text, $processCallback=null, $args=[], $extratags=[], $removetags=[], $warnCallback=null)
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments. ...
static getSectionNameFromStrippedText( $text)
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace...
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
__construct( $svcOptions=null, MagicWordFactory $magicWordFactory=null, Language $contLang=null, ParserFactory $factory=null, $urlProtocols=null, SpecialPageFactory $spFactory=null, $linkRendererFactory=null, $nsInfo=null, $logger=null, BadFileLookup $badFileLookup=null)
Constructing parsers directly is deprecated! Use a ParserFactory.
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
static getDefaultPreprocessorClass()
Which class should we use for the preprocessor if not otherwise specified?
insertStripItem( $text)
Add an item to the strip state Returns the unique tag which must be inserted into the stripped text T...
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
validateSig( $text)
Check that the user's signature contains no bad XML.
startExternalParse(?Title $title, ParserOptions $options, $outputType, $clearState=true, $revId=null)
Set up some variables which are usually set up in parse() so that an external function can call some ...
static normalizeSectionNameWhitespace( $section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(), for use in the id's that are used for section links.
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
getOptions()
Get the ParserOptions object.
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
OutputType( $x=null)
Accessor/mutator for the output type.
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
static delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
parse( $text, Title $title, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.