30use Psr\Log\NullLogger;
31use Wikimedia\ScopedCallback;
32use Psr\Log\LoggerInterface;
88 # Flags for Parser::setFunctionHook
92 # Constants needed for external link processing
93 # Everything except bracket, space, or control characters
94 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
95 # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
96 # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
97 # uses to replace invalid HTML characters.
99 # Simplified expression to match an IPv4 or IPv6 address, or
100 # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
101 const EXT_LINK_ADDR =
'(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
102 # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
104 const EXT_IMAGE_REGEX =
'/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
105 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
107 # Regular expression for a non-newline space
110 # Flags for preprocessToDom
113 # Allowed values for $this->mOutputType
114 # Parameter to startExternalParse().
119 const
OT_PLAIN = 4;
# like extractSections() - portions of the original are returned unchanged.
138 const MARKER_SUFFIX =
"-QINU`\"'\x7f";
141 # Markers used for wrapping the table of contents
163 public $mFirstCall =
true;
165 # Initialised by initializeVariables()
183 # Initialised in constructor
186 # Initialized in getPreprocessor()
190 # Cleared with clearState():
220 public $mUser; #
User object; only used when doing pre-save transform
223 # These are variables reset at least once per parse regardless of $clearState
237 public $mTitle; #
Title context, used
for self-link rendering and similar things
240 public $mRevisionObject;
# The revision object of the specified revision ID
245 public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
322 'EnableScaryTranscluding',
323 'ExtraInterlanguageLinkPrefixes',
335 'TranscludeCacheExpiry',
357 $urlProtocols =
null,
359 $linkRendererFactory =
null,
368 if ( empty( $this->mConf[
'class'] ) ) {
369 $this->mConf[
'class'] = self::class;
371 if ( empty( $this->mConf[
'preprocessorClass'] ) ) {
372 $this->mConf[
'preprocessorClass'] = self::getDefaultPreprocessorClass();
374 $this->svcOptions =
new ServiceOptions( self::$constructorOptions,
375 $this->mConf, func_num_args() > 6
376 ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
379 $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) :
null;
393 $this->mExtLinkBracketedRegex =
'/\[(((?i)' . $this->mUrlProtocols .
')' .
394 self::EXT_LINK_ADDR .
395 self::EXT_LINK_URL_CLASS .
'*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
398 MediaWikiServices::getInstance()->getMagicWordFactory();
400 $this->contLang =
$contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
402 $this->factory =
$factory ?? MediaWikiServices::getInstance()->getParserFactory();
403 $this->specialPageFactory = $spFactory ??
404 MediaWikiServices::getInstance()->getSpecialPageFactory();
406 MediaWikiServices::getInstance()->getLinkRendererFactory();
407 $this->nsInfo =
$nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
408 $this->logger =
$logger ?:
new NullLogger();
410 MediaWikiServices::getInstance()->getBadFileLookup();
417 if ( isset( $this->mLinkHolders ) ) {
419 unset( $this->mLinkHolders );
422 foreach ( $this as $name => $value ) {
423 unset( $this->$name );
431 $this->mInParse =
false;
439 foreach ( [
'mStripState',
'mVarCache' ] as $k ) {
447 Hooks::run(
'ParserCloned', [ $this ] );
458 return Preprocessor_Hash::class;
465 if ( !$this->mFirstCall ) {
468 $this->mFirstCall =
false;
476 Hooks::run(
'ParserFirstCallInit', [ &$parser ] );
487 $this->mAutonumber = 0;
488 $this->mIncludeCount = [];
491 $this->mRevisionObject = $this->mRevisionTimestamp =
492 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize =
null;
493 $this->mVarCache = [];
495 $this->mLangLinkLanguages = [];
496 $this->currentRevisionCache =
null;
500 # Clear these on every parse, T6549
501 $this->mTplRedirCache = $this->mTplDomCache = [];
503 $this->mShowToc =
true;
504 $this->mForceTocPosition =
false;
505 $this->mIncludeSizes = [
509 $this->mPPNodeCount = 0;
510 $this->mGeneratedPPNodeCount = 0;
511 $this->mHighestExpansionDepth = 0;
512 $this->mDefaultSort =
false;
513 $this->mHeadings = [];
514 $this->mDoubleUnderscores = [];
515 $this->mExpensiveFunctionCount = 0;
518 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
519 $this->mPreprocessor =
null;
526 Hooks::run(
'ParserClearState', [ &$parser ] );
534 $this->mOptions->registerWatcher( [ $this->mOutput,
'recordOption' ] );
556 $linestart =
true, $clearState =
true, $revid =
null
561 $text = strtr( $text,
"\x7f",
"?" );
562 $magicScopeVariable = $this->
lock();
565 $text = str_replace(
"\000",
'', $text );
569 $this->currentRevisionCache =
null;
570 $this->mInputSize = strlen( $text );
571 if ( $this->mOptions->getEnableLimitReport() ) {
572 $this->mOutput->resetParseStartTime();
575 $oldRevisionId = $this->mRevisionId;
576 $oldRevisionObject = $this->mRevisionObject;
577 $oldRevisionTimestamp = $this->mRevisionTimestamp;
578 $oldRevisionUser = $this->mRevisionUser;
579 $oldRevisionSize = $this->mRevisionSize;
580 if ( $revid !==
null ) {
581 $this->mRevisionId = $revid;
582 $this->mRevisionObject =
null;
583 $this->mRevisionTimestamp =
null;
584 $this->mRevisionUser =
null;
585 $this->mRevisionSize =
null;
590 Hooks::run(
'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
592 Hooks::run(
'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
594 Hooks::run(
'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
606 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] )
607 || isset( $this->mDoubleUnderscores[
'notitleconvert'] )
608 || $this->mOutput->getDisplayTitle() !==
false )
611 if ( $convruletitle ) {
612 $this->mOutput->setTitleText( $convruletitle );
615 $this->mOutput->setTitleText( $titleText );
619 # Compute runtime adaptive expiry if set
620 $this->mOutput->finalizeAdaptiveCacheExpiry();
622 # Warn if too many heavyweight parser functions were used
623 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
625 $this->mExpensiveFunctionCount,
626 $this->mOptions->getExpensiveParserFunctionLimit()
630 # Information on limits, for the benefit of users who try to skirt them
631 if ( $this->mOptions->getEnableLimitReport() ) {
635 # Wrap non-interface parser output in a <div> so it can be targeted
637 $class = $this->mOptions->getWrapOutputClass();
638 if ( $class !==
false && !$this->mOptions->getInterfaceMessage() ) {
639 $this->mOutput->addWrapperDivClass( $class );
642 $this->mOutput->setText( $text );
644 $this->mRevisionId = $oldRevisionId;
645 $this->mRevisionObject = $oldRevisionObject;
646 $this->mRevisionTimestamp = $oldRevisionTimestamp;
647 $this->mRevisionUser = $oldRevisionUser;
648 $this->mRevisionSize = $oldRevisionSize;
649 $this->mInputSize =
false;
650 $this->currentRevisionCache =
null;
652 return $this->mOutput;
662 $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
664 $cpuTime = $this->mOutput->getTimeSinceStart(
'cpu' );
665 if ( $cpuTime !==
null ) {
666 $this->mOutput->setLimitReportData(
'limitreport-cputime',
667 sprintf(
"%.3f", $cpuTime )
671 $wallTime = $this->mOutput->getTimeSinceStart(
'wall' );
672 $this->mOutput->setLimitReportData(
'limitreport-walltime',
673 sprintf(
"%.3f", $wallTime )
676 $this->mOutput->setLimitReportData(
'limitreport-ppvisitednodes',
677 [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
679 $this->mOutput->setLimitReportData(
'limitreport-ppgeneratednodes',
680 [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
682 $this->mOutput->setLimitReportData(
'limitreport-postexpandincludesize',
683 [ $this->mIncludeSizes[
'post-expand'], $maxIncludeSize ]
685 $this->mOutput->setLimitReportData(
'limitreport-templateargumentsize',
686 [ $this->mIncludeSizes[
'arg'], $maxIncludeSize ]
688 $this->mOutput->setLimitReportData(
'limitreport-expansiondepth',
689 [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
691 $this->mOutput->setLimitReportData(
'limitreport-expensivefunctioncount',
692 [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
695 foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
696 $this->mOutput->setLimitReportData( $key, $value );
699 Hooks::run(
'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
701 $limitReport =
"NewPP limit report\n";
702 if ( $this->svcOptions->get(
'ShowHostnames' ) ) {
703 $limitReport .=
'Parsed by ' .
wfHostname() .
"\n";
705 $limitReport .=
'Cached time: ' . $this->mOutput->getCacheTime() .
"\n";
706 $limitReport .=
'Cache expiry: ' . $this->mOutput->getCacheExpiry() .
"\n";
707 $limitReport .=
'Dynamic content: ' .
708 ( $this->mOutput->hasDynamicContent() ?
'true' :
'false' ) .
710 $limitReport .=
'Complications: [' . implode(
', ', $this->mOutput->getAllFlags() ) .
"]\n";
712 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
713 if ( Hooks::run(
'ParserLimitReportFormat',
714 [ $key, &$value, &$limitReport,
false,
false ]
716 $keyMsg =
wfMessage( $key )->inLanguage(
'en' )->useDatabase(
false );
717 $valueMsg =
wfMessage( [
"$key-value-text",
"$key-value" ] )
718 ->inLanguage(
'en' )->useDatabase(
false );
719 if ( !$valueMsg->exists() ) {
722 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
723 $valueMsg->params( $value );
724 $limitReport .=
"{$keyMsg->text()}: {$valueMsg->text()}\n";
730 $limitReport = htmlspecialchars_decode( $limitReport );
734 $limitReport = str_replace( [
'-',
'&' ], [
'‐',
'&' ], $limitReport );
735 $text =
"\n<!-- \n$limitReport-->\n";
738 $dataByFunc = $this->mProfiler->getFunctionStats();
739 uasort( $dataByFunc,
function ( $a, $b ) {
740 return $b[
'real'] <=> $a[
'real'];
743 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
744 $profileReport[] = sprintf(
"%6.2f%% %8.3f %6d %s",
745 $item[
'%real'], $item[
'real'], $item[
'calls'],
746 htmlspecialchars( $item[
'name'] ) );
748 $text .=
"<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
749 $text .= implode(
"\n", $profileReport ) .
"\n-->\n";
751 $this->mOutput->setLimitReportData(
'limitreport-timingprofile', $profileReport );
754 if ( $this->svcOptions->get(
'ShowHostnames' ) ) {
755 $this->mOutput->setLimitReportData(
'cachereport-origin',
wfHostname() );
757 $this->mOutput->setLimitReportData(
'cachereport-timestamp',
758 $this->mOutput->getCacheTime() );
759 $this->mOutput->setLimitReportData(
'cachereport-ttl',
760 $this->mOutput->getCacheExpiry() );
761 $this->mOutput->setLimitReportData(
'cachereport-transientcontent',
762 $this->mOutput->hasDynamicContent() );
764 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
765 wfDebugLog(
'generated-pp-node-count', $this->mGeneratedPPNodeCount .
' ' .
766 $this->mTitle->getPrefixedDBkey() );
798 Hooks::run(
'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
799 Hooks::run(
'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
843 $magicScopeVariable = $this->
lock();
845 if ( $revid !==
null ) {
846 $this->mRevisionId = $revid;
850 Hooks::run(
'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
851 Hooks::run(
'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
853 $text = $this->mStripState->unstripBoth( $text );
868 $text = $this->mStripState->unstripBoth( $text );
887 $text = $msg->params( $params )->plain();
889 # Parser (re)initialisation
890 $magicScopeVariable = $this->
lock();
893 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
896 $text = $this->mStripState->unstripBoth( $text );
907 $this->mUser = $user;
920 if (
$t->hasFragment() ) {
921 # Strip the fragment to avoid various odd effects
922 $this->mTitle =
$t->createFragmentTarget(
'' );
936 if ( $this->mTitle === null ) {
937 wfDeprecated(
'Parser title should never be null',
'1.34' );
958 $this->mOutputType = $ot;
975 return wfSetVar( $this->mOutputType, $x );
984 return $this->mOutput;
993 return $this->mOptions;
1003 return wfSetVar( $this->mOptions, $x );
1010 return $this->mLinkID++;
1017 $this->mLinkID = $id;
1025 return $this->getTargetLanguage();
1038 $target = $this->mOptions->getTargetLanguage();
1040 if ( $target !==
null ) {
1042 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1043 return $this->mOptions->getUserLangObj();
1044 } elseif ( is_null( $this->mTitle ) ) {
1045 throw new MWException( __METHOD__ .
': $this->mTitle is null' );
1048 return $this->mTitle->getPageLanguage();
1058 return $this->getTargetLanguage();
1068 if ( !is_null( $this->mUser ) ) {
1069 return $this->mUser;
1071 return $this->mOptions->getUser();
1080 if ( !isset( $this->mPreprocessor ) ) {
1081 $class = $this->svcOptions->get(
'preprocessorClass' );
1082 $this->mPreprocessor =
new $class( $this );
1084 return $this->mPreprocessor;
1095 if ( !$this->mLinkRenderer ) {
1096 $this->mLinkRenderer = $this->linkRendererFactory->create();
1097 $this->mLinkRenderer->setStubThreshold(
1098 $this->getOptions()->getStubThreshold()
1102 return $this->mLinkRenderer;
1112 return $this->magicWordFactory;
1122 return $this->contLang;
1149 $taglist = implode(
'|', $elements );
1150 $start =
"/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1152 while ( $text !=
'' ) {
1153 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1155 if ( count( $p ) < 5 ) {
1158 if ( count( $p ) > 5 ) {
1166 list( , $element, $attributes, $close, $inside ) = $p;
1169 $marker = self::MARKER_PREFIX .
"-$element-" . sprintf(
'%08X', $n++ ) . self::MARKER_SUFFIX;
1170 $stripped .= $marker;
1172 if ( $close ===
'/>' ) {
1173 # Empty element tag, <tag />
1178 if ( $element ===
'!--' ) {
1181 $end =
"/(<\\/$element\\s*>)/i";
1183 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1185 if ( count( $q ) < 3 ) {
1186 # No end tag -- let it run out to the end of the text.
1190 list( , $tail, $text ) = $q;
1196 Sanitizer::decodeTagAttributes( $attributes ),
1197 "<$element$attributes$close$content$tail" ];
1208 return $this->mStripList;
1217 return $this->mStripState;
1230 $marker = self::MARKER_PREFIX .
"-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1231 $this->mMarkerIndex++;
1232 $this->mStripState->addGeneral( $marker, $text );
1246 return $this->handleTables( $text );
1256 $lines = StringUtils::explode(
"\n", $text );
1258 $td_history = []; # Is currently a td tag open?
1259 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1260 $tr_history = []; # Is currently a tr tag open?
1261 $tr_attributes = []; # history of tr attributes
1262 $has_opened_tr = []; # Did
this table open a <tr> element?
1263 $indent_level = 0; # indent level of the table
1265 foreach (
$lines as $outLine ) {
1266 $line = trim( $outLine );
1268 if (
$line ===
'' ) { # empty line, go to next line
1269 $out .= $outLine .
"\n";
1273 $first_character =
$line[0];
1274 $first_two = substr(
$line, 0, 2 );
1277 if ( preg_match(
'/^(:*)\s*\{\|(.*)$/',
$line,
$matches ) ) {
1278 # First check if we are starting a new table
1279 $indent_level = strlen(
$matches[1] );
1281 $attributes = $this->mStripState->unstripBoth(
$matches[2] );
1282 $attributes = Sanitizer::fixTagAttributes( $attributes,
'table' );
1284 $outLine = str_repeat(
'<dl><dd>', $indent_level ) .
"<table{$attributes}>";
1285 array_push( $td_history,
false );
1286 array_push( $last_tag_history,
'' );
1287 array_push( $tr_history,
false );
1288 array_push( $tr_attributes,
'' );
1289 array_push( $has_opened_tr,
false );
1290 } elseif ( count( $td_history ) == 0 ) {
1291 # Don't do any of the following
1292 $out .= $outLine .
"\n";
1294 } elseif ( $first_two ===
'|}' ) {
1295 # We are ending a table
1297 $last_tag = array_pop( $last_tag_history );
1299 if ( !array_pop( $has_opened_tr ) ) {
1300 $line =
"<tr><td></td></tr>{$line}";
1303 if ( array_pop( $tr_history ) ) {
1304 $line =
"</tr>{$line}";
1307 if ( array_pop( $td_history ) ) {
1308 $line =
"</{$last_tag}>{$line}";
1310 array_pop( $tr_attributes );
1311 if ( $indent_level > 0 ) {
1312 $outLine = rtrim(
$line ) . str_repeat(
'</dd></dl>', $indent_level );
1316 } elseif ( $first_two ===
'|-' ) {
1317 # Now we have a table row
1318 $line = preg_replace(
'#^\|-+#',
'',
$line );
1320 # Whats after the tag is now only attributes
1321 $attributes = $this->mStripState->unstripBoth(
$line );
1322 $attributes = Sanitizer::fixTagAttributes( $attributes,
'tr' );
1323 array_pop( $tr_attributes );
1324 array_push( $tr_attributes, $attributes );
1327 $last_tag = array_pop( $last_tag_history );
1328 array_pop( $has_opened_tr );
1329 array_push( $has_opened_tr,
true );
1331 if ( array_pop( $tr_history ) ) {
1335 if ( array_pop( $td_history ) ) {
1336 $line =
"</{$last_tag}>{$line}";
1340 array_push( $tr_history,
false );
1341 array_push( $td_history,
false );
1342 array_push( $last_tag_history,
'' );
1343 } elseif ( $first_character ===
'|'
1344 || $first_character ===
'!'
1345 || $first_two ===
'|+'
1347 # This might be cell elements, td, th or captions
1348 if ( $first_two ===
'|+' ) {
1349 $first_character =
'+';
1356 if ( $first_character ===
'!' ) {
1357 $line = StringUtils::replaceMarkup(
'!!',
'||',
$line );
1360 # Split up multiple cells on the same line.
1361 # FIXME : This can result in improper nesting of tags processed
1362 # by earlier parser steps.
1363 $cells = explode(
'||',
$line );
1367 # Loop through each table cell
1368 foreach ( $cells as $cell ) {
1370 if ( $first_character !==
'+' ) {
1371 $tr_after = array_pop( $tr_attributes );
1372 if ( !array_pop( $tr_history ) ) {
1373 $previous =
"<tr{$tr_after}>\n";
1375 array_push( $tr_history,
true );
1376 array_push( $tr_attributes,
'' );
1377 array_pop( $has_opened_tr );
1378 array_push( $has_opened_tr,
true );
1381 $last_tag = array_pop( $last_tag_history );
1383 if ( array_pop( $td_history ) ) {
1384 $previous =
"</{$last_tag}>\n{$previous}";
1387 if ( $first_character ===
'|' ) {
1389 } elseif ( $first_character ===
'!' ) {
1391 } elseif ( $first_character ===
'+' ) {
1392 $last_tag =
'caption';
1397 array_push( $last_tag_history, $last_tag );
1399 # A cell could contain both parameters and data
1400 $cell_data = explode(
'|', $cell, 2 );
1402 # T2553: Note that a '|' inside an invalid link should not
1403 # be mistaken as delimiting cell parameters
1404 # Bug T153140: Neither should language converter markup.
1405 if ( preg_match(
'/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1406 $cell =
"{$previous}<{$last_tag}>" . trim( $cell );
1407 } elseif ( count( $cell_data ) == 1 ) {
1409 $cell =
"{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1411 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1412 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1414 $cell =
"{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1418 array_push( $td_history,
true );
1421 $out .= $outLine .
"\n";
1424 # Closing open td, tr && table
1425 while ( count( $td_history ) > 0 ) {
1426 if ( array_pop( $td_history ) ) {
1429 if ( array_pop( $tr_history ) ) {
1432 if ( !array_pop( $has_opened_tr ) ) {
1433 $out .=
"<tr><td></td></tr>\n";
1436 $out .=
"</table>\n";
1439 # Remove trailing line-ending (b/c)
1440 if ( substr( $out, -1 ) ===
"\n" ) {
1441 $out = substr( $out, 0, -1 );
1444 # special case: don't return empty table
1445 if ( $out ===
"<table>\n<tr><td></td></tr>\n</table>" ) {
1471 # Hook to suspend the parser in this state
1472 if ( !Hooks::run(
'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1476 # if $frame is provided, then use $frame for replacing any variables
1478 # use frame depth to infer how include/noinclude tags should be handled
1479 # depth=0 means this is the top-level document; otherwise it's an included document
1480 if ( !$frame->depth ) {
1483 $flag = self::PTD_FOR_INCLUSION;
1485 $dom = $this->preprocessToDom( $text, $flag );
1486 $text = $frame->expand( $dom );
1488 # if $frame is not provided, then use old-style replaceVariables
1489 $text = $this->replaceVariables( $text );
1492 Hooks::run(
'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1493 $text = Sanitizer::removeHTMLtags(
1495 [ $this,
'attributeStripCallback' ],
1497 array_keys( $this->mTransparentTagHooks ),
1499 [ $this,
'addTrackingCategory' ]
1501 Hooks::run(
'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1503 # Tables need to come after variable replacement for things to work
1504 # properly; putting them before other transformations should keep
1505 # exciting things like link expansions from showing up in surprising
1507 $text = $this->handleTables( $text );
1509 $text = preg_replace(
'/(^|\n)-----*/',
'\\1<hr />', $text );
1511 $text = $this->handleDoubleUnderscore( $text );
1513 $text = $this->handleHeadings( $text );
1514 $text = $this->handleInternalLinks( $text );
1515 $text = $this->handleAllQuotes( $text );
1516 $text = $this->handleExternalLinks( $text );
1518 # handleInternalLinks may sometimes leave behind
1519 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1520 $text = str_replace( self::MARKER_PREFIX .
'NOPARSE',
'', $text );
1522 $text = $this->handleMagicLinks( $text );
1523 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1538 $text = $this->mStripState->unstripGeneral( $text );
1544 Hooks::run(
'ParserAfterUnstrip', [ &$parser, &$text ] );
1547 # Clean up special characters, only run once, next-to-last before doBlockLevels
1548 $text = Sanitizer::armorFrenchSpaces( $text );
1550 $text = $this->doBlockLevels( $text, $linestart );
1552 $this->replaceLinkHoldersPrivate( $text );
1561 if ( !( $this->mOptions->getDisableContentConversion()
1562 || isset( $this->mDoubleUnderscores[
'nocontentconvert'] ) )
1563 && !$this->mOptions->getInterfaceMessage()
1565 # The position of the convert() call should not be changed. it
1566 # assumes that the links are all replaced and the only thing left
1567 # is the <nowiki> mark.
1568 $text = $this->getTargetLanguage()->convert( $text );
1571 $text = $this->mStripState->unstripNoWiki( $text );
1574 Hooks::run(
'ParserBeforeTidy', [ &$parser, &$text ] );
1577 $text = $this->replaceTransparentTags( $text );
1578 $text = $this->mStripState->unstripGeneral( $text );
1580 $text = Sanitizer::normalizeCharReferences( $text );
1582 if ( MWTidy::isEnabled() ) {
1583 if ( $this->mOptions->getTidy() ) {
1584 $text = MWTidy::tidy( $text );
1587 # attempt to sanitize at least some nesting problems
1588 # (T4702 and quite a few others)
1589 # This code path is buggy and deprecated!
1592 # ''Something [http:
1593 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1594 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1595 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1596 # fix up an anchor inside another anchor, only
1597 # at least for a single single nested link (T5695)
1598 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1599 '\\1\\2</a>\\3</a>\\1\\4</a>',
1600 # fix div inside inline elements- doBlockLevels won't wrap a line which
1601 # contains a div, so fix it up here; replace
1602 # div with escaped text
1603 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1604 '\\1\\3<div\\5>\\6</div>\\8\\9',
1605 # remove empty italic or bold tag pairs, some
1606 # introduced by rules above
1607 '/<([bi])><\/\\1>/' =>
'',
1610 $text = preg_replace(
1611 array_keys( $tidyregs ),
1612 array_values( $tidyregs ),
1617 Hooks::run(
'ParserAfterTidy', [ &$parser, &$text ] );
1635 return $this->handleMagicLinks( $text );
1650 $urlChar = self::EXT_LINK_URL_CLASS;
1651 $addr = self::EXT_LINK_ADDR;
1652 $space = self::SPACE_NOT_NL; # non-newline space
1653 $spdash =
"(?:-|$space)"; # a dash or a non-newline space
1654 $spaces =
"$space++"; # possessive match of 1 or more spaces
1655 $text = preg_replace_callback(
1657 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1658 (<.*?>) | # m[2]: Skip stuff inside HTML elements' .
"
1659 (\b # m[3]: Free external links
1661 ($addr$urlChar*) # m[4]: Post-protocol path
1663 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1665 \bISBN $spaces ( # m[6]: ISBN, capture number
1666 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1667 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1668 [0-9Xx] # check digit
1670 )!xu", [ $this,
'magicLinkCallback' ], $text );
1680 if ( isset( $m[1] ) && $m[1] !==
'' ) {
1683 } elseif ( isset( $m[2] ) && $m[2] !==
'' ) {
1686 } elseif ( isset( $m[3] ) && $m[3] !==
'' ) {
1687 # Free external link
1688 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1689 } elseif ( isset( $m[5] ) && $m[5] !==
'' ) {
1691 if ( substr( $m[0], 0, 3 ) ===
'RFC' ) {
1692 if ( !$this->mOptions->getMagicRFCLinks() ) {
1697 $cssClass =
'mw-magiclink-rfc';
1698 $trackingCat =
'magiclink-tracking-rfc';
1700 } elseif ( substr( $m[0], 0, 4 ) ===
'PMID' ) {
1701 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1705 $urlmsg =
'pubmedurl';
1706 $cssClass =
'mw-magiclink-pmid';
1707 $trackingCat =
'magiclink-tracking-pmid';
1710 throw new MWException( __METHOD__ .
': unrecognised match type "' .
1711 substr( $m[0], 0, 20 ) .
'"' );
1713 $url =
wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1714 $this->addTrackingCategory( $trackingCat );
1716 } elseif ( isset( $m[6] ) && $m[6] !==
''
1717 && $this->mOptions->getMagicISBNLinks()
1721 $space = self::SPACE_NOT_NL; # non-newline space
1722 $isbn = preg_replace(
"/$space/",
' ', $isbn );
1723 $num = strtr( $isbn, [
1728 $this->addTrackingCategory(
'magiclink-tracking-isbn' );
1729 return $this->getLinkRenderer()->makeKnownLink(
1730 SpecialPage::getTitleFor(
'Booksources', $num ),
1733 'class' =>
'internal mw-magiclink-isbn',
1754 # The characters '<' and '>' (which were escaped by
1755 # removeHTMLtags()) should not be included in
1756 # URLs, per RFC 2396.
1757 # Make terminate a URL as well (bug T84937)
1760 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1765 $trail = substr( $url, $m2[0][1] ) . $trail;
1766 $url = substr( $url, 0, $m2[0][1] );
1769 # Move trailing punctuation to $trail
1771 # If there is no left bracket, then consider right brackets fair game too
1772 if ( strpos( $url,
'(' ) ===
false ) {
1776 $urlRev = strrev( $url );
1777 $numSepChars = strspn( $urlRev, $sep );
1778 # Don't break a trailing HTML entity by moving the ; into $trail
1779 # This is in hot code, so use substr_compare to avoid having to
1780 # create a new string object for the comparison
1781 if ( $numSepChars && substr_compare( $url,
";", -$numSepChars, 1 ) === 0 ) {
1782 # more optimization: instead of running preg_match with a $
1783 # anchor, which can be slow, do the match on the reversed
1784 # string starting at the desired offset.
1785 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1786 if ( preg_match(
'/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1790 if ( $numSepChars ) {
1791 $trail = substr( $url, -$numSepChars ) . $trail;
1792 $url = substr( $url, 0, -$numSepChars );
1795 # Verify that we still have a real URL after trail removal, and
1796 # not just lone protocol
1797 if ( strlen( $trail ) >= $numPostProto ) {
1798 return $url . $trail;
1801 $url = Sanitizer::cleanUrl( $url );
1803 # Is this an external image?
1804 $text = $this->maybeMakeExternalImage( $url );
1805 if ( $text ===
false ) {
1806 # Not an image, make a link
1808 $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1810 $this->getExternalLinkAttribs( $url ), $this->mTitle );
1811 # Register it in the output object...
1812 $this->mOutput->addExternalLink( $url );
1814 return $text . $trail;
1827 return $this->handleHeadings( $text );
1837 for ( $i = 6; $i >= 1; --$i ) {
1838 $h = str_repeat(
'=', $i );
1841 $text = preg_replace(
"/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m",
"<h$i>\\1</h$i>", $text );
1857 return $this->handleAllQuotes( $text );
1869 $lines = StringUtils::explode(
"\n", $text );
1871 $outtext .= $this->doQuotes(
$line ) .
"\n";
1873 $outtext = substr( $outtext, 0, -1 );
1886 $arr = preg_split(
"/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1887 $countarr = count( $arr );
1888 if ( $countarr == 1 ) {
1897 for ( $i = 1; $i < $countarr; $i += 2 ) {
1898 $thislen = strlen( $arr[$i] );
1902 if ( $thislen == 4 ) {
1903 $arr[$i - 1] .=
"'";
1906 } elseif ( $thislen > 5 ) {
1910 $arr[$i - 1] .= str_repeat(
"'", $thislen - 5 );
1915 if ( $thislen == 2 ) {
1917 } elseif ( $thislen == 3 ) {
1919 } elseif ( $thislen == 5 ) {
1929 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1930 $firstsingleletterword = -1;
1931 $firstmultiletterword = -1;
1933 for ( $i = 1; $i < $countarr; $i += 2 ) {
1934 if ( strlen( $arr[$i] ) == 3 ) {
1935 $x1 = substr( $arr[$i - 1], -1 );
1936 $x2 = substr( $arr[$i - 1], -2, 1 );
1937 if ( $x1 ===
' ' ) {
1938 if ( $firstspace == -1 ) {
1941 } elseif ( $x2 ===
' ' ) {
1942 $firstsingleletterword = $i;
1946 } elseif ( $firstmultiletterword == -1 ) {
1947 $firstmultiletterword = $i;
1953 if ( $firstsingleletterword > -1 ) {
1954 $arr[$firstsingleletterword] =
"''";
1955 $arr[$firstsingleletterword - 1] .=
"'";
1956 } elseif ( $firstmultiletterword > -1 ) {
1958 $arr[$firstmultiletterword] =
"''";
1959 $arr[$firstmultiletterword - 1] .=
"'";
1960 } elseif ( $firstspace > -1 ) {
1964 $arr[$firstspace] =
"''";
1965 $arr[$firstspace - 1] .=
"'";
1974 foreach ( $arr as $r ) {
1975 if ( ( $i % 2 ) == 0 ) {
1976 if ( $state ===
'both' ) {
1982 $thislen = strlen( $r );
1983 if ( $thislen == 2 ) {
1984 if ( $state ===
'i' ) {
1987 } elseif ( $state ===
'bi' ) {
1990 } elseif ( $state ===
'ib' ) {
1991 $output .=
'</b></i><b>';
1993 } elseif ( $state ===
'both' ) {
1994 $output .=
'<b><i>' . $buffer .
'</i>';
2000 } elseif ( $thislen == 3 ) {
2001 if ( $state ===
'b' ) {
2004 } elseif ( $state ===
'bi' ) {
2005 $output .=
'</i></b><i>';
2007 } elseif ( $state ===
'ib' ) {
2010 } elseif ( $state ===
'both' ) {
2011 $output .=
'<i><b>' . $buffer .
'</b>';
2017 } elseif ( $thislen == 5 ) {
2018 if ( $state ===
'b' ) {
2019 $output .=
'</b><i>';
2021 } elseif ( $state ===
'i' ) {
2022 $output .=
'</i><b>';
2024 } elseif ( $state ===
'bi' ) {
2025 $output .=
'</i></b>';
2027 } elseif ( $state ===
'ib' ) {
2028 $output .=
'</b></i>';
2030 } elseif ( $state ===
'both' ) {
2031 $output .=
'<i><b>' . $buffer .
'</b></i>';
2042 if ( $state ===
'b' || $state ===
'ib' ) {
2045 if ( $state ===
'i' || $state ===
'bi' || $state ===
'ib' ) {
2048 if ( $state ===
'bi' ) {
2052 if ( $state ===
'both' && $buffer ) {
2053 $output .=
'<b><i>' . $buffer .
'</i></b>';
2073 return $this->handleExternalLinks( $text );
2087 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2089 if ( $bits ===
false ) {
2090 throw new MWException(
"PCRE needs to be compiled with "
2091 .
"--enable-unicode-properties in order for MediaWiki to function" );
2093 $s = array_shift( $bits );
2096 while ( $i < count( $bits ) ) {
2099 $text = $bits[$i++];
2100 $trail = $bits[$i++];
2102 # The characters '<' and '>' (which were escaped by
2103 # removeHTMLtags()) should not be included in
2104 # URLs, per RFC 2396.
2106 if ( preg_match(
'/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2107 $text = substr( $url, $m2[0][1] ) .
' ' . $text;
2108 $url = substr( $url, 0, $m2[0][1] );
2111 # If the link text is an image URL, replace it with an <img> tag
2112 # This happened by accident in the original parser, but some people used it extensively
2113 $img = $this->maybeMakeExternalImage( $text );
2114 if ( $img !==
false ) {
2120 # Set linktype for CSS
2123 # No link text, e.g. [http:
2124 if ( $text ==
'' ) {
2126 $langObj = $this->getTargetLanguage();
2127 $text =
'[' . $langObj->formatNum( ++$this->mAutonumber ) .
']';
2128 $linktype =
'autonumber';
2130 # Have link text, e.g. [http:
2137 $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
2140 $url = Sanitizer::cleanUrl( $url );
2142 # Use the encoded URL
2143 # This means that users can paste URLs directly into the text
2144 # Funny characters like ö aren't valid in URLs anyway
2145 # This was changed in August 2004
2147 $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
2149 # Register link in the output object.
2150 $this->mOutput->addExternalLink( $url );
2190 $rel = self::getExternalLinkRel( $url, $this->mTitle );
2192 $target = $this->mOptions->getExternalLinkTarget();
2194 $attribs[
'target'] = $target;
2195 if ( !in_array( $target, [
'_self',
'_parent',
'_top' ] ) ) {
2199 if ( $rel !==
'' ) {
2202 $rel .=
'noreferrer noopener';
2205 $attribs[
'rel'] = $rel;
2220 # Test for RFC 3986 IPv6 syntax
2221 $scheme =
'[a-z][a-z0-9+.-]*:';
2222 $userinfo =
'(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2223 $ipv6Host =
'\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2224 if ( preg_match(
"<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2225 IP::isValid( rawurldecode( $m[1] ) )
2227 $isIPv6 = rawurldecode( $m[1] );
2232 # Make sure unsafe characters are encoded
2233 $url = preg_replace_callback(
'/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2235 return rawurlencode( $m[0] );
2241 $end = strlen( $url );
2243 # Fragment part - 'fragment'
2244 $start = strpos( $url,
'#' );
2245 if ( $start !==
false && $start < $end ) {
2246 $ret = self::normalizeUrlComponent(
2247 substr( $url, $start, $end - $start ),
'"#%<>[\]^`{|}' ) . $ret;
2251 # Query part - 'query' minus &=+;
2252 $start = strpos( $url,
'?' );
2253 if ( $start !==
false && $start < $end ) {
2254 $ret = self::normalizeUrlComponent(
2255 substr( $url, $start, $end - $start ),
'"#%<>[\]^`{|}&=+;' ) . $ret;
2259 # Scheme and path part - 'pchar'
2260 # (we assume no userinfo or encoded colons in the host)
2261 $ret = self::normalizeUrlComponent(
2262 substr( $url, 0, $end ),
'"#%<>[\]^`{|}/?' ) . $ret;
2265 if ( $isIPv6 !==
false ) {
2266 $ipv6Host =
"%5B({$isIPv6})%5D";
2267 $ret = preg_replace(
2268 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2278 $callback =
function (
$matches ) use ( $unsafe ) {
2280 $ord = ord( $char );
2281 if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) ===
false ) {
2285 # Leave it escaped, but use uppercase for a-f
2289 return preg_replace_callback(
'/%[0-9A-Fa-f]{2}/', $callback, $component );
2301 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2302 $imagesexception = !empty( $imagesfrom );
2304 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2305 if ( $imagesexception && is_array( $imagesfrom ) ) {
2306 $imagematch =
false;
2307 foreach ( $imagesfrom as $match ) {
2308 if ( strpos( $url, $match ) === 0 ) {
2313 } elseif ( $imagesexception ) {
2314 $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2316 $imagematch =
false;
2319 if ( $this->mOptions->getAllowExternalImages()
2320 || ( $imagesexception && $imagematch )
2322 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2327 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2328 && preg_match( self::EXT_IMAGE_REGEX, $url )
2330 $whitelist = explode(
2332 wfMessage(
'external_image_whitelist' )->inContentLanguage()->text()
2335 foreach ( $whitelist as $entry ) {
2336 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2337 if ( strpos( $entry,
'#' ) === 0 || $entry ===
'' ) {
2340 if ( preg_match(
'/' . str_replace(
'/',
'\\/', $entry ) .
'/i', $url ) ) {
2341 # Image matches a whitelist entry
2362 return $this->handleInternalLinks( $text );
2373 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2388 return $this->handleInternalLinks2( $text );
2398 static $tc =
false, $e1, $e1_img;
2399 # the % is needed to support urlencoded titles as well
2401 $tc = Title::legalChars() .
'#%';
2402 # Match a link having the form [[namespace:link|alternate]]trail
2403 $e1 =
"/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2404 # Match cases where there is no "]]", which might still be images
2405 $e1_img =
"/^([{$tc}]+)\\|(.*)\$/sD";
2410 # split the entire text string on occurrences of [[
2411 $a = StringUtils::explode(
'[[',
' ' .
$s );
2412 # get the first element (all text up to first [[), and remove the space we added
2415 $line = $a->current(); # Workaround
for broken ArrayIterator::next() that returns
"void"
2416 $s = substr(
$s, 1 );
2418 if ( is_null( $this->mTitle ) ) {
2419 throw new MWException( __METHOD__ .
": \$this->mTitle is null\n" );
2421 $nottalk = !$this->mTitle->isTalkPage();
2423 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2425 if ( $useLinkPrefixExtension ) {
2426 # Match the end of a line for a word that's not followed by whitespace,
2427 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2428 $charset = $this->contLang->linkPrefixCharset();
2429 $e2 =
"/^((?>.*[^$charset]|))(.+)$/sDu";
2431 if ( preg_match( $e2,
$s, $m ) ) {
2432 $first_prefix = $m[2];
2434 $first_prefix =
false;
2440 # Some namespaces don't allow subpages
2441 $useSubpages = $this->nsInfo->hasSubpages(
2442 $this->mTitle->getNamespace()
2445 # Loop for each link
2446 for ( ;
$line !==
false &&
$line !==
null; $a->next(),
$line = $a->current() ) {
2447 # Check for excessive memory usage
2448 if ( $holders->isBig() ) {
2450 # Do the existence check, replace the link holders and clear the array
2451 $holders->replace(
$s );
2455 if ( $useLinkPrefixExtension ) {
2456 if ( preg_match( $e2,
$s, $m ) ) {
2457 list( ,
$s, $prefix ) = $m;
2462 if ( $first_prefix ) {
2463 $prefix = $first_prefix;
2464 $first_prefix =
false;
2468 $might_be_img =
false;
2470 if ( preg_match( $e1,
$line, $m ) ) { # page with normal text or alt
2472 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2473 # [[Image:Foo.jpg|[http:
2474 # the real problem is with the $e1 regex
2476 # Still some problems for cases where the ] is meant to be outside punctuation,
2477 # and no image is in sight. See T4095.
2479 && substr( $m[3], 0, 1 ) ===
']'
2480 && strpos( $text,
'[' ) !==
false
2482 $text .=
']'; # so that handleExternalLinks($text) works later
2483 $m[3] = substr( $m[3], 1 );
2485 # fix up urlencoded title texts
2486 if ( strpos( $m[1],
'%' ) !==
false ) {
2487 # Should anchors '#' also be rejected?
2488 $m[1] = str_replace( [
'<',
'>' ], [
'<',
'>' ], rawurldecode( $m[1] ) );
2491 } elseif ( preg_match( $e1_img,
$line, $m ) ) {
2492 # Invalid, but might be an image with a link in its caption
2493 $might_be_img =
true;
2495 if ( strpos( $m[1],
'%' ) !==
false ) {
2496 $m[1] = str_replace( [
'<',
'>' ], [
'<',
'>' ], rawurldecode( $m[1] ) );
2499 }
else { # Invalid form; output directly
2504 $origLink = ltrim( $m[1],
' ' );
2506 # Don't allow internal links to pages containing
2507 # PROTO: where PROTO is a valid URL protocol; these
2508 # should be external links.
2509 if ( preg_match(
'/^(?i:' . $this->mUrlProtocols .
')/', $origLink ) ) {
2514 # Make subpage if necessary
2515 if ( $useSubpages ) {
2517 $this->mTitle, $origLink, $text
2527 $unstrip = $this->mStripState->killMarkers( $link );
2528 $noMarkers = ( $unstrip === $link );
2530 $nt = $noMarkers ? Title::newFromText( $link ) :
null;
2531 if ( $nt ===
null ) {
2536 $ns = $nt->getNamespace();
2537 $iw = $nt->getInterwiki();
2539 $noforce = ( substr( $origLink, 0, 1 ) !==
':' );
2541 if ( $might_be_img ) { #
if this is actually an invalid link
2542 if ( $ns ==
NS_FILE && $noforce ) { # but might be an image
2545 # look at the next 'line' to see if we can close it there
2547 $next_line = $a->current();
2548 if ( $next_line ===
false || $next_line ===
null ) {
2551 $m = explode(
']]', $next_line, 3 );
2552 if ( count( $m ) == 3 ) {
2553 # the first ]] closes the inner link, the second the image
2555 $text .=
"[[{$m[0]}]]{$m[1]}";
2558 } elseif ( count( $m ) == 2 ) {
2559 # if there's exactly one ]] that's fine, we'll keep looking
2560 $text .=
"[[{$m[0]}]]{$m[1]}";
2562 # if $next_line is invalid too, we need look no further
2563 $text .=
'[[' . $next_line;
2568 # we couldn't find the end of this imageLink, so output it raw
2569 # but don't ignore what might be perfectly normal links in the text we've examined
2570 $holders->merge( $this->handleInternalLinks2( $text ) );
2571 $s .=
"{$prefix}[[$link|$text";
2572 # note: no $trail, because without an end, there *is* no trail
2575 }
else { # it
's not an image, so output it raw
2576 $s .= "{$prefix}[[$link|$text";
2577 # note: no $trail, because without an end, there *is* no trail
2582 $wasblank = ( $text == '' );
2586 # Strip off leading ':
'
2587 $text = substr( $text, 1 );
2590 # T6598 madness. Handle the quotes only if they come from the alternate part
2591 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2592 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2593 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2594 $text = $this->doQuotes( $text );
2597 # Link not escaped by : , create the various objects
2598 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2601 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2602 Language::fetchLanguageName( $iw, null, 'mw
' ) ||
2603 in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes
' ) )
2606 # T26502: filter duplicates
2607 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2608 $this->mLangLinkLanguages[$iw] = true;
2609 $this->mOutput->addLanguageLink( $nt->getFullText() );
2615 $s = rtrim( $s . $prefix ) . $trail; # T175416
2619 if ( $ns == NS_FILE ) {
2620 if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->mTitle ) ) {
2622 # if no parameters were passed, $text
2623 # becomes something like "File:Foo.png",
2624 # which we don't want to pass on to the
2628 # recursively parse links inside the image caption
2629 # actually, this will parse them in any other parameters, too,
2630 # but it might be hard to fix that, and it doesn't matter ATM
2631 $text = $this->handleExternalLinks( $text );
2632 $holders->merge( $this->handleInternalLinks2( $text ) );
2634 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2635 $s .= $prefix . $this->armorLinksPrivate(
2636 $this->makeImage( $nt, $text, $holders ) ) . $trail;
2643 $s = rtrim(
$s . $prefix ) . $trail; # T2087, T87753
2646 $sortkey = $this->getDefaultSort();
2650 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2651 $sortkey = str_replace(
"\n",
'', $sortkey );
2652 $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2653 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2659 # Self-link checking. For some languages, variants of the title are checked in
2660 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2661 # for linking to a different variant.
2662 if ( $ns !=
NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2667 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2668 # @todo FIXME: Should do batch file existence checks, see comment below
2670 # Give extensions a chance to select the file revision for us
2674 [ $this, $nt, &$options, &$descQuery ] );
2675 # Fetch and register the file (file title may be different via hooks)
2676 list(
$file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2677 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2678 $s .= $prefix . $this->armorLinksPrivate(
2683 # Some titles, such as valid special pages or files in foreign repos, should
2684 # be shown as bluelinks even though they're not included in the page table
2685 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2686 # batch file existence checks for NS_FILE and NS_MEDIA
2687 if ( $iw ==
'' && $nt->isAlwaysKnown() ) {
2688 $this->mOutput->addLink( $nt );
2689 $s .= $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2691 # Links will be added to the output link list after checking
2692 $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2714 return $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2733 if ( $text ==
'' ) {
2734 $text = htmlspecialchars( $nt->getPrefixedText() );
2737 $link = $this->getLinkRenderer()->makeKnownLink(
2738 $nt,
new HtmlArmor(
"$prefix$text$inside" )
2741 return $this->armorLinksPrivate( $link ) . $trail;
2757 return $this->armorLinksPrivate( $text );
2771 return preg_replace(
'/\b((?i)' . $this->mUrlProtocols .
')/',
2772 self::MARKER_PREFIX .
"NOPARSE$1", $text );
2781 # Some namespaces don't allow subpages
2783 return $this->nsInfo->hasSubpages( $this->mTitle->getNamespace() );
2826 return $this->expandMagicVariable( $index, $frame );
2841 if ( is_null( $this->mTitle ) ) {
2846 throw new MWException( __METHOD__ .
' Should only be '
2847 .
' called while parsing (no title set)' );
2858 Hooks::run(
'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) &&
2859 isset( $this->mVarCache[$index] )
2861 return $this->mVarCache[$index];
2864 $ts =
wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2865 Hooks::run(
'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2867 $pageLang = $this->getFunctionLang();
2873 case 'currentmonth':
2874 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'm' ),
true );
2876 case 'currentmonth1':
2877 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'n' ),
true );
2879 case 'currentmonthname':
2880 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format(
'n' ) );
2882 case 'currentmonthnamegen':
2883 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format(
'n' ) );
2885 case 'currentmonthabbrev':
2886 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format(
'n' ) );
2889 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'j' ),
true );
2892 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'd' ),
true );
2895 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'm' ),
true );
2898 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'n' ),
true );
2900 case 'localmonthname':
2901 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format(
'n' ) );
2903 case 'localmonthnamegen':
2904 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format(
'n' ) );
2906 case 'localmonthabbrev':
2907 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format(
'n' ) );
2910 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'j' ),
true );
2913 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'd' ),
true );
2921 case 'fullpagename':
2924 case 'fullpagenamee':
2930 case 'subpagenamee':
2933 case 'rootpagename':
2936 case 'rootpagenamee':
2940 $this->mTitle->getRootText()
2943 case 'basepagename':
2946 case 'basepagenamee':
2950 $this->mTitle->getBaseText()
2953 case 'talkpagename':
2954 if ( $this->mTitle->canHaveTalkPage() ) {
2955 $talkPage = $this->mTitle->getTalkPage();
2961 case 'talkpagenamee':
2962 if ( $this->mTitle->canHaveTalkPage() ) {
2963 $talkPage = $this->mTitle->getTalkPage();
2969 case 'subjectpagename':
2970 $subjPage = $this->mTitle->getSubjectPage();
2973 case 'subjectpagenamee':
2974 $subjPage = $this->mTitle->getSubjectPage();
2978 # Inform the edit saving system that getting the canonical output
2979 # after page insertion requires a parse that used that exact page ID
2980 $this->setOutputFlag(
'vary-page-id',
'{{PAGEID}} used' );
2981 $value = $this->mTitle->getArticleID();
2983 $value = $this->mOptions->getSpeculativePageId();
2985 $this->mOutput->setSpeculativePageIdUsed( $value );
2991 $this->svcOptions->get(
'MiserMode' ) &&
2992 !$this->mOptions->getInterfaceMessage() &&
2994 $this->nsInfo->isContent( $this->mTitle->getNamespace() )
2998 if ( $this->getRevisionId() || $this->mOptions->getSpeculativeRevId() ) {
3001 $this->setOutputFlag(
'vary-revision-exists',
'{{REVISIONID}} used' );
3005 # Inform the edit saving system that getting the canonical output after
3006 # revision insertion requires a parse that used that exact revision ID
3007 $this->setOutputFlag(
'vary-revision-id',
'{{REVISIONID}} used' );
3008 $value = $this->getRevisionId();
3009 if ( $value === 0 ) {
3010 $rev = $this->getRevisionObject();
3011 $value = $rev ? $rev->getId() : $value;
3014 $value = $this->mOptions->getSpeculativeRevId();
3016 $this->mOutput->setSpeculativeRevIdUsed( $value );
3022 $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
3024 case 'revisionday2':
3025 $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
3027 case 'revisionmonth':
3028 $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
3030 case 'revisionmonth1':
3031 $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
3033 case 'revisionyear':
3034 $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
3036 case 'revisiontimestamp':
3037 $value = $this->getRevisionTimestampSubstring( 0, 14, self::MAX_TTS, $index );
3039 case 'revisionuser':
3040 # Inform the edit saving system that getting the canonical output after
3041 # revision insertion requires a parse that used the actual user ID
3042 $this->setOutputFlag(
'vary-user',
'{{REVISIONUSER}} used' );
3043 $value = $this->getRevisionUser();
3045 case 'revisionsize':
3046 $value = $this->getRevisionSize();
3049 $value = str_replace(
'_',
' ',
3050 $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
3053 $value =
wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
3055 case 'namespacenumber':
3056 $value = $this->mTitle->getNamespace();
3059 $value = $this->mTitle->canHaveTalkPage()
3060 ? str_replace(
'_',
' ', $this->mTitle->getTalkNsText() )
3064 $value = $this->mTitle->canHaveTalkPage() ?
wfUrlencode( $this->mTitle->getTalkNsText() ) :
'';
3066 case 'subjectspace':
3067 $value = str_replace(
'_',
' ', $this->mTitle->getSubjectNsText() );
3069 case 'subjectspacee':
3070 $value = (
wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3072 case 'currentdayname':
3073 $value = $pageLang->getWeekdayName( (
int)MWTimestamp::getInstance( $ts )->format(
'w' ) + 1 );
3076 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'Y' ),
true );
3079 $value = $pageLang->time(
wfTimestamp( TS_MW, $ts ),
false, false );
3082 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'H' ),
true );
3085 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
3086 # int to remove the padding
3087 $value = $pageLang->formatNum( (
int)MWTimestamp::getInstance( $ts )->format(
'W' ) );
3090 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format(
'w' ) );
3092 case 'localdayname':
3093 $value = $pageLang->getWeekdayName(
3094 (
int)MWTimestamp::getLocalInstance( $ts )->format(
'w' ) + 1
3098 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'Y' ),
true );
3101 $value = $pageLang->time(
3102 MWTimestamp::getLocalInstance( $ts )->format(
'YmdHis' ),
3108 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'H' ),
true );
3111 # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
3112 # int to remove the padding
3113 $value = $pageLang->formatNum( (
int)MWTimestamp::getLocalInstance( $ts )->format(
'W' ) );
3116 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format(
'w' ) );
3118 case 'numberofarticles':
3121 case 'numberoffiles':
3124 case 'numberofusers':
3127 case 'numberofactiveusers':
3130 case 'numberofpages':
3133 case 'numberofadmins':
3136 case 'numberofedits':
3139 case 'currenttimestamp':
3142 case 'localtimestamp':
3143 $value = MWTimestamp::getLocalInstance( $ts )->format(
'YmdHis' );
3145 case 'currentversion':
3149 return $this->svcOptions->get(
'ArticlePath' );
3151 return $this->svcOptions->get(
'Sitename' );
3153 return $this->svcOptions->get(
'Server' );
3155 return $this->svcOptions->get(
'ServerName' );
3157 return $this->svcOptions->get(
'ScriptPath' );
3159 return $this->svcOptions->get(
'StylePath' );
3160 case 'directionmark':
3161 return $pageLang->getDirMark();
3162 case 'contentlanguage':
3163 return $this->svcOptions->get(
'LanguageCode' );
3164 case 'pagelanguage':
3165 $value = $pageLang->getCode();
3167 case 'cascadingsources':
3173 'ParserGetVariableValueSwitch',
3174 [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
3181 $this->mVarCache[$index] = $value;
3195 # Get the timezone-adjusted timestamp to be used for this revision
3196 $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
3197 # Possibly set vary-revision if there is not yet an associated revision
3198 if ( !$this->getRevisionObject() ) {
3199 # Get the timezone-adjusted timestamp $mtts seconds in the future.
3200 # This future is relative to the current time and not that of the
3201 # parser options. The rendered timestamp can be compared to that
3202 # of the timestamp specified by the parser options.
3204 $this->contLang->userAdjust(
wfTimestamp( TS_MW, time() + $mtts ),
'' ),
3209 if ( $resNow !== $resThen ) {
3210 # Inform the edit saving system that getting the canonical output after
3211 # revision insertion requires a parse that used an actual revision timestamp
3212 $this->setOutputFlag(
'vary-revision-timestamp',
"$variable used" );
3227 $this->initializeVariables();
3235 $variableIDs = $this->magicWordFactory->getVariableIDs();
3236 $substIDs = $this->magicWordFactory->getSubstIDs();
3238 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
3239 $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
3265 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3279 $ltrimmed = ltrim(
$s );
3280 $w1 = substr(
$s, 0, strlen(
$s ) - strlen( $ltrimmed ) );
3281 $trimmed = rtrim( $ltrimmed );
3282 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3284 $w2 = substr( $ltrimmed, -$diff );
3288 return [ $w1, $trimmed, $w2 ];
3312 # Is there any text? Also, Prevent too big inclusions!
3313 $textSize = strlen( $text );
3314 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3318 if ( $frame ===
false ) {
3319 $frame = $this->getPreprocessor()->newFrame();
3320 } elseif ( !( $frame instanceof
PPFrame ) ) {
3321 $this->logger->debug(
3322 __METHOD__ .
" called using plain parameters instead of " .
3323 "a PPFrame instance. Creating custom frame."
3325 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3328 $dom = $this->preprocessToDom( $text );
3329 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3330 $text = $frame->expand( $dom, $flags );
3347 foreach (
$args as $arg ) {
3348 $eqpos = strpos( $arg,
'=' );
3349 if ( $eqpos ===
false ) {
3350 $assocArgs[$index++] = $arg;
3352 $name = trim( substr( $arg, 0, $eqpos ) );
3353 $value = trim( substr( $arg, $eqpos + 1 ) );
3354 if ( $value ===
false ) {
3357 if ( $name !==
false ) {
3358 $assocArgs[$name] = $value;
3393 # does no harm if $current and $max are present but are unnecessary for the message
3394 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3395 # only during preview, and that would split the parser cache unnecessarily.
3396 $warning =
wfMessage(
"$limitationType-warning" )->numParams( $current, $max )
3398 $this->mOutput->addWarning( $warning );
3399 $this->addTrackingCategory(
"$limitationType-category" );
3425 $forceRawInterwiki =
false;
3427 $isChildObj =
false;
3429 $isLocalObj =
false;
3431 # Title object, where $text came from
3434 # $part1 is the bit before the first |, and must contain only title characters.
3435 # Various prefixes will be stripped from it later.
3436 $titleWithSpaces = $frame->expand( $piece[
'title'] );
3437 $part1 = trim( $titleWithSpaces );
3440 # Original title text preserved for various purposes
3441 $originalTitle = $part1;
3443 # $args is a list of argument nodes, starting from index 0, not including $part1
3444 # @todo FIXME: If piece['parts'] is null then the call to getLength()
3445 # below won't work b/c this $args isn't an object
3446 $args = ( $piece[
'parts'] == null ) ? [] : $piece[
'parts'];
3448 $profileSection =
null;
3452 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3454 # Possibilities for substMatch: "subst", "safesubst" or FALSE
3455 # Decide whether to expand template or keep wikitext as-is.
3456 if ( $this->ot[
'wiki'] ) {
3457 if ( $substMatch ===
false ) {
3458 $literal =
true; # literal when in PST with no prefix
3460 $literal =
false; # expand when in PST with subst: or safesubst:
3463 if ( $substMatch ==
'subst' ) {
3464 $literal =
true; # literal when not in PST with plain subst:
3466 $literal =
false; # expand when not in PST with safesubst: or no prefix
3470 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3477 if ( !$found &&
$args->getLength() == 0 ) {
3478 $id = $this->mVariables->matchStartToEnd( $part1 );
3479 if ( $id !==
false ) {
3480 $text = $this->expandMagicVariable( $id, $frame );
3481 if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3482 $this->mOutput->updateCacheExpiry(
3483 $this->magicWordFactory->getCacheTTL( $id ) );
3489 # MSG, MSGNW and RAW
3492 $mwMsgnw = $this->magicWordFactory->get(
'msgnw' );
3493 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3496 # Remove obsolete MSG:
3497 $mwMsg = $this->magicWordFactory->get(
'msg' );
3498 $mwMsg->matchStartAndRemove( $part1 );
3502 $mwRaw = $this->magicWordFactory->get(
'raw' );
3503 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3504 $forceRawInterwiki =
true;
3510 $colonPos = strpos( $part1,
':' );
3511 if ( $colonPos !==
false ) {
3512 $func = substr( $part1, 0, $colonPos );
3513 $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3514 $argsLength =
$args->getLength();
3515 for ( $i = 0; $i < $argsLength; $i++ ) {
3516 $funcArgs[] =
$args->item( $i );
3519 $result = $this->callParserFunction( $frame, $func, $funcArgs );
3522 if ( isset( $result[
'title'] ) ) {
3523 $title = $result[
'title'];
3525 if ( isset( $result[
'found'] ) ) {
3526 $found = $result[
'found'];
3528 if ( array_key_exists(
'text', $result ) ) {
3530 $text = $result[
'text'];
3532 if ( isset( $result[
'nowiki'] ) ) {
3533 $nowiki = $result[
'nowiki'];
3535 if ( isset( $result[
'isHTML'] ) ) {
3536 $isHTML = $result[
'isHTML'];
3538 if ( isset( $result[
'forceRawInterwiki'] ) ) {
3539 $forceRawInterwiki = $result[
'forceRawInterwiki'];
3541 if ( isset( $result[
'isChildObj'] ) ) {
3542 $isChildObj = $result[
'isChildObj'];
3544 if ( isset( $result[
'isLocalObj'] ) ) {
3545 $isLocalObj = $result[
'isLocalObj'];
3550 # Finish mangling title and then check for loops.
3551 # Set $title to a Title object and $titleText to the PDBK
3554 # Split the title into page and subpage
3557 $this->mTitle, $part1, $subpage
3559 if ( $part1 !== $relative ) {
3561 $ns = $this->mTitle->getNamespace();
3563 $title = Title::newFromText( $part1, $ns );
3565 $titleText =
$title->getPrefixedText();
3566 # Check for language variants if the template is not found
3567 if ( $this->getTargetLanguage()->hasVariants() &&
$title->getArticleID() == 0 ) {
3568 $this->getTargetLanguage()->findVariantLink( $part1,
$title,
true );
3570 # Do recursion depth check
3571 $limit = $this->mOptions->getMaxTemplateDepth();
3572 if ( $frame->depth >= $limit ) {
3574 $text =
'<span class="error">'
3575 .
wfMessage(
'parser-template-recursion-depth-warning' )
3576 ->numParams( $limit )->inContentLanguage()->text()
3582 # Load from database
3583 if ( !$found &&
$title ) {
3584 $profileSection = $this->mProfiler->scopedProfileIn(
$title->getPrefixedDBkey() );
3585 if ( !
$title->isExternal() ) {
3586 if (
$title->isSpecialPage()
3587 && $this->mOptions->getAllowSpecialInclusion()
3588 && $this->ot[
'html']
3590 $specialPage = $this->specialPageFactory->getPage(
$title->getDBkey() );
3595 $argsLength =
$args->getLength();
3596 for ( $i = 0; $i < $argsLength; $i++ ) {
3597 $bits =
$args->item( $i )->splitArg();
3598 if ( strval( $bits[
'index'] ) ===
'' ) {
3599 $name = trim( $frame->expand( $bits[
'name'], PPFrame::STRIP_COMMENTS ) );
3600 $value = trim( $frame->expand( $bits[
'value'] ) );
3601 $pageArgs[$name] = $value;
3609 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3613 $context->setUser( User::newFromName(
'127.0.0.1',
false ) );
3615 $context->setLanguage( $this->mOptions->getUserLangObj() );
3616 $ret = $this->specialPageFactory->capturePath(
$title,
$context, $this->getLinkRenderer() );
3618 $text =
$context->getOutput()->getHTML();
3619 $this->mOutput->addOutputPageMetadata(
$context->getOutput() );
3622 if ( $specialPage && $specialPage->maxIncludeCacheTime() !==
false ) {
3623 $this->mOutput->updateRuntimeAdaptiveExpiry(
3624 $specialPage->maxIncludeCacheTime()
3628 } elseif ( $this->nsInfo->isNonincludable(
$title->getNamespace() ) ) {
3629 $found =
false; # access denied
3630 $this->logger->debug(
3632 ": template inclusion denied for " .
$title->getPrefixedDBkey()
3635 list( $text,
$title ) = $this->getTemplateDom(
$title );
3636 if ( $text !==
false ) {
3642 # If the title is valid but undisplayable, make a link to it
3643 if ( !$found && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3644 $text =
"[[:$titleText]]";
3647 } elseif (
$title->isTrans() ) {
3648 # Interwiki transclusion
3649 if ( $this->ot[
'html'] && !$forceRawInterwiki ) {
3650 $text = $this->interwikiTransclude(
$title,
'render' );
3653 $text = $this->interwikiTransclude(
$title,
'raw' );
3654 # Preprocess it like a template
3655 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3661 # Do infinite loop check
3662 # This has to be done after redirect resolution to avoid infinite loops via redirects
3663 if ( !$frame->loopCheck(
$title ) ) {
3665 $text =
'<span class="error">'
3666 .
wfMessage(
'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3668 $this->addTrackingCategory(
'template-loop-category' );
3669 $this->mOutput->addWarning(
wfMessage(
'template-loop-warning',
3671 $this->logger->debug( __METHOD__ .
": template loop broken at '$titleText'" );
3675 # If we haven't found text to substitute by now, we're done
3676 # Recover the source wikitext and return it
3678 $text = $frame->virtualBracketedImplode(
'{{',
'|',
'}}', $titleWithSpaces,
$args );
3679 if ( $profileSection ) {
3680 $this->mProfiler->scopedProfileOut( $profileSection );
3682 return [
'object' => $text ];
3685 # Expand DOM-style return values in a child frame
3686 if ( $isChildObj ) {
3687 # Clean up argument array
3691 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3692 } elseif ( $titleText !==
false && $newFrame->isEmpty() ) {
3693 # Expansion is eligible for the empty-frame cache
3694 $text = $newFrame->cachedExpand( $titleText, $text );
3696 # Uncached expansion
3697 $text = $newFrame->expand( $text );
3700 if ( $isLocalObj && $nowiki ) {
3701 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3702 $isLocalObj =
false;
3705 if ( $profileSection ) {
3706 $this->mProfiler->scopedProfileOut( $profileSection );
3709 # Replace raw HTML by a placeholder
3711 $text = $this->insertStripItem( $text );
3712 } elseif ( $nowiki && ( $this->ot[
'html'] || $this->ot[
'pre'] ) ) {
3713 # Escape nowiki-style return values
3715 } elseif ( is_string( $text )
3716 && !$piece[
'lineStart']
3717 && preg_match(
'/^(?:{\\||:|;|#|\*)/', $text )
3719 # T2529: if the template begins with a table or block-level
3720 # element, it should be treated as beginning a new line.
3721 # This behavior is somewhat controversial.
3722 $text =
"\n" . $text;
3725 if ( is_string( $text ) && !$this->incrementIncludeSize(
'post-expand', strlen( $text ) ) ) {
3726 # Error, oversize inclusion
3727 if ( $titleText !==
false ) {
3728 # Make a working, properly escaped link if possible (T25588)
3729 $text =
"[[:$titleText]]";
3731 # This will probably not be a working link, but at least it may
3732 # provide some hint of where the problem is
3733 preg_replace(
'/^:/',
'', $originalTitle );
3734 $text =
"[[:$originalTitle]]";
3736 $text .= $this->insertStripItem(
'<!-- WARNING: template omitted, '
3737 .
'post-expand include size too large -->' );
3738 $this->limitationWarn(
'post-expand-template-inclusion' );
3741 if ( $isLocalObj ) {
3742 $ret = [
'object' => $text ];
3744 $ret = [
'text' => $text ];
3770 # Case sensitive functions
3771 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3772 $function = $this->mFunctionSynonyms[1][$function];
3774 # Case insensitive functions
3775 $function = $this->contLang->lc( $function );
3776 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3777 $function = $this->mFunctionSynonyms[0][$function];
3779 return [
'found' => false ];
3783 list( $callback, $flags ) = $this->mFunctionHooks[$function];
3788 $allArgs = [ &$parser ];
3789 if ( $flags & self::SFH_OBJECT_ARGS ) {
3790 # Convert arguments to PPNodes and collect for appending to $allArgs
3792 foreach (
$args as $k => $v ) {
3793 if ( $v instanceof
PPNode || $k === 0 ) {
3796 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3800 # Add a frame parameter, and pass the arguments as an array
3801 $allArgs[] = $frame;
3802 $allArgs[] = $funcArgs;
3804 # Convert arguments to plain text and append to $allArgs
3805 foreach (
$args as $k => $v ) {
3806 if ( $v instanceof
PPNode ) {
3807 $allArgs[] = trim( $frame->expand( $v ) );
3808 } elseif ( is_int( $k ) && $k >= 0 ) {
3809 $allArgs[] = trim( $v );
3811 $allArgs[] = trim(
"$k=$v" );
3816 $result = $callback( ...$allArgs );
3818 # The interface for function hooks allows them to return a wikitext
3819 # string or an array containing the string and any flags. This mungs
3820 # things around to match what this method should return.
3821 if ( !is_array( $result ) ) {
3827 if ( isset( $result[0] ) && !isset( $result[
'text'] ) ) {
3828 $result[
'text'] = $result[0];
3830 unset( $result[0] );
3837 $preprocessFlags = 0;
3838 if ( isset( $result[
'noparse'] ) ) {
3839 $noparse = $result[
'noparse'];
3841 if ( isset( $result[
'preprocessFlags'] ) ) {
3842 $preprocessFlags = $result[
'preprocessFlags'];
3846 $result[
'text'] = $this->preprocessToDom( $result[
'text'], $preprocessFlags );
3847 $result[
'isChildObj'] =
true;
3863 $titleText =
$title->getPrefixedDBkey();
3865 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3866 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3867 $title = Title::makeTitle( $ns, $dbk );
3868 $titleText =
$title->getPrefixedDBkey();
3870 if ( isset( $this->mTplDomCache[$titleText] ) ) {
3871 return [ $this->mTplDomCache[$titleText],
$title ];
3874 # Cache miss, go to the database
3875 list( $text,
$title ) = $this->fetchTemplateAndTitle(
$title );
3877 if ( $text ===
false ) {
3878 $this->mTplDomCache[$titleText] =
false;
3879 return [
false,
$title ];
3882 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3883 $this->mTplDomCache[$titleText] = $dom;
3885 if ( !
$title->equals( $cacheTitle ) ) {
3886 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3905 $cacheKey =
$title->getPrefixedDBkey();
3906 if ( !$this->currentRevisionCache ) {
3907 $this->currentRevisionCache =
new MapCacheLRU( 100 );
3909 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3910 $this->currentRevisionCache->set( $cacheKey,
3912 call_user_func( $this->mOptions->getCurrentRevisionCallback(),
$title, $this )
3915 return $this->currentRevisionCache->get( $cacheKey );
3925 $this->currentRevisionCache &&
3926 $this->currentRevisionCache->has(
$title->getPrefixedText() )
3952 $templateCb = $this->mOptions->getTemplateCallback();
3953 $stuff = call_user_func( $templateCb,
$title, $this );
3954 $rev = $stuff[
'revision'] ??
null;
3955 $text = $stuff[
'text'];
3956 if ( is_string( $stuff[
'text'] ) ) {
3958 $text = strtr( $text,
"\x7f",
"?" );
3960 $finalTitle = $stuff[
'finalTitle'] ??
$title;
3961 foreach ( ( $stuff[
'deps'] ?? [] ) as $dep ) {
3962 $this->mOutput->addTemplate( $dep[
'title'], $dep[
'page_id'], $dep[
'rev_id'] );
3963 if ( $dep[
'title']->equals( $this->
getTitle() ) && $rev instanceof
Revision ) {
3965 $this->setOutputFlag(
'vary-revision-sha1',
'Self transclusion' );
3966 $this->getOutput()->setRevisionUsedSha1Base36( $rev->getSha1() );
3970 return [ $text, $finalTitle ];
3979 return $this->fetchTemplateAndTitle(
$title )[0];
3992 $text = $skip =
false;
3997 # Loop to fetch the article, with up to 1 redirect
3998 for ( $i = 0; $i < 2 && is_object(
$title ); $i++ ) {
3999 # Give extensions a chance to select the revision instead
4000 $id =
false; # Assume current
4001 Hooks::run(
'BeforeParserFetchTemplateAndtitle',
4002 [ $parser,
$title, &$skip, &$id ] );
4008 'page_id' =>
$title->getArticleID(),
4015 $rev = Revision::newFromId( $id );
4016 } elseif ( $parser ) {
4017 $rev = $parser->fetchCurrentRevisionOfTitle(
$title );
4019 $rev = Revision::newFromTitle(
$title );
4021 $rev_id = $rev ? $rev->getId() : 0;
4022 # If there is no current revision, there is no page
4023 if ( $id ===
false && !$rev ) {
4024 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
4025 $linkCache->addBadLinkObj(
$title );
4030 'page_id' =>
$title->getArticleID(),
4033 if ( $rev && !
$title->equals( $rev->getTitle() ) ) {
4034 # We fetched a rev from a different title; register it too...
4036 'title' => $rev->getTitle(),
4037 'page_id' => $rev->getPage(),
4046 Hooks::run(
'ParserFetchTemplate',
4047 [ $parser,
$title, $rev, &$text, &$deps ] );
4049 if ( $text ===
false || $text ===
null ) {
4054 $message =
wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
4055 lcfirst(
$title->getText() ) )->inContentLanguage();
4056 if ( !$message->exists() ) {
4061 $text = $message->plain();
4075 'finalTitle' => $finalTitle,
4088 $file = $this->fetchFileNoRegister(
$title, $options );
4090 $time =
$file ?
$file->getTimestamp() :
false;
4092 # Register the file as a dependency...
4093 $this->mOutput->addImage(
$title->getDBkey(), $time, $sha1 );
4095 # Update fetched file title
4097 $this->mOutput->addImage(
$title->getDBkey(), $time, $sha1 );
4113 if ( isset( $options[
'broken'] ) ) {
4115 } elseif ( isset( $options[
'sha1'] ) ) {
4116 $file = RepoGroup::singleton()->findFileFromKey( $options[
'sha1'], $options );
4118 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
$title, $options );
4132 if ( !$this->svcOptions->get(
'EnableScaryTranscluding' ) ) {
4133 return wfMessage(
'scarytranscludedisabled' )->inContentLanguage()->text();
4136 $url =
$title->getFullURL( [
'action' => $action ] );
4137 if ( strlen( $url ) > 1024 ) {
4138 return wfMessage(
'scarytranscludetoolong' )->inContentLanguage()->text();
4141 $wikiId =
$title->getTransWikiID();
4143 $fname = __METHOD__;
4144 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
4146 $data =
$cache->getWithSetCallback(
4148 'interwiki-transclude',
4149 ( $wikiId !==
false ) ? $wikiId :
'external',
4152 $this->svcOptions->get(
'TranscludeCacheExpiry' ),
4153 function ( $oldValue, &$ttl ) use ( $url, $fname,
$cache ) {
4154 $req = MWHttpRequest::factory( $url, [], $fname );
4156 $status = $req->execute();
4157 if ( !$status->isOK() ) {
4158 $ttl = $cache::TTL_UNCACHEABLE;
4159 } elseif ( $req->getResponseHeader(
'X-Database-Lagged' ) !==
null ) {
4160 $ttl = min( $cache::TTL_LAGGED, $ttl );
4164 'text' => $status->isOK() ? $req->getContent() :
null,
4165 'code' => $req->getStatus()
4169 'checkKeys' => ( $wikiId !== false )
4170 ? [
$cache->makeGlobalKey(
'interwiki-page', $wikiId,
$title->getDBkey() ) ]
4172 'pcGroup' =>
'interwiki-transclude:5',
4173 'pcTTL' => $cache::TTL_PROC_LONG
4177 if ( is_string( $data[
'text'] ) ) {
4178 $text = $data[
'text'];
4179 } elseif ( $data[
'code'] != 200 ) {
4181 $text =
wfMessage(
'scarytranscludefailed-httpstatus' )
4182 ->params( $url, $data[
'code'] )->inContentLanguage()->text();
4184 $text =
wfMessage(
'scarytranscludefailed', $url )->inContentLanguage()->text();
4201 $parts = $piece[
'parts'];
4202 $nameWithSpaces = $frame->expand( $piece[
'title'] );
4203 $argName = trim( $nameWithSpaces );
4205 $text = $frame->getArgument( $argName );
4206 if ( $text ===
false && $parts->getLength() > 0
4207 && ( $this->ot[
'html']
4209 || ( $this->ot[
'wiki'] && $frame->isTemplate() )
4212 # No match in frame, use the supplied default
4213 $object = $parts->item( 0 )->getChildren();
4215 if ( !$this->incrementIncludeSize(
'arg', strlen( $text ) ) ) {
4216 $error =
'<!-- WARNING: argument omitted, expansion size too large -->';
4217 $this->limitationWarn(
'post-expand-template-argument' );
4220 if ( $text ===
false && $object ===
false ) {
4222 $object = $frame->virtualBracketedImplode(
'{{{',
'|',
'}}}', $nameWithSpaces, $parts );
4224 if ( $error !==
false ) {
4227 if ( $object !==
false ) {
4228 $ret = [
'object' => $object ];
4230 $ret = [
'text' => $text ];
4253 static $errorStr =
'<span class="error">';
4254 static $errorLen = 20;
4256 $name = $frame->expand( $params[
'name'] );
4257 if ( substr( $name, 0, $errorLen ) === $errorStr ) {
4263 $attrText = !isset( $params[
'attr'] ) ? null : $frame->expand( $params[
'attr'] );
4264 if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
4272 $content = !isset( $params[
'inner'] ) ? null : $frame->expand( $params[
'inner'] );
4274 $marker = self::MARKER_PREFIX .
"-$name-"
4275 . sprintf(
'%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4277 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4278 ( $this->ot[
'html'] || $this->ot[
'pre'] );
4279 if ( $isFunctionTag ) {
4280 $markerType =
'none';
4282 $markerType =
'general';
4284 if ( $this->ot[
'html'] || $isFunctionTag ) {
4285 $name = strtolower( $name );
4286 $attributes = Sanitizer::decodeTagAttributes( $attrText );
4287 if ( isset( $params[
'attributes'] ) ) {
4288 $attributes += $params[
'attributes'];
4291 if ( isset( $this->mTagHooks[$name] ) ) {
4292 $output = call_user_func_array( $this->mTagHooks[$name],
4293 [
$content, $attributes, $this, $frame ] );
4294 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4295 list( $callback, ) = $this->mFunctionTagHooks[$name];
4299 $output = call_user_func_array( $callback, [ &$parser, $frame,
$content, $attributes ] );
4301 $output =
'<span class="error">Invalid tag extension name: ' .
4302 htmlspecialchars( $name ) .
'</span>';
4305 if ( is_array( $output ) ) {
4308 $output = $flags[0];
4309 if ( isset( $flags[
'markerType'] ) ) {
4310 $markerType = $flags[
'markerType'];
4314 if ( is_null( $attrText ) ) {
4317 if ( isset( $params[
'attributes'] ) ) {
4318 foreach ( $params[
'attributes'] as $attrName => $attrValue ) {
4319 $attrText .=
' ' . htmlspecialchars( $attrName ) .
'="' .
4320 htmlspecialchars( $attrValue ) .
'"';
4324 $output =
"<$name$attrText/>";
4326 $close = is_null( $params[
'close'] ) ?
'' : $frame->expand( $params[
'close'] );
4327 if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4331 $output =
"<$name$attrText>$content$close";
4335 if ( $markerType ===
'none' ) {
4337 } elseif ( $markerType ===
'nowiki' ) {
4338 $this->mStripState->addNoWiki( $marker, $output );
4339 } elseif ( $markerType ===
'general' ) {
4340 $this->mStripState->addGeneral( $marker, $output );
4342 throw new MWException( __METHOD__ .
': invalid marker type' );
4355 if ( $this->mIncludeSizes[
$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4358 $this->mIncludeSizes[
$type] += $size;
4369 $this->mExpensiveFunctionCount++;
4370 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4383 return $this->handleDoubleUnderscore( $text );
4394 # The position of __TOC__ needs to be recorded
4395 $mw = $this->magicWordFactory->get(
'toc' );
4396 if ( $mw->match( $text ) ) {
4397 $this->mShowToc =
true;
4398 $this->mForceTocPosition =
true;
4400 # Set a placeholder. At the end we'll fill it in with the TOC.
4401 $text = $mw->replace(
'<!--MWTOC\'"-->', $text, 1 );
4403 # Only keep the first one.
4404 $text = $mw->replace(
'', $text );
4407 # Now match and remove the rest of them
4408 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4409 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4411 if ( isset( $this->mDoubleUnderscores[
'nogallery'] ) ) {
4412 $this->mOutput->mNoGallery =
true;
4414 if ( isset( $this->mDoubleUnderscores[
'notoc'] ) && !$this->mForceTocPosition ) {
4415 $this->mShowToc =
false;
4417 if ( isset( $this->mDoubleUnderscores[
'hiddencat'] )
4420 $this->addTrackingCategory(
'hidden-category-category' );
4422 # (T10068) Allow control over whether robots index a page.
4423 # __INDEX__ always overrides __NOINDEX__, see T16899
4424 if ( isset( $this->mDoubleUnderscores[
'noindex'] ) && $this->mTitle->canUseNoindex() ) {
4425 $this->mOutput->setIndexPolicy(
'noindex' );
4426 $this->addTrackingCategory(
'noindex-category' );
4428 if ( isset( $this->mDoubleUnderscores[
'index'] ) && $this->mTitle->canUseNoindex() ) {
4429 $this->mOutput->setIndexPolicy(
'index' );
4430 $this->addTrackingCategory(
'index-category' );
4433 # Cache all double underscores in the database
4434 foreach ( $this->mDoubleUnderscores as $key => $val ) {
4435 $this->mOutput->setProperty( $key,
'' );
4447 return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4469 return $this->finalizeHeadings( $text, $origText, $isMain );
4488 # Inhibit editsection links if requested in the page
4489 if ( isset( $this->mDoubleUnderscores[
'noeditsection'] ) ) {
4490 $maybeShowEditLink =
false;
4492 $maybeShowEditLink =
true;
4495 # Get all headlines for numbering them and adding funky stuff like [edit]
4496 # links - this is for later, but we need the number of headlines right now
4497 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4498 # be trimmed here since whitespace in HTML headings is significant.
4500 $numMatches = preg_match_all(
4501 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4506 # if there are fewer than 4 headlines in the article, do not show TOC
4507 # unless it's been explicitly enabled.
4508 $enoughToc = $this->mShowToc &&
4509 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4511 # Allow user to stipulate that a page should have a "new section"
4512 # link added via __NEWSECTIONLINK__
4513 if ( isset( $this->mDoubleUnderscores[
'newsectionlink'] ) ) {
4514 $this->mOutput->setNewSection(
true );
4517 # Allow user to remove the "new section"
4518 # link via __NONEWSECTIONLINK__
4519 if ( isset( $this->mDoubleUnderscores[
'nonewsectionlink'] ) ) {
4520 $this->mOutput->hideNewSection(
true );
4523 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4524 # override above conditions and always show TOC above first header
4525 if ( isset( $this->mDoubleUnderscores[
'forcetoc'] ) ) {
4526 $this->mShowToc =
true;
4534 # Ugh .. the TOC should have neat indentation levels which can be
4535 # passed to the skin functions. These are determined here
4539 $sublevelCount = [];
4545 $markerRegex = self::MARKER_PREFIX .
"-h-(\d+)-" . self::MARKER_SUFFIX;
4546 $baseTitleText = $this->mTitle->getPrefixedDBkey();
4547 $oldType = $this->mOutputType;
4548 $this->setOutputType( self::OT_WIKI );
4549 $frame = $this->getPreprocessor()->newFrame();
4550 $root = $this->preprocessToDom( $origText );
4551 $node = $root->getFirstChild();
4556 $headlines = $numMatches !==
false ?
$matches[3] : [];
4558 $maxTocLevel = $this->svcOptions->get(
'MaxTocLevel' );
4559 foreach ( $headlines as $headline ) {
4560 $isTemplate =
false;
4562 $sectionIndex =
false;
4564 $markerMatches = [];
4565 if ( preg_match(
"/^$markerRegex/", $headline, $markerMatches ) ) {
4566 $serial = $markerMatches[1];
4567 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4568 $isTemplate = ( $titleText != $baseTitleText );
4569 $headline = preg_replace(
"/^$markerRegex\\s*/",
"", $headline );
4573 $prevlevel = $level;
4575 $level =
$matches[1][$headlineCount];
4577 if ( $level > $prevlevel ) {
4578 # Increase TOC level
4580 $sublevelCount[$toclevel] = 0;
4581 if ( $toclevel < $maxTocLevel ) {
4582 $prevtoclevel = $toclevel;
4586 } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4587 # Decrease TOC level, find level to jump to
4589 for ( $i = $toclevel; $i > 0; $i-- ) {
4590 if ( $levelCount[$i] == $level ) {
4591 # Found last matching level
4594 } elseif ( $levelCount[$i] < $level ) {
4595 # Found first matching level below current level
4603 if ( $toclevel < $maxTocLevel ) {
4604 if ( $prevtoclevel < $maxTocLevel ) {
4605 # Unindent only if the previous toc level was shown :p
4607 $prevtoclevel = $toclevel;
4613 # No change in level, end TOC line
4614 if ( $toclevel < $maxTocLevel ) {
4619 $levelCount[$toclevel] = $level;
4621 # count number of headlines for each level
4622 $sublevelCount[$toclevel]++;
4624 for ( $i = 1; $i <= $toclevel; $i++ ) {
4625 if ( !empty( $sublevelCount[$i] ) ) {
4629 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4634 # The safe header is a version of the header text safe to use for links
4636 # Remove link placeholders by the link text.
4637 # <!--LINK number-->
4639 # link text with suffix
4640 # Do this before unstrip since link text can contain strip markers
4641 $safeHeadline = $this->replaceLinkHoldersTextPrivate( $headline );
4643 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4644 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4646 # Remove any <style> or <script> tags (T198618)
4647 $safeHeadline = preg_replace(
4648 '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4653 # Strip out HTML (first regex removes any tag not allowed)
4655 # * <sup> and <sub> (T10393)
4659 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4660 # * <s> and <strike> (T35715)
4661 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4662 # to allow setting directionality in toc items.
4663 $tocline = preg_replace(
4665 '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4666 '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4672 # Strip '<span></span>', which is the result from the above if
4673 # <span id="foo"></span> is used to produce an additional anchor
4675 $tocline = str_replace(
'<span></span>',
'', $tocline );
4677 $tocline = trim( $tocline );
4679 # For the anchor, strip out HTML-y stuff period
4680 $safeHeadline = preg_replace(
'/<.*?>/',
'', $safeHeadline );
4681 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4683 # Save headline for section edit hint before it's escaped
4684 $headlineHint = $safeHeadline;
4686 # Decode HTML entities
4687 $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4689 $safeHeadline = self::normalizeSectionName( $safeHeadline );
4691 $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4692 $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4693 $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4694 if ( $fallbackHeadline === $safeHeadline ) {
4695 # No reason to have both (in fact, we can't)
4696 $fallbackHeadline =
false;
4699 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4700 # @todo FIXME: We may be changing them depending on the current locale.
4701 $arrayKey = strtolower( $safeHeadline );
4702 if ( $fallbackHeadline ===
false ) {
4703 $fallbackArrayKey =
false;
4705 $fallbackArrayKey = strtolower( $fallbackHeadline );
4708 # Create the anchor for linking from the TOC to the section
4709 $anchor = $safeHeadline;
4710 $fallbackAnchor = $fallbackHeadline;
4711 if ( isset( $refers[$arrayKey] ) ) {
4713 for ( $i = 2; isset( $refers[
"${arrayKey}_$i"] ); ++$i );
4715 $linkAnchor .=
"_$i";
4716 $refers[
"${arrayKey}_$i"] =
true;
4718 $refers[$arrayKey] =
true;
4720 if ( $fallbackHeadline !==
false && isset( $refers[$fallbackArrayKey] ) ) {
4722 for ( $i = 2; isset( $refers[
"${fallbackArrayKey}_$i"] ); ++$i );
4723 $fallbackAnchor .=
"_$i";
4724 $refers[
"${fallbackArrayKey}_$i"] =
true;
4726 $refers[$fallbackArrayKey] =
true;
4729 # Don't number the heading if it is the only one (looks silly)
4730 if ( count(
$matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4731 # the two are different if the line contains a link
4732 $headline = Html::element(
4734 [
'class' =>
'mw-headline-number' ],
4736 ) .
' ' . $headline;
4739 if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4741 $numbering, $toclevel, ( $isTemplate ?
false : $sectionIndex ) );
4744 # Add the section to the section tree
4745 # Find the DOM node for this header
4746 $noOffset = ( $isTemplate || $sectionIndex === false );
4747 while ( $node && !$noOffset ) {
4748 if ( $node->getName() ===
'h' ) {
4749 $bits = $node->splitHeading();
4750 if ( $bits[
'i'] == $sectionIndex ) {
4754 $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4755 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4756 $node = $node->getNextSibling();
4759 'toclevel' => $toclevel,
4762 'number' => $numbering,
4763 'index' => ( $isTemplate ?
'T-' :
'' ) . $sectionIndex,
4764 'fromtitle' => $titleText,
4765 'byteoffset' => ( $noOffset ?
null : $byteOffset ),
4766 'anchor' => $anchor,
4769 # give headline the correct <h#> tag
4770 if ( $maybeShowEditLink && $sectionIndex !==
false ) {
4772 if ( $isTemplate ) {
4773 # Put a T flag in the section identifier, to indicate to extractSections()
4774 # that sections inside <includeonly> should be counted.
4775 $editsectionPage = $titleText;
4776 $editsectionSection =
"T-$sectionIndex";
4777 $editsectionContent =
null;
4779 $editsectionPage = $this->mTitle->getPrefixedText();
4780 $editsectionSection = $sectionIndex;
4781 $editsectionContent = $headlineHint;
4795 $editlink =
'<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4796 $editlink .=
'" section="' . htmlspecialchars( $editsectionSection ) .
'"';
4797 if ( $editsectionContent !==
null ) {
4798 $editlink .=
'>' . $editsectionContent .
'</mw:editsection>';
4806 $matches[
'attrib'][$headlineCount], $anchor, $headline,
4807 $editlink, $fallbackAnchor );
4812 $this->setOutputType( $oldType );
4814 # Never ever show TOC if no headers
4815 if ( $numVisible < 1 ) {
4820 if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4824 $this->mOutput->setTOCHTML( $toc );
4825 $toc = self::TOC_START . $toc . self::TOC_END;
4829 $this->mOutput->setSections( $tocraw );
4832 # split up and insert constructed headlines
4833 $blocks = preg_split(
'/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4838 foreach ( $blocks as $block ) {
4840 if ( empty( $head[$i - 1] ) ) {
4841 $sections[$i] = $block;
4843 $sections[$i] = $head[$i - 1] . $block;
4856 Hooks::run(
'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4861 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4864 $sections[0] .= $toc .
"\n";
4867 $full .= implode(
'', $sections );
4869 if ( $this->mForceTocPosition ) {
4870 return str_replace(
'<!--MWTOC\'"-->', $toc, $full );
4890 if ( $clearState ) {
4891 $magicScopeVariable = $this->lock();
4893 $this->startParse(
$title, $options, self::OT_WIKI, $clearState );
4894 $this->setUser( $user );
4897 $text = str_replace(
"\000",
'', $text );
4905 $text = $this->pstPass2( $text, $user );
4907 $text = $this->mStripState->unstripBoth( $text );
4909 $this->setUser(
null ); # Reset
4923 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4924 # $this->contLang here in order to give everyone the same signature and use the default one
4925 # rather than the one selected in each user's preferences. (see also T14815)
4926 $ts = $this->mOptions->getTimestamp();
4927 $timestamp = MWTimestamp::getLocalInstance( $ts );
4928 $ts = $timestamp->format(
'YmdHis' );
4929 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4931 $d = $this->contLang->timeanddate( $ts,
false,
false ) .
" ($tzMsg)";
4933 # Variable replacement
4934 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4935 $text = $this->replaceVariables( $text );
4937 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4938 # which may corrupt this parser instance via its wfMessage()->text() call-
4941 if ( strpos( $text,
'~~~' ) !==
false ) {
4942 $sigText = $this->getUserSig( $user );
4943 $text = strtr( $text, [
4945 '~~~~' =>
"$sigText $d",
4948 # The main two signature forms used above are time-sensitive
4949 $this->setOutputFlag(
'user-signature',
'User signature detected' );
4952 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4953 $tc =
'[' . Title::legalChars() .
']';
4954 $nc =
'[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4957 $p1 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4959 $p4 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4961 $p3 =
"/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4963 $p2 =
"/\[\[\\|($tc+)]]/";
4965 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4966 $text = preg_replace( $p1,
'[[\\1\\2\\3|\\2]]', $text );
4967 $text = preg_replace( $p4,
'[[\\1\\2\\3|\\2]]', $text );
4968 $text = preg_replace( $p3,
'[[\\1\\2\\3\\4|\\2]]', $text );
4970 $t = $this->mTitle->getText();
4972 if ( preg_match(
"/^($nc+:|)$tc+?( \\($tc+\\))$/",
$t, $m ) ) {
4973 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4974 } elseif ( preg_match(
"/^($nc+:|)$tc+?(, $tc+|)$/",
$t, $m ) &&
"$m[1]$m[2]" !=
'' ) {
4975 $text = preg_replace( $p2,
"[[$m[1]\\1$m[2]|\\1]]", $text );
4977 # if there's no context, don't bother duplicating the title
4978 $text = preg_replace( $p2,
'[[\\1]]', $text );
4998 public function getUserSig( &$user, $nickname =
false, $fancySig =
null ) {
4999 $username = $user->getName();
5001 # If not given, retrieve from the user object.
5002 if ( $nickname ===
false ) {
5003 $nickname = $user->getOption(
'nickname' );
5006 if ( is_null( $fancySig ) ) {
5007 $fancySig = $user->getBoolOption(
'fancysig' );
5010 $nickname = $nickname ==
null ? $username : $nickname;
5012 if ( mb_strlen( $nickname ) > $this->svcOptions->get(
'MaxSigChars' ) ) {
5013 $nickname = $username;
5014 $this->logger->debug( __METHOD__ .
": $username has overlong signature." );
5015 } elseif ( $fancySig !==
false ) {
5016 # Sig. might contain markup; validate this
5017 if ( $this->validateSig( $nickname ) !==
false ) {
5018 # Validated; clean up (if needed) and return it
5019 return $this->cleanSig( $nickname,
true );
5021 # Failed to validate; fall back to the default
5022 $nickname = $username;
5023 $this->logger->debug( __METHOD__ .
": $username has bad XML tags in signature." );
5027 # Make sure nickname doesnt get a sig in a sig
5028 $nickname = self::cleanSigInSig( $nickname );
5030 # If we're still here, make it a link to the user page
5033 $msgName = $user->isAnon() ?
'signature-anon' :
'signature';
5035 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
5036 ->title( $this->
getTitle() )->text();
5046 return Xml::isWellFormedXmlFragment( $text ) ? $text :
false;
5062 $magicScopeVariable = $this->lock();
5066 # Option to disable this feature
5067 if ( !$this->mOptions->getCleanSignatures() ) {
5071 # @todo FIXME: Regex doesn't respect extension tags or nowiki
5072 # => Move this logic to braceSubstitution()
5073 $substWord = $this->magicWordFactory->get(
'subst' );
5074 $substRegex =
'/\{\{(?!(?:' . $substWord->getBaseRegex() .
'))/x' . $substWord->getRegexCase();
5075 $substText =
'{{' . $substWord->getSynonym( 0 );
5077 $text = preg_replace( $substRegex, $substText, $text );
5078 $text = self::cleanSigInSig( $text );
5079 $dom = $this->preprocessToDom( $text );
5080 $frame = $this->getPreprocessor()->newFrame();
5081 $text = $frame->expand( $dom );
5084 $text = $this->mStripState->unstripBoth( $text );
5097 $text = preg_replace(
'/~{3,5}/',
'', $text );
5112 $outputType, $clearState =
true, $revId =
null
5114 $this->startParse(
$title, $options, $outputType, $clearState );
5115 if ( $revId !==
null ) {
5116 $this->mRevisionId = $revId;
5127 $outputType, $clearState =
true
5129 $this->setTitle(
$title );
5130 $this->mOptions = $options;
5131 $this->setOutputType( $outputType );
5132 if ( $clearState ) {
5133 $this->clearState();
5146 static $executing =
false;
5148 # Guard against infinite recursion
5159 $text = $this->preprocess( $text,
$title, $options );
5189 public function setHook( $tag, callable $callback ) {
5190 $tag = strtolower( $tag );
5191 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
5192 throw new MWException(
"Invalid character {$m[0]} in setHook('$tag', ...) call" );
5194 $oldVal = $this->mTagHooks[$tag] ??
null;
5195 $this->mTagHooks[$tag] = $callback;
5196 if ( !in_array( $tag, $this->mStripList ) ) {
5197 $this->mStripList[] = $tag;
5221 $tag = strtolower( $tag );
5222 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
5223 throw new MWException(
"Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5225 $oldVal = $this->mTransparentTagHooks[$tag] ??
null;
5226 $this->mTransparentTagHooks[$tag] = $callback;
5235 $this->mTagHooks = [];
5236 $this->mFunctionTagHooks = [];
5237 $this->mStripList = $this->mDefaultStripList;
5284 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] :
null;
5285 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5287 # Add to function cache
5288 $mw = $this->magicWordFactory->get( $id );
5290 throw new MWException( __METHOD__ .
'() expecting a magic word identifier.' );
5293 $synonyms = $mw->getSynonyms();
5294 $sensitive = intval( $mw->isCaseSensitive() );
5296 foreach ( $synonyms as $syn ) {
5298 if ( !$sensitive ) {
5299 $syn = $this->contLang->lc( $syn );
5302 if ( !( $flags & self::SFH_NO_HASH ) ) {
5305 # Remove trailing colon
5306 if ( substr( $syn, -1, 1 ) ===
':' ) {
5307 $syn = substr( $syn, 0, -1 );
5309 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5320 $this->firstCallInit();
5321 return array_keys( $this->mFunctionHooks );
5335 $tag = strtolower( $tag );
5336 if ( preg_match(
'/[<>\r\n]/', $tag, $m ) ) {
5337 throw new MWException(
"Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5339 $old = $this->mFunctionTagHooks[$tag] ??
null;
5340 $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5342 if ( !in_array( $tag, $this->mStripList ) ) {
5343 $this->mStripList[] = $tag;
5358 $this->replaceLinkHoldersPrivate( $text, $options );
5369 $this->mLinkHolders->replace( $text );
5382 return $this->replaceLinkHoldersTextPrivate( $text );
5393 return $this->mLinkHolders->replaceText( $text );
5411 if ( isset( $params[
'mode'] ) ) {
5412 $mode = $params[
'mode'];
5416 $ig = ImageGalleryBase::factory( $mode );
5417 }
catch ( Exception $e ) {
5419 $ig = ImageGalleryBase::factory(
false );
5422 $ig->setContextTitle( $this->mTitle );
5423 $ig->setShowBytes(
false );
5424 $ig->setShowDimensions(
false );
5425 $ig->setShowFilename(
false );
5426 $ig->setParser( $this );
5427 $ig->setHideBadImages();
5428 $ig->setAttributes( Sanitizer::validateTagAttributes( $params,
'ul' ) );
5430 if ( isset( $params[
'showfilename'] ) ) {
5431 $ig->setShowFilename(
true );
5433 $ig->setShowFilename(
false );
5435 if ( isset( $params[
'caption'] ) ) {
5439 $caption = $this->recursiveTagParse( $params[
'caption'] );
5440 $ig->setCaptionHtml( $caption );
5442 if ( isset( $params[
'perrow'] ) ) {
5443 $ig->setPerRow( $params[
'perrow'] );
5445 if ( isset( $params[
'widths'] ) ) {
5446 $ig->setWidths( $params[
'widths'] );
5448 if ( isset( $params[
'heights'] ) ) {
5449 $ig->setHeights( $params[
'heights'] );
5451 $ig->setAdditionalOptions( $params );
5455 Hooks::run(
'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5457 $lines = StringUtils::explode(
"\n", $text );
5459 # match lines like these:
5460 # Image:someimage.jpg|This is some image
5468 if ( strpos(
$matches[0],
'%' ) !==
false ) {
5472 if ( is_null(
$title ) ) {
5473 # Bogus title. Ignore these so we don't bomb out later.
5477 # We need to get what handler the file uses, to figure out parameters.
5478 # Note, a hook can overide the file name, and chose an entirely different
5479 # file (which potentially could be of a different type and have different handler).
5482 Hooks::run(
'BeforeParserFetchFileAndTitle',
5483 [ $this,
$title, &$options, &$descQuery ] );
5484 # Don't register it now, as TraditionalImageGallery does that later.
5485 $file = $this->fetchFileNoRegister(
$title, $options );
5486 $handler =
$file ?
$file->getHandler() :
false;
5489 'img_alt' =>
'gallery-internal-alt',
5490 'img_link' =>
'gallery-internal-link',
5493 $paramMap += $handler->getParamMap();
5496 unset( $paramMap[
'img_width'] );
5499 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5504 $handlerOptions = [];
5514 $parameterMatches = StringUtils::delimiterExplode(
5518 foreach ( $parameterMatches as $parameterMatch ) {
5519 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5521 $paramName = $paramMap[$magicName];
5523 switch ( $paramName ) {
5524 case 'gallery-internal-alt':
5525 $alt = $this->stripAltTextPrivate( $match,
false );
5527 case 'gallery-internal-link':
5528 $linkValue = $this->stripAltTextPrivate( $match,
false );
5529 if ( preg_match(
'/^-{R|(.*)}-$/', $linkValue ) ) {
5532 $linkValue = substr( $linkValue, 4, -2 );
5534 list(
$type, $target ) = $this->parseLinkParameterPrivate( $linkValue );
5535 if (
$type ===
'link-url' ) {
5537 $this->mOutput->addExternalLink( $target );
5538 } elseif (
$type ===
'link-title' ) {
5539 $link = $target->getLinkURL();
5540 $this->mOutput->addLink( $target );
5545 if ( $handler->validateParam( $paramName, $match ) ) {
5546 $handlerOptions[$paramName] = $match;
5549 $this->logger->debug(
5550 "$parameterMatch failed parameter validation" );
5551 $label = $parameterMatch;
5557 $label = $parameterMatch;
5562 $ig->add(
$title, $label, $alt, $link, $handlerOptions );
5564 $html = $ig->toHTML();
5565 Hooks::run(
'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5576 return $this->getImageParamsPrivate( $handler );
5585 $handlerClass = get_class( $handler );
5589 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5590 # Initialise static lists
5591 static $internalParamNames = [
5592 'horizAlign' => [
'left',
'right',
'center',
'none' ],
5593 'vertAlign' => [
'baseline',
'sub',
'super',
'top',
'text-top',
'middle',
5594 'bottom',
'text-bottom' ],
5595 'frame' => [
'thumbnail',
'manualthumb',
'framed',
'frameless',
5596 'upright',
'border',
'link',
'alt',
'class' ],
5598 static $internalParamMap;
5599 if ( !$internalParamMap ) {
5600 $internalParamMap = [];
5601 foreach ( $internalParamNames as
$type => $names ) {
5602 foreach ( $names as $name ) {
5608 $magicName = str_replace(
'-',
'_',
"img_$name" );
5609 $internalParamMap[$magicName] = [
$type, $name ];
5614 # Add handler params
5615 $paramMap = $internalParamMap;
5617 $handlerParamMap = $handler->getParamMap();
5618 foreach ( $handlerParamMap as $magic => $paramName ) {
5619 $paramMap[$magic] = [
'handler', $paramName ];
5622 $this->mImageParams[$handlerClass] = $paramMap;
5623 $this->mImageParamsMagicArray[$handlerClass] =
5624 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5626 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5638 # Check if the options text is of the form "options|alt text"
5640 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5641 # * left no resizing, just left align. label is used for alt= only
5642 # * right same, but right aligned
5643 # * none same, but not aligned
5644 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5645 # * center center the image
5646 # * frame Keep original image size, no magnify-button.
5647 # * framed Same as "frame"
5648 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5649 # * upright reduce width for upright images, rounded to full __0 px
5650 # * border draw a 1px border around the image
5651 # * alt Text for HTML alt attribute (defaults to empty)
5652 # * class Set a class for img node
5653 # * link Set the target of the image link. Can be external, interwiki, or local
5654 # vertical-align values (no % or length right now):
5664 # Protect LanguageConverter markup when splitting into parts
5665 $parts = StringUtils::delimiterExplode(
5666 '-{',
'}-',
'|', $options,
true
5669 # Give extensions a chance to select the file revision for us
5672 Hooks::run(
'BeforeParserFetchFileAndTitle',
5673 [ $this,
$title, &$options, &$descQuery ] );
5674 # Fetch and register the file (file title may be different via hooks)
5678 $handler =
$file ?
$file->getHandler() :
false;
5680 list( $paramMap, $mwArray ) = $this->getImageParamsPrivate( $handler );
5683 $this->addTrackingCategory(
'broken-file-category' );
5686 # Process the input parameters
5688 $params = [
'frame' => [],
'handler' => [],
5689 'horizAlign' => [],
'vertAlign' => [] ];
5690 $seenformat =
false;
5691 foreach ( $parts as $part ) {
5692 $part = trim( $part );
5693 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5695 if ( isset( $paramMap[$magicName] ) ) {
5696 list(
$type, $paramName ) = $paramMap[$magicName];
5698 # Special case; width and height come in one variable together
5699 if (
$type ===
'handler' && $paramName ===
'width' ) {
5700 $parsedWidthParam = self::parseWidthParam( $value );
5701 if ( isset( $parsedWidthParam[
'width'] ) ) {
5702 $width = $parsedWidthParam[
'width'];
5703 if ( $handler->validateParam(
'width', $width ) ) {
5704 $params[
$type][
'width'] = $width;
5708 if ( isset( $parsedWidthParam[
'height'] ) ) {
5709 $height = $parsedWidthParam[
'height'];
5710 if ( $handler->validateParam(
'height', $height ) ) {
5711 $params[
$type][
'height'] = $height;
5715 # else no validation -- T15436
5717 if (
$type ===
'handler' ) {
5718 # Validate handler parameter
5719 $validated = $handler->validateParam( $paramName, $value );
5721 # Validate internal parameters
5722 switch ( $paramName ) {
5726 # @todo FIXME: Possibly check validity here for
5727 # manualthumb? downstream behavior seems odd with
5728 # missing manual thumbs.
5730 $value = $this->stripAltTextPrivate( $value, $holders );
5733 list( $paramName, $value ) =
5734 $this->parseLinkParameterPrivate(
5735 $this->stripAltTextPrivate( $value, $holders )
5739 if ( $paramName ===
'no-link' ) {
5742 if ( ( $paramName ===
'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5743 $params[
$type][
'link-target'] = $this->mOptions->getExternalLinkTarget();
5751 $validated = !$seenformat;
5755 # Most other things appear to be empty or numeric...
5756 $validated = ( $value ===
false || is_numeric( trim( $value ) ) );
5761 $params[
$type][$paramName] = $value;
5765 if ( !$validated ) {
5770 # Process alignment parameters
5771 if ( $params[
'horizAlign'] ) {
5772 $params[
'frame'][
'align'] = key( $params[
'horizAlign'] );
5774 if ( $params[
'vertAlign'] ) {
5775 $params[
'frame'][
'valign'] = key( $params[
'vertAlign'] );
5778 $params[
'frame'][
'caption'] = $caption;
5780 # Will the image be presented in a frame, with the caption below?
5781 $imageIsFramed = isset( $params[
'frame'][
'frame'] )
5782 || isset( $params[
'frame'][
'framed'] )
5783 || isset( $params[
'frame'][
'thumbnail'] )
5784 || isset( $params[
'frame'][
'manualthumb'] );
5786 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5787 # came to also set the caption, ordinary text after the image -- which
5788 # makes no sense, because that just repeats the text multiple times in
5789 # screen readers. It *also* came to set the title attribute.
5790 # Now that we have an alt attribute, we should not set the alt text to
5791 # equal the caption: that's worse than useless, it just repeats the
5792 # text. This is the framed/thumbnail case. If there's no caption, we
5793 # use the unnamed parameter for alt text as well, just for the time be-
5794 # ing, if the unnamed param is set and the alt param is not.
5795 # For the future, we need to figure out if we want to tweak this more,
5796 # e.g., introducing a title= parameter for the title; ignoring the un-
5797 # named parameter entirely for images without a caption; adding an ex-
5798 # plicit caption= parameter and preserving the old magic unnamed para-
5800 if ( $imageIsFramed ) { # Framed image
5801 if ( $caption ===
'' && !isset( $params[
'frame'][
'alt'] ) ) {
5802 # No caption or alt text, add the filename as the alt text so
5803 # that screen readers at least get some description of the image
5804 $params[
'frame'][
'alt'] =
$title->getText();
5806 # Do not set $params['frame']['title'] because tooltips don't make sense
5808 }
else { # Inline image
5809 if ( !isset( $params[
'frame'][
'alt'] ) ) {
5810 # No alt text, use the "caption" for the alt text
5811 if ( $caption !==
'' ) {
5812 $params[
'frame'][
'alt'] = $this->stripAltTextPrivate( $caption, $holders );
5814 # No caption, fall back to using the filename for the
5816 $params[
'frame'][
'alt'] =
$title->getText();
5819 # Use the "caption" for the tooltip text
5820 $params[
'frame'][
'title'] = $this->stripAltTextPrivate( $caption, $holders );
5822 $params[
'handler'][
'targetlang'] = $this->getTargetLanguage()->getCode();
5824 Hooks::run(
'ParserMakeImageParams', [
$title,
$file, &$params, $this ] );
5826 # Linker does the rest
5827 $time = $options[
'time'] ??
false;
5829 $time, $descQuery, $this->mOptions->getThumbSize() );
5831 # Give the handler a chance to modify the parser object
5833 $handler->parserTransformHook( $this,
$file );
5860 return $this->parseLinkParameterPrivate( $value );
5882 $chars = self::EXT_LINK_URL_CLASS;
5883 $addr = self::EXT_LINK_ADDR;
5884 $prots = $this->mUrlProtocols;
5887 if ( $value ===
'' ) {
5889 } elseif ( preg_match(
"/^((?i)$prots)/", $value ) ) {
5890 if ( preg_match(
"/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5891 $this->mOutput->addExternalLink( $value );
5896 $linkTitle = Title::newFromText( $value );
5898 $this->mOutput->addLink( $linkTitle );
5899 $type =
'link-title';
5900 $target = $linkTitle;
5903 return [
$type, $target ];
5914 return $this->stripAltTextPrivate( $caption, $holders );
5923 # Strip bad stuff out of the title (tooltip). We can't just use
5924 # replaceLinkHoldersText() here, because if this function is called
5925 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5927 $tooltip = $holders->replaceText( $caption );
5929 $tooltip = $this->replaceLinkHoldersTextPrivate( $caption );
5932 # make sure there are no placeholders in thumbnail attributes
5933 # that are later expanded to html- so expand them now and
5935 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5936 # Compatibility hack! In HTML certain entity references not terminated
5937 # by a semicolon are decoded (but not if we're in an attribute; that's
5938 # how link URLs get away without properly escaping & in queries).
5939 # But wikitext has always required semicolon-termination of entities,
5940 # so encode & where needed to avoid decode of semicolon-less entities.
5943 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5944 $tooltip = preg_replace(
"/
5945 & # 1. entity prefix
5946 (?= # 2. followed by:
5947 (?: # a. one of the legacy semicolon-less named entities
5948 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5949 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5950 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5951 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5952 U(?:acute|circ|grave|uml)|Yacute|
5953 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5954 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5955 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5956 frac(?:1(?:2|4)|34)|
5957 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5958 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5959 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5960 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5961 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5962 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5963 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5964 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5965 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5967 (?:[^;]|$)) # b. and not followed by a semicolon
5968 # S = study, for efficiency
5969 /Sx",
'&', $tooltip );
5970 $tooltip = Sanitizer::stripAllTags( $tooltip );
5982 $this->logger->debug(
"Parser output marked as uncacheable." );
5983 if ( !$this->mOutput ) {
5985 " can only be called when actually parsing something" );
5987 $this->mOutput->updateCacheExpiry( 0 );
5999 $text = $this->replaceVariables( $text, $frame );
6000 $text = $this->mStripState->unstripBoth( $text );
6010 $this->firstCallInit();
6012 array_keys( $this->mTransparentTagHooks ),
6013 array_keys( $this->mTagHooks ),
6014 array_keys( $this->mFunctionTagHooks )
6023 $this->firstCallInit();
6024 return $this->mFunctionSynonyms;
6032 return $this->mUrlProtocols;
6047 $elements = array_keys( $this->mTransparentTagHooks );
6048 $text = self::extractTagsAndParams( $elements, $text,
$matches );
6051 foreach (
$matches as $marker => $data ) {
6052 list( $element,
$content, $params, $tag ) = $data;
6053 $tagName = strtolower( $element );
6054 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
6055 $output = call_user_func_array(
6056 $this->mTransparentTagHooks[$tagName],
6062 $replacements[$marker] = $output;
6064 return strtr( $text, $replacements );
6097 global
$wgTitle; # not generally used but removes an ugly failure mode
6099 $magicScopeVariable = $this->lock();
6102 $frame = $this->getPreprocessor()->newFrame();
6104 # Process section extraction flags
6106 $sectionParts = explode(
'-', $sectionId );
6107 $sectionIndex = array_pop( $sectionParts );
6108 foreach ( $sectionParts as $part ) {
6109 if ( $part ===
'T' ) {
6110 $flags |= self::PTD_FOR_INCLUSION;
6114 # Check for empty input
6115 if ( strval( $text ) ===
'' ) {
6116 # Only sections 0 and T-0 exist in an empty document
6117 if ( $sectionIndex == 0 ) {
6118 if ( $mode ===
'get' ) {
6124 if ( $mode ===
'get' ) {
6132 # Preprocess the text
6133 $root = $this->preprocessToDom( $text, $flags );
6135 # <h> nodes indicate section breaks
6136 # They can only occur at the top level, so we can find them by iterating the root's children
6137 $node = $root->getFirstChild();
6139 # Find the target section
6140 if ( $sectionIndex == 0 ) {
6141 # Section zero doesn't nest, level=big
6142 $targetLevel = 1000;
6145 if ( $node->getName() ===
'h' ) {
6146 $bits = $node->splitHeading();
6147 if ( $bits[
'i'] == $sectionIndex ) {
6148 $targetLevel = $bits[
'level'];
6152 if ( $mode ===
'replace' ) {
6153 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6155 $node = $node->getNextSibling();
6161 if ( $mode ===
'get' ) {
6168 # Find the end of the section, including nested sections
6170 if ( $node->getName() ===
'h' ) {
6171 $bits = $node->splitHeading();
6172 $curLevel = $bits[
'level'];
6173 if ( $bits[
'i'] != $sectionIndex && $curLevel <= $targetLevel ) {
6177 if ( $mode ===
'get' ) {
6178 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6180 $node = $node->getNextSibling();
6183 # Write out the remainder (in replace mode only)
6184 if ( $mode ===
'replace' ) {
6185 # Output the replacement text
6186 # Add two newlines on -- trailing whitespace in $newText is conventionally
6187 # stripped by the editor, so we need both newlines to restore the paragraph gap
6188 # Only add trailing whitespace if there is newText
6189 if ( $newText !=
"" ) {
6190 $outText .= $newText .
"\n\n";
6194 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6195 $node = $node->getNextSibling();
6199 if ( is_string( $outText ) ) {
6200 # Re-insert stripped tags
6201 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
6221 public function getSection( $text, $sectionId, $defaultText =
'' ) {
6222 return $this->extractSections( $text, $sectionId,
'get', $defaultText );
6238 return $this->extractSections( $oldText, $sectionId,
'replace', $newText );
6252 return $this->mRevisionId;
6262 if ( $this->mRevisionObject ) {
6263 return $this->mRevisionObject;
6273 $rev = call_user_func(
6274 $this->mOptions->getCurrentRevisionCallback(),
6279 if ( $this->mRevisionId ===
null && $rev && $rev->getId() ) {
6291 if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
6292 $rev = Revision::newFromId( $this->mRevisionId );
6295 $this->mRevisionObject = $rev;
6297 return $this->mRevisionObject;
6306 if ( $this->mRevisionTimestamp !==
null ) {
6307 return $this->mRevisionTimestamp;
6310 # Use specified revision timestamp, falling back to the current timestamp
6311 $revObject = $this->getRevisionObject();
6312 $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
6313 $this->mOutput->setRevisionTimestampUsed( $timestamp );
6315 # The cryptic '' timezone parameter tells to use the site-default
6316 # timezone offset instead of the user settings.
6317 # Since this value will be saved into the parser cache, served
6318 # to other users, and potentially even used inside links and such,
6319 # it needs to be consistent for all visitors.
6320 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp,
'' );
6322 return $this->mRevisionTimestamp;
6331 if ( is_null( $this->mRevisionUser ) ) {
6332 $revObject = $this->getRevisionObject();
6334 # if this template is subst: the revision id will be blank,
6335 # so just use the current user's name
6337 $this->mRevisionUser = $revObject->getUserText();
6338 } elseif ( $this->ot[
'wiki'] || $this->mOptions->getIsPreview() ) {
6339 $this->mRevisionUser = $this->
getUser()->getName();
6342 return $this->mRevisionUser;
6351 if ( is_null( $this->mRevisionSize ) ) {
6352 $revObject = $this->getRevisionObject();
6354 # if this variable is subst: the revision id will be blank,
6355 # so just use the parser input size, because the own substituation
6356 # will change the size.
6358 $this->mRevisionSize = $revObject->getSize();
6360 $this->mRevisionSize = $this->mInputSize;
6363 return $this->mRevisionSize;
6372 $this->mDefaultSort =
$sort;
6373 $this->mOutput->setProperty(
'defaultsort',
$sort );
6387 if ( $this->mDefaultSort !==
false ) {
6388 return $this->mDefaultSort;
6401 return $this->mDefaultSort;
6405 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6406 $text = Sanitizer::decodeCharReferences( $text );
6407 $text = self::normalizeSectionName( $text );
6412 return '#' . Sanitizer::escapeIdForLink( $sectionName );
6416 $fragmentMode = $this->svcOptions->get(
'FragmentMode' );
6417 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] ===
'legacy' ) {
6419 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6421 $id = Sanitizer::escapeIdForLink( $sectionName );
6436 # Strip out wikitext links(they break the anchor)
6437 $text = $this->stripSectionName( $text );
6438 $sectionName = self::getSectionNameFromStrippedText( $text );
6439 return self::makeAnchor( $sectionName );
6452 # Strip out wikitext links(they break the anchor)
6453 $text = $this->stripSectionName( $text );
6454 $sectionName = self::getSectionNameFromStrippedText( $text );
6455 return $this->makeLegacyAnchor( $sectionName );
6464 $sectionName = self::getSectionNameFromStrippedText( $text );
6465 return self::makeAnchor( $sectionName );
6475 # T90902: ensure the same normalization is applied for IDs as to links
6477 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6478 '@phan-var MediaWikiTitleCodec $titleParser';
6481 $parts = $titleParser->splitTitleString(
"#$text" );
6485 return $parts[
'fragment'];
6503 # Strip internal link markup
6504 $text = preg_replace(
'/\[\[:?([^[|]+)\|([^[]+)\]\]/',
'$2', $text );
6505 $text = preg_replace(
'/\[\[:?([^[]+)\|?\]\]/',
'$1', $text );
6507 # Strip external link markup
6508 # @todo FIXME: Not tolerant to blank link text
6510 # on how many empty links there are on the page - need to figure that out.
6511 $text = preg_replace(
'/\[(?i:' . $this->mUrlProtocols .
')([^ ]+?) ([^[]+)\]/',
'$2', $text );
6513 # Parse wikitext quotes (italics & bold)
6514 $text = $this->doQuotes( $text );
6517 $text = StringUtils::delimiterReplace(
'<',
'>',
'', $text );
6533 $outputType = self::OT_HTML
6536 return $this->fuzzTestSrvus( $text,
$title, $options, $outputType );
6550 $outputType = self::OT_HTML
6552 $magicScopeVariable = $this->lock();
6553 $this->startParse(
$title, $options, $outputType,
true );
6555 $text = $this->replaceVariables( $text );
6556 $text = $this->mStripState->unstripBoth( $text );
6557 $text = Sanitizer::removeHTMLtags( $text );
6570 return $this->fuzzTestPst( $text,
$title, $options );
6580 return $this->preSaveTransform( $text,
$title, $options->
getUser(), $options );
6592 return $this->fuzzTestPreprocess( $text,
$title, $options );
6602 return $this->fuzzTestSrvus( $text,
$title, $options, self::OT_PREPROCESS );
6624 while ( $i < strlen(
$s ) ) {
6625 $markerStart = strpos(
$s, self::MARKER_PREFIX, $i );
6626 if ( $markerStart ===
false ) {
6627 $out .= call_user_func( $callback, substr(
$s, $i ) );
6630 $out .= call_user_func( $callback, substr(
$s, $i, $markerStart - $i ) );
6631 $markerEnd = strpos(
$s, self::MARKER_SUFFIX, $markerStart );
6632 if ( $markerEnd ===
false ) {
6633 $out .= substr(
$s, $markerStart );
6636 $markerEnd += strlen( self::MARKER_SUFFIX );
6637 $out .= substr(
$s, $markerStart, $markerEnd - $markerStart );
6652 return $this->mStripState->killMarkers( $text );
6676 'version' => self::HALF_PARSED_VERSION,
6677 'stripState' => $this->mStripState->getSubState( $text ),
6678 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6701 if ( !isset( $data[
'version'] ) || $data[
'version'] != self::HALF_PARSED_VERSION ) {
6702 throw new MWException( __METHOD__ .
': invalid version' );
6705 # First, extract the strip state.
6706 $texts = [ $data[
'text'] ];
6707 $texts = $this->mStripState->merge( $data[
'stripState'], $texts );
6709 # Now renumber links
6710 $texts = $this->mLinkHolders->mergeForeign( $data[
'linkHolders'], $texts );
6712 # Should be good to go.
6728 return isset( $data[
'version'] ) && $data[
'version'] == self::HALF_PARSED_VERSION;
6741 $parsedWidthParam = [];
6742 if ( $value ===
'' ) {
6743 return $parsedWidthParam;
6746 # (T15500) In both cases (width/height and width only),
6747 # permit trailing "px" for backward compatibility.
6748 if ( $parseHeight && preg_match(
'/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6749 $width = intval( $m[1] );
6750 $height = intval( $m[2] );
6751 $parsedWidthParam[
'width'] = $width;
6752 $parsedWidthParam[
'height'] = $height;
6753 } elseif ( preg_match(
'/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6754 $width = intval( $value );
6755 $parsedWidthParam[
'width'] = $width;
6757 return $parsedWidthParam;
6770 if ( $this->mInParse ) {
6771 throw new MWException(
"Parser state cleared while parsing. "
6772 .
"Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6778 $this->mInParse = $e->getTraceAsString();
6780 $recursiveCheck =
new ScopedCallback(
function () {
6781 $this->mInParse =
false;
6784 return $recursiveCheck;
6799 if ( preg_match(
'/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1],
'</p>' ) ===
false ) {
6817 if ( $this->mInParse ) {
6818 return $this->factory->create();
6831 OutputPage::setupOOUI();
6832 $this->mOutput->setEnableOOUI(
true );
6840 $this->mOutput->setFlag( $flag );
6841 $name = $this->mTitle->getPrefixedText();
6842 $this->logger->debug( __METHOD__ .
": set $flag flag on '$name'; $reason" );
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfHostname()
Get host name of the current machine, for use in error reporting.
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...
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
static register( $parser)
static register( $parser)
WebRequest clone which takes values from a provided array.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Marks HTML that shouldn't be escaped.
Internationalisation code.
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
static normalizeSubpageLink( $contextTitle, $target, &$text)
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
static tocIndent()
Add another level to the Table of Contents.
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...
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
static tocList( $toc, Language $lang=null)
Wraps the TOC in a table and provides the hide/collapse javascript.
static tocLineEnd()
End a Table Of Contents line.
Class for handling an array of magic words.
A factory that stores information about MagicWords, and creates them on demand with caching.
Handles a simple LRU key/value map with a maximum number of entries.
Factory for handling the special page list and generating SpecialPage objects.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Set options of the Parser.
getPreSaveTransform()
Transform wiki markup when saving the page?
getDisableTitleConversion()
Whether title conversion should be disabled.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
addTrackingCategory( $msg)
getTargetLanguage()
Get the target language for the content being parsed.
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
static normalizeUrlComponent( $component, $unsafe)
bool string $mInParse
Recursive call protection.
handleInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
extensionSubstitution( $params, $frame)
Return the text to be used for a given extension tag.
setDefaultSort( $sort)
Mutator for $mDefaultSort.
static stripOuterParagraph( $html)
Strip outer.
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
MagicWordFactory $magicWordFactory
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
__clone()
Allow extensions to clean up when the parser is cloned.
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
fetchFileNoRegister( $title, $options=[])
Helper function for fetchFileAndTitle.
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
LinkRenderer $mLinkRenderer
preprocess( $text, Title $title=null, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
renderImageGallery( $text, $params)
Renders an image gallery from a text with one line per image.
__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 getSectionNameFromStrippedText( $text)
stripAltText( $caption, $holders)
getOptions()
Get the ParserOptions object.
cleanSig( $text, $parsing=false)
Clean up signature text.
getStripState()
Get the StripState.
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...
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
getRevisionUser()
Get the name of the user that edited the last revision.
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
replaceExternalLinks( $text)
Replace external links (REL)
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
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...
armorLinks( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
setTitle(Title $t=null)
Set the context title.
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
doMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
unserializeHalfParsedText( $data)
Load the parser state given in the $data array, which is assumed to have been generated by serializeH...
limitationWarn( $limitationType, $current='', $max='')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
LinkHolderArray $mLinkHolders
makeImage( $title, $options, $holders=false)
Parse image options text and use it to make an image.
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached.
pstPass2( $text, $user)
Pre-save transform helper function.
transformMsg( $text, $options, $title=null)
Wrapper for preprocess()
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated.
getRevisionSize()
Get the size of the revision.
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
BadFileLookup $badFileLookup
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
getRevisionTimestampSubstring( $start, $len, $mtts, $variable)
getImageParams( $handler)
serializeHalfParsedText( $text)
Save the parser state required to convert the given half-parsed text to HTML.
formatHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
replaceInternalLinks2(&$text)
Process [[ ]] wikilinks (RIL)
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
getTemplateDom( $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
bool $mFirstCall
Whether firstCallInit still needs to be called.
doTableStuff( $text)
Parse the wiki syntax used to render tables.
getTitle()
Accessor for the Title object.
handleHeadings( $text)
Parse headers and return html.
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
startExternalParse(Title $title=null, 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 ...
testSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
MagicWordArray $mSubstWords
replaceTransparentTags( $text)
Replace transparent tags in $text with the values given by the callbacks.
MapCacheLRU null $currentRevisionCache
getLinkRenderer()
Get a LinkRenderer instance to make links with.
static splitWhitespace( $s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
setOutputType( $ot)
Set the output type.
makeLegacyAnchor( $sectionName)
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
getVariableValue( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
ServiceOptions $svcOptions
This is called $svcOptions instead of $options like elsewhere to avoid confusion with $mOptions,...
Title(Title $x=null)
Accessor/mutator for the Title object.
MagicWordArray $mVariables
getStripList()
Get a list of strippable XML-like elements.
setUser( $user)
Set the current user.
setOutputFlag( $flag, $reason)
markerSkipCallback( $s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
expandMagicVariable( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
static getDefaultPreprocessorClass()
Which class should we use for the preprocessor if not otherwise specified?
replaceLinkHoldersTextPrivate( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
doDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
getImageParamsPrivate( $handler)
fuzzTestPreprocess( $text, Title $title, ParserOptions $options)
argSubstitution( $piece, $frame)
Triple brace replacement – used for template arguments.
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
LinkRendererFactory $linkRendererFactory
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
handleInternalLinks( $text)
Process [[ ]] wikilinks.
fetchFileAndTitle( $title, $options=[])
Fetch a file and its title and register a reference to it.
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
static extractTagsAndParams( $elements, $text, &$matches)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
interwikiTransclude( $title, $action)
Transclude an interwiki link.
armorLinksPrivate( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
makeKnownLinkHolderPrivate( $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
incrementIncludeSize( $type, $size)
Increment an include size counter.
maybeDoSubpageLink( $target, &$text)
Handle link to subpage if necessary.
handleExternalLinks( $text)
Replace external links (REL)
getRevisionObject()
Get the revision object for $this->mRevisionId.
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
fetchTemplateAndTitle( $title)
Fetch the unparsed text of a template and register a reference to it.
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
getFunctionHooks()
Get all registered function hook identifiers.
doAllQuotes( $text)
Replace single quotes with HTML markup.
handleTables( $text)
Parse the wiki syntax used to render tables.
getContentLanguage()
Get the content language that this Parser is using.
Preprocessor $mPreprocessor
fetchCurrentRevisionOfTitle( $title)
Fetch the current revision of a given title.
getOutput()
Get the ParserOutput object.
Options( $x=null)
Accessor/mutator for the ParserOptions object.
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
clearTagHooks()
Remove all tag hooks.
getRevisionId()
Get the ID of the revision we are parsing.
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
validateSig( $text)
Check that the user's signature contains no bad XML.
getPreprocessor()
Get a preprocessor object.
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
OutputType( $x=null)
Accessor/mutator for the output type.
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
__destruct()
Reduce memory usage to reduce the impact of circular references.
resetOutput()
Reset the ParserOutput.
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
doQuotes( $text)
Helper function for doAllQuotes()
fetchTemplate( $title)
Fetch the unparsed text of a template and register a reference to it.
doHeadings( $text)
Parse headers and return html.
finalizeHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
insertStripItem( $text)
Add an item to the strip state Returns the unique tag which must be inserted into the stripped text T...
handleMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
setTransparentTagHook( $tag, callable $callback)
As setHook(), but letting the contents be parsed.
incrementExpensiveFunctionCount()
Increment the expensive function count.
testPreprocess( $text, Title $title, ParserOptions $options)
static makeAnchor( $sectionName)
handleAllQuotes( $text)
Replace single quotes with HTML markup.
SpecialPageFactory $specialPageFactory
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
clearState()
Clear Parser state.
killMarkers( $text)
Remove any strip markers found in the given text.
static array $constructorOptions
TODO Make this a const when HHVM support is dropped (T192166)
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
getUserSig(&$user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext.
braceSubstitution( $piece, $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
fuzzTestPst( $text, Title $title, ParserOptions $options)
parseLinkParameterPrivate( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
fuzzTestSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
isValidHalfParsedText( $data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(),...
replaceInternalLinks( $text)
Process [[ ]] wikilinks.
lock()
Lock the current instance of the parser.
static createAssocArgs( $args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
isCurrentRevisionOfTitleCached( $title)
parse( $text, Title $title, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
stripAltTextPrivate( $caption, $holders)
testPst( $text, Title $title, ParserOptions $options)
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
callParserFunction( $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
SectionProfiler $mProfiler
getConverterLanguage()
Get the language object for language conversion.
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Variant of the Message class.
Group all the pieces relevant to the context of a request into one instance.
setTitle(Title $title=null)
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static numberingroup( $group)
Find the number of users in a given user group.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Represents a title within MediaWiki.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
return[ 'OATHAuth'=> function(MediaWikiServices $services) { return new OATHAuth($services->getMainConfig(), $services->getDBLoadBalancerFactory());}, 'OATHUserRepository'=> function(MediaWikiServices $services) { global $wgOATHAuthDatabase;$auth=$services->getService( 'OATHAuth');return new OATHUserRepository($services->getDBLoadBalancerFactory() ->getMainLB( $wgOATHAuthDatabase), new \HashBagOStuff(['maxKey'=> 5]), $auth);}]
There are three types of nodes:
if(PHP_SAPI !=='cli' &&PHP_SAPI !=='phpdbg' $chars)
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.