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;
77use Psr\Log\LoggerInterface;
78use RuntimeException;
79use UnexpectedValueException;
80use Wikimedia\Bcp47Code\Bcp47CodeValue;
82use 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 implements MessageLocalizer {
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 private LinkAlwaysKnownLookup $linkAlwaysKnownLookup,
422 ) {
423 $this->deprecateDynamicPropertiesAccess( '1.42', __CLASS__ );
424 $this->deprecatePublicProperty( 'ot', '1.35', __CLASS__ );
425 $this->deprecatePublicProperty( 'mTitle', '1.35', __CLASS__ );
426 $this->deprecatePublicProperty( 'mOptions', '1.35', __CLASS__ );
427
429 // Direct construction of Parser was deprecated in 1.34 and
430 // removed in 1.36; use a ParserFactory instead.
431 throw new BadMethodCallException( 'Direct construction of Parser not allowed' );
432 }
433 $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
434
435 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->urlUtils->validProtocols() . ')' .
436 self::EXT_LINK_ADDR .
437 self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*)\]/Su';
438
439 $this->hookRunner = new HookRunner( $hookContainer );
440
441 $this->mPreprocessor = new Preprocessor_Hash(
442 $this,
443 $this->wanCache,
444 [
445 'cacheThreshold' => $svcOptions->get( MainConfigNames::PreprocessorCacheThreshold ),
446 'disableLangConversion' => $languageConverterFactory->isConversionDisabled(),
447 ]
448 );
449
450 // These steps used to be done in "::firstCallInit()"
451 // (if you're chasing a reference from some old code)
452 CoreParserFunctions::register(
453 $this,
454 new ServiceOptions( CoreParserFunctions::REGISTER_OPTIONS, $svcOptions )
455 );
456 $parserCoreTagHooks->register( $this );
457 $this->initializeVariables();
458
459 $this->hookRunner->onParserFirstCallInit( $this );
460 $this->mTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/Missing' );
461 }
462
466 public function __destruct() {
467 // @phan-suppress-next-line PhanRedundantCondition Typed property not set in constructor, may be uninitialized
468 if ( isset( $this->mLinkHolders ) ) {
469 // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
470 unset( $this->mLinkHolders );
471 }
472 // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
473 foreach ( $this as $name => $value ) {
474 unset( $this->$name );
475 }
476 }
477
481 public function __clone() {
482 $this->mInParse = false;
483
484 $this->mPreprocessor = clone $this->mPreprocessor;
485 $this->mPreprocessor->resetParser( $this );
486
487 $this->hookRunner->onParserCloned( $this );
488 }
489
497 public function firstCallInit() {
498 /*
499 * This method should be hard-deprecated once remaining calls are
500 * removed; it no longer does anything.
501 */
502 }
503
509 public function clearState() {
510 $this->resetOutput();
511 $this->mAutonumber = 0;
512 $this->mLinkHolders = new LinkHolderArray(
513 $this,
514 $this->getContentLanguageConverter(),
515 $this->getHookContainer()
516 );
517 $this->mLinkID = 0;
518 $this->mRevisionTimestamp = null;
519 $this->mRevisionId = null;
520 $this->mRevisionUser = null;
521 $this->mRevisionSize = null;
522 $this->mRevisionRecordObject = null;
523 $this->mVarCache = [];
524 $this->mUser = null;
525 $this->currentRevisionCache = null;
526
527 $this->mStripState = new StripState( $this );
528
529 # Clear these on every parse, T6549
530 $this->mTplRedirCache = [];
531 $this->mTplDomCache = [];
532
533 $this->mShowToc = true;
534 $this->mForceTocPosition = false;
535 $this->mIncludeSizes = [
536 'post-expand' => 0,
537 'arg' => 0,
538 ];
539 $this->mPPNodeCount = 0;
540 $this->mHighestExpansionDepth = 0;
541 $this->mHeadings = [];
542 $this->mDoubleUnderscores = [];
543 $this->mExpensiveFunctionCount = 0;
544
545 $this->mProfiler = new SectionProfiler();
546
547 $this->hookRunner->onParserClearState( $this );
548 }
549
554 public function resetOutput() {
555 $this->mOutput = new ParserOutput;
556 $this->mOptions->registerWatcher( $this->mOutput->recordOption( ... ) );
557 }
558
566 public function getParseTime(): DateTime {
567 $ts = $this->mOptions->getTimestamp(); /* TS::MW */
568 $date = DateTime::createFromFormat(
569 'YmdHis', $ts, new DateTimeZone( 'UTC' )
570 );
571 if ( $this->hookContainer->isRegistered( 'ParserGetVariableValueTs' ) ) {
572 $s = $date->format( 'U' );
573 $this->hookRunner->onParserGetVariableValueTs( $this, $s );
574 $date = ( new MWTimestamp( $s ) )->timestamp;
575 }
576 return $date;
577 }
578
597 public function parse(
598 $text, PageReference $page, ParserOptions $options,
599 $linestart = true, $clearState = true, $revid = null
600 ) {
601 if ( $clearState ) {
602 // We use U+007F DELETE to construct strip markers, so we have to make
603 // sure that this character does not occur in the input text.
604 $text = strtr( $text, "\x7f", "?" );
605 $magicScopeVariable = $this->lock();
606 }
607 // Strip U+0000 NULL (T159174)
608 $text = str_replace( "\000", '', $text );
609
610 $this->startParse( $page, $options, self::OT_HTML, $clearState );
611
612 $this->currentRevisionCache = null;
613 $this->mInputSize = strlen( $text );
614 $this->mOutput->resetParseStartTime();
615
616 $oldRevisionId = $this->mRevisionId;
617 $oldRevisionRecordObject = $this->mRevisionRecordObject;
618 $oldRevisionTimestamp = $this->mRevisionTimestamp;
619 $oldRevisionUser = $this->mRevisionUser;
620 $oldRevisionSize = $this->mRevisionSize;
621 if ( $revid !== null ) {
622 $this->mRevisionId = $revid;
623 $this->mRevisionRecordObject = null;
624 $this->mRevisionTimestamp = null;
625 $this->mRevisionUser = null;
626 $this->mRevisionSize = null;
627 }
628
629 $text = $this->internalParse( $text );
630 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
631
632 $text = $this->internalParseHalfParsed( $text, true, $linestart );
633
641 if ( !$options->getDisableTitleConversion()
642 && !isset( $this->mDoubleUnderscores['nocontentconvert'] )
643 && !isset( $this->mDoubleUnderscores['notitleconvert'] )
644 && $this->mOutput->getDisplayTitle() === false
645 ) {
646 $converter = $this->getTargetLanguageConverter();
647 $titleText = $converter->getConvRuleTitle();
648 if ( $titleText !== false ) {
649 $titleText = Sanitizer::removeSomeTags( $titleText );
650 // Split off localized namespace prefix from -{T|...}- result
651 // if we can (T314399); see CoreParserFunctions::displaytitle()
652 [ $nsText, $nsSeparator, $mainText ] =
653 $this->splitDisplayTitle( new HtmlArmor( $titleText ), $converter );
654 } else {
655 [ $nsText, $nsSeparator, $mainText ] = $converter->convertSplitTitle( $page );
656 $titleLang = $this->languageFactory->getLanguage( $converter->getPreferredVariant() );
657 $titleText = self::formatPageTitle( $nsText, $nsSeparator, $mainText, $titleLang );
658 }
659 $this->mOutput->setDisplayTitleParts(
660 $nsText, $nsSeparator, $mainText, new HtmlArmor( $titleText )
661 );
662 }
663
664 # Recording timing info. Must be called before finalizeAdaptiveCacheExpiry() and
665 # makeLimitReport(), which make use of the timing info.
666 $this->mOutput->recordTimeProfile();
667
668 # Compute runtime adaptive expiry if set
669 $this->mOutput->finalizeAdaptiveCacheExpiry();
670
671 # Warn if too many heavyweight parser functions were used
672 if ( $this->mExpensiveFunctionCount > $options->getExpensiveParserFunctionLimit() ) {
673 $this->limitationWarn( 'expensive-parserfunction',
674 $this->mExpensiveFunctionCount,
676 );
677 }
678
679 # Information on limits, for the benefit of users who try to skirt them
680 $this->makeLimitReport( $this->mOptions, $this->mOutput );
681
682 $this->mOutput->setFromParserOptions( $options );
683
684 $this->mOutput->setContentHolderText( $text );
685
686 $this->mRevisionId = $oldRevisionId;
687 $this->mRevisionRecordObject = $oldRevisionRecordObject;
688 $this->mRevisionTimestamp = $oldRevisionTimestamp;
689 $this->mRevisionUser = $oldRevisionUser;
690 $this->mRevisionSize = $oldRevisionSize;
691 $this->mInputSize = false;
692 $this->currentRevisionCache = null;
693
694 return $this->mOutput;
695 }
696
701 public function makeLimitReport(
702 ParserOptions $parserOptions, ParserOutput $parserOutput
703 ) {
704 if ( !$this->svcOptions->get( MainConfigNames::EnableParserLimitReporting ) ) {
705 return;
706 }
707 if ( $parserOptions->isMessage() ) {
708 // No need to include limit report information in
709 // user interface messages.
710 return;
711 }
712
713 $maxIncludeSize = $parserOptions->getMaxIncludeSize();
714
715 $cpuTime = $parserOutput->getTimeProfile( 'cpu' );
716 if ( $cpuTime !== null ) {
717 $parserOutput->setLimitReportData( 'limitreport-cputime',
718 sprintf( "%.3f", $cpuTime )
719 );
720 }
721
722 $wallTime = $parserOutput->getTimeProfile( 'wall' );
723 $parserOutput->setLimitReportData( 'limitreport-walltime',
724 sprintf( "%.3f", $wallTime )
725 );
726
727 $parserOutput->setLimitReportData( 'limitreport-ppvisitednodes',
728 [ $this->mPPNodeCount, $parserOptions->getMaxPPNodeCount() ]
729 );
730 $revisionSize = $this->mInputSize !== false ? $this->mInputSize :
731 $this->getRevisionSize();
732 $parserOutput->setLimitReportData( 'limitreport-revisionsize',
733 [ $revisionSize ?? -1, $this->svcOptions->get( MainConfigNames::MaxArticleSize ) * 1024 ]
734 );
735 $parserOutput->setLimitReportData( 'limitreport-postexpandincludesize',
736 [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
737 );
738 $parserOutput->setLimitReportData( 'limitreport-templateargumentsize',
739 [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
740 );
741 $parserOutput->setLimitReportData( 'limitreport-expansiondepth',
742 [ $this->mHighestExpansionDepth, $parserOptions->getMaxPPExpandDepth() ]
743 );
744 $parserOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
745 [ $this->mExpensiveFunctionCount, $parserOptions->getExpensiveParserFunctionLimit() ]
746 );
747
748 foreach ( $this->mStripState->getLimitReport() as [ $key, $value ] ) {
749 $parserOutput->setLimitReportData( $key, $value );
750 }
751
752 $this->hookRunner->onParserLimitReportPrepare( $this, $parserOutput );
753
754 // Add on template profiling data in human/machine readable way
755 $dataByFunc = $this->mProfiler->getFunctionStats();
756 uasort( $dataByFunc, static function ( $a, $b ) {
757 return $b['real'] <=> $a['real']; // descending order
758 } );
759 $profileReport = [];
760 foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
761 $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
762 $item['%real'], $item['real'], $item['calls'],
763 htmlspecialchars( $item['name'] ) );
764 }
765
766 $parserOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
767
768 // Add other cache related metadata
769 if ( $this->svcOptions->get( MainConfigNames::ShowHostnames ) ) {
770 $parserOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
771 }
772 $parserOutput->setLimitReportData( 'cachereport-timestamp',
773 $parserOutput->getCacheTime() );
774 $parserOutput->setLimitReportData( 'cachereport-ttl',
775 $parserOutput->getCacheExpiry() );
776 $parserOutput->setLimitReportData( 'cachereport-transientcontent',
777 $parserOutput->hasReducedExpiry() );
778 if ( $parserOutput->getCacheExpirySource() !== null ) {
779 $parserOutput->setLimitReportData( 'cachereport-expiry-source',
780 $parserOutput->getCacheExpirySource() );
781 }
782 }
783
809 public function recursiveTagParse( $text, $frame = false ): string {
810 $text = $this->internalParse( $text, false, $frame );
811 return $text;
812 }
813
833 public function recursiveTagParseFully( $text, $frame = false ): string {
834 $text = $this->recursiveTagParse( $text, $frame );
835 $text = $this->internalParseHalfParsed( $text, false );
836 return $text;
837 }
838
859 public function parseExtensionTagAsTopLevelDoc( string $text, PPFrame|false $frame = false ): string {
860 $text = $this->recursiveTagParse( $text, $frame );
861 $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
862 $text = $this->internalParseHalfParsed( $text, true );
863 return $text;
864 }
865
878 public function preprocess(
879 $text,
880 ?PageReference $page,
881 ParserOptions $options,
882 $revid = null,
883 $frame = false
884 ) {
885 $magicScopeVariable = $this->lock();
886 $this->startParse( $page, $options, self::OT_PREPROCESS, true );
887 if ( $revid !== null ) {
888 $this->mRevisionId = $revid;
889 }
890 $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
891 $text = $this->replaceVariables( $text, $frame );
892 $text = $this->mStripState->unstripBoth( $text );
893 return $text;
894 }
895
905 public function recursivePreprocess( $text, $frame = false ): string {
906 $text = $this->replaceVariables( $text, $frame );
907 $text = $this->mStripState->unstripBoth( $text );
908 return $text;
909 }
910
925 public function getPreloadText( $text, PageReference $page, ParserOptions $options, $params = [] ): string {
926 $msg = new RawMessage( $text );
927 $text = $msg->params( $params )->plain();
928
929 # Parser (re)initialisation
930 $magicScopeVariable = $this->lock();
931 $this->startParse( $page, $options, self::OT_PLAIN, true );
932
933 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
934 $dom = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION );
935 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
936 $text = $this->mStripState->unstripBoth( $text );
937 return $text;
938 }
939
947 public function setUser( ?UserIdentity $user ) {
948 $this->mUser = $user;
949 }
950
958 public function setTitle( ?Title $t = null ) {
959 $this->setPage( $t );
960 }
961
967 public function getTitle(): Title {
968 return $this->mTitle;
969 }
970
977 public function setPage( ?PageReference $t = null ) {
978 if ( !$t ) {
979 $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
980 } else {
981 // For now (early 1.37 alpha), always convert to Title, so we don't have to do it over
982 // and over again in other methods. Eventually, we will no longer need to have a Title
983 // instance internally.
984 $t = Title::newFromPageReference( $t );
985 }
986
987 if ( $t->hasFragment() ) {
988 # Strip the fragment to avoid various odd effects
989 $this->mTitle = $t->createFragmentTarget( '' );
990 } else {
991 $this->mTitle = $t;
992 }
993 }
994
1000 public function getPage(): PageReference {
1001 if ( $this->mTitle->isSpecial( 'Badtitle' ) ) {
1002 [ , $subPage ] = $this->specialPageFactory->resolveAlias( $this->mTitle->getDBkey() );
1003
1004 if ( $subPage === 'Missing' ) {
1005 wfDeprecated( __METHOD__ . ' without a Title set', '1.34' );
1006 }
1007 }
1008
1009 return $this->mTitle;
1010 }
1011
1017 public function getOutputType(): int {
1018 return $this->mOutputType;
1019 }
1020
1026 public function setOutputType( $ot ): void {
1027 $this->mOutputType = $ot;
1028 # Shortcut alias
1029 $this->ot = [
1030 'html' => $ot == self::OT_HTML,
1031 'wiki' => $ot == self::OT_WIKI,
1032 'pre' => $ot == self::OT_PREPROCESS,
1033 'plain' => $ot == self::OT_PLAIN,
1034 ];
1035 }
1036
1041 public function getOutput(): ParserOutput {
1042 return $this->mOutput;
1043 }
1044
1049 public function getOptions() {
1050 return $this->mOptions;
1051 }
1052
1058 public function setOptions( ParserOptions $options ): void {
1059 $this->mOptions = $options;
1060 }
1061
1066 public function nextLinkID() {
1067 return $this->mLinkID++;
1068 }
1069
1074 public function setLinkID( $id ) {
1075 $this->mLinkID = $id;
1076 }
1077
1086 public function getTargetLanguage() {
1087 $target = $this->mOptions->getTargetLanguage();
1088
1089 if ( $target !== null ) {
1090 return $target;
1091 } elseif ( $this->mOptions->getInterfaceMessage() ) {
1092 return $this->mOptions->getUserLangObj();
1093 }
1094
1095 return $this->getTitle()->getPageLanguage();
1096 }
1097
1105 public function getUserIdentity(): UserIdentity {
1106 return $this->mUser ?? $this->getOptions()->getUserIdentity();
1107 }
1108
1115 public function getPreprocessor() {
1116 return $this->mPreprocessor;
1117 }
1118
1125 public function getLinkRenderer() {
1126 // XXX We make the LinkRenderer with current options and then cache it forever
1127 if ( !$this->mLinkRenderer ) {
1128 $this->mLinkRenderer = $this->linkRendererFactory->create();
1129 }
1130
1131 return $this->mLinkRenderer;
1132 }
1133
1140 public function getMagicWordFactory() {
1141 return $this->magicWordFactory;
1142 }
1143
1150 public function getContentLanguage() {
1151 return $this->contLang;
1152 }
1153
1160 public function getBadFileLookup() {
1161 return $this->badFileLookup;
1162 }
1163
1183 public static function extractTagsAndParams( array $elements, $text, &$matches ): string {
1184 static $n = 1;
1185 $stripped = '';
1186 $matches = [];
1187
1188 $taglist = implode( '|', $elements );
1189 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1190
1191 while ( $text != '' ) {
1192 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1193 $stripped .= $p[0];
1194 if ( count( $p ) < 5 ) {
1195 break;
1196 }
1197 if ( count( $p ) > 5 ) {
1198 # comment
1199 $element = $p[4];
1200 $attributes = '';
1201 $close = '';
1202 $inside = $p[5];
1203 } else {
1204 # tag
1205 [ , $element, $attributes, $close, $inside ] = $p;
1206 }
1207
1208 $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1209 $stripped .= $marker;
1210
1211 if ( $close === '/>' ) {
1212 # Empty element tag, <tag />
1213 $content = null;
1214 $text = $inside;
1215 $tail = null;
1216 } else {
1217 if ( $element === '!--' ) {
1218 $end = '/(-->)/';
1219 } else {
1220 $end = "/(<\\/$element\\s*>)/i";
1221 }
1222 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1223 $content = $q[0];
1224 if ( count( $q ) < 3 ) {
1225 # No end tag -- let it run out to the end of the text.
1226 $tail = '';
1227 $text = '';
1228 } else {
1229 [ , $tail, $text ] = $q;
1230 }
1231 }
1232
1233 $matches[$marker] = [ $element,
1234 $content,
1235 Sanitizer::decodeTagAttributes( $attributes ),
1236 "<$element$attributes$close$content$tail" ];
1237 }
1238 return $stripped;
1239 }
1240
1246 public function getStripList() {
1247 return $this->mStripList;
1248 }
1249
1254 public function getStripState() {
1255 return $this->mStripState;
1256 }
1257
1267 public function insertStripItem( $text ): string {
1268 $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1269 $this->mMarkerIndex++;
1270 $this->mStripState->addGeneral( $marker, $text );
1271 return $marker;
1272 }
1273
1280 private function handleTables( string $text ): string {
1281 $lines = StringUtils::explode( "\n", $text );
1282 $out = '';
1283 $td_history = []; # Is currently a td tag open?
1284 $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1285 $tr_history = []; # Is currently a tr tag open?
1286 $tr_attributes = []; # history of tr attributes
1287 $has_opened_tr = []; # Did this table open a <tr> element?
1288 $indent_level = 0; # indent level of the table
1289
1290 foreach ( $lines as $outLine ) {
1291 $line = trim( $outLine );
1292
1293 if ( $line === '' ) { # empty line, go to next line
1294 $out .= $outLine . "\n";
1295 continue;
1296 }
1297
1298 $first_character = $line[0];
1299 $first_two = substr( $line, 0, 2 );
1300 $matches = [];
1301
1302 if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1303 # First check if we are starting a new table
1304 $indent_level = strlen( $matches[1] );
1305
1306 $attributes = $this->mStripState->unstripBoth( $matches[2] );
1307 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1308
1309 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1310 $td_history[] = false;
1311 $last_tag_history[] = '';
1312 $tr_history[] = false;
1313 $tr_attributes[] = '';
1314 $has_opened_tr[] = false;
1315 } elseif ( count( $td_history ) == 0 ) {
1316 # Don't do any of the following
1317 $out .= $outLine . "\n";
1318 continue;
1319 } elseif ( $first_two === '|}' ) {
1320 # We are ending a table
1321 $line = '</table>' . substr( $line, 2 );
1322 $last_tag = array_pop( $last_tag_history );
1323
1324 if ( !array_pop( $has_opened_tr ) ) {
1325 $line = "<tr><td></td></tr>{$line}";
1326 }
1327
1328 if ( array_pop( $tr_history ) ) {
1329 $line = "</tr>{$line}";
1330 }
1331
1332 if ( array_pop( $td_history ) ) {
1333 $line = "</{$last_tag}>{$line}";
1334 }
1335 array_pop( $tr_attributes );
1336 if ( $indent_level > 0 ) {
1337 $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1338 } else {
1339 $outLine = $line;
1340 }
1341 } elseif ( $first_two === '|-' ) {
1342 # Now we have a table row
1343 $line = preg_replace( '#^\|-+#', '', $line );
1344
1345 # Whats after the tag is now only attributes
1346 $attributes = $this->mStripState->unstripBoth( $line );
1347 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1348 array_pop( $tr_attributes );
1349 $tr_attributes[] = $attributes;
1350
1351 $line = '';
1352 $last_tag = array_pop( $last_tag_history );
1353 array_pop( $has_opened_tr );
1354 $has_opened_tr[] = true;
1355
1356 if ( array_pop( $tr_history ) ) {
1357 $line = '</tr>';
1358 }
1359
1360 if ( array_pop( $td_history ) ) {
1361 $line = "</{$last_tag}>{$line}";
1362 }
1363
1364 $outLine = $line;
1365 $tr_history[] = false;
1366 $td_history[] = false;
1367 $last_tag_history[] = '';
1368 } elseif ( $first_character === '|'
1369 || $first_character === '!'
1370 || $first_two === '|+'
1371 ) {
1372 # This might be cell elements, td, th or captions
1373 if ( $first_two === '|+' ) {
1374 $first_character = '+';
1375 $line = substr( $line, 2 );
1376 } else {
1377 $line = substr( $line, 1 );
1378 }
1379
1380 // Implies both are valid for table headings.
1381 if ( $first_character === '!' ) {
1382 $line = StringUtils::replaceMarkup( '!!', '||', $line );
1383 }
1384
1385 # Split up multiple cells on the same line.
1386 # FIXME : This can result in improper nesting of tags processed
1387 # by earlier parser steps.
1388 $cells = explode( '||', $line );
1389
1390 $outLine = '';
1391
1392 # Loop through each table cell
1393 foreach ( $cells as $cell ) {
1394 $previous = '';
1395 if ( $first_character !== '+' ) {
1396 $tr_after = array_pop( $tr_attributes );
1397 if ( !array_pop( $tr_history ) ) {
1398 $previous = "<tr{$tr_after}>\n";
1399 }
1400 $tr_history[] = true;
1401 $tr_attributes[] = '';
1402 array_pop( $has_opened_tr );
1403 $has_opened_tr[] = true;
1404 }
1405
1406 $last_tag = array_pop( $last_tag_history );
1407
1408 if ( array_pop( $td_history ) ) {
1409 $previous = "</{$last_tag}>\n{$previous}";
1410 }
1411
1412 if ( $first_character === '|' ) {
1413 $last_tag = 'td';
1414 } elseif ( $first_character === '!' ) {
1415 $last_tag = 'th';
1416 } elseif ( $first_character === '+' ) {
1417 $last_tag = 'caption';
1418 } else {
1419 $last_tag = '';
1420 }
1421
1422 $last_tag_history[] = $last_tag;
1423
1424 # A cell could contain both parameters and data
1425 $cell_data = explode( '|', $cell, 2 );
1426
1427 # T2553: Note that a '|' inside an invalid link should not
1428 # be mistaken as delimiting cell parameters
1429 # Bug T153140: Neither should language converter markup.
1430 if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1431 $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1432 } elseif ( count( $cell_data ) == 1 ) {
1433 // Whitespace in cells is trimmed
1434 $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1435 } else {
1436 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1437 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1438 // Whitespace in cells is trimmed
1439 $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1440 }
1441
1442 $outLine .= $cell;
1443 $td_history[] = true;
1444 }
1445 }
1446 $out .= $outLine . "\n";
1447 }
1448
1449 # Closing open td, tr && table
1450 while ( count( $td_history ) > 0 ) {
1451 if ( array_pop( $td_history ) ) {
1452 $out .= "</td>\n";
1453 }
1454 if ( array_pop( $tr_history ) ) {
1455 $out .= "</tr>\n";
1456 }
1457 if ( !array_pop( $has_opened_tr ) ) {
1458 $out .= "<tr><td></td></tr>\n";
1459 }
1460
1461 $out .= "</table>\n";
1462 }
1463
1464 # Remove trailing line-ending (b/c)
1465 if ( substr( $out, -1 ) === "\n" ) {
1466 $out = substr( $out, 0, -1 );
1467 }
1468
1469 # special case: don't return empty table
1470 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1471 $out = '';
1472 }
1473
1474 return $out;
1475 }
1476
1490 public function internalParse( $text, $isMain = true, $frame = false ): string {
1491 $origText = $text;
1492
1493 # Hook to suspend the parser in this state
1494 if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1495 return $text;
1496 }
1497
1498 # if $frame is provided, then use $frame for replacing any variables
1499 if ( $frame ) {
1500 # use frame depth to infer how include/noinclude tags should be handled
1501 # depth=0 means this is the top-level document; otherwise it's an included document
1502 if ( !$frame->depth ) {
1503 $flag = 0;
1504 } else {
1505 $flag = Preprocessor::DOM_FOR_INCLUSION;
1506 }
1507 $dom = $this->preprocessToDom( $text, $flag );
1508 $text = $frame->expand( $dom );
1509 } else {
1510 # if $frame is not provided, then use old-style replaceVariables
1511 $text = $this->replaceVariables( $text );
1512 }
1513
1514 $text = Sanitizer::internalRemoveHtmlTags(
1515 $text,
1516 // Callback from the Sanitizer for unstripping items found in
1517 // HTML attribute values, so they can be safely tested and escaped.
1518 function ( &$text, $frame = false ) {
1519 $text = $this->mStripState->unstripBoth( $text );
1520 },
1521 false,
1522 [],
1523 []
1524 );
1525 $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1526
1527 # Tables need to come after variable replacement for things to work
1528 # properly; putting them before other transformations should keep
1529 # exciting things like link expansions from showing up in surprising
1530 # places.
1531 $text = $this->handleTables( $text );
1532
1533 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1534
1535 $text = $this->handleDoubleUnderscore( $text );
1536
1537 $text = $this->handleHeadings( $text );
1538 $text = $this->handleInternalLinks( $text );
1539 $text = $this->handleAllQuotes( $text );
1540 $text = $this->handleExternalLinks( $text );
1541
1542 # handleInternalLinks may sometimes leave behind
1543 # absolute URLs, which have to be masked to hide them from handleExternalLinks
1544 $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1545
1546 $text = $this->handleMagicLinks( $text );
1547 $text = $this->finalizeHeadings( $text, $origText, $isMain );
1548
1549 return $text;
1550 }
1551
1559 return $this->languageConverterFactory->getLanguageConverter(
1560 $this->getTargetLanguage()
1561 );
1562 }
1563
1567 private function getContentLanguageConverter(): ILanguageConverter {
1568 return $this->languageConverterFactory->getLanguageConverter(
1569 $this->getContentLanguage()
1570 );
1571 }
1572
1580 protected function getHookContainer() {
1581 return $this->hookContainer;
1582 }
1583
1588 public function getNamespaceInfo(): NamespaceInfo {
1589 return $this->nsInfo;
1590 }
1591
1596 public function getMiserMode(): bool {
1597 return $this->svcOptions->get( MainConfigNames::MiserMode );
1598 }
1599
1608 protected function getHookRunner() {
1609 return $this->hookRunner;
1610 }
1611
1621 private function internalParseHalfParsed( string $text, bool $isMain = true, bool $linestart = true ): string {
1622 $text = $this->mStripState->unstripGeneral( $text );
1623
1624 $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1625
1626 $this->replaceLinkHoldersPrivate( $text );
1627
1635 $converter = null;
1636 if ( !( $this->mOptions->getDisableContentConversion()
1637 || isset( $this->mDoubleUnderscores['nocontentconvert'] )
1638 || $this->mOptions->getInterfaceMessage()
1639 || $this->mOptions->getUseParsoid()
1640 ) ) {
1641 # The position of the convert() call should not be changed. it
1642 # assumes that the links are all replaced and the only thing left
1643 # is the <nowiki> mark.
1644 $converter = $this->getTargetLanguageConverter();
1645 $text = $converter->convert( $text );
1646 // TOC will be converted below.
1647 }
1648 // Convert the TOC. This is done *after* the main text
1649 // so that all the editor-defined conversion rules (by convention
1650 // defined at the start of the article) are applied to the TOC
1651 self::localizeTOC(
1652 $this->mOutput->getTOCData(),
1653 $this->getTargetLanguage(),
1654 $converter, // null if conversion is to be suppressed.
1655 $converter?->getPreferredVariant()
1656 );
1657 if ( $converter ) {
1658 $this->mOutput->setLanguage( new Bcp47CodeValue(
1659 LanguageCode::bcp47( $converter->getPreferredVariant() )
1660 ) );
1661 } else {
1662 $this->mOutput->setLanguage( $this->getTargetLanguage() );
1663 }
1664
1665 $text = $this->mStripState->unstripNoWiki( $text );
1666
1667 $text = $this->mStripState->unstripGeneral( $text );
1668
1669 $text = $this->tidy->tidy( $text, Sanitizer::armorFrenchSpaces( ... ) );
1670
1671 if ( $isMain ) {
1672 $this->mOutput->setTitle( $this->getPage() );
1673 $this->hookRunner->onParserAfterTidy( $this, $text );
1674 }
1675
1676 return $text;
1677 }
1678
1689 private function handleMagicLinks( string $text ): string {
1690 $prots = $this->urlUtils->validAbsoluteProtocols();
1691 $urlChar = self::EXT_LINK_URL_CLASS;
1692 $addr = self::EXT_LINK_ADDR;
1693 $space = self::SPACE_NOT_NL; # non-newline space
1694 $spdash = "(?:-|$space)"; # a dash or a non-newline space
1695 $spaces = "$space++"; # possessive match of 1 or more spaces
1696 $resultText = preg_replace_callback(
1697 '!(?: # Start cases
1698 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1699 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1700 (\b # m[3]: Free external links
1701 (?i:$prots)
1702 ($addr$urlChar*) # m[4]: Post-protocol path
1703 ) |
1704 \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1705 ([0-9]+)\b |
1706 \bISBN $spaces ( # m[6]: ISBN, capture number
1707 (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1708 (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1709 [0-9Xx] # check digit
1710 )\b
1711 )!xu",
1712 $this->magicLinkCallback( ... ),
1713 $text
1714 );
1715 // preg_replace_callback in "u" mode returns null, when a non-valid utf-8 character is discovered in the input
1716 if ( $resultText === null ) {
1717 $this->logger->warning( "Input text contains non-valid UTF-8 characters (" . __METHOD__ . ")" );
1718 return $text;
1719 }
1720 return $resultText;
1721 }
1722
1727 private function magicLinkCallback( array $m ): string {
1728 if ( isset( $m[1] ) && $m[1] !== '' ) {
1729 # Skip anchor
1730 return $m[0];
1731 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1732 # Skip HTML element
1733 return $m[0];
1734 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1735 # Free external link
1736 return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1737 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1738 # RFC or PMID
1739 if ( str_starts_with( $m[0], 'RFC' ) ) {
1740 if ( !$this->mOptions->getMagicRFCLinks() ) {
1741 return $m[0];
1742 }
1743 $keyword = 'RFC';
1744 $urlmsg = 'rfcurl';
1745 $cssClass = 'mw-magiclink-rfc';
1746 $trackingCat = 'magiclink-tracking-rfc';
1747 $id = $m[5];
1748 } elseif ( str_starts_with( $m[0], 'PMID' ) ) {
1749 if ( !$this->mOptions->getMagicPMIDLinks() ) {
1750 return $m[0];
1751 }
1752 $keyword = 'PMID';
1753 $urlmsg = 'pubmedurl';
1754 $cssClass = 'mw-magiclink-pmid';
1755 $trackingCat = 'magiclink-tracking-pmid';
1756 $id = $m[5];
1757 } else {
1758 // Should never happen
1759 throw new UnexpectedValueException( __METHOD__ . ': unrecognised match type "' .
1760 substr( $m[0], 0, 20 ) . '"' );
1761 }
1762 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1763 $this->addTrackingCategory( $trackingCat );
1764 return $this->getLinkRenderer()->makeExternalLink(
1765 $url,
1766 "{$keyword} {$id}",
1767 $this->getTitle(),
1768 $cssClass,
1769 []
1770 );
1771 } elseif ( isset( $m[6] ) && $m[6] !== ''
1772 && $this->mOptions->getMagicISBNLinks()
1773 ) {
1774 # ISBN
1775 $isbn = $m[6];
1776 $space = self::SPACE_NOT_NL; # non-newline space
1777 $isbn = preg_replace( "/$space/", ' ', $isbn );
1778 $num = strtr( $isbn, [
1779 '-' => '',
1780 ' ' => '',
1781 'x' => 'X',
1782 ] );
1783 $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1784 return $this->getLinkRenderer()->makeKnownLink(
1785 SpecialPage::getTitleFor( 'Booksources', $num ),
1786 "ISBN $isbn",
1787 [
1788 'class' => 'internal mw-magiclink-isbn',
1789 'title' => false // suppress title attribute
1790 ]
1791 );
1792 } else {
1793 return $m[0];
1794 }
1795 }
1796
1806 private function makeFreeExternalLink( string $url, int $numPostProto ): string {
1807 $trail = '';
1808
1809 # The characters '<' and '>' (which were escaped by
1810 # internalRemoveHtmlTags()) should not be included in
1811 # URLs, per RFC 2396.
1812 # Make &nbsp; terminate a URL as well (bug T84937)
1813 $m2 = [];
1814 if ( preg_match(
1815 '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1816 $url,
1817 $m2,
1818 PREG_OFFSET_CAPTURE
1819 ) ) {
1820 $trail = substr( $url, $m2[0][1] ) . $trail;
1821 $url = substr( $url, 0, $m2[0][1] );
1822 }
1823
1824 # Move trailing punctuation to $trail
1825 $sep = ',;\.:!?';
1826 # If there is no left bracket, then consider right brackets fair game too
1827 if ( !str_contains( $url, '(' ) ) {
1828 $sep .= ')';
1829 }
1830
1831 $urlRev = strrev( $url );
1832 $numSepChars = strspn( $urlRev, $sep );
1833 # Don't break a trailing HTML entity by moving the ; into $trail
1834 # This is in hot code, so use substr_compare to avoid having to
1835 # create a new string object for the comparison
1836 if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1837 # more optimization: instead of running preg_match with a $
1838 # anchor, which can be slow, do the match on the reversed
1839 # string starting at the desired offset.
1840 # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1841 if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1842 $numSepChars--;
1843 }
1844 }
1845 if ( $numSepChars ) {
1846 $trail = substr( $url, -$numSepChars ) . $trail;
1847 $url = substr( $url, 0, -$numSepChars );
1848 }
1849
1850 # Verify that we still have a real URL after trail removal, and
1851 # not just lone protocol
1852 if ( strlen( $trail ) >= $numPostProto ) {
1853 return $url . $trail;
1854 }
1855
1856 $url = Sanitizer::cleanUrl( $url );
1857
1858 # Is this an external image?
1859 $text = $this->maybeMakeExternalImage( $url );
1860 if ( $text === false ) {
1861 # Not an image, make a link
1862 $text = $this->getLinkRenderer()->makeExternalLink(
1863 $url,
1864 $this->getTargetLanguageConverter()->markNoConversion( $url ),
1865 $this->getTitle(),
1866 'free',
1867 $this->getExternalLinkAttribs( $url )
1868 );
1869 # Register it in the output object...
1870 $this->mOutput->addExternalLink( $url );
1871 }
1872 return $text . $trail;
1873 }
1874
1881 private function handleHeadings( string $text ): string {
1882 for ( $i = 6; $i >= 1; --$i ) {
1883 $h = str_repeat( '=', $i );
1884 // Trim non-newline whitespace from headings
1885 // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1886 $text = preg_replace(
1887 "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m",
1888 "<h$i data-mw-wikitext>\\1</h$i>",
1889 $text
1890 );
1891 }
1892 // T428677: temporarily mark parser output generated with the
1893 // data-mw-wikitext marking of wikitext headings.
1894 $this->getOutput()->setExtensionData( 'core:new-heading-attr', true );
1895 return $text;
1896 }
1897
1905 private function handleAllQuotes( string $text ): string {
1906 $outtext = '';
1907 $lines = StringUtils::explode( "\n", $text );
1908 foreach ( $lines as $line ) {
1909 $outtext .= $this->doQuotes( $line ) . "\n";
1910 }
1911 $outtext = substr( $outtext, 0, -1 );
1912 return $outtext;
1913 }
1914
1923 public function doQuotes( $text ): string {
1924 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1925 $countarr = count( $arr );
1926 if ( $countarr == 1 ) {
1927 return $text;
1928 }
1929
1930 // First, do some preliminary work. This may shift some apostrophes from
1931 // being mark-up to being text. It also counts the number of occurrences
1932 // of bold and italics mark-ups.
1933 $numbold = 0;
1934 $numitalics = 0;
1935 for ( $i = 1; $i < $countarr; $i += 2 ) {
1936 $thislen = strlen( $arr[$i] );
1937 // If there are ever four apostrophes, assume the first is supposed to
1938 // be text, and the remaining three constitute mark-up for bold text.
1939 // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1940 if ( $thislen == 4 ) {
1941 $arr[$i - 1] .= "'";
1942 $arr[$i] = "'''";
1943 $thislen = 3;
1944 } elseif ( $thislen > 5 ) {
1945 // If there are more than 5 apostrophes in a row, assume they're all
1946 // text except for the last 5.
1947 // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1948 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1949 $arr[$i] = "'''''";
1950 $thislen = 5;
1951 }
1952 // Count the number of occurrences of bold and italics mark-ups.
1953 if ( $thislen == 2 ) {
1954 $numitalics++;
1955 } elseif ( $thislen == 3 ) {
1956 $numbold++;
1957 } elseif ( $thislen == 5 ) {
1958 $numitalics++;
1959 $numbold++;
1960 }
1961 }
1962
1963 // If there is an odd number of both bold and italics, it is likely
1964 // that one of the bold ones was meant to be an apostrophe followed
1965 // by italics. Which one we cannot know for certain, but it is more
1966 // likely to be one that has a single-letter word before it.
1967 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1968 $firstsingleletterword = -1;
1969 $firstmultiletterword = -1;
1970 $firstspace = -1;
1971 for ( $i = 1; $i < $countarr; $i += 2 ) {
1972 if ( strlen( $arr[$i] ) == 3 ) {
1973 $x1 = substr( $arr[$i - 1], -1 );
1974 $x2 = substr( $arr[$i - 1], -2, 1 );
1975 if ( $x1 === ' ' ) {
1976 if ( $firstspace == -1 ) {
1977 $firstspace = $i;
1978 }
1979 } elseif ( $x2 === ' ' ) {
1980 $firstsingleletterword = $i;
1981 // if $firstsingleletterword is set, we don't
1982 // look at the other options, so we can bail early.
1983 break;
1984 } elseif ( $firstmultiletterword == -1 ) {
1985 $firstmultiletterword = $i;
1986 }
1987 }
1988 }
1989
1990 // If there is a single-letter word, use it!
1991 if ( $firstsingleletterword > -1 ) {
1992 $arr[$firstsingleletterword] = "''";
1993 $arr[$firstsingleletterword - 1] .= "'";
1994 } elseif ( $firstmultiletterword > -1 ) {
1995 // If not, but there's a multi-letter word, use that one.
1996 $arr[$firstmultiletterword] = "''";
1997 $arr[$firstmultiletterword - 1] .= "'";
1998 } elseif ( $firstspace > -1 ) {
1999 // ... otherwise use the first one that has neither.
2000 // (notice that it is possible for all three to be -1 if, for example,
2001 // there is only one pentuple-apostrophe in the line)
2002 $arr[$firstspace] = "''";
2003 $arr[$firstspace - 1] .= "'";
2004 }
2005 }
2006
2007 // Now let's actually convert our apostrophic mush to HTML!
2008 $output = '';
2009 $buffer = '';
2010 $state = '';
2011 $i = 0;
2012 foreach ( $arr as $r ) {
2013 if ( ( $i % 2 ) == 0 ) {
2014 if ( $state === 'both' ) {
2015 $buffer .= $r;
2016 } else {
2017 $output .= $r;
2018 }
2019 } else {
2020 $thislen = strlen( $r );
2021 if ( $thislen == 2 ) {
2022 // two quotes - open or close italics
2023 if ( $state === 'i' ) {
2024 $output .= '</i>';
2025 $state = '';
2026 } elseif ( $state === 'bi' ) {
2027 $output .= '</i>';
2028 $state = 'b';
2029 } elseif ( $state === 'ib' ) {
2030 $output .= '</b></i><b>';
2031 $state = 'b';
2032 } elseif ( $state === 'both' ) {
2033 $output .= '<b><i>' . $buffer . '</i>';
2034 $state = 'b';
2035 } else { // $state can be 'b' or ''
2036 $output .= '<i>';
2037 $state .= 'i';
2038 }
2039 } elseif ( $thislen == 3 ) {
2040 // three quotes - open or close bold
2041 if ( $state === 'b' ) {
2042 $output .= '</b>';
2043 $state = '';
2044 } elseif ( $state === 'bi' ) {
2045 $output .= '</i></b><i>';
2046 $state = 'i';
2047 } elseif ( $state === 'ib' ) {
2048 $output .= '</b>';
2049 $state = 'i';
2050 } elseif ( $state === 'both' ) {
2051 $output .= '<i><b>' . $buffer . '</b>';
2052 $state = 'i';
2053 } else { // $state can be 'i' or ''
2054 $output .= '<b>';
2055 $state .= 'b';
2056 }
2057 } elseif ( $thislen == 5 ) {
2058 // five quotes - open or close both separately
2059 if ( $state === 'b' ) {
2060 $output .= '</b><i>';
2061 $state = 'i';
2062 } elseif ( $state === 'i' ) {
2063 $output .= '</i><b>';
2064 $state = 'b';
2065 } elseif ( $state === 'bi' ) {
2066 $output .= '</i></b>';
2067 $state = '';
2068 } elseif ( $state === 'ib' ) {
2069 $output .= '</b></i>';
2070 $state = '';
2071 } elseif ( $state === 'both' ) {
2072 $output .= '<i><b>' . $buffer . '</b></i>';
2073 $state = '';
2074 } else { // ($state == '')
2075 $buffer = '';
2076 $state = 'both';
2077 }
2078 }
2079 }
2080 $i++;
2081 }
2082 // Now close all remaining tags. Notice that the order is important.
2083 if ( $state === 'b' || $state === 'ib' ) {
2084 $output .= '</b>';
2085 }
2086 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2087 $output .= '</i>';
2088 }
2089 if ( $state === 'bi' ) {
2090 $output .= '</b>';
2091 }
2092 // There might be lonely ''''', so make sure we have a buffer
2093 if ( $state === 'both' && $buffer ) {
2094 $output .= '<b><i>' . $buffer . '</i></b>';
2095 }
2096 return $output;
2097 }
2098
2108 private function handleExternalLinks( string $text ): string {
2109 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2110 if ( $bits === false ) {
2111 // T321234: Don't try to fix old revisions with broken UTF-8, just return $text as is
2112 return $text;
2113 }
2114 $s = array_shift( $bits );
2115
2116 $i = 0;
2117 while ( $i < count( $bits ) ) {
2118 $url = $bits[$i++];
2119 $i++; // protocol
2120 $text = $bits[$i++];
2121 $trail = $bits[$i++];
2122
2123 # The characters '<' and '>' (which were escaped by
2124 # internalRemoveHtmlTags()) should not be included in
2125 # URLs, per RFC 2396.
2126 $m2 = [];
2127 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2128 $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2129 $url = substr( $url, 0, $m2[0][1] );
2130 }
2131
2132 # If the link text is an image URL, replace it with an <img> tag
2133 # This happened by accident in the original parser, but some people used it extensively
2134 $img = $this->maybeMakeExternalImage( $text );
2135 if ( $img !== false ) {
2136 $text = $img;
2137 }
2138
2139 $dtrail = '';
2140
2141 # Set linktype for CSS
2142 $linktype = 'text';
2143
2144 # No link text, e.g. [http://domain.tld/some.link]
2145 if ( $text == '' ) {
2146 # Autonumber
2147 $langObj = $this->getTargetLanguage();
2148 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2149 $linktype = 'autonumber';
2150 } else {
2151 # Have link text, e.g. [http://domain.tld/some.link text]s
2152 # Check for trail
2153 [ $dtrail, $trail ] = Linker::splitTrail( $trail );
2154 }
2155
2156 // Excluding protocol-relative URLs may avoid many false positives.
2157 if ( preg_match( '/^(?:' . $this->urlUtils->validAbsoluteProtocols() . ')/', $text ) ) {
2158 $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2159 }
2160
2161 $url = Sanitizer::cleanUrl( $url );
2162
2163 # Use the encoded URL
2164 # This means that users can paste URLs directly into the text
2165 # Funny characters like ö aren't valid in URLs anyway
2166 # This was changed in August 2004
2167 $s .= $this->getLinkRenderer()->makeExternalLink(
2168 $url,
2169 new HtmlArmor( $text ),
2170 $this->getTitle(),
2171 $linktype,
2172 $this->getExternalLinkAttribs( $url )
2173 ) . $dtrail . $trail;
2174
2175 # Register link in the output object.
2176 $this->mOutput->addExternalLink( $url );
2177 }
2178
2179 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive from array_shift
2180 return $s;
2181 }
2182
2194 public static function getExternalLinkRel( $url = false, $title = null ): ?string {
2195 wfDeprecated( __METHOD__, '1.47' );
2196 return MediaWikiServices::getInstance()->getLinkRenderer()
2197 ->getExternalLinkRel( $url, $title );
2198 }
2199
2211 public function getExternalLinkAttribs( $url ) {
2212 $attribs = [];
2213 $rel = $this->getLinkRenderer()->getExternalLinkRel( $url, $this->getTitle() ) ?? '';
2214
2215 $target = $this->mOptions->getExternalLinkTarget();
2216 if ( $target ) {
2217 $attribs['target'] = $target;
2218 // T133507/T427561: we used to set additional 'rel' attributes
2219 // here, but no longer need to do so.
2220 }
2221 if ( $rel !== '' ) {
2222 $attribs['rel'] = $rel;
2223 }
2224 return $attribs;
2225 }
2226
2237 public static function normalizeLinkUrl( $url ): string {
2238 # Test for RFC 3986 IPv6 syntax
2239 $scheme = '[a-z][a-z0-9+.-]*:';
2240 $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2241 $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2242 if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2243 IPUtils::isValid( rawurldecode( $m[1] ) )
2244 ) {
2245 $isIPv6 = rawurldecode( $m[1] );
2246 } else {
2247 $isIPv6 = false;
2248 }
2249
2250 # Make sure unsafe characters are encoded
2251 $url = preg_replace_callback(
2252 '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]+/',
2253 static fn ( $m ) => rawurlencode( $m[0] ),
2254 $url
2255 );
2256
2257 $ret = '';
2258 $end = strlen( $url );
2259
2260 # Fragment part - 'fragment'
2261 $start = strpos( $url, '#' );
2262 if ( $start !== false && $start < $end ) {
2263 $ret = self::normalizeUrlComponent(
2264 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2265 $end = $start;
2266 }
2267
2268 # Query part - 'query' minus &=+;
2269 $start = strpos( $url, '?' );
2270 if ( $start !== false && $start < $end ) {
2271 $ret = self::normalizeUrlComponent(
2272 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2273 $end = $start;
2274 }
2275
2276 # Path part - 'pchar', remove dot segments
2277 # (find first '/' after the optional '//' after the scheme)
2278 $start = strpos( $url, '//' );
2279 $start = strpos( $url, '/', $start === false ? 0 : $start + 2 );
2280 if ( $start !== false && $start < $end ) {
2281 $ret = UrlUtils::removeDotSegments( self::normalizeUrlComponent(
2282 substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}/?' ) ) . $ret;
2283 $end = $start;
2284 }
2285
2286 # Scheme and host part - 'pchar'
2287 # (we assume no userinfo or encoded colons in the host)
2288 $ret = self::normalizeUrlComponent(
2289 substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2290
2291 # Fix IPv6 syntax
2292 if ( $isIPv6 !== false ) {
2293 $ipv6Host = "%5B({$isIPv6})%5D";
2294 $ret = preg_replace(
2295 "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2296 "$1[$2]",
2297 $ret
2298 );
2299 }
2300
2301 return $ret;
2302 }
2303
2304 private static function normalizeUrlComponent( string $component, string $unsafe ): string {
2305 $callback = static function ( $matches ) use ( $unsafe ) {
2306 $char = urldecode( $matches[0] );
2307 $ord = ord( $char );
2308 if ( $ord > 32 && $ord < 127 && !str_contains( $unsafe, $char ) ) {
2309 # Unescape it
2310 return $char;
2311 } else {
2312 # Leave it escaped, but use uppercase for a-f
2313 return strtoupper( $matches[0] );
2314 }
2315 };
2316 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2317 }
2318
2327 private function maybeMakeExternalImage( string $url ): string|false {
2328 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2329 $imagesexception = (bool)$imagesfrom;
2330 $text = false;
2331 # $imagesfrom could be either a single string or an array of strings, parse out the latter
2332 if ( $imagesexception && is_array( $imagesfrom ) ) {
2333 $imagematch = false;
2334 foreach ( $imagesfrom as $match ) {
2335 if ( str_starts_with( $url, $match ) ) {
2336 $imagematch = true;
2337 break;
2338 }
2339 }
2340 } elseif ( $imagesexception ) {
2341 $imagematch = str_starts_with( $url, $imagesfrom );
2342 } else {
2343 $imagematch = false;
2344 }
2345
2346 if ( $this->mOptions->getAllowExternalImages()
2347 || ( $imagesexception && $imagematch )
2348 ) {
2349 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2350 # Image found
2351 $text = Linker::makeExternalImage( $url );
2352 }
2353 }
2354 if ( !$text && $this->mOptions->getEnableImageWhitelist()
2355 && preg_match( self::EXT_IMAGE_REGEX, $url )
2356 ) {
2357 $whitelist = explode(
2358 "\n",
2359 wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2360 );
2361
2362 foreach ( $whitelist as $entry ) {
2363 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2364 if ( $entry === '' || str_starts_with( $entry, '#' ) ) {
2365 continue;
2366 }
2367 // @phan-suppress-next-line SecurityCheck-ReDoS preg_quote is not wanted here
2368 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2369 # Image matches a whitelist entry
2370 $text = Linker::makeExternalImage( $url );
2371 break;
2372 }
2373 }
2374 }
2375 return $text;
2376 }
2377
2385 private function handleInternalLinks( string $text ): string {
2386 $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2387 return $text;
2388 }
2389
2395 private function handleInternalLinks2( &$s ) {
2396 static $tc = false, $e1, $e1_img;
2397 # the % is needed to support urlencoded titles as well
2398 if ( !$tc ) {
2399 $tc = Title::legalChars() . '#%';
2400 # Match a link having the form [[namespace:link|alternate]]trail
2401 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2402 # Match cases where there is no "]]", which might still be images
2403 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2404 }
2405
2406 $holders = new LinkHolderArray(
2407 $this,
2408 $this->getContentLanguageConverter(),
2409 $this->getHookContainer() );
2410
2411 # split the entire text string on occurrences of [[
2412 $a = StringUtils::explode( '[[', ' ' . $s );
2413 # get the first element (all text up to first [[), and remove the space we added
2414 $s = $a->current();
2415 $a->next();
2416 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2417 $s = substr( $s, 1 );
2418
2419 $nottalk = !$this->getTitle()->isTalkPage();
2420
2421 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2422 $e2 = null;
2423 if ( $useLinkPrefixExtension ) {
2424 # Match the end of a line for a word that's not followed by whitespace,
2425 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2426 $charset = $this->contLang->linkPrefixCharset();
2427 $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2428 $m = [];
2429 if ( preg_match( $e2, $s, $m ) ) {
2430 $first_prefix = $m[2];
2431 } else {
2432 $first_prefix = false;
2433 }
2434 $prefix = false;
2435 } else {
2436 $first_prefix = false;
2437 $prefix = '';
2438 }
2439
2440 # Some namespaces don't allow subpages
2441 $useSubpages = $this->nsInfo->hasSubpages(
2442 $this->getTitle()->getNamespace()
2443 );
2444
2445 # Loop for each link
2446 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2447 # Check for excessive memory usage
2448 if ( $holders->isBig() ) {
2449 # Too big
2450 # Do the existence check, replace the link holders and clear the array
2451 $holders->replace( $s );
2452 $holders->clear();
2453 }
2454
2455 if ( $useLinkPrefixExtension ) {
2456 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal $e2 is set under this condition
2457 if ( preg_match( $e2, $s, $m ) ) {
2458 [ , $s, $prefix ] = $m;
2459 } else {
2460 $prefix = '';
2461 }
2462 # first link
2463 if ( $first_prefix ) {
2464 $prefix = $first_prefix;
2465 $first_prefix = false;
2466 }
2467 }
2468
2469 $might_be_img = false;
2470
2471 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2472 $text = $m[2];
2473 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2474 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2475 # the real problem is with the $e1 regex
2476 # See T3300.
2477 # Still some problems for cases where the ] is meant to be outside punctuation,
2478 # and no image is in sight. See T4095.
2479 if ( $text !== ''
2480 && substr( $m[3], 0, 1 ) === ']'
2481 && strpos( $text, '[' ) !== false
2482 ) {
2483 $text .= ']'; # so that handleExternalLinks($text) works later
2484 $m[3] = substr( $m[3], 1 );
2485 }
2486 # fix up urlencoded title texts
2487 if ( str_contains( $m[1], '%' ) ) {
2488 # Should anchors '#' also be rejected?
2489 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2490 }
2491 $trail = $m[3];
2492 } elseif ( preg_match( $e1_img, $line, $m ) ) {
2493 # Invalid, but might be an image with a link in its caption
2494 $might_be_img = true;
2495 $text = $m[2];
2496 if ( str_contains( $m[1], '%' ) ) {
2497 $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2498 }
2499 $trail = "";
2500 } else { # Invalid form; output directly
2501 $s .= $prefix . '[[' . $line;
2502 continue;
2503 }
2504
2505 $origLink = ltrim( $m[1], ' ' );
2506
2507 # Don't allow internal links to pages containing
2508 # PROTO: where PROTO is a valid URL protocol; these
2509 # should be external links.
2510 if ( preg_match( '/^(?i:' . $this->urlUtils->validProtocols() . ')/', $origLink ) ) {
2511 $s .= $prefix . '[[' . $line;
2512 continue;
2513 }
2514
2515 # Make subpage if necessary
2516 if ( $useSubpages ) {
2517 $link = Linker::normalizeSubpageLink(
2518 $this->getTitle(), $origLink, $text
2519 );
2520 } else {
2521 $link = $origLink;
2522 }
2523
2524 // \x7f isn't a default legal title char, so most likely strip
2525 // markers will force us into the "invalid form" path above. But,
2526 // just in case, let's assert that xmlish tags aren't valid in
2527 // the title position.
2528 $unstrip = $this->mStripState->killMarkers( $link );
2529 $noMarkers = ( $unstrip === $link );
2530
2531 $nt = $noMarkers ? Title::newFromText( $link ) : null;
2532 if ( $nt === null ) {
2533 $s .= $prefix . '[[' . $line;
2534 continue;
2535 }
2536
2537 $ns = $nt->getNamespace();
2538 $iw = $nt->getInterwiki();
2539
2540 $noforce = !str_starts_with( $origLink, ':' );
2541
2542 if ( $might_be_img ) { # if this is actually an invalid link
2543 if ( $ns === NS_FILE && $noforce ) { # but might be an image
2544 $found = false;
2545 while ( true ) {
2546 # look at the next 'line' to see if we can close it there
2547 $a->next();
2548 $next_line = $a->current();
2549 if ( $next_line === false || $next_line === null ) {
2550 break;
2551 }
2552 $m = explode( ']]', $next_line, 3 );
2553 if ( count( $m ) == 3 ) {
2554 # the first ]] closes the inner link, the second the image
2555 $found = true;
2556 $text .= "[[{$m[0]}]]{$m[1]}";
2557 $trail = $m[2];
2558 break;
2559 } elseif ( count( $m ) == 2 ) {
2560 # if there's exactly one ]] that's fine, we'll keep looking
2561 $text .= "[[{$m[0]}]]{$m[1]}";
2562 } else {
2563 # if $next_line is invalid too, we need look no further
2564 $text .= '[[' . $next_line;
2565 break;
2566 }
2567 }
2568 if ( !$found ) {
2569 # we couldn't find the end of this imageLink, so output it raw
2570 # but don't ignore what might be perfectly normal links in the text we've examined
2571 $holders->merge( $this->handleInternalLinks2( $text ) );
2572 $s .= "{$prefix}[[$link|$text";
2573 # note: no $trail, because without an end, there *is* no trail
2574 continue;
2575 }
2576 } else { # it's not an image, so output it raw
2577 $s .= "{$prefix}[[$link|$text";
2578 # note: no $trail, because without an end, there *is* no trail
2579 continue;
2580 }
2581 }
2582
2583 $wasblank = ( $text == '' );
2584 if ( $wasblank ) {
2585 $text = $link;
2586 if ( !$noforce ) {
2587 # Strip off leading ':'
2588 $text = substr( $text, 1 );
2589 }
2590 } else {
2591 # T6598 madness. Handle the quotes only if they come from the alternate part
2592 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2593 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2594 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2595 $text = $this->doQuotes( $text );
2596 }
2597
2598 # Link not escaped by : , create the various objects
2599 if ( $noforce && !$nt->wasLocalInterwiki() ) {
2600 # Interwikis
2601 if (
2602 $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2603 $this->languageNameUtils->getLanguageName(
2604 $iw,
2605 LanguageNameUtils::AUTONYMS,
2606 LanguageNameUtils::DEFINED
2607 )
2608 || in_array( $iw, $this->svcOptions->get( MainConfigNames::ExtraInterlanguageLinkPrefixes ) )
2609 )
2610 ) {
2611 # T26502: duplicates are resolved in ParserOutput
2612 $this->mOutput->addLanguageLink( $nt );
2613
2618 $s = preg_replace( '/\n\s*$/', '', $s . $prefix ) . $trail;
2619 continue;
2620 }
2621
2622 if ( $ns === NS_FILE ) {
2623 if ( $wasblank ) {
2624 # if no parameters were passed, $text
2625 # becomes something like "File:Foo.png",
2626 # which we don't want to pass on to the
2627 # image generator
2628 $text = '';
2629 } else {
2630 # recursively parse links inside the image caption
2631 # actually, this will parse them in any other parameters, too,
2632 # but it might be hard to fix that, and it doesn't matter ATM
2633 $text = $this->handleExternalLinks( $text );
2634 $holders->merge( $this->handleInternalLinks2( $text ) );
2635 }
2636 # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2637 $s .= $prefix . $this->armorLinks(
2638 $this->makeImageInternal( $nt, $text, $holders ) ) . $trail;
2639 continue;
2640 } elseif ( $ns === NS_CATEGORY ) {
2641 # Strip newlines from the left hand context of Category
2642 # links.
2643 # See T2087, T87753, T174639, T359886
2644 $s = preg_replace( '/\n\s*$/', '', $s . $prefix ) . $trail;
2645
2646 $sortkey = ''; // filled in by CategoryLinksTable
2647 if ( !$wasblank ) {
2648 $sortkey = $text;
2649 }
2650 $this->mOutput->addCategory( $nt, $sortkey );
2651
2652 continue;
2653 }
2654 }
2655
2656 # Self-link checking. For some languages, variants of the title are checked in
2657 # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2658 # for linking to a different variant.
2659 if ( $ns !== NS_SPECIAL && $nt->equals( $this->getTitle() ) ) {
2660 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail, '',
2661 Sanitizer::escapeIdForLink( $nt->getFragment() ) );
2662 continue;
2663 }
2664
2665 # NS_MEDIA is a pseudo-namespace for linking directly to a file
2666 # @todo FIXME: Should do batch file existence checks, see comment below
2667 if ( $ns === NS_MEDIA ) {
2668 # Give extensions a chance to select the file revision for us
2669 $options = [];
2670 $descQuery = false;
2671 $this->hookRunner->onBeforeParserFetchFileAndTitle(
2672 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
2673 $this, $nt, $options, $descQuery
2674 );
2675 # Fetch and register the file (file title may be different via hooks)
2676 [ $file, $nt ] = $this->fetchFileAndTitle( $nt, $options );
2677 # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2678 $s .= $prefix . $this->armorLinks(
2679 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2680 continue;
2681 }
2682
2683 # Some titles, such as valid special pages or files in foreign repos, should
2684 # be shown as bluelinks even though they're not included in the page table
2685 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2686 # batch file existence checks for NS_FILE and NS_MEDIA
2687 if ( $iw == '' && $this->linkAlwaysKnownLookup->isAlwaysKnown( $nt ) ) {
2688 $this->mOutput->addLink( $nt );
2689 $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2690 } else {
2691 # Links will be added to the output link list after checking
2692 $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2693 }
2694 }
2695 return $holders;
2696 }
2697
2711 private function makeKnownLinkHolder(
2712 LinkTarget $nt, string $text = '',
2713 string $trail = '', string $prefix = ''
2714 ): string {
2715 [ $inside, $trail ] = Linker::splitTrail( $trail );
2716
2717 if ( $text == '' ) {
2718 $text = htmlspecialchars( $this->titleFormatter->getPrefixedText( $nt ) );
2719 }
2720
2721 $link = $this->getLinkRenderer()->makeKnownLink(
2722 $nt, new HtmlArmor( "$prefix$text$inside" )
2723 );
2724
2725 return $this->armorLinks( $link ) . $trail;
2726 }
2727
2738 private function armorLinks( string $text ): string {
2739 return preg_replace( '/\b((?i)' . $this->urlUtils->validProtocols() . ')/',
2740 self::MARKER_PREFIX . "NOPARSE$1", $text );
2741 }
2742
2751 private function expandMagicVariable( string $index, $frame = false ): string {
2756 if ( isset( $this->mVarCache[$index] ) ) {
2757 return $this->mVarCache[$index];
2758 }
2759
2760 $value = CoreMagicVariables::expand(
2761 $this, $index, new MWTimestamp( $this->getParseTime() ),
2762 $this->svcOptions, $this->logger, $frame
2763 );
2764
2765 if ( $value === null ) {
2766 // Not a defined core magic word
2767 // Don't give this hook unrestricted access to mVarCache
2768 $fakeCache = [];
2769 $this->hookRunner->onParserGetVariableValueSwitch(
2770 // @phan-suppress-next-line PhanTypeMismatchArgument $value is passed as null but returned as string
2771 $this, $fakeCache, $index, $value, $frame
2772 );
2773 // Cache the value returned by the hook by falling through here.
2774 if ( $value === null ) {
2775 // T419880: If a magic word ID was registered with the
2776 // GetMagicVariableIDs hook, the registering extension should
2777 // *not* let that $value remain `null` when the
2778 // ParserGetVariableValueSwitch hook is invoked.
2780 __METHOD__ . " called hook for $index and got null",
2781 "1.46"
2782 );
2783 $value = '';
2784 }
2785 }
2786
2787 $this->mVarCache[$index] = $value;
2788
2789 return $value;
2790 }
2791
2796 private function initializeVariables() {
2797 $variableIDs = $this->magicWordFactory->getVariableIDs();
2798
2799 $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2800 $this->mSubstWords = $this->magicWordFactory->getSubstArray();
2801 }
2802
2821 public function preprocessToDom( $text, $flags = 0 ) {
2822 return $this->getPreprocessor()->preprocessToObj( $text, $flags );
2823 }
2824
2851 public function replaceVariables(
2852 $text, $frame = false, $argsOnly = false, array $options = []
2853 ): string {
2854 # Is there any text? Also, Prevent too big inclusions!
2855 $textSize = strlen( $text );
2856 if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2857 return $text;
2858 }
2859
2860 if ( $frame === false ) {
2861 $frame = $this->getPreprocessor()->newFrame();
2862 } elseif ( !( $frame instanceof PPFrame ) ) {
2864 __METHOD__ . " called using plain parameters instead of " .
2865 "a PPFrame instance. Creating custom frame.",
2866 '1.43'
2867 );
2868 $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2869 }
2870
2871 $ppFlags = 0;
2872 if ( $options['parsoidTopLevelCall'] ?? false ) {
2873 $ppFlags |= Preprocessor::START_IN_SOL_STATE;
2874 }
2875 $dom = $this->preprocessToDom( $text, $ppFlags );
2876 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2877 return $frame->expand( $dom, $flags );
2878 }
2879
2881 public function setUseParsoidFragments( bool $val ) {
2882 $this->useParsoidFragments = $val;
2883 }
2884
2886 public function useParsoidFragments(): bool {
2887 return $this->useParsoidFragments;
2888 }
2889
2917 public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2918 # does no harm if $current and $max are present but are unnecessary for the message
2919 # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2920 # only during preview, and that would split the parser cache unnecessarily.
2921 $this->mOutput->addWarningMsg(
2922 "$limitationType-warning",
2923 Message::numParam( $current ),
2924 Message::numParam( $max )
2925 );
2926 $this->addTrackingCategory( "$limitationType-category" );
2927 }
2928
2942 public function braceSubstitution( array $piece, PPFrame $frame ): string|array {
2943 // Flags
2944
2945 // $text has been filled
2946 $found = false;
2947 $text = '';
2948 // wiki markup in $text should be escaped
2949 $nowiki = false;
2950 // $text is HTML, armour it against most wikitext transformation
2951 // (it still participates in doBlockLevels, language conversion,
2952 // and the other steps at the start of ::internalParseHalfParsed)
2953 $isHTML = false;
2954 // $text is raw HTML, armour it against all wikitext transformation
2955 $isRawHTML = false;
2956 // Force interwiki transclusion to be done in raw mode not rendered
2957 $forceRawInterwiki = false;
2958 // $text is a DOM node needing expansion in a child frame
2959 $isChildObj = false;
2960 // $text is a DOM node needing expansion in the current frame
2961 $isLocalObj = false;
2962
2963 # Title object, where $text came from
2964 $title = false;
2965
2966 # $part1 is the bit before the first |, and must contain only title characters.
2967 # Various prefixes will be stripped from it later.
2968 $titleWithSpaces = $frame->expand( $piece['title'] );
2969 $part1 = trim( $titleWithSpaces );
2970 $titleText = false;
2971
2972 # Original title text preserved for various purposes
2973 $originalTitle = $part1;
2974
2975 # $args is a list of argument nodes, starting from index 0, not including $part1
2976 $args = $piece['parts'];
2977
2978 $profileSection = null; // profile templates
2979
2980 $sawDeprecatedTemplateEquals = false; // T91154
2981
2982 $isParsoid = $this->mOptions->getUseParsoid();
2983
2984 # SUBST
2985 // @phan-suppress-next-line PhanImpossibleCondition
2986 if ( !$found ) {
2987 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2988 $part1 = trim( $part1 );
2989
2990 # Possibilities for substMatch: "subst", "safesubst" or FALSE
2991 # Decide whether to expand template or keep wikitext as-is.
2992 if ( $this->ot['wiki'] ) {
2993 if ( $substMatch === false ) {
2994 $literal = true; # literal when in PST with no prefix
2995 } else {
2996 $literal = false; # expand when in PST with subst: or safesubst:
2997 }
2998 } else {
2999 if ( $substMatch == 'subst' ) {
3000 $literal = true; # literal when not in PST with plain subst:
3001 } else {
3002 $literal = false; # expand when not in PST with safesubst: or no prefix
3003 }
3004 }
3005 if ( $literal ) {
3006 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3007 $isLocalObj = true;
3008 $found = true;
3009 }
3010 }
3011
3012 # Variables
3013 if ( !$found && $args->getLength() == 0 ) {
3014 $id = $this->mVariables->matchStartToEnd( $part1 );
3015 if ( $id !== false ) {
3016 if ( str_contains( $part1, ':' ) ) {
3018 'Registering a magic variable with a name including a colon',
3019 '1.39', false, false
3020 );
3021 }
3022 $text = $this->expandMagicVariable( $id, $frame );
3023 $found = true;
3024 }
3025 }
3026
3027 # MSG, MSGNW and RAW
3028 if ( !$found ) {
3029 # Check for MSGNW:
3030 $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3031 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3032 $nowiki = true;
3033 } else {
3034 # Remove obsolete MSG:
3035 $mwMsg = $this->magicWordFactory->get( 'msg' );
3036 $mwMsg->matchStartAndRemove( $part1 );
3037 }
3038
3039 # Check for RAW:
3040 $mwRaw = $this->magicWordFactory->get( 'raw' );
3041 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3042 $forceRawInterwiki = true;
3043 }
3044 }
3045
3046 # Parser functions
3047 if ( !$found ) {
3048 // Allow colon or Japanese double-width colon as arg delimiter
3049 if ( preg_match( '/[::]/u', $part1, $colonMatches, PREG_OFFSET_CAPTURE ) ) {
3050 [ $colonStr, $colonPos ] = $colonMatches[0];
3051 $func = substr( $part1, 0, $colonPos );
3052 $funcArgs = [ trim( substr( $part1, $colonPos + strlen( $colonStr ) ) ) ];
3053 $argsLength = $args->getLength();
3054 for ( $i = 0; $i < $argsLength; $i++ ) {
3055 $funcArgs[] = $args->item( $i );
3056 }
3057
3058 $result = $this->callParserFunction(
3059 $frame, $func, $funcArgs, $isParsoid && $piece['lineStart']
3060 );
3061
3062 // Extract any forwarded flags
3063 if ( isset( $result['title'] ) ) {
3064 $title = $result['title'];
3065 }
3066 if ( isset( $result['found'] ) ) {
3067 $found = $result['found'];
3068 }
3069 if ( array_key_exists( 'text', $result ) ) {
3070 // a string or null
3071 $text = $result['text'];
3072 }
3073 if ( isset( $result['nowiki'] ) ) {
3074 $nowiki = $result['nowiki'];
3075 }
3076 if ( isset( $result['isHTML'] ) ) {
3077 $isHTML = $result['isHTML'];
3078 }
3079 if ( isset( $result['isRawHTML'] ) ) {
3080 $isRawHTML = $result['isRawHTML'];
3081 }
3082 if ( isset( $result['forceRawInterwiki'] ) ) {
3083 $forceRawInterwiki = $result['forceRawInterwiki'];
3084 }
3085 if ( isset( $result['isChildObj'] ) ) {
3086 $isChildObj = $result['isChildObj'];
3087 }
3088 if ( isset( $result['isLocalObj'] ) ) {
3089 $isLocalObj = $result['isLocalObj'];
3090 }
3091 }
3092 }
3093
3094 # Finish mangling title and then check for loops.
3095 # Set $title to a Title object and $titleText to the PDBK
3096 if ( !$found ) {
3097 $ns = NS_TEMPLATE;
3098 # Split the title into page and subpage
3099 $subpage = '';
3100 $relative = Linker::normalizeSubpageLink(
3101 $this->getTitle(), $part1, $subpage
3102 );
3103 if ( $part1 !== $relative ) {
3104 $part1 = $relative;
3105 $ns = $this->getTitle()->getNamespace();
3106 }
3107 $title = Title::newFromText( $part1, $ns );
3108 if ( $title ) {
3109 $titleText = $title->getPrefixedText();
3110 # Check for language variants if the template is not found
3111 if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3112 $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3113 }
3114 # Do recursion depth check
3115 $limit = $this->mOptions->getMaxTemplateDepth();
3116 if ( $frame->depth >= $limit ) {
3117 $found = true;
3118 $text = '<span class="error">'
3119 . wfMessage( 'parser-template-recursion-depth-warning' )
3120 ->numParams( $limit )->inContentLanguage()->text()
3121 . '</span>';
3122 }
3123 }
3124 }
3125
3126 # Load from database
3127 if ( !$found && $title ) {
3128 $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3129 if ( !$title->isExternal() ) {
3130 if ( $title->isSpecialPage()
3131 && $this->mOptions->getAllowSpecialInclusion()
3132 && ( $this->ot['html'] || ( $this->useParsoidFragments && $this->ot['pre'] ) )
3133 ) {
3134 $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3135 // Pass the template arguments as URL parameters.
3136 // "uselang" will have no effect since the Language object
3137 // is forced to the one defined in ParserOptions.
3138 $pageArgs = [];
3139 $argsLength = $args->getLength();
3140 for ( $i = 0; $i < $argsLength; $i++ ) {
3141 $bits = $args->item( $i )->splitArg();
3142 if ( strval( $bits['index'] ) === '' ) {
3143 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3144 $value = trim( $frame->expand( $bits['value'] ) );
3145 $pageArgs[$name] = $value;
3146 }
3147 }
3148
3149 // Create a new context to execute the special page, that is expensive
3150 if ( $this->incrementExpensiveFunctionCount() ) {
3151 $context = new RequestContext;
3152 $context->setTitle( $title );
3153 $context->setRequest( new FauxRequest( $pageArgs ) );
3154 if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3155 $context->setUser( $this->userFactory->newFromUserIdentity( $this->getUserIdentity() ) );
3156 } else {
3157 // If this page is cached, then we better not be per user.
3158 $context->setUser( User::newFromName( '127.0.0.1', false ) );
3159 }
3160 $context->setLanguage( $this->mOptions->getUserLangObj() );
3161 $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3162 if ( $ret ) {
3163 $text = $context->getOutput()->getHTML();
3164 $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3165 $found = true;
3166 $isHTML = true;
3167 if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3168 $this->mOutput->updateRuntimeAdaptiveExpiry(
3169 $specialPage->maxIncludeCacheTime()
3170 );
3171 }
3172 }
3173 }
3174 } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3175 $found = false; # access denied
3176 $this->logger->debug(
3177 __METHOD__ .
3178 ": template inclusion denied for " . $title->getPrefixedDBkey()
3179 );
3180 } else {
3181 [ $text, $title ] = $this->getTemplateDom( $title, $isParsoid && $piece['lineStart'] );
3182 if ( $text !== false ) {
3183 $found = true;
3184 $isChildObj = true;
3185 if (
3186 $title->getNamespace() === NS_TEMPLATE &&
3187 $title->getDBkey() === '=' &&
3188 $originalTitle === '='
3189 ) {
3190 // Note that we won't get here if `=` is evaluated
3191 // (in the future) as a parser function, nor if
3192 // the Template namespace is given explicitly,
3193 // ie `{{Template:=}}`. Only `{{=}}` triggers.
3194 $sawDeprecatedTemplateEquals = true; // T91154
3195 }
3196 }
3197 }
3198
3199 # If the title is valid but undisplayable, make a link to it
3200 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3201 $text = "[[:$titleText]]";
3202 $found = true;
3203 }
3204 } elseif ( $title->isTrans() ) {
3205 # Interwiki transclusion
3206 if ( $this->ot['html'] && !$forceRawInterwiki ) {
3207 $text = $this->interwikiTransclude( $title, 'render' );
3208 $isHTML = true;
3209 } else {
3210 $text = $this->interwikiTransclude( $title, 'raw' );
3211 # Preprocess it like a template
3212 $sol = ( $isParsoid && $piece['lineStart'] ) ? Preprocessor::START_IN_SOL_STATE : 0;
3213 $text = $this->preprocessToDom( $text, Preprocessor::DOM_FOR_INCLUSION | $sol );
3214 $isChildObj = true;
3215 }
3216 $found = true;
3217 }
3218
3219 # Do infinite loop check
3220 # This has to be done after redirect resolution to avoid infinite loops via redirects
3221 if ( !$frame->loopCheck( $title ) ) {
3222 $found = true;
3223 $text = '<span class="error">'
3224 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3225 . '</span>';
3226 $this->addTrackingCategory( 'template-loop-category' );
3227 $this->mOutput->addWarningMsg(
3228 'template-loop-warning',
3229 wfEscapeWikiText( $titleText )
3230 );
3231 $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3232 }
3233 }
3234
3235 # If we haven't found text to substitute by now, we're done
3236 # Recover the source wikitext and return it
3237 if ( !$found ) {
3238 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3239 if ( $profileSection ) {
3240 $this->mProfiler->scopedProfileOut( $profileSection );
3241 }
3242 return [ 'object' => $text ];
3243 }
3244
3245 # Expand DOM-style return values in a child frame
3246 if ( $isChildObj ) {
3247 # Clean up argument array
3248 $newFrame = $frame->newChild( $args, $title );
3249
3250 if ( $nowiki ) {
3251 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3252 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3253 # Expansion is eligible for the empty-frame cache
3254 $text = $newFrame->cachedExpand( $titleText, $text );
3255 } else {
3256 # Uncached expansion
3257 $text = $newFrame->expand( $text );
3258 }
3259 }
3260 if ( $isLocalObj && $nowiki ) {
3261 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3262 $isLocalObj = false;
3263 }
3264
3265 if ( $profileSection ) {
3266 $this->mProfiler->scopedProfileOut( $profileSection );
3267 }
3268 if (
3269 $sawDeprecatedTemplateEquals &&
3270 $this->mStripState->unstripBoth( $text ) !== '='
3271 ) {
3272 // T91154: {{=}} is deprecated when it doesn't expand to `=`;
3273 // use {{Template:=}} if you must.
3274 $this->addTrackingCategory( 'template-equals-category' );
3275 $this->mOutput->addWarningMsg( 'template-equals-warning' );
3276 }
3277
3278 # Replace raw HTML by a placeholder
3279 if ( $isHTML ) {
3280 // @phan-suppress-next-line SecurityCheck-XSS
3281 $text = $this->insertStripItem( $text );
3282 } elseif ( $isRawHTML ) {
3283 $marker = self::MARKER_PREFIX . "-pf-"
3284 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3285 // use 'nowiki' type to protect this from doBlockLevels,
3286 // language conversion, etc.
3287 // @phan-suppress-next-line SecurityCheck-XSS
3288 $this->mStripState->addNoWiki( $marker, $text );
3289 $text = $marker;
3290 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3291 # Escape nowiki-style return values
3292 // @phan-suppress-next-line SecurityCheck-DoubleEscaped
3293 $text = wfEscapeWikiText( $text );
3294 } elseif ( is_string( $text )
3295 && !$piece['lineStart']
3296 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3297 ) {
3298 // T2529: if the template begins with a table or block-level
3299 // element, it should be treated as beginning a new line.
3300 // This behavior is somewhat controversial.
3301 //
3302 // T382464: Parsoid sets $piece['lineStart'] at top-level when
3303 // expanding templates, so this hack is restricted to nested expansions.
3304 $text = "\n" . $text;
3305 }
3306
3307 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3308 # Error, oversize inclusion
3309 if ( $titleText !== false ) {
3310 # Make a working, properly escaped link if possible (T25588)
3311 $text = "[[:$titleText]]";
3312 } else {
3313 # This will probably not be a working link, but at least it may
3314 # provide some hint of where the problem is
3315 $originalTitle = preg_replace( '/^:/', '', $originalTitle );
3316 $text = "[[:$originalTitle]]";
3317 }
3318 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3319 . 'post-expand include size too large -->' );
3320 $this->limitationWarn( 'post-expand-template-inclusion' );
3321 }
3322
3323 if ( $isLocalObj ) {
3324 $ret = [ 'object' => $text ];
3325 } else {
3326 $ret = [ 'text' => $text ];
3327 }
3328
3329 return $ret;
3330 }
3331
3357 public function callParserFunction( PPFrame $frame, $function, array $args = [], bool $inSolState = false ) {
3358 # Case sensitive functions
3359 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3360 $function = $this->mFunctionSynonyms[1][$function];
3361 } else {
3362 # Case insensitive functions
3363 $function = $this->contLang->lc( $function );
3364 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3365 $function = $this->mFunctionSynonyms[0][$function];
3366 } else {
3367 return [ 'found' => false ];
3368 }
3369 }
3370
3371 [ $callback, $flags ] = $this->mFunctionHooks[$function];
3372
3373 $allArgs = [ $this ];
3374 if ( $flags & self::SFH_OBJECT_ARGS ) {
3375 # Convert arguments to PPNodes and collect for appending to $allArgs
3376 $funcArgs = [];
3377 foreach ( $args as $k => $v ) {
3378 if ( $v instanceof PPNode || $k === 0 ) {
3379 $funcArgs[] = $v;
3380 } else {
3381 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3382 }
3383 }
3384
3385 # Add a frame parameter, and pass the arguments as an array
3386 $allArgs[] = $frame;
3387 $allArgs[] = $funcArgs;
3388 } else {
3389 # Convert arguments to plain text and append to $allArgs
3390 foreach ( $args as $k => $v ) {
3391 if ( $v instanceof PPNode ) {
3392 $allArgs[] = trim( $frame->expand( $v ) );
3393 } elseif ( is_int( $k ) && $k >= 0 ) {
3394 $allArgs[] = trim( $v );
3395 } else {
3396 $allArgs[] = trim( "$k=$v" );
3397 }
3398 }
3399 }
3400
3401 $result = $callback( ...$allArgs );
3402
3403 # The interface for function hooks allows them to return a wikitext
3404 # string or an array containing the string and any flags. This mungs
3405 # things around to match what this method should return.
3406 if ( !is_array( $result ) ) {
3407 $result = [
3408 'found' => true,
3409 'text' => $result,
3410 ];
3411 } else {
3412 if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3413 $result['text'] = $result[0];
3414 }
3415 unset( $result[0] );
3416 $result += [
3417 'found' => true,
3418 ];
3419 }
3420
3421 $noparse = $result['noparse'] ?? true;
3422 if ( !$noparse ) {
3423 $preprocessFlags = $result['preprocessFlags'] ?? 0;
3424 if ( $inSolState ) {
3425 $preprocessFlags |= Preprocessor::START_IN_SOL_STATE;
3426 }
3427 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3428 $result['isChildObj'] = true;
3429 }
3430
3431 return $result;
3432 }
3433
3452 public function getTemplateDom( LinkTarget $title, bool $inSolState = false ) {
3453 $cacheTitle = $title;
3454 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3455
3456 if ( isset( $this->mTplRedirCache[$titleKey] ) ) {
3457 [ $ns, $dbk ] = $this->mTplRedirCache[$titleKey];
3458 $title = Title::makeTitle( $ns, $dbk );
3459 $titleKey = CacheKeyHelper::getKeyForPage( $title );
3460 }
3461
3462 // Factor in sol-state in the cache key
3463 $titleKey = "$titleKey:sol=" . ( $inSolState ? "0" : "1" );
3464 if ( isset( $this->mTplDomCache[$titleKey] ) ) {
3465 return [ $this->mTplDomCache[$titleKey], $title ];
3466 }
3467
3468 # Cache miss, go to the database
3469 // FIXME T383919: if $title is changed by this call, caching below
3470 // will be ineffective.
3471 [ $text, $title ] = $this->fetchTemplateAndTitle( $title );
3472
3473 // T299359: Verify that the real title that's actually being transcluded is includable
3474 if ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3475 return [ false, $title ];
3476 }
3477
3478 if ( $text === false ) {
3479 $this->mTplDomCache[$titleKey] = false;
3480 return [ false, $title ];
3481 }
3482
3483 $flags = Preprocessor::DOM_FOR_INCLUSION | ( $inSolState ? Preprocessor::START_IN_SOL_STATE : 0 );
3484 $dom = $this->preprocessToDom( $text, $flags );
3485 $this->mTplDomCache[$titleKey] = $dom;
3486
3487 if ( !$title->isSameLinkAs( $cacheTitle ) ) {
3488 $this->mTplRedirCache[ CacheKeyHelper::getKeyForPage( $cacheTitle ) ] =
3489 [ $title->getNamespace(), $title->getDBkey() ];
3490 }
3491
3492 return [ $dom, $title ];
3493 }
3494
3508 public function fetchCurrentRevisionRecordOfTitle( LinkTarget $link ) {
3509 $cacheKey = CacheKeyHelper::getKeyForPage( $link );
3510 if ( !$this->currentRevisionCache ) {
3511 $this->currentRevisionCache = new MapCacheLRU( 100 );
3512 }
3513 if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3514 $title = Title::newFromLinkTarget( $link ); // hook signature compat
3515 $revisionRecord =
3516 // Defaults to Parser::defaultFetchRevisionRecord()
3517 $this->mOptions->getCurrentRevisionRecordCallback()(
3518 $title,
3519 $this
3520 );
3521 if ( $revisionRecord === false ) {
3522 // Parser::defaultFetchRevisionRecord() can return false;
3523 // normalize it to null.
3524 $revisionRecord = null;
3525 }
3526 $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3527 }
3528 return $this->currentRevisionCache->get( $cacheKey );
3529 }
3530
3537 public function isCurrentRevisionOfTitleCached( LinkTarget $link ) {
3538 $key = CacheKeyHelper::getKeyForPage( $link );
3539 return (
3540 $this->currentRevisionCache &&
3541 $this->currentRevisionCache->has( $key )
3542 );
3543 }
3544
3555 public static function defaultFetchRevisionRecord(
3556 RevisionLookup $revisionLookup,
3557 LinkTarget $link,
3558 $parser
3559 ) {
3560 if ( $link instanceof PageIdentity ) {
3561 // probably a Title, just use it.
3562 $page = $link;
3563 } else {
3564 // XXX: use RevisionStore::getPageForLink()!
3565 // ...but get the info for the current revision at the same time?
3566 // Should RevisionStore::getKnownCurrentRevision accept a LinkTarget?
3567 $page = Title::newFromLinkTarget( $link );
3568 }
3569
3570 return $revisionLookup->getKnownLatestRevision( $page );
3571 }
3572
3580 public static function statelessFetchRevisionRecord( LinkTarget $link, $parser = null ) {
3581 wfDeprecated( __METHOD__, '1.47' );
3582 return self::defaultFetchRevisionRecord(
3583 MediaWikiServices::getInstance()->getRevisionLookup(),
3584 $link,
3585 $parser
3586 );
3587 }
3588
3595 public function fetchTemplateAndTitle( LinkTarget $link ) {
3596 // Use Title for compatibility with callbacks and return type
3597 $title = Title::newFromLinkTarget( $link );
3598
3599 // Defaults to Parser::defaultFetchTemplate()
3600 $templateCb = $this->mOptions->getTemplateCallback();
3601 $stuff = $templateCb( $title, $this );
3602 $revRecord = $stuff['revision-record'] ?? null;
3603
3604 $text = $stuff['text'];
3605 if ( is_string( $stuff['text'] ) ) {
3606 // We use U+007F DELETE to distinguish strip markers from regular text
3607 $text = strtr( $text, "\x7f", "?" );
3608 }
3609 $finalTitle = $stuff['finalTitle'] ?? $title;
3610 foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3611 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3612 if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3613 // Self-transclusion; final result may change based on the new page version
3614 try {
3615 $sha1 = $revRecord->getSha1();
3616 } catch ( RevisionAccessException ) {
3617 $sha1 = null;
3618 }
3619 $this->setOutputFlag( ParserOutputFlags::VARY_REVISION_SHA1, 'Self transclusion' );
3620 $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3621 }
3622 }
3623
3624 return [ $text, $finalTitle ];
3625 }
3626
3651 public static function defaultFetchTemplate(
3652 RevisionLookup $revLookup,
3653 HookRunner $hookRunner,
3654 LinkCache $linkCache,
3655 ShadowPageLoader $shadowPageLoader,
3656 $link,
3657 $parser
3658 ) {
3659 $title = Title::castFromLinkTarget( $link ); // for compatibility with return type
3660 $text = $skip = false;
3661 $content = null;
3662 $finalTitle = $title;
3663 $deps = [];
3664 $revRecord = null;
3665 $contextTitle = $parser ? $parser->getTitle() : null;
3666
3667 # Loop to fetch the article, with up to 2 redirects
3668
3669 # Note that $title (including redirect targets) could be
3670 # external; we do allow hooks a chance to redirect the
3671 # external title to a local one (which might be useful), but
3672 # are careful not to add external titles to the dependency
3673 # list. (T362221)
3674
3675 for ( $i = 0; $i < 3 && is_object( $title ); $i++ ) {
3676 # Give extensions a chance to select the revision instead
3677 $revRecord = null; # Assume no hook
3678 $origTitle = $title;
3679 $titleChanged = false;
3681 # The $title is a not a PageIdentity, as it may
3682 # contain fragments or even represent an attempt to transclude
3683 # a broken or otherwise-missing Title, which the hook may
3684 # fix up. Similarly, the $contextTitle may represent a special
3685 # page or other page which "exists" as a parsing context but
3686 # is not in the DB.
3687 $contextTitle, $title,
3688 $skip, $revRecord
3689 );
3690
3691 if ( $skip ) {
3692 $text = false;
3693 if ( !$title->isExternal() ) {
3694 $deps[] = [
3695 'title' => $title,
3696 'page_id' => $title->getArticleID(),
3697 'rev_id' => null
3698 ];
3699 }
3700 break;
3701 }
3702 # Get the revision
3703 if ( !$revRecord ) {
3704 if ( $parser ) {
3705 $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3706 } else {
3707 $revRecord = $revLookup->getRevisionByTitle( $title );
3708 }
3709 }
3710 if ( $revRecord ) {
3711 # Update title, as $revRecord may have been changed by hook
3712 $title = Title::newFromPageIdentity( $revRecord->getPage() );
3713 // Assuming title is not external if we've got a $revRecord
3714 $deps[] = [
3715 'title' => $title,
3716 'page_id' => $revRecord->getPageId(),
3717 'rev_id' => $revRecord->getId(),
3718 ];
3719 } elseif ( !$title->isExternal() ) {
3720 $deps[] = [
3721 'title' => $title,
3722 'page_id' => $title->getArticleID(),
3723 'rev_id' => null,
3724 ];
3725 }
3726 if ( !$title->equals( $origTitle ) ) {
3727 # If we fetched a rev from a different title, register
3728 # the original title too...
3729 if ( !$origTitle->isExternal() ) {
3730 $deps[] = [
3731 'title' => $origTitle,
3732 'page_id' => $origTitle->getArticleID(),
3733 'rev_id' => null,
3734 ];
3735 }
3736 $titleChanged = true;
3737 }
3738 # If there is no current revision, there is no page
3739 if ( $revRecord === null || $revRecord->getId() === null ) {
3740 $linkCache->addBadLinkObj( $title );
3741 }
3742 if ( $revRecord ) {
3743 if ( $titleChanged && !$revRecord->hasSlot( SlotRecord::MAIN ) ) {
3744 // We've added this (missing) title to the dependencies;
3745 // give the hook another chance to redirect it to an
3746 // actual page.
3747 $text = false;
3748 $finalTitle = $title;
3749 continue;
3750 }
3751 if ( $revRecord->hasSlot( SlotRecord::MAIN ) ) { // T276476
3752 $content = $revRecord->getContent( SlotRecord::MAIN );
3753 $text = $content ? $content->getWikitextForTransclusion() : null;
3754 } else {
3755 $text = false;
3756 }
3757
3758 if ( $text === false || $text === null ) {
3759 $text = false;
3760 break;
3761 }
3762 } else {
3763 $content = $shadowPageLoader->get( $title )?->getContentForTransclusion();
3764 if ( $content ) {
3765 $text = $content->getWikitextForTransclusion();
3766 } else {
3767 $text = false;
3768 }
3769 break;
3770 }
3771 if ( !$content ) {
3772 break;
3773 }
3774 # Redirect?
3775 $finalTitle = $title;
3776 $title = $content->getRedirectTarget();
3777 }
3778
3779 $retValues = [
3780 // previously, when this also returned a Revision object, we set
3781 // 'revision-record' to false instead of null if it was unavailable,
3782 // so that callers to use isset and then rely on the revision-record
3783 // key instead of the revision key, even if there was no corresponding
3784 // object - we continue to set to false here for backwards compatibility
3785 'revision-record' => $revRecord ?: false,
3786 'text' => $text,
3787 'finalTitle' => $finalTitle,
3788 'deps' => $deps
3789 ];
3790 return $retValues;
3791 }
3792
3800 public static function statelessFetchTemplate( $page, $parser = false ) {
3801 $services = MediaWikiServices::getInstance();
3802 return self::defaultFetchTemplate(
3803 $services->getRevisionLookup(),
3804 new HookRunner( $services->getHookContainer() ),
3805 $services->getLinkCache(),
3806 $services->getShadowPageLoader(),
3807 $page,
3808 $parser
3809 );
3810 }
3811
3820 public function fetchFileAndTitle( LinkTarget $link, array $options = [] ) {
3821 $file = $this->fetchFileNoRegister( $link, $options );
3822
3823 $time = $file ? $file->getTimestamp() : false;
3824 $sha1 = $file ? $file->getSha1() : false;
3825 # Register the file as a dependency...
3826 $this->mOutput->addImage( $link, $time, $sha1 );
3827 if ( $file && !$link->isSameLinkAs( $file->getTitle() ) ) {
3828 # Update fetched file title after resolving redirects, etc.
3829 $link = $file->getTitle();
3830 $this->mOutput->addImage( $link, $time, $sha1 );
3831 }
3832
3833 $title = Title::newFromLinkTarget( $link ); // for return type compat
3834 return [ $file, $title ];
3835 }
3836
3847 protected function fetchFileNoRegister( LinkTarget $link, array $options = [] ) {
3848 if ( isset( $options['broken'] ) ) {
3849 $file = false; // broken thumbnail forced by hook
3850 } else {
3851 if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3852 $file = $this->repoGroup->findFileFromKey( $options['sha1'], $options );
3853 } else { // get by (name,timestamp)
3854 $link = TitleValue::newFromLinkTarget( $link );
3855 $file = $this->repoGroup->findFile( $link, $options );
3856 }
3857 }
3858 return $file;
3859 }
3860
3870 public function interwikiTransclude( LinkTarget $link, $action ): string {
3871 if ( !$this->svcOptions->get( MainConfigNames::EnableScaryTranscluding ) ) {
3872 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3873 }
3874
3875 // TODO: extract relevant functionality from Title
3876 $title = Title::newFromLinkTarget( $link );
3877
3878 $url = $title->getFullURL( [ 'action' => $action ] );
3879 if ( strlen( $url ) > 1024 ) {
3880 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3881 }
3882
3883 $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3884
3885 $fname = __METHOD__;
3886
3887 $cache = $this->wanCache;
3888 $data = $cache->getWithSetCallback(
3889 $cache->makeGlobalKey(
3890 'interwiki-transclude',
3891 ( $wikiId !== false ) ? $wikiId : 'external',
3892 sha1( $url )
3893 ),
3894 $this->svcOptions->get( MainConfigNames::TranscludeCacheExpiry ),
3895 function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3896 $req = $this->httpRequestFactory->create( $url, [], $fname );
3897
3898 $status = $req->execute(); // Status object
3899 if ( !$status->isOK() ) {
3900 $ttl = $cache::TTL_UNCACHEABLE;
3901 } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3902 // Align $wgCdnMaxageLagged, although not currently configurable
3903 $ttl = min( 30, $ttl );
3904 }
3905
3906 return [
3907 'text' => $status->isOK() ? $req->getContent() : null,
3908 'code' => $req->getStatus()
3909 ];
3910 },
3911 [
3912 'checkKeys' => ( $wikiId !== false )
3913 ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3914 : [],
3915 'pcGroup' => 'interwiki-transclude:5',
3916 'pcTTL' => $cache::TTL_PROC_LONG
3917 ]
3918 );
3919
3920 if ( is_string( $data['text'] ) ) {
3921 $text = $data['text'];
3922 } elseif ( $data['code'] != 200 ) {
3923 // Though we failed to fetch the content, this status is useless.
3924 $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3925 ->params( $url, $data['code'] )->inContentLanguage()->text();
3926 } else {
3927 $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3928 }
3929
3930 return $text;
3931 }
3932
3942 public function argSubstitution( array $piece, PPFrame $frame ) {
3943 $error = false;
3944 $parts = $piece['parts'];
3945 $nameWithSpaces = $frame->expand( $piece['title'] );
3946 $argName = trim( $nameWithSpaces );
3947 $object = false;
3948 $text = $frame->getArgument( $argName );
3949 if ( $text === false && $parts->getLength() > 0
3950 && ( $this->ot['html']
3951 || $this->ot['pre']
3952 || ( $this->ot['wiki'] && $frame->isTemplate() )
3953 )
3954 ) {
3955 # No match in frame, use the supplied default
3956 $object = $parts->item( 0 )->getChildren();
3957 }
3958 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3959 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3960 $this->limitationWarn( 'post-expand-template-argument' );
3961 }
3962
3963 if ( $text === false && $object === false ) {
3964 # No match anywhere
3965 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3966 }
3967 if ( $error !== false ) {
3968 $text .= $error;
3969 }
3970 if ( $object !== false ) {
3971 $ret = [ 'object' => $object ];
3972 } else {
3973 $ret = [ 'text' => $text ];
3974 }
3975
3976 return $ret;
3977 }
3978
3979 public function tagNeedsNowikiStrippedInTagPF( string $lowerTagName ): bool {
3980 $parsoidSiteConfig = MediaWikiServices::getInstance()->getParsoidSiteConfig();
3981 return $parsoidSiteConfig->tagNeedsNowikiStrippedInTagPF( $lowerTagName );
3982 }
3983
3999 public function extensionSubstitution( array $params, PPFrame $frame ): string {
4000 static $errorStr = '<span class="error">';
4001
4002 $name = $frame->expand( $params['name'] );
4003 if ( str_starts_with( $name, $errorStr ) ) {
4004 // Probably expansion depth or node count exceeded. Just punt the
4005 // error up.
4006 return $name;
4007 }
4008
4009 // Parse attributes from XML-like wikitext syntax
4010 $attrText = !isset( $params['attr'] ) ? '' : $frame->expand( $params['attr'] );
4011 if ( str_starts_with( $attrText, $errorStr ) ) {
4012 // See above
4013 return $attrText;
4014 }
4015
4016 // We can't safely check if the expansion for $content resulted in an
4017 // error, because the content could happen to be the error string
4018 // (T149622).
4019 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4020
4021 $marker = self::MARKER_PREFIX . "-$name-"
4022 . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4023
4024 $normalizedName = strtolower( $name );
4025 $isNowiki = $normalizedName === 'nowiki';
4026 $markerType = $isNowiki ? 'nowiki' : 'general';
4027
4028 // The content is stored as extra data to potentially be pulled out
4029 // with StripState::replaceNoWikis
4030 $extra = $isNowiki ? ( $content ?? '' ) : null;
4031
4032 if ( $this->ot['html'] || ( $isNowiki && $this->useParsoidFragments ) ) {
4033 $attributes = Sanitizer::decodeTagAttributes( $attrText );
4034 // Merge in attributes passed via {{#tag:}} parser function
4035 if ( isset( $params['attributes'] ) ) {
4036 $attributes += $params['attributes'];
4037 }
4038
4039 if ( isset( $this->mTagHooks[$normalizedName] ) ) {
4040 // Note that $content may be null here, for example if the
4041 // tag is self-closed.
4042 $output = $this->mTagHooks[$normalizedName]( $content, $attributes, $this, $frame );
4043 } else {
4044 $output = '<span class="error">Invalid tag extension name: ' .
4045 htmlspecialchars( $normalizedName ) . '</span>';
4046 }
4047
4048 if ( is_array( $output ) ) {
4049 // Extract flags
4050 $flags = $output;
4051 $output = $flags[0];
4052 if ( isset( $flags['isRawHTML'] ) ) {
4053 $markerType = 'nowiki';
4054 }
4055 if ( isset( $flags['markerType'] ) ) {
4056 $markerType = $flags['markerType'];
4057 }
4058 }
4059 } else {
4060 // We're substituting a {{subst:#tag:}} parser function.
4061 // Convert the attributes it passed into the XML-like string.
4062 if ( isset( $params['attributes'] ) ) {
4063 foreach ( $params['attributes'] as $attrName => $attrValue ) {
4064 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4065 htmlspecialchars( $this->getStripState()->unstripBoth( $attrValue ), ENT_COMPAT ) . '"';
4066 }
4067 }
4068 if ( $content === null ) {
4069 $output = "<$name$attrText/>";
4070 } else {
4071 $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
4072 if ( str_starts_with( $close, $errorStr ) ) {
4073 // See above
4074 return $close;
4075 }
4076 $output = "<$name$attrText>$content$close";
4077 }
4078 if ( $this->useParsoidFragments ) {
4079 $markerType = 'exttag';
4080 }
4081 }
4082
4083 if ( $markerType === 'none' ) {
4084 return $output;
4085 } elseif ( $markerType === 'nowiki' ) {
4086 $this->mStripState->addNoWiki( $marker, $output, $extra );
4087 } elseif ( $markerType === 'general' ) {
4088 $this->mStripState->addGeneral( $marker, $output );
4089 } elseif ( $markerType === 'exttag' ) {
4090 $this->mStripState->addExtTag( $marker, $output, $frame );
4091 } else {
4092 throw new UnexpectedValueException( __METHOD__ . ': invalid marker type' );
4093 }
4094 return $marker;
4095 }
4096
4104 private function incrementIncludeSize( $type, $size ) {
4105 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4106 return false;
4107 } else {
4108 $this->mIncludeSizes[$type] += $size;
4109 return true;
4110 }
4111 }
4112
4118 $this->mExpensiveFunctionCount++;
4119 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4120 }
4121
4129 private function handleDoubleUnderscore( string $text ): string {
4130 # The position of __TOC__ needs to be recorded
4131 $mw = $this->magicWordFactory->get( 'toc' );
4132 $tocAlias = null;
4133 if ( $mw->match( $text ) ) {
4134 $this->mShowToc = true;
4135 $this->mForceTocPosition = true;
4136 # record the alias used
4137 preg_match( $mw->getRegex(), $text, $tocAlias );
4138
4139 # Set a placeholder. At the end we'll fill it in with the TOC.
4140 $text = $mw->replace( self::TOC_PLACEHOLDER, $text, 1 );
4141
4142 # Only keep the first one.
4143 $text = $mw->replace( '', $text );
4144 }
4145
4146 # Now match and remove the rest of them
4147 $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4148 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text, returnAlias: true );
4149 if ( $tocAlias ) {
4150 # For consistency with all other double-underscores (see below)
4151 $this->mDoubleUnderscores['toc'] = $tocAlias[0];
4152 }
4153
4154 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4155 $this->mOutput->setNoGallery( true );
4156 }
4157 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4158 $this->mShowToc = false;
4159 }
4160 if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4161 && $this->getTitle()->getNamespace() === NS_CATEGORY
4162 ) {
4163 $this->addTrackingCategory( 'hidden-category-category' );
4164 }
4165 # (T10068) Allow control over whether robots index a page.
4166 # __NOINDEX__ always overrides __INDEX__, see T16899
4167 if (
4168 isset( $this->mDoubleUnderscores['noindex'] ) &&
4169 $this->nsInfo->canUseNoindex( $this->getPage()->getNamespace() )
4170 ) {
4171 $this->mOutput->setIndexPolicy( 'noindex' );
4172 $this->addTrackingCategory( 'noindex-category' );
4173 }
4174 if (
4175 isset( $this->mDoubleUnderscores['index'] ) &&
4176 $this->nsInfo->canUseNoindex( $this->getPage()->getNamespace() )
4177 ) {
4178 $this->mOutput->setIndexPolicy( 'index' );
4179 $this->addTrackingCategory( 'index-category' );
4180 }
4181
4182 foreach ( $this->mDoubleUnderscores as $key => $alias ) {
4183 # Cache all double underscores in the database
4184 $this->mOutput->setUnsortedPageProperty( $key );
4185 # Check for deprecated local aliases (T407289)
4186 $ascii = str_starts_with( $alias, '__' ) && str_ends_with( $alias, '__' );
4187 $wide = str_starts_with( $alias, '__' ) && str_ends_with( $alias, '__' );
4188 if ( !( $ascii || $wide ) ) {
4189 $this->addTrackingCategory( 'bad-double-underscore-category' );
4190 }
4191 }
4192
4193 return $text;
4194 }
4195
4202 public function addTrackingCategory( $msg ) {
4203 return $this->trackingCategories->addTrackingCategory(
4204 $this->mOutput, $msg, $this->getPage()
4205 );
4206 }
4207
4219 public function msg( $key, ...$params ): Message {
4220 return wfMessage( $key, ...$params )
4221 ->inLanguage( $this->getTargetLanguage() )
4222 ->page( $this->getPage() );
4223 }
4224
4225 private function cleanUpTocLine( Node $container ) {
4226 '@phan-var Element|DocumentFragment $container'; // @var Element|DocumentFragment $container
4227 # Strip out HTML
4228 # Allowed tags are:
4229 # * <sup> and <sub> (T10393)
4230 # * <i> (T28375)
4231 # * <b> (r105284)
4232 # * <bdi> (T74884)
4233 # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4234 # * <s> and <strike> (T35715)
4235 # * <q> (T251672)
4236 # We strip any parameter from accepted tags, except dir="rtl|ltr" from <span>,
4237 # to allow setting directionality in toc items.
4238 $allowedTags = [ 'span', 'sup', 'sub', 'bdi', 'i', 'b', 's', 'strike', 'q' ];
4239 $node = $container->firstChild;
4240 while ( $node !== null ) {
4241 $next = $node->nextSibling;
4242 if ( $node instanceof Element ) {
4243 $nodeName = DOMUtils::nodeName( $node );
4244 if ( in_array( $nodeName, [ 'style', 'script' ], true ) ) {
4245 # Remove any <style> or <script> tags (T198618)
4246 DOMCompat::remove( $node );
4247 } elseif ( in_array( $nodeName, $allowedTags, true ) ) {
4248 // Keep tag, remove attributes
4249 $removeAttrs = [];
4250 foreach ( $node->attributes as $attr ) {
4251 if (
4252 $nodeName === 'span' && $attr->name === 'dir'
4253 && ( $attr->value === 'rtl' || $attr->value === 'ltr' )
4254 ) {
4255 // Keep <span dir="rtl"> and <span dir="ltr">
4256 continue;
4257 }
4258 $removeAttrs[] = $attr;
4259 }
4260 foreach ( $removeAttrs as $attr ) {
4261 $node->removeAttributeNode( $attr );
4262 }
4263 $this->cleanUpTocLine( $node );
4264 # Strip '<span></span>', which is the result from the above if
4265 # <span id="foo"></span> is used to produce an additional anchor
4266 # for a section.
4267 if ( $nodeName === 'span' && !$node->hasChildNodes() ) {
4268 DOMCompat::remove( $node );
4269 }
4270 } else {
4271 // Strip tag
4272 if ( $node->firstChild !== null ) {
4273 $next = $node->firstChild;
4274 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
4275 while ( $childNode = $node->firstChild ) {
4276 $node->parentNode->insertBefore( $childNode, $node );
4277 }
4278 }
4279 DOMCompat::remove( $node );
4280 }
4281 } elseif ( $node instanceof Comment ) {
4282 // Extensions may add comments to headings;
4283 // these shouldn't appear in the ToC either.
4284 DOMCompat::remove( $node );
4285 }
4286 $node = $next;
4287 }
4288 }
4289
4305 private function finalizeHeadings( string $text, string $origText, bool $isMain = true ): string {
4306 # Inhibit editsection links if requested in the page
4307 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4308 $maybeShowEditLink = false;
4309 } else {
4310 $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4311 }
4312
4313 # Get all headlines for numbering them and adding funky stuff like [edit]
4314 # links - this is for later, but we need the number of headlines right now
4315 # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4316 # be trimmed here since whitespace in HTML headings is significant.
4317 $matches = [];
4318 $numMatches = preg_match_all(
4319 '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4320 $text,
4321 $matches
4322 );
4323
4324 # if there are fewer than 4 headlines in the article, do not show TOC
4325 # unless it's been explicitly enabled.
4326 $enoughToc = $this->mShowToc &&
4327 ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4328
4329 # Allow user to stipulate that a page should have a "new section"
4330 # link added via __NEWSECTIONLINK__
4331 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4332 $this->mOutput->setNewSection( true );
4333 }
4334
4335 # Allow user to remove the "new section"
4336 # link via __NONEWSECTIONLINK__
4337 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4338 $this->mOutput->setHideNewSection( true );
4339 }
4340
4341 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4342 # override above conditions and always show TOC above first header
4343 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4344 $this->mShowToc = true;
4345 $enoughToc = true;
4346 }
4347
4348 if ( !$numMatches ) {
4349 return $text;
4350 }
4351
4352 # headline counter
4353 $headlineCount = 0;
4354 $haveTocEntries = false;
4355
4356 # Ugh .. the TOC should have neat indentation levels which can be
4357 # passed to the skin functions. These are determined here
4358 $head = [];
4359 $level = 0;
4360 $tocData = new TOCData();
4361 $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4362 $oldType = $this->mOutputType;
4363 $this->setOutputType( self::OT_WIKI );
4364 $frame = $this->getPreprocessor()->newFrame();
4365 $root = $this->preprocessToDom( $origText );
4366 $node = $root->getFirstChild();
4367 $cpOffset = 0;
4368 $refers = [];
4369
4370 $maxTocLevel = $this->svcOptions->get( MainConfigNames::MaxTocLevel );
4371 $domDocument = DOMCompat::newDocument();
4372 foreach ( $matches[3] as $headline ) {
4373 // $headline is half-parsed HTML
4374 $isTemplate = false;
4375 $titleText = false;
4376 $sectionIndex = false;
4377 if ( preg_match( self::HEADLINE_MARKER_REGEX, $headline, $markerMatches ) ) {
4378 $serial = (int)$markerMatches[1];
4379 [ $titleText, $sectionIndex ] = $this->mHeadings[$serial];
4380 $isTemplate = ( $titleText != $baseTitleText );
4381 $headline = ltrim( substr( $headline, strlen( $markerMatches[0] ) ) );
4382 }
4383
4384 $sectionMetadata = SectionMetadata::fromLegacy( [
4385 "fromtitle" => $titleText ?: null,
4386 "index" => $sectionIndex === false
4387 ? '' : ( ( $isTemplate ? 'T-' : '' ) . $sectionIndex )
4388 ] );
4389 $tocData->addSection( $sectionMetadata );
4390
4391 $oldLevel = $level;
4392 $level = (int)$matches[1][$headlineCount];
4393 $tocData->processHeading( $oldLevel, $level, $sectionMetadata );
4394
4395 if ( $tocData->getCurrentTOCLevel() < $maxTocLevel ) {
4396 $haveTocEntries = true;
4397 }
4398
4399 # Remove link placeholders by the link text.
4400 # <!--LINK number-->
4401 # turns into
4402 # link text with suffix
4403 # Do this before unstrip since link text can contain strip markers
4404 $fullyParsedHeadline = $this->replaceLinkHoldersText( $headline );
4405
4406 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4407 $fullyParsedHeadline = $this->mStripState->unstripBoth( $fullyParsedHeadline );
4408
4409 // Run Tidy to convert wikitext entities to HTML entities (T355386),
4410 // conveniently also giving us a way to handle French spaces (T324763)
4411 $fullyParsedHeadline = $this->tidy->tidy( $fullyParsedHeadline, Sanitizer::armorFrenchSpaces( ... ) );
4412
4413 // Wrap the safe headline to parse the heading attributes
4414 // Literal HTML tags should be sanitized at this point
4415 // cleanUpTocLine will strip the headline tag
4416 $wrappedHeadline = "<h$level" . $matches['attrib'][$headlineCount] . $fullyParsedHeadline . "</h$level>";
4417
4418 // Parse the heading contents as HTML. This makes it easier to strip out some HTML tags,
4419 // and ensures that we generate balanced HTML at the end (T218330).
4420 $headlineDom = DOMUtils::parseHTMLToFragment( $domDocument, $wrappedHeadline );
4421
4422 // Extract a user defined id on the heading
4423 // A heading is expected as the first child and could be asserted
4424 $h = $headlineDom->firstChild;
4425 $headingId = ( $h instanceof Element && DOMUtils::isHeading( $h ) ) ?
4426 DOMCompat::getAttribute( $h, 'id' ) : null;
4427
4428 $this->cleanUpTocLine( $headlineDom );
4429
4430 // Serialize back to HTML
4431 // $tocline is for the TOC display, fully-parsed HTML with some tags removed
4432 $tocline = trim( DOMUtils::getFragmentInnerHTML( $headlineDom ) );
4433
4434 // $headlineText is for the "Edit section: $1" tooltip, plain text
4435 $headlineText = trim( $headlineDom->textContent );
4436
4437 if ( $headingId === null || $headingId === '' ) {
4438 $headingId = Sanitizer::normalizeSectionNameWhitespace( $headlineText );
4439 $headingId = self::normalizeSectionName( $headingId );
4440 }
4441
4442 # Create the anchor for linking from the TOC to the section
4443 $fallbackAnchor = Sanitizer::escapeIdForAttribute( $headingId, Sanitizer::ID_FALLBACK );
4444 $linkAnchor = Sanitizer::escapeIdForLink( $headingId );
4445 $anchor = Sanitizer::escapeIdForAttribute( $headingId, Sanitizer::ID_PRIMARY );
4446 if ( $fallbackAnchor === $anchor ) {
4447 # No reason to have both (in fact, we can't)
4448 $fallbackAnchor = false;
4449 }
4450
4451 # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4452 $arrayKey = strtolower( $anchor );
4453 if ( $fallbackAnchor === false ) {
4454 $fallbackArrayKey = false;
4455 } else {
4456 $fallbackArrayKey = strtolower( $fallbackAnchor );
4457 }
4458
4459 if ( isset( $refers[$arrayKey] ) ) {
4460 for ( $i = 2; isset( $refers["{$arrayKey}_$i"] ); ++$i );
4461 $anchor .= "_$i";
4462 $linkAnchor .= "_$i";
4463 $refers["{$arrayKey}_$i"] = true;
4464 } else {
4465 $refers[$arrayKey] = true;
4466 }
4467 if ( $fallbackAnchor !== false && isset( $refers[$fallbackArrayKey] ) ) {
4468 for ( $i = 2; isset( $refers["{$fallbackArrayKey}_$i"] ); ++$i );
4469 $fallbackAnchor .= "_$i";
4470 $refers["{$fallbackArrayKey}_$i"] = true;
4471 } else {
4472 $refers[$fallbackArrayKey] = true;
4473 }
4474
4475 # Add the section to the section tree
4476 # Find the DOM node for this header
4477 $noOffset = ( $isTemplate || $sectionIndex === false );
4478 while ( $node && !$noOffset ) {
4479 if ( $node->getName() === 'h' ) {
4480 $bits = $node->splitHeading();
4481 if ( $bits['i'] == $sectionIndex ) {
4482 break;
4483 }
4484 }
4485 $cpOffset += mb_strlen(
4486 $this->mStripState->unstripBoth(
4487 $frame->expand( $node, PPFrame::RECOVER_ORIG )
4488 )
4489 );
4490 $node = $node->getNextSibling();
4491 }
4492 $sectionMetadata->line = $tocline;
4493 $sectionMetadata->codepointOffset = ( $noOffset ? null : $cpOffset );
4494 $sectionMetadata->anchor = $anchor;
4495 $sectionMetadata->linkAnchor = $linkAnchor;
4496
4497 if ( $maybeShowEditLink && $sectionIndex !== false ) {
4498 // Output edit section links as markers with styles that can be customized by skins
4499 if ( $isTemplate ) {
4500 # Put a T flag in the section identifier, to indicate to extractSections()
4501 # that sections inside <includeonly> should be counted.
4502 $editsectionPage = $titleText;
4503 $editsectionSection = "T-$sectionIndex";
4504 } else {
4505 $editsectionPage = $this->getTitle()->getPrefixedText();
4506 $editsectionSection = $sectionIndex;
4507 }
4508 // Construct a pseudo-HTML tag as a placeholder for the section edit link. It is replaced in
4509 // MediaWiki\OutputTransform\Stages\HandleSectionLinks with the real link.
4510 //
4511 // Any HTML markup in the input has already been escaped,
4512 // so we don't have to worry about a user trying to input one of these markers directly.
4513 //
4514 // We put the page and section in attributes to stop the language converter from
4515 // converting them, but put the headline hint in tag content
4516 // because it is supposed to be able to convert that.
4517 $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage, ENT_COMPAT );
4518 $editlink .= '" section="' . htmlspecialchars( $editsectionSection, ENT_COMPAT ) . '"';
4519 $editlink .= '>' . htmlspecialchars( $headlineText ) . '</mw:editsection>';
4520 } else {
4521 $editlink = '';
4522 }
4523 // Reconstruct the original <h#> tag with added attributes. It is replaced in
4524 // MediaWiki\OutputTransform\Stages\HandleSectionLinks to add anchors and stuff.
4525 //
4526 // data-mw-... attributes are forbidden in Sanitizer::isReservedDataAttribute(),
4527 // so we don't have to worry about a user trying to input one of these markers directly.
4528 //
4529 // We put the anchors in attributes to stop the language converter from converting them.
4530 $head[$headlineCount] = "<h$level" . Html::expandAttributes( [
4531 'data-mw-anchor' => $anchor,
4532 'data-mw-fallback-anchor' => $fallbackAnchor,
4533 ] ) . $matches['attrib'][$headlineCount] . $headline . $editlink . "</h$level>";
4534
4535 $headlineCount++;
4536 }
4537
4538 $this->setOutputType( $oldType );
4539
4540 # Never ever show TOC if no headers (or suppressed)
4541 $suppressToc = $this->mOptions->getSuppressTOC();
4542 if ( !$haveTocEntries ) {
4543 $enoughToc = false;
4544 }
4545 $addTOCPlaceholder = false;
4546
4547 if ( $isMain && !$suppressToc ) {
4548 // We generally output the section information via the API
4549 // even if there isn't "enough" of a ToC to merit showing
4550 // it -- but the "suppress TOC" parser option is set when
4551 // any sections that might be found aren't "really there"
4552 // (ie, JavaScript content that might have spurious === or
4553 // <h2>: T307691) so we will *not* set section information
4554 // in that case.
4555 $this->mOutput->setTOCData( $tocData );
4556
4557 // T294950: Record a suggestion that the TOC should be shown.
4558 // Skins are free to ignore this suggestion and implement their
4559 // own criteria for showing/suppressing TOC (T318186).
4560 if ( $enoughToc ) {
4561 $this->mOutput->setOutputFlag( ParserOutputFlags::SHOW_TOC );
4562 if ( !$this->mForceTocPosition ) {
4563 $addTOCPlaceholder = true;
4564 }
4565 }
4566
4567 // If __NOTOC__ is used on the page (and not overridden by
4568 // __TOC__ or __FORCETOC__) set the NO_TOC flag to tell
4569 // the skin that although the section information is
4570 // valid, it should perhaps not be presented as a Table Of
4571 // Contents.
4572 if ( !$this->mShowToc ) {
4573 $this->mOutput->setOutputFlag( ParserOutputFlags::NO_TOC );
4574 }
4575 }
4576
4577 # split up and insert constructed headlines
4578 $blocks = preg_split( '/<h[1-6]\b[^>]*>.*?<\/h[1-6]>/is', $text );
4579 $i = 0;
4580
4581 // build an array of document sections
4582 $sections = [];
4583 foreach ( $blocks as $block ) {
4584 // $head is zero-based, sections aren't.
4585 if ( empty( $head[$i - 1] ) ) {
4586 $sections[$i] = $block;
4587 } else {
4588 $sections[$i] = $head[$i - 1] . $block;
4589 }
4590
4591 $i++;
4592 }
4593
4594 if ( $addTOCPlaceholder ) {
4595 // append the TOC at the beginning
4596 // Top anchor now in skin
4597 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset At least one element when enoughToc is true
4598 $sections[0] .= self::TOC_PLACEHOLDER . "\n";
4599 }
4600
4601 return implode( '', $sections );
4602 }
4603
4615 public static function localizeTOC(
4616 ?TOCData $tocData, Language $lang, ?ILanguageConverter $converter,
4617 ?string $preferredVariant = null
4618 ) {
4619 if ( $tocData === null ) {
4620 return; // Nothing to do
4621 }
4622 foreach ( $tocData->getSections() as $s ) {
4623 // Localize heading
4624 if ( $converter ) {
4625 $preferredVariant ??= $converter->getPreferredVariant();
4626 // T331316: don't use 'convert' or 'convertTo' as these reset
4627 // the language converter state.
4628 $s->line = $converter->convertTo(
4629 $s->line, $preferredVariant, false
4630 );
4631 }
4632 // Localize numbering
4633 $dot = '.';
4634 $pieces = explode( $dot, $s->number );
4635 $numbering = '';
4636 foreach ( $pieces as $i => $p ) {
4637 if ( $i > 0 ) {
4638 $numbering .= $dot;
4639 }
4640 $numbering .= $lang->formatNum( $p );
4641 }
4642 $s->number = $numbering;
4643 }
4644 }
4645
4658 public function preSaveTransform(
4659 $text,
4660 PageReference $page,
4661 UserIdentity $user,
4662 ParserOptions $options,
4663 $clearState = true
4664 ): string {
4665 if ( $clearState ) {
4666 $magicScopeVariable = $this->lock();
4667 }
4668 $this->startParse( $page, $options, self::OT_WIKI, $clearState );
4669 $this->setUser( $user );
4670
4671 // Strip U+0000 NULL (T159174)
4672 $text = str_replace( "\000", '', $text );
4673
4674 // We still normalize line endings (including trimming trailing whitespace) for
4675 // backwards-compatibility with other code that just calls PST, but this should already
4676 // be handled in TextContent subclasses
4677 $text = TextContent::normalizeLineEndings( $text );
4678
4679 if ( $options->getPreSaveTransform() ) {
4680 $text = $this->pstPass2( $text, $user );
4681 }
4682 $text = $this->mStripState->unstripBoth( $text );
4683
4684 // Trim trailing whitespace again, because the previous steps can introduce it.
4685 $text = rtrim( $text );
4686
4687 $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4688
4689 $this->setUser( null ); # Reset
4690
4691 return $text;
4692 }
4693
4702 private function pstPass2( string $text, UserIdentity $user ): string {
4703 # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4704 # $this->contLang here in order to give everyone the same signature and use the default one
4705 # rather than the one selected in each user's preferences. (see also T14815)
4706 $ts = $this->mOptions->getTimestamp();
4707 $timestamp = MWTimestamp::getLocalInstance( $ts );
4708 $ts = $timestamp->format( 'YmdHis' );
4709 $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4710
4711 $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4712
4713 # Variable replacement
4714 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4715 $text = $this->replaceVariables( $text );
4716
4717 # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4718 # which may corrupt this parser instance via its wfMessage()->text() call-
4719
4720 # Signatures
4721 if ( str_contains( $text, '~~~' ) ) {
4722 $sigText = $this->getUserSig( $user );
4723 $text = strtr( $text, [
4724 '~~~~~' => $d,
4725 '~~~~' => "$sigText $d",
4726 '~~~' => $sigText
4727 ] );
4728 # The main two signature forms used above are time-sensitive
4729 $this->setOutputFlag( ParserOutputFlags::USER_SIGNATURE, 'User signature detected' );
4730 }
4731
4732 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4733 $tc = '[' . Title::legalChars() . ']';
4734 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4735
4736 // [[ns:page (context)|]]
4737 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4738 // [[ns:page(context)|]] (double-width brackets, added in r40257)
4739 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4740 // [[ns:page (context), context|]] (using single, double-width or Arabic comma)
4741 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,|، )$tc+|)\\|]]/";
4742 // [[|page]] (reverse pipe trick: add context from page title)
4743 $p2 = "/\[\[\\|($tc+)]]/";
4744
4745 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4746 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4747 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4748 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4749
4750 $t = $this->getTitle()->getText();
4751 $m = [];
4752 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4753 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4754 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4755 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4756 } else {
4757 # if there's no context, don't bother duplicating the title
4758 $text = preg_replace( $p2, '[[\\1]]', $text );
4759 }
4760
4761 return $text;
4762 }
4763
4779 public function getUserSig( UserIdentity $user, $nickname = false, $fancySig = null ): string {
4780 $username = $user->getName();
4781
4782 # If not given, retrieve from the user object.
4783 if ( $nickname === false ) {
4784 $nickname = $this->userOptionsLookup->getOption( $user, 'nickname' );
4785 }
4786
4787 $fancySig ??= $this->userOptionsLookup->getBoolOption( $user, 'fancysig' );
4788
4789 if ( $nickname === null || $nickname === '' ) {
4790 // Empty value results in the default signature (even when fancysig is enabled)
4791 $nickname = $username;
4792 } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( MainConfigNames::MaxSigChars ) ) {
4793 $nickname = $username;
4794 $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4795 } elseif ( $fancySig !== false ) {
4796 # Sig. might contain markup; validate this
4797 $isValid = $this->validateSig( $nickname ) !== false;
4798
4799 # New validator
4800 $sigValidation = $this->svcOptions->get( MainConfigNames::SignatureValidation );
4801 if ( $isValid && $sigValidation === 'disallow' ) {
4802 $parserOpts = new ParserOptions(
4803 $this->mOptions->getUserIdentity(),
4804 $this->contLang
4805 );
4806 $validator = $this->signatureValidatorFactory
4807 ->newSignatureValidator( $user, null, $parserOpts );
4808 $isValid = !$validator->validateSignature( $nickname );
4809 }
4810
4811 if ( $isValid ) {
4812 # Validated; clean up (if needed) and return it
4813 return $this->cleanSig( $nickname, true );
4814 } else {
4815 # Failed to validate; fall back to the default
4816 $nickname = $username;
4817 $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4818 }
4819 }
4820
4821 # Make sure nickname doesn't get a sig in a sig
4822 $nickname = self::cleanSigInSig( $nickname );
4823
4824 # If we're still here, make it a link to the user page
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 $message = wfMessage( $msgName )->inContentLanguage()->page( $this->getPage() );
4833
4834 $userText = wfEscapeWikiText( $username );
4835 $nickText = wfEscapeWikiText( $nickname );
4836 // If the signature message doesn't contain any tildes, we can safely un-escape the leading
4837 // tildes in the username/nickname, in order to make temporary account names more readable.
4838 if ( !str_contains( $message->plain(), '~' ) && !str_starts_with( $message->plain(), '$' ) ) {
4839 if ( $userText !== '&#126;' ) {
4840 $userText = preg_replace( '/^&#126;/', '~', $userText );
4841 }
4842 if ( $nickText !== '&#126;' ) {
4843 $nickText = preg_replace( '/^&#126;/', '~', $nickText );
4844 }
4845 }
4846
4847 return $message->params( $userText, $nickText )->text();
4848 }
4849
4857 public function validateSig( $text ): string|false {
4858 // Self-close HTML5 void elements with no attributes (e.g. <br> -> <br />)
4859 // so that both spellings are treated identically by the XML
4860 // well-formedness check below. This list mirrors Html::VOID_ELEMENTS.
4861 // Only bare tags (optional whitespace, no attributes) are matched, to
4862 // avoid the ambiguity and abuse surface of a generic attribute match.
4863 $text = preg_replace(
4864 '/<(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)\s*>/i',
4865 '<$1 />',
4866 $text
4867 );
4868 return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4869 }
4870
4882 public function cleanSig( $text, $parsing = false ): string {
4883 if ( !$parsing ) {
4884 $magicScopeVariable = $this->lock();
4885 $this->startParse(
4886 $this->mTitle,
4887 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
4888 self::OT_PREPROCESS,
4889 true
4890 );
4891 }
4892
4893 # Option to disable this feature
4894 if ( !$this->mOptions->getCleanSignatures() ) {
4895 return $text;
4896 }
4897
4898 # @todo FIXME: Regex doesn't respect extension tags or nowiki
4899 # => Move this logic to braceSubstitution()
4900 $substWord = $this->magicWordFactory->get( 'subst' );
4901 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4902 $substText = '{{' . $substWord->getSynonym( 0 );
4903
4904 $text = preg_replace( $substRegex, $substText, $text );
4905 $text = self::cleanSigInSig( $text );
4906 $dom = $this->preprocessToDom( $text );
4907 $frame = $this->getPreprocessor()->newFrame();
4908 $text = $frame->expand( $dom );
4909
4910 if ( !$parsing ) {
4911 $text = $this->mStripState->unstripBoth( $text );
4912 }
4913
4914 return $text;
4915 }
4916
4924 public static function cleanSigInSig( $text ): string {
4925 $text = preg_replace( '/~{3,5}/', '', $text );
4926 return $text;
4927 }
4928
4945 public static function replaceTableOfContentsMarker( $text, $toc ): string {
4946 // Optimization: Avoid a potentially expensive Remex tokenization and reserialization
4947 // if the content does not contain a TOC placeholder, such as during message parsing,
4948 // which may occur hundreds of times per request (T394059).
4949 if ( !str_contains( $text, 'mw:PageProp/toc' ) ) {
4950 return $text;
4951 }
4952
4953 $replaced = false;
4954 return HtmlHelper::modifyElements(
4955 $text,
4956 static function ( SerializerNode $node ): bool {
4957 $prop = $node->attrs['property'] ?? '';
4958 return $node->name === 'meta' && $prop === 'mw:PageProp/toc';
4959 },
4960 static function ( SerializerNode $node ) use ( &$replaced, $toc ) {
4961 if ( $replaced ) {
4962 // Remove the additional metas. While not strictly
4963 // necessary, this also ensures idempotence if we
4964 // run the pass more than once on a given content.
4965 return '';
4966 }
4967 $replaced = true;
4968 return $toc; // outerHTML replacement.
4969 },
4970 false /* use legacy-compatible serialization */
4971 );
4972 }
4973
4985 public function startExternalParse( ?PageReference $page, ParserOptions $options,
4986 $outputType, $clearState = true, $revId = null
4987 ) {
4988 $this->startParse( $page, $options, $outputType, $clearState );
4989 if ( $revId !== null ) {
4990 $this->mRevisionId = $revId;
4991 }
4992 }
4993
5000 private function startParse( ?PageReference $page, ParserOptions $options,
5001 $outputType, $clearState = true
5002 ) {
5003 $this->setPage( $page );
5004 $this->mOptions = $options;
5005 $this->setOutputType( $outputType );
5006 if ( $clearState ) {
5007 $this->clearState();
5008 }
5009 }
5010
5020 public function transformMsg( $text, ParserOptions $options, ?PageReference $page = null ): string {
5021 static $executing = false;
5022
5023 # Guard against infinite recursion
5024 if ( $executing ) {
5025 return $text;
5026 }
5027 $executing = true;
5028
5029 $text = $this->preprocess( $text, $page ?? $this->mTitle, $options );
5030
5031 $executing = false;
5032 return $text;
5033 }
5034
5054 public function setHook( $tag, callable $callback ) {
5055 $tag = strtolower( $tag );
5056 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5057 throw new InvalidArgumentException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5058 }
5059 $oldVal = $this->mTagHooks[$tag] ?? null;
5060 $this->mTagHooks[$tag] = $callback;
5061 if ( !in_array( $tag, $this->mStripList ) ) {
5062 $this->mStripList[] = $tag;
5063 }
5064
5065 return $oldVal;
5066 }
5067
5072 public function clearTagHooks() {
5073 $this->mTagHooks = [];
5074 $this->mStripList = [];
5075 }
5076
5125 public function setFunctionHook( $id, callable $callback, $flags = 0 ): ?callable {
5126 $oldVal = $this->mFunctionHooks[$id][0] ?? null;
5127 $this->mFunctionHooks[$id] = [ $callback, $flags ];
5128
5129 # Add to function cache
5130 $mw = $this->magicWordFactory->get( $id );
5131
5132 $synonyms = $mw->getSynonyms();
5133 $sensitive = intval( $mw->isCaseSensitive() );
5134
5135 foreach ( $synonyms as $syn ) {
5136 # Case
5137 if ( !$sensitive ) {
5138 $syn = $this->contLang->lc( $syn );
5139 }
5140 # Add leading hash
5141 if ( !( $flags & self::SFH_NO_HASH ) ) {
5142 $syn = '#' . $syn;
5143 }
5144 # Remove trailing colon (or Japanese double-width colon)
5145 if ( str_ends_with( $syn, ':' ) || str_ends_with( $syn, ':' ) ) {
5146 $syn = mb_substr( $syn, 0, -1 );
5147 }
5148 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5149 }
5150 return $oldVal;
5151 }
5152
5159 public function getFunctionHooks() {
5160 return array_keys( $this->mFunctionHooks );
5161 }
5162
5169 private function replaceLinkHoldersPrivate( string &$text ): void {
5170 $this->mLinkHolders->replace( $text );
5171 }
5172
5180 private function replaceLinkHoldersText( string $text ): string {
5181 return $this->mLinkHolders->replaceText( $text );
5182 }
5183
5198 public function renderImageGallery( $text, array $params ): string {
5199 $mode = $params['mode'] ?? false;
5200
5201 try {
5202 $ig = ImageGalleryBase::factory( $mode );
5204 // If invalid type set, fallback to default.
5205 $ig = ImageGalleryBase::factory();
5206 }
5207
5208 $ig->setContextTitle( $this->getTitle() );
5209 $ig->setShowBytes( false );
5210 $ig->setShowDimensions( false );
5211 $ig->setParser( $this );
5212 $ig->setHideBadImages();
5213 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5214
5215 $ig->setShowFilename( isset( $params['showfilename'] ) );
5216 if ( isset( $params['caption'] ) ) {
5217 // NOTE: We aren't passing a frame here or below. Frame info
5218 // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5219 // See T107332#4030582
5220 $caption = $this->recursiveTagParse( $params['caption'] );
5221 $ig->setCaptionHtml( $caption );
5222 }
5223 if ( isset( $params['perrow'] ) ) {
5224 $ig->setPerRow( $params['perrow'] );
5225 }
5226 if ( isset( $params['widths'] ) ) {
5227 $ig->setWidths( $params['widths'] );
5228 }
5229 if ( isset( $params['heights'] ) ) {
5230 $ig->setHeights( $params['heights'] );
5231 }
5232 $ig->setAdditionalOptions( $params );
5233
5234 $lines = StringUtils::explode( "\n", $text );
5235 foreach ( $lines as $line ) {
5236 # match lines like these:
5237 # Image:someimage.jpg|This is some image
5238 $matches = [];
5239 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5240 # Skip empty lines
5241 if ( count( $matches ) == 0 ) {
5242 continue;
5243 }
5244
5245 if ( str_contains( $matches[0], '%' ) ) {
5246 $matches[1] = rawurldecode( $matches[1] );
5247 }
5248 $title = Title::newFromText( $matches[1], NS_FILE );
5249 if ( $title === null ) {
5250 # Bogus title. Ignore these so we don't bomb out later.
5251 continue;
5252 }
5253
5254 # We need to get what handler the file uses, to figure out parameters.
5255 # Note, a hook can override the file name, and chose an entirely different
5256 # file (which potentially could be of a different type and have different handler).
5257 $options = [];
5258 $descQuery = false;
5259 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5260 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5261 $this, $title, $options, $descQuery
5262 );
5263 # Don't register it now, as TraditionalImageGallery does that later.
5264 $file = $this->fetchFileNoRegister( $title, $options );
5265 $handler = $file ? $file->getHandler() : false;
5266
5267 $paramMap = [
5268 'img_alt' => 'gallery-internal-alt',
5269 'img_class' => 'gallery-internal-class',
5270 'img_link' => 'gallery-internal-link',
5271 ];
5272 if ( $handler ) {
5273 $paramMap += $handler->getParamMap();
5274 // We don't want people to specify per-image widths.
5275 // Additionally the width parameter would need special casing anyhow.
5276 unset( $paramMap['img_width'] );
5277 }
5278
5279 $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5280
5281 $label = '';
5282 $alt = null;
5283 $handlerOptions = [];
5284 $imageOptions = [];
5285 $hasAlt = false;
5286
5287 if ( isset( $matches[3] ) ) {
5288 // look for an |alt= definition while trying not to break existing
5289 // captions with multiple pipes (|) in it, until a more sensible grammar
5290 // is defined for images in galleries
5291
5292 // FIXME: Doing recursiveTagParse at this stage is a bit odd,
5293 // and different from makeImage.
5294 $matches[3] = $this->recursiveTagParse( $matches[3] );
5295 // Protect LanguageConverter markup
5296 $parameterMatches = StringUtils::delimiterExplode(
5297 '-{', '}-',
5298 '|',
5299 $matches[3],
5300 true /* nested */
5301 );
5302
5303 foreach ( $parameterMatches as $parameterMatch ) {
5304 [ $magicName, $match ] = $mwArray->matchVariableStartToEnd( trim( $parameterMatch ) );
5305 if ( !$magicName ) {
5306 // Last pipe wins.
5307 $label = $parameterMatch;
5308 continue;
5309 }
5310
5311 $paramName = $paramMap[$magicName];
5312 switch ( $paramName ) {
5313 case 'gallery-internal-alt':
5314 $hasAlt = true;
5315 $alt = $this->stripAltText( $match );
5316 break;
5317 case 'gallery-internal-class':
5318 $imageOptions['class'] = $this->stripAltText( $match );
5319 break;
5320 case 'gallery-internal-link':
5321 $linkValue = $this->stripAltText( $match );
5322 if ( preg_match( '/^-{R\|(.*)}-$/', $linkValue ) ) {
5323 // Result of LanguageConverter::markNoConversion
5324 // invoked on an external link.
5325 $linkValue = substr( $linkValue, 4, -2 );
5326 }
5327 [ $type, $target ] = $this->parseLinkParameter( $linkValue );
5328 if ( $type ) {
5329 if ( $type === 'no-link' ) {
5330 $target = true;
5331 }
5332 $imageOptions[$type] = $target;
5333 }
5334 break;
5335 default:
5336 // Must be a handler specific parameter.
5337 if ( $handler->validateParam( $paramName, $match ) ) {
5338 $handlerOptions[$paramName] = $match;
5339 } else {
5340 // Guess not, consider it as caption.
5341 $this->logger->debug(
5342 "$parameterMatch failed parameter validation" );
5343 $label = $parameterMatch;
5344 }
5345 }
5346 }
5347 }
5348
5349 // Match makeImage when !$hasVisibleCaption
5350 if ( !$hasAlt && $label !== '' ) {
5351 $alt = $this->stripAltText( $label );
5352 }
5353 $imageOptions['title'] = $this->stripAltText( $label );
5354
5355 // Match makeImage which sets this unconditionally
5356 $handlerOptions['targetlang'] = $this->getTargetLanguage()->getCode();
5357
5358 $ig->add(
5359 $title, $label, $alt, '', $handlerOptions,
5360 ImageGalleryBase::LOADING_DEFAULT, $imageOptions
5361 );
5362 }
5363 $html = $ig->toHTML();
5364 $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5365 return $html;
5366 }
5367
5372 private function getImageParams( $handler ) {
5373 $handlerClass = $handler ? get_class( $handler ) : '';
5374 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5375 # Initialise static lists
5376 static $internalParamNames = [
5377 'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5378 'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5379 'bottom', 'text-bottom' ],
5380 'frame' => [ 'thumbnail', 'framed', 'frameless', 'border',
5381 // These parameters take arguments, so to ensure literals
5382 // have precedence, keep them listed last (T372935):
5383 'manualthumb', 'upright', 'link', 'alt', 'class' ],
5384 ];
5385 static $internalParamMap;
5386 if ( !$internalParamMap ) {
5387 $internalParamMap = [];
5388 foreach ( $internalParamNames as $type => $names ) {
5389 foreach ( $names as $name ) {
5390 // For grep: img_left, img_right, img_center, img_none,
5391 // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5392 // img_bottom, img_text_bottom,
5393 // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5394 // img_border, img_link, img_alt, img_class
5395 $magicName = str_replace( '-', '_', "img_$name" );
5396 $internalParamMap[$magicName] = [ $type, $name ];
5397 }
5398 }
5399 }
5400
5401 # Add handler params
5402 # Since img_width is one of these, it is important it is listed
5403 # *after* the literal parameter names above (T372935).
5404 $paramMap = $internalParamMap;
5405 if ( $handler ) {
5406 $handlerParamMap = $handler->getParamMap();
5407 foreach ( $handlerParamMap as $magic => $paramName ) {
5408 $paramMap[$magic] = [ 'handler', $paramName ];
5409 }
5410 } else {
5411 // Parse the size for non-existent files. See T273013
5412 $paramMap[ 'img_width' ] = [ 'handler', 'width' ];
5413 }
5414 $this->mImageParams[$handlerClass] = $paramMap;
5415 $this->mImageParamsMagicArray[$handlerClass] =
5416 $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5417 }
5418 return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5419 }
5420
5429 public function makeImageHtml( LinkTarget $link, string $options ): string {
5430 return $this->makeImageInternal(
5431 $link, $options, shouldReplaceLinkHolders: true
5432 );
5433 }
5434
5449 public function makeImage( LinkTarget $link, $options, $holders = false ): string {
5450 wfDeprecated( __METHOD__, '1.46' ); // warnings since 1.46
5451 return $this->makeImageInternal(
5452 $link, $options, $holders ?: null, shouldReplaceLinkHolders: false
5453 );
5454 }
5455
5465 private function makeImageInternal(
5466 LinkTarget $link,
5467 string $options,
5468 ?LinkHolderArray $holders = null,
5469 bool $shouldReplaceLinkHolders = false
5470 ): string {
5471 # Check if the options text is of the form "options|alt text"
5472 # Options are:
5473 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5474 # * left no resizing, just left align. label is used for alt= only
5475 # * right same, but right aligned
5476 # * none same, but not aligned
5477 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5478 # * center center the image
5479 # * framed Keep original image size, no magnify-button.
5480 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5481 # * upright reduce width for upright images, rounded to full __0 px
5482 # * border draw a 1px border around the image
5483 # * alt Text for HTML alt attribute (defaults to empty)
5484 # * class Set a class for img node
5485 # * link Set the target of the image link. Can be external, interwiki, or local
5486 # vertical-align values (no % or length right now):
5487 # * baseline
5488 # * sub
5489 # * super
5490 # * top
5491 # * text-top
5492 # * middle
5493 # * bottom
5494 # * text-bottom
5495
5496 # Protect LanguageConverter markup when splitting into parts
5497 $parts = StringUtils::delimiterExplode(
5498 '-{', '}-', '|', $options, true /* allow nesting */
5499 );
5500
5501 # Give extensions a chance to select the file revision for us
5502 $options = [];
5503 $descQuery = false;
5504 $title = Title::castFromLinkTarget( $link ); // hook signature compat
5505 $this->hookRunner->onBeforeParserFetchFileAndTitle(
5506 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
5507 $this, $title, $options, $descQuery
5508 );
5509 # Fetch and register the file (file title may be different via hooks)
5510 [ $file, $link ] = $this->fetchFileAndTitle( $link, $options );
5511
5512 # Get parameter map
5513 $handler = $file ? $file->getHandler() : false;
5514
5515 [ $paramMap, $mwArray ] = $this->getImageParams( $handler );
5516
5517 if ( !$file ) {
5518 $this->addTrackingCategory( 'broken-file-category' );
5519 }
5520
5521 # Process the input parameters
5522 $caption = '';
5523 $params = [ 'frame' => [], 'handler' => [],
5524 'horizAlign' => [], 'vertAlign' => [] ];
5525 $seenformat = false;
5526 foreach ( $parts as $part ) {
5527 [ $magicName, $value ] = $mwArray->matchVariableStartToEnd( trim( $part ) );
5528 $validated = false;
5529 if ( isset( $paramMap[$magicName] ) ) {
5530 [ $type, $paramName ] = $paramMap[$magicName];
5531
5532 # Special case; width and height come in one variable together
5533 if ( $type === 'handler' && $paramName === 'width' ) {
5534 // The 'px' suffix has already been localized by img_width
5535 $parsedWidthParam = $this->parseWidthParam( $value, true, true );
5536 // Parsoid applies data-(width|height) attributes to broken
5537 // media spans, for client use. See T273013
5538 $validateFunc = static function ( $name, $value ) use ( $handler ) {
5539 return $handler
5540 ? $handler->validateParam( $name, $value )
5541 : $value > 0;
5542 };
5543 if ( isset( $parsedWidthParam['width'] ) ) {
5544 $width = $parsedWidthParam['width'];
5545 if ( $validateFunc( 'width', $width ) ) {
5546 $params[$type]['width'] = $width;
5547 $validated = true;
5548 }
5549 }
5550 if ( isset( $parsedWidthParam['height'] ) ) {
5551 $height = $parsedWidthParam['height'];
5552 if ( $validateFunc( 'height', $height ) ) {
5553 $params[$type]['height'] = $height;
5554 $validated = true;
5555 }
5556 }
5557 # else no validation -- T15436
5558 } else {
5559 if ( $type === 'handler' ) {
5560 # Validate handler parameter
5561 $validated = $handler->validateParam( $paramName, $value );
5562 } else {
5563 # Validate internal parameters
5564 switch ( $paramName ) {
5565 case 'alt':
5566 case 'class':
5567 $validated = true;
5568 $value = $this->stripAltText( $value, $holders );
5569 break;
5570 case 'link':
5571 [ $paramName, $value ] =
5572 $this->parseLinkParameter(
5573 $this->stripAltText( $value, $holders )
5574 );
5575 if ( $paramName ) {
5576 $validated = true;
5577 if ( $paramName === 'no-link' ) {
5578 $value = true;
5579 }
5580 }
5581 break;
5582 case 'manualthumb':
5583 # @todo FIXME: Possibly check validity here for
5584 # manualthumb? downstream behavior seems odd with
5585 # missing manual thumbs.
5586 $value = $this->stripAltText( $value, $holders );
5587 // fall through
5588 case 'frameless':
5589 case 'framed':
5590 case 'thumbnail':
5591 // use first appearing option, discard others.
5592 $validated = !$seenformat;
5593 $seenformat = true;
5594 break;
5595 default:
5596 # Most other things appear to be empty or numeric...
5597 $validated = ( $value === false || is_numeric( trim( $value ) ) );
5598 }
5599 }
5600
5601 if ( $validated ) {
5602 $params[$type][$paramName] = $value;
5603 }
5604 }
5605 }
5606 if ( !$validated ) {
5607 $caption = $part;
5608 }
5609 }
5610
5611 # Process alignment parameters
5612 if ( $params['horizAlign'] !== [] ) {
5613 $params['frame']['align'] = array_key_first( $params['horizAlign'] );
5614 }
5615 if ( $params['vertAlign'] !== [] ) {
5616 $params['frame']['valign'] = array_key_first( $params['vertAlign'] );
5617 }
5618
5619 $params['frame']['caption'] = $caption;
5620
5621 # Will the image be presented in a frame, with the caption below?
5622 // @phan-suppress-next-line PhanImpossibleCondition
5623 $hasVisibleCaption = isset( $params['frame']['framed'] )
5624 || isset( $params['frame']['thumbnail'] )
5625 || isset( $params['frame']['manualthumb'] );
5626
5627 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5628 # came to also set the caption, ordinary text after the image -- which
5629 # makes no sense, because that just repeats the text multiple times in
5630 # screen readers. It *also* came to set the title attribute.
5631 # Now that we have an alt attribute, we should not set the alt text to
5632 # equal the caption: that's worse than useless, it just repeats the
5633 # text. This is the framed/thumbnail case. If there's no caption, we
5634 # use the unnamed parameter for alt text as well, just for the time be-
5635 # ing, if the unnamed param is set and the alt param is not.
5636 # For the future, we need to figure out if we want to tweak this more,
5637 # e.g., introducing a title= parameter for the title; ignoring the un-
5638 # named parameter entirely for images without a caption; adding an ex-
5639 # plicit caption= parameter and preserving the old magic unnamed para-
5640 # meter for BC; ...
5641
5642 if ( !$hasVisibleCaption ) {
5643 // @phan-suppress-next-line PhanImpossibleCondition
5644 if ( !isset( $params['frame']['alt'] ) && $caption !== '' ) {
5645 # No alt text, use the "caption" for the alt text
5646 $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5647 }
5648 # Use the "caption" for the tooltip text
5649 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5650 }
5651 $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5652
5653 // hook signature compat again, $link may have changed
5654 $title = Title::castFromLinkTarget( $link );
5655 $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5656
5657 # Linker does the rest
5658 $time = $options['time'] ?? false;
5659 $params['handler']['requestProvenance'] = 'parser';
5660 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
5661 $defaultThumbSize = $userOptionsLookup->getDefaultOption( 'thumbsize' );
5662 $ret = Linker::makeImageLink( $this, $link, $file, $params['frame'], $params['handler'],
5663 $time, $descQuery, $defaultThumbSize );
5664
5665 # Give the handler a chance to modify the parser object
5666 if ( $handler ) {
5667 $handler->parserTransformHook( $this, $file );
5668 }
5669 if ( $file ) {
5670 $this->modifyImageHtml( $file, $params, $ret );
5671 }
5672 if ( $shouldReplaceLinkHolders ) {
5673 $this->replaceLinkHoldersPrivate( $ret );
5674 }
5675
5676 return $ret;
5677 }
5678
5697 private function parseLinkParameter( $value ) {
5698 $chars = self::EXT_LINK_URL_CLASS;
5699 $addr = self::EXT_LINK_ADDR;
5700 $prots = $this->urlUtils->validProtocols();
5701 $type = null;
5702 $target = false;
5703 if ( $value === '' ) {
5704 $type = 'no-link';
5705 } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5706 if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value ) ) {
5707 $this->mOutput->addExternalLink( $value );
5708 $type = 'link-url';
5709 $target = $value;
5710 }
5711 } else {
5712 // Percent-decode link arguments for consistency with wikilink
5713 // handling (T216003#7836261).
5714 //
5715 // There's slight concern here though. The |link= option supports
5716 // two formats, link=Test%22test vs link=[[Test%22test]], both of
5717 // which are about to be decoded.
5718 //
5719 // In the former case, the decoding here is straightforward and
5720 // desirable.
5721 //
5722 // In the latter case, there's a potential for double decoding,
5723 // because the wikilink syntax has a higher precedence and has
5724 // already been parsed as a link before we get here. $value
5725 // has had stripAltText() called on it, which in turn calls
5726 // replaceLinkHoldersText() on the link. So, the text we're
5727 // getting at this point has already been percent decoded.
5728 //
5729 // The problematic case is if %25 is in the title, since that
5730 // decodes to %, which could combine with trailing characters.
5731 // However, % is not a valid link title character, so it would
5732 // not parse as a link and the string we received here would
5733 // still contain the encoded %25.
5734 //
5735 // Hence, double decoded is not an issue. See the test,
5736 // "Should not double decode the link option"
5737 if ( str_contains( $value, '%' ) ) {
5738 $value = rawurldecode( $value );
5739 }
5740 $linkTitle = Title::newFromText( $value );
5741 if ( $linkTitle ) {
5742 $this->mOutput->addLink( $linkTitle );
5743 $type = 'link-title';
5744 $target = $linkTitle;
5745 }
5746 }
5747 return [ $type, $target ];
5748 }
5749
5757 public function modifyImageHtml( File $file, array $params, string &$html ) {
5758 $this->hookRunner->onParserModifyImageHTML( $this, $file, $params, $html );
5759 }
5760
5761 private function stripAltText( string $caption, ?LinkHolderArray $holders = null ): string {
5762 # Strip bad stuff out of the title (tooltip). We can't just use
5763 # replaceLinkHoldersText() here, because if this function is called
5764 # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5765 if ( $holders !== null ) {
5766 $tooltip = $holders->replaceText( $caption );
5767 } else {
5768 $tooltip = $this->replaceLinkHoldersText( $caption );
5769 }
5770
5771 # make sure there are no placeholders in thumbnail attributes
5772 # that are later expanded to html- so expand them now and
5773 # remove the tags
5774 $tooltip = $this->mStripState->unstripBoth( $tooltip );
5775 # Compatibility hack! In HTML certain entity references not terminated
5776 # by a semicolon are decoded (but not if we're in an attribute; that's
5777 # how link URLs get away without properly escaping & in queries).
5778 # But wikitext has always required semicolon-termination of entities,
5779 # so encode & where needed to avoid decode of semicolon-less entities.
5780 # See T209236 and
5781 # https://www.w3.org/TR/html5/syntax.html#named-character-references
5782 # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5783 $tooltip = preg_replace( "/
5784 & # 1. entity prefix
5785 (?= # 2. followed by:
5786 (?: # a. one of the legacy semicolon-less named entities
5787 A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5788 C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5789 GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5790 O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5791 U(?:acute|circ|grave|uml)|Yacute|
5792 a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5793 c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5794 divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5795 frac(?:1(?:2|4)|34)|
5796 gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5797 i(?:acute|circ|excl|grave|quest|uml)|laquo|
5798 lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5799 m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5800 not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5801 o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5802 p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5803 s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5804 u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5805 )
5806 (?:[^;]|$)) # b. and not followed by a semicolon
5807 # S = study, for efficiency
5808 /Sx", '&amp;', $tooltip );
5809 $tooltip = Sanitizer::stripAllTags( $tooltip );
5810
5811 return $tooltip;
5812 }
5813
5820 public function getTags(): array {
5821 return array_keys( $this->mTagHooks );
5822 }
5823
5828 public function getFunctionSynonyms() {
5829 return $this->mFunctionSynonyms;
5830 }
5831
5836 public function getUrlProtocols(): string {
5837 return $this->urlUtils->validProtocols();
5838 }
5839
5871 private function extractSections(
5872 string $text, string|int $sectionId, string $mode,
5873 string|false $newText, ?PageReference $page = null
5874 ): string|false {
5875 $magicScopeVariable = $this->lock();
5876 $this->startParse(
5877 $page,
5878 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
5879 self::OT_PLAIN,
5880 true
5881 );
5882 $outText = '';
5883 $frame = $this->getPreprocessor()->newFrame();
5884
5885 # Process section extraction flags
5886 $flags = 0;
5887 $sectionParts = explode( '-', $sectionId );
5888 // The section ID may either be a magic string such as 'new' (which should be treated as 0),
5889 // or a numbered section ID in the format of "T-<section index>".
5890 // Explicitly coerce the section index into a number accordingly. (T323373)
5891 $sectionIndex = (int)array_pop( $sectionParts );
5892 foreach ( $sectionParts as $part ) {
5893 if ( $part === 'T' ) {
5894 $flags |= Preprocessor::DOM_FOR_INCLUSION;
5895 }
5896 }
5897
5898 # Check for empty input
5899 if ( $text === '' ) {
5900 # Only sections 0 and T-0 exist in an empty document
5901 if ( $sectionIndex === 0 ) {
5902 return $mode === 'get' ? '' : $newText;
5903 } else {
5904 return $mode === 'get' ? $newText : $text;
5905 }
5906 }
5907
5908 # Preprocess the text
5909 $root = $this->preprocessToDom( $text, $flags );
5910
5911 # <h> nodes indicate section breaks
5912 # They can only occur at the top level, so we can find them by iterating the root's children
5913 $node = $root->getFirstChild();
5914
5915 # Find the target section
5916 if ( $sectionIndex === 0 ) {
5917 # Section zero doesn't nest, level=big
5918 $targetLevel = 1000;
5919 } else {
5920 while ( $node ) {
5921 if ( $node->getName() === 'h' ) {
5922 $bits = $node->splitHeading();
5923 if ( $bits['i'] == $sectionIndex ) {
5924 $targetLevel = $bits['level'];
5925 break;
5926 }
5927 }
5928 if ( $mode === 'replace' ) {
5929 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5930 }
5931 $node = $node->getNextSibling();
5932 }
5933 }
5934
5935 if ( !$node ) {
5936 # Not found
5937 return $mode === 'get' ? $newText : $text;
5938 }
5939
5940 # Find the end of the section, including nested sections
5941 do {
5942 if ( $node->getName() === 'h' ) {
5943 $bits = $node->splitHeading();
5944 $curLevel = $bits['level'];
5945 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
5946 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5947 break;
5948 }
5949 }
5950 if ( $mode === 'get' ) {
5951 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5952 }
5953 $node = $node->getNextSibling();
5954 } while ( $node );
5955
5956 # Write out the remainder (in replace mode only)
5957 if ( $mode === 'replace' ) {
5958 # Output the replacement text.
5959 # Add two newlines. Trailing whitespace in $newText is conventionally
5960 # stripped by the editor, so we need both newlines to restore the paragraph gap.
5961 # Only add trailing whitespace if there is newText.
5962 if ( $newText != "" ) {
5963 $outText .= $newText . "\n\n";
5964 }
5965
5966 while ( $node ) {
5967 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5968 $node = $node->getNextSibling();
5969 }
5970 }
5971
5972 # Re-insert stripped tags
5973 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5974
5975 return $outText;
5976 }
5977
5994 public function getSection( $text, $sectionId, $defaultText = '' ): string|false {
5995 return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5996 }
5997
6011 public function replaceSection( $oldText, $sectionId, $newText ): string|false {
6012 return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
6013 }
6014
6044 public function getFlatSectionInfo( $text ) {
6045 $magicScopeVariable = $this->lock();
6046 $this->startParse(
6047 null,
6048 ParserOptions::newFromUser( RequestContext::getMain()->getUser() ),
6049 self::OT_PLAIN,
6050 true
6051 );
6052 $frame = $this->getPreprocessor()->newFrame();
6053 $root = $this->preprocessToDom( $text, 0 );
6054 $node = $root->getFirstChild();
6055 $offset = 0;
6056 $currentSection = [
6057 'index' => 0,
6058 'level' => 0,
6059 'offset' => 0,
6060 'heading' => '',
6061 'text' => ''
6062 ];
6063 $sections = [];
6064
6065 while ( $node ) {
6066 $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
6067 if ( $node->getName() === 'h' ) {
6068 $bits = $node->splitHeading();
6069 $sections[] = $currentSection;
6070 $currentSection = [
6071 'index' => $bits['i'],
6072 'level' => $bits['level'],
6073 'offset' => $offset,
6074 'heading' => $nodeText,
6075 'text' => $nodeText
6076 ];
6077 } else {
6078 $currentSection['text'] .= $nodeText;
6079 }
6080 $offset += strlen( $nodeText );
6081 $node = $node->getNextSibling();
6082 }
6083 $sections[] = $currentSection;
6084 return $sections;
6085 }
6086
6098 public function getRevisionId() {
6099 return $this->mRevisionId;
6100 }
6101
6108 public function getRevisionRecordObject() {
6109 if ( $this->mRevisionRecordObject ) {
6110 return $this->mRevisionRecordObject;
6111 }
6112 if ( $this->mOptions->isMessage() ) {
6113 return null;
6114 }
6115
6116 // NOTE: try to get the RevisionRecord object even if mRevisionId is null.
6117 // This is useful when parsing a revision that has not yet been saved.
6118 // However, if we get back a saved revision even though we are in
6119 // preview mode, we'll have to ignore it, see below.
6120 // NOTE: This callback may be used to inject an OLD revision that was
6121 // already loaded, so "current" is a bit of a misnomer. We can't just
6122 // skip it if mRevisionId is set.
6123 $rev = $this->mOptions->getCurrentRevisionRecordCallback()(
6124 $this->getTitle(),
6125 $this
6126 );
6127
6128 if ( !$rev ) {
6129 // The revision record callback returns `false` (not null) to
6130 // indicate that the revision is missing. (See for example
6131 // Parser::defaultFetchRevisionRecord(), the default callback.)
6132 // This API expects `null` instead. (T251952)
6133 return null;
6134 }
6135
6136 if ( $this->mRevisionId === null && $rev->getId() ) {
6137 // We are in preview mode (mRevisionId is null), and the current revision callback
6138 // returned an existing revision. Ignore it and return null, it's probably the page's
6139 // current revision, which is not what we want here. Note that we do want to call the
6140 // callback to allow the unsaved revision to be injected here, e.g. for
6141 // self-transclusion previews.
6142 return null;
6143 }
6144
6145 // If the parse is for a new revision, then the callback should have
6146 // already been set to force the object and should match mRevisionId.
6147 // If not, try to fetch by mRevisionId instead.
6148 if ( $this->mRevisionId && $rev->getId() != $this->mRevisionId ) {
6149 $rev = MediaWikiServices::getInstance()
6150 ->getRevisionLookup()
6151 ->getRevisionById( $this->mRevisionId );
6152 }
6153
6154 $this->mRevisionRecordObject = $rev;
6155
6156 return $this->mRevisionRecordObject;
6157 }
6158
6165 public function getRevisionTimestamp(): string {
6166 if ( $this->mRevisionTimestamp !== null ) {
6167 return $this->mRevisionTimestamp;
6168 }
6169
6170 # Use specified revision timestamp, falling back to the current timestamp
6171 $revObject = $this->getRevisionRecordObject();
6172 $timestamp = $revObject && $revObject->getTimestamp()
6173 ? $revObject->getTimestamp()
6174 : $this->mOptions->getTimestamp();
6175 $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6176
6177 # The cryptic '' timezone parameter tells to use the site-default
6178 # timezone offset instead of the user settings.
6179 # Since this value will be saved into the parser cache, served
6180 # to other users, and potentially even used inside links and such,
6181 # it needs to be consistent for all visitors.
6182 $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6183
6184 return $this->mRevisionTimestamp;
6185 }
6186
6193 public function getRevisionUser(): ?string {
6194 if ( $this->mRevisionUser === null ) {
6195 $revObject = $this->getRevisionRecordObject();
6196
6197 # if this template is subst: the revision id will be blank,
6198 # so just use the current user's name
6199 if ( $revObject && $revObject->getUser() ) {
6200 $this->mRevisionUser = $revObject->getUser()->getName();
6201 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6202 $this->mRevisionUser = $this->getUserIdentity()->getName();
6203 } else {
6204 # Note that we fall through here with
6205 # $this->mRevisionUser still null
6206 }
6207 }
6208 return $this->mRevisionUser;
6209 }
6210
6217 public function getRevisionSize() {
6218 if ( $this->mRevisionSize === null ) {
6219 $revObject = $this->getRevisionRecordObject();
6220
6221 # if this variable is subst: the revision id will be blank,
6222 # so just use the parser input size, because the own substitution
6223 # will change the size.
6224 $this->mRevisionSize = $revObject ? $revObject->getSize() : $this->mInputSize;
6225 }
6226 return $this->mRevisionSize;
6227 }
6228
6229 private static function getSectionNameFromStrippedText( string $text ): string {
6230 $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6231 $text = Sanitizer::decodeCharReferences( $text );
6232 $text = self::normalizeSectionName( $text );
6233 return $text;
6234 }
6235
6236 private static function makeAnchor( string $sectionName ): string {
6237 return '#' . Sanitizer::escapeIdForLink( $sectionName );
6238 }
6239
6240 private function makeLegacyAnchor( string $sectionName ): string {
6241 $fragmentMode = $this->svcOptions->get( MainConfigNames::FragmentMode );
6242 if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6243 // ForAttribute() and ForLink() are the same for legacy encoding
6244 $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6245 } else {
6246 $id = Sanitizer::escapeIdForLink( $sectionName );
6247 }
6248
6249 return "#$id";
6250 }
6251
6261 public function guessSectionNameFromWikiText( $text ): string {
6262 # Strip out wikitext links(they break the anchor)
6263 $text = $this->stripSectionName( $text );
6264 $sectionName = self::getSectionNameFromStrippedText( $text );
6265 return self::makeAnchor( $sectionName );
6266 }
6267
6279 public function guessLegacySectionNameFromWikiText( $text ): string {
6280 wfDeprecated( __METHOD__, '1.45' ); // warnings since 1.45
6281 # Strip out wikitext links(they break the anchor)
6282 $text = $this->stripSectionName( $text );
6283 $sectionName = self::getSectionNameFromStrippedText( $text );
6284 return $this->makeLegacyAnchor( $sectionName );
6285 }
6286
6293 public static function guessSectionNameFromStrippedText( $text ): string {
6294 $sectionName = self::getSectionNameFromStrippedText( $text );
6295 return self::makeAnchor( $sectionName );
6296 }
6297
6304 private static function normalizeSectionName( string $text ): string {
6305 # T90902: ensure the same normalization is applied for IDs as to links
6306 $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6307 try {
6308 $parts = $titleParser->splitTitleString( "#$text" );
6309 } catch ( MalformedTitleException ) {
6310 return $text;
6311 }
6312 return $parts['fragment'];
6313 }
6314
6330 public function stripSectionName( $text ): string {
6331 # Strip internal link markup
6332 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6333 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6334
6335 # Strip external link markup
6336 # @todo FIXME: Not tolerant to blank link text
6337 # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6338 # on how many empty links there are on the page - need to figure that out.
6339 $text = preg_replace(
6340 '/\[(?i:' . $this->urlUtils->validProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6341
6342 # Parse wikitext quotes (italics & bold)
6343 $text = $this->doQuotes( $text );
6344
6345 # Strip HTML tags
6346 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6347 return $text;
6348 }
6349
6368 public function markerSkipCallback( $s, callable $callback ): string {
6369 $i = 0;
6370 $out = '';
6371 while ( $i < strlen( $s ) ) {
6372 $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6373 if ( $markerStart === false ) {
6374 $out .= $callback( substr( $s, $i ) );
6375 break;
6376 } else {
6377 $out .= $callback( substr( $s, $i, $markerStart - $i ) );
6378 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6379 if ( $markerEnd === false ) {
6380 $out .= substr( $s, $markerStart );
6381 break;
6382 } else {
6383 $markerEnd += strlen( self::MARKER_SUFFIX );
6384 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6385 $i = $markerEnd;
6386 }
6387 }
6388 }
6389 return $out;
6390 }
6391
6399 public function killMarkers( $text ): string {
6400 return $this->mStripState->killMarkers( $text );
6401 }
6402
6416 public function parseWidthParam( $value, $parseHeight = true, bool $localized = false ) {
6417 $parsedWidthParam = [];
6418 if ( $value === '' ) {
6419 return $parsedWidthParam;
6420 }
6421 $m = [];
6422 if ( !$localized ) {
6423 // Strip a localized 'px' suffix (T374311)
6424 $mwArray = $this->magicWordFactory->newArray( [ 'img_width' ] );
6425 [ $magicWord, $newValue ] = $mwArray->matchVariableStartToEnd( $value );
6426 $value = $magicWord ? $newValue : $value;
6427 }
6428
6429 # (T15500) In both cases (width/height and width only),
6430 # permit trailing "px" for backward compatibility.
6431 if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(px)?\s*$/', $value, $m ) ) {
6432 $parsedWidthParam['width'] = intval( $m[1] );
6433 $parsedWidthParam['height'] = intval( $m[2] );
6434 if ( $m[3] ?? false ) {
6435 $this->addTrackingCategory( 'double-px-category' );
6436 }
6437 } elseif ( preg_match( '/^([0-9]*)\s*(px)?\s*$/', $value, $m ) ) {
6438 $parsedWidthParam['width'] = intval( $m[1] );
6439 if ( $m[2] ?? false ) {
6440 $this->addTrackingCategory( 'double-px-category' );
6441 }
6442 }
6443 return $parsedWidthParam;
6444 }
6445
6452 #[\NoDiscard]
6453 protected function lock(): ScopedCallback {
6454 if ( $this->mInParse ) {
6455 $message = 'Parser state cleared while parsing. Did you call Parser::parse recursively?';
6456 $xdebugMode = ini_get( 'xdebug.mode' );
6457 if ( $xdebugMode !== false && str_contains( $xdebugMode, 'develop' ) ) {
6458 $message .= PHP_EOL . 'xdebug.mode=develop is known to cause this issue ' .
6459 '(xdebug bug #2222); consider ';
6460 $improvedMode = implode( ',',
6461 array_diff( explode( ',', $xdebugMode ),
6462 [ 'develop' ] ) );
6463 if ( $improvedMode !== '' ) {
6464 $message .= "using xdebug.mode=$improvedMode instead or ";
6465 }
6466 $message .= "disabling xdebug.";
6467 }
6468 $message .= PHP_EOL . 'lock is held by: ' . $this->mInParse;
6469 throw new LogicException( $message );
6470 }
6471
6472 // Save the backtrace when locking, so that if some code tries locking again,
6473 // we can print the lock owner's backtrace for easier debugging
6474 $e = new RuntimeException;
6475 $this->mInParse = $e->getTraceAsString();
6476
6477 $recursiveCheck = new ScopedCallback( function () {
6478 $this->mInParse = false;
6479 } );
6480
6481 return $recursiveCheck;
6482 }
6483
6491 public function isLocked() {
6492 return (bool)$this->mInParse;
6493 }
6494
6505 public static function stripOuterParagraph( $html ): string {
6506 $m = [];
6507 if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && !str_contains( $m[1], '</p>' ) ) {
6508 $html = $m[1];
6509 }
6510
6511 return $html;
6512 }
6513
6527 public function splitDisplayTitle(
6528 HtmlArmor $displayTitle, ?ILanguageConverter $languageConverter = null
6529 ): array {
6530 return self::splitPageTitle(
6531 $displayTitle,
6532 $this->getPage(),
6533 $languageConverter ?? $this->getContentLanguageConverter()
6534 );
6535 }
6536
6564 public static function splitPageTitle(
6565 HtmlArmor $pageTitle, LinkTarget|PageReference $page, ILanguageConverter $languageConverter
6566 ): array {
6567 // Document/enforce that $pageTitle is "safe HTML"
6568 $pageTitle = HtmlArmor::getHtml( $pageTitle );
6569
6570 // Is this is the output of ::formatPageTitle()?
6571 $parts = self::unformatPageTitle( $pageTitle );
6572 if ( $parts !== null ) {
6573 [ $nsText, $nsSeparator, $mainText ] = $parts;
6574 } else {
6575 // See if the plain text begins with this page's localized
6576 // namespace prefix (e.g. "Talk:Foo"). Use ::convertSplitTitle()
6577 // so that gender-distinct namespace aliases (T425402) are matched
6578 [ $expectedNsText, $expectedNsSeparator ] =
6579 $languageConverter->convertSplitTitle( $page );
6580 $prefixRegexp = '/^(' . preg_quote( $expectedNsText, '/' ) . ')(' .
6581 preg_quote( $expectedNsSeparator, '/' ) . ')/i';
6582 $matches = [];
6583 if ( $expectedNsText !== '' && preg_match( $prefixRegexp, $pageTitle, $matches ) ) {
6584 // Preserve the casing the user actually typed, both so the
6585 // namespace <span> reflects their input and so rejoining
6586 // the parts reproduces $pageTitle exactly.
6587 $nsText = $matches[1];
6588 $nsSeparator = $matches[2];
6589 $mainText = substr( $pageTitle, strlen( $matches[0] ) );
6590 } else {
6591 // We can't confidently split off a namespace; treat the
6592 // whole string as the main title and don't add a
6593 // namespace <span> at all (T314399).
6594 $nsText = '';
6595 $nsSeparator = ':';
6596 $mainText = $pageTitle;
6597 }
6598 }
6599
6600 // Wrap in HtmlArmor to document that the resulting pieces are HTML
6601 // not plaintext
6602 return [
6603 new HtmlArmor( $nsText ),
6604 new HtmlArmor( $nsSeparator ),
6605 new HtmlArmor( $mainText ),
6606 ];
6607 }
6608
6621 public static function formatPageTitle( $nsText, $nsSeparator, $mainText, ?Language $titleLang = null ): string {
6622 $html = '';
6623 if ( HtmlArmor::getHtml( $nsText ) !== '' ) {
6624 $html .= '<span class="mw-page-title-namespace">' . HtmlArmor::getHtml( $nsText ) . '</span>';
6625 $html .= '<span class="mw-page-title-separator">' . HtmlArmor::getHtml( $nsSeparator ) . '</span>';
6626 }
6627 $html .= '<span class="mw-page-title-main">' . HtmlArmor::getHtml( $mainText ) . '</span>';
6628 if ( $titleLang !== null ) {
6629 $html = Html::rawElement( 'span', [
6630 'lang' => $titleLang->getHtmlCode(),
6631 'dir' => $titleLang->getDir(),
6632 ], $html );
6633 }
6634 return $html;
6635 }
6636
6642 private const PAGE_TITLE_PLACEHOLDER = 'x-mw-page-title-part-x';
6643
6654 private static function unformatPageTitle( string $html ): ?array {
6655 static $nsPageTitleRegexp = null;
6656 static $mainNsPageTitleRegexp = null;
6657
6658 // Create a regexp with matching groups as placeholders for the
6659 // namespace, separator and main text
6660 $nsPageTitleRegexp ??= '/' . str_replace(
6661 preg_quote( '(.+?)', '/' ),
6662 '(.+?)',
6663 preg_quote( self::formatPageTitle( '(.+?)', '(.+?)', '(.+?)' ), '/' )
6664 ) . '/';
6665 // In the main namespace there's no namespace or separator span
6666 $mainNsPageTitleRegexp ??= '/' . str_replace(
6667 preg_quote( '(.+?)', '/' ),
6668 '(.+?)',
6669 preg_quote( self::formatPageTitle( '', '', '(.+?)' ), '/' )
6670 ) . '/';
6671
6672 $matches = [];
6673 if ( preg_match( $nsPageTitleRegexp, $html, $matches ) ) {
6674 $parts = [ $matches[1], $matches[2], $matches[3] ];
6675 } elseif ( preg_match( $mainNsPageTitleRegexp, $html, $matches ) ) {
6676 $parts = [ '', ':', $matches[1] ];
6677 } elseif ( preg_match( '/^<span lang="[^"<>]*" dir="[^"<>]*">(.*)<\/span>$/s', $html, $matches ) ) {
6678 // ::formatPageTitle() wraps its output in a <span> carrying the
6679 // title language when it is given one; retry without it. (The
6680 // wrapper itself is dropped: the parts are language-neutral.)
6681 return self::unformatPageTitle( $matches[1] );
6682 } else {
6683 // Perhaps this wasn't the output of ::formatPageTitle()
6684 return null;
6685 }
6686 // The regexps above could be manipulated by malicious user input,
6687 // sanitize the results just in case
6688 return array_map( Sanitizer::removeSomeTags( ... ), $parts );
6689 }
6690
6697 public static function extractBody( string $text ): string {
6698 $posStart = strpos( $text, '<body' );
6699 if ( $posStart === false ) {
6700 return $text;
6701 }
6702 $posStart = strpos( $text, '>', $posStart );
6703 if ( $posStart === false ) {
6704 return $text;
6705 }
6706 // Skip past the > character
6707 $posStart += 1;
6708 $posEnd = strrpos( $text, '</body>', $posStart );
6709 if ( $posEnd === false ) {
6710 // Strip <body> wrapper even if input was truncated (i.e. missing close tag)
6711 return substr( $text, $posStart );
6712 } else {
6713 return substr( $text, $posStart, $posEnd - $posStart );
6714 }
6715 }
6716
6723 private function setOutputFlag( ParserOutputFlags|string $flag, string $reason ): void {
6724 $this->mOutput->setOutputFlag( $flag );
6725 if ( $flag instanceof ParserOutputFlags ) {
6726 // Convert enumeration to string for logging.
6727 $flag = $flag->value;
6728 }
6729 $name = $this->getTitle()->getPrefixedText();
6730 $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6731 }
6732}
6733
6735class_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:52
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:6279
$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:3357
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition Parser.php:1558
setOutputType( $ot)
Mutator for the output type.
Definition Parser.php:1026
getParseTime()
Return the parse time.
Definition Parser.php:566
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition Parser.php:1160
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition Parser.php:6330
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:2917
makeImage(LinkTarget $link, $options, $holders=false)
Parse image options text and use it to make an image.
Definition Parser.php:5449
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:6293
static statelessFetchTemplate( $page, $parser=false)
Definition Parser.php:3800
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition Parser.php:6368
getPreloadText( $text, PageReference $page, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition Parser.php:925
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:3452
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition Parser.php:1125
__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, private LinkAlwaysKnownLookup $linkAlwaysKnownLookup,)
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:6621
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:597
tagNeedsNowikiStrippedInTagPF(string $lowerTagName)
Definition Parser.php:3979
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition Parser.php:1140
lock()
Lock the current instance of the parser.
Definition Parser.php:6453
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition Parser.php:5125
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:3555
preprocess( $text, ?PageReference $page, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition Parser.php:878
firstCallInit()
Used to do various kinds of initialisation on the first call of the parser.
Definition Parser.php:497
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition Parser.php:6261
replaceVariables( $text, $frame=false, $argsOnly=false, array $options=[])
Replace magic variables, templates, and template arguments with the appropriate text.
Definition Parser.php:2851
interwikiTransclude(LinkTarget $link, $action)
Transclude an interwiki link.
Definition Parser.php:3870
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition Parser.php:4857
isCurrentRevisionOfTitleCached(LinkTarget $link)
Definition Parser.php:3537
getRevisionId()
Get the ID of the revision we are parsing.
Definition Parser.php:6098
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:859
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition Parser.php:5198
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition Parser.php:3942
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition Parser.php:6011
transformMsg( $text, ParserOptions $options, ?PageReference $page=null)
Wrapper for preprocess()
Definition Parser.php:5020
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:1267
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition Parser.php:1490
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition Parser.php:2237
static getExternalLinkRel( $url=false, $title=null)
Get the rel attribute for a particular external link.
Definition Parser.php:2194
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:1183
splitDisplayTitle(HtmlArmor $displayTitle, ?ILanguageConverter $languageConverter=null)
Split a user-supplied display title for the page being parsed into namespace, separator,...
Definition Parser.php:6527
static statelessFetchRevisionRecord(LinkTarget $link, $parser=null)
Definition Parser.php:3580
getHookRunner()
Get a HookRunner for calling core hooks.
Definition Parser.php:1608
getContentLanguage()
Get the content language that this Parser is using.
Definition Parser.php:1150
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:3651
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition Parser.php:2211
parseWidthParam( $value, $parseHeight=true, bool $localized=false)
Parsed a width param of imagelink like 300px or 200x300px.
Definition Parser.php:6416
setPage(?PageReference $t=null)
Set the page used as context for parsing, e.g.
Definition Parser.php:977
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition Parser.php:1058
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:4658
killMarkers( $text)
Remove any strip markers found in the given text.
Definition Parser.php:6399
const OT_PREPROCESS
Output type: like Parser::preprocess()
Definition Parser.php:181
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition Parser.php:4882
isLocked()
Will entry points such as parse() throw an exception due to the parser already being active?
Definition Parser.php:6491
getRevisionUser()
Get the name of the user that edited the last revision.
Definition Parser.php:6193
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition Parser.php:6044
getTargetLanguage()
Get the target language for the content being parsed.
Definition Parser.php:1086
clearState()
Clear Parser state.
Definition Parser.php:509
getFunctionHooks()
Get all registered function hook identifiers.
Definition Parser.php:5159
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition Parser.php:2942
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition Parser.php:3999
getUserIdentity()
Get a user either from the user set on Parser if it's set, or from the ParserOptions object otherwise...
Definition Parser.php:1105
makeImageHtml(LinkTarget $link, string $options)
Parse image options text and use it to make an image.
Definition Parser.php:5429
setUseParsoidFragments(bool $val)
Definition Parser.php:2881
makeLimitReport(ParserOptions $parserOptions, ParserOutput $parserOutput)
Set the limit report data in the current ParserOutput.
Definition Parser.php:701
msg( $key,... $params)
Helper function to correctly set the target language and title of a message based on the parser conte...
Definition Parser.php:4219
setUser(?UserIdentity $user)
Set the current user.
Definition Parser.php:947
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition Parser.php:1580
getOutputType()
Accessor for the output type.
Definition Parser.php:1017
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition Parser.php:905
setTitle(?Title $t=null)
Set the context title.
Definition Parser.php:958
getRevisionSize()
Get the size of the revision.
Definition Parser.php:6217
getPreprocessor()
Get a preprocessor object.
Definition Parser.php:1115
getStripList()
Get a list of strippable XML-like elements.
Definition Parser.php:1246
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition Parser.php:5054
preprocessToDom( $text, $flags=0)
Get the document object model for the given wikitext.
Definition Parser.php:2821
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition Parser.php:5994
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:3595
static stripOuterParagraph( $html)
Strip outer.
Definition Parser.php:6505
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition Parser.php:6108
clearTagHooks()
Remove all tag hooks.
Definition Parser.php:5072
modifyImageHtml(File $file, array $params, string &$html)
Give hooks a chance to modify image thumbnail HTML.
Definition Parser.php:5757
static extractBody(string $text)
Strip everything but the <body> from the provided string.
Definition Parser.php:6697
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition Parser.php:6165
__clone()
Allow extensions to clean up when the parser is cloned.
Definition Parser.php:481
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition Parser.php:4924
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition Parser.php:466
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition Parser.php:809
static splitPageTitle(HtmlArmor $pageTitle, LinkTarget|PageReference $page, ILanguageConverter $languageConverter)
Split a page title into namespace, separator, and main title portions, so that they can be styled ind...
Definition Parser.php:6564
doQuotes( $text)
Helper function for handleAllQuotes()
Definition Parser.php:1923
static replaceTableOfContentsMarker( $text, $toc)
Replace table of contents marker in parsed HTML.
Definition Parser.php:4945
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:4615
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition Parser.php:833
fetchFileNoRegister(LinkTarget $link, array $options=[])
Helper function for fetchFileAndTitle.
Definition Parser.php:3847
getPage()
Returns the page used as context for parsing, e.g.
Definition Parser.php:1000
fetchFileAndTitle(LinkTarget $link, array $options=[])
Fetch a file and its title and register a reference to it.
Definition Parser.php:3820
fetchCurrentRevisionRecordOfTitle(LinkTarget $link)
Fetch the current revision of a given title as a RevisionRecord.
Definition Parser.php:3508
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:4985
resetOutput()
Reset the ParserOutput.
Definition Parser.php:554
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.
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
Create User objects.
UserNameUtils service.
User class for the MediaWiki software.
Definition User.php:129
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
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, 'EnableChunkedUploads'=> 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'=> '', 'SharedUploadDBschema'=> null, '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'=> true, '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 -l $lang -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'MaxAnimatedWebPArea'=> 12500000, 'WebPThumbnailType'=>['webp', 'image/webp',], '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'=>[], 'RemoteVirtualDomainsMapping'=>[], '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,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'SplitParsoidParserCache'=> false, '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, '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, 'PageShareSkinsEnabled'=>[], '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, ], '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', 'editmywatchlist' => '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', 'editmywatchlist' => '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', 'managesessions' => '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, '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' => [ 'class' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'needsPage' => false, ], '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', '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' => [ ], 'RestLocalModuleTestBaseUrl' => null, 'RestModuleOverrides' => [ ], 'RestExternalModules' => [ ], 'RestTermsOfServiceUrl' => null, '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, 'HTTPUserAgentContact' => false, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'GenerateReqIDFormat' => 'rand24', 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => 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' => [ ], 'UseLeximorph' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, 'ReturnExperimentalPFragmentTypes' => [ ], 'UseParsoidLinksUpdate' => null, 'UseParsoidMessages' => false, 'UseParsoidParser' => true, 'SiteLookup' => [ ], ], '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', 'SharedUploadDBschema' => [ 'string', 'null', ], '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', ], 'WebPThumbnailType' => 'array', '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', 'RemoteVirtualDomainsMapping' => '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', 'SplitParsoidParserCache' => 'boolean', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => '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', 'PageShareSkinsEnabled' => 'array', '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', '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', 'RestLocalModuleTestBaseUrl' => [ 'string', 'null', ], 'RestModuleOverrides' => 'object', 'RestExternalModules' => 'object', 'RestTermsOfServiceUrl' => [ 'string', 'null', ], 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'HTTPUserAgentContact' => [ 'string', 'boolean', ], 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'GenerateReqIDFormat' => 'string', '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', 'UseLeximorph' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', 'ReturnExperimentalPFragmentTypes' => 'array', 'UseParsoidLinksUpdate' => [ 'boolean', 'null', ], 'UseParsoidMessages' => [ 'boolean', 'null', ], 'UseParsoidParser' => 'boolean', 'SiteLookup' => 'object', ], 'mergeStrategy' => [ 'WebPThumbnailType' => 'replace', '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', ], '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', ], ], ], ], ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => [ 'integer', 'array', ], ], ], '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', ], 'skipRedirects' => [ 'type' => 'bool', ], ], ], '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', ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'availability' => [ 'type' => 'string', ], 'groups' => [ 'type' => 'array', ], ], ], ], '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.
convertSplitTitle( $title, ?string $variant=null)
Automatically converts a LinkTarget or PageReference to a readable string in the preferred variant,...
getPreferredVariant()
Get preferred language variant.
convertTo( $text, $variant, bool $clearState=true)
Same as convert() except a extra parameter to custom variant.
Interface for localizing messages in MediaWiki.
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.