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;
75use Psr\Log\LoggerInterface;
76use RuntimeException;
77use UnexpectedValueException;
78use Wikimedia\Bcp47Code\Bcp47CodeValue;
80use Wikimedia\IPUtils;
85use Wikimedia\Parsoid\Core\LinkTarget;
86use Wikimedia\Parsoid\Core\SectionMetadata;
87use Wikimedia\Parsoid\Core\TOCData;
88use Wikimedia\Parsoid\DOM\Comment;
89use Wikimedia\Parsoid\DOM\DocumentFragment;
90use Wikimedia\Parsoid\DOM\Element;
91use Wikimedia\Parsoid\DOM\Node;
92use Wikimedia\Parsoid\Utils\DOMCompat;
93use Wikimedia\Parsoid\Utils\DOMUtils;
94use Wikimedia\RemexHtml\Serializer\SerializerNode;
95use Wikimedia\ScopedCallback;
97
138#[\AllowDynamicProperties]
139class Parser {
140 use DeprecationHelper;
141
142 # Flags for Parser::setFunctionHook
143 public const SFH_NO_HASH = 1;
144 public const SFH_OBJECT_ARGS = 2;
145
146 # Constants needed for external link processing
154 public const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
159 // phpcs:ignore Generic.Files.LineLength
160 private const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
162 // phpcs:ignore Generic.Files.LineLength
163 private const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
164 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)avif|gif|jpg|jpeg|png|svg|webp)$/Sxu';
165
167 private const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
168
173 public const PTD_FOR_INCLUSION = Preprocessor::DOM_FOR_INCLUSION;
174
175 # Allowed values for $this->mOutputType
177 public const OT_HTML = 1;
179 public const OT_WIKI = 2;
181 public const OT_PREPROCESS = 3;
186 public const OT_PLAIN = 4;
187
205 public const MARKER_SUFFIX = "-QINU`\"'\x7f";
206 public const MARKER_PREFIX = "\x7f'\"`UNIQ-";
207 private const HEADLINE_MARKER_REGEX = '/^' . self::MARKER_PREFIX . '-h-(\d+)-' . self::MARKER_SUFFIX . '/';
208
223 public const TOC_PLACEHOLDER = '<meta property="mw:PageProp/toc" />';
224
225 # Persistent:
227 private array $mTagHooks = [];
229 private array $mFunctionHooks = [];
231 private array $mFunctionSynonyms = [ 0 => [], 1 => [] ];
233 private array $mStripList = [];
235 private array $mVarCache = [];
237 private array $mImageParams = [];
239 private array $mImageParamsMagicArray = [];
241 public $mMarkerIndex = 0;
242
243 // Initialised by initializeVariables()
245 private MagicWordArray $mVariables;
246 private MagicWordArray $mSubstWords;
247
248 // Initialised in constructor
250 private string $mExtLinkBracketedRegex;
251 private HookRunner $hookRunner;
252 private Preprocessor $mPreprocessor;
253
254 // Cleared with clearState():
256 private ParserOutput $mOutput;
257 private int $mAutonumber = 0;
258 private StripState $mStripState;
259 private LinkHolderArray $mLinkHolders;
260 private int $mLinkID = 0;
261 private array $mIncludeSizes;
272 private array $mTplRedirCache;
274 public array $mHeadings;
276 private array $mDoubleUnderscores;
282 private bool $mShowToc;
283 private bool $mForceTocPosition;
284 private array $mTplDomCache;
285 private ?UserIdentity $mUser;
286
287 # Temporary
288 # These are variables reset at least once per parse regardless of $clearState
289
294 private $mOptions;
295
296 # Deprecated "dynamic" properties
297 # These used to be dynamic properties added to the parser, but these
298 # have been deprecated since 1.42.
307
313 private Title $mTitle;
315 private int $mOutputType;
323 private bool $useParsoidFragments = false;
328 private array $ot;
330 private ?int $mRevisionId = null;
332 private ?string $mRevisionTimestamp = null;
334 private ?string $mRevisionUser = null;
336 private ?int $mRevisionSize = null;
338 private $mInputSize = false;
339
340 private ?RevisionRecord $mRevisionRecordObject = null;
341
347 private ?MapCacheLRU $currentRevisionCache = null;
348
353 private $mInParse = false;
354
355 private SectionProfiler $mProfiler;
356 private ?LinkRenderer $mLinkRenderer = null;
357
361 public const CONSTRUCTOR_OPTIONS = [
362 // See documentation for the corresponding config options
363 // Many of these are only used in (eg) CoreMagicVariables
387 ];
388
393 public function __construct(
394 // This is called $svcOptions instead of $options like elsewhere to avoid confusion with
395 // $mOptions, which is public and widely used, and also with the local variable $options used
396 // for ParserOptions throughout this file.
397 private ServiceOptions $svcOptions,
398 private ParserCoreTagHooks $parserCoreTagHooks,
399 private MagicWordFactory $magicWordFactory,
400 private Language $contLang,
401 private UrlUtils $urlUtils,
402 private SpecialPageFactory $specialPageFactory,
403 private LinkRendererFactory $linkRendererFactory,
404 private NamespaceInfo $nsInfo,
405 private LoggerInterface $logger,
406 private BadFileLookup $badFileLookup,
407 private RepoGroup $repoGroup,
408 private LanguageFactory $languageFactory,
409 private LanguageConverterFactory $languageConverterFactory,
410 private LanguageNameUtils $languageNameUtils,
411 private HookContainer $hookContainer,
412 private TidyDriverBase $tidy,
413 private WANObjectCache $wanCache,
414 private UserOptionsLookup $userOptionsLookup,
415 private UserFactory $userFactory,
416 private TitleFormatter $titleFormatter,
417 private HttpRequestFactory $httpRequestFactory,
418 private TrackingCategories $trackingCategories,
419 private SignatureValidatorFactory $signatureValidatorFactory,
420 private UserNameUtils $userNameUtils,
421 ) {
422 $this->deprecateDynamicPropertiesAccess( '1.42', __CLASS__ );
423 $this->deprecatePublicProperty( 'ot', '1.35', __CLASS__ );
424 $this->deprecatePublicProperty( 'mTitle', '1.35', __CLASS__ );
425 $this->deprecatePublicProperty( 'mOptions', '1.35', __CLASS__ );
426
428 // Direct construction of Parser was deprecated in 1.34 and
429 // removed in 1.36; use a ParserFactory instead.
430 throw new BadMethodCallException( 'Direct construction of Parser not allowed' );
431 }
432 $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
433
434 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->urlUtils->validProtocols() . ')' .
435 self::EXT_LINK_ADDR .
436 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*)\]/Su';
437
438 $this->hookRunner = new HookRunner( $hookContainer );
439
440 $this->mPreprocessor = new Preprocessor_Hash(
441 $this,
442 $this->wanCache,
443 [
444 'cacheThreshold' => $svcOptions->get( MainConfigNames::PreprocessorCacheThreshold ),
445 'disableLangConversion' => $languageConverterFactory->isConversionDisabled(),
446 ]
447 );
448
449 // These steps used to be done in "::firstCallInit()"
450 // (if you're chasing a reference from some old code)
451 CoreParserFunctions::register(
452 $this,
453 new ServiceOptions( CoreParserFunctions::REGISTER_OPTIONS, $svcOptions )
454 );
455 $parserCoreTagHooks->register( $this );
456 $this->initializeVariables();
457
458 $this->hookRunner->onParserFirstCallInit( $this );
459 $this->mTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/Missing' );
460 }
461
465 public function __destruct() {
466 // @phan-suppress-next-line PhanRedundantCondition Typed property not set in constructor, may be uninitialized
467 if ( isset( $this->mLinkHolders ) ) {
468 // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
469 unset( $this->mLinkHolders );
470 }
471 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
472 foreach ( $this as $name => $value ) {
473 unset( $this->$name );
474 }
475 }
476
480 public function __clone() {
481 $this->mInParse = false;
482
483 $this->mPreprocessor = clone $this->mPreprocessor;
484 $this->mPreprocessor->resetParser( $this );
485
486 $this->hookRunner->onParserCloned( $this );
487 }
488
496 public function firstCallInit() {
497 /*
498 * This method should be hard-deprecated once remaining calls are
499 * removed; it no longer does anything.
500 */
501 }
502
508 public function clearState() {
509 $this->resetOutput();
510 $this->mAutonumber = 0;
511 $this->mLinkHolders = new LinkHolderArray(
512 $this,
513 $this->getContentLanguageConverter(),
514 $this->getHookContainer()
515 );
516 $this->mLinkID = 0;
517 $this->mRevisionTimestamp = null;
518 $this->mRevisionId = null;
519 $this->mRevisionUser = null;
520 $this->mRevisionSize = null;
521 $this->mRevisionRecordObject = null;
522 $this->mVarCache = [];
523 $this->mUser = null;
524 $this->currentRevisionCache = null;
525
526 $this->mStripState = new StripState( $this );
527
528 # Clear these on every parse, T6549
529 $this->mTplRedirCache = [];
530 $this->mTplDomCache = [];
531
532 $this->mShowToc = true;
533 $this->mForceTocPosition = false;
534 $this->mIncludeSizes = [
535 'post-expand' => 0,
536 'arg' => 0,
537 ];
538 $this->mPPNodeCount = 0;
539 $this->mHighestExpansionDepth = 0;
540 $this->mHeadings = [];
541 $this->mDoubleUnderscores = [];
542 $this->mExpensiveFunctionCount = 0;
543
544 $this->mProfiler = new SectionProfiler();
545
546 $this->hookRunner->onParserClearState( $this );
547 }
548
553 public function resetOutput() {
554 $this->mOutput = new ParserOutput;
555 $this->mOptions->registerWatcher( $this->mOutput->recordOption( ... ) );
556 }
557
565 public function getParseTime(): DateTime {
566 $ts = $this->mOptions->getTimestamp(); /* TS::MW */
567 $date = DateTime::createFromFormat(
568 'YmdHis', $ts, new DateTimeZone( 'UTC' )
569 );
570 if ( $this->hookContainer->isRegistered( 'ParserGetVariableValueTs' ) ) {
571 $s = $date->format( 'U' );
572 $this->hookRunner->onParserGetVariableValueTs( $this, $s );
573 $date = ( new MWTimestamp( $s ) )->timestamp;
574 }
575 return $date;
576 }
577
596 public function parse(
597 $text, PageReference $page, ParserOptions $options,
598 $linestart = true, $clearState = true, $revid = null
599 ) {
600 if ( $clearState ) {
601 // We use U+007F DELETE to construct strip markers, so we have to make
602 // sure that this character does not occur in the input text.
603 $text = strtr( $text, "\x7f", "?" );
604 $magicScopeVariable = $this->lock();
605 }
606 // Strip U+0000 NULL (T159174)
607 $text = str_replace( "\000", '', $text );
608
609 $this->startParse( $page, $options, self::OT_HTML, $clearState );
610
611 $this->currentRevisionCache = null;
612 $this->mInputSize = strlen( $text );
613 $this->mOutput->resetParseStartTime();
614
615 $oldRevisionId = $this->mRevisionId;
616 $oldRevisionRecordObject = $this->mRevisionRecordObject;
617 $oldRevisionTimestamp = $this->mRevisionTimestamp;
618 $oldRevisionUser = $this->mRevisionUser;
619 $oldRevisionSize = $this->mRevisionSize;
620 if ( $revid !== null ) {
621 $this->mRevisionId = $revid;
622 $this->mRevisionRecordObject = null;
623 $this->mRevisionTimestamp = null;
624 $this->mRevisionUser = null;
625 $this->mRevisionSize = null;
626 }
627
628 $text = $this->internalParse( $text );
629 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
630
631 $text = $this->internalParseHalfParsed( $text, true, $linestart );
632
640 if ( !$options->getDisableTitleConversion()
641 && !isset( $this->mDoubleUnderscores['nocontentconvert'] )
642 && !isset( $this->mDoubleUnderscores['notitleconvert'] )
643 && $this->mOutput->getDisplayTitle() === false
644 ) {
645 $converter = $this->getTargetLanguageConverter();
646 $titleText = $converter->getConvRuleTitle();
647 if ( $titleText !== false ) {
648 $titleText = Sanitizer::removeSomeTags( $titleText );
649 } else {
650 [ $nsText, $nsSeparator, $mainText ] = $converter->convertSplitTitle( $page );
651 // In the future, those three pieces could be stored separately rather than joined into $titleText,
652 // and OutputPage would format them and join them together, to resolve T314399.
653 $titleLang = $this->languageFactory->getLanguage( $converter->getPreferredVariant() );
654 $titleText = self::formatPageTitle( $nsText, $nsSeparator, $mainText, $titleLang );
655 }
656 $this->mOutput->setTitleText( $titleText );
657 }
658
659 # Recording timing info. Must be called before finalizeAdaptiveCacheExpiry() and
660 # makeLimitReport(), which make use of the timing info.
661 $this->mOutput->recordTimeProfile();
662
663 # Compute runtime adaptive expiry if set
664 $this->mOutput->finalizeAdaptiveCacheExpiry();
665
666 # Warn if too many heavyweight parser functions were used
667 if ( $this->mExpensiveFunctionCount > $options->getExpensiveParserFunctionLimit() ) {
668 $this->limitationWarn( 'expensive-parserfunction',
669 $this->mExpensiveFunctionCount,
671 );
672 }
673
674 # Information on limits, for the benefit of users who try to skirt them
675 $this->makeLimitReport( $this->mOptions, $this->mOutput );
676
677 $this->mOutput->setFromParserOptions( $options );
678
679 $this->mOutput->setContentHolderText( $text );
680
681 $this->mRevisionId = $oldRevisionId;
682 $this->mRevisionRecordObject = $oldRevisionRecordObject;
683 $this->mRevisionTimestamp = $oldRevisionTimestamp;
684 $this->mRevisionUser = $oldRevisionUser;
685 $this->mRevisionSize = $oldRevisionSize;
686 $this->mInputSize = false;
687 $this->currentRevisionCache = null;
688
689 return $this->mOutput;
690 }
691
696 public function makeLimitReport(
697 ParserOptions $parserOptions, ParserOutput $parserOutput
698 ) {
699 if ( !$this->svcOptions->get( MainConfigNames::EnableParserLimitReporting ) ) {
700 return;
701 }
702 if ( $parserOptions->isMessage() ) {
703 // No need to include limit report information in
704 // user interface messages.
705 return;
706 }
707
708 $maxIncludeSize = $parserOptions->getMaxIncludeSize();
709
710 $cpuTime = $parserOutput->getTimeProfile( 'cpu' );
711 if ( $cpuTime !== null ) {
712 $parserOutput->setLimitReportData( 'limitreport-cputime',
713 sprintf( "%.3f", $cpuTime )
714 );
715 }
716
717 $wallTime = $parserOutput->getTimeProfile( 'wall' );
718 $parserOutput->setLimitReportData( 'limitreport-walltime',
719 sprintf( "%.3f", $wallTime )
720 );
721
722 $parserOutput->setLimitReportData( 'limitreport-ppvisitednodes',
723 [ $this->mPPNodeCount, $parserOptions->getMaxPPNodeCount() ]
724 );
725 $revisionSize = $this->mInputSize !== false ? $this->mInputSize :
726 $this->getRevisionSize();
727 $parserOutput->setLimitReportData( 'limitreport-revisionsize',
728 [ $revisionSize ?? -1, $this->svcOptions->get( MainConfigNames::MaxArticleSize ) * 1024 ]
729 );
730 $parserOutput->setLimitReportData( 'limitreport-postexpandincludesize',
731 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
732 );
733 $parserOutput->setLimitReportData( 'limitreport-templateargumentsize',
734 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
735 );
736 $parserOutput->setLimitReportData( 'limitreport-expansiondepth',
737 [ $this->mHighestExpansionDepth, $parserOptions->getMaxPPExpandDepth() ]
738 );
739 $parserOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
740 [ $this->mExpensiveFunctionCount, $parserOptions->getExpensiveParserFunctionLimit() ]
741 );
742
743 foreach ( $this->mStripState->getLimitReport() as [ $key, $value ] ) {
744 $parserOutput->setLimitReportData( $key, $value );
745 }
746
747 $this->hookRunner->onParserLimitReportPrepare( $this, $parserOutput );
748
749 // Add on template profiling data in human/machine readable way
750 $dataByFunc = $this->mProfiler->getFunctionStats();
751 uasort( $dataByFunc, static function ( $a, $b ) {
752 return $b['real'] <=> $a['real']; // descending order
753 } );
754 $profileReport = [];
755 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
756 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
757 $item['%real'], $item['real'], $item['calls'],
758 htmlspecialchars( $item['name'] ) );
759 }
760
761 $parserOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
762
763 // Add other cache related metadata
764 if ( $this->svcOptions->get( MainConfigNames::ShowHostnames ) ) {
765 $parserOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
766 }
767 $parserOutput->setLimitReportData( 'cachereport-timestamp',
768 $parserOutput->getCacheTime() );
769 $parserOutput->setLimitReportData( 'cachereport-ttl',
770 $parserOutput->getCacheExpiry() );
771 $parserOutput->setLimitReportData( 'cachereport-transientcontent',
772 $parserOutput->hasReducedExpiry() );
773 if ( $parserOutput->getCacheExpirySource() !== null ) {
774 $parserOutput->setLimitReportData( 'cachereport-expiry-source',
775 $parserOutput->getCacheExpirySource() );
776 }
777 }
778
804 public function recursiveTagParse( $text, $frame = false ): string {
805 $text = $this->internalParse( $text, false, $frame );
806 return $text;
807 }
808
828 public function recursiveTagParseFully( $text, $frame = false ): string {
829 $text = $this->recursiveTagParse( $text, $frame );
830 $text = $this->internalParseHalfParsed( $text, false );
831 return $text;
832 }
833
854 public function parseExtensionTagAsTopLevelDoc( string $text, PPFrame|false $frame = false ): string {
855 $text = $this->recursiveTagParse( $text, $frame );
856 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
857 $text = $this->internalParseHalfParsed( $text, true );
858 return $text;
859 }
860
873 public function preprocess(
874 $text,
875 ?PageReference $page,
876 ParserOptions $options,
877 $revid = null,
878 $frame = false
879 ) {
880 $magicScopeVariable = $this->lock();
881 $this->startParse( $page, $options, self::OT_PREPROCESS, true );
882 if ( $revid !== null ) {
883 $this->mRevisionId = $revid;
884 }
885 $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
886 $text = $this->replaceVariables( $text, $frame );
887 $text = $this->mStripState->unstripBoth( $text );
888 return $text;
889 }
890
900 public function recursivePreprocess( $text, $frame = false ): string {
901 $text = $this->replaceVariables( $text, $frame );
902 $text = $this->mStripState->unstripBoth( $text );
903 return $text;
904 }
905
920 public function getPreloadText( $text, PageReference $page, ParserOptions $options, $params = [] ): string {
921 $msg = new RawMessage( $text );
922 $text = $msg->params( $params )->plain();
923
924 # Parser (re)initialisation
925 $magicScopeVariable = $this->lock();
926 $this->startParse( $page, $options, self::OT_PLAIN, true );
927
928 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
929 $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
930 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
931 $text = $this->mStripState->unstripBoth( $text );
932 return $text;
933 }
934
942 public function setUser( ?UserIdentity $user ) {
943 $this->mUser = $user;
944 }
945
953 public function setTitle( ?Title $t = null ) {
954 $this->setPage( $t );
955 }
956
962 public function getTitle(): Title {
963 return $this->mTitle;
964 }
965
972 public function setPage( ?PageReference $t = null ) {
973 if ( !$t ) {
974 $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
975 } else {
976 // For now (early 1.37 alpha), always convert to Title, so we don't have to do it over
977 // and over again in other methods. Eventually, we will no longer need to have a Title
978 // instance internally.
979 $t = Title::newFromPageReference( $t );
980 }
981
982 if ( $t->hasFragment() ) {
983 # Strip the fragment to avoid various odd effects
984 $this->mTitle = $t->createFragmentTarget( '' );
985 } else {
986 $this->mTitle = $t;
987 }
988 }
989
995 public function getPage(): PageReference {
996 if ( $this->mTitle->isSpecial( 'Badtitle' ) ) {
997 [ , $subPage ] = $this->specialPageFactory->resolveAlias( $this->mTitle->getDBkey() );
998
999 if ( $subPage === 'Missing' ) {
1000 wfDeprecated( __METHOD__ . ' without a Title set', '1.34' );
1001 }
1002 }
1003
1004 return $this->mTitle;
1005 }
1006
1012 public function getOutputType(): int {
1013 return $this->mOutputType;
1014 }
1015
1021 public function setOutputType( $ot ): void {
1022 $this->mOutputType = $ot;
1023 # Shortcut alias
1024 $this->ot = [
1025 'html' => $ot == self::OT_HTML,
1026 'wiki' => $ot == self::OT_WIKI,
1027 'pre' => $ot == self::OT_PREPROCESS,
1028 'plain' => $ot == self::OT_PLAIN,
1029 ];
1030 }
1031
1036 public function getOutput(): ParserOutput {
1037 return $this->mOutput;
1038 }
1039
1044 public function getOptions() {
1045 return $this->mOptions;
1046 }
1047
1053 public function setOptions( ParserOptions $options ): void {
1054 $this->mOptions = $options;
1055 }
1056
1061 public function nextLinkID() {
1062 return $this->mLinkID++;
1063 }
1064
1069 public function setLinkID( $id ) {
1070 $this->mLinkID = $id;
1071 }
1072
1081 public function getTargetLanguage() {
1082 $target = $this->mOptions->getTargetLanguage();
1083
1084 if ( $target !== null ) {
1085 return $target;
1086 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1087 return $this->mOptions->getUserLangObj();
1088 }
1089
1090 return $this->getTitle()->getPageLanguage();
1091 }
1092
1100 public function getUserIdentity(): UserIdentity {
1101 return $this->mUser ?? $this->getOptions()->getUserIdentity();
1102 }
1103
1110 public function getPreprocessor() {
1111 return $this->mPreprocessor;
1112 }
1113
1120 public function getLinkRenderer() {
1121 // XXX We make the LinkRenderer with current options and then cache it forever
1122 if ( !$this->mLinkRenderer ) {
1123 $this->mLinkRenderer = $this->linkRendererFactory->create();
1124 }
1125
1126 return $this->mLinkRenderer;
1127 }
1128
1135 public function getMagicWordFactory() {
1136 return $this->magicWordFactory;
1137 }
1138
1145 public function getContentLanguage() {
1146 return $this->contLang;
1147 }
1148
1155 public function getBadFileLookup() {
1156 return $this->badFileLookup;
1157 }
1158
1178 public static function extractTagsAndParams( array $elements, $text, &$matches ): string {
1179 static $n = 1;
1180 $stripped = '';
1181 $matches = [];
1182
1183 $taglist = implode( '|', $elements );
1184 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1185
1186 while ( $text != '' ) {
1187 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1188 $stripped .= $p[0];
1189 if ( count( $p ) < 5 ) {
1190 break;
1191 }
1192 if ( count( $p ) > 5 ) {
1193 # comment
1194 $element = $p[4];
1195 $attributes = '';
1196 $close = '';
1197 $inside = $p[5];
1198 } else {
1199 # tag
1200 [ , $element, $attributes, $close, $inside ] = $p;
1201 }
1202
1203 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1204 $stripped .= $marker;
1205
1206 if ( $close === '/>' ) {
1207 # Empty element tag, <tag />
1208 $content = null;
1209 $text = $inside;
1210 $tail = null;
1211 } else {
1212 if ( $element === '!--' ) {
1213 $end = '/(-->)/';
1214 } else {
1215 $end = "/(<\\/$element\\s*>)/i";
1216 }
1217 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1218 $content = $q[0];
1219 if ( count( $q ) < 3 ) {
1220 # No end tag -- let it run out to the end of the text.
1221 $tail = '';
1222 $text = '';
1223 } else {
1224 [ , $tail, $text ] = $q;
1225 }
1226 }
1227
1228 $matches[$marker] = [ $element,
1229 $content,
1230 Sanitizer::decodeTagAttributes( $attributes ),
1231 "<$element$attributes$close$content$tail" ];
1232 }
1233 return $stripped;
1234 }
1235
1241 public function getStripList() {
1242 return $this->mStripList;
1243 }
1244
1249 public function getStripState() {
1250 return $this->mStripState;
1251 }
1252
1262 public function insertStripItem( $text ): string {
1263 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1264 $this->mMarkerIndex++;
1265 $this->mStripState->addGeneral( $marker, $text );
1266 return $marker;
1267 }
1268
1275 private function handleTables( string $text ): string {
1276 $lines = StringUtils::explode( "\n", $text );
1277 $out = '';
1278 $td_history = []; # Is currently a td tag open?
1279 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1280 $tr_history = []; # Is currently a tr tag open?
1281 $tr_attributes = []; # history of tr attributes
1282 $has_opened_tr = []; # Did this table open a <tr> element?
1283 $indent_level = 0; # indent level of the table
1284
1285 foreach ( $lines as $outLine ) {
1286 $line = trim( $outLine );
1287
1288 if ( $line === '' ) { # empty line, go to next line
1289 $out .= $outLine . "\n";
1290 continue;
1291 }
1292
1293 $first_character = $line[0];
1294 $first_two = substr( $line, 0, 2 );
1295 $matches = [];
1296
1297 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1298 # First check if we are starting a new table
1299 $indent_level = strlen( $matches[1] );
1300
1301 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1302 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1303
1304 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1305 $td_history[] = false;
1306 $last_tag_history[] = '';
1307 $tr_history[] = false;
1308 $tr_attributes[] = '';
1309 $has_opened_tr[] = false;
1310 } elseif ( count( $td_history ) == 0 ) {
1311 # Don't do any of the following
1312 $out .= $outLine . "\n";
1313 continue;
1314 } elseif ( $first_two === '|}' ) {
1315 # We are ending a table
1316 $line = '</table>' . substr( $line, 2 );
1317 $last_tag = array_pop( $last_tag_history );
1318
1319 if ( !array_pop( $has_opened_tr ) ) {
1320 $line = "<tr><td></td></tr>{$line}";
1321 }
1322
1323 if ( array_pop( $tr_history ) ) {
1324 $line = "</tr>{$line}";
1325 }
1326
1327 if ( array_pop( $td_history ) ) {
1328 $line = "</{$last_tag}>{$line}";
1329 }
1330 array_pop( $tr_attributes );
1331 if ( $indent_level > 0 ) {
1332 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1333 } else {
1334 $outLine = $line;
1335 }
1336 } elseif ( $first_two === '|-' ) {
1337 # Now we have a table row
1338 $line = preg_replace( '#^\|-+#', '', $line );
1339
1340 # Whats after the tag is now only attributes
1341 $attributes = $this->mStripState->unstripBoth( $line );
1342 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1343 array_pop( $tr_attributes );
1344 $tr_attributes[] = $attributes;
1345
1346 $line = '';
1347 $last_tag = array_pop( $last_tag_history );
1348 array_pop( $has_opened_tr );
1349 $has_opened_tr[] = true;
1350
1351 if ( array_pop( $tr_history ) ) {
1352 $line = '</tr>';
1353 }
1354
1355 if ( array_pop( $td_history ) ) {
1356 $line = "</{$last_tag}>{$line}";
1357 }
1358
1359 $outLine = $line;
1360 $tr_history[] = false;
1361 $td_history[] = false;
1362 $last_tag_history[] = '';
1363 } elseif ( $first_character === '|'
1364 || $first_character === '!'
1365 || $first_two === '|+'
1366 ) {
1367 # This might be cell elements, td, th or captions
1368 if ( $first_two === '|+' ) {
1369 $first_character = '+';
1370 $line = substr( $line, 2 );
1371 } else {
1372 $line = substr( $line, 1 );
1373 }
1374
1375 // Implies both are valid for table headings.
1376 if ( $first_character === '!' ) {
1377 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1378 }
1379
1380 # Split up multiple cells on the same line.
1381 # FIXME : This can result in improper nesting of tags processed
1382 # by earlier parser steps.
1383 $cells = explode( '||', $line );
1384
1385 $outLine = '';
1386
1387 # Loop through each table cell
1388 foreach ( $cells as $cell ) {
1389 $previous = '';
1390 if ( $first_character !== '+' ) {
1391 $tr_after = array_pop( $tr_attributes );
1392 if ( !array_pop( $tr_history ) ) {
1393 $previous = "<tr{$tr_after}>\n";
1394 }
1395 $tr_history[] = true;
1396 $tr_attributes[] = '';
1397 array_pop( $has_opened_tr );
1398 $has_opened_tr[] = true;
1399 }
1400
1401 $last_tag = array_pop( $last_tag_history );
1402
1403 if ( array_pop( $td_history ) ) {
1404 $previous = "</{$last_tag}>\n{$previous}";
1405 }
1406
1407 if ( $first_character === '|' ) {
1408 $last_tag = 'td';
1409 } elseif ( $first_character === '!' ) {
1410 $last_tag = 'th';
1411 } elseif ( $first_character === '+' ) {
1412 $last_tag = 'caption';
1413 } else {
1414 $last_tag = '';
1415 }
1416
1417 $last_tag_history[] = $last_tag;
1418
1419 # A cell could contain both parameters and data
1420 $cell_data = explode( '|', $cell, 2 );
1421
1422 # T2553: Note that a '|' inside an invalid link should not
1423 # be mistaken as delimiting cell parameters
1424 # Bug T153140: Neither should language converter markup.
1425 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1426 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1427 } elseif ( count( $cell_data ) == 1 ) {
1428 // Whitespace in cells is trimmed
1429 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1430 } else {
1431 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1432 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1433 // Whitespace in cells is trimmed
1434 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1435 }
1436
1437 $outLine .= $cell;
1438 $td_history[] = true;
1439 }
1440 }
1441 $out .= $outLine . "\n";
1442 }
1443
1444 # Closing open td, tr && table
1445 while ( count( $td_history ) > 0 ) {
1446 if ( array_pop( $td_history ) ) {
1447 $out .= "</td>\n";
1448 }
1449 if ( array_pop( $tr_history ) ) {
1450 $out .= "</tr>\n";
1451 }
1452 if ( !array_pop( $has_opened_tr ) ) {
1453 $out .= "<tr><td></td></tr>\n";
1454 }
1455
1456 $out .= "</table>\n";
1457 }
1458
1459 # Remove trailing line-ending (b/c)
1460 if ( substr( $out, -1 ) === "\n" ) {
1461 $out = substr( $out, 0, -1 );
1462 }
1463
1464 # special case: don't return empty table
1465 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1466 $out = '';
1467 }
1468
1469 return $out;
1470 }
1471
1485 public function internalParse( $text, $isMain = true, $frame = false ): string {
1486 $origText = $text;
1487
1488 # Hook to suspend the parser in this state
1489 if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1490 return $text;
1491 }
1492
1493 # if $frame is provided, then use $frame for replacing any variables
1494 if ( $frame ) {
1495 # use frame depth to infer how include/noinclude tags should be handled
1496 # depth=0 means this is the top-level document; otherwise it's an included document
1497 if ( !$frame->depth ) {
1498 $flag = 0;
1499 } else {
1500 $flag = Preprocessor::DOM_FOR_INCLUSION;
1501 }
1502 $dom = $this->preprocessToDom( $text, $flag );
1503 $text = $frame->expand( $dom );
1504 } else {
1505 # if $frame is not provided, then use old-style replaceVariables
1506 $text = $this->replaceVariables( $text );
1507 }
1508
1509 $text = Sanitizer::internalRemoveHtmlTags(
1510 $text,
1511 // Callback from the Sanitizer for unstripping items found in
1512 // HTML attribute values, so they can be safely tested and escaped.
1513 function ( &$text, $frame = false ) {
1514 $text = $this->mStripState->unstripBoth( $text );
1515 },
1516 false,
1517 [],
1518 []
1519 );
1520 $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1521
1522 # Tables need to come after variable replacement for things to work
1523 # properly; putting them before other transformations should keep
1524 # exciting things like link expansions from showing up in surprising
1525 # places.
1526 $text = $this->handleTables( $text );
1527
1528 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1529
1530 $text = $this->handleDoubleUnderscore( $text );
1531
1532 $text = $this->handleHeadings( $text );
1533 $text = $this->handleInternalLinks( $text );
1534 $text = $this->handleAllQuotes( $text );
1535 $text = $this->handleExternalLinks( $text );
1536
1537 # handleInternalLinks may sometimes leave behind
1538 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1539 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1540
1541 $text = $this->handleMagicLinks( $text );
1542 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1543
1544 return $text;
1545 }
1546
1554 return $this->languageConverterFactory->getLanguageConverter(
1555 $this->getTargetLanguage()
1556 );
1557 }
1558
1562 private function getContentLanguageConverter(): ILanguageConverter {
1563 return $this->languageConverterFactory->getLanguageConverter(
1564 $this->getContentLanguage()
1565 );
1566 }
1567
1575 protected function getHookContainer() {
1576 return $this->hookContainer;
1577 }
1578
1583 public function getNamespaceInfo(): NamespaceInfo {
1584 return $this->nsInfo;
1585 }
1586
1591 public function getMiserMode(): bool {
1592 return $this->svcOptions->get( MainConfigNames::MiserMode );
1593 }
1594
1603 protected function getHookRunner() {
1604 return $this->hookRunner;
1605 }
1606
1616 private function internalParseHalfParsed( string $text, bool $isMain = true, bool $linestart = true ): string {
1617 $text = $this->mStripState->unstripGeneral( $text );
1618
1619 $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1620
1621 $this->replaceLinkHoldersPrivate( $text );
1622
1630 $converter = null;
1631 if ( !( $this->mOptions->getDisableContentConversion()
1632 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
1633 || $this->mOptions->getInterfaceMessage()
1634 || $this->mOptions->getUseParsoid()
1635 ) ) {
1636 # The position of the convert() call should not be changed. it
1637 # assumes that the links are all replaced and the only thing left
1638 # is the <nowiki> mark.
1639 $converter = $this->getTargetLanguageConverter();
1640 $text = $converter->convert( $text );
1641 // TOC will be converted below.
1642 }
1643 // Convert the TOC. This is done *after* the main text
1644 // so that all the editor-defined conversion rules (by convention
1645 // defined at the start of the article) are applied to the TOC
1646 self::localizeTOC(
1647 $this->mOutput->getTOCData(),
1648 $this->getTargetLanguage(),
1649 $converter, // null if conversion is to be suppressed.
1650 $converter?->getPreferredVariant()
1651 );
1652 if ( $converter ) {
1653 $this->mOutput->setLanguage( new Bcp47CodeValue(
1654 LanguageCode::bcp47( $converter->getPreferredVariant() )
1655 ) );
1656 } else {
1657 $this->mOutput->setLanguage( $this->getTargetLanguage() );
1658 }
1659
1660 $text = $this->mStripState->unstripNoWiki( $text );
1661
1662 $text = $this->mStripState->unstripGeneral( $text );
1663
1664 $text = $this->tidy->tidy( $text, Sanitizer::armorFrenchSpaces( ... ) );
1665
1666 if ( $isMain ) {
1667 $this->mOutput->setTitle( $this->getPage() );
1668 $this->hookRunner->onParserAfterTidy( $this, $text );
1669 }
1670
1671 return $text;
1672 }
1673
1684 private function handleMagicLinks( string $text ): string {
1685 $prots = $this->urlUtils->validAbsoluteProtocols();
1686 $urlChar = self::EXT_LINK_URL_CLASS;
1687 $addr = self::EXT_LINK_ADDR;
1688 $space = self::SPACE_NOT_NL; # non-newline space
1689 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1690 $spaces = "$space++"; # possessive match of 1 or more spaces
1691 $resultText = preg_replace_callback(
1692 '!(?: # Start cases
1693 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1694 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1695 (\b # m[3]: Free external links
1696 (?i:$prots)
1697 ($addr$urlChar*) # m[4]: Post-protocol path
1698 ) |
1699 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1700 ([0-9]+)\b |
1701 \bISBN $spaces ( # m[6]: ISBN, capture number
1702 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1703 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1704 [0-9Xx] # check digit
1705 )\b
1706 )!xu",
1707 $this->magicLinkCallback( ... ),
1708 $text
1709 );
1710 // preg_replace_callback in "u" mode returns null, when a non-valid utf-8 character is discovered in the input
1711 if ( $resultText === null ) {
1712 $this->logger->warning( "Input text contains non-valid UTF-8 characters (" . __METHOD__ . ")" );
1713 return $text;
1714 }
1715 return $resultText;
1716 }
1717
1722 private function magicLinkCallback( array $m ): string {
1723 if ( isset( $m[1] ) && $m[1] !== '' ) {
1724 # Skip anchor
1725 return $m[0];
1726 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1727 # Skip HTML element
1728 return $m[0];
1729 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1730 # Free external link
1731 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1732 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1733 # RFC or PMID
1734 if ( str_starts_with( $m[0], 'RFC' ) ) {
1735 if ( !$this->mOptions->getMagicRFCLinks() ) {
1736 return $m[0];
1737 }
1738 $keyword = 'RFC';
1739 $urlmsg = 'rfcurl';
1740 $cssClass = 'mw-magiclink-rfc';
1741 $trackingCat = 'magiclink-tracking-rfc';
1742 $id = $m[5];
1743 } elseif ( str_starts_with( $m[0], 'PMID' ) ) {
1744 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1745 return $m[0];
1746 }
1747 $keyword = 'PMID';
1748 $urlmsg = 'pubmedurl';
1749 $cssClass = 'mw-magiclink-pmid';
1750 $trackingCat = 'magiclink-tracking-pmid';
1751 $id = $m[5];
1752 } else {
1753 // Should never happen
1754 throw new UnexpectedValueException( __METHOD__ . ': unrecognised match type "' .
1755 substr( $m[0], 0, 20 ) . '"' );
1756 }
1757 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1758 $this->addTrackingCategory( $trackingCat );
1759 return $this->getLinkRenderer()->makeExternalLink(
1760 $url,
1761 "{$keyword} {$id}",
1762 $this->getTitle(),
1763 $cssClass,
1764 []
1765 );
1766 } elseif ( isset( $m[6] ) && $m[6] !== ''
1767 && $this->mOptions->getMagicISBNLinks()
1768 ) {
1769 # ISBN
1770 $isbn = $m[6];
1771 $space = self::SPACE_NOT_NL; # non-newline space
1772 $isbn = preg_replace( "/$space/", ' ', $isbn );
1773 $num = strtr( $isbn, [
1774 '-' => '',
1775 ' ' => '',
1776 'x' => 'X',
1777 ] );
1778 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1779 return $this->getLinkRenderer()->makeKnownLink(
1780 SpecialPage::getTitleFor( 'Booksources', $num ),
1781 "ISBN $isbn",
1782 [
1783 'class' => 'internal mw-magiclink-isbn',
1784 'title' => false // suppress title attribute
1785 ]
1786 );
1787 } else {
1788 return $m[0];
1789 }
1790 }
1791
1801 private function makeFreeExternalLink( string $url, int $numPostProto ): string {
1802 $trail = '';
1803
1804 # The characters '<' and '>' (which were escaped by
1805 # internalRemoveHtmlTags()) should not be included in
1806 # URLs, per RFC 2396.
1807 # Make &nbsp; terminate a URL as well (bug T84937)
1808 $m2 = [];
1809 if ( preg_match(
1810 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1811 $url,
1812 $m2,
1813 PREG_OFFSET_CAPTURE
1814 ) ) {
1815 $trail = substr( $url, $m2[0][1] ) . $trail;
1816 $url = substr( $url, 0, $m2[0][1] );
1817 }
1818
1819 # Move trailing punctuation to $trail
1820 $sep = ',;\.:!?';
1821 # If there is no left bracket, then consider right brackets fair game too
1822 if ( !str_contains( $url, '(' ) ) {
1823 $sep .= ')';
1824 }
1825
1826 $urlRev = strrev( $url );
1827 $numSepChars = strspn( $urlRev, $sep );
1828 # Don't break a trailing HTML entity by moving the ; into $trail
1829 # This is in hot code, so use substr_compare to avoid having to
1830 # create a new string object for the comparison
1831 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1832 # more optimization: instead of running preg_match with a $
1833 # anchor, which can be slow, do the match on the reversed
1834 # string starting at the desired offset.
1835 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1836 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1837 $numSepChars--;
1838 }
1839 }
1840 if ( $numSepChars ) {
1841 $trail = substr( $url, -$numSepChars ) . $trail;
1842 $url = substr( $url, 0, -$numSepChars );
1843 }
1844
1845 # Verify that we still have a real URL after trail removal, and
1846 # not just lone protocol
1847 if ( strlen( $trail ) >= $numPostProto ) {
1848 return $url . $trail;
1849 }
1850
1851 $url = Sanitizer::cleanUrl( $url );
1852
1853 # Is this an external image?
1854 $text = $this->maybeMakeExternalImage( $url );
1855 if ( $text === false ) {
1856 # Not an image, make a link
1857 $text = $this->getLinkRenderer()->makeExternalLink(
1858 $url,
1859 $this->getTargetLanguageConverter()->markNoConversion( $url ),
1860 $this->getTitle(),
1861 'free',
1862 $this->getExternalLinkAttribs( $url )
1863 );
1864 # Register it in the output object...
1865 $this->mOutput->addExternalLink( $url );
1866 }
1867 return $text . $trail;
1868 }
1869
1876 private function handleHeadings( string $text ): string {
1877 for ( $i = 6; $i >= 1; --$i ) {
1878 $h = str_repeat( '=', $i );
1879 // Trim non-newline whitespace from headings
1880 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1881 $text = preg_replace(
1882 "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m",
1883 "<h$i data-mw-wikitext>\\1</h$i>",
1884 $text
1885 );
1886 }
1887 // T428677: temporarily mark parser output generated with the
1888 // data-mw-wikitext marking of wikitext headings.
1889 $this->getOutput()->setExtensionData( 'core:new-heading-attr', true );
1890 return $text;
1891 }
1892
1900 private function handleAllQuotes( string $text ): string {
1901 $outtext = '';
1902 $lines = StringUtils::explode( "\n", $text );
1903 foreach ( $lines as $line ) {
1904 $outtext .= $this->doQuotes( $line ) . "\n";
1905 }
1906 $outtext = substr( $outtext, 0, -1 );
1907 return $outtext;
1908 }
1909
1918 public function doQuotes( $text ): string {
1919 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1920 $countarr = count( $arr );
1921 if ( $countarr == 1 ) {
1922 return $text;
1923 }
1924
1925 // First, do some preliminary work. This may shift some apostrophes from
1926 // being mark-up to being text. It also counts the number of occurrences
1927 // of bold and italics mark-ups.
1928 $numbold = 0;
1929 $numitalics = 0;
1930 for ( $i = 1; $i < $countarr; $i += 2 ) {
1931 $thislen = strlen( $arr[$i] );
1932 // If there are ever four apostrophes, assume the first is supposed to
1933 // be text, and the remaining three constitute mark-up for bold text.
1934 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1935 if ( $thislen == 4 ) {
1936 $arr[$i - 1] .= "'";
1937 $arr[$i] = "'''";
1938 $thislen = 3;
1939 } elseif ( $thislen > 5 ) {
1940 // If there are more than 5 apostrophes in a row, assume they're all
1941 // text except for the last 5.
1942 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1943 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1944 $arr[$i] = "'''''";
1945 $thislen = 5;
1946 }
1947 // Count the number of occurrences of bold and italics mark-ups.
1948 if ( $thislen == 2 ) {
1949 $numitalics++;
1950 } elseif ( $thislen == 3 ) {
1951 $numbold++;
1952 } elseif ( $thislen == 5 ) {
1953 $numitalics++;
1954 $numbold++;
1955 }
1956 }
1957
1958 // If there is an odd number of both bold and italics, it is likely
1959 // that one of the bold ones was meant to be an apostrophe followed
1960 // by italics. Which one we cannot know for certain, but it is more
1961 // likely to be one that has a single-letter word before it.
1962 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1963 $firstsingleletterword = -1;
1964 $firstmultiletterword = -1;
1965 $firstspace = -1;
1966 for ( $i = 1; $i < $countarr; $i += 2 ) {
1967 if ( strlen( $arr[$i] ) == 3 ) {
1968 $x1 = substr( $arr[$i - 1], -1 );
1969 $x2 = substr( $arr[$i - 1], -2, 1 );
1970 if ( $x1 === ' ' ) {
1971 if ( $firstspace == -1 ) {
1972 $firstspace = $i;
1973 }
1974 } elseif ( $x2 === ' ' ) {
1975 $firstsingleletterword = $i;
1976 // if $firstsingleletterword is set, we don't
1977 // look at the other options, so we can bail early.
1978 break;
1979 } elseif ( $firstmultiletterword == -1 ) {
1980 $firstmultiletterword = $i;
1981 }
1982 }
1983 }
1984
1985 // If there is a single-letter word, use it!
1986 if ( $firstsingleletterword > -1 ) {
1987 $arr[$firstsingleletterword] = "''";
1988 $arr[$firstsingleletterword - 1] .= "'";
1989 } elseif ( $firstmultiletterword > -1 ) {
1990 // If not, but there's a multi-letter word, use that one.
1991 $arr[$firstmultiletterword] = "''";
1992 $arr[$firstmultiletterword - 1] .= "'";
1993 } elseif ( $firstspace > -1 ) {
1994 // ... otherwise use the first one that has neither.
1995 // (notice that it is possible for all three to be -1 if, for example,
1996 // there is only one pentuple-apostrophe in the line)
1997 $arr[$firstspace] = "''";
1998 $arr[$firstspace - 1] .= "'";
1999 }
2000 }
2001
2002 // Now let's actually convert our apostrophic mush to HTML!
2003 $output = '';
2004 $buffer = '';
2005 $state = '';
2006 $i = 0;
2007 foreach ( $arr as $r ) {
2008 if ( ( $i % 2 ) == 0 ) {
2009 if ( $state === 'both' ) {
2010 $buffer .= $r;
2011 } else {
2012 $output .= $r;
2013 }
2014 } else {
2015 $thislen = strlen( $r );
2016 if ( $thislen == 2 ) {
2017 // two quotes - open or close italics
2018 if ( $state === 'i' ) {
2019 $output .= '</i>';
2020 $state = '';
2021 } elseif ( $state === 'bi' ) {
2022 $output .= '</i>';
2023 $state = 'b';
2024 } elseif ( $state === 'ib' ) {
2025 $output .= '</b></i><b>';
2026 $state = 'b';
2027 } elseif ( $state === 'both' ) {
2028 $output .= '<b><i>' . $buffer . '</i>';
2029 $state = 'b';
2030 } else { // $state can be 'b' or ''
2031 $output .= '<i>';
2032 $state .= 'i';
2033 }
2034 } elseif ( $thislen == 3 ) {
2035 // three quotes - open or close bold
2036 if ( $state === 'b' ) {
2037 $output .= '</b>';
2038 $state = '';
2039 } elseif ( $state === 'bi' ) {
2040 $output .= '</i></b><i>';
2041 $state = 'i';
2042 } elseif ( $state === 'ib' ) {
2043 $output .= '</b>';
2044 $state = 'i';
2045 } elseif ( $state === 'both' ) {
2046 $output .= '<i><b>' . $buffer . '</b>';
2047 $state = 'i';
2048 } else { // $state can be 'i' or ''
2049 $output .= '<b>';
2050 $state .= 'b';
2051 }
2052 } elseif ( $thislen == 5 ) {
2053 // five quotes - open or close both separately
2054 if ( $state === 'b' ) {
2055 $output .= '</b><i>';
2056 $state = 'i';
2057 } elseif ( $state === 'i' ) {
2058 $output .= '</i><b>';
2059 $state = 'b';
2060 } elseif ( $state === 'bi' ) {
2061 $output .= '</i></b>';
2062 $state = '';
2063 } elseif ( $state === 'ib' ) {
2064 $output .= '</b></i>';
2065 $state = '';
2066 } elseif ( $state === 'both' ) {
2067 $output .= '<i><b>' . $buffer . '</b></i>';
2068 $state = '';
2069 } else { // ($state == '')
2070 $buffer = '';
2071 $state = 'both';
2072 }
2073 }
2074 }
2075 $i++;
2076 }
2077 // Now close all remaining tags. Notice that the order is important.
2078 if ( $state === 'b' || $state === 'ib' ) {
2079 $output .= '</b>';
2080 }
2081 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2082 $output .= '</i>';
2083 }
2084 if ( $state === 'bi' ) {
2085 $output .= '</b>';
2086 }
2087 // There might be lonely ''''', so make sure we have a buffer
2088 if ( $state === 'both' && $buffer ) {
2089 $output .= '<b><i>' . $buffer . '</i></b>';
2090 }
2091 return $output;
2092 }
2093
2103 private function handleExternalLinks( string $text ): string {
2104 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2105 if ( $bits === false ) {
2106 // T321234: Don't try to fix old revisions with broken UTF-8, just return $text as is
2107 return $text;
2108 }
2109 $s = array_shift( $bits );
2110
2111 $i = 0;
2112 while ( $i < count( $bits ) ) {
2113 $url = $bits[$i++];
2114 $i++; // protocol
2115 $text = $bits[$i++];
2116 $trail = $bits[$i++];
2117
2118 # The characters '<' and '>' (which were escaped by
2119 # internalRemoveHtmlTags()) should not be included in
2120 # URLs, per RFC 2396.
2121 $m2 = [];
2122 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2123 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2124 $url = substr( $url, 0, $m2[0][1] );
2125 }
2126
2127 # If the link text is an image URL, replace it with an <img> tag
2128 # This happened by accident in the original parser, but some people used it extensively
2129 $img = $this->maybeMakeExternalImage( $text );
2130 if ( $img !== false ) {
2131 $text = $img;
2132 }
2133
2134 $dtrail = '';
2135
2136 # Set linktype for CSS
2137 $linktype = 'text';
2138
2139 # No link text, e.g. [http://domain.tld/some.link]
2140 if ( $text == '' ) {
2141 # Autonumber
2142 $langObj = $this->getTargetLanguage();
2143 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2144 $linktype = 'autonumber';
2145 } else {
2146 # Have link text, e.g. [http://domain.tld/some.link text]s
2147 # Check for trail
2148 [ $dtrail, $trail ] = Linker::splitTrail( $trail );
2149 }
2150
2151 // Excluding protocol-relative URLs may avoid many false positives.
2152 if ( preg_match( '/^(?:' . $this->urlUtils->validAbsoluteProtocols() . ')/', $text ) ) {
2153 $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2154 }
2155
2156 $url = Sanitizer::cleanUrl( $url );
2157
2158 # Use the encoded URL
2159 # This means that users can paste URLs directly into the text
2160 # Funny characters like ö aren't valid in URLs anyway
2161 # This was changed in August 2004
2162 $s .= $this->getLinkRenderer()->makeExternalLink(
2163 $url,
2164 new HtmlArmor( $text ),
2165 $this->getTitle(),
2166 $linktype,
2167 $this->getExternalLinkAttribs( $url )
2168 ) . $dtrail . $trail;
2169
2170 # Register link in the output object.
2171 $this->mOutput->addExternalLink( $url );
2172 }
2173
2174 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive from array_shift
2175 return $s;
2176 }
2177
2189 public static function getExternalLinkRel( $url = false, $title = null ): ?string {
2190 wfDeprecated( __METHOD__, '1.47' );
2191 return MediaWikiServices::getInstance()->getLinkRenderer()
2192 ->getExternalLinkRel( $url, $title );
2193 }
2194
2206 public function getExternalLinkAttribs( $url ) {
2207 $attribs = [];
2208 $rel = $this->getLinkRenderer()->getExternalLinkRel( $url, $this->getTitle() ) ?? '';
2209
2210 $target = $this->mOptions->getExternalLinkTarget();
2211 if ( $target ) {
2212 $attribs['target'] = $target;
2213 // T133507/T427561: we used to set additional 'rel' attributes
2214 // here, but no longer need to do so.
2215 }
2216 if ( $rel !== '' ) {
2217 $attribs['rel'] = $rel;
2218 }
2219 return $attribs;
2220 }
2221
2232 public static function normalizeLinkUrl( $url ): string {
2233 # Test for RFC 3986 IPv6 syntax
2234 $scheme = '[a-z][a-z0-9+.-]*:';
2235 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2236 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2237 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2238 IPUtils::isValid( rawurldecode( $m[1] ) )
2239 ) {
2240 $isIPv6 = rawurldecode( $m[1] );
2241 } else {
2242 $isIPv6 = false;
2243 }
2244
2245 # Make sure unsafe characters are encoded
2246 $url = preg_replace_callback(
2247 '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]+/',
2248 static fn ( $m ) => rawurlencode( $m[0] ),
2249 $url
2250 );
2251
2252 $ret = '';
2253 $end = strlen( $url );
2254
2255 # Fragment part - 'fragment'
2256 $start = strpos( $url, '#' );
2257 if ( $start !== false && $start < $end ) {
2258 $ret = self::normalizeUrlComponent(
2259 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2260 $end = $start;
2261 }
2262
2263 # Query part - 'query' minus &=+;
2264 $start = strpos( $url, '?' );
2265 if ( $start !== false && $start < $end ) {
2266 $ret = self::normalizeUrlComponent(
2267 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2268 $end = $start;
2269 }
2270
2271 # Path part - 'pchar', remove dot segments
2272 # (find first '/' after the optional '//' after the scheme)
2273 $start = strpos( $url, '//' );
2274 $start = strpos( $url, '/', $start === false ? 0 : $start + 2 );
2275 if ( $start !== false && $start < $end ) {
2276 $ret = UrlUtils::removeDotSegments( self::normalizeUrlComponent(
2277 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}/?' ) ) . $ret;
2278 $end = $start;
2279 }
2280
2281 # Scheme and host part - 'pchar'
2282 # (we assume no userinfo or encoded colons in the host)
2283 $ret = self::normalizeUrlComponent(
2284 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2285
2286 # Fix IPv6 syntax
2287 if ( $isIPv6 !== false ) {
2288 $ipv6Host = "%5B({$isIPv6})%5D";
2289 $ret = preg_replace(
2290 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2291 "$1[$2]",
2292 $ret
2293 );
2294 }
2295
2296 return $ret;
2297 }
2298
2299 private static function normalizeUrlComponent( string $component, string $unsafe ): string {
2300 $callback = static function ( $matches ) use ( $unsafe ) {
2301 $char = urldecode( $matches[0] );
2302 $ord = ord( $char );
2303 if ( $ord > 32 && $ord < 127 && !str_contains( $unsafe, $char ) ) {
2304 # Unescape it
2305 return $char;
2306 } else {
2307 # Leave it escaped, but use uppercase for a-f
2308 return strtoupper( $matches[0] );
2309 }
2310 };
2311 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2312 }
2313
2322 private function maybeMakeExternalImage( string $url ): string|false {
2323 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2324 $imagesexception = (bool)$imagesfrom;
2325 $text = false;
2326 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2327 if ( $imagesexception && is_array( $imagesfrom ) ) {
2328 $imagematch = false;
2329 foreach ( $imagesfrom as $match ) {
2330 if ( str_starts_with( $url, $match ) ) {
2331 $imagematch = true;
2332 break;
2333 }
2334 }
2335 } elseif ( $imagesexception ) {
2336 $imagematch = str_starts_with( $url, $imagesfrom );
2337 } else {
2338 $imagematch = false;
2339 }
2340
2341 if ( $this->mOptions->getAllowExternalImages()
2342 || ( $imagesexception && $imagematch )
2343 ) {
2344 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2345 # Image found
2346 $text = Linker::makeExternalImage( $url );
2347 }
2348 }
2349 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2350 && preg_match( self::EXT_IMAGE_REGEX, $url )
2351 ) {
2352 $whitelist = explode(
2353 "\n",
2354 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2355 );
2356
2357 foreach ( $whitelist as $entry ) {
2358 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2359 if ( $entry === '' || str_starts_with( $entry, '#' ) ) {
2360 continue;
2361 }
2362 // @phan-suppress-next-line SecurityCheck-ReDoS preg_quote is not wanted here
2363 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2364 # Image matches a whitelist entry
2365 $text = Linker::makeExternalImage( $url );
2366 break;
2367 }
2368 }
2369 }
2370 return $text;
2371 }
2372
2380 private function handleInternalLinks( string $text ): string {
2381 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2382 return $text;
2383 }
2384
2390 private function handleInternalLinks2( &$s ) {
2391 static $tc = false, $e1, $e1_img;
2392 # the % is needed to support urlencoded titles as well
2393 if ( !$tc ) {
2394 $tc = Title::legalChars() . '#%';
2395 # Match a link having the form [[namespace:link|alternate]]trail
2396 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2397 # Match cases where there is no "]]", which might still be images
2398 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2399 }
2400
2401 $holders = new LinkHolderArray(
2402 $this,
2403 $this->getContentLanguageConverter(),
2404 $this->getHookContainer() );
2405
2406 # split the entire text string on occurrences of [[
2407 $a = StringUtils::explode( '[[', ' ' . $s );
2408 # get the first element (all text up to first [[), and remove the space we added
2409 $s = $a->current();
2410 $a->next();
2411 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2412 $s = substr( $s, 1 );
2413
2414 $nottalk = !$this->getTitle()->isTalkPage();
2415
2416 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2417 $e2 = null;
2418 if ( $useLinkPrefixExtension ) {
2419 # Match the end of a line for a word that's not followed by whitespace,
2420 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2421 $charset = $this->contLang->linkPrefixCharset();
2422 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2423 $m = [];
2424 if ( preg_match( $e2, $s, $m ) ) {
2425 $first_prefix = $m[2];
2426 } else {
2427 $first_prefix = false;
2428 }
2429 $prefix = false;
2430 } else {
2431 $first_prefix = false;
2432 $prefix = '';
2433 }
2434
2435 # Some namespaces don't allow subpages
2436 $useSubpages = $this->nsInfo->hasSubpages(
2437 $this->getTitle()->getNamespace()
2438 );
2439
2440 # Loop for each link
2441 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2442 # Check for excessive memory usage
2443 if ( $holders->isBig() ) {
2444 # Too big
2445 # Do the existence check, replace the link holders and clear the array
2446 $holders->replace( $s );
2447 $holders->clear();
2448 }
2449
2450 if ( $useLinkPrefixExtension ) {
2451 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal $e2 is set under this condition
2452 if ( preg_match( $e2, $s, $m ) ) {
2453 [ , $s, $prefix ] = $m;
2454 } else {
2455 $prefix = '';
2456 }
2457 # first link
2458 if ( $first_prefix ) {
2459 $prefix = $first_prefix;
2460 $first_prefix = false;
2461 }
2462 }
2463
2464 $might_be_img = false;
2465
2466 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2467 $text = $m[2];
2468 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2469 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2470 # the real problem is with the $e1 regex
2471 # See T3300.
2472 # Still some problems for cases where the ] is meant to be outside punctuation,
2473 # and no image is in sight. See T4095.
2474 if ( $text !== ''
2475 && substr( $m[3], 0, 1 ) === ']'
2476 && strpos( $text, '[' ) !== false
2477 ) {
2478 $text .= ']'; # so that handleExternalLinks($text) works later
2479 $m[3] = substr( $m[3], 1 );
2480 }
2481 # fix up urlencoded title texts
2482 if ( str_contains( $m[1], '%' ) ) {
2483 # Should anchors '#' also be rejected?
2484 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2485 }
2486 $trail = $m[3];
2487 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2488 # Invalid, but might be an image with a link in its caption
2489 $might_be_img = true;
2490 $text = $m[2];
2491 if ( str_contains( $m[1], '%' ) ) {
2492 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2493 }
2494 $trail = "";
2495 } else { # Invalid form; output directly
2496 $s .= $prefix . '[[' . $line;
2497 continue;
2498 }
2499
2500 $origLink = ltrim( $m[1], ' ' );
2501
2502 # Don't allow internal links to pages containing
2503 # PROTO: where PROTO is a valid URL protocol; these
2504 # should be external links.
2505 if ( preg_match( '/^(?i:' . $this->urlUtils->validProtocols() . ')/', $origLink ) ) {
2506 $s .= $prefix . '[[' . $line;
2507 continue;
2508 }
2509
2510 # Make subpage if necessary
2511 if ( $useSubpages ) {
2512 $link = Linker::normalizeSubpageLink(
2513 $this->getTitle(), $origLink, $text
2514 );
2515 } else {
2516 $link = $origLink;
2517 }
2518
2519 // \x7f isn't a default legal title char, so most likely strip
2520 // markers will force us into the "invalid form" path above. But,
2521 // just in case, let's assert that xmlish tags aren't valid in
2522 // the title position.
2523 $unstrip = $this->mStripState->killMarkers( $link );
2524 $noMarkers = ( $unstrip === $link );
2525
2526 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2527 if ( $nt === null ) {
2528 $s .= $prefix . '[[' . $line;
2529 continue;
2530 }
2531
2532 $ns = $nt->getNamespace();
2533 $iw = $nt->getInterwiki();
2534
2535 $noforce = !str_starts_with( $origLink, ':' );
2536
2537 if ( $might_be_img ) { # if this is actually an invalid link
2538 if ( $ns === NS_FILE && $noforce ) { # but might be an image
2539 $found = false;
2540 while ( true ) {
2541 # look at the next 'line' to see if we can close it there
2542 $a->next();
2543 $next_line = $a->current();
2544 if ( $next_line === false || $next_line === null ) {
2545 break;
2546 }
2547 $m = explode( ']]', $next_line, 3 );
2548 if ( count( $m ) == 3 ) {
2549 # the first ]] closes the inner link, the second the image
2550 $found = true;
2551 $text .= "[[{$m[0]}]]{$m[1]}";
2552 $trail = $m[2];
2553 break;
2554 } elseif ( count( $m ) == 2 ) {
2555 # if there's exactly one ]] that's fine, we'll keep looking
2556 $text .= "[[{$m[0]}]]{$m[1]}";
2557 } else {
2558 # if $next_line is invalid too, we need look no further
2559 $text .= '[[' . $next_line;
2560 break;
2561 }
2562 }
2563 if ( !$found ) {
2564 # we couldn't find the end of this imageLink, so output it raw
2565 # but don't ignore what might be perfectly normal links in the text we've examined
2566 $holders->merge( $this->handleInternalLinks2( $text ) );
2567 $s .= "{$prefix}[[$link|$text";
2568 # note: no $trail, because without an end, there *is* no trail
2569 continue;
2570 }
2571 } else { # it's not an image, so output it raw
2572 $s .= "{$prefix}[[$link|$text";
2573 # note: no $trail, because without an end, there *is* no trail
2574 continue;
2575 }
2576 }
2577
2578 $wasblank = ( $text == '' );
2579 if ( $wasblank ) {
2580 $text = $link;
2581 if ( !$noforce ) {
2582 # Strip off leading ':'
2583 $text = substr( $text, 1 );
2584 }
2585 } else {
2586 # T6598 madness. Handle the quotes only if they come from the alternate part
2587 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2588 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2589 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2590 $text = $this->doQuotes( $text );
2591 }
2592
2593 # Link not escaped by : , create the various objects
2594 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2595 # Interwikis
2596 if (
2597 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2598 $this->languageNameUtils->getLanguageName(
2599 $iw,
2600 LanguageNameUtils::AUTONYMS,
2601 LanguageNameUtils::DEFINED
2602 )
2603 || in_array( $iw, $this->svcOptions->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) )
2604 )
2605 ) {
2606 # T26502: duplicates are resolved in ParserOutput
2607 $this->mOutput->addLanguageLink( $nt );
2608
2613 $s = preg_replace( '/\n\s*$/', '', $s . $prefix ) . $trail;
2614 continue;
2615 }
2616
2617 if ( $ns === NS_FILE ) {
2618 if ( $wasblank ) {
2619 # if no parameters were passed, $text
2620 # becomes something like "File:Foo.png",
2621 # which we don't want to pass on to the
2622 # image generator
2623 $text = '';
2624 } else {
2625 # recursively parse links inside the image caption
2626 # actually, this will parse them in any other parameters, too,
2627 # but it might be hard to fix that, and it doesn't matter ATM
2628 $text = $this->handleExternalLinks( $text );
2629 $holders->merge( $this->handleInternalLinks2( $text ) );
2630 }
2631 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2632 $s .= $prefix . $this->armorLinks(
2633 $this->makeImageInternal( $nt, $text, $holders ) ) . $trail;
2634 continue;
2635 } elseif ( $ns === NS_CATEGORY ) {
2636 # Strip newlines from the left hand context of Category
2637 # links.
2638 # See T2087, T87753, T174639, T359886
2639 $s = preg_replace( '/\n\s*$/', '', $s . $prefix ) . $trail;
2640
2641 $sortkey = ''; // filled in by CategoryLinksTable
2642 if ( !$wasblank ) {
2643 $sortkey = $text;
2644 }
2645 $this->mOutput->addCategory( $nt, $sortkey );
2646
2647 continue;
2648 }
2649 }
2650
2651 # Self-link checking. For some languages, variants of the title are checked in
2652 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2653 # for linking to a different variant.
2654 if ( $ns !== NS_SPECIAL && $nt->equals( $this->getTitle() ) ) {
2655 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail, '',
2656 Sanitizer::escapeIdForLink( $nt->getFragment() ) );
2657 continue;
2658 }
2659
2660 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2661 # @todo FIXME: Should do batch file existence checks, see comment below
2662 if ( $ns === NS_MEDIA ) {
2663 # Give extensions a chance to select the file revision for us
2664 $options = [];
2665 $descQuery = false;
2666 $this->hookRunner->onBeforeParserFetchFileAndTitle(
2667 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
2668 $this, $nt, $options, $descQuery
2669 );
2670 # Fetch and register the file (file title may be different via hooks)
2671 [ $file, $nt ] = $this->fetchFileAndTitle( $nt, $options );
2672 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2673 $s .= $prefix . $this->armorLinks(
2674 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2675 continue;
2676 }
2677
2678 # Some titles, such as valid special pages or files in foreign repos, should
2679 # be shown as bluelinks even though they're not included in the page table
2680 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2681 # batch file existence checks for NS_FILE and NS_MEDIA
2682 if ( $iw == '' && $nt->isAlwaysKnown() ) {
2683 $this->mOutput->addLink( $nt );
2684 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2685 } else {
2686 # Links will be added to the output link list after checking
2687 $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2688 }
2689 }
2690 return $holders;
2691 }
2692
2706 private function makeKnownLinkHolder(
2707 LinkTarget $nt, string $text = '',
2708 string $trail = '', string $prefix = ''
2709 ): string {
2710 [ $inside, $trail ] = Linker::splitTrail( $trail );
2711
2712 if ( $text == '' ) {
2713 $text = htmlspecialchars( $this->titleFormatter->getPrefixedText( $nt ) );
2714 }
2715
2716 $link = $this->getLinkRenderer()->makeKnownLink(
2717 $nt, new HtmlArmor( "$prefix$text$inside" )
2718 );
2719
2720 return $this->armorLinks( $link ) . $trail;
2721 }
2722
2733 private function armorLinks( string $text ): string {
2734 return preg_replace( '/\b((?i)' . $this->urlUtils->validProtocols() . ')/',
2735 self::MARKER_PREFIX . "NOPARSE$1", $text );
2736 }
2737
2746 private function expandMagicVariable( string $index, $frame = false ): string {
2751 if ( isset( $this->mVarCache[$index] ) ) {
2752 return $this->mVarCache[$index];
2753 }
2754
2755 $value = CoreMagicVariables::expand(
2756 $this, $index, new MWTimestamp( $this->getParseTime() ),
2757 $this->svcOptions, $this->logger, $frame
2758 );
2759
2760 if ( $value === null ) {
2761 // Not a defined core magic word
2762 // Don't give this hook unrestricted access to mVarCache
2763 $fakeCache = [];
2764 $this->hookRunner->onParserGetVariableValueSwitch(
2765 // @phan-suppress-next-line PhanTypeMismatchArgument $value is passed as null but returned as string
2766 $this, $fakeCache, $index, $value, $frame
2767 );
2768 // Cache the value returned by the hook by falling through here.
2769 if ( $value === null ) {
2770 // T419880: If a magic word ID was registered with the
2771 // GetMagicVariableIDs hook, the registering extension should
2772 // *not* let that $value remain `null` when the
2773 // ParserGetVariableValueSwitch hook is invoked.
2775 __METHOD__ . " called hook for $index and got null",
2776 "1.46"
2777 );
2778 $value = '';
2779 }
2780 }
2781
2782 $this->mVarCache[$index] = $value;
2783
2784 return $value;
2785 }
2786
2791 private function initializeVariables() {
2792 $variableIDs = $this->magicWordFactory->getVariableIDs();
2793
2794 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2795 $this->mSubstWords = $this->magicWordFactory->getSubstArray();
2796 }
2797
2816 public function preprocessToDom( $text, $flags = 0 ) {
2817 return $this->getPreprocessor()->preprocessToObj( $text, $flags );
2818 }
2819
2846 public function replaceVariables(
2847 $text, $frame = false, $argsOnly = false, array $options = []
2848 ): string {
2849 # Is there any text? Also, Prevent too big inclusions!
2850 $textSize = strlen( $text );
2851 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2852 return $text;
2853 }
2854
2855 if ( $frame === false ) {
2856 $frame = $this->getPreprocessor()->newFrame();
2857 } elseif ( !( $frame instanceof PPFrame ) ) {
2859 __METHOD__ . " called using plain parameters instead of " .
2860 "a PPFrame instance. Creating custom frame.",
2861 '1.43'
2862 );
2863 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2864 }
2865
2866 $ppFlags = 0;
2867 if ( $options['parsoidTopLevelCall'] ?? false ) {
2868 $ppFlags |= Preprocessor::START_IN_SOL_STATE;
2869 }
2870 $dom = $this->preprocessToDom( $text, $ppFlags );
2871 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2872 return $frame->expand( $dom, $flags );
2873 }
2874
2876 public function setUseParsoidFragments( bool $val ) {
2877 $this->useParsoidFragments = $val;
2878 }
2879
2881 public function useParsoidFragments(): bool {
2882 return $this->useParsoidFragments;
2883 }
2884
2912 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2913 # does no harm if $current and $max are present but are unnecessary for the message
2914 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2915 # only during preview, and that would split the parser cache unnecessarily.
2916 $this->mOutput->addWarningMsg(
2917 "$limitationType-warning",
2918 Message::numParam( $current ),
2919 Message::numParam( $max )
2920 );
2921 $this->addTrackingCategory( "$limitationType-category" );
2922 }
2923
2937 public function braceSubstitution( array $piece, PPFrame $frame ): string|array {
2938 // Flags
2939
2940 // $text has been filled
2941 $found = false;
2942 $text = '';
2943 // wiki markup in $text should be escaped
2944 $nowiki = false;
2945 // $text is HTML, armour it against most wikitext transformation
2946 // (it still participates in doBlockLevels, language conversion,
2947 // and the other steps at the start of ::internalParseHalfParsed)
2948 $isHTML = false;
2949 // $text is raw HTML, armour it against all wikitext transformation
2950 $isRawHTML = false;
2951 // Force interwiki transclusion to be done in raw mode not rendered
2952 $forceRawInterwiki = false;
2953 // $text is a DOM node needing expansion in a child frame
2954 $isChildObj = false;
2955 // $text is a DOM node needing expansion in the current frame
2956 $isLocalObj = false;
2957
2958 # Title object, where $text came from
2959 $title = false;
2960
2961 # $part1 is the bit before the first |, and must contain only title characters.
2962 # Various prefixes will be stripped from it later.
2963 $titleWithSpaces = $frame->expand( $piece['title'] );
2964 $part1 = trim( $titleWithSpaces );
2965 $titleText = false;
2966
2967 # Original title text preserved for various purposes
2968 $originalTitle = $part1;
2969
2970 # $args is a list of argument nodes, starting from index 0, not including $part1
2971 $args = $piece['parts'];
2972
2973 $profileSection = null; // profile templates
2974
2975 $sawDeprecatedTemplateEquals = false; // T91154
2976
2977 $isParsoid = $this->mOptions->getUseParsoid();
2978
2979 # SUBST
2980 // @phan-suppress-next-line PhanImpossibleCondition
2981 if ( !$found ) {
2982 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2983 $part1 = trim( $part1 );
2984
2985 # Possibilities for substMatch: "subst", "safesubst" or FALSE
2986 # Decide whether to expand template or keep wikitext as-is.
2987 if ( $this->ot['wiki'] ) {
2988 if ( $substMatch === false ) {
2989 $literal = true; # literal when in PST with no prefix
2990 } else {
2991 $literal = false; # expand when in PST with subst: or safesubst:
2992 }
2993 } else {
2994 if ( $substMatch == 'subst' ) {
2995 $literal = true; # literal when not in PST with plain subst:
2996 } else {
2997 $literal = false; # expand when not in PST with safesubst: or no prefix
2998 }
2999 }
3000 if ( $literal ) {
3001 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3002 $isLocalObj = true;
3003 $found = true;
3004 }
3005 }
3006
3007 # Variables
3008 if ( !$found && $args->getLength() == 0 ) {
3009 $id = $this->mVariables->matchStartToEnd( $part1 );
3010 if ( $id !== false ) {
3011 if ( str_contains( $part1, ':' ) ) {
3013 'Registering a magic variable with a name including a colon',
3014 '1.39', false, false
3015 );
3016 }
3017 $text = $this->expandMagicVariable( $id, $frame );
3018 $found = true;
3019 }
3020 }
3021
3022 # MSG, MSGNW and RAW
3023 if ( !$found ) {
3024 # Check for MSGNW:
3025 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3026 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3027 $nowiki = true;
3028 } else {
3029 # Remove obsolete MSG:
3030 $mwMsg = $this->magicWordFactory->get( 'msg' );
3031 $mwMsg->matchStartAndRemove( $part1 );
3032 }
3033
3034 # Check for RAW:
3035 $mwRaw = $this->magicWordFactory->get( 'raw' );
3036 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3037 $forceRawInterwiki = true;
3038 }
3039 }
3040
3041 # Parser functions
3042 if ( !$found ) {
3043 // Allow colon or Japanese double-width colon as arg delimiter
3044 if ( preg_match( '/[::]/u', $part1, $colonMatches, PREG_OFFSET_CAPTURE ) ) {
3045 [ $colonStr, $colonPos ] = $colonMatches[0];
3046 $func = substr( $part1, 0, $colonPos );
3047 $funcArgs = [ trim( substr( $part1, $colonPos + strlen( $colonStr ) ) ) ];
3048 $argsLength = $args->getLength();
3049 for ( $i = 0; $i < $argsLength; $i++ ) {
3050 $funcArgs[] = $args->item( $i );
3051 }
3052
3053 $result = $this->callParserFunction(
3054 $frame, $func, $funcArgs, $isParsoid && $piece['lineStart']
3055 );
3056
3057 // Extract any forwarded flags
3058 if ( isset( $result['title'] ) ) {
3059 $title = $result['title'];
3060 }
3061 if ( isset( $result['found'] ) ) {
3062 $found = $result['found'];
3063 }
3064 if ( array_key_exists( 'text', $result ) ) {
3065 // a string or null
3066 $text = $result['text'];
3067 }
3068 if ( isset( $result['nowiki'] ) ) {
3069 $nowiki = $result['nowiki'];
3070 }
3071 if ( isset( $result['isHTML'] ) ) {
3072 $isHTML = $result['isHTML'];
3073 }
3074 if ( isset( $result['isRawHTML'] ) ) {
3075 $isRawHTML = $result['isRawHTML'];
3076 }
3077 if ( isset( $result['forceRawInterwiki'] ) ) {
3078 $forceRawInterwiki = $result['forceRawInterwiki'];
3079 }
3080 if ( isset( $result['isChildObj'] ) ) {
3081 $isChildObj = $result['isChildObj'];
3082 }
3083 if ( isset( $result['isLocalObj'] ) ) {
3084 $isLocalObj = $result['isLocalObj'];
3085 }
3086 }
3087 }
3088
3089 # Finish mangling title and then check for loops.
3090 # Set $title to a Title object and $titleText to the PDBK
3091 if ( !$found ) {
3092 $ns = NS_TEMPLATE;
3093 # Split the title into page and subpage
3094 $subpage = '';
3095 $relative = Linker::normalizeSubpageLink(
3096 $this->getTitle(), $part1, $subpage
3097 );
3098 if ( $part1 !== $relative ) {
3099 $part1 = $relative;
3100 $ns = $this->getTitle()->getNamespace();
3101 }
3102 $title = Title::newFromText( $part1, $ns );
3103 if ( $title ) {
3104 $titleText = $title->getPrefixedText();
3105 # Check for language variants if the template is not found
3106 if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3107 $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3108 }
3109 # Do recursion depth check
3110 $limit = $this->mOptions->getMaxTemplateDepth();
3111 if ( $frame->depth >= $limit ) {
3112 $found = true;
3113 $text = '<span class="error">'
3114 . wfMessage( 'parser-template-recursion-depth-warning' )
3115 ->numParams( $limit )->inContentLanguage()->text()
3116 . '</span>';
3117 }
3118 }
3119 }
3120
3121 # Load from database
3122 if ( !$found && $title ) {
3123 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3124 if ( !$title->isExternal() ) {
3125 if ( $title->isSpecialPage()
3126 && $this->mOptions->getAllowSpecialInclusion()
3127 && ( $this->ot['html'] || ( $this->useParsoidFragments && $this->ot['pre'] ) )
3128 ) {
3129 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3130 // Pass the template arguments as URL parameters.
3131 // "uselang" will have no effect since the Language object
3132 // is forced to the one defined in ParserOptions.
3133 $pageArgs = [];
3134 $argsLength = $args->getLength();
3135 for ( $i = 0; $i < $argsLength; $i++ ) {
3136 $bits = $args->item( $i )->splitArg();
3137 if ( strval( $bits['index'] ) === '' ) {
3138 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3139 $value = trim( $frame->expand( $bits['value'] ) );
3140 $pageArgs[$name] = $value;
3141 }
3142 }
3143
3144 // Create a new context to execute the special page, that is expensive
3145 if ( $this->incrementExpensiveFunctionCount() ) {
3146 $context = new RequestContext;
3147 $context->setTitle( $title );
3148 $context->setRequest( new FauxRequest( $pageArgs ) );
3149 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3150 $context->setUser( $this->userFactory->newFromUserIdentity( $this->getUserIdentity() ) );
3151 } else {
3152 // If this page is cached, then we better not be per user.
3153 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3154 }
3155 $context->setLanguage( $this->mOptions->getUserLangObj() );
3156 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3157 if ( $ret ) {
3158 $text = $context->getOutput()->getHTML();
3159 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3160 $found = true;
3161 $isHTML = true;
3162 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3163 $this->mOutput->updateRuntimeAdaptiveExpiry(
3164 $specialPage->maxIncludeCacheTime()
3165 );
3166 }
3167 }
3168 }
3169 } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3170 $found = false; # access denied
3171 $this->logger->debug(
3172 __METHOD__ .
3173 ": template inclusion denied for " . $title->getPrefixedDBkey()
3174 );
3175 } else {
3176 [ $text, $title ] = $this->getTemplateDom( $title, $isParsoid && $piece['lineStart'] );
3177 if ( $text !== false ) {
3178 $found = true;
3179 $isChildObj = true;
3180 if (
3181 $title->getNamespace() === NS_TEMPLATE &&
3182 $title->getDBkey() === '=' &&
3183 $originalTitle === '='
3184 ) {
3185 // Note that we won't get here if `=` is evaluated
3186 // (in the future) as a parser function, nor if
3187 // the Template namespace is given explicitly,
3188 // ie `{{Template:=}}`. Only `{{=}}` triggers.
3189 $sawDeprecatedTemplateEquals = true; // T91154
3190 }
3191 }
3192 }
3193
3194 # If the title is valid but undisplayable, make a link to it
3195 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3196 $text = "[[:$titleText]]";
3197 $found = true;
3198 }
3199 } elseif ( $title->isTrans() ) {
3200 # Interwiki transclusion
3201 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3202 $text = $this->interwikiTransclude( $title, 'render' );
3203 $isHTML = true;
3204 } else {
3205 $text = $this->interwikiTransclude( $title, 'raw' );
3206 # Preprocess it like a template
3207 $sol = ( $isParsoid && $piece['lineStart'] ) ? Preprocessor::START_IN_SOL_STATE : 0;
3208 $text = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION | $sol );
3209 $isChildObj = true;
3210 }
3211 $found = true;
3212 }
3213
3214 # Do infinite loop check
3215 # This has to be done after redirect resolution to avoid infinite loops via redirects
3216 if ( !$frame->loopCheck( $title ) ) {
3217 $found = true;
3218 $text = '<span class="error">'
3219 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3220 . '</span>';
3221 $this->addTrackingCategory( 'template-loop-category' );
3222 $this->mOutput->addWarningMsg(
3223 'template-loop-warning',
3224 wfEscapeWikiText( $titleText )
3225 );
3226 $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3227 }
3228 }
3229
3230 # If we haven't found text to substitute by now, we're done
3231 # Recover the source wikitext and return it
3232 if ( !$found ) {
3233 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3234 if ( $profileSection ) {
3235 $this->mProfiler->scopedProfileOut( $profileSection );
3236 }
3237 return [ 'object' => $text ];
3238 }
3239
3240 # Expand DOM-style return values in a child frame
3241 if ( $isChildObj ) {
3242 # Clean up argument array
3243 $newFrame = $frame->newChild( $args, $title );
3244
3245 if ( $nowiki ) {
3246 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3247 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3248 # Expansion is eligible for the empty-frame cache
3249 $text = $newFrame->cachedExpand( $titleText, $text );
3250 } else {
3251 # Uncached expansion
3252 $text = $newFrame->expand( $text );
3253 }
3254 }
3255 if ( $isLocalObj && $nowiki ) {
3256 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3257 $isLocalObj = false;
3258 }
3259
3260 if ( $profileSection ) {
3261 $this->mProfiler->scopedProfileOut( $profileSection );
3262 }
3263 if (
3264 $sawDeprecatedTemplateEquals &&
3265 $this->mStripState->unstripBoth( $text ) !== '='
3266 ) {
3267 // T91154: {{=}} is deprecated when it doesn't expand to `=`;
3268 // use {{Template:=}} if you must.
3269 $this->addTrackingCategory( 'template-equals-category' );
3270 $this->mOutput->addWarningMsg( 'template-equals-warning' );
3271 }
3272
3273 # Replace raw HTML by a placeholder
3274 if ( $isHTML ) {
3275 // @phan-suppress-next-line SecurityCheck-XSS
3276 $text = $this->insertStripItem( $text );
3277 } elseif ( $isRawHTML ) {
3278 $marker = self::MARKER_PREFIX . "-pf-"
3279 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3280 // use 'nowiki' type to protect this from doBlockLevels,
3281 // language conversion, etc.
3282 // @phan-suppress-next-line SecurityCheck-XSS
3283 $this->mStripState->addNoWiki( $marker, $text );
3284 $text = $marker;
3285 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3286 # Escape nowiki-style return values
3287 // @phan-suppress-next-line SecurityCheck-DoubleEscaped
3288 $text = wfEscapeWikiText( $text );
3289 } elseif ( is_string( $text )
3290 && !$piece['lineStart']
3291 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3292 ) {
3293 // T2529: if the template begins with a table or block-level
3294 // element, it should be treated as beginning a new line.
3295 // This behavior is somewhat controversial.
3296 //
3297 // T382464: Parsoid sets $piece['lineStart'] at top-level when
3298 // expanding templates, so this hack is restricted to nested expansions.
3299 $text = "\n" . $text;
3300 }
3301
3302 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3303 # Error, oversize inclusion
3304 if ( $titleText !== false ) {
3305 # Make a working, properly escaped link if possible (T25588)
3306 $text = "[[:$titleText]]";
3307 } else {
3308 # This will probably not be a working link, but at least it may
3309 # provide some hint of where the problem is
3310 $originalTitle = preg_replace( '/^:/', '', $originalTitle );
3311 $text = "[[:$originalTitle]]";
3312 }
3313 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3314 . 'post-expand include size too large -->' );
3315 $this->limitationWarn( 'post-expand-template-inclusion' );
3316 }
3317
3318 if ( $isLocalObj ) {
3319 $ret = [ 'object' => $text ];
3320 } else {
3321 $ret = [ 'text' => $text ];
3322 }
3323
3324 return $ret;
3325 }
3326
3352 public function callParserFunction( PPFrame $frame, $function, array $args = [], bool $inSolState = false ) {
3353 # Case sensitive functions
3354 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3355 $function = $this->mFunctionSynonyms[1][$function];
3356 } else {
3357 # Case insensitive functions
3358 $function = $this->contLang->lc( $function );
3359 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3360 $function = $this->mFunctionSynonyms[0][$function];
3361 } else {
3362 return [ 'found' => false ];
3363 }
3364 }
3365
3366 [ $callback, $flags ] = $this->mFunctionHooks[$function];
3367
3368 $allArgs = [ $this ];
3369 if ( $flags & self::SFH_OBJECT_ARGS ) {
3370 # Convert arguments to PPNodes and collect for appending to $allArgs
3371 $funcArgs = [];
3372 foreach ( $args as $k => $v ) {
3373 if ( $v instanceof PPNode || $k === 0 ) {
3374 $funcArgs[] = $v;
3375 } else {
3376 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3377 }
3378 }
3379
3380 # Add a frame parameter, and pass the arguments as an array
3381 $allArgs[] = $frame;
3382 $allArgs[] = $funcArgs;
3383 } else {
3384 # Convert arguments to plain text and append to $allArgs
3385 foreach ( $args as $k => $v ) {
3386 if ( $v instanceof PPNode ) {
3387 $allArgs[] = trim( $frame->expand( $v ) );
3388 } elseif ( is_int( $k ) && $k >= 0 ) {
3389 $allArgs[] = trim( $v );
3390 } else {
3391 $allArgs[] = trim( "$k=$v" );
3392 }
3393 }
3394 }
3395
3396 $result = $callback( ...$allArgs );
3397
3398 # The interface for function hooks allows them to return a wikitext
3399 # string or an array containing the string and any flags. This mungs
3400 # things around to match what this method should return.
3401 if ( !is_array( $result ) ) {
3402 $result = [
3403 'found' => true,
3404 'text' => $result,
3405 ];
3406 } else {
3407 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3408 $result['text'] = $result[0];
3409 }
3410 unset( $result[0] );
3411 $result += [
3412 'found' => true,
3413 ];
3414 }
3415
3416 $noparse = $result['noparse'] ?? true;
3417 if ( !$noparse ) {
3418 $preprocessFlags = $result['preprocessFlags'] ?? 0;
3419 if ( $inSolState ) {
3420 $preprocessFlags |= Preprocessor::START_IN_SOL_STATE;
3421 }
3422 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3423 $result['isChildObj'] = true;
3424 }
3425
3426 return $result;
3427 }
3428
3447 public function getTemplateDom( LinkTarget $title, bool $inSolState = false ) {
3448 $cacheTitle = $title;
3449 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3450
3451 if ( isset( $this->mTplRedirCache[$titleKey] ) ) {
3452 [ $ns, $dbk ] = $this->mTplRedirCache[$titleKey];
3453 $title = Title::makeTitle( $ns, $dbk );
3454 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3455 }
3456
3457 // Factor in sol-state in the cache key
3458 $titleKey = "$titleKey:sol=" . ( $inSolState ? "0" : "1" );
3459 if ( isset( $this->mTplDomCache[$titleKey] ) ) {
3460 return [ $this->mTplDomCache[$titleKey], $title ];
3461 }
3462
3463 # Cache miss, go to the database
3464 // FIXME T383919: if $title is changed by this call, caching below
3465 // will be ineffective.
3466 [ $text, $title ] = $this->fetchTemplateAndTitle( $title );
3467
3468 // T299359: Verify that the real title that's actually being transcluded is includable
3469 if ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3470 return [ false, $title ];
3471 }
3472
3473 if ( $text === false ) {
3474 $this->mTplDomCache[$titleKey] = false;
3475 return [ false, $title ];
3476 }
3477
3478 $flags = Preprocessor::DOM_FOR_INCLUSION | ( $inSolState ? Preprocessor::START_IN_SOL_STATE : 0 );
3479 $dom = $this->preprocessToDom( $text, $flags );
3480 $this->mTplDomCache[$titleKey] = $dom;
3481
3482 if ( !$title->isSameLinkAs( $cacheTitle ) ) {
3483 $this->mTplRedirCache[ CacheKeyHelper::getKeyForPage( $cacheTitle ) ] =
3484 [ $title->getNamespace(), $title->getDBkey() ];
3485 }
3486
3487 return [ $dom, $title ];
3488 }
3489
3503 public function fetchCurrentRevisionRecordOfTitle( LinkTarget $link ) {
3504 $cacheKey = CacheKeyHelper::getKeyForPage( $link );
3505 if ( !$this->currentRevisionCache ) {
3506 $this->currentRevisionCache = new MapCacheLRU( 100 );
3507 }
3508 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3509 $title = Title::newFromLinkTarget( $link ); // hook signature compat
3510 $revisionRecord =
3511 // Defaults to Parser::defaultFetchRevisionRecord()
3512 $this->mOptions->getCurrentRevisionRecordCallback()(
3513 $title,
3514 $this
3515 );
3516 if ( $revisionRecord === false ) {
3517 // Parser::defaultFetchRevisionRecord() can return false;
3518 // normalize it to null.
3519 $revisionRecord = null;
3520 }
3521 $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3522 }
3523 return $this->currentRevisionCache->get( $cacheKey );
3524 }
3525
3532 public function isCurrentRevisionOfTitleCached( LinkTarget $link ) {
3533 $key = CacheKeyHelper::getKeyForPage( $link );
3534 return (
3535 $this->currentRevisionCache &&
3536 $this->currentRevisionCache->has( $key )
3537 );
3538 }
3539
3550 public static function defaultFetchRevisionRecord(
3551 RevisionLookup $revisionLookup,
3552 LinkTarget $link,
3553 $parser
3554 ) {
3555 if ( $link instanceof PageIdentity ) {
3556 // probably a Title, just use it.
3557 $page = $link;
3558 } else {
3559 // XXX: use RevisionStore::getPageForLink()!
3560 // ...but get the info for the current revision at the same time?
3561 // Should RevisionStore::getKnownCurrentRevision accept a LinkTarget?
3562 $page = Title::newFromLinkTarget( $link );
3563 }
3564
3565 return $revisionLookup->getKnownLatestRevision( $page );
3566 }
3567
3575 public static function statelessFetchRevisionRecord( LinkTarget $link, $parser = null ) {
3576 wfDeprecated( __METHOD__, '1.47' );
3577 return self::defaultFetchRevisionRecord(
3578 MediaWikiServices::getInstance()->getRevisionLookup(),
3579 $link,
3580 $parser
3581 );
3582 }
3583
3590 public function fetchTemplateAndTitle( LinkTarget $link ) {
3591 // Use Title for compatibility with callbacks and return type
3592 $title = Title::newFromLinkTarget( $link );
3593
3594 // Defaults to Parser::defaultFetchTemplate()
3595 $templateCb = $this->mOptions->getTemplateCallback();
3596 $stuff = $templateCb( $title, $this );
3597 $revRecord = $stuff['revision-record'] ?? null;
3598
3599 $text = $stuff['text'];
3600 if ( is_string( $stuff['text'] ) ) {
3601 // We use U+007F DELETE to distinguish strip markers from regular text
3602 $text = strtr( $text, "\x7f", "?" );
3603 }
3604 $finalTitle = $stuff['finalTitle'] ?? $title;
3605 foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3606 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3607 if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3608 // Self-transclusion; final result may change based on the new page version
3609 try {
3610 $sha1 = $revRecord->getSha1();
3611 } catch ( RevisionAccessException ) {
3612 $sha1 = null;
3613 }
3614 $this->setOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1, 'Self transclusion' );
3615 $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3616 }
3617 }
3618
3619 return [ $text, $finalTitle ];
3620 }
3621
3646 public static function defaultFetchTemplate(
3647 RevisionLookup $revLookup,
3648 HookRunner $hookRunner,
3649 LinkCache $linkCache,
3650 ShadowPageLoader $shadowPageLoader,
3651 $link,
3652 $parser
3653 ) {
3654 $title = Title::castFromLinkTarget( $link ); // for compatibility with return type
3655 $text = $skip = false;
3656 $content = null;
3657 $finalTitle = $title;
3658 $deps = [];
3659 $revRecord = null;
3660 $contextTitle = $parser ? $parser->getTitle() : null;
3661
3662 # Loop to fetch the article, with up to 2 redirects
3663
3664 # Note that $title (including redirect targets) could be
3665 # external; we do allow hooks a chance to redirect the
3666 # external title to a local one (which might be useful), but
3667 # are careful not to add external titles to the dependency
3668 # list. (T362221)
3669
3670 for ( $i = 0; $i < 3 && is_object( $title ); $i++ ) {
3671 # Give extensions a chance to select the revision instead
3672 $revRecord = null; # Assume no hook
3673 $origTitle = $title;
3674 $titleChanged = false;
3676 # The $title is a not a PageIdentity, as it may
3677 # contain fragments or even represent an attempt to transclude
3678 # a broken or otherwise-missing Title, which the hook may
3679 # fix up. Similarly, the $contextTitle may represent a special
3680 # page or other page which "exists" as a parsing context but
3681 # is not in the DB.
3682 $contextTitle, $title,
3683 $skip, $revRecord
3684 );
3685
3686 if ( $skip ) {
3687 $text = false;
3688 if ( !$title->isExternal() ) {
3689 $deps[] = [
3690 'title' => $title,
3691 'page_id' => $title->getArticleID(),
3692 'rev_id' => null
3693 ];
3694 }
3695 break;
3696 }
3697 # Get the revision
3698 if ( !$revRecord ) {
3699 if ( $parser ) {
3700 $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3701 } else {
3702 $revRecord = $revLookup->getRevisionByTitle( $title );
3703 }
3704 }
3705 if ( $revRecord ) {
3706 # Update title, as $revRecord may have been changed by hook
3707 $title = Title::newFromPageIdentity( $revRecord->getPage() );
3708 // Assuming title is not external if we've got a $revRecord
3709 $deps[] = [
3710 'title' => $title,
3711 'page_id' => $revRecord->getPageId(),
3712 'rev_id' => $revRecord->getId(),
3713 ];
3714 } elseif ( !$title->isExternal() ) {
3715 $deps[] = [
3716 'title' => $title,
3717 'page_id' => $title->getArticleID(),
3718 'rev_id' => null,
3719 ];
3720 }
3721 if ( !$title->equals( $origTitle ) ) {
3722 # If we fetched a rev from a different title, register
3723 # the original title too...
3724 if ( !$origTitle->isExternal() ) {
3725 $deps[] = [
3726 'title' => $origTitle,
3727 'page_id' => $origTitle->getArticleID(),
3728 'rev_id' => null,
3729 ];
3730 }
3731 $titleChanged = true;
3732 }
3733 # If there is no current revision, there is no page
3734 if ( $revRecord === null || $revRecord->getId() === null ) {
3735 $linkCache->addBadLinkObj( $title );
3736 }
3737 if ( $revRecord ) {
3738 if ( $titleChanged && !$revRecord->hasSlot( SlotRecord::MAIN ) ) {
3739 // We've added this (missing) title to the dependencies;
3740 // give the hook another chance to redirect it to an
3741 // actual page.
3742 $text = false;
3743 $finalTitle = $title;
3744 continue;
3745 }
3746 if ( $revRecord->hasSlot( SlotRecord::MAIN ) ) { // T276476
3747 $content = $revRecord->getContent( SlotRecord::MAIN );
3748 $text = $content ? $content->getWikitextForTransclusion() : null;
3749 } else {
3750 $text = false;
3751 }
3752
3753 if ( $text === false || $text === null ) {
3754 $text = false;
3755 break;
3756 }
3757 } else {
3758 $content = $shadowPageLoader->get( $title )?->getContentForTransclusion();
3759 if ( $content ) {
3760 $text = $content->getWikitextForTransclusion();
3761 } else {
3762 $text = false;
3763 }
3764 break;
3765 }
3766 if ( !$content ) {
3767 break;
3768 }
3769 # Redirect?
3770 $finalTitle = $title;
3771 $title = $content->getRedirectTarget();
3772 }
3773
3774 $retValues = [
3775 // previously, when this also returned a Revision object, we set
3776 // 'revision-record' to false instead of null if it was unavailable,
3777 // so that callers to use isset and then rely on the revision-record
3778 // key instead of the revision key, even if there was no corresponding
3779 // object - we continue to set to false here for backwards compatibility
3780 'revision-record' => $revRecord ?: false,
3781 'text' => $text,
3782 'finalTitle' => $finalTitle,
3783 'deps' => $deps
3784 ];
3785 return $retValues;
3786 }
3787
3795 public static function statelessFetchTemplate( $page, $parser = false ) {
3796 $services = MediaWikiServices::getInstance();
3797 return self::defaultFetchTemplate(
3798 $services->getRevisionLookup(),
3799 new HookRunner( $services->getHookContainer() ),
3800 $services->getLinkCache(),
3801 $services->getShadowPageLoader(),
3802 $page,
3803 $parser
3804 );
3805 }
3806
3815 public function fetchFileAndTitle( LinkTarget $link, array $options = [] ) {
3816 $file = $this->fetchFileNoRegister( $link, $options );
3817
3818 $time = $file ? $file->getTimestamp() : false;
3819 $sha1 = $file ? $file->getSha1() : false;
3820 # Register the file as a dependency...
3821 $this->mOutput->addImage( $link, $time, $sha1 );
3822 if ( $file && !$link->isSameLinkAs( $file->getTitle() ) ) {
3823 # Update fetched file title after resolving redirects, etc.
3824 $link = $file->getTitle();
3825 $this->mOutput->addImage( $link, $time, $sha1 );
3826 }
3827
3828 $title = Title::newFromLinkTarget( $link ); // for return type compat
3829 return [ $file, $title ];
3830 }
3831
3842 protected function fetchFileNoRegister( LinkTarget $link, array $options = [] ) {
3843 if ( isset( $options['broken'] ) ) {
3844 $file = false; // broken thumbnail forced by hook
3845 } else {
3846 if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3847 $file = $this->repoGroup->findFileFromKey( $options['sha1'], $options );
3848 } else { // get by (name,timestamp)
3849 $link = TitleValue::newFromLinkTarget( $link );
3850 $file = $this->repoGroup->findFile( $link, $options );
3851 }
3852 }
3853 return $file;
3854 }
3855
3865 public function interwikiTransclude( LinkTarget $link, $action ): string {
3866 if ( !$this->svcOptions->get( MainConfigNames::EnableScaryTranscluding ) ) {
3867 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3868 }
3869
3870 // TODO: extract relevant functionality from Title
3871 $title = Title::newFromLinkTarget( $link );
3872
3873 $url = $title->getFullURL( [ 'action' => $action ] );
3874 if ( strlen( $url ) > 1024 ) {
3875 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3876 }
3877
3878 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3879
3880 $fname = __METHOD__;
3881
3882 $cache = $this->wanCache;
3883 $data = $cache->getWithSetCallback(
3884 $cache->makeGlobalKey(
3885 'interwiki-transclude',
3886 ( $wikiId !== false ) ? $wikiId : 'external',
3887 sha1( $url )
3888 ),
3889 $this->svcOptions->get( MainConfigNames::TranscludeCacheExpiry ),
3890 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3891 $req = $this->httpRequestFactory->create( $url, [], $fname );
3892
3893 $status = $req->execute(); // Status object
3894 if ( !$status->isOK() ) {
3895 $ttl = $cache::TTL_UNCACHEABLE;
3896 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3897 $ttl = min( $cache::TTL_LAGGED, $ttl );
3898 }
3899
3900 return [
3901 'text' => $status->isOK() ? $req->getContent() : null,
3902 'code' => $req->getStatus()
3903 ];
3904 },
3905 [
3906 'checkKeys' => ( $wikiId !== false )
3907 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3908 : [],
3909 'pcGroup' => 'interwiki-transclude:5',
3910 'pcTTL' => $cache::TTL_PROC_LONG
3911 ]
3912 );
3913
3914 if ( is_string( $data['text'] ) ) {
3915 $text = $data['text'];
3916 } elseif ( $data['code'] != 200 ) {
3917 // Though we failed to fetch the content, this status is useless.
3918 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3919 ->params( $url, $data['code'] )->inContentLanguage()->text();
3920 } else {
3921 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3922 }
3923
3924 return $text;
3925 }
3926
3936 public function argSubstitution( array $piece, PPFrame $frame ) {
3937 $error = false;
3938 $parts = $piece['parts'];
3939 $nameWithSpaces = $frame->expand( $piece['title'] );
3940 $argName = trim( $nameWithSpaces );
3941 $object = false;
3942 $text = $frame->getArgument( $argName );
3943 if ( $text === false && $parts->getLength() > 0
3944 && ( $this->ot['html']
3945 || $this->ot['pre']
3946 || ( $this->ot['wiki'] && $frame->isTemplate() )
3947 )
3948 ) {
3949 # No match in frame, use the supplied default
3950 $object = $parts->item( 0 )->getChildren();
3951 }
3952 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3953 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3954 $this->limitationWarn( 'post-expand-template-argument' );
3955 }
3956
3957 if ( $text === false && $object === false ) {
3958 # No match anywhere
3959 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3960 }
3961 if ( $error !== false ) {
3962 $text .= $error;
3963 }
3964 if ( $object !== false ) {
3965 $ret = [ 'object' => $object ];
3966 } else {
3967 $ret = [ 'text' => $text ];
3968 }
3969
3970 return $ret;
3971 }
3972
3973 public function tagNeedsNowikiStrippedInTagPF( string $lowerTagName ): bool {
3974 $parsoidSiteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
3975 return $parsoidSiteConfig->tagNeedsNowikiStrippedInTagPF( $lowerTagName );
3976 }
3977
3993 public function extensionSubstitution( array $params, PPFrame $frame ): string {
3994 static $errorStr = '<span class="error">';
3995
3996 $name = $frame->expand( $params['name'] );
3997 if ( str_starts_with( $name, $errorStr ) ) {
3998 // Probably expansion depth or node count exceeded. Just punt the
3999 // error up.
4000 return $name;
4001 }
4002
4003 // Parse attributes from XML-like wikitext syntax
4004 $attrText = !isset( $params['attr'] ) ? '' : $frame->expand( $params['attr'] );
4005 if ( str_starts_with( $attrText, $errorStr ) ) {
4006 // See above
4007 return $attrText;
4008 }
4009
4010 // We can't safely check if the expansion for $content resulted in an
4011 // error, because the content could happen to be the error string
4012 // (T149622).
4013 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4014
4015 $marker = self::MARKER_PREFIX . "-$name-"
4016 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4017
4018 $normalizedName = strtolower( $name );
4019 $isNowiki = $normalizedName === 'nowiki';
4020 $markerType = $isNowiki ? 'nowiki' : 'general';
4021
4022 // The content is stored as extra data to potentially be pulled out
4023 // with StripState::replaceNoWikis
4024 $extra = $isNowiki ? ( $content ?? '' ) : null;
4025
4026 if ( $this->ot['html'] || ( $isNowiki && $this->useParsoidFragments ) ) {
4027 $attributes = Sanitizer::decodeTagAttributes( $attrText );
4028 // Merge in attributes passed via {{#tag:}} parser function
4029 if ( isset( $params['attributes'] ) ) {
4030 $attributes += $params['attributes'];
4031 }
4032
4033 if ( isset( $this->mTagHooks[$normalizedName] ) ) {
4034 // Note that $content may be null here, for example if the
4035 // tag is self-closed.
4036 $output = $this->mTagHooks[$normalizedName]( $content, $attributes, $this, $frame );
4037 } else {
4038 $output = '<span class="error">Invalid tag extension name: ' .
4039 htmlspecialchars( $normalizedName ) . '</span>';
4040 }
4041
4042 if ( is_array( $output ) ) {
4043 // Extract flags
4044 $flags = $output;
4045 $output = $flags[0];
4046 if ( isset( $flags['isRawHTML'] ) ) {
4047 $markerType = 'nowiki';
4048 }
4049 if ( isset( $flags['markerType'] ) ) {
4050 $markerType = $flags['markerType'];
4051 }
4052 }
4053 } else {
4054 // We're substituting a {{subst:#tag:}} parser function.
4055 // Convert the attributes it passed into the XML-like string.
4056 if ( isset( $params['attributes'] ) ) {
4057 foreach ( $params['attributes'] as $attrName => $attrValue ) {
4058 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4059 htmlspecialchars( $this->getStripState()->unstripBoth( $attrValue ), ENT_COMPAT ) . '"';
4060 }
4061 }
4062 if ( $content === null ) {
4063 $output = "<$name$attrText/>";
4064 } else {
4065 $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
4066 if ( str_starts_with( $close, $errorStr ) ) {
4067 // See above
4068 return $close;
4069 }
4070 $output = "<$name$attrText>$content$close";
4071 }
4072 if ( $this->useParsoidFragments ) {
4073 $markerType = 'exttag';
4074 }
4075 }
4076
4077 if ( $markerType === 'none' ) {
4078 return $output;
4079 } elseif ( $markerType === 'nowiki' ) {
4080 $this->mStripState->addNoWiki( $marker, $output, $extra );
4081 } elseif ( $markerType === 'general' ) {
4082 $this->mStripState->addGeneral( $marker, $output );
4083 } elseif ( $markerType === 'exttag' ) {
4084 $this->mStripState->addExtTag( $marker, $output, $frame );
4085 } else {
4086 throw new UnexpectedValueException( __METHOD__ . ': invalid marker type' );
4087 }
4088 return $marker;
4089 }
4090
4098 private function incrementIncludeSize( $type, $size ) {
4099 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4100 return false;
4101 } else {
4102 $this->mIncludeSizes[$type] += $size;
4103 return true;
4104 }
4105 }
4106
4112 $this->mExpensiveFunctionCount++;
4113 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4114 }
4115
4123 private function handleDoubleUnderscore( string $text ): string {
4124 # The position of __TOC__ needs to be recorded
4125 $mw = $this->magicWordFactory->get( 'toc' );
4126 $tocAlias = null;
4127 if ( $mw->match( $text ) ) {
4128 $this->mShowToc = true;
4129 $this->mForceTocPosition = true;
4130 # record the alias used
4131 preg_match( $mw->getRegex(), $text, $tocAlias );
4132
4133 # Set a placeholder. At the end we'll fill it in with the TOC.
4134 $text = $mw->replace( self::TOC_PLACEHOLDER, $text, 1 );
4135
4136 # Only keep the first one.
4137 $text = $mw->replace( '', $text );
4138 }
4139
4140 # Now match and remove the rest of them
4141 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4142 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text, returnAlias: true );
4143 if ( $tocAlias ) {
4144 # For consistency with all other double-underscores (see below)
4145 $this->mDoubleUnderscores['toc'] = $tocAlias[0];
4146 }
4147
4148 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4149 $this->mOutput->setNoGallery( true );
4150 }
4151 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4152 $this->mShowToc = false;
4153 }
4154 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4155 && $this->getTitle()->getNamespace() === NS_CATEGORY
4156 ) {
4157 $this->addTrackingCategory( 'hidden-category-category' );
4158 }
4159 # (T10068) Allow control over whether robots index a page.
4160 # __NOINDEX__ always overrides __INDEX__, see T16899
4161 if (
4162 isset( $this->mDoubleUnderscores['noindex'] ) &&
4163 $this->nsInfo->canUseNoindex( $this->getPage()->getNamespace() )
4164 ) {
4165 $this->mOutput->setIndexPolicy( 'noindex' );
4166 $this->addTrackingCategory( 'noindex-category' );
4167 }
4168 if (
4169 isset( $this->mDoubleUnderscores['index'] ) &&
4170 $this->nsInfo->canUseNoindex( $this->getPage()->getNamespace() )
4171 ) {
4172 $this->mOutput->setIndexPolicy( 'index' );
4173 $this->addTrackingCategory( 'index-category' );
4174 }
4175
4176 foreach ( $this->mDoubleUnderscores as $key => $alias ) {
4177 # Cache all double underscores in the database
4178 $this->mOutput->setUnsortedPageProperty( $key );
4179 # Check for deprecated local aliases (T407289)
4180 $ascii = str_starts_with( $alias, '__' ) && str_ends_with( $alias, '__' );
4181 $wide = str_starts_with( $alias, '__' ) && str_ends_with( $alias, '__' );
4182 if ( !( $ascii || $wide ) ) {
4183 $this->addTrackingCategory( 'bad-double-underscore-category' );
4184 }
4185 }
4186
4187 return $text;
4188 }
4189
4196 public function addTrackingCategory( $msg ) {
4197 return $this->trackingCategories->addTrackingCategory(
4198 $this->mOutput, $msg, $this->getPage()
4199 );
4200 }
4201
4217 public function msg( string $msg, ...$params ): Message {
4218 return wfMessage( $msg, ...$params )
4219 ->inLanguage( $this->getTargetLanguage() )
4220 ->page( $this->getPage() );
4221 }
4222
4223 private function cleanUpTocLine( Node $container ) {
4224 '@phan-var Element|DocumentFragment $container'; // @var Element|DocumentFragment $container
4225 # Strip out HTML
4226 # Allowed tags are:
4227 # * <sup> and <sub> (T10393)
4228 # * <i> (T28375)
4229 # * <b> (r105284)
4230 # * <bdi> (T74884)
4231 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4232 # * <s> and <strike> (T35715)
4233 # * <q> (T251672)
4234 # We strip any parameter from accepted tags, except dir="rtl|ltr" from <span>,
4235 # to allow setting directionality in toc items.
4236 $allowedTags = [ 'span', 'sup', 'sub', 'bdi', 'i', 'b', 's', 'strike', 'q' ];
4237 $node = $container->firstChild;
4238 while ( $node !== null ) {
4239 $next = $node->nextSibling;
4240 if ( $node instanceof Element ) {
4241 $nodeName = DOMUtils::nodeName( $node );
4242 if ( in_array( $nodeName, [ 'style', 'script' ], true ) ) {
4243 # Remove any <style> or <script> tags (T198618)
4244 DOMCompat::remove( $node );
4245 } elseif ( in_array( $nodeName, $allowedTags, true ) ) {
4246 // Keep tag, remove attributes
4247 $removeAttrs = [];
4248 foreach ( $node->attributes as $attr ) {
4249 if (
4250 $nodeName === 'span' && $attr->name === 'dir'
4251 && ( $attr->value === 'rtl' || $attr->value === 'ltr' )
4252 ) {
4253 // Keep <span dir="rtl"> and <span dir="ltr">
4254 continue;
4255 }
4256 $removeAttrs[] = $attr;
4257 }
4258 foreach ( $removeAttrs as $attr ) {
4259 $node->removeAttributeNode( $attr );
4260 }
4261 $this->cleanUpTocLine( $node );
4262 # Strip '<span></span>', which is the result from the above if
4263 # <span id="foo"></span> is used to produce an additional anchor
4264 # for a section.
4265 if ( $nodeName === 'span' && !$node->hasChildNodes() ) {
4266 DOMCompat::remove( $node );
4267 }
4268 } else {
4269 // Strip tag
4270 if ( $node->firstChild !== null ) {
4271 $next = $node->firstChild;
4272 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
4273 while ( $childNode = $node->firstChild ) {
4274 $node->parentNode->insertBefore( $childNode, $node );
4275 }
4276 }
4277 DOMCompat::remove( $node );
4278 }
4279 } elseif ( $node instanceof Comment ) {
4280 // Extensions may add comments to headings;
4281 // these shouldn't appear in the ToC either.
4282 DOMCompat::remove( $node );
4283 }
4284 $node = $next;
4285 }
4286 }
4287
4303 private function finalizeHeadings( string $text, string $origText, bool $isMain = true ): string {
4304 # Inhibit editsection links if requested in the page
4305 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4306 $maybeShowEditLink = false;
4307 } else {
4308 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4309 }
4310
4311 # Get all headlines for numbering them and adding funky stuff like [edit]
4312 # links - this is for later, but we need the number of headlines right now
4313 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4314 # be trimmed here since whitespace in HTML headings is significant.
4315 $matches = [];
4316 $numMatches = preg_match_all(
4317 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4318 $text,
4319 $matches
4320 );
4321
4322 # if there are fewer than 4 headlines in the article, do not show TOC
4323 # unless it's been explicitly enabled.
4324 $enoughToc = $this->mShowToc &&
4325 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4326
4327 # Allow user to stipulate that a page should have a "new section"
4328 # link added via __NEWSECTIONLINK__
4329 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4330 $this->mOutput->setNewSection( true );
4331 }
4332
4333 # Allow user to remove the "new section"
4334 # link via __NONEWSECTIONLINK__
4335 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4336 $this->mOutput->setHideNewSection( true );
4337 }
4338
4339 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4340 # override above conditions and always show TOC above first header
4341 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4342 $this->mShowToc = true;
4343 $enoughToc = true;
4344 }
4345
4346 if ( !$numMatches ) {
4347 return $text;
4348 }
4349
4350 # headline counter
4351 $headlineCount = 0;
4352 $haveTocEntries = false;
4353
4354 # Ugh .. the TOC should have neat indentation levels which can be
4355 # passed to the skin functions. These are determined here
4356 $head = [];
4357 $level = 0;
4358 $tocData = new TOCData();
4359 $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4360 $oldType = $this->mOutputType;
4361 $this->setOutputType( self::OT_WIKI );
4362 $frame = $this->getPreprocessor()->newFrame();
4363 $root = $this->preprocessToDom( $origText );
4364 $node = $root->getFirstChild();
4365 $cpOffset = 0;
4366 $refers = [];
4367
4368 $maxTocLevel = $this->svcOptions->get( MainConfigNames::MaxTocLevel );
4369 $domDocument = DOMCompat::newDocument();
4370 foreach ( $matches[3] as $headline ) {
4371 // $headline is half-parsed HTML
4372 $isTemplate = false;
4373 $titleText = false;
4374 $sectionIndex = false;
4375 if ( preg_match( self::HEADLINE_MARKER_REGEX, $headline, $markerMatches ) ) {
4376 $serial = (int)$markerMatches[1];
4377 [ $titleText, $sectionIndex ] = $this->mHeadings[$serial];
4378 $isTemplate = ( $titleText != $baseTitleText );
4379 $headline = ltrim( substr( $headline, strlen( $markerMatches[0] ) ) );
4380 }
4381
4382 $sectionMetadata = SectionMetadata::fromLegacy( [
4383 "fromtitle" => $titleText ?: null,
4384 "index" => $sectionIndex === false
4385 ? '' : ( ( $isTemplate ? 'T-' : '' ) . $sectionIndex )
4386 ] );
4387 $tocData->addSection( $sectionMetadata );
4388
4389 $oldLevel = $level;
4390 $level = (int)$matches[1][$headlineCount];
4391 $tocData->processHeading( $oldLevel, $level, $sectionMetadata );
4392
4393 if ( $tocData->getCurrentTOCLevel() < $maxTocLevel ) {
4394 $haveTocEntries = true;
4395 }
4396
4397 # Remove link placeholders by the link text.
4398 # <!--LINK number-->
4399 # turns into
4400 # link text with suffix
4401 # Do this before unstrip since link text can contain strip markers
4402 $fullyParsedHeadline = $this->replaceLinkHoldersText( $headline );
4403
4404 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4405 $fullyParsedHeadline = $this->mStripState->unstripBoth( $fullyParsedHeadline );
4406
4407 // Run Tidy to convert wikitext entities to HTML entities (T355386),
4408 // conveniently also giving us a way to handle French spaces (T324763)
4409 $fullyParsedHeadline = $this->tidy->tidy( $fullyParsedHeadline, Sanitizer::armorFrenchSpaces( ... ) );
4410
4411 // Wrap the safe headline to parse the heading attributes
4412 // Literal HTML tags should be sanitized at this point
4413 // cleanUpTocLine will strip the headline tag
4414 $wrappedHeadline = "<h$level" . $matches['attrib'][$headlineCount] . $fullyParsedHeadline . "</h$level>";
4415
4416 // Parse the heading contents as HTML. This makes it easier to strip out some HTML tags,
4417 // and ensures that we generate balanced HTML at the end (T218330).
4418 $headlineDom = DOMUtils::parseHTMLToFragment( $domDocument, $wrappedHeadline );
4419
4420 // Extract a user defined id on the heading
4421 // A heading is expected as the first child and could be asserted
4422 $h = $headlineDom->firstChild;
4423 $headingId = ( $h instanceof Element && DOMUtils::isHeading( $h ) ) ?
4424 DOMCompat::getAttribute( $h, 'id' ) : null;
4425
4426 $this->cleanUpTocLine( $headlineDom );
4427
4428 // Serialize back to HTML
4429 // $tocline is for the TOC display, fully-parsed HTML with some tags removed
4430 $tocline = trim( DOMUtils::getFragmentInnerHTML( $headlineDom ) );
4431
4432 // $headlineText is for the "Edit section: $1" tooltip, plain text
4433 $headlineText = trim( $headlineDom->textContent );
4434
4435 if ( $headingId === null || $headingId === '' ) {
4436 $headingId = Sanitizer::normalizeSectionNameWhitespace( $headlineText );
4437 $headingId = self::normalizeSectionName( $headingId );
4438 }
4439
4440 # Create the anchor for linking from the TOC to the section
4441 $fallbackAnchor = Sanitizer::escapeIdForAttribute( $headingId, Sanitizer::ID_FALLBACK );
4442 $linkAnchor = Sanitizer::escapeIdForLink( $headingId );
4443 $anchor = Sanitizer::escapeIdForAttribute( $headingId, Sanitizer::ID_PRIMARY );
4444 if ( $fallbackAnchor === $anchor ) {
4445 # No reason to have both (in fact, we can't)
4446 $fallbackAnchor = false;
4447 }
4448
4449 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4450 $arrayKey = strtolower( $anchor );
4451 if ( $fallbackAnchor === false ) {
4452 $fallbackArrayKey = false;
4453 } else {
4454 $fallbackArrayKey = strtolower( $fallbackAnchor );
4455 }
4456
4457 if ( isset( $refers[$arrayKey] ) ) {
4458 for ( $i = 2; isset( $refers["{$arrayKey}_$i"] ); ++$i );
4459 $anchor .= "_$i";
4460 $linkAnchor .= "_$i";
4461 $refers["{$arrayKey}_$i"] = true;
4462 } else {
4463 $refers[$arrayKey] = true;
4464 }
4465 if ( $fallbackAnchor !== false && isset( $refers[$fallbackArrayKey] ) ) {
4466 for ( $i = 2; isset( $refers["{$fallbackArrayKey}_$i"] ); ++$i );
4467 $fallbackAnchor .= "_$i";
4468 $refers["{$fallbackArrayKey}_$i"] = true;
4469 } else {
4470 $refers[$fallbackArrayKey] = true;
4471 }
4472
4473 # Add the section to the section tree
4474 # Find the DOM node for this header
4475 $noOffset = ( $isTemplate || $sectionIndex === false );
4476 while ( $node && !$noOffset ) {
4477 if ( $node->getName() === 'h' ) {
4478 $bits = $node->splitHeading();
4479 if ( $bits['i'] == $sectionIndex ) {
4480 break;
4481 }
4482 }
4483 $cpOffset += mb_strlen(
4484 $this->mStripState->unstripBoth(
4485 $frame->expand( $node, PPFrame::RECOVER_ORIG )
4486 )
4487 );
4488 $node = $node->getNextSibling();
4489 }
4490 $sectionMetadata->line = $tocline;
4491 $sectionMetadata->codepointOffset = ( $noOffset ? null : $cpOffset );
4492 $sectionMetadata->anchor = $anchor;
4493 $sectionMetadata->linkAnchor = $linkAnchor;
4494
4495 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4496 // Output edit section links as markers with styles that can be customized by skins
4497 if ( $isTemplate ) {
4498 # Put a T flag in the section identifier, to indicate to extractSections()
4499 # that sections inside <includeonly> should be counted.
4500 $editsectionPage = $titleText;
4501 $editsectionSection = "T-$sectionIndex";
4502 } else {
4503 $editsectionPage = $this->getTitle()->getPrefixedText();
4504 $editsectionSection = $sectionIndex;
4505 }
4506 // Construct a pseudo-HTML tag as a placeholder for the section edit link. It is replaced in
4507 // MediaWiki\OutputTransform\Stages\HandleSectionLinks with the real link.
4508 //
4509 // Any HTML markup in the input has already been escaped,
4510 // so we don't have to worry about a user trying to input one of these markers directly.
4511 //
4512 // We put the page and section in attributes to stop the language converter from
4513 // converting them, but put the headline hint in tag content
4514 // because it is supposed to be able to convert that.
4515 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage, ENT_COMPAT );
4516 $editlink .= '" section="' . htmlspecialchars( $editsectionSection, ENT_COMPAT ) . '"';
4517 $editlink .= '>' . htmlspecialchars( $headlineText ) . '</mw:editsection>';
4518 } else {
4519 $editlink = '';
4520 }
4521 // Reconstruct the original <h#> tag with added attributes. It is replaced in
4522 // MediaWiki\OutputTransform\Stages\HandleSectionLinks to add anchors and stuff.
4523 //
4524 // data-mw-... attributes are forbidden in Sanitizer::isReservedDataAttribute(),
4525 // so we don't have to worry about a user trying to input one of these markers directly.
4526 //
4527 // We put the anchors in attributes to stop the language converter from converting them.
4528 $head[$headlineCount] = "<h$level" . Html::expandAttributes( [
4529 'data-mw-anchor' => $anchor,
4530 'data-mw-fallback-anchor' => $fallbackAnchor,
4531 ] ) . $matches['attrib'][$headlineCount] . $headline . $editlink . "</h$level>";
4532
4533 $headlineCount++;
4534 }
4535
4536 $this->setOutputType( $oldType );
4537
4538 # Never ever show TOC if no headers (or suppressed)
4539 $suppressToc = $this->mOptions->getSuppressTOC();
4540 if ( !$haveTocEntries ) {
4541 $enoughToc = false;
4542 }
4543 $addTOCPlaceholder = false;
4544
4545 if ( $isMain && !$suppressToc ) {
4546 // We generally output the section information via the API
4547 // even if there isn't "enough" of a ToC to merit showing
4548 // it -- but the "suppress TOC" parser option is set when
4549 // any sections that might be found aren't "really there"
4550 // (ie, JavaScript content that might have spurious === or
4551 // <h2>: T307691) so we will *not* set section information
4552 // in that case.
4553 $this->mOutput->setTOCData( $tocData );
4554
4555 // T294950: Record a suggestion that the TOC should be shown.
4556 // Skins are free to ignore this suggestion and implement their
4557 // own criteria for showing/suppressing TOC (T318186).
4558 if ( $enoughToc ) {
4559 $this->mOutput->setOutputFlag( ParserOutputFlags::SHOW_TOC );
4560 if ( !$this->mForceTocPosition ) {
4561 $addTOCPlaceholder = true;
4562 }
4563 }
4564
4565 // If __NOTOC__ is used on the page (and not overridden by
4566 // __TOC__ or __FORCETOC__) set the NO_TOC flag to tell
4567 // the skin that although the section information is
4568 // valid, it should perhaps not be presented as a Table Of
4569 // Contents.
4570 if ( !$this->mShowToc ) {
4571 $this->mOutput->setOutputFlag( ParserOutputFlags::NO_TOC );
4572 }
4573 }
4574
4575 # split up and insert constructed headlines
4576 $blocks = preg_split( '/<h[1-6]\b[^>]*>.*?<\/h[1-6]>/is', $text );
4577 $i = 0;
4578
4579 // build an array of document sections
4580 $sections = [];
4581 foreach ( $blocks as $block ) {
4582 // $head is zero-based, sections aren't.
4583 if ( empty( $head[$i - 1] ) ) {
4584 $sections[$i] = $block;
4585 } else {
4586 $sections[$i] = $head[$i - 1] . $block;
4587 }
4588
4589 $i++;
4590 }
4591
4592 if ( $addTOCPlaceholder ) {
4593 // append the TOC at the beginning
4594 // Top anchor now in skin
4595 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset At least one element when enoughToc is true
4596 $sections[0] .= self::TOC_PLACEHOLDER . "\n";
4597 }
4598
4599 return implode( '', $sections );
4600 }
4601
4613 public static function localizeTOC(
4614 ?TOCData $tocData, Language $lang, ?ILanguageConverter $converter,
4615 ?string $preferredVariant = null
4616 ) {
4617 if ( $tocData === null ) {
4618 return; // Nothing to do
4619 }
4620 foreach ( $tocData->getSections() as $s ) {
4621 // Localize heading
4622 if ( $converter ) {
4623 $preferredVariant ??= $converter->getPreferredVariant();
4624 // T331316: don't use 'convert' or 'convertTo' as these reset
4625 // the language converter state.
4626 $s->line = $converter->convertTo(
4627 $s->line, $preferredVariant, false
4628 );
4629 }
4630 // Localize numbering
4631 $dot = '.';
4632 $pieces = explode( $dot, $s->number );
4633 $numbering = '';
4634 foreach ( $pieces as $i => $p ) {
4635 if ( $i > 0 ) {
4636 $numbering .= $dot;
4637 }
4638 $numbering .= $lang->formatNum( $p );
4639 }
4640 $s->number = $numbering;
4641 }
4642 }
4643
4656 public function preSaveTransform(
4657 $text,
4658 PageReference $page,
4659 UserIdentity $user,
4660 ParserOptions $options,
4661 $clearState = true
4662 ): string {
4663 if ( $clearState ) {
4664 $magicScopeVariable = $this->lock();
4665 }
4666 $this->startParse( $page, $options, self::OT_WIKI, $clearState );
4667 $this->setUser( $user );
4668
4669 // Strip U+0000 NULL (T159174)
4670 $text = str_replace( "\000", '', $text );
4671
4672 // We still normalize line endings (including trimming trailing whitespace) for
4673 // backwards-compatibility with other code that just calls PST, but this should already
4674 // be handled in TextContent subclasses
4675 $text = TextContent::normalizeLineEndings( $text );
4676
4677 if ( $options->getPreSaveTransform() ) {
4678 $text = $this->pstPass2( $text, $user );
4679 }
4680 $text = $this->mStripState->unstripBoth( $text );
4681
4682 // Trim trailing whitespace again, because the previous steps can introduce it.
4683 $text = rtrim( $text );
4684
4685 $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4686
4687 $this->setUser( null ); # Reset
4688
4689 return $text;
4690 }
4691
4700 private function pstPass2( string $text, UserIdentity $user ): string {
4701 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4702 # $this->contLang here in order to give everyone the same signature and use the default one
4703 # rather than the one selected in each user's preferences. (see also T14815)
4704 $ts = $this->mOptions->getTimestamp();
4705 $timestamp = MWTimestamp::getLocalInstance( $ts );
4706 $ts = $timestamp->format( 'YmdHis' );
4707 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4708
4709 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4710
4711 # Variable replacement
4712 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4713 $text = $this->replaceVariables( $text );
4714
4715 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4716 # which may corrupt this parser instance via its wfMessage()->text() call-
4717
4718 # Signatures
4719 if ( str_contains( $text, '~~~' ) ) {
4720 $sigText = $this->getUserSig( $user );
4721 $text = strtr( $text, [
4722 '~~~~~' => $d,
4723 '~~~~' => "$sigText $d",
4724 '~~~' => $sigText
4725 ] );
4726 # The main two signature forms used above are time-sensitive
4727 $this->setOutputFlag( ParserOutputFlags::USER_SIGNATURE, 'User signature detected' );
4728 }
4729
4730 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4731 $tc = '[' . Title::legalChars() . ']';
4732 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4733
4734 // [[ns:page (context)|]]
4735 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4736 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4737 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4738 // [[ns:page (context), context|]] (using single, double-width or Arabic comma)
4739 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,|، )$tc+|)\\|]]/";
4740 // [[|page]] (reverse pipe trick: add context from page title)
4741 $p2 = "/\[\[\\|($tc+)]]/";
4742
4743 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4744 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4745 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4746 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4747
4748 $t = $this->getTitle()->getText();
4749 $m = [];
4750 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4751 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4752 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4753 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4754 } else {
4755 # if there's no context, don't bother duplicating the title
4756 $text = preg_replace( $p2, '[[\\1]]', $text );
4757 }
4758
4759 return $text;
4760 }
4761
4777 public function getUserSig( UserIdentity $user, $nickname = false, $fancySig = null ): string {
4778 $username = $user->getName();
4779
4780 # If not given, retrieve from the user object.
4781 if ( $nickname === false ) {
4782 $nickname = $this->userOptionsLookup->getOption( $user, 'nickname' );
4783 }
4784
4785 $fancySig ??= $this->userOptionsLookup->getBoolOption( $user, 'fancysig' );
4786
4787 if ( $nickname === null || $nickname === '' ) {
4788 // Empty value results in the default signature (even when fancysig is enabled)
4789 $nickname = $username;
4790 } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( MainConfigNames::MaxSigChars ) ) {
4791 $nickname = $username;
4792 $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4793 } elseif ( $fancySig !== false ) {
4794 # Sig. might contain markup; validate this
4795 $isValid = $this->validateSig( $nickname ) !== false;
4796
4797 # New validator
4798 $sigValidation = $this->svcOptions->get( MainConfigNames::SignatureValidation );
4799 if ( $isValid && $sigValidation === 'disallow' ) {
4800 $parserOpts = new ParserOptions(
4801 $this->mOptions->getUserIdentity(),
4802 $this->contLang
4803 );
4804 $validator = $this->signatureValidatorFactory
4805 ->newSignatureValidator( $user, null, $parserOpts );
4806 $isValid = !$validator->validateSignature( $nickname );
4807 }
4808
4809 if ( $isValid ) {
4810 # Validated; clean up (if needed) and return it
4811 return $this->cleanSig( $nickname, true );
4812 } else {
4813 # Failed to validate; fall back to the default
4814 $nickname = $username;
4815 $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4816 }
4817 }
4818
4819 # Make sure nickname doesn't get a sig in a sig
4820 $nickname = self::cleanSigInSig( $nickname );
4821
4822 # If we're still here, make it a link to the user page
4823 $userText = wfEscapeWikiText( $username );
4824 $nickText = wfEscapeWikiText( $nickname );
4825 if ( $this->userNameUtils->isTemp( $username ) ) {
4826 $msgName = 'signature-temp';
4827 } elseif ( $user->isRegistered() ) {
4828 $msgName = 'signature';
4829 } else {
4830 $msgName = 'signature-anon';
4831 }
4832
4833 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4834 ->page( $this->getPage() )->text();
4835 }
4836
4844 public function validateSig( $text ): string|false {
4845 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4846 }
4847
4859 public function cleanSig( $text, $parsing = false ): string {
4860 if ( !$parsing ) {
4861 $magicScopeVariable = $this->lock();
4862 $this->startParse(
4863 $this->mTitle,
4864 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
4865 self::OT_PREPROCESS,
4866 true
4867 );
4868 }
4869
4870 # Option to disable this feature
4871 if ( !$this->mOptions->getCleanSignatures() ) {
4872 return $text;
4873 }
4874
4875 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4876 # => Move this logic to braceSubstitution()
4877 $substWord = $this->magicWordFactory->get( 'subst' );
4878 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4879 $substText = '{{' . $substWord->getSynonym( 0 );
4880
4881 $text = preg_replace( $substRegex, $substText, $text );
4882 $text = self::cleanSigInSig( $text );
4883 $dom = $this->preprocessToDom( $text );
4884 $frame = $this->getPreprocessor()->newFrame();
4885 $text = $frame->expand( $dom );
4886
4887 if ( !$parsing ) {
4888 $text = $this->mStripState->unstripBoth( $text );
4889 }
4890
4891 return $text;
4892 }
4893
4901 public static function cleanSigInSig( $text ): string {
4902 $text = preg_replace( '/~{3,5}/', '', $text );
4903 return $text;
4904 }
4905
4922 public static function replaceTableOfContentsMarker( $text, $toc ): string {
4923 // Optimization: Avoid a potentially expensive Remex tokenization and reserialization
4924 // if the content does not contain a TOC placeholder, such as during message parsing,
4925 // which may occur hundreds of times per request (T394059).
4926 if ( !str_contains( $text, 'mw:PageProp/toc' ) ) {
4927 return $text;
4928 }
4929
4930 $replaced = false;
4931 return HtmlHelper::modifyElements(
4932 $text,
4933 static function ( SerializerNode $node ): bool {
4934 $prop = $node->attrs['property'] ?? '';
4935 return $node->name === 'meta' && $prop === 'mw:PageProp/toc';
4936 },
4937 static function ( SerializerNode $node ) use ( &$replaced, $toc ) {
4938 if ( $replaced ) {
4939 // Remove the additional metas. While not strictly
4940 // necessary, this also ensures idempotence if we
4941 // run the pass more than once on a given content.
4942 return '';
4943 }
4944 $replaced = true;
4945 return $toc; // outerHTML replacement.
4946 },
4947 false /* use legacy-compatible serialization */
4948 );
4949 }
4950
4962 public function startExternalParse( ?PageReference $page, ParserOptions $options,
4963 $outputType, $clearState = true, $revId = null
4964 ) {
4965 $this->startParse( $page, $options, $outputType, $clearState );
4966 if ( $revId !== null ) {
4967 $this->mRevisionId = $revId;
4968 }
4969 }
4970
4977 private function startParse( ?PageReference $page, ParserOptions $options,
4978 $outputType, $clearState = true
4979 ) {
4980 $this->setPage( $page );
4981 $this->mOptions = $options;
4982 $this->setOutputType( $outputType );
4983 if ( $clearState ) {
4984 $this->clearState();
4985 }
4986 }
4987
4997 public function transformMsg( $text, ParserOptions $options, ?PageReference $page = null ): string {
4998 static $executing = false;
4999
5000 # Guard against infinite recursion
5001 if ( $executing ) {
5002 return $text;
5003 }
5004 $executing = true;
5005
5006 $text = $this->preprocess( $text, $page ?? $this->mTitle, $options );
5007
5008 $executing = false;
5009 return $text;
5010 }
5011
5031 public function setHook( $tag, callable $callback ) {
5032 $tag = strtolower( $tag );
5033 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5034 throw new InvalidArgumentException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5035 }
5036 $oldVal = $this->mTagHooks[$tag] ?? null;
5037 $this->mTagHooks[$tag] = $callback;
5038 if ( !in_array( $tag, $this->mStripList ) ) {
5039 $this->mStripList[] = $tag;
5040 }
5041
5042 return $oldVal;
5043 }
5044
5049 public function clearTagHooks() {
5050 $this->mTagHooks = [];
5051 $this->mStripList = [];
5052 }
5053
5102 public function setFunctionHook( $id, callable $callback, $flags = 0 ): ?callable {
5103 $oldVal = $this->mFunctionHooks[$id][0] ?? null;
5104 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5105
5106 # Add to function cache
5107 $mw = $this->magicWordFactory->get( $id );
5108
5109 $synonyms = $mw->getSynonyms();
5110 $sensitive = intval( $mw->isCaseSensitive() );
5111
5112 foreach ( $synonyms as $syn ) {
5113 # Case
5114 if ( !$sensitive ) {
5115 $syn = $this->contLang->lc( $syn );
5116 }
5117 # Add leading hash
5118 if ( !( $flags & self::SFH_NO_HASH ) ) {
5119 $syn = '#' . $syn;
5120 }
5121 # Remove trailing colon (or Japanese double-width colon)
5122 if ( str_ends_with( $syn, ':' ) || str_ends_with( $syn, ':' ) ) {
5123 $syn = mb_substr( $syn, 0, -1 );
5124 }
5125 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5126 }
5127 return $oldVal;
5128 }
5129
5136 public function getFunctionHooks() {
5137 return array_keys( $this->mFunctionHooks );
5138 }
5139
5146 private function replaceLinkHoldersPrivate( string &$text ): void {
5147 $this->mLinkHolders->replace( $text );
5148 }
5149
5157 private function replaceLinkHoldersText( string $text ): string {
5158 return $this->mLinkHolders->replaceText( $text );
5159 }
5160
5175 public function renderImageGallery( $text, array $params ): string {
5176 $mode = $params['mode'] ?? false;
5177
5178 try {
5179 $ig = ImageGalleryBase::factory( $mode );
5181 // If invalid type set, fallback to default.
5182 $ig = ImageGalleryBase::factory();
5183 }
5184
5185 $ig->setContextTitle( $this->getTitle() );
5186 $ig->setShowBytes( false );
5187 $ig->setShowDimensions( false );
5188 $ig->setParser( $this );
5189 $ig->setHideBadImages();
5190 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5191
5192 $ig->setShowFilename( isset( $params['showfilename'] ) );
5193 if ( isset( $params['caption'] ) ) {
5194 // NOTE: We aren't passing a frame here or below. Frame info
5195 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5196 // See T107332#4030582
5197 $caption = $this->recursiveTagParse( $params['caption'] );
5198 $ig->setCaptionHtml( $caption );
5199 }
5200 if ( isset( $params['perrow'] ) ) {
5201 $ig->setPerRow( $params['perrow'] );
5202 }
5203 if ( isset( $params['widths'] ) ) {
5204 $ig->setWidths( $params['widths'] );
5205 }
5206 if ( isset( $params['heights'] ) ) {
5207 $ig->setHeights( $params['heights'] );
5208 }
5209 $ig->setAdditionalOptions( $params );
5210
5211 $lines = StringUtils::explode( "\n", $text );
5212 foreach ( $lines as $line ) {
5213 # match lines like these:
5214 # Image:someimage.jpg|This is some image
5215 $matches = [];
5216 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5217 # Skip empty lines
5218 if ( count( $matches ) == 0 ) {
5219 continue;
5220 }
5221
5222 if ( str_contains( $matches[0], '%' ) ) {
5223 $matches[1] = rawurldecode( $matches[1] );
5224 }
5225 $title = Title::newFromText( $matches[1], NS_FILE );
5226 if ( $title === null ) {
5227 # Bogus title. Ignore these so we don't bomb out later.
5228 continue;
5229 }
5230
5231 # We need to get what handler the file uses, to figure out parameters.
5232 # Note, a hook can override the file name, and chose an entirely different
5233 # file (which potentially could be of a different type and have different handler).
5234 $options = [];
5235 $descQuery = false;
5236 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5237 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5238 $this, $title, $options, $descQuery
5239 );
5240 # Don't register it now, as TraditionalImageGallery does that later.
5241 $file = $this->fetchFileNoRegister( $title, $options );
5242 $handler = $file ? $file->getHandler() : false;
5243
5244 $paramMap = [
5245 'img_alt' => 'gallery-internal-alt',
5246 'img_link' => 'gallery-internal-link',
5247 ];
5248 if ( $handler ) {
5249 $paramMap += $handler->getParamMap();
5250 // We don't want people to specify per-image widths.
5251 // Additionally the width parameter would need special casing anyhow.
5252 unset( $paramMap['img_width'] );
5253 }
5254
5255 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5256
5257 $label = '';
5258 $alt = null;
5259 $handlerOptions = [];
5260 $imageOptions = [];
5261 $hasAlt = false;
5262
5263 if ( isset( $matches[3] ) ) {
5264 // look for an |alt= definition while trying not to break existing
5265 // captions with multiple pipes (|) in it, until a more sensible grammar
5266 // is defined for images in galleries
5267
5268 // FIXME: Doing recursiveTagParse at this stage is a bit odd,
5269 // and different from makeImage.
5270 $matches[3] = $this->recursiveTagParse( $matches[3] );
5271 // Protect LanguageConverter markup
5272 $parameterMatches = StringUtils::delimiterExplode(
5273 '-{', '}-',
5274 '|',
5275 $matches[3],
5276 true /* nested */
5277 );
5278
5279 foreach ( $parameterMatches as $parameterMatch ) {
5280 [ $magicName, $match ] = $mwArray->matchVariableStartToEnd( trim( $parameterMatch ) );
5281 if ( !$magicName ) {
5282 // Last pipe wins.
5283 $label = $parameterMatch;
5284 continue;
5285 }
5286
5287 $paramName = $paramMap[$magicName];
5288 switch ( $paramName ) {
5289 case 'gallery-internal-alt':
5290 $hasAlt = true;
5291 $alt = $this->stripAltText( $match );
5292 break;
5293 case 'gallery-internal-link':
5294 $linkValue = $this->stripAltText( $match );
5295 if ( preg_match( '/^-{R\|(.*)}-$/', $linkValue ) ) {
5296 // Result of LanguageConverter::markNoConversion
5297 // invoked on an external link.
5298 $linkValue = substr( $linkValue, 4, -2 );
5299 }
5300 [ $type, $target ] = $this->parseLinkParameter( $linkValue );
5301 if ( $type ) {
5302 if ( $type === 'no-link' ) {
5303 $target = true;
5304 }
5305 $imageOptions[$type] = $target;
5306 }
5307 break;
5308 default:
5309 // Must be a handler specific parameter.
5310 if ( $handler->validateParam( $paramName, $match ) ) {
5311 $handlerOptions[$paramName] = $match;
5312 } else {
5313 // Guess not, consider it as caption.
5314 $this->logger->debug(
5315 "$parameterMatch failed parameter validation" );
5316 $label = $parameterMatch;
5317 }
5318 }
5319 }
5320 }
5321
5322 // Match makeImage when !$hasVisibleCaption
5323 if ( !$hasAlt && $label !== '' ) {
5324 $alt = $this->stripAltText( $label );
5325 }
5326 $imageOptions['title'] = $this->stripAltText( $label );
5327
5328 // Match makeImage which sets this unconditionally
5329 $handlerOptions['targetlang'] = $this->getTargetLanguage()->getCode();
5330
5331 $ig->add(
5332 $title, $label, $alt, '', $handlerOptions,
5333 ImageGalleryBase::LOADING_DEFAULT, $imageOptions
5334 );
5335 }
5336 $html = $ig->toHTML();
5337 $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5338 return $html;
5339 }
5340
5345 private function getImageParams( $handler ) {
5346 $handlerClass = $handler ? get_class( $handler ) : '';
5347 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5348 # Initialise static lists
5349 static $internalParamNames = [
5350 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5351 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5352 'bottom', 'text-bottom' ],
5353 'frame' => [ 'thumbnail', 'framed', 'frameless', 'border',
5354 // These parameters take arguments, so to ensure literals
5355 // have precedence, keep them listed last (T372935):
5356 'manualthumb', 'upright', 'link', 'alt', 'class' ],
5357 ];
5358 static $internalParamMap;
5359 if ( !$internalParamMap ) {
5360 $internalParamMap = [];
5361 foreach ( $internalParamNames as $type => $names ) {
5362 foreach ( $names as $name ) {
5363 // For grep: img_left, img_right, img_center, img_none,
5364 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5365 // img_bottom, img_text_bottom,
5366 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5367 // img_border, img_link, img_alt, img_class
5368 $magicName = str_replace( '-', '_', "img_$name" );
5369 $internalParamMap[$magicName] = [ $type, $name ];
5370 }
5371 }
5372 }
5373
5374 # Add handler params
5375 # Since img_width is one of these, it is important it is listed
5376 # *after* the literal parameter names above (T372935).
5377 $paramMap = $internalParamMap;
5378 if ( $handler ) {
5379 $handlerParamMap = $handler->getParamMap();
5380 foreach ( $handlerParamMap as $magic => $paramName ) {
5381 $paramMap[$magic] = [ 'handler', $paramName ];
5382 }
5383 } else {
5384 // Parse the size for non-existent files. See T273013
5385 $paramMap[ 'img_width' ] = [ 'handler', 'width' ];
5386 }
5387 $this->mImageParams[$handlerClass] = $paramMap;
5388 $this->mImageParamsMagicArray[$handlerClass] =
5389 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5390 }
5391 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5392 }
5393
5402 public function makeImageHtml( LinkTarget $link, string $options ): string {
5403 return $this->makeImageInternal(
5404 $link, $options, shouldReplaceLinkHolders: true
5405 );
5406 }
5407
5422 public function makeImage( LinkTarget $link, $options, $holders = false ): string {
5423 wfDeprecated( __METHOD__, '1.46' ); // warnings since 1.46
5424 return $this->makeImageInternal(
5425 $link, $options, $holders ?: null, shouldReplaceLinkHolders: false
5426 );
5427 }
5428
5438 private function makeImageInternal(
5439 LinkTarget $link,
5440 string $options,
5441 ?LinkHolderArray $holders = null,
5442 bool $shouldReplaceLinkHolders = false
5443 ): string {
5444 # Check if the options text is of the form "options|alt text"
5445 # Options are:
5446 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5447 # * left no resizing, just left align. label is used for alt= only
5448 # * right same, but right aligned
5449 # * none same, but not aligned
5450 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5451 # * center center the image
5452 # * framed Keep original image size, no magnify-button.
5453 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5454 # * upright reduce width for upright images, rounded to full __0 px
5455 # * border draw a 1px border around the image
5456 # * alt Text for HTML alt attribute (defaults to empty)
5457 # * class Set a class for img node
5458 # * link Set the target of the image link. Can be external, interwiki, or local
5459 # vertical-align values (no % or length right now):
5460 # * baseline
5461 # * sub
5462 # * super
5463 # * top
5464 # * text-top
5465 # * middle
5466 # * bottom
5467 # * text-bottom
5468
5469 # Protect LanguageConverter markup when splitting into parts
5470 $parts = StringUtils::delimiterExplode(
5471 '-{', '}-', '|', $options, true /* allow nesting */
5472 );
5473
5474 # Give extensions a chance to select the file revision for us
5475 $options = [];
5476 $descQuery = false;
5477 $title = Title::castFromLinkTarget( $link ); // hook signature compat
5478 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5479 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5480 $this, $title, $options, $descQuery
5481 );
5482 # Fetch and register the file (file title may be different via hooks)
5483 [ $file, $link ] = $this->fetchFileAndTitle( $link, $options );
5484
5485 # Get parameter map
5486 $handler = $file ? $file->getHandler() : false;
5487
5488 [ $paramMap, $mwArray ] = $this->getImageParams( $handler );
5489
5490 if ( !$file ) {
5491 $this->addTrackingCategory( 'broken-file-category' );
5492 }
5493
5494 # Process the input parameters
5495 $caption = '';
5496 $params = [ 'frame' => [], 'handler' => [],
5497 'horizAlign' => [], 'vertAlign' => [] ];
5498 $seenformat = false;
5499 foreach ( $parts as $part ) {
5500 [ $magicName, $value ] = $mwArray->matchVariableStartToEnd( trim( $part ) );
5501 $validated = false;
5502 if ( isset( $paramMap[$magicName] ) ) {
5503 [ $type, $paramName ] = $paramMap[$magicName];
5504
5505 # Special case; width and height come in one variable together
5506 if ( $type === 'handler' && $paramName === 'width' ) {
5507 // The 'px' suffix has already been localized by img_width
5508 $parsedWidthParam = $this->parseWidthParam( $value, true, true );
5509 // Parsoid applies data-(width|height) attributes to broken
5510 // media spans, for client use. See T273013
5511 $validateFunc = static function ( $name, $value ) use ( $handler ) {
5512 return $handler
5513 ? $handler->validateParam( $name, $value )
5514 : $value > 0;
5515 };
5516 if ( isset( $parsedWidthParam['width'] ) ) {
5517 $width = $parsedWidthParam['width'];
5518 if ( $validateFunc( 'width', $width ) ) {
5519 $params[$type]['width'] = $width;
5520 $validated = true;
5521 }
5522 }
5523 if ( isset( $parsedWidthParam['height'] ) ) {
5524 $height = $parsedWidthParam['height'];
5525 if ( $validateFunc( 'height', $height ) ) {
5526 $params[$type]['height'] = $height;
5527 $validated = true;
5528 }
5529 }
5530 # else no validation -- T15436
5531 } else {
5532 if ( $type === 'handler' ) {
5533 # Validate handler parameter
5534 $validated = $handler->validateParam( $paramName, $value );
5535 } else {
5536 # Validate internal parameters
5537 switch ( $paramName ) {
5538 case 'alt':
5539 case 'class':
5540 $validated = true;
5541 $value = $this->stripAltText( $value, $holders );
5542 break;
5543 case 'link':
5544 [ $paramName, $value ] =
5545 $this->parseLinkParameter(
5546 $this->stripAltText( $value, $holders )
5547 );
5548 if ( $paramName ) {
5549 $validated = true;
5550 if ( $paramName === 'no-link' ) {
5551 $value = true;
5552 }
5553 }
5554 break;
5555 case 'manualthumb':
5556 # @todo FIXME: Possibly check validity here for
5557 # manualthumb? downstream behavior seems odd with
5558 # missing manual thumbs.
5559 $value = $this->stripAltText( $value, $holders );
5560 // fall through
5561 case 'frameless':
5562 case 'framed':
5563 case 'thumbnail':
5564 // use first appearing option, discard others.
5565 $validated = !$seenformat;
5566 $seenformat = true;
5567 break;
5568 default:
5569 # Most other things appear to be empty or numeric...
5570 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5571 }
5572 }
5573
5574 if ( $validated ) {
5575 $params[$type][$paramName] = $value;
5576 }
5577 }
5578 }
5579 if ( !$validated ) {
5580 $caption = $part;
5581 }
5582 }
5583
5584 # Process alignment parameters
5585 if ( $params['horizAlign'] !== [] ) {
5586 $params['frame']['align'] = array_key_first( $params['horizAlign'] );
5587 }
5588 if ( $params['vertAlign'] !== [] ) {
5589 $params['frame']['valign'] = array_key_first( $params['vertAlign'] );
5590 }
5591
5592 $params['frame']['caption'] = $caption;
5593
5594 # Will the image be presented in a frame, with the caption below?
5595 // @phan-suppress-next-line PhanImpossibleCondition
5596 $hasVisibleCaption = isset( $params['frame']['framed'] )
5597 || isset( $params['frame']['thumbnail'] )
5598 || isset( $params['frame']['manualthumb'] );
5599
5600 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5601 # came to also set the caption, ordinary text after the image -- which
5602 # makes no sense, because that just repeats the text multiple times in
5603 # screen readers. It *also* came to set the title attribute.
5604 # Now that we have an alt attribute, we should not set the alt text to
5605 # equal the caption: that's worse than useless, it just repeats the
5606 # text. This is the framed/thumbnail case. If there's no caption, we
5607 # use the unnamed parameter for alt text as well, just for the time be-
5608 # ing, if the unnamed param is set and the alt param is not.
5609 # For the future, we need to figure out if we want to tweak this more,
5610 # e.g., introducing a title= parameter for the title; ignoring the un-
5611 # named parameter entirely for images without a caption; adding an ex-
5612 # plicit caption= parameter and preserving the old magic unnamed para-
5613 # meter for BC; ...
5614
5615 if ( !$hasVisibleCaption ) {
5616 // @phan-suppress-next-line PhanImpossibleCondition
5617 if ( !isset( $params['frame']['alt'] ) && $caption !== '' ) {
5618 # No alt text, use the "caption" for the alt text
5619 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5620 }
5621 # Use the "caption" for the tooltip text
5622 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5623 }
5624 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5625
5626 // hook signature compat again, $link may have changed
5627 $title = Title::castFromLinkTarget( $link );
5628 $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5629
5630 # Linker does the rest
5631 $time = $options['time'] ?? false;
5632 $params['handler']['requestProvenance'] = 'parser';
5633 $ret = Linker::makeImageLink( $this, $link, $file, $params['frame'], $params['handler'],
5634 $time, $descQuery, $this->mOptions->getThumbSize() );
5635
5636 # Give the handler a chance to modify the parser object
5637 if ( $handler ) {
5638 $handler->parserTransformHook( $this, $file );
5639 }
5640 if ( $file ) {
5641 $this->modifyImageHtml( $file, $params, $ret );
5642 }
5643 if ( $shouldReplaceLinkHolders ) {
5644 $this->replaceLinkHoldersPrivate( $ret );
5645 }
5646
5647 return $ret;
5648 }
5649
5668 private function parseLinkParameter( $value ) {
5669 $chars = self::EXT_LINK_URL_CLASS;
5670 $addr = self::EXT_LINK_ADDR;
5671 $prots = $this->urlUtils->validProtocols();
5672 $type = null;
5673 $target = false;
5674 if ( $value === '' ) {
5675 $type = 'no-link';
5676 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5677 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value ) ) {
5678 $this->mOutput->addExternalLink( $value );
5679 $type = 'link-url';
5680 $target = $value;
5681 }
5682 } else {
5683 // Percent-decode link arguments for consistency with wikilink
5684 // handling (T216003#7836261).
5685 //
5686 // There's slight concern here though. The |link= option supports
5687 // two formats, link=Test%22test vs link=[[Test%22test]], both of
5688 // which are about to be decoded.
5689 //
5690 // In the former case, the decoding here is straightforward and
5691 // desirable.
5692 //
5693 // In the latter case, there's a potential for double decoding,
5694 // because the wikilink syntax has a higher precedence and has
5695 // already been parsed as a link before we get here. $value
5696 // has had stripAltText() called on it, which in turn calls
5697 // replaceLinkHoldersText() on the link. So, the text we're
5698 // getting at this point has already been percent decoded.
5699 //
5700 // The problematic case is if %25 is in the title, since that
5701 // decodes to %, which could combine with trailing characters.
5702 // However, % is not a valid link title character, so it would
5703 // not parse as a link and the string we received here would
5704 // still contain the encoded %25.
5705 //
5706 // Hence, double decoded is not an issue. See the test,
5707 // "Should not double decode the link option"
5708 if ( str_contains( $value, '%' ) ) {
5709 $value = rawurldecode( $value );
5710 }
5711 $linkTitle = Title::newFromText( $value );
5712 if ( $linkTitle ) {
5713 $this->mOutput->addLink( $linkTitle );
5714 $type = 'link-title';
5715 $target = $linkTitle;
5716 }
5717 }
5718 return [ $type, $target ];
5719 }
5720
5728 public function modifyImageHtml( File $file, array $params, string &$html ) {
5729 $this->hookRunner->onParserModifyImageHTML( $this, $file, $params, $html );
5730 }
5731
5732 private function stripAltText( string $caption, ?LinkHolderArray $holders = null ): string {
5733 # Strip bad stuff out of the title (tooltip). We can't just use
5734 # replaceLinkHoldersText() here, because if this function is called
5735 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5736 if ( $holders !== null ) {
5737 $tooltip = $holders->replaceText( $caption );
5738 } else {
5739 $tooltip = $this->replaceLinkHoldersText( $caption );
5740 }
5741
5742 # make sure there are no placeholders in thumbnail attributes
5743 # that are later expanded to html- so expand them now and
5744 # remove the tags
5745 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5746 # Compatibility hack! In HTML certain entity references not terminated
5747 # by a semicolon are decoded (but not if we're in an attribute; that's
5748 # how link URLs get away without properly escaping & in queries).
5749 # But wikitext has always required semicolon-termination of entities,
5750 # so encode & where needed to avoid decode of semicolon-less entities.
5751 # See T209236 and
5752 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5753 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5754 $tooltip = preg_replace( "/
5755 & # 1. entity prefix
5756 (?= # 2. followed by:
5757 (?: # a. one of the legacy semicolon-less named entities
5758 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5759 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5760 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5761 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5762 U(?:acute|circ|grave|uml)|Yacute|
5763 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5764 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5765 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5766 frac(?:1(?:2|4)|34)|
5767 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5768 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5769 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5770 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5771 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5772 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5773 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5774 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5775 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5776 )
5777 (?:[^;]|$)) # b. and not followed by a semicolon
5778 # S = study, for efficiency
5779 /Sx", '&amp;', $tooltip );
5780 $tooltip = Sanitizer::stripAllTags( $tooltip );
5781
5782 return $tooltip;
5783 }
5784
5791 public function getTags(): array {
5792 return array_keys( $this->mTagHooks );
5793 }
5794
5799 public function getFunctionSynonyms() {
5800 return $this->mFunctionSynonyms;
5801 }
5802
5807 public function getUrlProtocols(): string {
5808 return $this->urlUtils->validProtocols();
5809 }
5810
5842 private function extractSections(
5843 string $text, string|int $sectionId, string $mode,
5844 string|false $newText, ?PageReference $page = null
5845 ): string|false {
5846 $magicScopeVariable = $this->lock();
5847 $this->startParse(
5848 $page,
5849 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
5850 self::OT_PLAIN,
5851 true
5852 );
5853 $outText = '';
5854 $frame = $this->getPreprocessor()->newFrame();
5855
5856 # Process section extraction flags
5857 $flags = 0;
5858 $sectionParts = explode( '-', $sectionId );
5859 // The section ID may either be a magic string such as 'new' (which should be treated as 0),
5860 // or a numbered section ID in the format of "T-<section index>".
5861 // Explicitly coerce the section index into a number accordingly. (T323373)
5862 $sectionIndex = (int)array_pop( $sectionParts );
5863 foreach ( $sectionParts as $part ) {
5864 if ( $part === 'T' ) {
5865 $flags |= Preprocessor::DOM_FOR_INCLUSION;
5866 }
5867 }
5868
5869 # Check for empty input
5870 if ( $text === '' ) {
5871 # Only sections 0 and T-0 exist in an empty document
5872 if ( $sectionIndex === 0 ) {
5873 return $mode === 'get' ? '' : $newText;
5874 } else {
5875 return $mode === 'get' ? $newText : $text;
5876 }
5877 }
5878
5879 # Preprocess the text
5880 $root = $this->preprocessToDom( $text, $flags );
5881
5882 # <h> nodes indicate section breaks
5883 # They can only occur at the top level, so we can find them by iterating the root's children
5884 $node = $root->getFirstChild();
5885
5886 # Find the target section
5887 if ( $sectionIndex === 0 ) {
5888 # Section zero doesn't nest, level=big
5889 $targetLevel = 1000;
5890 } else {
5891 while ( $node ) {
5892 if ( $node->getName() === 'h' ) {
5893 $bits = $node->splitHeading();
5894 if ( $bits['i'] == $sectionIndex ) {
5895 $targetLevel = $bits['level'];
5896 break;
5897 }
5898 }
5899 if ( $mode === 'replace' ) {
5900 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5901 }
5902 $node = $node->getNextSibling();
5903 }
5904 }
5905
5906 if ( !$node ) {
5907 # Not found
5908 return $mode === 'get' ? $newText : $text;
5909 }
5910
5911 # Find the end of the section, including nested sections
5912 do {
5913 if ( $node->getName() === 'h' ) {
5914 $bits = $node->splitHeading();
5915 $curLevel = $bits['level'];
5916 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
5917 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5918 break;
5919 }
5920 }
5921 if ( $mode === 'get' ) {
5922 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5923 }
5924 $node = $node->getNextSibling();
5925 } while ( $node );
5926
5927 # Write out the remainder (in replace mode only)
5928 if ( $mode === 'replace' ) {
5929 # Output the replacement text.
5930 # Add two newlines. Trailing whitespace in $newText is conventionally
5931 # stripped by the editor, so we need both newlines to restore the paragraph gap.
5932 # Only add trailing whitespace if there is newText.
5933 if ( $newText != "" ) {
5934 $outText .= $newText . "\n\n";
5935 }
5936
5937 while ( $node ) {
5938 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5939 $node = $node->getNextSibling();
5940 }
5941 }
5942
5943 # Re-insert stripped tags
5944 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5945
5946 return $outText;
5947 }
5948
5965 public function getSection( $text, $sectionId, $defaultText = '' ): string|false {
5966 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5967 }
5968
5982 public function replaceSection( $oldText, $sectionId, $newText ): string|false {
5983 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5984 }
5985
6015 public function getFlatSectionInfo( $text ) {
6016 $magicScopeVariable = $this->lock();
6017 $this->startParse(
6018 null,
6019 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
6020 self::OT_PLAIN,
6021 true
6022 );
6023 $frame = $this->getPreprocessor()->newFrame();
6024 $root = $this->preprocessToDom( $text, 0 );
6025 $node = $root->getFirstChild();
6026 $offset = 0;
6027 $currentSection = [
6028 'index' => 0,
6029 'level' => 0,
6030 'offset' => 0,
6031 'heading' => '',
6032 'text' => ''
6033 ];
6034 $sections = [];
6035
6036 while ( $node ) {
6037 $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
6038 if ( $node->getName() === 'h' ) {
6039 $bits = $node->splitHeading();
6040 $sections[] = $currentSection;
6041 $currentSection = [
6042 'index' => $bits['i'],
6043 'level' => $bits['level'],
6044 'offset' => $offset,
6045 'heading' => $nodeText,
6046 'text' => $nodeText
6047 ];
6048 } else {
6049 $currentSection['text'] .= $nodeText;
6050 }
6051 $offset += strlen( $nodeText );
6052 $node = $node->getNextSibling();
6053 }
6054 $sections[] = $currentSection;
6055 return $sections;
6056 }
6057
6069 public function getRevisionId() {
6070 return $this->mRevisionId;
6071 }
6072
6079 public function getRevisionRecordObject() {
6080 if ( $this->mRevisionRecordObject ) {
6081 return $this->mRevisionRecordObject;
6082 }
6083 if ( $this->mOptions->isMessage() ) {
6084 return null;
6085 }
6086
6087 // NOTE: try to get the RevisionRecord object even if mRevisionId is null.
6088 // This is useful when parsing a revision that has not yet been saved.
6089 // However, if we get back a saved revision even though we are in
6090 // preview mode, we'll have to ignore it, see below.
6091 // NOTE: This callback may be used to inject an OLD revision that was
6092 // already loaded, so "current" is a bit of a misnomer. We can't just
6093 // skip it if mRevisionId is set.
6094 $rev = $this->mOptions->getCurrentRevisionRecordCallback()(
6095 $this->getTitle(),
6096 $this
6097 );
6098
6099 if ( !$rev ) {
6100 // The revision record callback returns `false` (not null) to
6101 // indicate that the revision is missing. (See for example
6102 // Parser::defaultFetchRevisionRecord(), the default callback.)
6103 // This API expects `null` instead. (T251952)
6104 return null;
6105 }
6106
6107 if ( $this->mRevisionId === null && $rev->getId() ) {
6108 // We are in preview mode (mRevisionId is null), and the current revision callback
6109 // returned an existing revision. Ignore it and return null, it's probably the page's
6110 // current revision, which is not what we want here. Note that we do want to call the
6111 // callback to allow the unsaved revision to be injected here, e.g. for
6112 // self-transclusion previews.
6113 return null;
6114 }
6115
6116 // If the parse is for a new revision, then the callback should have
6117 // already been set to force the object and should match mRevisionId.
6118 // If not, try to fetch by mRevisionId instead.
6119 if ( $this->mRevisionId && $rev->getId() != $this->mRevisionId ) {
6120 $rev = MediaWikiServices::getInstance()
6121 ->getRevisionLookup()
6122 ->getRevisionById( $this->mRevisionId );
6123 }
6124
6125 $this->mRevisionRecordObject = $rev;
6126
6127 return $this->mRevisionRecordObject;
6128 }
6129
6136 public function getRevisionTimestamp(): string {
6137 if ( $this->mRevisionTimestamp !== null ) {
6138 return $this->mRevisionTimestamp;
6139 }
6140
6141 # Use specified revision timestamp, falling back to the current timestamp
6142 $revObject = $this->getRevisionRecordObject();
6143 $timestamp = $revObject && $revObject->getTimestamp()
6144 ? $revObject->getTimestamp()
6145 : $this->mOptions->getTimestamp();
6146 $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6147
6148 # The cryptic '' timezone parameter tells to use the site-default
6149 # timezone offset instead of the user settings.
6150 # Since this value will be saved into the parser cache, served
6151 # to other users, and potentially even used inside links and such,
6152 # it needs to be consistent for all visitors.
6153 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6154
6155 return $this->mRevisionTimestamp;
6156 }
6157
6164 public function getRevisionUser(): ?string {
6165 if ( $this->mRevisionUser === null ) {
6166 $revObject = $this->getRevisionRecordObject();
6167
6168 # if this template is subst: the revision id will be blank,
6169 # so just use the current user's name
6170 if ( $revObject && $revObject->getUser() ) {
6171 $this->mRevisionUser = $revObject->getUser()->getName();
6172 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6173 $this->mRevisionUser = $this->getUserIdentity()->getName();
6174 } else {
6175 # Note that we fall through here with
6176 # $this->mRevisionUser still null
6177 }
6178 }
6179 return $this->mRevisionUser;
6180 }
6181
6188 public function getRevisionSize() {
6189 if ( $this->mRevisionSize === null ) {
6190 $revObject = $this->getRevisionRecordObject();
6191
6192 # if this variable is subst: the revision id will be blank,
6193 # so just use the parser input size, because the own substitution
6194 # will change the size.
6195 $this->mRevisionSize = $revObject ? $revObject->getSize() : $this->mInputSize;
6196 }
6197 return $this->mRevisionSize;
6198 }
6199
6200 private static function getSectionNameFromStrippedText( string $text ): string {
6201 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6202 $text = Sanitizer::decodeCharReferences( $text );
6203 $text = self::normalizeSectionName( $text );
6204 return $text;
6205 }
6206
6207 private static function makeAnchor( string $sectionName ): string {
6208 return '#' . Sanitizer::escapeIdForLink( $sectionName );
6209 }
6210
6211 private function makeLegacyAnchor( string $sectionName ): string {
6212 $fragmentMode = $this->svcOptions->get( MainConfigNames::FragmentMode );
6213 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6214 // ForAttribute() and ForLink() are the same for legacy encoding
6215 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6216 } else {
6217 $id = Sanitizer::escapeIdForLink( $sectionName );
6218 }
6219
6220 return "#$id";
6221 }
6222
6232 public function guessSectionNameFromWikiText( $text ): string {
6233 # Strip out wikitext links(they break the anchor)
6234 $text = $this->stripSectionName( $text );
6235 $sectionName = self::getSectionNameFromStrippedText( $text );
6236 return self::makeAnchor( $sectionName );
6237 }
6238
6250 public function guessLegacySectionNameFromWikiText( $text ): string {
6251 wfDeprecated( __METHOD__, '1.45' ); // warnings since 1.45
6252 # Strip out wikitext links(they break the anchor)
6253 $text = $this->stripSectionName( $text );
6254 $sectionName = self::getSectionNameFromStrippedText( $text );
6255 return $this->makeLegacyAnchor( $sectionName );
6256 }
6257
6264 public static function guessSectionNameFromStrippedText( $text ): string {
6265 $sectionName = self::getSectionNameFromStrippedText( $text );
6266 return self::makeAnchor( $sectionName );
6267 }
6268
6275 private static function normalizeSectionName( string $text ): string {
6276 # T90902: ensure the same normalization is applied for IDs as to links
6277 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6278 try {
6279 $parts = $titleParser->splitTitleString( "#$text" );
6280 } catch ( MalformedTitleException ) {
6281 return $text;
6282 }
6283 return $parts['fragment'];
6284 }
6285
6301 public function stripSectionName( $text ): string {
6302 # Strip internal link markup
6303 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6304 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6305
6306 # Strip external link markup
6307 # @todo FIXME: Not tolerant to blank link text
6308 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6309 # on how many empty links there are on the page - need to figure that out.
6310 $text = preg_replace(
6311 '/\[(?i:' . $this->urlUtils->validProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6312
6313 # Parse wikitext quotes (italics & bold)
6314 $text = $this->doQuotes( $text );
6315
6316 # Strip HTML tags
6317 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6318 return $text;
6319 }
6320
6339 public function markerSkipCallback( $s, callable $callback ): string {
6340 $i = 0;
6341 $out = '';
6342 while ( $i < strlen( $s ) ) {
6343 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6344 if ( $markerStart === false ) {
6345 $out .= $callback( substr( $s, $i ) );
6346 break;
6347 } else {
6348 $out .= $callback( substr( $s, $i, $markerStart - $i ) );
6349 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6350 if ( $markerEnd === false ) {
6351 $out .= substr( $s, $markerStart );
6352 break;
6353 } else {
6354 $markerEnd += strlen( self::MARKER_SUFFIX );
6355 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6356 $i = $markerEnd;
6357 }
6358 }
6359 }
6360 return $out;
6361 }
6362
6370 public function killMarkers( $text ): string {
6371 return $this->mStripState->killMarkers( $text );
6372 }
6373
6387 public function parseWidthParam( $value, $parseHeight = true, bool $localized = false ) {
6388 $parsedWidthParam = [];
6389 if ( $value === '' ) {
6390 return $parsedWidthParam;
6391 }
6392 $m = [];
6393 if ( !$localized ) {
6394 // Strip a localized 'px' suffix (T374311)
6395 $mwArray = $this->magicWordFactory->newArray( [ 'img_width' ] );
6396 [ $magicWord, $newValue ] = $mwArray->matchVariableStartToEnd( $value );
6397 $value = $magicWord ? $newValue : $value;
6398 }
6399
6400 # (T15500) In both cases (width/height and width only),
6401 # permit trailing "px" for backward compatibility.
6402 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(px)?\s*$/', $value, $m ) ) {
6403 $parsedWidthParam['width'] = intval( $m[1] );
6404 $parsedWidthParam['height'] = intval( $m[2] );
6405 if ( $m[3] ?? false ) {
6406 $this->addTrackingCategory( 'double-px-category' );
6407 }
6408 } elseif ( preg_match( '/^([0-9]*)\s*(px)?\s*$/', $value, $m ) ) {
6409 $parsedWidthParam['width'] = intval( $m[1] );
6410 if ( $m[2] ?? false ) {
6411 $this->addTrackingCategory( 'double-px-category' );
6412 }
6413 }
6414 return $parsedWidthParam;
6415 }
6416
6423 #[\NoDiscard]
6424 protected function lock(): ScopedCallback {
6425 if ( $this->mInParse ) {
6426 $message = 'Parser state cleared while parsing. Did you call Parser::parse recursively?';
6427 $xdebugMode = ini_get( 'xdebug.mode' );
6428 if ( $xdebugMode !== false && str_contains( $xdebugMode, 'develop' ) ) {
6429 $message .= PHP_EOL . 'xdebug.mode=develop is known to cause this issue ' .
6430 '(xdebug bug #2222); consider ';
6431 $improvedMode = implode( ',',
6432 array_diff( explode( ',', $xdebugMode ),
6433 [ 'develop' ] ) );
6434 if ( $improvedMode !== '' ) {
6435 $message .= "using xdebug.mode=$improvedMode instead or ";
6436 }
6437 $message .= "disabling xdebug.";
6438 }
6439 $message .= PHP_EOL . 'lock is held by: ' . $this->mInParse;
6440 throw new LogicException( $message );
6441 }
6442
6443 // Save the backtrace when locking, so that if some code tries locking again,
6444 // we can print the lock owner's backtrace for easier debugging
6445 $e = new RuntimeException;
6446 $this->mInParse = $e->getTraceAsString();
6447
6448 $recursiveCheck = new ScopedCallback( function () {
6449 $this->mInParse = false;
6450 } );
6451
6452 return $recursiveCheck;
6453 }
6454
6462 public function isLocked() {
6463 return (bool)$this->mInParse;
6464 }
6465
6476 public static function stripOuterParagraph( $html ): string {
6477 $m = [];
6478 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && !str_contains( $m[1], '</p>' ) ) {
6479 $html = $m[1];
6480 }
6481
6482 return $html;
6483 }
6484
6497 public static function formatPageTitle( $nsText, $nsSeparator, $mainText, ?Language $titleLang = null ): string {
6498 $html = '';
6499 if ( $nsText !== '' ) {
6500 $html .= '<span class="mw-page-title-namespace">' . HtmlArmor::getHtml( $nsText ) . '</span>';
6501 $html .= '<span class="mw-page-title-separator">' . HtmlArmor::getHtml( $nsSeparator ) . '</span>';
6502 }
6503 $html .= '<span class="mw-page-title-main">' . HtmlArmor::getHtml( $mainText ) . '</span>';
6504 if ( $titleLang !== null ) {
6505 $html = Html::rawElement( 'span', [
6506 'lang' => $titleLang->getHtmlCode(),
6507 'dir' => $titleLang->getDir(),
6508 ], $html );
6509 }
6510 return $html;
6511 }
6512
6519 public static function extractBody( string $text ): string {
6520 $posStart = strpos( $text, '<body' );
6521 if ( $posStart === false ) {
6522 return $text;
6523 }
6524 $posStart = strpos( $text, '>', $posStart );
6525 if ( $posStart === false ) {
6526 return $text;
6527 }
6528 // Skip past the > character
6529 $posStart += 1;
6530 $posEnd = strrpos( $text, '</body>', $posStart );
6531 if ( $posEnd === false ) {
6532 // Strip <body> wrapper even if input was truncated (i.e. missing close tag)
6533 return substr( $text, $posStart );
6534 } else {
6535 return substr( $text, $posStart, $posEnd - $posStart );
6536 }
6537 }
6538
6545 private function setOutputFlag( ParserOutputFlags|string $flag, string $reason ): void {
6546 $this->mOutput->setOutputFlag( $flag );
6547 if ( $flag instanceof ParserOutputFlags ) {
6548 // Convert enumeration to string for logging.
6549 $flag = $flag->value;
6550 }
6551 $name = $this->getTitle()->getPrefixedText();
6552 $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6553 }
6554}
6555
6557class_alias( Parser::class, 'Parser' );
const OT_WIKI
Definition Defines.php:172
const NS_FILE
Definition Defines.php:57
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.
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:71
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:80
Prioritized list of file repositories.
Definition RepoGroup.php:30
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:44
Factory creating MWHttpRequest objects.
Methods for dealing with language codes.
An interface for creating language converters.
isConversionDisabled()
Whether to disable language variant conversion.
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
A service that provides utilities to do with language names and codes.
Base class for language-specific code.
Definition Language.php:65
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:48
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
params(... $params)
Adds parameters to the parameter list of this message.
Definition Message.php:588
Helper class for mapping page value objects to a string key.
Page existence and metadata cache.
Definition LinkCache.php:54
addBadLinkObj( $page, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about a missing page to the process cache.
Class for handling an array of magic words.
Store information about magic words, and create/cache MagicWord objects.
Various tag hooks, registered in every Parser.
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.This method is used by ParserCach...
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:139
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition Parser.php:6250
$mExpensiveFunctionCount
Number of expensive parser function calls.
Definition Parser.php:281
callParserFunction(PPFrame $frame, $function, array $args=[], bool $inSolState=false)
Call a parser function and return an array with text and flags.
Definition Parser.php:3352
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1553
setOutputType( $ot)
Mutator for the output type.
Definition Parser.php:1021
getParseTime()
Return the parse time.
Definition Parser.php:565
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1155
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6301
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:2912
makeImage(LinkTarget $link, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5422
const OT_PLAIN
Output type: like Parser::extractSections() - portions of the original are returned unchanged.
Definition Parser.php:186
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition Parser.php:6264
static statelessFetchTemplate( $page, $parser=false)
Definition Parser.php:3795
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6339
getPreloadText( $text, PageReference $page, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:920
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:3447
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1120
__construct(private ServiceOptions $svcOptions, private ParserCoreTagHooks $parserCoreTagHooks, 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 RepoGroup $repoGroup, private LanguageFactory $languageFactory, 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:393
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:6497
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:596
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:3973
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1135
lock()
Lock the current instance of the parser.
Definition Parser.php:6424
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5102
const EXT_LINK_URL_CLASS
Everything except bracket, space, or control characters.
Definition Parser.php:154
static defaultFetchRevisionRecord(RevisionLookup $revisionLookup, LinkTarget $link, $parser)
Default implementation of fetchCurrentRevisionRecordOfTitle()
Definition Parser.php:3550
preprocess( $text, ?PageReference $page, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition Parser.php:873
firstCallInit()
Used to do various kinds of initialisation on the first call of the parser.
Definition Parser.php:496
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6232
replaceVariables( $text, $frame=false, $argsOnly=false, array $options=[])
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2846
interwikiTransclude(LinkTarget $link, $action)
Transclude an interwiki link.
Definition Parser.php:3865
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4844
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3532
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6069
parseExtensionTagAsTopLevelDoc(string $text, PPFrame|false $frame=false)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition Parser.php:854
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5175
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3936
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:5982
transformMsg( $text, ParserOptions $options, ?PageReference $page=null)
Wrapper for preprocess()
Definition Parser.php:4997
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:1262
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1485
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2232
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:2189
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:1178
static statelessFetchRevisionRecord(LinkTarget $link, $parser=null)
Definition Parser.php:3575
getHookRunner()
Get a HookRunner for calling core hooks.
Definition Parser.php:1603
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1145
static defaultFetchTemplate(RevisionLookup $revLookup, HookRunner $hookRunner, LinkCache $linkCache, ShadowPageLoader $shadowPageLoader, $link, $parser)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback(),...
Definition Parser.php:3646
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2206
parseWidthParam( $value, $parseHeight=true, bool $localized=false)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6387
setPage(?PageReference $t=null)
Set the page used as context for parsing, e.g.
Definition Parser.php:972
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition Parser.php:1053
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:4656
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6370
const OT_PREPROCESS
Output type: like Parser::preprocess()
Definition Parser.php:181
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4859
isLocked()
Will entry points such as parse() throw an exception due to the parser already being active?
Definition Parser.php:6462
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6164
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition Parser.php:6015
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1081
clearState()
Clear Parser state.
Definition Parser.php:508
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:5136
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:4217
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:2937
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3993
getUserIdentity()
Get a user either from the user set on Parser if it's set, or from the ParserOptions object otherwise...
Definition Parser.php:1100
makeImageHtml(LinkTarget $link, string $options)
Parse image options text and use it to make an image.
Definition Parser.php:5402
setUseParsoidFragments(bool $val)
Definition Parser.php:2876
makeLimitReport(ParserOptions $parserOptions, ParserOutput $parserOutput)
Set the limit report data in the current ParserOutput.
Definition Parser.php:696
setUser(?UserIdentity $user)
Set the current user.
Definition Parser.php:942
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition Parser.php:1575
getOutputType()
Accessor for the output type.
Definition Parser.php:1012
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:900
setTitle(?Title $t=null)
Set the context title.
Definition Parser.php:953
getRevisionSize()
Get the size of the revision.
Definition Parser.php:6188
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:1110
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1241
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:5031
preprocessToDom( $text, $flags=0)
Get the document object model for the given wikitext.
Definition Parser.php:2816
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5965
const OT_WIKI
Output type: like Parser::preSaveTransform()
Definition Parser.php:179
fetchTemplateAndTitle(LinkTarget $link)
Fetch the unparsed text of a template and register a reference to it.
Definition Parser.php:3590
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6476
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6079
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:5049
modifyImageHtml(File $file, array $params, string &$html)
Give hooks a chance to modify image thumbnail HTML.
Definition Parser.php:5728
static extractBody(string $text)
Strip everything but the <body> from the provided string.
Definition Parser.php:6519
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6136
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:480
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4901
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:465
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:804
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1918
static replaceTableOfContentsMarker( $text, $toc)
Replace table of contents marker in parsed HTML.
Definition Parser.php:4922
const OT_HTML
Output type: like Parser::parse()
Definition Parser.php:177
static localizeTOC(?TOCData $tocData, Language $lang, ?ILanguageConverter $converter, ?string $preferredVariant=null)
Localize the TOC into the given target language; this includes invoking the language converter on the...
Definition Parser.php:4613
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:828
fetchFileNoRegister(LinkTarget $link, array $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3842
getPage()
Returns the page used as context for parsing, e.g.
Definition Parser.php:995
fetchFileAndTitle(LinkTarget $link, array $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3815
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3503
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:4962
resetOutput()
Reset the ParserOutput.
Definition Parser.php:553
Differences from DOM schema:
const DOM_FOR_INCLUSION
Transclusion mode flag for Preprocessor::preprocessToObj()
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:34
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.
A service which loads shadow content, which is content that is displayed on a nonexistent page with a...
get(PageReference $title)
Try to get a ShadowPage for the given title.
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:18
Maintenance script that protects or unprotects a page.
Definition protect.php:23
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Value object representing a message parameter with one of the types from {.
Store key-value entries in a size-limited in-memory LRU cache.
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'=>[], 'DefaultLockManager'=> null, '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, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> 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, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, '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, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], '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,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, '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',],],], 'EnableSectionShare'=> false, '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, ], 'NamespacesWithoutAutoSummaries' => [ ], '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, '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, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], '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, 'autocreateaccount' => 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, 'createwithcontentmodel' => true, 'logout' => 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, 'createpreviouslyrenamedaccount' => 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' => [ ], 'RestrictUserPageEditing' => false, '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, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => 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, 'createwithcontentmodel' => 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, 'createwithcontentmodel' => 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, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => 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', 'managesessions' => '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, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'ReauthenticateForActions' => [ 'edituserjs' => 'edituserjscss', 'editusercss' => 'edituserjscss', 'editsitejs' => 'editsitejscss', 'editsitecss' => 'editsitejscss', ], 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPUseReportURIDirective' => 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, 'ApiClientErrorSampleRate' => 1.0, '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: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], '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' => [ ], '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, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'RestrictedTagViewRights' => [ ], '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, 'EnableWatchstarPopover' => 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\\RecentChanges\\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', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], 'RestModuleOverrides' => [ ], 'RestExternalModules' => [ ], '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, 'GenerateReqIDFormat' => 'rand24', '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, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, 'ReturnExperimentalPFragmentTypes' => [ ], ], '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', 'DefaultLockManager' => [ 'string', 'null', ], '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', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => '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', 'NamespacesWithoutAutoSummaries' => 'array', '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', 'RestrictUserPageEditing' => 'boolean', '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', ], 'BotPasswordsLimit' => 'integer', 'ReauthenticateForActions' => 'object', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ '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', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RestrictedTagViewRights' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => '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', 'RestModuleOverrides' => 'object', 'RestExternalModules' => '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', ], 'GenerateReqIDFormat' => 'string', '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', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', 'ReturnExperimentalPFragmentTypes' => 'array', ], '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', 'RestModuleOverrides' => 'array_replace_recursive', 'RestExternalModules' => 'array_replace_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', ], ], 'UseCopyrightUpload' => [ 'deprecated' => 'since 1.47 This feature is being removed.', ], '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', ], ], ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'mode' => [ 'type' => 'string', ], ], 'required' => [ 'mode', ], ], ], 'RestExternalModules' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'info' => [ 'type' => 'object', 'properties' => [ 'version' => [ 'type' => 'string', ], 'title' => [ 'type' => 'string', ], 'x-i18n-title' => [ 'type' => 'string', ], 'description' => [ 'type' => 'string', ], 'x-i18n-description' => [ 'type' => 'string', ], ], 'required' => [ 'version', ], ], 'base' => [ 'type' => 'string', 'format' => 'uri', ], 'spec' => [ 'type' => 'string', 'format' => 'uri', ], ], 'required' => [ 'info', 'base', 'spec', ], ], ], '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
Service for looking up page revisions.
getKnownLatestRevision(PageIdentity $page, $revId=0)
Load a revision based on a known page ID and latest revision ID from the DB.
getRevisionByTitle( $page, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given title.
Interface for objects representing user identity.
array $params
The job parameters.