MediaWiki master
Parser.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Parser;
11
12use BadMethodCallException;
13use DateTime;
14use DateTimeZone;
15use Exception;
16use InvalidArgumentException;
17use LogicException;
22use MediaWiki\Debug\DeprecationHelper;
35use MediaWiki\Languages\LanguageConverterFactory;
36use MediaWiki\Languages\LanguageNameUtils;
71use Psr\Log\LoggerInterface;
72use RuntimeException;
73use UnexpectedValueException;
74use Wikimedia\Bcp47Code\Bcp47CodeValue;
76use Wikimedia\IPUtils;
81use Wikimedia\Parsoid\Core\LinkTarget;
82use Wikimedia\Parsoid\Core\SectionMetadata;
83use Wikimedia\Parsoid\Core\TOCData;
84use Wikimedia\Parsoid\DOM\Comment;
85use Wikimedia\Parsoid\DOM\DocumentFragment;
86use Wikimedia\Parsoid\DOM\Element;
87use Wikimedia\Parsoid\DOM\Node;
88use Wikimedia\Parsoid\Utils\DOMCompat;
89use Wikimedia\Parsoid\Utils\DOMUtils;
90use Wikimedia\RemexHtml\Serializer\SerializerNode;
91use Wikimedia\ScopedCallback;
93
134#[\AllowDynamicProperties]
135class Parser {
136 use DeprecationHelper;
137
138 # Flags for Parser::setFunctionHook
139 public const SFH_NO_HASH = 1;
140 public const SFH_OBJECT_ARGS = 2;
141
142 # Constants needed for external link processing
150 public const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
155 // phpcs:ignore Generic.Files.LineLength
156 private const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
158 // phpcs:ignore Generic.Files.LineLength
159 private const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
160 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)avif|gif|jpg|jpeg|png|svg|webp)$/Sxu';
161
163 private const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
164
169 public const PTD_FOR_INCLUSION = Preprocessor::DOM_FOR_INCLUSION;
170
171 # Allowed values for $this->mOutputType
173 public const OT_HTML = 1;
175 public const OT_WIKI = 2;
177 public const OT_PREPROCESS = 3;
182 public const OT_PLAIN = 4;
183
201 public const MARKER_SUFFIX = "-QINU`\"'\x7f";
202 public const MARKER_PREFIX = "\x7f'\"`UNIQ-";
203 private const HEADLINE_MARKER_REGEX = '/^' . self::MARKER_PREFIX . '-h-(\d+)-' . self::MARKER_SUFFIX . '/';
204
219 public const TOC_PLACEHOLDER = '<meta property="mw:PageProp/toc" />';
220
221 # Persistent:
223 private array $mTagHooks = [];
225 private array $mFunctionHooks = [];
227 private array $mFunctionSynonyms = [ 0 => [], 1 => [] ];
229 private array $mStripList = [];
231 private array $mVarCache = [];
233 private array $mImageParams = [];
235 private array $mImageParamsMagicArray = [];
237 public $mMarkerIndex = 0;
238
239 // Initialised by initializeVariables()
241 private MagicWordArray $mVariables;
242 private MagicWordArray $mSubstWords;
243
244 // Initialised in constructor
246 private string $mExtLinkBracketedRegex;
247 private HookRunner $hookRunner;
248 private Preprocessor $mPreprocessor;
249
250 // Cleared with clearState():
252 private ParserOutput $mOutput;
253 private int $mAutonumber = 0;
254 private StripState $mStripState;
255 private LinkHolderArray $mLinkHolders;
256 private int $mLinkID = 0;
257 private array $mIncludeSizes;
268 private array $mTplRedirCache;
270 public array $mHeadings;
272 private array $mDoubleUnderscores;
278 private bool $mShowToc;
279 private bool $mForceTocPosition;
280 private array $mTplDomCache;
281 private ?UserIdentity $mUser;
282
283 # Temporary
284 # These are variables reset at least once per parse regardless of $clearState
285
290 private $mOptions;
291
292 # Deprecated "dynamic" properties
293 # These used to be dynamic properties added to the parser, but these
294 # have been deprecated since 1.42.
303
309 private Title $mTitle;
311 private int $mOutputType;
319 private bool $mStripExtTags = true;
324 private array $ot;
326 private ?int $mRevisionId = null;
328 private ?string $mRevisionTimestamp = null;
330 private ?string $mRevisionUser = null;
332 private ?int $mRevisionSize = null;
334 private $mInputSize = false;
335
336 private ?RevisionRecord $mRevisionRecordObject = null;
337
343 private ?MapCacheLRU $currentRevisionCache = null;
344
349 private $mInParse = false;
350
351 private SectionProfiler $mProfiler;
352 private ?LinkRenderer $mLinkRenderer = null;
353
357 public const CONSTRUCTOR_OPTIONS = [
358 // See documentation for the corresponding config options
359 // Many of these are only used in (eg) CoreMagicVariables
383 ];
384
389 public function __construct(
390 // This is called $svcOptions instead of $options like elsewhere to avoid confusion with
391 // $mOptions, which is public and widely used, and also with the local variable $options used
392 // for ParserOptions throughout this file.
393 private ServiceOptions $svcOptions,
394 private MagicWordFactory $magicWordFactory,
395 private Language $contLang,
396 private UrlUtils $urlUtils,
397 private SpecialPageFactory $specialPageFactory,
398 private LinkRendererFactory $linkRendererFactory,
399 private NamespaceInfo $nsInfo,
400 private LoggerInterface $logger,
401 private BadFileLookup $badFileLookup,
402 private LanguageConverterFactory $languageConverterFactory,
403 private LanguageNameUtils $languageNameUtils,
404 private HookContainer $hookContainer,
405 private TidyDriverBase $tidy,
406 private WANObjectCache $wanCache,
407 private UserOptionsLookup $userOptionsLookup,
408 private UserFactory $userFactory,
409 private TitleFormatter $titleFormatter,
410 private HttpRequestFactory $httpRequestFactory,
411 private TrackingCategories $trackingCategories,
412 private SignatureValidatorFactory $signatureValidatorFactory,
413 private UserNameUtils $userNameUtils,
414 ) {
415 $this->deprecateDynamicPropertiesAccess( '1.42', __CLASS__ );
416 $this->deprecatePublicProperty( 'ot', '1.35', __CLASS__ );
417 $this->deprecatePublicProperty( 'mTitle', '1.35', __CLASS__ );
418 $this->deprecatePublicProperty( 'mOptions', '1.35', __CLASS__ );
419
421 // Direct construction of Parser was deprecated in 1.34 and
422 // removed in 1.36; use a ParserFactory instead.
423 throw new BadMethodCallException( 'Direct construction of Parser not allowed' );
424 }
425 $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
426
427 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->urlUtils->validProtocols() . ')' .
428 self::EXT_LINK_ADDR .
429 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*)\]/Su';
430
431 $this->hookRunner = new HookRunner( $hookContainer );
432
433 $this->mPreprocessor = new Preprocessor_Hash(
434 $this,
435 $this->wanCache,
436 [
437 'cacheThreshold' => $svcOptions->get( MainConfigNames::PreprocessorCacheThreshold ),
438 'disableLangConversion' => $languageConverterFactory->isConversionDisabled(),
439 ]
440 );
441
442 // These steps used to be done in "::firstCallInit()"
443 // (if you're chasing a reference from some old code)
444 CoreParserFunctions::register(
445 $this,
446 new ServiceOptions( CoreParserFunctions::REGISTER_OPTIONS, $svcOptions )
447 );
449 $this,
451 );
452 $this->initializeVariables();
453
454 $this->hookRunner->onParserFirstCallInit( $this );
455 $this->mTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/Missing' );
456 }
457
461 public function __destruct() {
462 // @phan-suppress-next-line PhanRedundantCondition Typed property not set in constructor, may be uninitialized
463 if ( isset( $this->mLinkHolders ) ) {
464 // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
465 unset( $this->mLinkHolders );
466 }
467 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
468 foreach ( $this as $name => $value ) {
469 unset( $this->$name );
470 }
471 }
472
476 public function __clone() {
477 $this->mInParse = false;
478
479 $this->mPreprocessor = clone $this->mPreprocessor;
480 $this->mPreprocessor->resetParser( $this );
481
482 $this->hookRunner->onParserCloned( $this );
483 }
484
492 public function firstCallInit() {
493 /*
494 * This method should be hard-deprecated once remaining calls are
495 * removed; it no longer does anything.
496 */
497 }
498
504 public function clearState() {
505 $this->resetOutput();
506 $this->mAutonumber = 0;
507 $this->mLinkHolders = new LinkHolderArray(
508 $this,
509 $this->getContentLanguageConverter(),
510 $this->getHookContainer()
511 );
512 $this->mLinkID = 0;
513 $this->mRevisionTimestamp = null;
514 $this->mRevisionId = null;
515 $this->mRevisionUser = null;
516 $this->mRevisionSize = null;
517 $this->mRevisionRecordObject = null;
518 $this->mVarCache = [];
519 $this->mUser = null;
520 $this->currentRevisionCache = null;
521
522 $this->mStripState = new StripState( $this );
523
524 # Clear these on every parse, T6549
525 $this->mTplRedirCache = [];
526 $this->mTplDomCache = [];
527
528 $this->mShowToc = true;
529 $this->mForceTocPosition = false;
530 $this->mIncludeSizes = [
531 'post-expand' => 0,
532 'arg' => 0,
533 ];
534 $this->mPPNodeCount = 0;
535 $this->mHighestExpansionDepth = 0;
536 $this->mHeadings = [];
537 $this->mDoubleUnderscores = [];
538 $this->mExpensiveFunctionCount = 0;
539
540 $this->mProfiler = new SectionProfiler();
541
542 $this->hookRunner->onParserClearState( $this );
543 }
544
549 public function resetOutput() {
550 $this->mOutput = new ParserOutput;
551 $this->mOptions->registerWatcher( $this->mOutput->recordOption( ... ) );
552 }
553
561 public function getParseTime(): DateTime {
562 $ts = $this->mOptions->getTimestamp(); /* TS::MW */
563 $date = DateTime::createFromFormat(
564 'YmdHis', $ts, new DateTimeZone( 'UTC' )
565 );
566 if ( $this->hookContainer->isRegistered( 'ParserGetVariableValueTs' ) ) {
567 $s = $date->format( 'U' );
568 $this->hookRunner->onParserGetVariableValueTs( $this, $s );
569 $date = ( new MWTimestamp( $s ) )->timestamp;
570 }
571 return $date;
572 }
573
592 public function parse(
593 $text, PageReference $page, ParserOptions $options,
594 $linestart = true, $clearState = true, $revid = null
595 ) {
596 if ( $clearState ) {
597 // We use U+007F DELETE to construct strip markers, so we have to make
598 // sure that this character does not occur in the input text.
599 $text = strtr( $text, "\x7f", "?" );
600 $magicScopeVariable = $this->lock();
601 }
602 // Strip U+0000 NULL (T159174)
603 $text = str_replace( "\000", '', $text );
604
605 $this->startParse( $page, $options, self::OT_HTML, $clearState );
606
607 $this->currentRevisionCache = null;
608 $this->mInputSize = strlen( $text );
609 $this->mOutput->resetParseStartTime();
610
611 $oldRevisionId = $this->mRevisionId;
612 $oldRevisionRecordObject = $this->mRevisionRecordObject;
613 $oldRevisionTimestamp = $this->mRevisionTimestamp;
614 $oldRevisionUser = $this->mRevisionUser;
615 $oldRevisionSize = $this->mRevisionSize;
616 if ( $revid !== null ) {
617 $this->mRevisionId = $revid;
618 $this->mRevisionRecordObject = null;
619 $this->mRevisionTimestamp = null;
620 $this->mRevisionUser = null;
621 $this->mRevisionSize = null;
622 }
623
624 $text = $this->internalParse( $text );
625 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
626
627 $text = $this->internalParseHalfParsed( $text, true, $linestart );
628
636 if ( !$options->getDisableTitleConversion()
637 && !isset( $this->mDoubleUnderscores['nocontentconvert'] )
638 && !isset( $this->mDoubleUnderscores['notitleconvert'] )
639 && $this->mOutput->getDisplayTitle() === false
640 ) {
641 $titleText = $this->getTargetLanguageConverter()->getConvRuleTitle();
642 if ( $titleText !== false ) {
643 $titleText = Sanitizer::removeSomeTags( $titleText );
644 } else {
645 [ $nsText, $nsSeparator, $mainText ] = $this->getTargetLanguageConverter()->convertSplitTitle( $page );
646 // In the future, those three pieces could be stored separately rather than joined into $titleText,
647 // and OutputPage would format them and join them together, to resolve T314399.
648 $titleText = self::formatPageTitle( $nsText, $nsSeparator, $mainText );
649 }
650 $this->mOutput->setTitleText( $titleText );
651 }
652
653 # Recording timing info. Must be called before finalizeAdaptiveCacheExpiry() and
654 # makeLimitReport(), which make use of the timing info.
655 $this->mOutput->recordTimeProfile();
656
657 # Compute runtime adaptive expiry if set
658 $this->mOutput->finalizeAdaptiveCacheExpiry();
659
660 # Warn if too many heavyweight parser functions were used
661 if ( $this->mExpensiveFunctionCount > $options->getExpensiveParserFunctionLimit() ) {
662 $this->limitationWarn( 'expensive-parserfunction',
663 $this->mExpensiveFunctionCount,
665 );
666 }
667
668 # Information on limits, for the benefit of users who try to skirt them
669 $this->makeLimitReport( $this->mOptions, $this->mOutput );
670
671 $this->mOutput->setFromParserOptions( $options );
672
673 $this->mOutput->setRawText( $text );
674
675 $this->mRevisionId = $oldRevisionId;
676 $this->mRevisionRecordObject = $oldRevisionRecordObject;
677 $this->mRevisionTimestamp = $oldRevisionTimestamp;
678 $this->mRevisionUser = $oldRevisionUser;
679 $this->mRevisionSize = $oldRevisionSize;
680 $this->mInputSize = false;
681 $this->currentRevisionCache = null;
682
683 return $this->mOutput;
684 }
685
690 public function makeLimitReport(
691 ParserOptions $parserOptions, ParserOutput $parserOutput
692 ) {
693 if ( !$this->svcOptions->get( MainConfigNames::EnableParserLimitReporting ) ) {
694 return;
695 }
696 if ( $parserOptions->isMessage() ) {
697 // No need to include limit report information in
698 // user interface messages.
699 return;
700 }
701
702 $maxIncludeSize = $parserOptions->getMaxIncludeSize();
703
704 $cpuTime = $parserOutput->getTimeProfile( 'cpu' );
705 if ( $cpuTime !== null ) {
706 $parserOutput->setLimitReportData( 'limitreport-cputime',
707 sprintf( "%.3f", $cpuTime )
708 );
709 }
710
711 $wallTime = $parserOutput->getTimeProfile( 'wall' );
712 $parserOutput->setLimitReportData( 'limitreport-walltime',
713 sprintf( "%.3f", $wallTime )
714 );
715
716 $parserOutput->setLimitReportData( 'limitreport-ppvisitednodes',
717 [ $this->mPPNodeCount, $parserOptions->getMaxPPNodeCount() ]
718 );
719 $revisionSize = $this->mInputSize !== false ? $this->mInputSize :
720 $this->getRevisionSize();
721 $parserOutput->setLimitReportData( 'limitreport-revisionsize',
722 [ $revisionSize ?? -1, $this->svcOptions->get( MainConfigNames::MaxArticleSize ) * 1024 ]
723 );
724 $parserOutput->setLimitReportData( 'limitreport-postexpandincludesize',
725 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
726 );
727 $parserOutput->setLimitReportData( 'limitreport-templateargumentsize',
728 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
729 );
730 $parserOutput->setLimitReportData( 'limitreport-expansiondepth',
731 [ $this->mHighestExpansionDepth, $parserOptions->getMaxPPExpandDepth() ]
732 );
733 $parserOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
734 [ $this->mExpensiveFunctionCount, $parserOptions->getExpensiveParserFunctionLimit() ]
735 );
736
737 foreach ( $this->mStripState->getLimitReport() as [ $key, $value ] ) {
738 $parserOutput->setLimitReportData( $key, $value );
739 }
740
741 $this->hookRunner->onParserLimitReportPrepare( $this, $parserOutput );
742
743 // Add on template profiling data in human/machine readable way
744 $dataByFunc = $this->mProfiler->getFunctionStats();
745 uasort( $dataByFunc, static function ( $a, $b ) {
746 return $b['real'] <=> $a['real']; // descending order
747 } );
748 $profileReport = [];
749 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
750 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
751 $item['%real'], $item['real'], $item['calls'],
752 htmlspecialchars( $item['name'] ) );
753 }
754
755 $parserOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
756
757 // Add other cache related metadata
758 if ( $this->svcOptions->get( MainConfigNames::ShowHostnames ) ) {
759 $parserOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
760 }
761 $parserOutput->setLimitReportData( 'cachereport-timestamp',
762 $parserOutput->getCacheTime() );
763 $parserOutput->setLimitReportData( 'cachereport-ttl',
764 $parserOutput->getCacheExpiry() );
765 $parserOutput->setLimitReportData( 'cachereport-transientcontent',
766 $parserOutput->hasReducedExpiry() );
767 }
768
794 public function recursiveTagParse( $text, $frame = false ) {
795 $text = $this->internalParse( $text, false, $frame );
796 return $text;
797 }
798
818 public function recursiveTagParseFully( $text, $frame = false ) {
819 $text = $this->recursiveTagParse( $text, $frame );
820 $text = $this->internalParseHalfParsed( $text, false );
821 return $text;
822 }
823
843 public function parseExtensionTagAsTopLevelDoc( string $text ): string {
844 $text = $this->recursiveTagParse( $text );
845 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
846 $text = $this->internalParseHalfParsed( $text, true );
847 return $text;
848 }
849
862 public function preprocess(
863 $text,
864 ?PageReference $page,
865 ParserOptions $options,
866 $revid = null,
867 $frame = false
868 ) {
869 $magicScopeVariable = $this->lock();
870 $this->startParse( $page, $options, self::OT_PREPROCESS, true );
871 if ( $revid !== null ) {
872 $this->mRevisionId = $revid;
873 }
874 $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
875 $text = $this->replaceVariables( $text, $frame );
876 $text = $this->mStripState->unstripBoth( $text );
877 return $text;
878 }
879
889 public function recursivePreprocess( $text, $frame = false ) {
890 $text = $this->replaceVariables( $text, $frame );
891 $text = $this->mStripState->unstripBoth( $text );
892 return $text;
893 }
894
909 public function getPreloadText( $text, PageReference $page, ParserOptions $options, $params = [] ) {
910 $msg = new RawMessage( $text );
911 $text = $msg->params( $params )->plain();
912
913 # Parser (re)initialisation
914 $magicScopeVariable = $this->lock();
915 $this->startParse( $page, $options, self::OT_PLAIN, true );
916
917 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
918 $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
919 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
920 $text = $this->mStripState->unstripBoth( $text );
921 return $text;
922 }
923
931 public function setUser( ?UserIdentity $user ) {
932 $this->mUser = $user;
933 }
934
942 public function setTitle( ?Title $t = null ) {
943 $this->setPage( $t );
944 }
945
951 public function getTitle(): Title {
952 return $this->mTitle;
953 }
954
961 public function setPage( ?PageReference $t = null ) {
962 if ( !$t ) {
963 $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
964 } else {
965 // For now (early 1.37 alpha), always convert to Title, so we don't have to do it over
966 // and over again in other methods. Eventually, we will no longer need to have a Title
967 // instance internally.
968 $t = Title::newFromPageReference( $t );
969 }
970
971 if ( $t->hasFragment() ) {
972 # Strip the fragment to avoid various odd effects
973 $this->mTitle = $t->createFragmentTarget( '' );
974 } else {
975 $this->mTitle = $t;
976 }
977 }
978
984 public function getPage(): ?PageReference {
985 if ( $this->mTitle->isSpecial( 'Badtitle' ) ) {
986 [ , $subPage ] = $this->specialPageFactory->resolveAlias( $this->mTitle->getDBkey() );
987
988 if ( $subPage === 'Missing' ) {
989 wfDeprecated( __METHOD__ . ' without a Title set', '1.34' );
990 return null;
991 }
992 }
993
994 return $this->mTitle;
995 }
996
1002 public function getOutputType(): int {
1003 return $this->mOutputType;
1004 }
1005
1011 public function setOutputType( $ot ): void {
1012 $this->mOutputType = $ot;
1013 # Shortcut alias
1014 $this->ot = [
1015 'html' => $ot == self::OT_HTML,
1016 'wiki' => $ot == self::OT_WIKI,
1017 'pre' => $ot == self::OT_PREPROCESS,
1018 'plain' => $ot == self::OT_PLAIN,
1019 ];
1020 }
1021
1026 public function getOutput() {
1027 // @phan-suppress-next-line PhanRedundantCondition False positive, see https://github.com/phan/phan/issues/4720
1028 if ( !isset( $this->mOutput ) ) {
1029 wfDeprecated( __METHOD__ . ' before initialization', '1.42' );
1030 // @phan-suppress-next-line PhanTypeMismatchReturnProbablyReal We don’t want to tell anyone we’re doing this
1031 return null;
1032 }
1033 return $this->mOutput;
1034 }
1035
1040 public function getOptions() {
1041 return $this->mOptions;
1042 }
1043
1049 public function setOptions( ParserOptions $options ): void {
1050 $this->mOptions = $options;
1051 }
1052
1057 public function nextLinkID() {
1058 return $this->mLinkID++;
1059 }
1060
1065 public function setLinkID( $id ) {
1066 $this->mLinkID = $id;
1067 }
1068
1077 public function getTargetLanguage() {
1078 $target = $this->mOptions->getTargetLanguage();
1079
1080 if ( $target !== null ) {
1081 return $target;
1082 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1083 return $this->mOptions->getUserLangObj();
1084 }
1085
1086 return $this->getTitle()->getPageLanguage();
1087 }
1088
1096 public function getUserIdentity(): UserIdentity {
1097 return $this->mUser ?? $this->getOptions()->getUserIdentity();
1098 }
1099
1106 public function getPreprocessor() {
1107 return $this->mPreprocessor;
1108 }
1109
1116 public function getLinkRenderer() {
1117 // XXX We make the LinkRenderer with current options and then cache it forever
1118 if ( !$this->mLinkRenderer ) {
1119 $this->mLinkRenderer = $this->linkRendererFactory->create();
1120 }
1121
1122 return $this->mLinkRenderer;
1123 }
1124
1131 public function getMagicWordFactory() {
1132 return $this->magicWordFactory;
1133 }
1134
1141 public function getContentLanguage() {
1142 return $this->contLang;
1143 }
1144
1151 public function getBadFileLookup() {
1152 return $this->badFileLookup;
1153 }
1154
1174 public static function extractTagsAndParams( array $elements, $text, &$matches ) {
1175 static $n = 1;
1176 $stripped = '';
1177 $matches = [];
1178
1179 $taglist = implode( '|', $elements );
1180 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1181
1182 while ( $text != '' ) {
1183 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1184 $stripped .= $p[0];
1185 if ( count( $p ) < 5 ) {
1186 break;
1187 }
1188 if ( count( $p ) > 5 ) {
1189 # comment
1190 $element = $p[4];
1191 $attributes = '';
1192 $close = '';
1193 $inside = $p[5];
1194 } else {
1195 # tag
1196 [ , $element, $attributes, $close, $inside ] = $p;
1197 }
1198
1199 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1200 $stripped .= $marker;
1201
1202 if ( $close === '/>' ) {
1203 # Empty element tag, <tag />
1204 $content = null;
1205 $text = $inside;
1206 $tail = null;
1207 } else {
1208 if ( $element === '!--' ) {
1209 $end = '/(-->)/';
1210 } else {
1211 $end = "/(<\\/$element\\s*>)/i";
1212 }
1213 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1214 $content = $q[0];
1215 if ( count( $q ) < 3 ) {
1216 # No end tag -- let it run out to the end of the text.
1217 $tail = '';
1218 $text = '';
1219 } else {
1220 [ , $tail, $text ] = $q;
1221 }
1222 }
1223
1224 $matches[$marker] = [ $element,
1225 $content,
1226 Sanitizer::decodeTagAttributes( $attributes ),
1227 "<$element$attributes$close$content$tail" ];
1228 }
1229 return $stripped;
1230 }
1231
1237 public function getStripList() {
1238 return $this->mStripList;
1239 }
1240
1245 public function getStripState() {
1246 return $this->mStripState;
1247 }
1248
1258 public function insertStripItem( $text ) {
1259 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1260 $this->mMarkerIndex++;
1261 $this->mStripState->addGeneral( $marker, $text );
1262 return $marker;
1263 }
1264
1271 private function handleTables( $text ) {
1272 $lines = StringUtils::explode( "\n", $text );
1273 $out = '';
1274 $td_history = []; # Is currently a td tag open?
1275 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1276 $tr_history = []; # Is currently a tr tag open?
1277 $tr_attributes = []; # history of tr attributes
1278 $has_opened_tr = []; # Did this table open a <tr> element?
1279 $indent_level = 0; # indent level of the table
1280
1281 foreach ( $lines as $outLine ) {
1282 $line = trim( $outLine );
1283
1284 if ( $line === '' ) { # empty line, go to next line
1285 $out .= $outLine . "\n";
1286 continue;
1287 }
1288
1289 $first_character = $line[0];
1290 $first_two = substr( $line, 0, 2 );
1291 $matches = [];
1292
1293 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1294 # First check if we are starting a new table
1295 $indent_level = strlen( $matches[1] );
1296
1297 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1298 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1299
1300 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1301 $td_history[] = false;
1302 $last_tag_history[] = '';
1303 $tr_history[] = false;
1304 $tr_attributes[] = '';
1305 $has_opened_tr[] = false;
1306 } elseif ( count( $td_history ) == 0 ) {
1307 # Don't do any of the following
1308 $out .= $outLine . "\n";
1309 continue;
1310 } elseif ( $first_two === '|}' ) {
1311 # We are ending a table
1312 $line = '</table>' . substr( $line, 2 );
1313 $last_tag = array_pop( $last_tag_history );
1314
1315 if ( !array_pop( $has_opened_tr ) ) {
1316 $line = "<tr><td></td></tr>{$line}";
1317 }
1318
1319 if ( array_pop( $tr_history ) ) {
1320 $line = "</tr>{$line}";
1321 }
1322
1323 if ( array_pop( $td_history ) ) {
1324 $line = "</{$last_tag}>{$line}";
1325 }
1326 array_pop( $tr_attributes );
1327 if ( $indent_level > 0 ) {
1328 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1329 } else {
1330 $outLine = $line;
1331 }
1332 } elseif ( $first_two === '|-' ) {
1333 # Now we have a table row
1334 $line = preg_replace( '#^\|-+#', '', $line );
1335
1336 # Whats after the tag is now only attributes
1337 $attributes = $this->mStripState->unstripBoth( $line );
1338 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1339 array_pop( $tr_attributes );
1340 $tr_attributes[] = $attributes;
1341
1342 $line = '';
1343 $last_tag = array_pop( $last_tag_history );
1344 array_pop( $has_opened_tr );
1345 $has_opened_tr[] = true;
1346
1347 if ( array_pop( $tr_history ) ) {
1348 $line = '</tr>';
1349 }
1350
1351 if ( array_pop( $td_history ) ) {
1352 $line = "</{$last_tag}>{$line}";
1353 }
1354
1355 $outLine = $line;
1356 $tr_history[] = false;
1357 $td_history[] = false;
1358 $last_tag_history[] = '';
1359 } elseif ( $first_character === '|'
1360 || $first_character === '!'
1361 || $first_two === '|+'
1362 ) {
1363 # This might be cell elements, td, th or captions
1364 if ( $first_two === '|+' ) {
1365 $first_character = '+';
1366 $line = substr( $line, 2 );
1367 } else {
1368 $line = substr( $line, 1 );
1369 }
1370
1371 // Implies both are valid for table headings.
1372 if ( $first_character === '!' ) {
1373 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1374 }
1375
1376 # Split up multiple cells on the same line.
1377 # FIXME : This can result in improper nesting of tags processed
1378 # by earlier parser steps.
1379 $cells = explode( '||', $line );
1380
1381 $outLine = '';
1382
1383 # Loop through each table cell
1384 foreach ( $cells as $cell ) {
1385 $previous = '';
1386 if ( $first_character !== '+' ) {
1387 $tr_after = array_pop( $tr_attributes );
1388 if ( !array_pop( $tr_history ) ) {
1389 $previous = "<tr{$tr_after}>\n";
1390 }
1391 $tr_history[] = true;
1392 $tr_attributes[] = '';
1393 array_pop( $has_opened_tr );
1394 $has_opened_tr[] = true;
1395 }
1396
1397 $last_tag = array_pop( $last_tag_history );
1398
1399 if ( array_pop( $td_history ) ) {
1400 $previous = "</{$last_tag}>\n{$previous}";
1401 }
1402
1403 if ( $first_character === '|' ) {
1404 $last_tag = 'td';
1405 } elseif ( $first_character === '!' ) {
1406 $last_tag = 'th';
1407 } elseif ( $first_character === '+' ) {
1408 $last_tag = 'caption';
1409 } else {
1410 $last_tag = '';
1411 }
1412
1413 $last_tag_history[] = $last_tag;
1414
1415 # A cell could contain both parameters and data
1416 $cell_data = explode( '|', $cell, 2 );
1417
1418 # T2553: Note that a '|' inside an invalid link should not
1419 # be mistaken as delimiting cell parameters
1420 # Bug T153140: Neither should language converter markup.
1421 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1422 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1423 } elseif ( count( $cell_data ) == 1 ) {
1424 // Whitespace in cells is trimmed
1425 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1426 } else {
1427 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1428 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1429 // Whitespace in cells is trimmed
1430 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1431 }
1432
1433 $outLine .= $cell;
1434 $td_history[] = true;
1435 }
1436 }
1437 $out .= $outLine . "\n";
1438 }
1439
1440 # Closing open td, tr && table
1441 while ( count( $td_history ) > 0 ) {
1442 if ( array_pop( $td_history ) ) {
1443 $out .= "</td>\n";
1444 }
1445 if ( array_pop( $tr_history ) ) {
1446 $out .= "</tr>\n";
1447 }
1448 if ( !array_pop( $has_opened_tr ) ) {
1449 $out .= "<tr><td></td></tr>\n";
1450 }
1451
1452 $out .= "</table>\n";
1453 }
1454
1455 # Remove trailing line-ending (b/c)
1456 if ( substr( $out, -1 ) === "\n" ) {
1457 $out = substr( $out, 0, -1 );
1458 }
1459
1460 # special case: don't return empty table
1461 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1462 $out = '';
1463 }
1464
1465 return $out;
1466 }
1467
1481 public function internalParse( $text, $isMain = true, $frame = false ) {
1482 $origText = $text;
1483
1484 # Hook to suspend the parser in this state
1485 if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1486 return $text;
1487 }
1488
1489 # if $frame is provided, then use $frame for replacing any variables
1490 if ( $frame ) {
1491 # use frame depth to infer how include/noinclude tags should be handled
1492 # depth=0 means this is the top-level document; otherwise it's an included document
1493 if ( !$frame->depth ) {
1494 $flag = 0;
1495 } else {
1496 $flag = Preprocessor::DOM_FOR_INCLUSION;
1497 }
1498 $dom = $this->preprocessToDom( $text, $flag );
1499 $text = $frame->expand( $dom );
1500 } else {
1501 # if $frame is not provided, then use old-style replaceVariables
1502 $text = $this->replaceVariables( $text );
1503 }
1504
1505 $text = Sanitizer::internalRemoveHtmlTags(
1506 $text,
1507 // Callback from the Sanitizer for expanding items found in
1508 // HTML attribute values, so they can be safely tested and escaped.
1509 function ( &$text, $frame = false ) {
1510 $text = $this->replaceVariables( $text, $frame );
1511 $text = $this->mStripState->unstripBoth( $text );
1512 },
1513 false,
1514 [],
1515 []
1516 );
1517 $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1518
1519 # Tables need to come after variable replacement for things to work
1520 # properly; putting them before other transformations should keep
1521 # exciting things like link expansions from showing up in surprising
1522 # places.
1523 $text = $this->handleTables( $text );
1524
1525 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1526
1527 $text = $this->handleDoubleUnderscore( $text );
1528
1529 $text = $this->handleHeadings( $text );
1530 $text = $this->handleInternalLinks( $text );
1531 $text = $this->handleAllQuotes( $text );
1532 $text = $this->handleExternalLinks( $text );
1533
1534 # handleInternalLinks may sometimes leave behind
1535 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1536 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1537
1538 $text = $this->handleMagicLinks( $text );
1539 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1540
1541 return $text;
1542 }
1543
1551 return $this->languageConverterFactory->getLanguageConverter(
1552 $this->getTargetLanguage()
1553 );
1554 }
1555
1559 private function getContentLanguageConverter(): ILanguageConverter {
1560 return $this->languageConverterFactory->getLanguageConverter(
1561 $this->getContentLanguage()
1562 );
1563 }
1564
1572 protected function getHookContainer() {
1573 return $this->hookContainer;
1574 }
1575
1584 protected function getHookRunner() {
1585 return $this->hookRunner;
1586 }
1587
1597 private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1598 $text = $this->mStripState->unstripGeneral( $text );
1599
1600 $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1601
1602 $this->replaceLinkHoldersPrivate( $text );
1603
1611 $converter = null;
1612 if ( !( $this->mOptions->getDisableContentConversion()
1613 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
1614 || $this->mOptions->getInterfaceMessage() )
1615 ) {
1616 # The position of the convert() call should not be changed. it
1617 # assumes that the links are all replaced and the only thing left
1618 # is the <nowiki> mark.
1619 $converter = $this->getTargetLanguageConverter();
1620 $text = $converter->convert( $text );
1621 // TOC will be converted below.
1622 }
1623 // Convert the TOC. This is done *after* the main text
1624 // so that all the editor-defined conversion rules (by convention
1625 // defined at the start of the article) are applied to the TOC
1626 self::localizeTOC(
1627 $this->mOutput->getTOCData(),
1628 $this->getTargetLanguage(),
1629 $converter // null if conversion is to be suppressed.
1630 );
1631 if ( $converter ) {
1632 $this->mOutput->setLanguage( new Bcp47CodeValue(
1633 LanguageCode::bcp47( $converter->getPreferredVariant() )
1634 ) );
1635 } else {
1636 $this->mOutput->setLanguage( $this->getTargetLanguage() );
1637 }
1638
1639 $text = $this->mStripState->unstripNoWiki( $text );
1640
1641 $text = $this->mStripState->unstripGeneral( $text );
1642
1643 $text = $this->tidy->tidy( $text, Sanitizer::armorFrenchSpaces( ... ) );
1644
1645 if ( $isMain ) {
1646 $title = $this->getPage();
1647 if ( $title ) {
1648 $this->mOutput->setTitle( $title );
1649 }
1650 $this->hookRunner->onParserAfterTidy( $this, $text );
1651 }
1652
1653 return $text;
1654 }
1655
1666 private function handleMagicLinks( $text ) {
1667 $prots = $this->urlUtils->validAbsoluteProtocols();
1668 $urlChar = self::EXT_LINK_URL_CLASS;
1669 $addr = self::EXT_LINK_ADDR;
1670 $space = self::SPACE_NOT_NL; # non-newline space
1671 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1672 $spaces = "$space++"; # possessive match of 1 or more spaces
1673 $text = preg_replace_callback(
1674 '!(?: # Start cases
1675 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1676 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1677 (\b # m[3]: Free external links
1678 (?i:$prots)
1679 ($addr$urlChar*) # m[4]: Post-protocol path
1680 ) |
1681 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1682 ([0-9]+)\b |
1683 \bISBN $spaces ( # m[6]: ISBN, capture number
1684 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1685 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1686 [0-9Xx] # check digit
1687 )\b
1688 )!xu",
1689 $this->magicLinkCallback( ... ),
1690 $text
1691 );
1692 return $text;
1693 }
1694
1699 private function magicLinkCallback( array $m ) {
1700 if ( isset( $m[1] ) && $m[1] !== '' ) {
1701 # Skip anchor
1702 return $m[0];
1703 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1704 # Skip HTML element
1705 return $m[0];
1706 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1707 # Free external link
1708 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1709 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1710 # RFC or PMID
1711 if ( str_starts_with( $m[0], 'RFC' ) ) {
1712 if ( !$this->mOptions->getMagicRFCLinks() ) {
1713 return $m[0];
1714 }
1715 $keyword = 'RFC';
1716 $urlmsg = 'rfcurl';
1717 $cssClass = 'mw-magiclink-rfc';
1718 $trackingCat = 'magiclink-tracking-rfc';
1719 $id = $m[5];
1720 } elseif ( str_starts_with( $m[0], 'PMID' ) ) {
1721 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1722 return $m[0];
1723 }
1724 $keyword = 'PMID';
1725 $urlmsg = 'pubmedurl';
1726 $cssClass = 'mw-magiclink-pmid';
1727 $trackingCat = 'magiclink-tracking-pmid';
1728 $id = $m[5];
1729 } else {
1730 // Should never happen
1731 throw new UnexpectedValueException( __METHOD__ . ': unrecognised match type "' .
1732 substr( $m[0], 0, 20 ) . '"' );
1733 }
1734 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1735 $this->addTrackingCategory( $trackingCat );
1736 return $this->getLinkRenderer()->makeExternalLink(
1737 $url,
1738 "{$keyword} {$id}",
1739 $this->getTitle(),
1740 $cssClass,
1741 []
1742 );
1743 } elseif ( isset( $m[6] ) && $m[6] !== ''
1744 && $this->mOptions->getMagicISBNLinks()
1745 ) {
1746 # ISBN
1747 $isbn = $m[6];
1748 $space = self::SPACE_NOT_NL; # non-newline space
1749 $isbn = preg_replace( "/$space/", ' ', $isbn );
1750 $num = strtr( $isbn, [
1751 '-' => '',
1752 ' ' => '',
1753 'x' => 'X',
1754 ] );
1755 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1756 return $this->getLinkRenderer()->makeKnownLink(
1757 SpecialPage::getTitleFor( 'Booksources', $num ),
1758 "ISBN $isbn",
1759 [
1760 'class' => 'internal mw-magiclink-isbn',
1761 'title' => false // suppress title attribute
1762 ]
1763 );
1764 } else {
1765 return $m[0];
1766 }
1767 }
1768
1778 private function makeFreeExternalLink( $url, $numPostProto ) {
1779 $trail = '';
1780
1781 # The characters '<' and '>' (which were escaped by
1782 # internalRemoveHtmlTags()) should not be included in
1783 # URLs, per RFC 2396.
1784 # Make &nbsp; terminate a URL as well (bug T84937)
1785 $m2 = [];
1786 if ( preg_match(
1787 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1788 $url,
1789 $m2,
1790 PREG_OFFSET_CAPTURE
1791 ) ) {
1792 $trail = substr( $url, $m2[0][1] ) . $trail;
1793 $url = substr( $url, 0, $m2[0][1] );
1794 }
1795
1796 # Move trailing punctuation to $trail
1797 $sep = ',;\.:!?';
1798 # If there is no left bracket, then consider right brackets fair game too
1799 if ( !str_contains( $url, '(' ) ) {
1800 $sep .= ')';
1801 }
1802
1803 $urlRev = strrev( $url );
1804 $numSepChars = strspn( $urlRev, $sep );
1805 # Don't break a trailing HTML entity by moving the ; into $trail
1806 # This is in hot code, so use substr_compare to avoid having to
1807 # create a new string object for the comparison
1808 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1809 # more optimization: instead of running preg_match with a $
1810 # anchor, which can be slow, do the match on the reversed
1811 # string starting at the desired offset.
1812 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1813 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1814 $numSepChars--;
1815 }
1816 }
1817 if ( $numSepChars ) {
1818 $trail = substr( $url, -$numSepChars ) . $trail;
1819 $url = substr( $url, 0, -$numSepChars );
1820 }
1821
1822 # Verify that we still have a real URL after trail removal, and
1823 # not just lone protocol
1824 if ( strlen( $trail ) >= $numPostProto ) {
1825 return $url . $trail;
1826 }
1827
1828 $url = Sanitizer::cleanUrl( $url );
1829
1830 # Is this an external image?
1831 $text = $this->maybeMakeExternalImage( $url );
1832 if ( $text === false ) {
1833 # Not an image, make a link
1834 $text = $this->getLinkRenderer()->makeExternalLink(
1835 $url,
1836 $this->getTargetLanguageConverter()->markNoConversion( $url ),
1837 $this->getTitle(),
1838 'free',
1839 $this->getExternalLinkAttribs( $url )
1840 );
1841 # Register it in the output object...
1842 $this->mOutput->addExternalLink( $url );
1843 }
1844 return $text . $trail;
1845 }
1846
1853 private function handleHeadings( $text ) {
1854 for ( $i = 6; $i >= 1; --$i ) {
1855 $h = str_repeat( '=', $i );
1856 // Trim non-newline whitespace from headings
1857 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1858 $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1859 }
1860 return $text;
1861 }
1862
1870 private function handleAllQuotes( $text ) {
1871 $outtext = '';
1872 $lines = StringUtils::explode( "\n", $text );
1873 foreach ( $lines as $line ) {
1874 $outtext .= $this->doQuotes( $line ) . "\n";
1875 }
1876 $outtext = substr( $outtext, 0, -1 );
1877 return $outtext;
1878 }
1879
1888 public function doQuotes( $text ) {
1889 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1890 $countarr = count( $arr );
1891 if ( $countarr == 1 ) {
1892 return $text;
1893 }
1894
1895 // First, do some preliminary work. This may shift some apostrophes from
1896 // being mark-up to being text. It also counts the number of occurrences
1897 // of bold and italics mark-ups.
1898 $numbold = 0;
1899 $numitalics = 0;
1900 for ( $i = 1; $i < $countarr; $i += 2 ) {
1901 $thislen = strlen( $arr[$i] );
1902 // If there are ever four apostrophes, assume the first is supposed to
1903 // be text, and the remaining three constitute mark-up for bold text.
1904 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1905 if ( $thislen == 4 ) {
1906 $arr[$i - 1] .= "'";
1907 $arr[$i] = "'''";
1908 $thislen = 3;
1909 } elseif ( $thislen > 5 ) {
1910 // If there are more than 5 apostrophes in a row, assume they're all
1911 // text except for the last 5.
1912 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1913 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1914 $arr[$i] = "'''''";
1915 $thislen = 5;
1916 }
1917 // Count the number of occurrences of bold and italics mark-ups.
1918 if ( $thislen == 2 ) {
1919 $numitalics++;
1920 } elseif ( $thislen == 3 ) {
1921 $numbold++;
1922 } elseif ( $thislen == 5 ) {
1923 $numitalics++;
1924 $numbold++;
1925 }
1926 }
1927
1928 // If there is an odd number of both bold and italics, it is likely
1929 // that one of the bold ones was meant to be an apostrophe followed
1930 // by italics. Which one we cannot know for certain, but it is more
1931 // likely to be one that has a single-letter word before it.
1932 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1933 $firstsingleletterword = -1;
1934 $firstmultiletterword = -1;
1935 $firstspace = -1;
1936 for ( $i = 1; $i < $countarr; $i += 2 ) {
1937 if ( strlen( $arr[$i] ) == 3 ) {
1938 $x1 = substr( $arr[$i - 1], -1 );
1939 $x2 = substr( $arr[$i - 1], -2, 1 );
1940 if ( $x1 === ' ' ) {
1941 if ( $firstspace == -1 ) {
1942 $firstspace = $i;
1943 }
1944 } elseif ( $x2 === ' ' ) {
1945 $firstsingleletterword = $i;
1946 // if $firstsingleletterword is set, we don't
1947 // look at the other options, so we can bail early.
1948 break;
1949 } elseif ( $firstmultiletterword == -1 ) {
1950 $firstmultiletterword = $i;
1951 }
1952 }
1953 }
1954
1955 // If there is a single-letter word, use it!
1956 if ( $firstsingleletterword > -1 ) {
1957 $arr[$firstsingleletterword] = "''";
1958 $arr[$firstsingleletterword - 1] .= "'";
1959 } elseif ( $firstmultiletterword > -1 ) {
1960 // If not, but there's a multi-letter word, use that one.
1961 $arr[$firstmultiletterword] = "''";
1962 $arr[$firstmultiletterword - 1] .= "'";
1963 } elseif ( $firstspace > -1 ) {
1964 // ... otherwise use the first one that has neither.
1965 // (notice that it is possible for all three to be -1 if, for example,
1966 // there is only one pentuple-apostrophe in the line)
1967 $arr[$firstspace] = "''";
1968 $arr[$firstspace - 1] .= "'";
1969 }
1970 }
1971
1972 // Now let's actually convert our apostrophic mush to HTML!
1973 $output = '';
1974 $buffer = '';
1975 $state = '';
1976 $i = 0;
1977 foreach ( $arr as $r ) {
1978 if ( ( $i % 2 ) == 0 ) {
1979 if ( $state === 'both' ) {
1980 $buffer .= $r;
1981 } else {
1982 $output .= $r;
1983 }
1984 } else {
1985 $thislen = strlen( $r );
1986 if ( $thislen == 2 ) {
1987 // two quotes - open or close italics
1988 if ( $state === 'i' ) {
1989 $output .= '</i>';
1990 $state = '';
1991 } elseif ( $state === 'bi' ) {
1992 $output .= '</i>';
1993 $state = 'b';
1994 } elseif ( $state === 'ib' ) {
1995 $output .= '</b></i><b>';
1996 $state = 'b';
1997 } elseif ( $state === 'both' ) {
1998 $output .= '<b><i>' . $buffer . '</i>';
1999 $state = 'b';
2000 } else { // $state can be 'b' or ''
2001 $output .= '<i>';
2002 $state .= 'i';
2003 }
2004 } elseif ( $thislen == 3 ) {
2005 // three quotes - open or close bold
2006 if ( $state === 'b' ) {
2007 $output .= '</b>';
2008 $state = '';
2009 } elseif ( $state === 'bi' ) {
2010 $output .= '</i></b><i>';
2011 $state = 'i';
2012 } elseif ( $state === 'ib' ) {
2013 $output .= '</b>';
2014 $state = 'i';
2015 } elseif ( $state === 'both' ) {
2016 $output .= '<i><b>' . $buffer . '</b>';
2017 $state = 'i';
2018 } else { // $state can be 'i' or ''
2019 $output .= '<b>';
2020 $state .= 'b';
2021 }
2022 } elseif ( $thislen == 5 ) {
2023 // five quotes - open or close both separately
2024 if ( $state === 'b' ) {
2025 $output .= '</b><i>';
2026 $state = 'i';
2027 } elseif ( $state === 'i' ) {
2028 $output .= '</i><b>';
2029 $state = 'b';
2030 } elseif ( $state === 'bi' ) {
2031 $output .= '</i></b>';
2032 $state = '';
2033 } elseif ( $state === 'ib' ) {
2034 $output .= '</b></i>';
2035 $state = '';
2036 } elseif ( $state === 'both' ) {
2037 $output .= '<i><b>' . $buffer . '</b></i>';
2038 $state = '';
2039 } else { // ($state == '')
2040 $buffer = '';
2041 $state = 'both';
2042 }
2043 }
2044 }
2045 $i++;
2046 }
2047 // Now close all remaining tags. Notice that the order is important.
2048 if ( $state === 'b' || $state === 'ib' ) {
2049 $output .= '</b>';
2050 }
2051 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2052 $output .= '</i>';
2053 }
2054 if ( $state === 'bi' ) {
2055 $output .= '</b>';
2056 }
2057 // There might be lonely ''''', so make sure we have a buffer
2058 if ( $state === 'both' && $buffer ) {
2059 $output .= '<b><i>' . $buffer . '</i></b>';
2060 }
2061 return $output;
2062 }
2063
2073 private function handleExternalLinks( $text ) {
2074 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2075 if ( $bits === false ) {
2076 // T321234: Don't try to fix old revisions with broken UTF-8, just return $text as is
2077 return $text;
2078 }
2079 $s = array_shift( $bits );
2080
2081 $i = 0;
2082 while ( $i < count( $bits ) ) {
2083 $url = $bits[$i++];
2084 $i++; // protocol
2085 $text = $bits[$i++];
2086 $trail = $bits[$i++];
2087
2088 # The characters '<' and '>' (which were escaped by
2089 # internalRemoveHtmlTags()) should not be included in
2090 # URLs, per RFC 2396.
2091 $m2 = [];
2092 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2093 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2094 $url = substr( $url, 0, $m2[0][1] );
2095 }
2096
2097 # If the link text is an image URL, replace it with an <img> tag
2098 # This happened by accident in the original parser, but some people used it extensively
2099 $img = $this->maybeMakeExternalImage( $text );
2100 if ( $img !== false ) {
2101 $text = $img;
2102 }
2103
2104 $dtrail = '';
2105
2106 # Set linktype for CSS
2107 $linktype = 'text';
2108
2109 # No link text, e.g. [http://domain.tld/some.link]
2110 if ( $text == '' ) {
2111 # Autonumber
2112 $langObj = $this->getTargetLanguage();
2113 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2114 $linktype = 'autonumber';
2115 } else {
2116 # Have link text, e.g. [http://domain.tld/some.link text]s
2117 # Check for trail
2118 [ $dtrail, $trail ] = Linker::splitTrail( $trail );
2119 }
2120
2121 // Excluding protocol-relative URLs may avoid many false positives.
2122 if ( preg_match( '/^(?:' . $this->urlUtils->validAbsoluteProtocols() . ')/', $text ) ) {
2123 $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2124 }
2125
2126 $url = Sanitizer::cleanUrl( $url );
2127
2128 # Use the encoded URL
2129 # This means that users can paste URLs directly into the text
2130 # Funny characters like ö aren't valid in URLs anyway
2131 # This was changed in August 2004
2132 $s .= $this->getLinkRenderer()->makeExternalLink(
2133 $url,
2134 new HtmlArmor( $text ),
2135 $this->getTitle(),
2136 $linktype,
2137 $this->getExternalLinkAttribs( $url )
2138 ) . $dtrail . $trail;
2139
2140 # Register link in the output object.
2141 $this->mOutput->addExternalLink( $url );
2142 }
2143
2144 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive from array_shift
2145 return $s;
2146 }
2147
2158 public static function getExternalLinkRel( $url = false, $title = null ) {
2159 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
2160 $noFollowLinks = $mainConfig->get( MainConfigNames::NoFollowLinks );
2161 $noFollowNsExceptions = $mainConfig->get( MainConfigNames::NoFollowNsExceptions );
2162 $noFollowDomainExceptions = $mainConfig->get( MainConfigNames::NoFollowDomainExceptions );
2163 $ns = $title ? $title->getNamespace() : false;
2164 if (
2165 $noFollowLinks && !in_array( $ns, $noFollowNsExceptions )
2166 && !wfGetUrlUtils()->matchesDomainList( (string)$url, $noFollowDomainExceptions )
2167 ) {
2168 return 'nofollow';
2169 }
2170 return null;
2171 }
2172
2184 public function getExternalLinkAttribs( $url ) {
2185 $attribs = [];
2186 $rel = self::getExternalLinkRel( $url, $this->getTitle() ) ?? '';
2187
2188 $target = $this->mOptions->getExternalLinkTarget();
2189 if ( $target ) {
2190 $attribs['target'] = $target;
2191 if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2192 // T133507. New windows can navigate parent cross-origin.
2193 // Including noreferrer due to lacking browser
2194 // support of noopener. Eventually noreferrer should be removed.
2195 if ( $rel !== '' ) {
2196 $rel .= ' ';
2197 }
2198 $rel .= 'noreferrer noopener';
2199 }
2200 }
2201 if ( $rel !== '' ) {
2202 $attribs['rel'] = $rel;
2203 }
2204 return $attribs;
2205 }
2206
2217 public static function normalizeLinkUrl( $url ) {
2218 # Test for RFC 3986 IPv6 syntax
2219 $scheme = '[a-z][a-z0-9+.-]*:';
2220 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2221 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2222 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2223 IPUtils::isValid( rawurldecode( $m[1] ) )
2224 ) {
2225 $isIPv6 = rawurldecode( $m[1] );
2226 } else {
2227 $isIPv6 = false;
2228 }
2229
2230 # Make sure unsafe characters are encoded
2231 $url = preg_replace_callback(
2232 '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]+/',
2233 static fn ( $m ) => rawurlencode( $m[0] ),
2234 $url
2235 );
2236
2237 $ret = '';
2238 $end = strlen( $url );
2239
2240 # Fragment part - 'fragment'
2241 $start = strpos( $url, '#' );
2242 if ( $start !== false && $start < $end ) {
2243 $ret = self::normalizeUrlComponent(
2244 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2245 $end = $start;
2246 }
2247
2248 # Query part - 'query' minus &=+;
2249 $start = strpos( $url, '?' );
2250 if ( $start !== false && $start < $end ) {
2251 $ret = self::normalizeUrlComponent(
2252 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2253 $end = $start;
2254 }
2255
2256 # Path part - 'pchar', remove dot segments
2257 # (find first '/' after the optional '//' after the scheme)
2258 $start = strpos( $url, '//' );
2259 $start = strpos( $url, '/', $start === false ? 0 : $start + 2 );
2260 if ( $start !== false && $start < $end ) {
2261 $ret = UrlUtils::removeDotSegments( self::normalizeUrlComponent(
2262 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}/?' ) ) . $ret;
2263 $end = $start;
2264 }
2265
2266 # Scheme and host part - 'pchar'
2267 # (we assume no userinfo or encoded colons in the host)
2268 $ret = self::normalizeUrlComponent(
2269 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2270
2271 # Fix IPv6 syntax
2272 if ( $isIPv6 !== false ) {
2273 $ipv6Host = "%5B({$isIPv6})%5D";
2274 $ret = preg_replace(
2275 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2276 "$1[$2]",
2277 $ret
2278 );
2279 }
2280
2281 return $ret;
2282 }
2283
2284 private static function normalizeUrlComponent( string $component, string $unsafe ): string {
2285 $callback = static function ( $matches ) use ( $unsafe ) {
2286 $char = urldecode( $matches[0] );
2287 $ord = ord( $char );
2288 if ( $ord > 32 && $ord < 127 && !str_contains( $unsafe, $char ) ) {
2289 # Unescape it
2290 return $char;
2291 } else {
2292 # Leave it escaped, but use uppercase for a-f
2293 return strtoupper( $matches[0] );
2294 }
2295 };
2296 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2297 }
2298
2307 private function maybeMakeExternalImage( $url ) {
2308 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2309 $imagesexception = (bool)$imagesfrom;
2310 $text = false;
2311 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2312 if ( $imagesexception && is_array( $imagesfrom ) ) {
2313 $imagematch = false;
2314 foreach ( $imagesfrom as $match ) {
2315 if ( str_starts_with( $url, $match ) ) {
2316 $imagematch = true;
2317 break;
2318 }
2319 }
2320 } elseif ( $imagesexception ) {
2321 $imagematch = str_starts_with( $url, $imagesfrom );
2322 } else {
2323 $imagematch = false;
2324 }
2325
2326 if ( $this->mOptions->getAllowExternalImages()
2327 || ( $imagesexception && $imagematch )
2328 ) {
2329 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2330 # Image found
2331 $text = Linker::makeExternalImage( $url );
2332 }
2333 }
2334 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2335 && preg_match( self::EXT_IMAGE_REGEX, $url )
2336 ) {
2337 $whitelist = explode(
2338 "\n",
2339 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2340 );
2341
2342 foreach ( $whitelist as $entry ) {
2343 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2344 if ( $entry === '' || str_starts_with( $entry, '#' ) ) {
2345 continue;
2346 }
2347 // @phan-suppress-next-line SecurityCheck-ReDoS preg_quote is not wanted here
2348 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2349 # Image matches a whitelist entry
2350 $text = Linker::makeExternalImage( $url );
2351 break;
2352 }
2353 }
2354 }
2355 return $text;
2356 }
2357
2365 private function handleInternalLinks( $text ) {
2366 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2367 return $text;
2368 }
2369
2375 private function handleInternalLinks2( &$s ) {
2376 static $tc = false, $e1, $e1_img;
2377 # the % is needed to support urlencoded titles as well
2378 if ( !$tc ) {
2379 $tc = Title::legalChars() . '#%';
2380 # Match a link having the form [[namespace:link|alternate]]trail
2381 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2382 # Match cases where there is no "]]", which might still be images
2383 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2384 }
2385
2386 $holders = new LinkHolderArray(
2387 $this,
2388 $this->getContentLanguageConverter(),
2389 $this->getHookContainer() );
2390
2391 # split the entire text string on occurrences of [[
2392 $a = StringUtils::explode( '[[', ' ' . $s );
2393 # get the first element (all text up to first [[), and remove the space we added
2394 $s = $a->current();
2395 $a->next();
2396 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2397 $s = substr( $s, 1 );
2398
2399 $nottalk = !$this->getTitle()->isTalkPage();
2400
2401 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2402 $e2 = null;
2403 if ( $useLinkPrefixExtension ) {
2404 # Match the end of a line for a word that's not followed by whitespace,
2405 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2406 $charset = $this->contLang->linkPrefixCharset();
2407 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2408 $m = [];
2409 if ( preg_match( $e2, $s, $m ) ) {
2410 $first_prefix = $m[2];
2411 } else {
2412 $first_prefix = false;
2413 }
2414 $prefix = false;
2415 } else {
2416 $first_prefix = false;
2417 $prefix = '';
2418 }
2419
2420 # Some namespaces don't allow subpages
2421 $useSubpages = $this->nsInfo->hasSubpages(
2422 $this->getTitle()->getNamespace()
2423 );
2424
2425 # Loop for each link
2426 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2427 # Check for excessive memory usage
2428 if ( $holders->isBig() ) {
2429 # Too big
2430 # Do the existence check, replace the link holders and clear the array
2431 $holders->replace( $s );
2432 $holders->clear();
2433 }
2434
2435 if ( $useLinkPrefixExtension ) {
2436 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal $e2 is set under this condition
2437 if ( preg_match( $e2, $s, $m ) ) {
2438 [ , $s, $prefix ] = $m;
2439 } else {
2440 $prefix = '';
2441 }
2442 # first link
2443 if ( $first_prefix ) {
2444 $prefix = $first_prefix;
2445 $first_prefix = false;
2446 }
2447 }
2448
2449 $might_be_img = false;
2450
2451 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2452 $text = $m[2];
2453 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2454 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2455 # the real problem is with the $e1 regex
2456 # See T1500.
2457 # Still some problems for cases where the ] is meant to be outside punctuation,
2458 # and no image is in sight. See T4095.
2459 if ( $text !== ''
2460 && substr( $m[3], 0, 1 ) === ']'
2461 && strpos( $text, '[' ) !== false
2462 ) {
2463 $text .= ']'; # so that handleExternalLinks($text) works later
2464 $m[3] = substr( $m[3], 1 );
2465 }
2466 # fix up urlencoded title texts
2467 if ( str_contains( $m[1], '%' ) ) {
2468 # Should anchors '#' also be rejected?
2469 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2470 }
2471 $trail = $m[3];
2472 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2473 # Invalid, but might be an image with a link in its caption
2474 $might_be_img = true;
2475 $text = $m[2];
2476 if ( str_contains( $m[1], '%' ) ) {
2477 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2478 }
2479 $trail = "";
2480 } else { # Invalid form; output directly
2481 $s .= $prefix . '[[' . $line;
2482 continue;
2483 }
2484
2485 $origLink = ltrim( $m[1], ' ' );
2486
2487 # Don't allow internal links to pages containing
2488 # PROTO: where PROTO is a valid URL protocol; these
2489 # should be external links.
2490 if ( preg_match( '/^(?i:' . $this->urlUtils->validProtocols() . ')/', $origLink ) ) {
2491 $s .= $prefix . '[[' . $line;
2492 continue;
2493 }
2494
2495 # Make subpage if necessary
2496 if ( $useSubpages ) {
2497 $link = Linker::normalizeSubpageLink(
2498 $this->getTitle(), $origLink, $text
2499 );
2500 } else {
2501 $link = $origLink;
2502 }
2503
2504 // \x7f isn't a default legal title char, so most likely strip
2505 // markers will force us into the "invalid form" path above. But,
2506 // just in case, let's assert that xmlish tags aren't valid in
2507 // the title position.
2508 $unstrip = $this->mStripState->killMarkers( $link );
2509 $noMarkers = ( $unstrip === $link );
2510
2511 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2512 if ( $nt === null ) {
2513 $s .= $prefix . '[[' . $line;
2514 continue;
2515 }
2516
2517 $ns = $nt->getNamespace();
2518 $iw = $nt->getInterwiki();
2519
2520 $noforce = !str_starts_with( $origLink, ':' );
2521
2522 if ( $might_be_img ) { # if this is actually an invalid link
2523 if ( $ns === NS_FILE && $noforce ) { # but might be an image
2524 $found = false;
2525 while ( true ) {
2526 # look at the next 'line' to see if we can close it there
2527 $a->next();
2528 $next_line = $a->current();
2529 if ( $next_line === false || $next_line === null ) {
2530 break;
2531 }
2532 $m = explode( ']]', $next_line, 3 );
2533 if ( count( $m ) == 3 ) {
2534 # the first ]] closes the inner link, the second the image
2535 $found = true;
2536 $text .= "[[{$m[0]}]]{$m[1]}";
2537 $trail = $m[2];
2538 break;
2539 } elseif ( count( $m ) == 2 ) {
2540 # if there's exactly one ]] that's fine, we'll keep looking
2541 $text .= "[[{$m[0]}]]{$m[1]}";
2542 } else {
2543 # if $next_line is invalid too, we need look no further
2544 $text .= '[[' . $next_line;
2545 break;
2546 }
2547 }
2548 if ( !$found ) {
2549 # we couldn't find the end of this imageLink, so output it raw
2550 # but don't ignore what might be perfectly normal links in the text we've examined
2551 $holders->merge( $this->handleInternalLinks2( $text ) );
2552 $s .= "{$prefix}[[$link|$text";
2553 # note: no $trail, because without an end, there *is* no trail
2554 continue;
2555 }
2556 } else { # it's not an image, so output it raw
2557 $s .= "{$prefix}[[$link|$text";
2558 # note: no $trail, because without an end, there *is* no trail
2559 continue;
2560 }
2561 }
2562
2563 $wasblank = ( $text == '' );
2564 if ( $wasblank ) {
2565 $text = $link;
2566 if ( !$noforce ) {
2567 # Strip off leading ':'
2568 $text = substr( $text, 1 );
2569 }
2570 } else {
2571 # T6598 madness. Handle the quotes only if they come from the alternate part
2572 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2573 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2574 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2575 $text = $this->doQuotes( $text );
2576 }
2577
2578 # Link not escaped by : , create the various objects
2579 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2580 # Interwikis
2581 if (
2582 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2583 $this->languageNameUtils->getLanguageName(
2584 $iw,
2585 LanguageNameUtils::AUTONYMS,
2586 LanguageNameUtils::DEFINED
2587 )
2588 || in_array( $iw, $this->svcOptions->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) )
2589 )
2590 ) {
2591 # T26502: duplicates are resolved in ParserOutput
2592 $this->mOutput->addLanguageLink( $nt );
2593
2598 $s = preg_replace( '/\n\s*$/', '', $s . $prefix ) . $trail;
2599 continue;
2600 }
2601
2602 if ( $ns === NS_FILE ) {
2603 if ( $wasblank ) {
2604 # if no parameters were passed, $text
2605 # becomes something like "File:Foo.png",
2606 # which we don't want to pass on to the
2607 # image generator
2608 $text = '';
2609 } else {
2610 # recursively parse links inside the image caption
2611 # actually, this will parse them in any other parameters, too,
2612 # but it might be hard to fix that, and it doesn't matter ATM
2613 $text = $this->handleExternalLinks( $text );
2614 $holders->merge( $this->handleInternalLinks2( $text ) );
2615 }
2616 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2617 $s .= $prefix . $this->armorLinks(
2618 $this->makeImageInternal( $nt, $text, $holders ) ) . $trail;
2619 continue;
2620 } elseif ( $ns === NS_CATEGORY ) {
2621 # Strip newlines from the left hand context of Category
2622 # links.
2623 # See T2087, T87753, T174639, T359886
2624 $s = preg_replace( '/\n\s*$/', '', $s . $prefix ) . $trail;
2625
2626 $sortkey = ''; // filled in by CategoryLinksTable
2627 if ( !$wasblank ) {
2628 $sortkey = $text;
2629 }
2630 $this->mOutput->addCategory( $nt, $sortkey );
2631
2632 continue;
2633 }
2634 }
2635
2636 # Self-link checking. For some languages, variants of the title are checked in
2637 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2638 # for linking to a different variant.
2639 if ( $ns !== NS_SPECIAL && $nt->equals( $this->getTitle() ) ) {
2640 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail, '',
2641 Sanitizer::escapeIdForLink( $nt->getFragment() ) );
2642 continue;
2643 }
2644
2645 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2646 # @todo FIXME: Should do batch file existence checks, see comment below
2647 if ( $ns === NS_MEDIA ) {
2648 # Give extensions a chance to select the file revision for us
2649 $options = [];
2650 $descQuery = false;
2651 $this->hookRunner->onBeforeParserFetchFileAndTitle(
2652 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
2653 $this, $nt, $options, $descQuery
2654 );
2655 # Fetch and register the file (file title may be different via hooks)
2656 [ $file, $nt ] = $this->fetchFileAndTitle( $nt, $options );
2657 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2658 $s .= $prefix . $this->armorLinks(
2659 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2660 continue;
2661 }
2662
2663 # Some titles, such as valid special pages or files in foreign repos, should
2664 # be shown as bluelinks even though they're not included in the page table
2665 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2666 # batch file existence checks for NS_FILE and NS_MEDIA
2667 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2668 $this->mOutput->addLink( $nt );
2669 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2670 } else {
2671 # Links will be added to the output link list after checking
2672 $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2673 }
2674 }
2675 return $holders;
2676 }
2677
2691 private function makeKnownLinkHolder( LinkTarget $nt, $text = '', $trail = '', $prefix = '' ) {
2692 [ $inside, $trail ] = Linker::splitTrail( $trail );
2693
2694 if ( $text == '' ) {
2695 $text = htmlspecialchars( $this->titleFormatter->getPrefixedText( $nt ) );
2696 }
2697
2698 $link = $this->getLinkRenderer()->makeKnownLink(
2699 $nt, new HtmlArmor( "$prefix$text$inside" )
2700 );
2701
2702 return $this->armorLinks( $link ) . $trail;
2703 }
2704
2715 private function armorLinks( $text ) {
2716 return preg_replace( '/\b((?i)' . $this->urlUtils->validProtocols() . ')/',
2717 self::MARKER_PREFIX . "NOPARSE$1", $text );
2718 }
2719
2728 private function expandMagicVariable( $index, $frame = false ) {
2733 if ( isset( $this->mVarCache[$index] ) ) {
2734 return $this->mVarCache[$index];
2735 }
2736
2737 $value = CoreMagicVariables::expand(
2738 $this, $index, new MWTimestamp( $this->getParseTime() ),
2739 $this->svcOptions, $this->logger
2740 );
2741
2742 if ( $value === null ) {
2743 // Not a defined core magic word
2744 // Don't give this hook unrestricted access to mVarCache
2745 $fakeCache = [];
2746 $this->hookRunner->onParserGetVariableValueSwitch(
2747 // @phan-suppress-next-line PhanTypeMismatchArgument $value is passed as null but returned as string
2748 $this, $fakeCache, $index, $value, $frame
2749 );
2750 // Cache the value returned by the hook by falling through here.
2751 // Assert the the hook returned a non-null value for this MV
2752 '@phan-var string $value';
2753 }
2754
2755 $this->mVarCache[$index] = $value;
2756
2757 return $value;
2758 }
2759
2764 private function initializeVariables() {
2765 $variableIDs = $this->magicWordFactory->getVariableIDs();
2766
2767 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2768 $this->mSubstWords = $this->magicWordFactory->getSubstArray();
2769 }
2770
2789 public function preprocessToDom( $text, $flags = 0 ) {
2790 return $this->getPreprocessor()->preprocessToObj( $text, $flags );
2791 }
2792
2820 public function replaceVariables(
2821 $text, $frame = false, $argsOnly = false, array $options = []
2822 ) {
2823 # Is there any text? Also, Prevent too big inclusions!
2824 $textSize = strlen( $text );
2825 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2826 return $text;
2827 }
2828
2829 if ( $frame === false ) {
2830 $frame = $this->getPreprocessor()->newFrame();
2831 } elseif ( !( $frame instanceof PPFrame ) ) {
2833 __METHOD__ . " called using plain parameters instead of " .
2834 "a PPFrame instance. Creating custom frame.",
2835 '1.43'
2836 );
2837 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2838 }
2839
2840 $ppFlags = 0;
2841 if ( $options['parsoidTopLevelCall'] ?? false ) {
2842 $ppFlags |= Preprocessor::START_IN_SOL_STATE;
2843 }
2844 $dom = $this->preprocessToDom( $text, $ppFlags );
2845 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2846 if ( $options['processNowiki'] ?? false ) {
2847 $flags |= PPFrame::PROCESS_NOWIKI;
2848 }
2849 $text = $frame->expand( $dom, $flags );
2850
2851 return $text;
2852 }
2853
2855 public function setStripExtTags( bool $val ) {
2856 $this->mStripExtTags = $val;
2857 }
2858
2886 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2887 # does no harm if $current and $max are present but are unnecessary for the message
2888 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2889 # only during preview, and that would split the parser cache unnecessarily.
2890 $this->mOutput->addWarningMsg(
2891 "$limitationType-warning",
2892 Message::numParam( $current ),
2893 Message::numParam( $max )
2894 );
2895 $this->addTrackingCategory( "$limitationType-category" );
2896 }
2897
2911 public function braceSubstitution( array $piece, PPFrame $frame ) {
2912 // Flags
2913
2914 // $text has been filled
2915 $found = false;
2916 $text = '';
2917 // wiki markup in $text should be escaped
2918 $nowiki = false;
2919 // $text is HTML, armour it against most wikitext transformation
2920 // (it still participates in doBlockLevels, language conversion,
2921 // and the other steps at the start of ::internalParseHalfParsed)
2922 $isHTML = false;
2923 // $text is raw HTML, armour it against all wikitext transformation
2924 $isRawHTML = false;
2925 // Force interwiki transclusion to be done in raw mode not rendered
2926 $forceRawInterwiki = false;
2927 // $text is a DOM node needing expansion in a child frame
2928 $isChildObj = false;
2929 // $text is a DOM node needing expansion in the current frame
2930 $isLocalObj = false;
2931
2932 # Title object, where $text came from
2933 $title = false;
2934
2935 # $part1 is the bit before the first |, and must contain only title characters.
2936 # Various prefixes will be stripped from it later.
2937 $titleWithSpaces = $frame->expand( $piece['title'] );
2938 $part1 = trim( $titleWithSpaces );
2939 $titleText = false;
2940
2941 # Original title text preserved for various purposes
2942 $originalTitle = $part1;
2943
2944 # $args is a list of argument nodes, starting from index 0, not including $part1
2945 $args = $piece['parts'];
2946
2947 $profileSection = null; // profile templates
2948
2949 $sawDeprecatedTemplateEquals = false; // T91154
2950
2951 $isParsoid = $this->mOptions->getUseParsoid();
2952
2953 # SUBST
2954 // @phan-suppress-next-line PhanImpossibleCondition
2955 if ( !$found ) {
2956 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2957 $part1 = trim( $part1 );
2958
2959 # Possibilities for substMatch: "subst", "safesubst" or FALSE
2960 # Decide whether to expand template or keep wikitext as-is.
2961 if ( $this->ot['wiki'] ) {
2962 if ( $substMatch === false ) {
2963 $literal = true; # literal when in PST with no prefix
2964 } else {
2965 $literal = false; # expand when in PST with subst: or safesubst:
2966 }
2967 } else {
2968 if ( $substMatch == 'subst' ) {
2969 $literal = true; # literal when not in PST with plain subst:
2970 } else {
2971 $literal = false; # expand when not in PST with safesubst: or no prefix
2972 }
2973 }
2974 if ( $literal ) {
2975 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
2976 $isLocalObj = true;
2977 $found = true;
2978 }
2979 }
2980
2981 # Variables
2982 if ( !$found && $args->getLength() == 0 ) {
2983 $id = $this->mVariables->matchStartToEnd( $part1 );
2984 if ( $id !== false ) {
2985 if ( str_contains( $part1, ':' ) ) {
2987 'Registering a magic variable with a name including a colon',
2988 '1.39', false, false
2989 );
2990 }
2991 $text = $this->expandMagicVariable( $id, $frame );
2992 $found = true;
2993 }
2994 }
2995
2996 # MSG, MSGNW and RAW
2997 if ( !$found ) {
2998 # Check for MSGNW:
2999 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3000 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3001 $nowiki = true;
3002 } else {
3003 # Remove obsolete MSG:
3004 $mwMsg = $this->magicWordFactory->get( 'msg' );
3005 $mwMsg->matchStartAndRemove( $part1 );
3006 }
3007
3008 # Check for RAW:
3009 $mwRaw = $this->magicWordFactory->get( 'raw' );
3010 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3011 $forceRawInterwiki = true;
3012 }
3013 }
3014
3015 # Parser functions
3016 if ( !$found ) {
3017 // Allow colon or Japanese double-width colon as arg delimiter
3018 if ( preg_match( '/[::]/u', $part1, $colonMatches, PREG_OFFSET_CAPTURE ) ) {
3019 [ $colonStr, $colonPos ] = $colonMatches[0];
3020 $func = substr( $part1, 0, $colonPos );
3021 $funcArgs = [ trim( substr( $part1, $colonPos + strlen( $colonStr ) ) ) ];
3022 $argsLength = $args->getLength();
3023 for ( $i = 0; $i < $argsLength; $i++ ) {
3024 $funcArgs[] = $args->item( $i );
3025 }
3026
3027 $result = $this->callParserFunction(
3028 $frame, $func, $funcArgs, $isParsoid && $piece['lineStart']
3029 );
3030
3031 // Extract any forwarded flags
3032 if ( isset( $result['title'] ) ) {
3033 $title = $result['title'];
3034 }
3035 if ( isset( $result['found'] ) ) {
3036 $found = $result['found'];
3037 }
3038 if ( array_key_exists( 'text', $result ) ) {
3039 // a string or null
3040 $text = $result['text'];
3041 }
3042 if ( isset( $result['nowiki'] ) ) {
3043 $nowiki = $result['nowiki'];
3044 }
3045 if ( isset( $result['isHTML'] ) ) {
3046 $isHTML = $result['isHTML'];
3047 }
3048 if ( isset( $result['isRawHTML'] ) ) {
3049 $isRawHTML = $result['isRawHTML'];
3050 }
3051 if ( isset( $result['forceRawInterwiki'] ) ) {
3052 $forceRawInterwiki = $result['forceRawInterwiki'];
3053 }
3054 if ( isset( $result['isChildObj'] ) ) {
3055 $isChildObj = $result['isChildObj'];
3056 }
3057 if ( isset( $result['isLocalObj'] ) ) {
3058 $isLocalObj = $result['isLocalObj'];
3059 }
3060 }
3061 }
3062
3063 # Finish mangling title and then check for loops.
3064 # Set $title to a Title object and $titleText to the PDBK
3065 if ( !$found ) {
3066 $ns = NS_TEMPLATE;
3067 # Split the title into page and subpage
3068 $subpage = '';
3069 $relative = Linker::normalizeSubpageLink(
3070 $this->getTitle(), $part1, $subpage
3071 );
3072 if ( $part1 !== $relative ) {
3073 $part1 = $relative;
3074 $ns = $this->getTitle()->getNamespace();
3075 }
3076 $title = Title::newFromText( $part1, $ns );
3077 if ( $title ) {
3078 $titleText = $title->getPrefixedText();
3079 # Check for language variants if the template is not found
3080 if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3081 $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3082 }
3083 # Do recursion depth check
3084 $limit = $this->mOptions->getMaxTemplateDepth();
3085 if ( $frame->depth >= $limit ) {
3086 $found = true;
3087 $text = '<span class="error">'
3088 . wfMessage( 'parser-template-recursion-depth-warning' )
3089 ->numParams( $limit )->inContentLanguage()->text()
3090 . '</span>';
3091 }
3092 }
3093 }
3094
3095 # Load from database
3096 if ( !$found && $title ) {
3097 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3098 if ( !$title->isExternal() ) {
3099 if ( $title->isSpecialPage()
3100 && $this->mOptions->getAllowSpecialInclusion()
3101 && ( $this->ot['html'] ||
3102 // PFragment for Parsoid
3103 ( !$this->mStripExtTags && $this->ot['pre'] ) )
3104 ) {
3105 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3106 // Pass the template arguments as URL parameters.
3107 // "uselang" will have no effect since the Language object
3108 // is forced to the one defined in ParserOptions.
3109 $pageArgs = [];
3110 $argsLength = $args->getLength();
3111 for ( $i = 0; $i < $argsLength; $i++ ) {
3112 $bits = $args->item( $i )->splitArg();
3113 if ( strval( $bits['index'] ) === '' ) {
3114 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3115 $value = trim( $frame->expand( $bits['value'] ) );
3116 $pageArgs[$name] = $value;
3117 }
3118 }
3119
3120 // Create a new context to execute the special page, that is expensive
3121 if ( $this->incrementExpensiveFunctionCount() ) {
3122 $context = new RequestContext;
3123 $context->setTitle( $title );
3124 $context->setRequest( new FauxRequest( $pageArgs ) );
3125 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3126 $context->setUser( $this->userFactory->newFromUserIdentity( $this->getUserIdentity() ) );
3127 } else {
3128 // If this page is cached, then we better not be per user.
3129 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3130 }
3131 $context->setLanguage( $this->mOptions->getUserLangObj() );
3132 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3133 if ( $ret ) {
3134 $text = $context->getOutput()->getHTML();
3135 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3136 $found = true;
3137 $isHTML = true;
3138 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3139 $this->mOutput->updateRuntimeAdaptiveExpiry(
3140 $specialPage->maxIncludeCacheTime()
3141 );
3142 }
3143 }
3144 }
3145 } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3146 $found = false; # access denied
3147 $this->logger->debug(
3148 __METHOD__ .
3149 ": template inclusion denied for " . $title->getPrefixedDBkey()
3150 );
3151 } else {
3152 [ $text, $title ] = $this->getTemplateDom( $title, $isParsoid && $piece['lineStart'] );
3153 if ( $text !== false ) {
3154 $found = true;
3155 $isChildObj = true;
3156 if (
3157 $title->getNamespace() === NS_TEMPLATE &&
3158 $title->getDBkey() === '=' &&
3159 $originalTitle === '='
3160 ) {
3161 // Note that we won't get here if `=` is evaluated
3162 // (in the future) as a parser function, nor if
3163 // the Template namespace is given explicitly,
3164 // ie `{{Template:=}}`. Only `{{=}}` triggers.
3165 $sawDeprecatedTemplateEquals = true; // T91154
3166 }
3167 }
3168 }
3169
3170 # If the title is valid but undisplayable, make a link to it
3171 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3172 $text = "[[:$titleText]]";
3173 $found = true;
3174 }
3175 } elseif ( $title->isTrans() ) {
3176 # Interwiki transclusion
3177 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3178 $text = $this->interwikiTransclude( $title, 'render' );
3179 $isHTML = true;
3180 } else {
3181 $text = $this->interwikiTransclude( $title, 'raw' );
3182 # Preprocess it like a template
3183 $sol = ( $isParsoid && $piece['lineStart'] ) ? Preprocessor::START_IN_SOL_STATE : 0;
3184 $text = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION | $sol );
3185 $isChildObj = true;
3186 }
3187 $found = true;
3188 }
3189
3190 # Do infinite loop check
3191 # This has to be done after redirect resolution to avoid infinite loops via redirects
3192 if ( !$frame->loopCheck( $title ) ) {
3193 $found = true;
3194 $text = '<span class="error">'
3195 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3196 . '</span>';
3197 $this->addTrackingCategory( 'template-loop-category' );
3198 $this->mOutput->addWarningMsg(
3199 'template-loop-warning',
3200 wfEscapeWikiText( $titleText )
3201 );
3202 $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3203 }
3204 }
3205
3206 # If we haven't found text to substitute by now, we're done
3207 # Recover the source wikitext and return it
3208 if ( !$found ) {
3209 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3210 if ( $profileSection ) {
3211 $this->mProfiler->scopedProfileOut( $profileSection );
3212 }
3213 return [ 'object' => $text ];
3214 }
3215
3216 # Expand DOM-style return values in a child frame
3217 if ( $isChildObj ) {
3218 # Clean up argument array
3219 $newFrame = $frame->newChild( $args, $title );
3220
3221 if ( $nowiki ) {
3222 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3223 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3224 # Expansion is eligible for the empty-frame cache
3225 $text = $newFrame->cachedExpand( $titleText, $text );
3226 } else {
3227 # Uncached expansion
3228 $text = $newFrame->expand( $text );
3229 }
3230 }
3231 if ( $isLocalObj && $nowiki ) {
3232 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3233 $isLocalObj = false;
3234 }
3235
3236 if ( $profileSection ) {
3237 $this->mProfiler->scopedProfileOut( $profileSection );
3238 }
3239 if (
3240 $sawDeprecatedTemplateEquals &&
3241 $this->mStripState->unstripBoth( $text ) !== '='
3242 ) {
3243 // T91154: {{=}} is deprecated when it doesn't expand to `=`;
3244 // use {{Template:=}} if you must.
3245 $this->addTrackingCategory( 'template-equals-category' );
3246 $this->mOutput->addWarningMsg( 'template-equals-warning' );
3247 }
3248
3249 # Replace raw HTML by a placeholder
3250 if ( $isHTML ) {
3251 // @phan-suppress-next-line SecurityCheck-XSS
3252 $text = $this->insertStripItem( $text );
3253 } elseif ( $isRawHTML ) {
3254 $marker = self::MARKER_PREFIX . "-pf-"
3255 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3256 // use 'nowiki' type to protect this from doBlockLevels,
3257 // language conversion, etc.
3258 // @phan-suppress-next-line SecurityCheck-XSS
3259 $this->mStripState->addNoWiki( $marker, $text );
3260 $text = $marker;
3261 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3262 # Escape nowiki-style return values
3263 // @phan-suppress-next-line SecurityCheck-DoubleEscaped
3264 $text = wfEscapeWikiText( $text );
3265 } elseif ( is_string( $text )
3266 && !$piece['lineStart']
3267 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3268 ) {
3269 // T2529: if the template begins with a table or block-level
3270 // element, it should be treated as beginning a new line.
3271 // This behavior is somewhat controversial.
3272 //
3273 // T382464: Parsoid sets $piece['lineStart'] at top-level when
3274 // expanding templates, so this hack is restricted to nested expansions.
3275 $text = "\n" . $text;
3276 }
3277
3278 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3279 # Error, oversize inclusion
3280 if ( $titleText !== false ) {
3281 # Make a working, properly escaped link if possible (T25588)
3282 $text = "[[:$titleText]]";
3283 } else {
3284 # This will probably not be a working link, but at least it may
3285 # provide some hint of where the problem is
3286 $originalTitle = preg_replace( '/^:/', '', $originalTitle );
3287 $text = "[[:$originalTitle]]";
3288 }
3289 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3290 . 'post-expand include size too large -->' );
3291 $this->limitationWarn( 'post-expand-template-inclusion' );
3292 }
3293
3294 if ( $isLocalObj ) {
3295 $ret = [ 'object' => $text ];
3296 } else {
3297 $ret = [ 'text' => $text ];
3298 }
3299
3300 return $ret;
3301 }
3302
3328 public function callParserFunction( PPFrame $frame, $function, array $args = [], bool $inSolState = false ) {
3329 # Case sensitive functions
3330 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3331 $function = $this->mFunctionSynonyms[1][$function];
3332 } else {
3333 # Case insensitive functions
3334 $function = $this->contLang->lc( $function );
3335 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3336 $function = $this->mFunctionSynonyms[0][$function];
3337 } else {
3338 return [ 'found' => false ];
3339 }
3340 }
3341
3342 [ $callback, $flags ] = $this->mFunctionHooks[$function];
3343
3344 $allArgs = [ $this ];
3345 if ( $flags & self::SFH_OBJECT_ARGS ) {
3346 # Convert arguments to PPNodes and collect for appending to $allArgs
3347 $funcArgs = [];
3348 foreach ( $args as $k => $v ) {
3349 if ( $v instanceof PPNode || $k === 0 ) {
3350 $funcArgs[] = $v;
3351 } else {
3352 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3353 }
3354 }
3355
3356 # Add a frame parameter, and pass the arguments as an array
3357 $allArgs[] = $frame;
3358 $allArgs[] = $funcArgs;
3359 } else {
3360 # Convert arguments to plain text and append to $allArgs
3361 foreach ( $args as $k => $v ) {
3362 if ( $v instanceof PPNode ) {
3363 $allArgs[] = trim( $frame->expand( $v ) );
3364 } elseif ( is_int( $k ) && $k >= 0 ) {
3365 $allArgs[] = trim( $v );
3366 } else {
3367 $allArgs[] = trim( "$k=$v" );
3368 }
3369 }
3370 }
3371
3372 $result = $callback( ...$allArgs );
3373
3374 # The interface for function hooks allows them to return a wikitext
3375 # string or an array containing the string and any flags. This mungs
3376 # things around to match what this method should return.
3377 if ( !is_array( $result ) ) {
3378 $result = [
3379 'found' => true,
3380 'text' => $result,
3381 ];
3382 } else {
3383 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3384 $result['text'] = $result[0];
3385 }
3386 unset( $result[0] );
3387 $result += [
3388 'found' => true,
3389 ];
3390 }
3391
3392 $noparse = $result['noparse'] ?? true;
3393 if ( !$noparse ) {
3394 $preprocessFlags = $result['preprocessFlags'] ?? 0;
3395 if ( $inSolState ) {
3396 $preprocessFlags |= Preprocessor::START_IN_SOL_STATE;
3397 }
3398 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3399 $result['isChildObj'] = true;
3400 }
3401
3402 return $result;
3403 }
3404
3423 public function getTemplateDom( LinkTarget $title, bool $inSolState = false ) {
3424 $cacheTitle = $title;
3425 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3426
3427 if ( isset( $this->mTplRedirCache[$titleKey] ) ) {
3428 [ $ns, $dbk ] = $this->mTplRedirCache[$titleKey];
3429 $title = Title::makeTitle( $ns, $dbk );
3430 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3431 }
3432
3433 // Factor in sol-state in the cache key
3434 $titleKey = "$titleKey:sol=" . ( $inSolState ? "0" : "1" );
3435 if ( isset( $this->mTplDomCache[$titleKey] ) ) {
3436 return [ $this->mTplDomCache[$titleKey], $title ];
3437 }
3438
3439 # Cache miss, go to the database
3440 // FIXME T383919: if $title is changed by this call, caching below
3441 // will be ineffective.
3442 [ $text, $title ] = $this->fetchTemplateAndTitle( $title );
3443
3444 if ( $text === false ) {
3445 $this->mTplDomCache[$titleKey] = false;
3446 return [ false, $title ];
3447 }
3448
3449 $flags = Preprocessor::DOM_FOR_INCLUSION | ( $inSolState ? Preprocessor::START_IN_SOL_STATE : 0 );
3450 $dom = $this->preprocessToDom( $text, $flags );
3451 $this->mTplDomCache[$titleKey] = $dom;
3452
3453 if ( !$title->isSameLinkAs( $cacheTitle ) ) {
3454 $this->mTplRedirCache[ CacheKeyHelper::getKeyForPage( $cacheTitle ) ] =
3455 [ $title->getNamespace(), $title->getDBkey() ];
3456 }
3457
3458 return [ $dom, $title ];
3459 }
3460
3474 public function fetchCurrentRevisionRecordOfTitle( LinkTarget $link ) {
3475 $cacheKey = CacheKeyHelper::getKeyForPage( $link );
3476 if ( !$this->currentRevisionCache ) {
3477 $this->currentRevisionCache = new MapCacheLRU( 100 );
3478 }
3479 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3480 $title = Title::newFromLinkTarget( $link ); // hook signature compat
3481 $revisionRecord =
3482 // Defaults to Parser::statelessFetchRevisionRecord()
3483 $this->mOptions->getCurrentRevisionRecordCallback()(
3484 $title,
3485 $this
3486 );
3487 if ( $revisionRecord === false ) {
3488 // Parser::statelessFetchRevisionRecord() can return false;
3489 // normalize it to null.
3490 $revisionRecord = null;
3491 }
3492 $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3493 }
3494 return $this->currentRevisionCache->get( $cacheKey );
3495 }
3496
3503 public function isCurrentRevisionOfTitleCached( LinkTarget $link ) {
3504 $key = CacheKeyHelper::getKeyForPage( $link );
3505 return (
3506 $this->currentRevisionCache &&
3507 $this->currentRevisionCache->has( $key )
3508 );
3509 }
3510
3519 public static function statelessFetchRevisionRecord( LinkTarget $link, $parser = null ) {
3520 if ( $link instanceof PageIdentity ) {
3521 // probably a Title, just use it.
3522 $page = $link;
3523 } else {
3524 // XXX: use RevisionStore::getPageForLink()!
3525 // ...but get the info for the current revision at the same time?
3526 // Should RevisionStore::getKnownCurrentRevision accept a LinkTarget?
3527 $page = Title::newFromLinkTarget( $link );
3528 }
3529
3530 $revRecord = MediaWikiServices::getInstance()
3531 ->getRevisionLookup()
3532 ->getKnownCurrentRevision( $page );
3533 return $revRecord;
3534 }
3535
3542 public function fetchTemplateAndTitle( LinkTarget $link ) {
3543 // Use Title for compatibility with callbacks and return type
3544 $title = Title::newFromLinkTarget( $link );
3545
3546 // Defaults to Parser::statelessFetchTemplate()
3547 $templateCb = $this->mOptions->getTemplateCallback();
3548 $stuff = $templateCb( $title, $this );
3549 $revRecord = $stuff['revision-record'] ?? null;
3550
3551 $text = $stuff['text'];
3552 if ( is_string( $stuff['text'] ) ) {
3553 // We use U+007F DELETE to distinguish strip markers from regular text
3554 $text = strtr( $text, "\x7f", "?" );
3555 }
3556 $finalTitle = $stuff['finalTitle'] ?? $title;
3557 foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3558 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3559 if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3560 // Self-transclusion; final result may change based on the new page version
3561 try {
3562 $sha1 = $revRecord->getSha1();
3563 } catch ( RevisionAccessException ) {
3564 $sha1 = null;
3565 }
3566 $this->setOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1, 'Self transclusion' );
3567 $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3568 }
3569 }
3570
3571 return [ $text, $finalTitle ];
3572 }
3573
3584 public static function statelessFetchTemplate( $page, $parser = false ) {
3585 $title = Title::castFromLinkTarget( $page ); // for compatibility with return type
3586 $text = $skip = false;
3587 $finalTitle = $title;
3588 $deps = [];
3589 $revRecord = null;
3590 $contextTitle = $parser ? $parser->getTitle() : null;
3591
3592 # Loop to fetch the article, with up to 2 redirects
3593
3594 # Note that $title (including redirect targets) could be
3595 # external; we do allow hooks a chance to redirect the
3596 # external title to a local one (which might be useful), but
3597 # are careful not to add external titles to the dependency
3598 # list. (T362221)
3599
3600 $services = MediaWikiServices::getInstance();
3601 $revLookup = $services->getRevisionLookup();
3602 $hookRunner = new HookRunner( $services->getHookContainer() );
3603 for ( $i = 0; $i < 3 && is_object( $title ); $i++ ) {
3604 # Give extensions a chance to select the revision instead
3605 $revRecord = null; # Assume no hook
3606 $origTitle = $title;
3607 $titleChanged = false;
3609 # The $title is a not a PageIdentity, as it may
3610 # contain fragments or even represent an attempt to transclude
3611 # a broken or otherwise-missing Title, which the hook may
3612 # fix up. Similarly, the $contextTitle may represent a special
3613 # page or other page which "exists" as a parsing context but
3614 # is not in the DB.
3615 $contextTitle, $title,
3616 $skip, $revRecord
3617 );
3618
3619 if ( $skip ) {
3620 $text = false;
3621 if ( !$title->isExternal() ) {
3622 $deps[] = [
3623 'title' => $title,
3624 'page_id' => $title->getArticleID(),
3625 'rev_id' => null
3626 ];
3627 }
3628 break;
3629 }
3630 # Get the revision
3631 if ( !$revRecord ) {
3632 if ( $parser ) {
3633 $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3634 } else {
3635 $revRecord = $revLookup->getRevisionByTitle( $title );
3636 }
3637 }
3638 if ( $revRecord ) {
3639 # Update title, as $revRecord may have been changed by hook
3640 $title = Title::newFromPageIdentity( $revRecord->getPage() );
3641 // Assuming title is not external if we've got a $revRecord
3642 $deps[] = [
3643 'title' => $title,
3644 'page_id' => $revRecord->getPageId(),
3645 'rev_id' => $revRecord->getId(),
3646 ];
3647 } elseif ( !$title->isExternal() ) {
3648 $deps[] = [
3649 'title' => $title,
3650 'page_id' => $title->getArticleID(),
3651 'rev_id' => null,
3652 ];
3653 }
3654 if ( !$title->equals( $origTitle ) ) {
3655 # If we fetched a rev from a different title, register
3656 # the original title too...
3657 if ( !$origTitle->isExternal() ) {
3658 $deps[] = [
3659 'title' => $origTitle,
3660 'page_id' => $origTitle->getArticleID(),
3661 'rev_id' => null,
3662 ];
3663 }
3664 $titleChanged = true;
3665 }
3666 # If there is no current revision, there is no page
3667 if ( $revRecord === null || $revRecord->getId() === null ) {
3668 $linkCache = $services->getLinkCache();
3669 $linkCache->addBadLinkObj( $title );
3670 }
3671 if ( $revRecord ) {
3672 if ( $titleChanged && !$revRecord->hasSlot( SlotRecord::MAIN ) ) {
3673 // We've added this (missing) title to the dependencies;
3674 // give the hook another chance to redirect it to an
3675 // actual page.
3676 $text = false;
3677 $finalTitle = $title;
3678 continue;
3679 }
3680 if ( $revRecord->hasSlot( SlotRecord::MAIN ) ) { // T276476
3681 $content = $revRecord->getContent( SlotRecord::MAIN );
3682 $text = $content ? $content->getWikitextForTransclusion() : null;
3683 } else {
3684 $text = false;
3685 }
3686
3687 if ( $text === false || $text === null ) {
3688 $text = false;
3689 break;
3690 }
3691 } elseif ( $title->getNamespace() === NS_MEDIAWIKI ) {
3692 $message = wfMessage( $services->getContentLanguage()->
3693 lcfirst( $title->getText() ) )->inContentLanguage();
3694 if ( !$message->exists() ) {
3695 $text = false;
3696 break;
3697 }
3698 $text = $message->plain();
3699 break;
3700 } else {
3701 break;
3702 }
3703 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Only reached when content is set
3704 if ( !$content ) {
3705 break;
3706 }
3707 # Redirect?
3708 $finalTitle = $title;
3709 $title = $content->getRedirectTarget();
3710 }
3711
3712 $retValues = [
3713 // previously, when this also returned a Revision object, we set
3714 // 'revision-record' to false instead of null if it was unavailable,
3715 // so that callers to use isset and then rely on the revision-record
3716 // key instead of the revision key, even if there was no corresponding
3717 // object - we continue to set to false here for backwards compatability
3718 'revision-record' => $revRecord ?: false,
3719 'text' => $text,
3720 'finalTitle' => $finalTitle,
3721 'deps' => $deps
3722 ];
3723 return $retValues;
3724 }
3725
3734 public function fetchFileAndTitle( LinkTarget $link, array $options = [] ) {
3735 $file = $this->fetchFileNoRegister( $link, $options );
3736
3737 $time = $file ? $file->getTimestamp() : false;
3738 $sha1 = $file ? $file->getSha1() : false;
3739 # Register the file as a dependency...
3740 $this->mOutput->addImage( $link, $time, $sha1 );
3741 if ( $file && !$link->isSameLinkAs( $file->getTitle() ) ) {
3742 # Update fetched file title after resolving redirects, etc.
3743 $link = $file->getTitle();
3744 $this->mOutput->addImage( $link, $time, $sha1 );
3745 }
3746
3747 $title = Title::newFromLinkTarget( $link ); // for return type compat
3748 return [ $file, $title ];
3749 }
3750
3761 protected function fetchFileNoRegister( LinkTarget $link, array $options = [] ) {
3762 if ( isset( $options['broken'] ) ) {
3763 $file = false; // broken thumbnail forced by hook
3764 } else {
3765 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
3766 if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3767 $file = $repoGroup->findFileFromKey( $options['sha1'], $options );
3768 } else { // get by (name,timestamp)
3769 $link = TitleValue::newFromLinkTarget( $link );
3770 $file = $repoGroup->findFile( $link, $options );
3771 }
3772 }
3773 return $file;
3774 }
3775
3785 public function interwikiTransclude( LinkTarget $link, $action ) {
3786 if ( !$this->svcOptions->get( MainConfigNames::EnableScaryTranscluding ) ) {
3787 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3788 }
3789
3790 // TODO: extract relevant functionality from Title
3791 $title = Title::newFromLinkTarget( $link );
3792
3793 $url = $title->getFullURL( [ 'action' => $action ] );
3794 if ( strlen( $url ) > 1024 ) {
3795 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3796 }
3797
3798 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3799
3800 $fname = __METHOD__;
3801
3802 $cache = $this->wanCache;
3803 $data = $cache->getWithSetCallback(
3804 $cache->makeGlobalKey(
3805 'interwiki-transclude',
3806 ( $wikiId !== false ) ? $wikiId : 'external',
3807 sha1( $url )
3808 ),
3809 $this->svcOptions->get( MainConfigNames::TranscludeCacheExpiry ),
3810 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3811 $req = $this->httpRequestFactory->create( $url, [], $fname );
3812
3813 $status = $req->execute(); // Status object
3814 if ( !$status->isOK() ) {
3815 $ttl = $cache::TTL_UNCACHEABLE;
3816 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3817 $ttl = min( $cache::TTL_LAGGED, $ttl );
3818 }
3819
3820 return [
3821 'text' => $status->isOK() ? $req->getContent() : null,
3822 'code' => $req->getStatus()
3823 ];
3824 },
3825 [
3826 'checkKeys' => ( $wikiId !== false )
3827 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3828 : [],
3829 'pcGroup' => 'interwiki-transclude:5',
3830 'pcTTL' => $cache::TTL_PROC_LONG
3831 ]
3832 );
3833
3834 if ( is_string( $data['text'] ) ) {
3835 $text = $data['text'];
3836 } elseif ( $data['code'] != 200 ) {
3837 // Though we failed to fetch the content, this status is useless.
3838 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3839 ->params( $url, $data['code'] )->inContentLanguage()->text();
3840 } else {
3841 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3842 }
3843
3844 return $text;
3845 }
3846
3856 public function argSubstitution( array $piece, PPFrame $frame ) {
3857 $error = false;
3858 $parts = $piece['parts'];
3859 $nameWithSpaces = $frame->expand( $piece['title'] );
3860 $argName = trim( $nameWithSpaces );
3861 $object = false;
3862 $text = $frame->getArgument( $argName );
3863 if ( $text === false && $parts->getLength() > 0
3864 && ( $this->ot['html']
3865 || $this->ot['pre']
3866 || ( $this->ot['wiki'] && $frame->isTemplate() )
3867 )
3868 ) {
3869 # No match in frame, use the supplied default
3870 $object = $parts->item( 0 )->getChildren();
3871 }
3872 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3873 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3874 $this->limitationWarn( 'post-expand-template-argument' );
3875 }
3876
3877 if ( $text === false && $object === false ) {
3878 # No match anywhere
3879 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3880 }
3881 if ( $error !== false ) {
3882 $text .= $error;
3883 }
3884 if ( $object !== false ) {
3885 $ret = [ 'object' => $object ];
3886 } else {
3887 $ret = [ 'text' => $text ];
3888 }
3889
3890 return $ret;
3891 }
3892
3893 public function tagNeedsNowikiStrippedInTagPF( string $lowerTagName ): bool {
3894 $parsoidSiteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
3895 return $parsoidSiteConfig->tagNeedsNowikiStrippedInTagPF( $lowerTagName );
3896 }
3897
3917 public function extensionSubstitution( array $params, PPFrame $frame, bool $processNowiki = false ) {
3918 static $errorStr = '<span class="error">';
3919
3920 $name = $frame->expand( $params['name'] );
3921 if ( str_starts_with( $name, $errorStr ) ) {
3922 // Probably expansion depth or node count exceeded. Just punt the
3923 // error up.
3924 return $name;
3925 }
3926
3927 // Parse attributes from XML-like wikitext syntax
3928 $attrText = !isset( $params['attr'] ) ? '' : $frame->expand( $params['attr'] );
3929 if ( str_starts_with( $attrText, $errorStr ) ) {
3930 // See above
3931 return $attrText;
3932 }
3933
3934 // We can't safely check if the expansion for $content resulted in an
3935 // error, because the content could happen to be the error string
3936 // (T149622).
3937 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3938
3939 $marker = self::MARKER_PREFIX . "-$name-"
3940 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3941
3942 $normalizedName = strtolower( $name );
3943 $isNowiki = $normalizedName === 'nowiki';
3944 $markerType = $isNowiki ? 'nowiki' : 'general';
3945 $extra = $isNowiki ? 'nowiki' : null;
3946 if ( !$this->mStripExtTags ) {
3947 $processNowiki = true;
3948 }
3949 if ( $this->ot['html'] || ( $processNowiki && $isNowiki ) ) {
3950 $attributes = Sanitizer::decodeTagAttributes( $attrText );
3951 // Merge in attributes passed via {{#tag:}} parser function
3952 if ( isset( $params['attributes'] ) ) {
3953 $attributes += $params['attributes'];
3954 }
3955
3956 if ( isset( $this->mTagHooks[$normalizedName] ) ) {
3957 // Note that $content may be null here, for example if the
3958 // tag is self-closed.
3959 $output = $this->mTagHooks[$normalizedName]( $content, $attributes, $this, $frame );
3960 } else {
3961 $output = '<span class="error">Invalid tag extension name: ' .
3962 htmlspecialchars( $normalizedName ) . '</span>';
3963 }
3964
3965 if ( is_array( $output ) ) {
3966 // Extract flags
3967 $flags = $output;
3968 $output = $flags[0];
3969 if ( isset( $flags['isRawHTML'] ) ) {
3970 $markerType = 'nowiki';
3971 }
3972 if ( isset( $flags['markerType'] ) ) {
3973 $markerType = $flags['markerType'];
3974 }
3975 }
3976 } else {
3977 // We're substituting a {{subst:#tag:}} parser function.
3978 // Convert the attributes it passed into the XML-like string.
3979 if ( isset( $params['attributes'] ) ) {
3980 foreach ( $params['attributes'] as $attrName => $attrValue ) {
3981 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3982 htmlspecialchars( $this->getStripState()->unstripBoth( $attrValue ), ENT_COMPAT ) . '"';
3983 }
3984 }
3985 if ( $content === null ) {
3986 $output = "<$name$attrText/>";
3987 } else {
3988 $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
3989 if ( str_starts_with( $close, $errorStr ) ) {
3990 // See above
3991 return $close;
3992 }
3993 $output = "<$name$attrText>$content$close";
3994 }
3995 if ( !$this->mStripExtTags ) {
3996 $markerType = 'exttag';
3997 }
3998 }
3999
4000 if ( $markerType === 'none' ) {
4001 return $output;
4002 } elseif ( $markerType === 'nowiki' ) {
4003 $this->mStripState->addNoWiki( $marker, $output, $extra );
4004 } elseif ( $markerType === 'general' ) {
4005 $this->mStripState->addGeneral( $marker, $output );
4006 } elseif ( $markerType === 'exttag' ) {
4007 $this->mStripState->addExtTag( $marker, $output );
4008 } else {
4009 throw new UnexpectedValueException( __METHOD__ . ': invalid marker type' );
4010 }
4011 return $marker;
4012 }
4013
4021 private function incrementIncludeSize( $type, $size ) {
4022 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4023 return false;
4024 } else {
4025 $this->mIncludeSizes[$type] += $size;
4026 return true;
4027 }
4028 }
4029
4035 $this->mExpensiveFunctionCount++;
4036 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4037 }
4038
4046 private function handleDoubleUnderscore( $text ) {
4047 # The position of __TOC__ needs to be recorded
4048 $mw = $this->magicWordFactory->get( 'toc' );
4049 $tocAlias = null;
4050 if ( $mw->match( $text ) ) {
4051 $this->mShowToc = true;
4052 $this->mForceTocPosition = true;
4053 # record the alias used
4054 preg_match( $mw->getRegex(), $text, $tocAlias );
4055
4056 # Set a placeholder. At the end we'll fill it in with the TOC.
4057 $text = $mw->replace( self::TOC_PLACEHOLDER, $text, 1 );
4058
4059 # Only keep the first one.
4060 $text = $mw->replace( '', $text );
4061 }
4062
4063 # Now match and remove the rest of them
4064 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4065 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text, returnAlias: true );
4066 if ( $tocAlias ) {
4067 # For consistency with all other double-underscores (see below)
4068 $this->mDoubleUnderscores['toc'] = $tocAlias[0];
4069 }
4070
4071 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4072 $this->mOutput->setNoGallery( true );
4073 }
4074 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4075 $this->mShowToc = false;
4076 }
4077 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4078 && $this->getTitle()->getNamespace() === NS_CATEGORY
4079 ) {
4080 $this->addTrackingCategory( 'hidden-category-category' );
4081 }
4082 # (T10068) Allow control over whether robots index a page.
4083 # __INDEX__ always overrides __NOINDEX__, see T16899
4084 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->getTitle()->canUseNoindex() ) {
4085 $this->mOutput->setIndexPolicy( 'noindex' );
4086 $this->addTrackingCategory( 'noindex-category' );
4087 }
4088 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->getTitle()->canUseNoindex() ) {
4089 $this->mOutput->setIndexPolicy( 'index' );
4090 $this->addTrackingCategory( 'index-category' );
4091 }
4092
4093 foreach ( $this->mDoubleUnderscores as $key => $alias ) {
4094 # Cache all double underscores in the database
4095 $this->mOutput->setUnsortedPageProperty( $key );
4096 # Check for deprecated local aliases (T407289)
4097 $ascii = str_starts_with( $alias, '__' ) && str_ends_with( $alias, '__' );
4098 $wide = str_starts_with( $alias, '__' ) && str_ends_with( $alias, '__' );
4099 if ( !( $ascii || $wide ) ) {
4100 $this->addTrackingCategory( 'bad-double-underscore-category' );
4101 }
4102 }
4103
4104 return $text;
4105 }
4106
4113 public function addTrackingCategory( $msg ) {
4114 return $this->trackingCategories->addTrackingCategory(
4115 $this->mOutput, $msg, $this->getPage()
4116 );
4117 }
4118
4134 public function msg( string $msg, ...$params ): Message {
4135 return wfMessage( $msg, ...$params )
4136 ->inLanguage( $this->getTargetLanguage() )
4137 ->page( $this->getPage() );
4138 }
4139
4140 private function cleanUpTocLine( Node $container ) {
4141 '@phan-var Element|DocumentFragment $container'; // @var Element|DocumentFragment $container
4142 # Strip out HTML
4143 # Allowed tags are:
4144 # * <sup> and <sub> (T10393)
4145 # * <i> (T28375)
4146 # * <b> (r105284)
4147 # * <bdi> (T74884)
4148 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4149 # * <s> and <strike> (T35715)
4150 # * <q> (T251672)
4151 # We strip any parameter from accepted tags, except dir="rtl|ltr" from <span>,
4152 # to allow setting directionality in toc items.
4153 $allowedTags = [ 'span', 'sup', 'sub', 'bdi', 'i', 'b', 's', 'strike', 'q' ];
4154 $node = $container->firstChild;
4155 while ( $node !== null ) {
4156 $next = $node->nextSibling;
4157 if ( $node instanceof Element ) {
4158 $nodeName = DOMUtils::nodeName( $node );
4159 if ( in_array( $nodeName, [ 'style', 'script' ], true ) ) {
4160 # Remove any <style> or <script> tags (T198618)
4161 DOMCompat::remove( $node );
4162 } elseif ( in_array( $nodeName, $allowedTags, true ) ) {
4163 // Keep tag, remove attributes
4164 $removeAttrs = [];
4165 foreach ( $node->attributes as $attr ) {
4166 if (
4167 $nodeName === 'span' && $attr->name === 'dir'
4168 && ( $attr->value === 'rtl' || $attr->value === 'ltr' )
4169 ) {
4170 // Keep <span dir="rtl"> and <span dir="ltr">
4171 continue;
4172 }
4173 $removeAttrs[] = $attr;
4174 }
4175 foreach ( $removeAttrs as $attr ) {
4176 $node->removeAttributeNode( $attr );
4177 }
4178 $this->cleanUpTocLine( $node );
4179 # Strip '<span></span>', which is the result from the above if
4180 # <span id="foo"></span> is used to produce an additional anchor
4181 # for a section.
4182 if ( $nodeName === 'span' && !$node->hasChildNodes() ) {
4183 DOMCompat::remove( $node );
4184 }
4185 } else {
4186 // Strip tag
4187 if ( $node->firstChild !== null ) {
4188 $next = $node->firstChild;
4189 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
4190 while ( $childNode = $node->firstChild ) {
4191 $node->parentNode->insertBefore( $childNode, $node );
4192 }
4193 }
4194 DOMCompat::remove( $node );
4195 }
4196 } elseif ( $node instanceof Comment ) {
4197 // Extensions may add comments to headings;
4198 // these shouldn't appear in the ToC either.
4199 DOMCompat::remove( $node );
4200 }
4201 $node = $next;
4202 }
4203 }
4204
4220 private function finalizeHeadings( $text, $origText, $isMain = true ) {
4221 # Inhibit editsection links if requested in the page
4222 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4223 $maybeShowEditLink = false;
4224 } else {
4225 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4226 }
4227
4228 # Get all headlines for numbering them and adding funky stuff like [edit]
4229 # links - this is for later, but we need the number of headlines right now
4230 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4231 # be trimmed here since whitespace in HTML headings is significant.
4232 $matches = [];
4233 $numMatches = preg_match_all(
4234 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4235 $text,
4236 $matches
4237 );
4238
4239 # if there are fewer than 4 headlines in the article, do not show TOC
4240 # unless it's been explicitly enabled.
4241 $enoughToc = $this->mShowToc &&
4242 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4243
4244 # Allow user to stipulate that a page should have a "new section"
4245 # link added via __NEWSECTIONLINK__
4246 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4247 $this->mOutput->setNewSection( true );
4248 }
4249
4250 # Allow user to remove the "new section"
4251 # link via __NONEWSECTIONLINK__
4252 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4253 $this->mOutput->setHideNewSection( true );
4254 }
4255
4256 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4257 # override above conditions and always show TOC above first header
4258 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4259 $this->mShowToc = true;
4260 $enoughToc = true;
4261 }
4262
4263 if ( !$numMatches ) {
4264 return $text;
4265 }
4266
4267 # headline counter
4268 $headlineCount = 0;
4269 $haveTocEntries = false;
4270
4271 # Ugh .. the TOC should have neat indentation levels which can be
4272 # passed to the skin functions. These are determined here
4273 $head = [];
4274 $level = 0;
4275 $tocData = new TOCData();
4276 $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4277 $oldType = $this->mOutputType;
4278 $this->setOutputType( self::OT_WIKI );
4279 $frame = $this->getPreprocessor()->newFrame();
4280 $root = $this->preprocessToDom( $origText );
4281 $node = $root->getFirstChild();
4282 $cpOffset = 0;
4283 $refers = [];
4284
4285 $maxTocLevel = $this->svcOptions->get( MainConfigNames::MaxTocLevel );
4286 $domDocument = DOMCompat::newDocument();
4287 foreach ( $matches[3] as $headline ) {
4288 // $headline is half-parsed HTML
4289 $isTemplate = false;
4290 $titleText = false;
4291 $sectionIndex = false;
4292 if ( preg_match( self::HEADLINE_MARKER_REGEX, $headline, $markerMatches ) ) {
4293 $serial = (int)$markerMatches[1];
4294 [ $titleText, $sectionIndex ] = $this->mHeadings[$serial];
4295 $isTemplate = ( $titleText != $baseTitleText );
4296 $headline = ltrim( substr( $headline, strlen( $markerMatches[0] ) ) );
4297 }
4298
4299 $sectionMetadata = SectionMetadata::fromLegacy( [
4300 "fromtitle" => $titleText ?: null,
4301 "index" => $sectionIndex === false
4302 ? '' : ( ( $isTemplate ? 'T-' : '' ) . $sectionIndex )
4303 ] );
4304 $tocData->addSection( $sectionMetadata );
4305
4306 $oldLevel = $level;
4307 $level = (int)$matches[1][$headlineCount];
4308 $tocData->processHeading( $oldLevel, $level, $sectionMetadata );
4309
4310 if ( $tocData->getCurrentTOCLevel() < $maxTocLevel ) {
4311 $haveTocEntries = true;
4312 }
4313
4314 # Remove link placeholders by the link text.
4315 # <!--LINK number-->
4316 # turns into
4317 # link text with suffix
4318 # Do this before unstrip since link text can contain strip markers
4319 $fullyParsedHeadline = $this->replaceLinkHoldersText( $headline );
4320
4321 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4322 $fullyParsedHeadline = $this->mStripState->unstripBoth( $fullyParsedHeadline );
4323
4324 // Run Tidy to convert wikitext entities to HTML entities (T355386),
4325 // conveniently also giving us a way to handle French spaces (T324763)
4326 $fullyParsedHeadline = $this->tidy->tidy( $fullyParsedHeadline, Sanitizer::armorFrenchSpaces( ... ) );
4327
4328 // Wrap the safe headline to parse the heading attributes
4329 // Literal HTML tags should be sanitized at this point
4330 // cleanUpTocLine will strip the headline tag
4331 $wrappedHeadline = "<h$level" . $matches['attrib'][$headlineCount] . $fullyParsedHeadline . "</h$level>";
4332
4333 // Parse the heading contents as HTML. This makes it easier to strip out some HTML tags,
4334 // and ensures that we generate balanced HTML at the end (T218330).
4335 $headlineDom = DOMUtils::parseHTMLToFragment( $domDocument, $wrappedHeadline );
4336
4337 // Extract a user defined id on the heading
4338 // A heading is expected as the first child and could be asserted
4339 $h = $headlineDom->firstChild;
4340 $headingId = ( $h instanceof Element && DOMUtils::isHeading( $h ) ) ?
4341 DOMCompat::getAttribute( $h, 'id' ) : null;
4342
4343 $this->cleanUpTocLine( $headlineDom );
4344
4345 // Serialize back to HTML
4346 // $tocline is for the TOC display, fully-parsed HTML with some tags removed
4347 $tocline = trim( DOMUtils::getFragmentInnerHTML( $headlineDom ) );
4348
4349 // $headlineText is for the "Edit section: $1" tooltip, plain text
4350 $headlineText = trim( $headlineDom->textContent );
4351
4352 if ( $headingId === null || $headingId === '' ) {
4353 $headingId = Sanitizer::normalizeSectionNameWhitespace( $headlineText );
4354 $headingId = self::normalizeSectionName( $headingId );
4355 }
4356
4357 # Create the anchor for linking from the TOC to the section
4358 $fallbackAnchor = Sanitizer::escapeIdForAttribute( $headingId, Sanitizer::ID_FALLBACK );
4359 $linkAnchor = Sanitizer::escapeIdForLink( $headingId );
4360 $anchor = Sanitizer::escapeIdForAttribute( $headingId, Sanitizer::ID_PRIMARY );
4361 if ( $fallbackAnchor === $anchor ) {
4362 # No reason to have both (in fact, we can't)
4363 $fallbackAnchor = false;
4364 }
4365
4366 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4367 $arrayKey = strtolower( $anchor );
4368 if ( $fallbackAnchor === false ) {
4369 $fallbackArrayKey = false;
4370 } else {
4371 $fallbackArrayKey = strtolower( $fallbackAnchor );
4372 }
4373
4374 if ( isset( $refers[$arrayKey] ) ) {
4375 for ( $i = 2; isset( $refers["{$arrayKey}_$i"] ); ++$i );
4376 $anchor .= "_$i";
4377 $linkAnchor .= "_$i";
4378 $refers["{$arrayKey}_$i"] = true;
4379 } else {
4380 $refers[$arrayKey] = true;
4381 }
4382 if ( $fallbackAnchor !== false && isset( $refers[$fallbackArrayKey] ) ) {
4383 for ( $i = 2; isset( $refers["{$fallbackArrayKey}_$i"] ); ++$i );
4384 $fallbackAnchor .= "_$i";
4385 $refers["{$fallbackArrayKey}_$i"] = true;
4386 } else {
4387 $refers[$fallbackArrayKey] = true;
4388 }
4389
4390 # Add the section to the section tree
4391 # Find the DOM node for this header
4392 $noOffset = ( $isTemplate || $sectionIndex === false );
4393 while ( $node && !$noOffset ) {
4394 if ( $node->getName() === 'h' ) {
4395 $bits = $node->splitHeading();
4396 if ( $bits['i'] == $sectionIndex ) {
4397 break;
4398 }
4399 }
4400 $cpOffset += mb_strlen(
4401 $this->mStripState->unstripBoth(
4402 $frame->expand( $node, PPFrame::RECOVER_ORIG )
4403 )
4404 );
4405 $node = $node->getNextSibling();
4406 }
4407 $sectionMetadata->line = $tocline;
4408 $sectionMetadata->codepointOffset = ( $noOffset ? null : $cpOffset );
4409 $sectionMetadata->anchor = $anchor;
4410 $sectionMetadata->linkAnchor = $linkAnchor;
4411
4412 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4413 // Output edit section links as markers with styles that can be customized by skins
4414 if ( $isTemplate ) {
4415 # Put a T flag in the section identifier, to indicate to extractSections()
4416 # that sections inside <includeonly> should be counted.
4417 $editsectionPage = $titleText;
4418 $editsectionSection = "T-$sectionIndex";
4419 } else {
4420 $editsectionPage = $this->getTitle()->getPrefixedText();
4421 $editsectionSection = $sectionIndex;
4422 }
4423 // Construct a pseudo-HTML tag as a placeholder for the section edit link. It is replaced in
4424 // MediaWiki\OutputTransform\Stages\HandleSectionLinks with the real link.
4425 //
4426 // Any HTML markup in the input has already been escaped,
4427 // so we don't have to worry about a user trying to input one of these markers directly.
4428 //
4429 // We put the page and section in attributes to stop the language converter from
4430 // converting them, but put the headline hint in tag content
4431 // because it is supposed to be able to convert that.
4432 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage, ENT_COMPAT );
4433 $editlink .= '" section="' . htmlspecialchars( $editsectionSection, ENT_COMPAT ) . '"';
4434 $editlink .= '>' . htmlspecialchars( $headlineText ) . '</mw:editsection>';
4435 } else {
4436 $editlink = '';
4437 }
4438 // Reconstruct the original <h#> tag with added attributes. It is replaced in
4439 // MediaWiki\OutputTransform\Stages\HandleSectionLinks to add anchors and stuff.
4440 //
4441 // data-mw-... attributes are forbidden in Sanitizer::isReservedDataAttribute(),
4442 // so we don't have to worry about a user trying to input one of these markers directly.
4443 //
4444 // We put the anchors in attributes to stop the language converter from converting them.
4445 $head[$headlineCount] = "<h$level" . Html::expandAttributes( [
4446 'data-mw-anchor' => $anchor,
4447 'data-mw-fallback-anchor' => $fallbackAnchor,
4448 ] ) . $matches['attrib'][$headlineCount] . $headline . $editlink . "</h$level>";
4449
4450 $headlineCount++;
4451 }
4452
4453 $this->setOutputType( $oldType );
4454
4455 # Never ever show TOC if no headers (or suppressed)
4456 $suppressToc = $this->mOptions->getSuppressTOC();
4457 if ( !$haveTocEntries ) {
4458 $enoughToc = false;
4459 }
4460 $addTOCPlaceholder = false;
4461
4462 if ( $isMain && !$suppressToc ) {
4463 // We generally output the section information via the API
4464 // even if there isn't "enough" of a ToC to merit showing
4465 // it -- but the "suppress TOC" parser option is set when
4466 // any sections that might be found aren't "really there"
4467 // (ie, JavaScript content that might have spurious === or
4468 // <h2>: T307691) so we will *not* set section information
4469 // in that case.
4470 $this->mOutput->setTOCData( $tocData );
4471
4472 // T294950: Record a suggestion that the TOC should be shown.
4473 // Skins are free to ignore this suggestion and implement their
4474 // own criteria for showing/suppressing TOC (T318186).
4475 if ( $enoughToc ) {
4476 $this->mOutput->setOutputFlag( ParserOutputFlags::SHOW_TOC );
4477 if ( !$this->mForceTocPosition ) {
4478 $addTOCPlaceholder = true;
4479 }
4480 }
4481
4482 // If __NOTOC__ is used on the page (and not overridden by
4483 // __TOC__ or __FORCETOC__) set the NO_TOC flag to tell
4484 // the skin that although the section information is
4485 // valid, it should perhaps not be presented as a Table Of
4486 // Contents.
4487 if ( !$this->mShowToc ) {
4488 $this->mOutput->setOutputFlag( ParserOutputFlags::NO_TOC );
4489 }
4490 }
4491
4492 # split up and insert constructed headlines
4493 $blocks = preg_split( '/<h[1-6]\b[^>]*>.*?<\/h[1-6]>/is', $text );
4494 $i = 0;
4495
4496 // build an array of document sections
4497 $sections = [];
4498 foreach ( $blocks as $block ) {
4499 // $head is zero-based, sections aren't.
4500 if ( empty( $head[$i - 1] ) ) {
4501 $sections[$i] = $block;
4502 } else {
4503 $sections[$i] = $head[$i - 1] . $block;
4504 }
4505
4506 $i++;
4507 }
4508
4509 if ( $addTOCPlaceholder ) {
4510 // append the TOC at the beginning
4511 // Top anchor now in skin
4512 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset At least one element when enoughToc is true
4513 $sections[0] .= self::TOC_PLACEHOLDER . "\n";
4514 }
4515
4516 return implode( '', $sections );
4517 }
4518
4528 public static function localizeTOC(
4529 ?TOCData $tocData, Language $lang, ?ILanguageConverter $converter
4530 ) {
4531 if ( $tocData === null ) {
4532 return; // Nothing to do
4533 }
4534 foreach ( $tocData->getSections() as $s ) {
4535 // Localize heading
4536 if ( $converter ) {
4537 // T331316: don't use 'convert' or 'convertTo' as these reset
4538 // the language converter state.
4539 $s->line = $converter->convertTo(
4540 $s->line, $converter->getPreferredVariant(), false
4541 );
4542 }
4543 // Localize numbering
4544 $dot = '.';
4545 $pieces = explode( $dot, $s->number );
4546 $numbering = '';
4547 foreach ( $pieces as $i => $p ) {
4548 if ( $i > 0 ) {
4549 $numbering .= $dot;
4550 }
4551 $numbering .= $lang->formatNum( $p );
4552 }
4553 $s->number = $numbering;
4554 }
4555 }
4556
4569 public function preSaveTransform(
4570 $text,
4571 PageReference $page,
4572 UserIdentity $user,
4573 ParserOptions $options,
4574 $clearState = true
4575 ) {
4576 if ( $clearState ) {
4577 $magicScopeVariable = $this->lock();
4578 }
4579 $this->startParse( $page, $options, self::OT_WIKI, $clearState );
4580 $this->setUser( $user );
4581
4582 // Strip U+0000 NULL (T159174)
4583 $text = str_replace( "\000", '', $text );
4584
4585 // We still normalize line endings (including trimming trailing whitespace) for
4586 // backwards-compatibility with other code that just calls PST, but this should already
4587 // be handled in TextContent subclasses
4588 $text = TextContent::normalizeLineEndings( $text );
4589
4590 if ( $options->getPreSaveTransform() ) {
4591 $text = $this->pstPass2( $text, $user );
4592 }
4593 $text = $this->mStripState->unstripBoth( $text );
4594
4595 // Trim trailing whitespace again, because the previous steps can introduce it.
4596 $text = rtrim( $text );
4597
4598 $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4599
4600 $this->setUser( null ); # Reset
4601
4602 return $text;
4603 }
4604
4613 private function pstPass2( $text, UserIdentity $user ) {
4614 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4615 # $this->contLang here in order to give everyone the same signature and use the default one
4616 # rather than the one selected in each user's preferences. (see also T14815)
4617 $ts = $this->mOptions->getTimestamp();
4618 $timestamp = MWTimestamp::getLocalInstance( $ts );
4619 $ts = $timestamp->format( 'YmdHis' );
4620 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4621
4622 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4623
4624 # Variable replacement
4625 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4626 $text = $this->replaceVariables( $text );
4627
4628 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4629 # which may corrupt this parser instance via its wfMessage()->text() call-
4630
4631 # Signatures
4632 if ( str_contains( $text, '~~~' ) ) {
4633 $sigText = $this->getUserSig( $user );
4634 $text = strtr( $text, [
4635 '~~~~~' => $d,
4636 '~~~~' => "$sigText $d",
4637 '~~~' => $sigText
4638 ] );
4639 # The main two signature forms used above are time-sensitive
4640 $this->setOutputFlag( ParserOutputFlags::USER_SIGNATURE, 'User signature detected' );
4641 }
4642
4643 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4644 $tc = '[' . Title::legalChars() . ']';
4645 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4646
4647 // [[ns:page (context)|]]
4648 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4649 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4650 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4651 // [[ns:page (context), context|]] (using single, double-width or Arabic comma)
4652 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,|، )$tc+|)\\|]]/";
4653 // [[|page]] (reverse pipe trick: add context from page title)
4654 $p2 = "/\[\[\\|($tc+)]]/";
4655
4656 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4657 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4658 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4659 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4660
4661 $t = $this->getTitle()->getText();
4662 $m = [];
4663 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4664 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4665 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4666 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4667 } else {
4668 # if there's no context, don't bother duplicating the title
4669 $text = preg_replace( $p2, '[[\\1]]', $text );
4670 }
4671
4672 return $text;
4673 }
4674
4690 public function getUserSig( UserIdentity $user, $nickname = false, $fancySig = null ) {
4691 $username = $user->getName();
4692
4693 # If not given, retrieve from the user object.
4694 if ( $nickname === false ) {
4695 $nickname = $this->userOptionsLookup->getOption( $user, 'nickname' );
4696 }
4697
4698 $fancySig ??= $this->userOptionsLookup->getBoolOption( $user, 'fancysig' );
4699
4700 if ( $nickname === null || $nickname === '' ) {
4701 // Empty value results in the default signature (even when fancysig is enabled)
4702 $nickname = $username;
4703 } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( MainConfigNames::MaxSigChars ) ) {
4704 $nickname = $username;
4705 $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4706 } elseif ( $fancySig !== false ) {
4707 # Sig. might contain markup; validate this
4708 $isValid = $this->validateSig( $nickname ) !== false;
4709
4710 # New validator
4711 $sigValidation = $this->svcOptions->get( MainConfigNames::SignatureValidation );
4712 if ( $isValid && $sigValidation === 'disallow' ) {
4713 $parserOpts = new ParserOptions(
4714 $this->mOptions->getUserIdentity(),
4715 $this->contLang
4716 );
4717 $validator = $this->signatureValidatorFactory
4718 ->newSignatureValidator( $user, null, $parserOpts );
4719 $isValid = !$validator->validateSignature( $nickname );
4720 }
4721
4722 if ( $isValid ) {
4723 # Validated; clean up (if needed) and return it
4724 return $this->cleanSig( $nickname, true );
4725 } else {
4726 # Failed to validate; fall back to the default
4727 $nickname = $username;
4728 $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4729 }
4730 }
4731
4732 # Make sure nickname doesnt get a sig in a sig
4733 $nickname = self::cleanSigInSig( $nickname );
4734
4735 # If we're still here, make it a link to the user page
4736 $userText = wfEscapeWikiText( $username );
4737 $nickText = wfEscapeWikiText( $nickname );
4738 if ( $this->userNameUtils->isTemp( $username ) ) {
4739 $msgName = 'signature-temp';
4740 } elseif ( $user->isRegistered() ) {
4741 $msgName = 'signature';
4742 } else {
4743 $msgName = 'signature-anon';
4744 }
4745
4746 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4747 ->page( $this->getPage() )->text();
4748 }
4749
4757 public function validateSig( $text ) {
4758 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4759 }
4760
4772 public function cleanSig( $text, $parsing = false ) {
4773 if ( !$parsing ) {
4774 $magicScopeVariable = $this->lock();
4775 $this->startParse(
4776 $this->mTitle,
4777 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
4778 self::OT_PREPROCESS,
4779 true
4780 );
4781 }
4782
4783 # Option to disable this feature
4784 if ( !$this->mOptions->getCleanSignatures() ) {
4785 return $text;
4786 }
4787
4788 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4789 # => Move this logic to braceSubstitution()
4790 $substWord = $this->magicWordFactory->get( 'subst' );
4791 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4792 $substText = '{{' . $substWord->getSynonym( 0 );
4793
4794 $text = preg_replace( $substRegex, $substText, $text );
4795 $text = self::cleanSigInSig( $text );
4796 $dom = $this->preprocessToDom( $text );
4797 $frame = $this->getPreprocessor()->newFrame();
4798 $text = $frame->expand( $dom );
4799
4800 if ( !$parsing ) {
4801 $text = $this->mStripState->unstripBoth( $text );
4802 }
4803
4804 return $text;
4805 }
4806
4814 public static function cleanSigInSig( $text ) {
4815 $text = preg_replace( '/~{3,5}/', '', $text );
4816 return $text;
4817 }
4818
4835 public static function replaceTableOfContentsMarker( $text, $toc ) {
4836 // Optimization: Avoid a potentially expensive Remex tokenization and reserialization
4837 // if the content does not contain a TOC placeholder, such as during message parsing,
4838 // which may occur hundreds of times per request (T394059).
4839 if ( !str_contains( $text, 'mw:PageProp/toc' ) ) {
4840 return $text;
4841 }
4842
4843 $replaced = false;
4844 return HtmlHelper::modifyElements(
4845 $text,
4846 static function ( SerializerNode $node ): bool {
4847 $prop = $node->attrs['property'] ?? '';
4848 return $node->name === 'meta' && $prop === 'mw:PageProp/toc';
4849 },
4850 static function ( SerializerNode $node ) use ( &$replaced, $toc ) {
4851 if ( $replaced ) {
4852 // Remove the additional metas. While not strictly
4853 // necessary, this also ensures idempotence if we
4854 // run the pass more than once on a given content.
4855 return '';
4856 }
4857 $replaced = true;
4858 return $toc; // outerHTML replacement.
4859 },
4860 false /* use legacy-compatible serialization */
4861 );
4862 }
4863
4875 public function startExternalParse( ?PageReference $page, ParserOptions $options,
4876 $outputType, $clearState = true, $revId = null
4877 ) {
4878 $this->startParse( $page, $options, $outputType, $clearState );
4879 if ( $revId !== null ) {
4880 $this->mRevisionId = $revId;
4881 }
4882 }
4883
4890 private function startParse( ?PageReference $page, ParserOptions $options,
4891 $outputType, $clearState = true
4892 ) {
4893 $this->setPage( $page );
4894 $this->mOptions = $options;
4895 $this->setOutputType( $outputType );
4896 if ( $clearState ) {
4897 $this->clearState();
4898 }
4899 }
4900
4910 public function transformMsg( $text, ParserOptions $options, ?PageReference $page = null ) {
4911 static $executing = false;
4912
4913 # Guard against infinite recursion
4914 if ( $executing ) {
4915 return $text;
4916 }
4917 $executing = true;
4918
4919 $text = $this->preprocess( $text, $page ?? $this->mTitle, $options );
4920
4921 $executing = false;
4922 return $text;
4923 }
4924
4944 public function setHook( $tag, callable $callback ) {
4945 $tag = strtolower( $tag );
4946 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4947 throw new InvalidArgumentException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4948 }
4949 $oldVal = $this->mTagHooks[$tag] ?? null;
4950 $this->mTagHooks[$tag] = $callback;
4951 if ( !in_array( $tag, $this->mStripList ) ) {
4952 $this->mStripList[] = $tag;
4953 }
4954
4955 return $oldVal;
4956 }
4957
4962 public function clearTagHooks() {
4963 $this->mTagHooks = [];
4964 $this->mStripList = [];
4965 }
4966
5015 public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
5016 $oldVal = $this->mFunctionHooks[$id][0] ?? null;
5017 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5018
5019 # Add to function cache
5020 $mw = $this->magicWordFactory->get( $id );
5021
5022 $synonyms = $mw->getSynonyms();
5023 $sensitive = intval( $mw->isCaseSensitive() );
5024
5025 foreach ( $synonyms as $syn ) {
5026 # Case
5027 if ( !$sensitive ) {
5028 $syn = $this->contLang->lc( $syn );
5029 }
5030 # Add leading hash
5031 if ( !( $flags & self::SFH_NO_HASH ) ) {
5032 $syn = '#' . $syn;
5033 }
5034 # Remove trailing colon (or Japanese double-width colon)
5035 if ( str_ends_with( $syn, ':' ) || str_ends_with( $syn, ':' ) ) {
5036 $syn = mb_substr( $syn, 0, -1 );
5037 }
5038 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5039 }
5040 return $oldVal;
5041 }
5042
5049 public function getFunctionHooks() {
5050 return array_keys( $this->mFunctionHooks );
5051 }
5052
5060 public function replaceLinkHolders( &$text ) {
5061 wfDeprecated( __METHOD__, '1.46' );
5062 $this->replaceLinkHoldersPrivate( $text );
5063 }
5064
5071 private function replaceLinkHoldersPrivate( &$text ) {
5072 $this->mLinkHolders->replace( $text );
5073 }
5074
5082 private function replaceLinkHoldersText( $text ) {
5083 return $this->mLinkHolders->replaceText( $text );
5084 }
5085
5100 public function renderImageGallery( $text, array $params ) {
5101 $mode = $params['mode'] ?? false;
5102
5103 try {
5104 $ig = ImageGalleryBase::factory( $mode );
5106 // If invalid type set, fallback to default.
5107 $ig = ImageGalleryBase::factory();
5108 }
5109
5110 $ig->setContextTitle( $this->getTitle() );
5111 $ig->setShowBytes( false );
5112 $ig->setShowDimensions( false );
5113 $ig->setParser( $this );
5114 $ig->setHideBadImages();
5115 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5116
5117 $ig->setShowFilename( isset( $params['showfilename'] ) );
5118 if ( isset( $params['caption'] ) ) {
5119 // NOTE: We aren't passing a frame here or below. Frame info
5120 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5121 // See T107332#4030582
5122 $caption = $this->recursiveTagParse( $params['caption'] );
5123 $ig->setCaptionHtml( $caption );
5124 }
5125 if ( isset( $params['perrow'] ) ) {
5126 $ig->setPerRow( $params['perrow'] );
5127 }
5128 if ( isset( $params['widths'] ) ) {
5129 $ig->setWidths( $params['widths'] );
5130 }
5131 if ( isset( $params['heights'] ) ) {
5132 $ig->setHeights( $params['heights'] );
5133 }
5134 $ig->setAdditionalOptions( $params );
5135
5136 $lines = StringUtils::explode( "\n", $text );
5137 foreach ( $lines as $line ) {
5138 # match lines like these:
5139 # Image:someimage.jpg|This is some image
5140 $matches = [];
5141 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5142 # Skip empty lines
5143 if ( count( $matches ) == 0 ) {
5144 continue;
5145 }
5146
5147 if ( str_contains( $matches[0], '%' ) ) {
5148 $matches[1] = rawurldecode( $matches[1] );
5149 }
5150 $title = Title::newFromText( $matches[1], NS_FILE );
5151 if ( $title === null ) {
5152 # Bogus title. Ignore these so we don't bomb out later.
5153 continue;
5154 }
5155
5156 # We need to get what handler the file uses, to figure out parameters.
5157 # Note, a hook can override the file name, and chose an entirely different
5158 # file (which potentially could be of a different type and have different handler).
5159 $options = [];
5160 $descQuery = false;
5161 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5162 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5163 $this, $title, $options, $descQuery
5164 );
5165 # Don't register it now, as TraditionalImageGallery does that later.
5166 $file = $this->fetchFileNoRegister( $title, $options );
5167 $handler = $file ? $file->getHandler() : false;
5168
5169 $paramMap = [
5170 'img_alt' => 'gallery-internal-alt',
5171 'img_link' => 'gallery-internal-link',
5172 ];
5173 if ( $handler ) {
5174 $paramMap += $handler->getParamMap();
5175 // We don't want people to specify per-image widths.
5176 // Additionally the width parameter would need special casing anyhow.
5177 unset( $paramMap['img_width'] );
5178 }
5179
5180 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5181
5182 $label = '';
5183 $alt = null;
5184 $handlerOptions = [];
5185 $imageOptions = [];
5186 $hasAlt = false;
5187
5188 if ( isset( $matches[3] ) ) {
5189 // look for an |alt= definition while trying not to break existing
5190 // captions with multiple pipes (|) in it, until a more sensible grammar
5191 // is defined for images in galleries
5192
5193 // FIXME: Doing recursiveTagParse at this stage is a bit odd,
5194 // and different from makeImage.
5195 $matches[3] = $this->recursiveTagParse( $matches[3] );
5196 // Protect LanguageConverter markup
5197 $parameterMatches = StringUtils::delimiterExplode(
5198 '-{', '}-',
5199 '|',
5200 $matches[3],
5201 true /* nested */
5202 );
5203
5204 foreach ( $parameterMatches as $parameterMatch ) {
5205 [ $magicName, $match ] = $mwArray->matchVariableStartToEnd( trim( $parameterMatch ) );
5206 if ( !$magicName ) {
5207 // Last pipe wins.
5208 $label = $parameterMatch;
5209 continue;
5210 }
5211
5212 $paramName = $paramMap[$magicName];
5213 switch ( $paramName ) {
5214 case 'gallery-internal-alt':
5215 $hasAlt = true;
5216 $alt = $this->stripAltText( $match );
5217 break;
5218 case 'gallery-internal-link':
5219 $linkValue = $this->stripAltText( $match );
5220 if ( preg_match( '/^-{R\|(.*)}-$/', $linkValue ) ) {
5221 // Result of LanguageConverter::markNoConversion
5222 // invoked on an external link.
5223 $linkValue = substr( $linkValue, 4, -2 );
5224 }
5225 [ $type, $target ] = $this->parseLinkParameter( $linkValue );
5226 if ( $type ) {
5227 if ( $type === 'no-link' ) {
5228 $target = true;
5229 }
5230 $imageOptions[$type] = $target;
5231 }
5232 break;
5233 default:
5234 // Must be a handler specific parameter.
5235 if ( $handler->validateParam( $paramName, $match ) ) {
5236 $handlerOptions[$paramName] = $match;
5237 } else {
5238 // Guess not, consider it as caption.
5239 $this->logger->debug(
5240 "$parameterMatch failed parameter validation" );
5241 $label = $parameterMatch;
5242 }
5243 }
5244 }
5245 }
5246
5247 // Match makeImage when !$hasVisibleCaption
5248 if ( !$hasAlt && $label !== '' ) {
5249 $alt = $this->stripAltText( $label );
5250 }
5251 $imageOptions['title'] = $this->stripAltText( $label );
5252
5253 // Match makeImage which sets this unconditionally
5254 $handlerOptions['targetlang'] = $this->getTargetLanguage()->getCode();
5255
5256 $ig->add(
5257 $title, $label, $alt, '', $handlerOptions,
5258 ImageGalleryBase::LOADING_DEFAULT, $imageOptions
5259 );
5260 }
5261 $html = $ig->toHTML();
5262 $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5263 return $html;
5264 }
5265
5270 private function getImageParams( $handler ) {
5271 $handlerClass = $handler ? get_class( $handler ) : '';
5272 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5273 # Initialise static lists
5274 static $internalParamNames = [
5275 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5276 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5277 'bottom', 'text-bottom' ],
5278 'frame' => [ 'thumbnail', 'framed', 'frameless', 'border',
5279 // These parameters take arguments, so to ensure literals
5280 // have precedence, keep them listed last (T372935):
5281 'manualthumb', 'upright', 'link', 'alt', 'class' ],
5282 ];
5283 static $internalParamMap;
5284 if ( !$internalParamMap ) {
5285 $internalParamMap = [];
5286 foreach ( $internalParamNames as $type => $names ) {
5287 foreach ( $names as $name ) {
5288 // For grep: img_left, img_right, img_center, img_none,
5289 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5290 // img_bottom, img_text_bottom,
5291 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5292 // img_border, img_link, img_alt, img_class
5293 $magicName = str_replace( '-', '_', "img_$name" );
5294 $internalParamMap[$magicName] = [ $type, $name ];
5295 }
5296 }
5297 }
5298
5299 # Add handler params
5300 # Since img_width is one of these, it is important it is listed
5301 # *after* the literal parameter names above (T372935).
5302 $paramMap = $internalParamMap;
5303 if ( $handler ) {
5304 $handlerParamMap = $handler->getParamMap();
5305 foreach ( $handlerParamMap as $magic => $paramName ) {
5306 $paramMap[$magic] = [ 'handler', $paramName ];
5307 }
5308 } else {
5309 // Parse the size for non-existent files. See T273013
5310 $paramMap[ 'img_width' ] = [ 'handler', 'width' ];
5311 }
5312 $this->mImageParams[$handlerClass] = $paramMap;
5313 $this->mImageParamsMagicArray[$handlerClass] =
5314 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5315 }
5316 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5317 }
5318
5327 public function makeImageHtml( LinkTarget $link, string $options ): string {
5328 return $this->makeImageInternal(
5329 $link, $options, shouldReplaceLinkHolders: true
5330 );
5331 }
5332
5347 public function makeImage( LinkTarget $link, $options, $holders = false ) {
5348 wfDeprecated( __METHOD__, '1.46' );
5349 return $this->makeImageInternal(
5350 $link, $options, $holders ?: null, shouldReplaceLinkHolders: false
5351 );
5352 }
5353
5363 private function makeImageInternal(
5364 LinkTarget $link,
5365 string $options,
5366 ?LinkHolderArray $holders = null,
5367 bool $shouldReplaceLinkHolders = false
5368 ): string {
5369 # Check if the options text is of the form "options|alt text"
5370 # Options are:
5371 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5372 # * left no resizing, just left align. label is used for alt= only
5373 # * right same, but right aligned
5374 # * none same, but not aligned
5375 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5376 # * center center the image
5377 # * framed Keep original image size, no magnify-button.
5378 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5379 # * upright reduce width for upright images, rounded to full __0 px
5380 # * border draw a 1px border around the image
5381 # * alt Text for HTML alt attribute (defaults to empty)
5382 # * class Set a class for img node
5383 # * link Set the target of the image link. Can be external, interwiki, or local
5384 # vertical-align values (no % or length right now):
5385 # * baseline
5386 # * sub
5387 # * super
5388 # * top
5389 # * text-top
5390 # * middle
5391 # * bottom
5392 # * text-bottom
5393
5394 # Protect LanguageConverter markup when splitting into parts
5395 $parts = StringUtils::delimiterExplode(
5396 '-{', '}-', '|', $options, true /* allow nesting */
5397 );
5398
5399 # Give extensions a chance to select the file revision for us
5400 $options = [];
5401 $descQuery = false;
5402 $title = Title::castFromLinkTarget( $link ); // hook signature compat
5403 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5404 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5405 $this, $title, $options, $descQuery
5406 );
5407 # Fetch and register the file (file title may be different via hooks)
5408 [ $file, $link ] = $this->fetchFileAndTitle( $link, $options );
5409
5410 # Get parameter map
5411 $handler = $file ? $file->getHandler() : false;
5412
5413 [ $paramMap, $mwArray ] = $this->getImageParams( $handler );
5414
5415 if ( !$file ) {
5416 $this->addTrackingCategory( 'broken-file-category' );
5417 }
5418
5419 # Process the input parameters
5420 $caption = '';
5421 $params = [ 'frame' => [], 'handler' => [],
5422 'horizAlign' => [], 'vertAlign' => [] ];
5423 $seenformat = false;
5424 foreach ( $parts as $part ) {
5425 [ $magicName, $value ] = $mwArray->matchVariableStartToEnd( trim( $part ) );
5426 $validated = false;
5427 if ( isset( $paramMap[$magicName] ) ) {
5428 [ $type, $paramName ] = $paramMap[$magicName];
5429
5430 # Special case; width and height come in one variable together
5431 if ( $type === 'handler' && $paramName === 'width' ) {
5432 // The 'px' suffix has already been localized by img_width
5433 $parsedWidthParam = $this->parseWidthParam( $value, true, true );
5434 // Parsoid applies data-(width|height) attributes to broken
5435 // media spans, for client use. See T273013
5436 $validateFunc = static function ( $name, $value ) use ( $handler ) {
5437 return $handler
5438 ? $handler->validateParam( $name, $value )
5439 : $value > 0;
5440 };
5441 if ( isset( $parsedWidthParam['width'] ) ) {
5442 $width = $parsedWidthParam['width'];
5443 if ( $validateFunc( 'width', $width ) ) {
5444 $params[$type]['width'] = $width;
5445 $validated = true;
5446 }
5447 }
5448 if ( isset( $parsedWidthParam['height'] ) ) {
5449 $height = $parsedWidthParam['height'];
5450 if ( $validateFunc( 'height', $height ) ) {
5451 $params[$type]['height'] = $height;
5452 $validated = true;
5453 }
5454 }
5455 # else no validation -- T15436
5456 } else {
5457 if ( $type === 'handler' ) {
5458 # Validate handler parameter
5459 $validated = $handler->validateParam( $paramName, $value );
5460 } else {
5461 # Validate internal parameters
5462 switch ( $paramName ) {
5463 case 'alt':
5464 case 'class':
5465 $validated = true;
5466 $value = $this->stripAltText( $value, $holders );
5467 break;
5468 case 'link':
5469 [ $paramName, $value ] =
5470 $this->parseLinkParameter(
5471 $this->stripAltText( $value, $holders )
5472 );
5473 if ( $paramName ) {
5474 $validated = true;
5475 if ( $paramName === 'no-link' ) {
5476 $value = true;
5477 }
5478 }
5479 break;
5480 case 'manualthumb':
5481 # @todo FIXME: Possibly check validity here for
5482 # manualthumb? downstream behavior seems odd with
5483 # missing manual thumbs.
5484 $value = $this->stripAltText( $value, $holders );
5485 // fall through
5486 case 'frameless':
5487 case 'framed':
5488 case 'thumbnail':
5489 // use first appearing option, discard others.
5490 $validated = !$seenformat;
5491 $seenformat = true;
5492 break;
5493 default:
5494 # Most other things appear to be empty or numeric...
5495 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5496 }
5497 }
5498
5499 if ( $validated ) {
5500 $params[$type][$paramName] = $value;
5501 }
5502 }
5503 }
5504 if ( !$validated ) {
5505 $caption = $part;
5506 }
5507 }
5508
5509 # Process alignment parameters
5510 if ( $params['horizAlign'] !== [] ) {
5511 $params['frame']['align'] = array_key_first( $params['horizAlign'] );
5512 }
5513 if ( $params['vertAlign'] !== [] ) {
5514 $params['frame']['valign'] = array_key_first( $params['vertAlign'] );
5515 }
5516
5517 $params['frame']['caption'] = $caption;
5518
5519 # Will the image be presented in a frame, with the caption below?
5520 // @phan-suppress-next-line PhanImpossibleCondition
5521 $hasVisibleCaption = isset( $params['frame']['framed'] )
5522 || isset( $params['frame']['thumbnail'] )
5523 || isset( $params['frame']['manualthumb'] );
5524
5525 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5526 # came to also set the caption, ordinary text after the image -- which
5527 # makes no sense, because that just repeats the text multiple times in
5528 # screen readers. It *also* came to set the title attribute.
5529 # Now that we have an alt attribute, we should not set the alt text to
5530 # equal the caption: that's worse than useless, it just repeats the
5531 # text. This is the framed/thumbnail case. If there's no caption, we
5532 # use the unnamed parameter for alt text as well, just for the time be-
5533 # ing, if the unnamed param is set and the alt param is not.
5534 # For the future, we need to figure out if we want to tweak this more,
5535 # e.g., introducing a title= parameter for the title; ignoring the un-
5536 # named parameter entirely for images without a caption; adding an ex-
5537 # plicit caption= parameter and preserving the old magic unnamed para-
5538 # meter for BC; ...
5539
5540 if ( !$hasVisibleCaption ) {
5541 // @phan-suppress-next-line PhanImpossibleCondition
5542 if ( !isset( $params['frame']['alt'] ) && $caption !== '' ) {
5543 # No alt text, use the "caption" for the alt text
5544 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5545 }
5546 # Use the "caption" for the tooltip text
5547 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5548 }
5549 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5550
5551 // hook signature compat again, $link may have changed
5552 $title = Title::castFromLinkTarget( $link );
5553 $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5554
5555 # Linker does the rest
5556 $time = $options['time'] ?? false;
5557 $ret = Linker::makeImageLink( $this, $link, $file, $params['frame'], $params['handler'],
5558 $time, $descQuery, $this->mOptions->getThumbSize() );
5559
5560 # Give the handler a chance to modify the parser object
5561 if ( $handler ) {
5562 $handler->parserTransformHook( $this, $file );
5563 }
5564 if ( $file ) {
5565 $this->modifyImageHtml( $file, $params, $ret );
5566 }
5567 if ( $shouldReplaceLinkHolders ) {
5568 $this->replaceLinkHoldersPrivate( $ret );
5569 }
5570
5571 return $ret;
5572 }
5573
5592 private function parseLinkParameter( $value ) {
5593 $chars = self::EXT_LINK_URL_CLASS;
5594 $addr = self::EXT_LINK_ADDR;
5595 $prots = $this->urlUtils->validProtocols();
5596 $type = null;
5597 $target = false;
5598 if ( $value === '' ) {
5599 $type = 'no-link';
5600 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5601 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value ) ) {
5602 $this->mOutput->addExternalLink( $value );
5603 $type = 'link-url';
5604 $target = $value;
5605 }
5606 } else {
5607 // Percent-decode link arguments for consistency with wikilink
5608 // handling (T216003#7836261).
5609 //
5610 // There's slight concern here though. The |link= option supports
5611 // two formats, link=Test%22test vs link=[[Test%22test]], both of
5612 // which are about to be decoded.
5613 //
5614 // In the former case, the decoding here is straightforward and
5615 // desirable.
5616 //
5617 // In the latter case, there's a potential for double decoding,
5618 // because the wikilink syntax has a higher precedence and has
5619 // already been parsed as a link before we get here. $value
5620 // has had stripAltText() called on it, which in turn calls
5621 // replaceLinkHoldersText() on the link. So, the text we're
5622 // getting at this point has already been percent decoded.
5623 //
5624 // The problematic case is if %25 is in the title, since that
5625 // decodes to %, which could combine with trailing characters.
5626 // However, % is not a valid link title character, so it would
5627 // not parse as a link and the string we received here would
5628 // still contain the encoded %25.
5629 //
5630 // Hence, double decoded is not an issue. See the test,
5631 // "Should not double decode the link option"
5632 if ( str_contains( $value, '%' ) ) {
5633 $value = rawurldecode( $value );
5634 }
5635 $linkTitle = Title::newFromText( $value );
5636 if ( $linkTitle ) {
5637 $this->mOutput->addLink( $linkTitle );
5638 $type = 'link-title';
5639 $target = $linkTitle;
5640 }
5641 }
5642 return [ $type, $target ];
5643 }
5644
5652 public function modifyImageHtml( File $file, array $params, string &$html ) {
5653 $this->hookRunner->onParserModifyImageHTML( $this, $file, $params, $html );
5654 }
5655
5656 private function stripAltText( string $caption, ?LinkHolderArray $holders = null ): string {
5657 # Strip bad stuff out of the title (tooltip). We can't just use
5658 # replaceLinkHoldersText() here, because if this function is called
5659 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5660 if ( $holders !== null ) {
5661 $tooltip = $holders->replaceText( $caption );
5662 } else {
5663 $tooltip = $this->replaceLinkHoldersText( $caption );
5664 }
5665
5666 # make sure there are no placeholders in thumbnail attributes
5667 # that are later expanded to html- so expand them now and
5668 # remove the tags
5669 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5670 # Compatibility hack! In HTML certain entity references not terminated
5671 # by a semicolon are decoded (but not if we're in an attribute; that's
5672 # how link URLs get away without properly escaping & in queries).
5673 # But wikitext has always required semicolon-termination of entities,
5674 # so encode & where needed to avoid decode of semicolon-less entities.
5675 # See T209236 and
5676 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5677 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5678 $tooltip = preg_replace( "/
5679 & # 1. entity prefix
5680 (?= # 2. followed by:
5681 (?: # a. one of the legacy semicolon-less named entities
5682 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5683 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5684 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5685 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5686 U(?:acute|circ|grave|uml)|Yacute|
5687 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5688 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5689 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5690 frac(?:1(?:2|4)|34)|
5691 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5692 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5693 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5694 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5695 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5696 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5697 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5698 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5699 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5700 )
5701 (?:[^;]|$)) # b. and not followed by a semicolon
5702 # S = study, for efficiency
5703 /Sx", '&amp;', $tooltip );
5704 $tooltip = Sanitizer::stripAllTags( $tooltip );
5705
5706 return $tooltip;
5707 }
5708
5718 public function attributeStripCallback( &$text, $frame = false ) {
5719 wfDeprecated( __METHOD__, '1.35' );
5720 $text = $this->replaceVariables( $text, $frame );
5721 $text = $this->mStripState->unstripBoth( $text );
5722 return $text;
5723 }
5724
5731 public function getTags(): array {
5732 return array_keys( $this->mTagHooks );
5733 }
5734
5739 public function getFunctionSynonyms() {
5740 return $this->mFunctionSynonyms;
5741 }
5742
5747 public function getUrlProtocols() {
5748 return $this->urlUtils->validProtocols();
5749 }
5750
5781 private function extractSections( $text, $sectionId, $mode, $newText, ?PageReference $page = null ) {
5782 $magicScopeVariable = $this->lock();
5783 $this->startParse(
5784 $page,
5785 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
5786 self::OT_PLAIN,
5787 true
5788 );
5789 $outText = '';
5790 $frame = $this->getPreprocessor()->newFrame();
5791
5792 # Process section extraction flags
5793 $flags = 0;
5794 $sectionParts = explode( '-', $sectionId );
5795 // The section ID may either be a magic string such as 'new' (which should be treated as 0),
5796 // or a numbered section ID in the format of "T-<section index>".
5797 // Explicitly coerce the section index into a number accordingly. (T323373)
5798 $sectionIndex = (int)array_pop( $sectionParts );
5799 foreach ( $sectionParts as $part ) {
5800 if ( $part === 'T' ) {
5801 $flags |= Preprocessor::DOM_FOR_INCLUSION;
5802 }
5803 }
5804
5805 # Check for empty input
5806 if ( strval( $text ) === '' ) {
5807 # Only sections 0 and T-0 exist in an empty document
5808 if ( $sectionIndex === 0 ) {
5809 return $mode === 'get' ? '' : $newText;
5810 } else {
5811 return $mode === 'get' ? $newText : $text;
5812 }
5813 }
5814
5815 # Preprocess the text
5816 $root = $this->preprocessToDom( $text, $flags );
5817
5818 # <h> nodes indicate section breaks
5819 # They can only occur at the top level, so we can find them by iterating the root's children
5820 $node = $root->getFirstChild();
5821
5822 # Find the target section
5823 if ( $sectionIndex === 0 ) {
5824 # Section zero doesn't nest, level=big
5825 $targetLevel = 1000;
5826 } else {
5827 while ( $node ) {
5828 if ( $node->getName() === 'h' ) {
5829 $bits = $node->splitHeading();
5830 if ( $bits['i'] == $sectionIndex ) {
5831 $targetLevel = $bits['level'];
5832 break;
5833 }
5834 }
5835 if ( $mode === 'replace' ) {
5836 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5837 }
5838 $node = $node->getNextSibling();
5839 }
5840 }
5841
5842 if ( !$node ) {
5843 # Not found
5844 return $mode === 'get' ? $newText : $text;
5845 }
5846
5847 # Find the end of the section, including nested sections
5848 do {
5849 if ( $node->getName() === 'h' ) {
5850 $bits = $node->splitHeading();
5851 $curLevel = $bits['level'];
5852 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
5853 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5854 break;
5855 }
5856 }
5857 if ( $mode === 'get' ) {
5858 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5859 }
5860 $node = $node->getNextSibling();
5861 } while ( $node );
5862
5863 # Write out the remainder (in replace mode only)
5864 if ( $mode === 'replace' ) {
5865 # Output the replacement text
5866 # Add two newlines on -- trailing whitespace in $newText is conventionally
5867 # stripped by the editor, so we need both newlines to restore the paragraph gap
5868 # Only add trailing whitespace if there is newText
5869 if ( $newText != "" ) {
5870 $outText .= $newText . "\n\n";
5871 }
5872
5873 while ( $node ) {
5874 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5875 $node = $node->getNextSibling();
5876 }
5877 }
5878
5879 # Re-insert stripped tags
5880 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5881
5882 return $outText;
5883 }
5884
5900 public function getSection( $text, $sectionId, $defaultText = '' ) {
5901 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5902 }
5903
5917 public function replaceSection( $oldText, $sectionId, $newText ) {
5918 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5919 }
5920
5950 public function getFlatSectionInfo( $text ) {
5951 $magicScopeVariable = $this->lock();
5952 $this->startParse(
5953 null,
5954 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
5955 self::OT_PLAIN,
5956 true
5957 );
5958 $frame = $this->getPreprocessor()->newFrame();
5959 $root = $this->preprocessToDom( $text, 0 );
5960 $node = $root->getFirstChild();
5961 $offset = 0;
5962 $currentSection = [
5963 'index' => 0,
5964 'level' => 0,
5965 'offset' => 0,
5966 'heading' => '',
5967 'text' => ''
5968 ];
5969 $sections = [];
5970
5971 while ( $node ) {
5972 $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
5973 if ( $node->getName() === 'h' ) {
5974 $bits = $node->splitHeading();
5975 $sections[] = $currentSection;
5976 $currentSection = [
5977 'index' => $bits['i'],
5978 'level' => $bits['level'],
5979 'offset' => $offset,
5980 'heading' => $nodeText,
5981 'text' => $nodeText
5982 ];
5983 } else {
5984 $currentSection['text'] .= $nodeText;
5985 }
5986 $offset += strlen( $nodeText );
5987 $node = $node->getNextSibling();
5988 }
5989 $sections[] = $currentSection;
5990 return $sections;
5991 }
5992
6004 public function getRevisionId() {
6005 return $this->mRevisionId;
6006 }
6007
6014 public function getRevisionRecordObject() {
6015 if ( $this->mRevisionRecordObject ) {
6016 return $this->mRevisionRecordObject;
6017 }
6018 if ( $this->mOptions->isMessage() ) {
6019 return null;
6020 }
6021
6022 // NOTE: try to get the RevisionRecord object even if mRevisionId is null.
6023 // This is useful when parsing a revision that has not yet been saved.
6024 // However, if we get back a saved revision even though we are in
6025 // preview mode, we'll have to ignore it, see below.
6026 // NOTE: This callback may be used to inject an OLD revision that was
6027 // already loaded, so "current" is a bit of a misnomer. We can't just
6028 // skip it if mRevisionId is set.
6029 $rev = $this->mOptions->getCurrentRevisionRecordCallback()(
6030 $this->getTitle(),
6031 $this
6032 );
6033
6034 if ( !$rev ) {
6035 // The revision record callback returns `false` (not null) to
6036 // indicate that the revision is missing. (See for example
6037 // Parser::statelessFetchRevisionRecord(), the default callback.)
6038 // This API expects `null` instead. (T251952)
6039 return null;
6040 }
6041
6042 if ( $this->mRevisionId === null && $rev->getId() ) {
6043 // We are in preview mode (mRevisionId is null), and the current revision callback
6044 // returned an existing revision. Ignore it and return null, it's probably the page's
6045 // current revision, which is not what we want here. Note that we do want to call the
6046 // callback to allow the unsaved revision to be injected here, e.g. for
6047 // self-transclusion previews.
6048 return null;
6049 }
6050
6051 // If the parse is for a new revision, then the callback should have
6052 // already been set to force the object and should match mRevisionId.
6053 // If not, try to fetch by mRevisionId instead.
6054 if ( $this->mRevisionId && $rev->getId() != $this->mRevisionId ) {
6055 $rev = MediaWikiServices::getInstance()
6056 ->getRevisionLookup()
6057 ->getRevisionById( $this->mRevisionId );
6058 }
6059
6060 $this->mRevisionRecordObject = $rev;
6061
6062 return $this->mRevisionRecordObject;
6063 }
6064
6071 public function getRevisionTimestamp() {
6072 if ( $this->mRevisionTimestamp !== null ) {
6073 return $this->mRevisionTimestamp;
6074 }
6075
6076 # Use specified revision timestamp, falling back to the current timestamp
6077 $revObject = $this->getRevisionRecordObject();
6078 $timestamp = $revObject && $revObject->getTimestamp()
6079 ? $revObject->getTimestamp()
6080 : $this->mOptions->getTimestamp();
6081 $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6082
6083 # The cryptic '' timezone parameter tells to use the site-default
6084 # timezone offset instead of the user settings.
6085 # Since this value will be saved into the parser cache, served
6086 # to other users, and potentially even used inside links and such,
6087 # it needs to be consistent for all visitors.
6088 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6089
6090 return $this->mRevisionTimestamp;
6091 }
6092
6099 public function getRevisionUser(): ?string {
6100 if ( $this->mRevisionUser === null ) {
6101 $revObject = $this->getRevisionRecordObject();
6102
6103 # if this template is subst: the revision id will be blank,
6104 # so just use the current user's name
6105 if ( $revObject && $revObject->getUser() ) {
6106 $this->mRevisionUser = $revObject->getUser()->getName();
6107 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6108 $this->mRevisionUser = $this->getUserIdentity()->getName();
6109 } else {
6110 # Note that we fall through here with
6111 # $this->mRevisionUser still null
6112 }
6113 }
6114 return $this->mRevisionUser;
6115 }
6116
6123 public function getRevisionSize() {
6124 if ( $this->mRevisionSize === null ) {
6125 $revObject = $this->getRevisionRecordObject();
6126
6127 # if this variable is subst: the revision id will be blank,
6128 # so just use the parser input size, because the own substitution
6129 # will change the size.
6130 $this->mRevisionSize = $revObject ? $revObject->getSize() : $this->mInputSize;
6131 }
6132 return $this->mRevisionSize;
6133 }
6134
6135 private static function getSectionNameFromStrippedText( string $text ): string {
6136 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6137 $text = Sanitizer::decodeCharReferences( $text );
6138 $text = self::normalizeSectionName( $text );
6139 return $text;
6140 }
6141
6142 private static function makeAnchor( string $sectionName ): string {
6143 return '#' . Sanitizer::escapeIdForLink( $sectionName );
6144 }
6145
6146 private function makeLegacyAnchor( string $sectionName ): string {
6147 $fragmentMode = $this->svcOptions->get( MainConfigNames::FragmentMode );
6148 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6149 // ForAttribute() and ForLink() are the same for legacy encoding
6150 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6151 } else {
6152 $id = Sanitizer::escapeIdForLink( $sectionName );
6153 }
6154
6155 return "#$id";
6156 }
6157
6167 public function guessSectionNameFromWikiText( $text ) {
6168 # Strip out wikitext links(they break the anchor)
6169 $text = $this->stripSectionName( $text );
6170 $sectionName = self::getSectionNameFromStrippedText( $text );
6171 return self::makeAnchor( $sectionName );
6172 }
6173
6185 public function guessLegacySectionNameFromWikiText( $text ) {
6186 wfDeprecated( __METHOD__, '1.45' );
6187 # Strip out wikitext links(they break the anchor)
6188 $text = $this->stripSectionName( $text );
6189 $sectionName = self::getSectionNameFromStrippedText( $text );
6190 return $this->makeLegacyAnchor( $sectionName );
6191 }
6192
6199 public static function guessSectionNameFromStrippedText( $text ) {
6200 $sectionName = self::getSectionNameFromStrippedText( $text );
6201 return self::makeAnchor( $sectionName );
6202 }
6203
6210 private static function normalizeSectionName( $text ) {
6211 # T90902: ensure the same normalization is applied for IDs as to links
6212 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6213 try {
6214 $parts = $titleParser->splitTitleString( "#$text" );
6215 } catch ( MalformedTitleException ) {
6216 return $text;
6217 }
6218 return $parts['fragment'];
6219 }
6220
6236 public function stripSectionName( $text ) {
6237 # Strip internal link markup
6238 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6239 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6240
6241 # Strip external link markup
6242 # @todo FIXME: Not tolerant to blank link text
6243 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6244 # on how many empty links there are on the page - need to figure that out.
6245 $text = preg_replace(
6246 '/\[(?i:' . $this->urlUtils->validProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6247
6248 # Parse wikitext quotes (italics & bold)
6249 $text = $this->doQuotes( $text );
6250
6251 # Strip HTML tags
6252 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6253 return $text;
6254 }
6255
6274 public function markerSkipCallback( $s, callable $callback ) {
6275 $i = 0;
6276 $out = '';
6277 while ( $i < strlen( $s ) ) {
6278 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6279 if ( $markerStart === false ) {
6280 $out .= $callback( substr( $s, $i ) );
6281 break;
6282 } else {
6283 $out .= $callback( substr( $s, $i, $markerStart - $i ) );
6284 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6285 if ( $markerEnd === false ) {
6286 $out .= substr( $s, $markerStart );
6287 break;
6288 } else {
6289 $markerEnd += strlen( self::MARKER_SUFFIX );
6290 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6291 $i = $markerEnd;
6292 }
6293 }
6294 }
6295 return $out;
6296 }
6297
6305 public function killMarkers( $text ) {
6306 return $this->mStripState->killMarkers( $text );
6307 }
6308
6322 public function parseWidthParam( $value, $parseHeight = true, bool $localized = false ) {
6323 $parsedWidthParam = [];
6324 if ( $value === '' ) {
6325 return $parsedWidthParam;
6326 }
6327 $m = [];
6328 if ( !$localized ) {
6329 // Strip a localized 'px' suffix (T374311)
6330 $mwArray = $this->magicWordFactory->newArray( [ 'img_width' ] );
6331 [ $magicWord, $newValue ] = $mwArray->matchVariableStartToEnd( $value );
6332 $value = $magicWord ? $newValue : $value;
6333 }
6334
6335 # (T15500) In both cases (width/height and width only),
6336 # permit trailing "px" for backward compatibility.
6337 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(px)?\s*$/', $value, $m ) ) {
6338 $parsedWidthParam['width'] = intval( $m[1] );
6339 $parsedWidthParam['height'] = intval( $m[2] );
6340 if ( $m[3] ?? false ) {
6341 $this->addTrackingCategory( 'double-px-category' );
6342 }
6343 } elseif ( preg_match( '/^([0-9]*)\s*(px)?\s*$/', $value, $m ) ) {
6344 $parsedWidthParam['width'] = intval( $m[1] );
6345 if ( $m[2] ?? false ) {
6346 $this->addTrackingCategory( 'double-px-category' );
6347 }
6348 }
6349 return $parsedWidthParam;
6350 }
6351
6358 #[\NoDiscard]
6359 protected function lock(): ScopedCallback {
6360 if ( $this->mInParse ) {
6361 throw new LogicException( "Parser state cleared while parsing. "
6362 . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6363 }
6364
6365 // Save the backtrace when locking, so that if some code tries locking again,
6366 // we can print the lock owner's backtrace for easier debugging
6367 $e = new RuntimeException;
6368 $this->mInParse = $e->getTraceAsString();
6369
6370 $recursiveCheck = new ScopedCallback( function () {
6371 $this->mInParse = false;
6372 } );
6373
6374 return $recursiveCheck;
6375 }
6376
6384 public function isLocked() {
6385 return (bool)$this->mInParse;
6386 }
6387
6398 public static function stripOuterParagraph( $html ) {
6399 $m = [];
6400 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && !str_contains( $m[1], '</p>' ) ) {
6401 $html = $m[1];
6402 }
6403
6404 return $html;
6405 }
6406
6419 public static function formatPageTitle( $nsText, $nsSeparator, $mainText, ?Language $titleLang = null ): string {
6420 $html = '';
6421 if ( $nsText !== '' ) {
6422 $html .= '<span class="mw-page-title-namespace">' . HtmlArmor::getHtml( $nsText ) . '</span>';
6423 $html .= '<span class="mw-page-title-separator">' . HtmlArmor::getHtml( $nsSeparator ) . '</span>';
6424 }
6425 $html .= '<span class="mw-page-title-main">' . HtmlArmor::getHtml( $mainText ) . '</span>';
6426 if ( $titleLang !== null ) {
6427 $html = Html::rawElement( 'span', [
6428 'lang' => $titleLang->getHtmlCode(),
6429 'dir' => $titleLang->getDir(),
6430 ], $html );
6431 }
6432 return $html;
6433 }
6434
6441 public static function extractBody( string $text ): string {
6442 $posStart = strpos( $text, '<body' );
6443 if ( $posStart === false ) {
6444 return $text;
6445 }
6446 $posStart = strpos( $text, '>', $posStart );
6447 if ( $posStart === false ) {
6448 return $text;
6449 }
6450 // Skip past the > character
6451 $posStart += 1;
6452 $posEnd = strrpos( $text, '</body>', $posStart );
6453 if ( $posEnd === false ) {
6454 // Strip <body> wrapper even if input was truncated (i.e. missing close tag)
6455 return substr( $text, $posStart );
6456 } else {
6457 return substr( $text, $posStart, $posEnd - $posStart );
6458 }
6459 }
6460
6468 public function enableOOUI() {
6469 wfDeprecated( __METHOD__, '1.35' );
6470 OutputPage::setupOOUI();
6471 $this->mOutput->setEnableOOUI( true );
6472 }
6473
6480 private function setOutputFlag( ParserOutputFlags|string $flag, string $reason ): void {
6481 $this->mOutput->setOutputFlag( $flag );
6482 if ( $flag instanceof ParserOutputFlags ) {
6483 // Convert enumeration to string for logging.
6484 $flag = $flag->value;
6485 }
6486 $name = $this->getTitle()->getPrefixedText();
6487 $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6488 }
6489}
6490
6492class_alias( Parser::class, 'Parser' );
const OT_WIKI
Definition Defines.php:172
const NS_FILE
Definition Defines.php:57
const NS_MEDIAWIKI
Definition Defines.php:59
const NS_TEMPLATE
Definition Defines.php:61
const NS_SPECIAL
Definition Defines.php:40
const OT_PLAIN
Definition Defines.php:174
const OT_PREPROCESS
Definition Defines.php:173
const OT_HTML
Definition Defines.php:171
const NS_MEDIA
Definition Defines.php:39
const NS_CATEGORY
Definition Defines.php:65
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfGetUrlUtils()
wfHostname()
Get host name of the current machine, for use in error reporting.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
This class performs some operations related to tracking categories, such as adding a tracking categor...
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Content object implementation for representing flat text.
Group all the pieces relevant to the context of a request into one instance.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:79
Class for exceptions thrown by ImageGalleryBase::factory().
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
onBeforeParserFetchTemplateRevisionRecord(?LinkTarget $contextTitle, LinkTarget $title, bool &$skip, ?RevisionRecord &$revRecord)
This hook is called before a template is fetched by Parser.It allows redirection of the title and/or ...
Static utilities for manipulating HTML strings.
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Factory creating MWHttpRequest objects.
Methods for dealing with language codes.
Base class for language-specific code.
Definition Language.php:68
formatNum( $number)
Normally we output all numbers in plain en_US style, that is 293,291.235 for two hundred ninety-three...
Variant of the Message class.
Factory to create LinkRender objects.
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:47
A class containing constants representing the names of configuration variables.
const EnableParserLimitReporting
Name constant for the EnableParserLimitReporting setting, for use with Config::get()
const MaxSigChars
Name constant for the MaxSigChars setting, for use with Config::get()
const ServerName
Name constant for the ServerName setting, for use with Config::get()
const ParserEnableUserLanguage
Name constant for the ParserEnableUserLanguage setting, for use with Config::get()
const AllowSlowParserFunctions
Name constant for the AllowSlowParserFunctions setting, for use with Config::get()
const AllowDisplayTitle
Name constant for the AllowDisplayTitle setting, for use with Config::get()
const StylePath
Name constant for the StylePath setting, for use with Config::get()
const MaxTocLevel
Name constant for the MaxTocLevel setting, for use with Config::get()
const Localtimezone
Name constant for the Localtimezone setting, for use with Config::get()
const Server
Name constant for the Server setting, for use with Config::get()
const FragmentMode
Name constant for the FragmentMode setting, for use with Config::get()
const MaxArticleSize
Name constant for the MaxArticleSize setting, for use with Config::get()
const EnableScaryTranscluding
Name constant for the EnableScaryTranscluding setting, for use with Config::get()
const TranscludeCacheExpiry
Name constant for the TranscludeCacheExpiry setting, for use with Config::get()
const Sitename
Name constant for the Sitename setting, for use with Config::get()
const ArticlePath
Name constant for the ArticlePath setting, for use with Config::get()
const ScriptPath
Name constant for the ScriptPath setting, for use with Config::get()
const SignatureValidation
Name constant for the SignatureValidation setting, for use with Config::get()
const MiserMode
Name constant for the MiserMode setting, for use with Config::get()
const RawHtml
Name constant for the RawHtml setting, for use with Config::get()
const PreprocessorCacheThreshold
Name constant for the PreprocessorCacheThreshold setting, for use with Config::get()
const ExtraInterlanguageLinkPrefixes
Name constant for the ExtraInterlanguageLinkPrefixes setting, for use with Config::get()
const ShowHostnames
Name constant for the ShowHostnames setting, for use with Config::get()
Service locator for MediaWiki core services.
Base media handler class.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
This is one of the Core classes and should be read at least once by any new developers.
Helper class for mapping page value objects to a string key.
static register(Parser $parser, ServiceOptions $options)
Class for handling an array of magic words.
Store information about magic words, and create/cache MagicWord objects.
static int $inParserFactory
Track calls to Parser constructor to aid in deprecation of direct Parser invocation.
Set options of the Parser.
getMaxIncludeSize()
Maximum size of template expansions, in bytes.
getDisableTitleConversion()
Whether title conversion should be disabled.
getExpensiveParserFunctionLimit()
Maximum number of calls per parse to expensive parser functions.
getMaxPPExpandDepth()
Maximum recursion depth in PPFrame::expand()
getPreSaveTransform()
Transform wiki markup when saving the page?
getMaxPPNodeCount()
Maximum number of nodes touched by PPFrame::expand()
ParserOutput is a rendering of a Content object or a message.
setLimitReportData( $key, $value)
Sets parser limit report data for a key.
getTimeProfile(string $clock)
Returns the time that elapsed between the most recent call to resetParseStartTime() and the first cal...
hasReducedExpiry()
Check whether the cache TTL was lowered from the site default.
getCacheExpiry()
Returns the number of seconds after which this object should expire.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:135
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:6185
$mExpensiveFunctionCount
Number of expensive parser function calls.
Definition Parser.php:277
static localizeTOC(?TOCData $tocData, Language $lang, ?ILanguageConverter $converter)
Localize the TOC into the given target language; this includes invoking the language converter on the...
Definition Parser.php:4528
callParserFunction(PPFrame $frame, $function, array $args=[], bool $inSolState=false)
Call a parser function and return an array with text and flags.
Definition Parser.php:3328
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1550
setOutputType( $ot)
Mutator for the output type.
Definition Parser.php:1011
getParseTime()
Return the parse time.
Definition Parser.php:561
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1151
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6236
limitationWarn( $limitationType, $current='', $max='')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
Definition Parser.php:2886
makeImage(LinkTarget $link, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5347
const OT_PLAIN
Output type: like Parser::extractSections() - portions of the original are returned unchanged.
Definition Parser.php:182
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:6199
static statelessFetchTemplate( $page, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition Parser.php:3584
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6274
getPreloadText( $text, PageReference $page, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:909
getTemplateDom(LinkTarget $title, bool $inSolState=false)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition Parser.php:3423
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1116
parseExtensionTagAsTopLevelDoc(string $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition Parser.php:843
setStripExtTags(bool $val)
Definition Parser.php:2855
static formatPageTitle( $nsText, $nsSeparator, $mainText, ?Language $titleLang=null)
Add HTML tags marking the parts of a page title, to be displayed in the first heading of the page.
Definition Parser.php:6419
parse( $text, PageReference $page, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition Parser.php:592
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:3893
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1131
lock()
Lock the current instance of the parser.
Definition Parser.php:6359
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5015
const EXT_LINK_URL_CLASS
Everything except bracket, space, or control characters.
Definition Parser.php:150
preprocess( $text, ?PageReference $page, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition Parser.php:862
firstCallInit()
Used to do various kinds of initialisation on the first call of the parser.
Definition Parser.php:492
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6167
getUserSig(UserIdentity $user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext.
Definition Parser.php:4690
replaceVariables( $text, $frame=false, $argsOnly=false, array $options=[])
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2820
interwikiTransclude(LinkTarget $link, $action)
Transclude an interwiki link.
Definition Parser.php:3785
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4757
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3503
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6004
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5100
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3856
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5917
transformMsg( $text, ParserOptions $options, ?PageReference $page=null)
Wrapper for preprocess()
Definition Parser.php:4910
insertStripItem( $text)
Add an item to the strip state Returns the unique tag which must be inserted into the stripped text T...
Definition Parser.php:1258
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1481
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2217
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:2158
replaceLinkHolders(&$text)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition Parser.php:5060
static extractTagsAndParams(array $elements, $text, &$matches)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition Parser.php:1174
static statelessFetchRevisionRecord(LinkTarget $link, $parser=null)
Wrapper around RevisionLookup::getKnownCurrentRevision.
Definition Parser.php:3519
getHookRunner()
Get a HookRunner for calling core hooks.
Definition Parser.php:1584
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1141
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2184
__construct(private ServiceOptions $svcOptions, private MagicWordFactory $magicWordFactory, private Language $contLang, private UrlUtils $urlUtils, private SpecialPageFactory $specialPageFactory, private LinkRendererFactory $linkRendererFactory, private NamespaceInfo $nsInfo, private LoggerInterface $logger, private BadFileLookup $badFileLookup, private LanguageConverterFactory $languageConverterFactory, private LanguageNameUtils $languageNameUtils, private HookContainer $hookContainer, private TidyDriverBase $tidy, private WANObjectCache $wanCache, private UserOptionsLookup $userOptionsLookup, private UserFactory $userFactory, private TitleFormatter $titleFormatter, private HttpRequestFactory $httpRequestFactory, private TrackingCategories $trackingCategories, private SignatureValidatorFactory $signatureValidatorFactory, private UserNameUtils $userNameUtils,)
Constructing parsers directly is not allowed! Use a ParserFactory.
Definition Parser.php:389
parseWidthParam( $value, $parseHeight=true, bool $localized=false)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6322
setPage(?PageReference $t=null)
Set the page used as context for parsing, e.g.
Definition Parser.php:961
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition Parser.php:1049
preSaveTransform( $text, PageReference $page, UserIdentity $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition Parser.php:4569
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6305
const OT_PREPROCESS
Output type: like Parser::preprocess()
Definition Parser.php:177
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4772
isLocked()
Will entry points such as parse() throw an exception due to the parser already being active?
Definition Parser.php:6384
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6099
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition Parser.php:5950
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1077
clearState()
Clear Parser state.
Definition Parser.php:504
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:5049
msg(string $msg,... $params)
Helper function to correctly set the target language and title of a message based on the parser conte...
Definition Parser.php:4134
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition Parser.php:6468
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:2911
getUserIdentity()
Get a user either from the user set on Parser if it's set, or from the ParserOptions object otherwise...
Definition Parser.php:1096
makeImageHtml(LinkTarget $link, string $options)
Parse image options text and use it to make an image.
Definition Parser.php:5327
makeLimitReport(ParserOptions $parserOptions, ParserOutput $parserOutput)
Set the limit report data in the current ParserOutput.
Definition Parser.php:690
setUser(?UserIdentity $user)
Set the current user.
Definition Parser.php:931
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition Parser.php:1572
getOutputType()
Accessor for the output type.
Definition Parser.php:1002
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:889
setTitle(?Title $t=null)
Set the context title.
Definition Parser.php:942
getRevisionSize()
Get the size of the revision.
Definition Parser.php:6123
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:1106
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1237
extensionSubstitution(array $params, PPFrame $frame, bool $processNowiki=false)
Return the text to be used for a given extension tag.
Definition Parser.php:3917
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:4944
preprocessToDom( $text, $flags=0)
Get the document object model for the given wikitext.
Definition Parser.php:2789
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5900
const OT_WIKI
Output type: like Parser::preSaveTransform()
Definition Parser.php:175
fetchTemplateAndTitle(LinkTarget $link)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3542
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6398
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6014
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:4962
modifyImageHtml(File $file, array $params, string &$html)
Give hooks a chance to modify image thumbnail HTML.
Definition Parser.php:5652
static extractBody(string $text)
Strip everything but the <body> from the provided string.
Definition Parser.php:6441
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6071
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:476
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4814
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition Parser.php:5718
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:461
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:794
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1888
static replaceTableOfContentsMarker( $text, $toc)
Replace table of contents marker in parsed HTML.
Definition Parser.php:4835
const OT_HTML
Output type: like Parser::parse()
Definition Parser.php:173
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:818
fetchFileNoRegister(LinkTarget $link, array $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3761
getPage()
Returns the page used as context for parsing, e.g.
Definition Parser.php:984
fetchFileAndTitle(LinkTarget $link, array $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3734
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3474
startExternalParse(?PageReference $page, 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 ...
Definition Parser.php:4875
resetOutput()
Reset the ParserOutput.
Definition Parser.php:549
Differences from DOM schema:
const DOM_FOR_INCLUSION
Transclusion mode flag for Preprocessor::preprocessToObj()
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:32
static removeSomeTags(string $text, array $options=[])
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments; the result will alw...
Arbitrary section name based PHP profiling.
WebRequest clone which takes values from a provided array.
Exception representing a failure to look up a revision.
Page revision base class.
Value object representing a content slot associated with a page revision.
Factory for handling the special page list and generating SpecialPage objects.
Parent class for all special pages.
Base class for HTML cleanup utilities.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
A title formatter service for MediaWiki.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Provides access to user options.
Create User objects.
UserNameUtils service.
User class for the MediaWiki software.
Definition User.php:130
Library for creating and parsing MW-style timestamps.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Module of static functions for generating XML.
Definition Xml.php:19
Maintenance script that protects or unprotects a page.
Definition protect.php:23
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Store key-value entries in a size-limited in-memory LRU cache.
Value object representing a message parameter with one of the types from {.
Multi-datacenter aware caching interface.
A collection of static methods to play with strings.
return[ 'config-schema-inverse'=>['default'=>['ConfigRegistry'=>['main'=> 'MediaWiki\\Config\\GlobalVarConfig::newInstance',], 'Sitename'=> 'MediaWiki', 'Server'=> false, 'CanonicalServer'=> false, 'ServerName'=> false, 'AssumeProxiesUseDefaultProtocolPorts'=> true, 'HttpsPort'=> 443, 'ForceHTTPS'=> false, 'ScriptPath'=> '/wiki', 'UsePathInfo'=> null, 'Script'=> false, 'LoadScript'=> false, 'RestPath'=> false, 'StylePath'=> false, 'LocalStylePath'=> false, 'ExtensionAssetsPath'=> false, 'ExtensionDirectory'=> null, 'StyleDirectory'=> null, 'ArticlePath'=> false, 'UploadPath'=> false, 'ImgAuthPath'=> false, 'ThumbPath'=> false, 'UploadDirectory'=> false, 'FileCacheDirectory'=> false, 'Logo'=> false, 'Logos'=> false, 'Favicon'=> '/favicon.ico', 'AppleTouchIcon'=> false, 'ReferrerPolicy'=> false, 'TmpDirectory'=> false, 'UploadBaseUrl'=> '', 'UploadStashScalerBaseUrl'=> false, 'ActionPaths'=>[], 'MainPageIsDomainRoot'=> false, 'EnableUploads'=> false, 'UploadStashMaxAge'=> 21600, 'EnableAsyncUploads'=> false, 'EnableAsyncUploadsByURL'=> false, 'UploadMaintenance'=> false, 'IllegalFileChars'=> ':\\/\\\\', 'DeletedDirectory'=> false, 'ImgAuthDetails'=> false, 'ImgAuthUrlPathMap'=>[], 'LocalFileRepo'=>['class'=> 'MediaWiki\\FileRepo\\LocalRepo', 'name'=> 'local', 'directory'=> null, 'scriptDirUrl'=> null, 'favicon'=> null, 'url'=> null, 'hashLevels'=> null, 'thumbScriptUrl'=> null, 'transformVia404'=> null, 'deletedDir'=> null, 'deletedHashLevels'=> null, 'updateCompatibleMetadata'=> null, 'reserializeMetadata'=> null,], 'ForeignFileRepos'=>[], 'UseInstantCommons'=> false, 'UseSharedUploads'=> false, 'SharedUploadDirectory'=> null, 'SharedUploadPath'=> null, 'HashedSharedUploadDirectory'=> true, 'RepositoryBaseUrl'=> 'https:'FetchCommonsDescriptions'=> false, 'SharedUploadDBname'=> false, 'SharedUploadDBprefix'=> '', 'CacheSharedUploads'=> true, 'ForeignUploadTargets'=>['local',], 'UploadDialog'=>['fields'=>['description'=> true, 'date'=> false, 'categories'=> false,], 'licensemessages'=>['local'=> 'generic-local', 'foreign'=> 'generic-foreign',], 'comment'=>['local'=> '', 'foreign'=> '',], 'format'=>['filepage'=> ' $DESCRIPTION', 'description'=> ' $TEXT', 'ownwork'=> '', 'license'=> '', 'uncategorized'=> '',],], 'FileBackends'=>[], 'LockManagers'=>[], 'ShowEXIF'=> null, 'UpdateCompatibleMetadata'=> false, 'AllowCopyUploads'=> false, 'CopyUploadsDomains'=>[], 'CopyUploadsFromSpecialUpload'=> false, 'CopyUploadProxy'=> false, 'CopyUploadTimeout'=> false, 'CopyUploadAllowOnWikiDomainConfig'=> false, 'MaxUploadSize'=> 104857600, 'MinUploadChunkSize'=> 1024, 'UploadNavigationUrl'=> false, 'UploadMissingFileUrl'=> false, 'ThumbnailScriptPath'=> false, 'SharedThumbnailScriptPath'=> false, 'HashedUploadDirectory'=> true, 'CSPUploadEntryPoint'=> true, 'FileExtensions'=>['png', 'gif', 'jpg', 'jpeg', 'webp',], 'ProhibitedFileExtensions'=>['html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', 'phar', 'shtml', 'jhtml', 'pl', 'py', 'cgi', 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl', 'xml',], 'MimeTypeExclusions'=>['text/html', 'application/javascript', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', 'application/x-php', 'text/x-php', 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', 'text/scriptlet', 'application/x-msdownload', 'application/x-msmetafile', 'application/java', 'application/xml', 'text/xml',], 'CheckFileExtensions'=> true, 'StrictFileExtensions'=> true, 'DisableUploadScriptChecks'=> false, 'UploadSizeWarning'=> false, 'TrustedMediaFormats'=>['BITMAP', 'AUDIO', 'VIDEO', 'image/svg+xml', 'application/pdf',], 'MediaHandlers'=>[], 'NativeImageLazyLoading'=> false, 'ParserTestMediaHandlers'=>['image/jpeg'=> 'MockBitmapHandler', 'image/png'=> 'MockBitmapHandler', 'image/gif'=> 'MockBitmapHandler', 'image/tiff'=> 'MockBitmapHandler', 'image/webp'=> 'MockBitmapHandler', 'image/x-ms-bmp'=> 'MockBitmapHandler', 'image/x-bmp'=> 'MockBitmapHandler', 'image/x-xcf'=> 'MockBitmapHandler', 'image/svg+xml'=> 'MockSvgHandler', 'image/vnd.djvu'=> 'MockDjVuHandler',], 'UseImageResize'=> true, 'UseImageMagick'=> false, 'ImageMagickConvertCommand'=> '/usr/bin/convert', 'MaxInterlacingAreas'=>[], 'SharpenParameter'=> '0x0.4', 'SharpenReductionThreshold'=> 0.85, 'ImageMagickTempDir'=> false, 'CustomConvertCommand'=> false, 'JpegTran'=> '/usr/bin/jpegtran', 'JpegPixelFormat'=> 'yuv420', 'JpegQuality'=> 80, 'Exiv2Command'=> '/usr/bin/exiv2', 'Exiftool'=> '/usr/bin/exiftool', 'SVGConverters'=>['ImageMagick'=> ' $path/convert -background "#ffffff00" -thumbnail $widthx$height\\! $input PNG:$output', 'inkscape'=> ' $path/inkscape -w $width -o $output $input', 'batik'=> 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg'=> ' $path/rsvg-convert -w $width -h $height -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'TiffThumbnailType'=>[], 'ThumbnailEpoch'=> '20030516000000', 'AttemptFailureEpoch'=> 1, 'IgnoreImageErrors'=> false, 'GenerateThumbnailOnParse'=> true, 'ShowArchiveThumbnails'=> true, 'EnableAutoRotation'=> null, 'Antivirus'=> null, 'AntivirusSetup'=>['clamav'=>['command'=> 'clamscan --no-summary ', 'codemap'=>[0=> 0, 1=> 1, 52=> -1, ' *'=> false,], 'messagepattern'=> '/.*?:(.*)/sim',],], 'AntivirusRequired'=> true, 'VerifyMimeType'=> true, 'MimeTypeFile'=> 'internal', 'MimeInfoFile'=> 'internal', 'MimeDetectorCommand'=> null, 'TrivialMimeDetection'=> false, 'XMLMimeTypes'=>['http:'svg'=> 'image/svg+xml', 'http:'http:'html'=> 'text/html',], 'ImageLimits'=>[[320, 240,], [640, 480,], [800, 600,], [1024, 768,], [1280, 1024,], [2560, 2048,],], 'ThumbLimits'=>[120, 150, 180, 200, 250, 300,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailStepsRatio'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'UserEmailConfirmationUseHTML'=> false, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ImageLinksSchemaMigrationStage'=> 769, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'UseLegacyMediaStyles' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'default' => true, ], 'ChangeCredentialsBlacklist' => [ 'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest', ], 'RemoveCredentialsBlacklist' => [ 'MediaWiki\\Auth\\PasswordAuthenticationRequest', ], 'InvalidPasswordReset' => true, 'PasswordDefault' => 'pbkdf2', 'PasswordConfig' => [ 'A' => [ 'class' => 'MediaWiki\\Password\\MWOldPassword', ], 'B' => [ 'class' => 'MediaWiki\\Password\\MWSaltedPassword', ], 'pbkdf2-legacyA' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'A', 'pbkdf2', ], ], 'pbkdf2-legacyB' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'B', 'pbkdf2', ], ], 'bcrypt' => [ 'class' => 'MediaWiki\\Password\\BcryptPassword', 'cost' => 9, ], 'pbkdf2' => [ 'class' => 'MediaWiki\\Password\\Pbkdf2PasswordUsingOpenSSL', 'algo' => 'sha512', 'cost' => '30000', 'length' => '64', ], 'argon2' => [ 'class' => 'MediaWiki\\Password\\Argon2Password', 'algo' => 'auto', ], ], 'PasswordResetRoutes' => [ 'username' => true, 'email' => true, ], 'MaxSigChars' => 255, 'SignatureValidation' => 'warning', 'SignatureAllowedLintErrors' => [ 'obsolete-tag', ], 'MaxNameChars' => 255, 'ReservedUsernames' => [ 'MediaWiki default', 'Conversion script', 'Maintenance script', 'Template namespace initialisation script', 'ScriptImporter', 'Delete page script', 'Move page script', 'Command line script', 'Unknown user', 'msg:double-redirect-fixer', 'msg:usermessage-editor', 'msg:proxyblocker', 'msg:sorbs', 'msg:spambot_username', 'msg:autochange-username', ], 'DefaultUserOptions' => [ 'ccmeonemails' => 0, 'date' => 'default', 'diffonly' => 0, 'diff-type' => 'table', 'disablemail' => 0, 'editfont' => 'monospace', 'editondblclick' => 0, 'editrecovery' => 0, 'editsectiononrightclick' => 0, 'email-allow-new-users' => 1, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, 'enotifwatchlistpages' => 1, 'extendwatchlist' => 1, 'fancysig' => 0, 'forceeditsummary' => 0, 'forcesafemode' => 0, 'gender' => 'unknown', 'hidecategorization' => 1, 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', 'norollbackdiff' => 0, 'prefershttps' => 1, 'previewonfirst' => 0, 'previewontop' => 1, 'pst-cssjs' => 1, 'rcdays' => 7, 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'requireemail' => 0, 'search-match-redirect' => true, 'search-special-page' => 'Search', 'search-thumbnail-extra-namespaces' => true, 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, 'showrollbackconfirmation' => 0, 'skin' => false, 'skin-responsive' => 1, 'thumbsize' => 5, 'underline' => 2, 'useeditwarning' => 1, 'uselivepreview' => 0, 'usenewrc' => 1, 'watchcreations' => 1, 'watchcreations-expiry' => 'infinite', 'watchdefault' => 1, 'watchdefault-expiry' => 'infinite', 'watchdeletion' => 0, 'watchlistdays' => 7, 'watchlisthideanons' => 0, 'watchlisthidebots' => 0, 'watchlisthidecategorization' => 1, 'watchlisthideliu' => 0, 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlistreloadautomatically' => 0, 'watchlistunwatchlinks' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'watchuploads' => 1, 'watchrollback-expiry' => 'infinite', 'watchstar-expiry' => 'infinite', 'wlenhancedfilters-disable' => 0, 'wllimit' => 250, ], 'ConditionalUserOptions' => [ ], 'HiddenPrefs' => [ ], 'UserJsPrefLimit' => 100, 'InvalidUsernameCharacters' => '@:>=', 'UserrightsInterwikiDelimiter' => '@', 'SecureLogin' => false, 'AuthenticationTokenVersion' => null, 'SessionProviders' => [ 'MediaWiki\\Session\\CookieSessionProvider' => [ 'class' => 'MediaWiki\\Session\\CookieSessionProvider', 'args' => [ [ 'priority' => 30, ], ], 'services' => [ 'JwtCodec', 'UrlUtils', ], ], 'MediaWiki\\Session\\BotPasswordSessionProvider' => [ 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => [ [ 'priority' => 75, ], ], 'services' => [ 'GrantsInfo', ], ], ], 'AutoCreateTempUser' => [ 'known' => false, 'enabled' => false, 'actions' => [ 'edit', ], 'genPattern' => '~$1', 'matchPattern' => null, 'reservedPattern' => '~$1', 'serialProvider' => [ 'type' => 'local', 'useYear' => true, ], 'serialMapping' => [ 'type' => 'readable-numeric', ], 'expireAfterDays' => 90, 'notifyBeforeExpirationDays' => 10, ], 'AutoblockExemptions' => [ ], 'AutoblockExpiry' => 86400, 'BlockAllowsUTEdit' => true, 'BlockCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 19, ], 'BlockDisablesLogin' => false, 'EnableMultiBlocks' => false, 'WhitelistRead' => false, 'WhitelistReadRegexp' => false, 'EmailConfirmToEdit' => false, 'HideIdentifiableRedirects' => true, 'GroupPermissions' => [ '*' => [ 'createaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', 'editviewmywatchlist' => 'watchlist-interaction', 'editmycssjs' => 'customization', 'editmyoptions' => 'customization', 'editinterface' => 'administration', 'editsiteconfig' => 'administration', 'rollback' => 'administration', 'blockusers' => 'administration', 'delete' => 'administration', 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', 'oversight' => 'administration', 'createaccount' => 'administration', 'mergehistory' => 'administration', 'import' => 'administration', 'highvolume' => 'high-volume', 'privateinfo' => 'private-information', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editviewmywatchlist' => 'low', 'editmycssjs' => 'security', 'editmyoptions' => 'security', 'editinterface' => 'vandalism', 'editsiteconfig' => 'security', 'rollback' => 'low', 'blockusers' => 'vandalism', 'delete' => 'vandalism', 'viewdeleted' => 'vandalism', 'viewrestrictedlogs' => 'security', 'protect' => 'vandalism', 'oversight' => 'security', 'createaccount' => 'low', 'mergehistory' => 'vandalism', 'import' => 'security', 'highvolume' => 'low', 'privateinfo' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'CachePrefix' => false, 'DebugToolbar' => false, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCEngines' => [ 'redis' => 'MediaWiki\\RCFeed\\RedisPubSubFeedEngine', 'udp' => 'MediaWiki\\RCFeed\\UDPRCFeedEngine', ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'cdnPurge' => 'MediaWiki\\JobQueue\\Jobs\\CdnPurgeJob', 'userGroupExpiry' => 'MediaWiki\\User\\UserGroupExpiryJob', 'clearWatchlistNotifications' => 'MediaWiki\\Watchlist\\ClearWatchlistNotificationsJob', 'userOptionsUpdate' => 'MediaWiki\\User\\Options\\UserOptionsUpdateJob', 'revertedTagUpdate' => 'MediaWiki\\JobQueue\\Jobs\\RevertedTagUpdateJob', 'null' => 'MediaWiki\\JobQueue\\Jobs\\NullJob', 'userEditCountInit' => 'MediaWiki\\User\\UserEditCountInitJob', 'parsoidCachePrewarm' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\ParsoidCachePrewarmJob', 'services' => [ 'ParserOutputAccess', 'PageStore', 'RevisionLookup', 'ParsoidSiteConfig', ], 'needsPage' => false, ], 'renameUserTable' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], 'renameUserDerived' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserDerivedJob', 'services' => [ 'RenameUserFactory', 'UserFactory', ], ], 'renameUser' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], ], 'JobTypesExcludedFromDefaultQueue' => [ 'AssembleUploadChunks', 'PublishStashedFile', 'UploadFromUrl', ], 'JobBackoffThrottling' => [ ], 'JobTypeConf' => [ 'default' => [ 'class' => 'MediaWiki\\JobQueue\\JobQueueDB', 'order' => 'random', 'claimTTL' => 3600, ], ], 'JobQueueIncludeInMaxLagFactor' => false, 'SpecialPageCacheUpdates' => [ 'Statistics' => [ 'MediaWiki\\Deferred\\SiteStatsUpdate', 'cacheUpdate', ], ], 'PagePropLinkInvalidations' => [ 'hiddencat' => 'categorylinks', ], 'CategoryMagicGallery' => true, 'CategoryPagingLimit' => 200, 'CategoryCollation' => 'uppercase', 'TempCategoryCollations' => [ ], 'SortedCategories' => false, 'TrackingCategories' => [ ], 'LogTypes' => [ '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'import', 'interwiki', 'patrol', 'merge', 'suppress', 'tag', 'managetags', 'contentmodel', 'renameuser', ], 'LogRestrictions' => [ 'suppress' => 'suppressionlog', ], 'FilterLogTypes' => [ 'patrol' => true, 'tag' => true, 'newusers' => false, ], 'LogNames' => [ '' => 'all-logs-page', 'block' => 'blocklogpage', 'protect' => 'protectlogpage', 'rights' => 'rightslog', 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', ], 'LogHeaders' => [ '' => 'alllogstext', 'block' => 'blocklogtext', 'delete' => 'dellogpagetext', 'import' => 'importlogpagetext', 'merge' => 'mergelogpagetext', 'move' => 'movelogpagetext', 'patrol' => 'patrol-log-header', 'protect' => 'protectlogtext', 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', 'upload' => 'uploadlogpagetext', ], 'LogActions' => [ ], 'LogActionsHandlers' => [ 'block/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/unblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'contentmodel/change' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'contentmodel/new' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'delete/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir2' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/restore' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'import/interwiki' => 'MediaWiki\\Logging\\ImportLogFormatter', 'import/upload' => 'MediaWiki\\Logging\\ImportLogFormatter', 'interwiki/iw_add' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_delete' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_edit' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'managetags/activate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/create' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/deactivate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/delete' => 'MediaWiki\\Logging\\LogFormatter', 'merge/merge' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'merge/merge-into' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move_redir' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'patrol/patrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'patrol/autopatrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'protect/modify' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/move_prot' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/protect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/unprotect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'renameuser/renameuser' => [ 'class' => 'MediaWiki\\Logging\\RenameuserLogFormatter', 'services' => [ 'TitleParser', ], ], 'rights/autopromote' => 'MediaWiki\\Logging\\RightsLogFormatter', 'rights/rights' => 'MediaWiki\\Logging\\RightsLogFormatter', 'suppress/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'tag/update' => 'MediaWiki\\Logging\\TagLogFormatter', 'upload/overwrite' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/revert' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/upload' => 'MediaWiki\\Logging\\UploadLogFormatter', ], 'ActionFilteredLogs' => [ 'block' => [ 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], 'unblock' => [ 'unblock', ], ], 'contentmodel' => [ 'change' => [ 'change', ], 'new' => [ 'new', ], ], 'delete' => [ 'delete' => [ 'delete', ], 'delete_redir' => [ 'delete_redir', 'delete_redir2', ], 'restore' => [ 'restore', ], 'event' => [ 'event', ], 'revision' => [ 'revision', ], ], 'import' => [ 'interwiki' => [ 'interwiki', ], 'upload' => [ 'upload', ], ], 'managetags' => [ 'create' => [ 'create', ], 'delete' => [ 'delete', ], 'activate' => [ 'activate', ], 'deactivate' => [ 'deactivate', ], ], 'move' => [ 'move' => [ 'move', ], 'move_redir' => [ 'move_redir', ], ], 'newusers' => [ 'create' => [ 'create', 'newusers', ], 'create2' => [ 'create2', ], 'autocreate' => [ 'autocreate', ], 'byemail' => [ 'byemail', ], ], 'protect' => [ 'protect' => [ 'protect', ], 'modify' => [ 'modify', ], 'unprotect' => [ 'unprotect', ], 'move_prot' => [ 'move_prot', ], ], 'rights' => [ 'rights' => [ 'rights', ], 'autopromote' => [ 'autopromote', ], ], 'suppress' => [ 'event' => [ 'event', ], 'revision' => [ 'revision', ], 'delete' => [ 'delete', ], 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], ], 'upload' => [ 'upload' => [ 'upload', ], 'overwrite' => [ 'overwrite', ], 'revert' => [ 'revert', ], ], ], 'NewUserLog' => true, 'PageCreationLog' => true, 'AllowSpecialInclusion' => true, 'DisableQueryPageUpdate' => false, 'CountCategorizedImagesAsUsed' => false, 'MaxRedirectLinksRetrieved' => 500, 'RangeContributionsCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 32, ], 'Actions' => [ ], 'DefaultRobotPolicy' => 'index,follow', 'NamespaceRobotPolicies' => [ ], 'ArticleRobotPolicies' => [ ], 'ExemptFromUserRobotsControl' => null, 'DebugAPI' => false, 'APIModules' => [ ], 'APIFormatModules' => [ ], 'APIMetaModules' => [ ], 'APIPropModules' => [ ], 'APIListModules' => [ ], 'APIMaxDBRows' => 5000, 'APIMaxResultSize' => 8388608, 'APIMaxUncachedDiffs' => 1, 'APIMaxLagThreshold' => 7, 'APICacheHelpTimeout' => 3600, 'APIUselessQueryPages' => [ 'MIMEsearch', 'LinkSearch', ], 'AjaxLicensePreview' => true, 'CrossSiteAJAXdomains' => [ ], 'CrossSiteAJAXdomainExceptions' => [ ], 'AllowedCorsHeaders' => [ 'Accept', 'Accept-Language', 'Content-Language', 'Content-Type', 'Accept-Encoding', 'DNT', 'Origin', 'User-Agent', 'Api-User-Agent', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], 'MaxShellMemory' => 307200, 'MaxShellFileSize' => 102400, 'MaxShellTime' => 180, 'MaxShellWallClockTime' => 180, 'ShellCgroup' => false, 'PhpCli' => '/usr/bin/php', 'ShellRestrictionMethod' => 'autodetect', 'ShellboxUrls' => [ 'default' => null, ], 'ShellboxSecretKey' => null, 'ShellboxShell' => '/bin/sh', 'HTTPTimeout' => 25, 'HTTPConnectTimeout' => 5.0, 'HTTPMaxTimeout' => 0, 'HTTPMaxConnectTimeout' => 0, 'HTTPImportTimeout' => 25, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'VirtualRestConfig' => [ 'paths' => [ ], 'modules' => [ ], 'global' => [ 'timeout' => 360, 'forwardCookies' => false, 'HTTPProxy' => null, ], ], 'EventRelayerConfig' => [ 'default' => [ 'class' => 'Wikimedia\\EventRelayer\\EventRelayerNull', ], ], 'Pingback' => false, 'OriginTrials' => [ ], 'ReportToExpiry' => 86400, 'ReportToEndpoints' => [ ], 'FeaturePolicyReportOnly' => [ ], 'SkinsPreferred' => [ 'vector-2022', 'vector', ], 'SpecialContributeSkinsEnabled' => [ ], 'SpecialContributeNewPageTarget' => null, 'EnableEditRecovery' => false, 'EditRecoveryExpiry' => 2592000, 'UseCodexSpecialBlock' => false, 'ShowLogoutConfirmation' => false, 'EnableProtectionIndicators' => true, 'OutputPipelineStages' => [ ], 'FeatureShutdown' => [ ], 'CloneArticleParserOutput' => true, 'UseLeximorph' => false, 'UsePostprocCache' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => false, 'ParserOptionsLogUnsafeSampleRate' => 0, ], 'type' => [ 'ConfigRegistry' => 'object', 'AssumeProxiesUseDefaultProtocolPorts' => 'boolean', 'ForceHTTPS' => 'boolean', 'ExtensionDirectory' => [ 'string', 'null', ], 'StyleDirectory' => [ 'string', 'null', ], 'UploadDirectory' => [ 'string', 'boolean', 'null', ], 'Logos' => [ 'object', 'boolean', ], 'ReferrerPolicy' => [ 'array', 'string', 'boolean', ], 'ActionPaths' => 'object', 'MainPageIsDomainRoot' => 'boolean', 'ImgAuthUrlPathMap' => 'object', 'LocalFileRepo' => 'object', 'ForeignFileRepos' => 'array', 'UseSharedUploads' => 'boolean', 'SharedUploadDirectory' => [ 'string', 'null', ], 'SharedUploadPath' => [ 'string', 'null', ], 'HashedSharedUploadDirectory' => 'boolean', 'FetchCommonsDescriptions' => 'boolean', 'SharedUploadDBname' => [ 'boolean', 'string', ], 'SharedUploadDBprefix' => 'string', 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', 'CopyUploadsDomains' => 'array', 'CopyUploadTimeout' => [ 'boolean', 'integer', ], 'SharedThumbnailScriptPath' => [ 'string', 'boolean', ], 'HashedUploadDirectory' => 'boolean', 'CSPUploadEntryPoint' => 'boolean', 'FileExtensions' => 'array', 'ProhibitedFileExtensions' => 'array', 'MimeTypeExclusions' => 'array', 'TrustedMediaFormats' => 'array', 'MediaHandlers' => 'object', 'NativeImageLazyLoading' => 'boolean', 'ParserTestMediaHandlers' => 'object', 'MaxInterlacingAreas' => 'object', 'SVGConverters' => 'object', 'SVGNativeRendering' => [ 'string', 'boolean', ], 'MaxImageArea' => [ 'string', 'integer', 'boolean', ], 'TiffThumbnailType' => 'array', 'GenerateThumbnailOnParse' => 'boolean', 'EnableAutoRotation' => [ 'boolean', 'null', ], 'Antivirus' => [ 'string', 'null', ], 'AntivirusSetup' => 'object', 'MimeDetectorCommand' => [ 'string', 'null', ], 'XMLMimeTypes' => 'object', 'ImageLimits' => 'array', 'ThumbLimits' => 'array', 'ThumbnailNamespaces' => 'array', 'ThumbnailSteps' => [ 'array', 'null', ], 'ThumbnailStepsRatio' => [ 'number', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'UserEmailConfirmationUseHTML' => 'boolean', 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ImageLinksSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'PHPSessionHandling' => 'string', 'SuspiciousIpExpiry' => [ 'integer', 'boolean', ], 'MemCachedServers' => 'array', 'LocalisationCacheConf' => 'object', 'ExtensionInfoMTime' => [ 'integer', 'boolean', ], 'CdnServers' => 'object', 'CdnServersNoPurge' => 'object', 'HTCPRouting' => 'object', 'GrammarForms' => 'object', 'ExtraInterlanguageLinkPrefixes' => 'array', 'InterlanguageLinkCodeMap' => 'object', 'ExtraLanguageNames' => 'object', 'ExtraLanguageCodes' => 'object', 'DummyLanguageCodes' => 'object', 'DisabledVariants' => 'object', 'ForceUIMsgAsContentMsg' => 'object', 'RawHtmlMessages' => 'array', 'OverrideUcfirstCharacters' => 'object', 'XhtmlNamespaces' => 'object', 'BrowserFormatDetection' => 'string', 'SkinMetaTags' => 'object', 'SkipSkins' => 'object', 'FragmentMode' => 'array', 'FooterIcons' => 'object', 'InterwikiLogoOverride' => 'array', 'ResourceModules' => 'object', 'ResourceModuleSkinStyles' => 'object', 'ResourceLoaderSources' => 'object', 'ResourceLoaderMaxage' => 'object', 'ResourceLoaderMaxQueryLength' => [ 'integer', 'boolean', ], 'CanonicalNamespaceNames' => 'object', 'ExtraNamespaces' => 'object', 'ExtraGenderNamespaces' => 'object', 'NamespaceAliases' => 'object', 'CapitalLinkOverrides' => 'object', 'NamespacesWithSubpages' => 'object', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => 'object', 'ChangeCredentialsBlacklist' => 'array', 'RemoveCredentialsBlacklist' => 'array', 'PasswordConfig' => 'object', 'PasswordResetRoutes' => 'object', 'SignatureAllowedLintErrors' => 'array', 'ReservedUsernames' => 'array', 'DefaultUserOptions' => 'object', 'ConditionalUserOptions' => 'object', 'HiddenPrefs' => 'array', 'UserJsPrefLimit' => 'integer', 'AuthenticationTokenVersion' => [ 'string', 'null', ], 'SessionProviders' => 'object', 'AutoCreateTempUser' => 'object', 'AutoblockExemptions' => 'array', 'BlockCIDRLimit' => 'object', 'EnableMultiBlocks' => 'boolean', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', 'UserRequirementsPrivateConditions' => 'array', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', 'NonincludableNamespaces' => 'object', 'Autopromote' => 'object', 'AutopromoteOnce' => 'object', 'AutopromoteOnceRCExcludedGroups' => 'array', 'AddGroups' => 'object', 'RemoveGroups' => 'object', 'AvailableRights' => 'array', 'ImplicitRights' => 'array', 'AccountCreationThrottle' => [ 'integer', 'array', ], 'TempAccountCreationThrottle' => 'array', 'TempAccountNameAcquisitionThrottle' => 'array', 'SpamRegex' => 'array', 'SummarySpamRegex' => 'array', 'DnsBlacklistUrls' => 'array', 'ProxyList' => [ 'string', 'array', ], 'ProxyWhitelist' => 'array', 'SoftBlockRanges' => 'array', 'RateLimits' => 'object', 'RateLimitsExcludedIPs' => 'array', 'ExternalQuerySources' => 'object', 'PasswordAttemptThrottle' => 'array', 'GrantPermissions' => 'object', 'GrantPermissionGroups' => 'object', 'GrantRiskGroups' => 'object', 'EnableBotPasswords' => 'boolean', 'BotPasswordsCluster' => [ 'string', 'boolean', ], 'BotPasswordsDatabase' => [ 'string', 'boolean', ], 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'RCEngines' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestSandboxSpecs' => 'object', 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'VirtualRestConfig' => 'object', 'EventRelayerConfig' => 'object', 'Pingback' => 'boolean', 'OriginTrials' => 'array', 'ReportToExpiry' => 'integer', 'ReportToEndpoints' => 'array', 'FeaturePolicyReportOnly' => 'array', 'SkinsPreferred' => 'array', 'SpecialContributeSkinsEnabled' => 'array', 'SpecialContributeNewPageTarget' => [ 'string', 'null', ], 'EnableEditRecovery' => 'boolean', 'EditRecoveryExpiry' => 'integer', 'UseCodexSpecialBlock' => 'boolean', 'ShowLogoutConfirmation' => 'boolean', 'EnableProtectionIndicators' => 'boolean', 'OutputPipelineStages' => 'object', 'FeatureShutdown' => 'array', 'CloneArticleParserOutput' => 'boolean', 'UseLeximorph' => 'boolean', 'UsePostprocCache' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', ], 'mergeStrategy' => [ 'TiffThumbnailType' => 'replace', 'LBFactoryConf' => 'replace', 'InterwikiCache' => 'replace', 'PasswordPolicy' => 'array_replace_recursive', 'AuthManagerAutoConfig' => 'array_plus_2d', 'GroupPermissions' => 'array_plus_2d', 'RevokePermissions' => 'array_plus_2d', 'AddGroups' => 'array_merge_recursive', 'RemoveGroups' => 'array_merge_recursive', 'RateLimits' => 'array_plus_2d', 'GrantPermissions' => 'array_plus_2d', 'MWLoggerDefaultSpi' => 'replace', 'Profiler' => 'replace', 'Hooks' => 'array_merge_recursive', 'VirtualRestConfig' => 'array_plus_2d', ], 'dynamicDefault' => [ 'UsePathInfo' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUsePathInfo', ], ], 'Script' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultScript', ], ], 'LoadScript' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLoadScript', ], ], 'RestPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultRestPath', ], ], 'StylePath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultStylePath', ], ], 'LocalStylePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalStylePath', ], ], 'ExtensionAssetsPath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultExtensionAssetsPath', ], ], 'ArticlePath' => [ 'use' => [ 'Script', 'UsePathInfo', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultArticlePath', ], ], 'UploadPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUploadPath', ], ], 'FileCacheDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultFileCacheDirectory', ], ], 'Logo' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLogo', ], ], 'DeletedDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDeletedDirectory', ], ], 'ShowEXIF' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultShowEXIF', ], ], 'SharedPrefix' => [ 'use' => [ 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedPrefix', ], ], 'SharedSchema' => [ 'use' => [ 'DBmwschema', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedSchema', ], ], 'DBerrorLogTZ' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDBerrorLogTZ', ], ], 'Localtimezone' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocaltimezone', ], ], 'LocalTZoffset' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalTZoffset', ], ], 'ResourceBasePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultResourceBasePath', ], ], 'MetaNamespace' => [ 'use' => [ 'Sitename', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultMetaNamespace', ], ], 'CookieSecure' => [ 'use' => [ 'ForceHTTPS', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookieSecure', ], ], 'CookiePrefix' => [ 'use' => [ 'SharedDB', 'SharedPrefix', 'SharedTables', 'DBname', 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookiePrefix', ], ], 'ReadOnlyFile' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultReadOnlyFile', ], ], ], ], 'config-schema' => [ 'UploadStashScalerBaseUrl' => [ 'deprecated' => 'since 1.36 Use thumbProxyUrl in $wgLocalFileRepo', ], 'IllegalFileChars' => [ 'deprecated' => 'since 1.41; no longer customizable', ], 'ThumbnailNamespaces' => [ 'items' => [ 'type' => 'integer', ], ], 'LocalDatabases' => [ 'items' => [ 'type' => 'string', ], ], 'ParserCacheFilterConfig' => [ 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of namespace IDs to filter definitions.', 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of filter names to values.', 'properties' => [ 'minCpuTime' => [ 'type' => 'number', ], ], ], ], ], 'PHPSessionHandling' => [ 'deprecated' => 'since 1.45 Integration with PHP session handling will be removed in the future', ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'ChangeCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'RemoveCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'GroupPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GroupInheritsPermissions' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'AvailableRights' => [ 'items' => [ 'type' => 'string', ], ], 'ImplicitRights' => [ 'items' => [ 'type' => 'string', ], ], 'SoftBlockRanges' => [ 'items' => [ 'type' => 'string', ], ], 'ExternalQuerySources' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'enabled' => [ 'type' => 'boolean', 'default' => false, ], 'url' => [ 'type' => 'string', 'format' => 'uri', ], 'timeout' => [ 'type' => 'integer', 'default' => 10, ], ], 'required' => [ 'enabled', 'url', ], 'additionalProperties' => false, ], ], 'GrantPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GrantPermissionGroups' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'SitemapNamespacesPriorities' => [ 'deprecated' => 'since 1.45 and ignored', ], 'SitemapApiConfig' => [ 'additionalProperties' => [ 'enabled' => [ 'type' => 'bool', ], 'sitemapsPerIndex' => [ 'type' => 'int', ], 'pagesPerSitemap' => [ 'type' => 'int', ], 'expiry' => [ 'type' => 'int', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'JobBackoffThrottling' => [ 'additionalProperties' => [ 'type' => 'number', ], ], 'JobTypeConf' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'class' => [ 'type' => 'string', ], 'order' => [ 'type' => 'string', ], 'claimTTL' => [ 'type' => 'integer', ], ], ], ], 'TrackingCategories' => [ 'deprecated' => 'since 1.25 Extensions should now register tracking categories using the new extension registration system.', ], 'RangeContributionsCIDRLimit' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'RestSandboxSpecs' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'url' => [ 'type' => 'string', 'format' => 'url', ], 'name' => [ 'type' => 'string', ], 'file' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], ], ], 'ShellboxUrls' => [ 'additionalProperties' => [ 'type' => [ 'string', 'boolean', 'null', ], ], ], ], 'obsolete-config' => [ 'MangleFlashPolicy' => 'Since 1.39; no longer has any effect.', 'EnableOpenSearchSuggest' => 'Since 1.35, no longer used', 'AutoloadAttemptLowercase' => 'Since 1.40; no longer has any effect.', ],]
The shared interface for all language converters.
getPreferredVariant()
Get preferred language variant.
convertTo( $text, $variant, bool $clearState=true)
Same as convert() except a extra parameter to custom variant.
Interface for objects (potentially) representing an editable wiki page.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getArgument( $name)
Get an argument to this frame by name.
newChild( $args=false, $title=false, $indexOffset=0)
Create a child frame.
expand( $root, $flags=0)
Expand a document tree node.
loopCheck( $title)
Returns true if the infinite loop check is OK, false if a loop is detected.
isTemplate()
Return true if the frame is a template frame.
virtualBracketedImplode( $start, $sep, $end,... $params)
Virtual implode with brackets.
There are three types of nodes:
Definition PPNode.php:23
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
array $params
The job parameters.