MediaWiki  master
Parser.php
Go to the documentation of this file.
1 <?php
39 use Psr\Log\LoggerInterface;
40 use Psr\Log\NullLogger;
41 use Wikimedia\IPUtils;
42 use Wikimedia\ScopedCallback;
43 
84 class Parser {
90  public const VERSION = '1.6.4';
91 
92  # Flags for Parser::setFunctionHook
93  public const SFH_NO_HASH = 1;
94  public const SFH_OBJECT_ARGS = 2;
95 
96  # Constants needed for external link processing
97  # Everything except bracket, space, or control characters
98  # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
99  # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
100  # \x{FFFD} is the Unicode replacement character, which the HTML5 spec
101  # uses to replace invalid HTML characters.
102  public const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
103  # Simplified expression to match an IPv4 or IPv6 address, or
104  # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
105  // phpcs:ignore Generic.Files.LineLength
106  private const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
107  # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
108  // phpcs:ignore Generic.Files.LineLength
109  private const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
110  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
111 
112  # Regular expression for a non-newline space
113  private const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
114 
115  # Flags for preprocessToDom
116  public const PTD_FOR_INCLUSION = 1;
117 
118  # Allowed values for $this->mOutputType
119  # Parameter to startExternalParse().
120  public const OT_HTML = 1; # like parse()
121  public const OT_WIKI = 2; # like preSaveTransform()
122  public const OT_PREPROCESS = 3; # like preprocess()
123  public const OT_MSG = 3;
124  # like extractSections() - portions of the original are returned unchanged.
125  public const OT_PLAIN = 4;
126 
144  public const MARKER_SUFFIX = "-QINU`\"'\x7f";
145  public const MARKER_PREFIX = "\x7f'\"`UNIQ-";
146 
147  # Markers used for wrapping the table of contents
148  public const TOC_START = '<mw:toc>';
149  public const TOC_END = '</mw:toc>';
150 
151  # Persistent:
152 
153  public $mTagHooks = [];
155  public $mFunctionHooks = [];
156  private $mFunctionSynonyms = [ 0 => [], 1 => [] ];
157  private $mStripList = [];
158  private $mVarCache = [];
159  private $mImageParams = [];
162  public $mMarkerIndex = 0;
167  public $mFirstCall = true;
168 
169  # Initialised by initializeVariables()
170 
174  private $mVariables;
175 
179  private $mSubstWords;
180 
185  private $mConf;
186 
187  # Initialised in constructor
189 
190  # Initialized in getPreprocessor()
191 
196 
197  # Cleared with clearState():
198 
202  public $mOutput;
203  private $mAutonumber;
204 
209  public $mStripState;
210 
214  private $mLinkHolders;
215 
220  public $mLinkID;
232  private $mDefaultSort;
235  public $mHeadings;
239  public $mExpensiveFunctionCount; # number of expensive parser function calls
241  public $mShowToc;
244  private $mTplDomCache;
245 
250  public $mUser; # User object; only used when doing pre-save transform
251 
252  # Temporary
253  # These are variables reset at least once per parse regardless of $clearState
254 
259  public $mOptions;
260 
268  public $mTitle; # Title context, used for self-link rendering and similar things
269  private $mOutputType; # Output type, one of the OT_xxx constants
271  public $ot; # Shortcut alias, see setOutputType()
273  public $mRevisionObject; # The revision object of the specified revision ID
274 
275  public $mRevisionId; # ID to display in {{REVISIONID}} tags
277  public $mRevisionTimestamp; # The timestamp of the specified revision ID
279  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
281  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
283  public $mInputSize = false; # For {{PAGESIZE}} on current page.
284 
287 
294 
302 
309  public $mInParse = false;
310 
312  private $mProfiler;
313 
317  private $mLinkRenderer;
318 
321 
323  private $contLang;
324 
327 
329  private $factory;
330 
333 
341  private $svcOptions;
342 
345 
347  private $nsInfo;
348 
350  private $logger;
351 
353  private $badFileLookup;
354 
356  private $hookContainer;
357 
359  private $hookRunner;
360 
364  public const CONSTRUCTOR_OPTIONS = [
365  // Deprecated and unused; from $wgParserConf
366  'class',
367  // See documentation for the corresponding config options
368  'ArticlePath',
369  'EnableScaryTranscluding',
370  'ExtraInterlanguageLinkPrefixes',
371  'FragmentMode',
372  'LanguageCode',
373  'MaxSigChars',
374  'MaxTocLevel',
375  'MiserMode',
376  'ScriptPath',
377  'Server',
378  'ServerName',
379  'ShowHostnames',
380  'SignatureValidation',
381  'Sitename',
382  'StylePath',
383  'TranscludeCacheExpiry',
384  ];
385 
403  public function __construct(
404  $svcOptions = null,
406  Language $contLang = null,
407  ParserFactory $factory = null,
408  $urlProtocols = null,
409  SpecialPageFactory $spFactory = null,
410  $linkRendererFactory = null,
411  $nsInfo = null,
412  $logger = null,
416  ) {
417  if ( ParserFactory::$inParserFactory === 0 ) {
418  // Direct construction of Parser is deprecated; use a ParserFactory
419  wfDeprecated( __METHOD__, '1.34' );
420  }
421  if ( !$svcOptions || is_array( $svcOptions ) ) {
422  wfDeprecated( 'old calling convention for ' . __METHOD__, '1.34' );
423  // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is
424  // Config, and the eighth is LinkRendererFactory.
425  $this->mConf = (array)$svcOptions;
426  if ( empty( $this->mConf['class'] ) ) {
427  $this->mConf['class'] = self::class;
428  }
429  $this->svcOptions = new ServiceOptions( self::CONSTRUCTOR_OPTIONS,
430  $this->mConf, func_num_args() > 6
431  ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
432  );
433  $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null;
434  $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null;
435  } else {
436  // New calling convention
437  $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
438  // $this->mConf is public, so we'll keep the option there for
439  // compatibility until it's removed
440  $this->mConf = [
441  'class' => $svcOptions->get( 'class' ),
442  ];
443  $this->svcOptions = $svcOptions;
444  }
445 
446  $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
447  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
448  self::EXT_LINK_ADDR .
449  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
450 
451  $this->magicWordFactory = $magicWordFactory ??
452  MediaWikiServices::getInstance()->getMagicWordFactory();
453 
454  $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
455 
456  $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory();
457  $this->specialPageFactory = $spFactory ??
458  MediaWikiServices::getInstance()->getSpecialPageFactory();
459  $this->linkRendererFactory = $linkRendererFactory ??
460  MediaWikiServices::getInstance()->getLinkRendererFactory();
461  $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
462  $this->logger = $logger ?: new NullLogger();
463  $this->badFileLookup = $badFileLookup ??
464  MediaWikiServices::getInstance()->getBadFileLookup();
465 
466  $this->languageConverterFactory = $languageConverterFactory ??
467  MediaWikiServices::getInstance()->getLanguageConverterFactory();
468 
469  $this->hookContainer = $hookContainer ??
470  MediaWikiServices::getInstance()->getHookContainer();
471  $this->hookRunner = new HookRunner( $this->hookContainer );
472 
473  // T250444: This will eventually be inlined here and the
474  // standalone method removed.
475  $this->firstCallInit();
476  }
477 
481  public function __destruct() {
482  if ( isset( $this->mLinkHolders ) ) {
483  // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
484  unset( $this->mLinkHolders );
485  }
486  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
487  foreach ( $this as $name => $value ) {
488  unset( $this->$name );
489  }
490  }
491 
495  public function __clone() {
496  $this->mInParse = false;
497 
498  // T58226: When you create a reference "to" an object field, that
499  // makes the object field itself be a reference too (until the other
500  // reference goes out of scope). When cloning, any field that's a
501  // reference is copied as a reference in the new object. Both of these
502  // are defined PHP5 behaviors, as inconvenient as it is for us when old
503  // hooks from PHP4 days are passing fields by reference.
504  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
505  // Make a non-reference copy of the field, then rebind the field to
506  // reference the new copy.
507  $tmp = $this->$k;
508  $this->$k =& $tmp;
509  unset( $tmp );
510  }
511 
512  $this->hookRunner->onParserCloned( $this );
513  }
514 
520  public function firstCallInit() {
521  if ( !$this->mFirstCall ) {
522  return;
523  }
524  $this->mFirstCall = false;
525 
527  CoreTagHooks::register( $this );
528  $this->initializeVariables();
529 
530  $this->hookRunner->onParserFirstCallInit( $this );
531  }
532 
538  public function clearState() {
539  $this->firstCallInit();
540  $this->resetOutput();
541  $this->mAutonumber = 0;
542  $this->mLinkHolders = new LinkHolderArray(
543  $this,
545  $this->getHookContainer()
546  );
547  $this->mLinkID = 0;
548  $this->mRevisionObject = $this->mRevisionTimestamp =
549  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
550  $this->mRevisionRecordObject = null;
551  $this->mVarCache = [];
552  $this->mUser = null;
553  $this->mLangLinkLanguages = [];
554  $this->currentRevisionCache = null;
555 
556  $this->mStripState = new StripState( $this );
557 
558  # Clear these on every parse, T6549
559  $this->mTplRedirCache = $this->mTplDomCache = [];
560 
561  $this->mShowToc = true;
562  $this->mForceTocPosition = false;
563  $this->mIncludeSizes = [
564  'post-expand' => 0,
565  'arg' => 0,
566  ];
567  $this->mPPNodeCount = 0;
568  $this->mGeneratedPPNodeCount = 0;
569  $this->mHighestExpansionDepth = 0;
570  $this->mDefaultSort = false;
571  $this->mHeadings = [];
572  $this->mDoubleUnderscores = [];
573  $this->mExpensiveFunctionCount = 0;
574 
575  # Fix cloning
576  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
577  $this->mPreprocessor = null;
578  }
579 
580  $this->mProfiler = new SectionProfiler();
581 
582  $this->hookRunner->onParserClearState( $this );
583  }
584 
588  public function resetOutput() {
589  $this->mOutput = new ParserOutput;
590  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
591  }
592 
610  public function parse(
611  $text, Title $title, ParserOptions $options,
612  $linestart = true, $clearState = true, $revid = null
613  ) {
614  if ( $clearState ) {
615  // We use U+007F DELETE to construct strip markers, so we have to make
616  // sure that this character does not occur in the input text.
617  $text = strtr( $text, "\x7f", "?" );
618  $magicScopeVariable = $this->lock();
619  }
620  // Strip U+0000 NULL (T159174)
621  $text = str_replace( "\000", '', $text );
622 
623  $this->startParse( $title, $options, self::OT_HTML, $clearState );
624 
625  $this->currentRevisionCache = null;
626  $this->mInputSize = strlen( $text );
627  if ( $this->mOptions->getEnableLimitReport() ) {
628  $this->mOutput->resetParseStartTime();
629  }
630 
631  $oldRevisionId = $this->mRevisionId;
632  $oldRevisionObject = $this->mRevisionObject;
633  $oldRevisionRecordObject = $this->mRevisionRecordObject;
634  $oldRevisionTimestamp = $this->mRevisionTimestamp;
635  $oldRevisionUser = $this->mRevisionUser;
636  $oldRevisionSize = $this->mRevisionSize;
637  if ( $revid !== null ) {
638  $this->mRevisionId = $revid;
639  $this->mRevisionObject = null;
640  $this->mRevisionRecordObject = null;
641  $this->mRevisionTimestamp = null;
642  $this->mRevisionUser = null;
643  $this->mRevisionSize = null;
644  }
645 
646  $text = $this->internalParse( $text );
647  $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
648 
649  $text = $this->internalParseHalfParsed( $text, true, $linestart );
650 
658  if ( !( $options->getDisableTitleConversion()
659  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
660  || isset( $this->mDoubleUnderscores['notitleconvert'] )
661  || $this->mOutput->getDisplayTitle() !== false )
662  ) {
663  $convruletitle = $this->getTargetLanguageConverter()->getConvRuleTitle();
664  if ( $convruletitle ) {
665  $this->mOutput->setTitleText( $convruletitle );
666  } else {
667  $titleText = $this->getTargetLanguageConverter()->convertTitle( $title );
668  $this->mOutput->setTitleText( $titleText );
669  }
670  }
671 
672  # Compute runtime adaptive expiry if set
673  $this->mOutput->finalizeAdaptiveCacheExpiry();
674 
675  # Warn if too many heavyweight parser functions were used
676  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
677  $this->limitationWarn( 'expensive-parserfunction',
678  $this->mExpensiveFunctionCount,
679  $this->mOptions->getExpensiveParserFunctionLimit()
680  );
681  }
682 
683  # Information on limits, for the benefit of users who try to skirt them
684  if ( $this->mOptions->getEnableLimitReport() ) {
685  $text .= $this->makeLimitReport();
686  }
687 
688  # Wrap non-interface parser output in a <div> so it can be targeted
689  # with CSS (T37247)
690  $class = $this->mOptions->getWrapOutputClass();
691  if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
692  $this->mOutput->addWrapperDivClass( $class );
693  }
694 
695  $this->mOutput->setText( $text );
696 
697  $this->mRevisionId = $oldRevisionId;
698  $this->mRevisionObject = $oldRevisionObject;
699  $this->mRevisionRecordObject = $oldRevisionRecordObject;
700  $this->mRevisionTimestamp = $oldRevisionTimestamp;
701  $this->mRevisionUser = $oldRevisionUser;
702  $this->mRevisionSize = $oldRevisionSize;
703  $this->mInputSize = false;
704  $this->currentRevisionCache = null;
705 
706  return $this->mOutput;
707  }
708 
715  protected function makeLimitReport() {
716  $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
717 
718  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
719  if ( $cpuTime !== null ) {
720  $this->mOutput->setLimitReportData( 'limitreport-cputime',
721  sprintf( "%.3f", $cpuTime )
722  );
723  }
724 
725  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
726  $this->mOutput->setLimitReportData( 'limitreport-walltime',
727  sprintf( "%.3f", $wallTime )
728  );
729 
730  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
731  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
732  );
733  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
734  [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
735  );
736  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
737  [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
738  );
739  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
740  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
741  );
742  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
743  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
744  );
745 
746  foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
747  $this->mOutput->setLimitReportData( $key, $value );
748  }
749 
750  $this->hookRunner->onParserLimitReportPrepare( $this, $this->mOutput );
751 
752  $limitReport = "NewPP limit report\n";
753  if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
754  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
755  }
756  $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
757  $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
758  $limitReport .= 'Dynamic content: ' .
759  ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
760  "\n";
761  $limitReport .= 'Complications: [' . implode( ', ', $this->mOutput->getAllFlags() ) . "]\n";
762 
763  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
764  if ( $this->hookRunner->onParserLimitReportFormat(
765  $key, $value, $limitReport, false, false )
766  ) {
767  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
768  $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
769  ->inLanguage( 'en' )->useDatabase( false );
770  if ( !$valueMsg->exists() ) {
771  $valueMsg = new RawMessage( '$1' );
772  }
773  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
774  $valueMsg->params( $value );
775  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
776  }
777  }
778  }
779  // Since we're not really outputting HTML, decode the entities and
780  // then re-encode the things that need hiding inside HTML comments.
781  $limitReport = htmlspecialchars_decode( $limitReport );
782 
783  // Sanitize for comment. Note '‐' in the replacement is U+2010,
784  // which looks much like the problematic '-'.
785  $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
786  $text = "\n<!-- \n$limitReport-->\n";
787 
788  // Add on template profiling data in human/machine readable way
789  $dataByFunc = $this->mProfiler->getFunctionStats();
790  uasort( $dataByFunc, function ( $a, $b ) {
791  return $b['real'] <=> $a['real']; // descending order
792  } );
793  $profileReport = [];
794  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
795  $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
796  $item['%real'], $item['real'], $item['calls'],
797  htmlspecialchars( $item['name'] ) );
798  }
799  $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
800  $text .= implode( "\n", $profileReport ) . "\n-->\n";
801 
802  $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
803 
804  // Add other cache related metadata
805  if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
806  $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
807  }
808  $this->mOutput->setLimitReportData( 'cachereport-timestamp',
809  $this->mOutput->getCacheTime() );
810  $this->mOutput->setLimitReportData( 'cachereport-ttl',
811  $this->mOutput->getCacheExpiry() );
812  $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
813  $this->mOutput->hasDynamicContent() );
814 
815  return $text;
816  }
817 
842  public function recursiveTagParse( $text, $frame = false ) {
843  $text = $this->internalParse( $text, false, $frame );
844  return $text;
845  }
846 
866  public function recursiveTagParseFully( $text, $frame = false ) {
867  $text = $this->recursiveTagParse( $text, $frame );
868  $text = $this->internalParseHalfParsed( $text, false );
869  return $text;
870  }
871 
891  public function parseExtensionTagAsTopLevelDoc( $text ) {
892  $text = $this->recursiveTagParse( $text );
893  $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
894  $text = $this->internalParseHalfParsed( $text, true );
895  return $text;
896  }
897 
909  public function preprocess( $text, ?Title $title,
910  ParserOptions $options, $revid = null, $frame = false
911  ) {
912  $magicScopeVariable = $this->lock();
913  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
914  if ( $revid !== null ) {
915  $this->mRevisionId = $revid;
916  }
917  $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
918  $text = $this->replaceVariables( $text, $frame );
919  $text = $this->mStripState->unstripBoth( $text );
920  return $text;
921  }
922 
932  public function recursivePreprocess( $text, $frame = false ) {
933  $text = $this->replaceVariables( $text, $frame );
934  $text = $this->mStripState->unstripBoth( $text );
935  return $text;
936  }
937 
951  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
952  $msg = new RawMessage( $text );
953  $text = $msg->params( $params )->plain();
954 
955  # Parser (re)initialisation
956  $magicScopeVariable = $this->lock();
957  $this->startParse( $title, $options, self::OT_PLAIN, true );
958 
960  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
961  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
962  $text = $this->mStripState->unstripBoth( $text );
963  return $text;
964  }
965 
972  public function setUser( ?User $user ) {
973  $this->mUser = $user;
974  }
975 
981  public function setTitle( Title $t = null ) {
982  if ( !$t ) {
983  $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
984  }
985 
986  if ( $t->hasFragment() ) {
987  # Strip the fragment to avoid various odd effects
988  $this->mTitle = $t->createFragmentTarget( '' );
989  } else {
990  $this->mTitle = $t;
991  }
992  }
993 
997  public function getTitle() : Title {
998  return $this->mTitle;
999  }
1000 
1008  public function Title( Title $x = null ) : ?Title {
1009  wfDeprecated( __METHOD__, '1.35' );
1010  return wfSetVar( $this->mTitle, $x );
1011  }
1012 
1018  public function getOutputType(): int {
1019  return $this->mOutputType;
1020  }
1021 
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 
1044  public function OutputType( $x = null ) {
1045  wfDeprecated( __METHOD__, '1.35' );
1046  return wfSetVar( $this->mOutputType, $x );
1047  }
1048 
1052  public function getOutput() {
1053  return $this->mOutput;
1054  }
1055 
1059  public function getOptions() {
1060  return $this->mOptions;
1061  }
1062 
1068  public function setOptions( ParserOptions $options ): void {
1069  $this->mOptions = $options;
1070  }
1071 
1079  public function Options( $x = null ) {
1080  wfDeprecated( __METHOD__, '1.35' );
1081  return wfSetVar( $this->mOptions, $x );
1082  }
1083 
1087  public function nextLinkID() {
1088  return $this->mLinkID++;
1089  }
1090 
1094  public function setLinkID( $id ) {
1095  $this->mLinkID = $id;
1096  }
1097 
1102  public function getFunctionLang() {
1103  return $this->getTargetLanguage();
1104  }
1105 
1114  public function getTargetLanguage() {
1115  $target = $this->mOptions->getTargetLanguage();
1116 
1117  if ( $target !== null ) {
1118  return $target;
1119  } elseif ( $this->mOptions->getInterfaceMessage() ) {
1120  return $this->mOptions->getUserLangObj();
1121  }
1122 
1123  return $this->getTitle()->getPageLanguage();
1124  }
1125 
1132  public function getUser() {
1133  if ( $this->mUser !== null ) {
1134  return $this->mUser;
1135  }
1136  return $this->mOptions->getUser();
1137  }
1138 
1144  public function getPreprocessor() {
1145  if ( !isset( $this->mPreprocessor ) ) {
1146  $this->mPreprocessor = new Preprocessor_Hash( $this );
1147  }
1148  return $this->mPreprocessor;
1149  }
1150 
1157  public function getLinkRenderer() {
1158  // XXX We make the LinkRenderer with current options and then cache it forever
1159  if ( !$this->mLinkRenderer ) {
1160  $this->mLinkRenderer = $this->linkRendererFactory->create();
1161  $this->mLinkRenderer->setStubThreshold(
1162  $this->getOptions()->getStubThreshold()
1163  );
1164  }
1165 
1166  return $this->mLinkRenderer;
1167  }
1168 
1175  public function getMagicWordFactory() {
1176  return $this->magicWordFactory;
1177  }
1178 
1185  public function getContentLanguage() {
1186  return $this->contLang;
1187  }
1188 
1195  public function getBadFileLookup() {
1196  return $this->badFileLookup;
1197  }
1198 
1218  public static function extractTagsAndParams( array $elements, $text, &$matches ) {
1219  static $n = 1;
1220  $stripped = '';
1221  $matches = [];
1222 
1223  $taglist = implode( '|', $elements );
1224  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1225 
1226  while ( $text != '' ) {
1227  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1228  $stripped .= $p[0];
1229  if ( count( $p ) < 5 ) {
1230  break;
1231  }
1232  if ( count( $p ) > 5 ) {
1233  # comment
1234  $element = $p[4];
1235  $attributes = '';
1236  $close = '';
1237  $inside = $p[5];
1238  } else {
1239  # tag
1240  list( , $element, $attributes, $close, $inside ) = $p;
1241  }
1242 
1243  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1244  $stripped .= $marker;
1245 
1246  if ( $close === '/>' ) {
1247  # Empty element tag, <tag />
1248  $content = null;
1249  $text = $inside;
1250  $tail = null;
1251  } else {
1252  if ( $element === '!--' ) {
1253  $end = '/(-->)/';
1254  } else {
1255  $end = "/(<\\/$element\\s*>)/i";
1256  }
1257  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1258  $content = $q[0];
1259  if ( count( $q ) < 3 ) {
1260  # No end tag -- let it run out to the end of the text.
1261  $tail = '';
1262  $text = '';
1263  } else {
1264  list( , $tail, $text ) = $q;
1265  }
1266  }
1267 
1268  $matches[$marker] = [ $element,
1269  $content,
1270  Sanitizer::decodeTagAttributes( $attributes ),
1271  "<$element$attributes$close$content$tail" ];
1272  }
1273  return $stripped;
1274  }
1275 
1281  public function getStripList() {
1282  return $this->mStripList;
1283  }
1284 
1290  public function getStripState() {
1291  return $this->mStripState;
1292  }
1293 
1303  public function insertStripItem( $text ) {
1304  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1305  $this->mMarkerIndex++;
1306  $this->mStripState->addGeneral( $marker, $text );
1307  return $marker;
1308  }
1309 
1316  private function handleTables( $text ) {
1317  $lines = StringUtils::explode( "\n", $text );
1318  $out = '';
1319  $td_history = []; # Is currently a td tag open?
1320  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1321  $tr_history = []; # Is currently a tr tag open?
1322  $tr_attributes = []; # history of tr attributes
1323  $has_opened_tr = []; # Did this table open a <tr> element?
1324  $indent_level = 0; # indent level of the table
1325 
1326  foreach ( $lines as $outLine ) {
1327  $line = trim( $outLine );
1328 
1329  if ( $line === '' ) { # empty line, go to next line
1330  $out .= $outLine . "\n";
1331  continue;
1332  }
1333 
1334  $first_character = $line[0];
1335  $first_two = substr( $line, 0, 2 );
1336  $matches = [];
1337 
1338  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1339  # First check if we are starting a new table
1340  $indent_level = strlen( $matches[1] );
1341 
1342  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1343  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1344 
1345  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1346  array_push( $td_history, false );
1347  array_push( $last_tag_history, '' );
1348  array_push( $tr_history, false );
1349  array_push( $tr_attributes, '' );
1350  array_push( $has_opened_tr, false );
1351  } elseif ( count( $td_history ) == 0 ) {
1352  # Don't do any of the following
1353  $out .= $outLine . "\n";
1354  continue;
1355  } elseif ( $first_two === '|}' ) {
1356  # We are ending a table
1357  $line = '</table>' . substr( $line, 2 );
1358  $last_tag = array_pop( $last_tag_history );
1359 
1360  if ( !array_pop( $has_opened_tr ) ) {
1361  $line = "<tr><td></td></tr>{$line}";
1362  }
1363 
1364  if ( array_pop( $tr_history ) ) {
1365  $line = "</tr>{$line}";
1366  }
1367 
1368  if ( array_pop( $td_history ) ) {
1369  $line = "</{$last_tag}>{$line}";
1370  }
1371  array_pop( $tr_attributes );
1372  if ( $indent_level > 0 ) {
1373  $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1374  } else {
1375  $outLine = $line;
1376  }
1377  } elseif ( $first_two === '|-' ) {
1378  # Now we have a table row
1379  $line = preg_replace( '#^\|-+#', '', $line );
1380 
1381  # Whats after the tag is now only attributes
1382  $attributes = $this->mStripState->unstripBoth( $line );
1383  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1384  array_pop( $tr_attributes );
1385  array_push( $tr_attributes, $attributes );
1386 
1387  $line = '';
1388  $last_tag = array_pop( $last_tag_history );
1389  array_pop( $has_opened_tr );
1390  array_push( $has_opened_tr, true );
1391 
1392  if ( array_pop( $tr_history ) ) {
1393  $line = '</tr>';
1394  }
1395 
1396  if ( array_pop( $td_history ) ) {
1397  $line = "</{$last_tag}>{$line}";
1398  }
1399 
1400  $outLine = $line;
1401  array_push( $tr_history, false );
1402  array_push( $td_history, false );
1403  array_push( $last_tag_history, '' );
1404  } elseif ( $first_character === '|'
1405  || $first_character === '!'
1406  || $first_two === '|+'
1407  ) {
1408  # This might be cell elements, td, th or captions
1409  if ( $first_two === '|+' ) {
1410  $first_character = '+';
1411  $line = substr( $line, 2 );
1412  } else {
1413  $line = substr( $line, 1 );
1414  }
1415 
1416  // Implies both are valid for table headings.
1417  if ( $first_character === '!' ) {
1418  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1419  }
1420 
1421  # Split up multiple cells on the same line.
1422  # FIXME : This can result in improper nesting of tags processed
1423  # by earlier parser steps.
1424  $cells = explode( '||', $line );
1425 
1426  $outLine = '';
1427 
1428  # Loop through each table cell
1429  foreach ( $cells as $cell ) {
1430  $previous = '';
1431  if ( $first_character !== '+' ) {
1432  $tr_after = array_pop( $tr_attributes );
1433  if ( !array_pop( $tr_history ) ) {
1434  $previous = "<tr{$tr_after}>\n";
1435  }
1436  array_push( $tr_history, true );
1437  array_push( $tr_attributes, '' );
1438  array_pop( $has_opened_tr );
1439  array_push( $has_opened_tr, true );
1440  }
1441 
1442  $last_tag = array_pop( $last_tag_history );
1443 
1444  if ( array_pop( $td_history ) ) {
1445  $previous = "</{$last_tag}>\n{$previous}";
1446  }
1447 
1448  if ( $first_character === '|' ) {
1449  $last_tag = 'td';
1450  } elseif ( $first_character === '!' ) {
1451  $last_tag = 'th';
1452  } elseif ( $first_character === '+' ) {
1453  $last_tag = 'caption';
1454  } else {
1455  $last_tag = '';
1456  }
1457 
1458  array_push( $last_tag_history, $last_tag );
1459 
1460  # A cell could contain both parameters and data
1461  $cell_data = explode( '|', $cell, 2 );
1462 
1463  # T2553: Note that a '|' inside an invalid link should not
1464  # be mistaken as delimiting cell parameters
1465  # Bug T153140: Neither should language converter markup.
1466  if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1467  $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1468  } elseif ( count( $cell_data ) == 1 ) {
1469  // Whitespace in cells is trimmed
1470  $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1471  } else {
1472  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1473  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1474  // Whitespace in cells is trimmed
1475  $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1476  }
1477 
1478  $outLine .= $cell;
1479  array_push( $td_history, true );
1480  }
1481  }
1482  $out .= $outLine . "\n";
1483  }
1484 
1485  # Closing open td, tr && table
1486  while ( count( $td_history ) > 0 ) {
1487  if ( array_pop( $td_history ) ) {
1488  $out .= "</td>\n";
1489  }
1490  if ( array_pop( $tr_history ) ) {
1491  $out .= "</tr>\n";
1492  }
1493  if ( !array_pop( $has_opened_tr ) ) {
1494  $out .= "<tr><td></td></tr>\n";
1495  }
1496 
1497  $out .= "</table>\n";
1498  }
1499 
1500  # Remove trailing line-ending (b/c)
1501  if ( substr( $out, -1 ) === "\n" ) {
1502  $out = substr( $out, 0, -1 );
1503  }
1504 
1505  # special case: don't return empty table
1506  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1507  $out = '';
1508  }
1509 
1510  return $out;
1511  }
1512 
1526  public function internalParse( $text, $isMain = true, $frame = false ) {
1527  $origText = $text;
1528 
1529  # Hook to suspend the parser in this state
1530  if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1531  return $text;
1532  }
1533 
1534  # if $frame is provided, then use $frame for replacing any variables
1535  if ( $frame ) {
1536  # use frame depth to infer how include/noinclude tags should be handled
1537  # depth=0 means this is the top-level document; otherwise it's an included document
1538  if ( !$frame->depth ) {
1539  $flag = 0;
1540  } else {
1541  $flag = self::PTD_FOR_INCLUSION;
1542  }
1543  $dom = $this->preprocessToDom( $text, $flag );
1544  $text = $frame->expand( $dom );
1545  } else {
1546  # if $frame is not provided, then use old-style replaceVariables
1547  $text = $this->replaceVariables( $text );
1548  }
1549 
1550  $this->hookRunner->onInternalParseBeforeSanitize( $this, $text, $this->mStripState );
1551  $text = Sanitizer::removeHTMLtags(
1552  $text,
1553  // Callback from the Sanitizer for expanding items found in
1554  // HTML attribute values, so they can be safely tested and escaped.
1555  function ( &$text, $frame = false ) {
1556  $text = $this->replaceVariables( $text, $frame );
1557  $text = $this->mStripState->unstripBoth( $text );
1558  },
1559  false,
1560  [],
1561  []
1562  );
1563  $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1564 
1565  # Tables need to come after variable replacement for things to work
1566  # properly; putting them before other transformations should keep
1567  # exciting things like link expansions from showing up in surprising
1568  # places.
1569  $text = $this->handleTables( $text );
1570 
1571  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1572 
1573  $text = $this->handleDoubleUnderscore( $text );
1574 
1575  $text = $this->handleHeadings( $text );
1576  $text = $this->handleInternalLinks( $text );
1577  $text = $this->handleAllQuotes( $text );
1578  $text = $this->handleExternalLinks( $text );
1579 
1580  # handleInternalLinks may sometimes leave behind
1581  # absolute URLs, which have to be masked to hide them from handleExternalLinks
1582  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1583 
1584  $text = $this->handleMagicLinks( $text );
1585  $text = $this->finalizeHeadings( $text, $origText, $isMain );
1586 
1587  return $text;
1588  }
1589 
1596  return $this->languageConverterFactory->getLanguageConverter(
1597  $this->getTargetLanguage()
1598  );
1599  }
1600 
1607  return $this->languageConverterFactory->getLanguageConverter(
1608  $this->getContentLanguage()
1609  );
1610  }
1611 
1619  protected function getHookContainer() {
1620  return $this->hookContainer;
1621  }
1622 
1631  protected function getHookRunner() {
1632  return $this->hookRunner;
1633  }
1634 
1644  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1645  $text = $this->mStripState->unstripGeneral( $text );
1646 
1647  $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1648 
1649  $this->replaceLinkHoldersPrivate( $text );
1650 
1658  if ( !( $this->mOptions->getDisableContentConversion()
1659  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1660  && !$this->mOptions->getInterfaceMessage()
1661  ) {
1662  # The position of the convert() call should not be changed. it
1663  # assumes that the links are all replaced and the only thing left
1664  # is the <nowiki> mark.
1665  $text = $this->getTargetLanguageConverter()->convert( $text );
1666  }
1667 
1668  $text = $this->mStripState->unstripNoWiki( $text );
1669 
1670  $text = $this->mStripState->unstripGeneral( $text );
1671 
1672  # Clean up special characters, only run once, after doBlockLevels
1673  $text = Sanitizer::armorFrenchSpaces( $text );
1674 
1675  $text = Sanitizer::normalizeCharReferences( $text );
1676 
1677  $text = MWTidy::tidy( $text );
1678 
1679  if ( $isMain ) {
1680  $this->hookRunner->onParserAfterTidy( $this, $text );
1681  }
1682 
1683  return $text;
1684  }
1685 
1696  private function handleMagicLinks( $text ) {
1697  $prots = wfUrlProtocolsWithoutProtRel();
1698  $urlChar = self::EXT_LINK_URL_CLASS;
1699  $addr = self::EXT_LINK_ADDR;
1700  $space = self::SPACE_NOT_NL; # non-newline space
1701  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1702  $spaces = "$space++"; # possessive match of 1 or more spaces
1703  $text = preg_replace_callback(
1704  '!(?: # Start cases
1705  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1706  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1707  (\b # m[3]: Free external links
1708  (?i:$prots)
1709  ($addr$urlChar*) # m[4]: Post-protocol path
1710  ) |
1711  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1712  ([0-9]+)\b |
1713  \bISBN $spaces ( # m[6]: ISBN, capture number
1714  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1715  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1716  [0-9Xx] # check digit
1717  )\b
1718  )!xu", [ $this, 'magicLinkCallback' ], $text );
1719  return $text;
1720  }
1721 
1727  private function magicLinkCallback( array $m ) {
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 ( substr( $m[0], 0, 3 ) === '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 ( substr( $m[0], 0, 4 ) === '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  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1759  substr( $m[0], 0, 20 ) . '"' );
1760  }
1761  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1762  $this->addTrackingCategory( $trackingCat );
1763  return Linker::makeExternalLink(
1764  $url,
1765  "{$keyword} {$id}",
1766  true,
1767  $cssClass,
1768  [],
1769  $this->getTitle()
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( $url, $numPostProto ) {
1807  $trail = '';
1808 
1809  # The characters '<' and '>' (which were escaped by
1810  # removeHTMLtags()) 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 ( strpos( $url, '(' ) === false ) {
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 = Linker::makeExternalLink( $url,
1863  $this->getTargetLanguageConverter()->markNoConversion( $url ),
1864  true, 'free',
1865  $this->getExternalLinkAttribs( $url ), $this->getTitle() );
1866  # Register it in the output object...
1867  $this->mOutput->addExternalLink( $url );
1868  }
1869  return $text . $trail;
1870  }
1871 
1878  private function handleHeadings( $text ) {
1879  for ( $i = 6; $i >= 1; --$i ) {
1880  $h = str_repeat( '=', $i );
1881  // Trim non-newline whitespace from headings
1882  // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1883  $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1884  }
1885  return $text;
1886  }
1887 
1895  private function handleAllQuotes( $text ) {
1896  $outtext = '';
1897  $lines = StringUtils::explode( "\n", $text );
1898  foreach ( $lines as $line ) {
1899  $outtext .= $this->doQuotes( $line ) . "\n";
1900  }
1901  $outtext = substr( $outtext, 0, -1 );
1902  return $outtext;
1903  }
1904 
1913  public function doQuotes( $text ) {
1914  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1915  $countarr = count( $arr );
1916  if ( $countarr == 1 ) {
1917  return $text;
1918  }
1919 
1920  // First, do some preliminary work. This may shift some apostrophes from
1921  // being mark-up to being text. It also counts the number of occurrences
1922  // of bold and italics mark-ups.
1923  $numbold = 0;
1924  $numitalics = 0;
1925  for ( $i = 1; $i < $countarr; $i += 2 ) {
1926  $thislen = strlen( $arr[$i] );
1927  // If there are ever four apostrophes, assume the first is supposed to
1928  // be text, and the remaining three constitute mark-up for bold text.
1929  // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1930  if ( $thislen == 4 ) {
1931  $arr[$i - 1] .= "'";
1932  $arr[$i] = "'''";
1933  $thislen = 3;
1934  } elseif ( $thislen > 5 ) {
1935  // If there are more than 5 apostrophes in a row, assume they're all
1936  // text except for the last 5.
1937  // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1938  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1939  $arr[$i] = "'''''";
1940  $thislen = 5;
1941  }
1942  // Count the number of occurrences of bold and italics mark-ups.
1943  if ( $thislen == 2 ) {
1944  $numitalics++;
1945  } elseif ( $thislen == 3 ) {
1946  $numbold++;
1947  } elseif ( $thislen == 5 ) {
1948  $numitalics++;
1949  $numbold++;
1950  }
1951  }
1952 
1953  // If there is an odd number of both bold and italics, it is likely
1954  // that one of the bold ones was meant to be an apostrophe followed
1955  // by italics. Which one we cannot know for certain, but it is more
1956  // likely to be one that has a single-letter word before it.
1957  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1958  $firstsingleletterword = -1;
1959  $firstmultiletterword = -1;
1960  $firstspace = -1;
1961  for ( $i = 1; $i < $countarr; $i += 2 ) {
1962  if ( strlen( $arr[$i] ) == 3 ) {
1963  $x1 = substr( $arr[$i - 1], -1 );
1964  $x2 = substr( $arr[$i - 1], -2, 1 );
1965  if ( $x1 === ' ' ) {
1966  if ( $firstspace == -1 ) {
1967  $firstspace = $i;
1968  }
1969  } elseif ( $x2 === ' ' ) {
1970  $firstsingleletterword = $i;
1971  // if $firstsingleletterword is set, we don't
1972  // look at the other options, so we can bail early.
1973  break;
1974  } elseif ( $firstmultiletterword == -1 ) {
1975  $firstmultiletterword = $i;
1976  }
1977  }
1978  }
1979 
1980  // If there is a single-letter word, use it!
1981  if ( $firstsingleletterword > -1 ) {
1982  $arr[$firstsingleletterword] = "''";
1983  $arr[$firstsingleletterword - 1] .= "'";
1984  } elseif ( $firstmultiletterword > -1 ) {
1985  // If not, but there's a multi-letter word, use that one.
1986  $arr[$firstmultiletterword] = "''";
1987  $arr[$firstmultiletterword - 1] .= "'";
1988  } elseif ( $firstspace > -1 ) {
1989  // ... otherwise use the first one that has neither.
1990  // (notice that it is possible for all three to be -1 if, for example,
1991  // there is only one pentuple-apostrophe in the line)
1992  $arr[$firstspace] = "''";
1993  $arr[$firstspace - 1] .= "'";
1994  }
1995  }
1996 
1997  // Now let's actually convert our apostrophic mush to HTML!
1998  $output = '';
1999  $buffer = '';
2000  $state = '';
2001  $i = 0;
2002  foreach ( $arr as $r ) {
2003  if ( ( $i % 2 ) == 0 ) {
2004  if ( $state === 'both' ) {
2005  $buffer .= $r;
2006  } else {
2007  $output .= $r;
2008  }
2009  } else {
2010  $thislen = strlen( $r );
2011  if ( $thislen == 2 ) {
2012  if ( $state === 'i' ) {
2013  $output .= '</i>';
2014  $state = '';
2015  } elseif ( $state === 'bi' ) {
2016  $output .= '</i>';
2017  $state = 'b';
2018  } elseif ( $state === 'ib' ) {
2019  $output .= '</b></i><b>';
2020  $state = 'b';
2021  } elseif ( $state === 'both' ) {
2022  $output .= '<b><i>' . $buffer . '</i>';
2023  $state = 'b';
2024  } else { // $state can be 'b' or ''
2025  $output .= '<i>';
2026  $state .= 'i';
2027  }
2028  } elseif ( $thislen == 3 ) {
2029  if ( $state === 'b' ) {
2030  $output .= '</b>';
2031  $state = '';
2032  } elseif ( $state === 'bi' ) {
2033  $output .= '</i></b><i>';
2034  $state = 'i';
2035  } elseif ( $state === 'ib' ) {
2036  $output .= '</b>';
2037  $state = 'i';
2038  } elseif ( $state === 'both' ) {
2039  $output .= '<i><b>' . $buffer . '</b>';
2040  $state = 'i';
2041  } else { // $state can be 'i' or ''
2042  $output .= '<b>';
2043  $state .= 'b';
2044  }
2045  } elseif ( $thislen == 5 ) {
2046  if ( $state === 'b' ) {
2047  $output .= '</b><i>';
2048  $state = 'i';
2049  } elseif ( $state === 'i' ) {
2050  $output .= '</i><b>';
2051  $state = 'b';
2052  } elseif ( $state === 'bi' ) {
2053  $output .= '</i></b>';
2054  $state = '';
2055  } elseif ( $state === 'ib' ) {
2056  $output .= '</b></i>';
2057  $state = '';
2058  } elseif ( $state === 'both' ) {
2059  $output .= '<i><b>' . $buffer . '</b></i>';
2060  $state = '';
2061  } else { // ($state == '')
2062  $buffer = '';
2063  $state = 'both';
2064  }
2065  }
2066  }
2067  $i++;
2068  }
2069  // Now close all remaining tags. Notice that the order is important.
2070  if ( $state === 'b' || $state === 'ib' ) {
2071  $output .= '</b>';
2072  }
2073  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2074  $output .= '</i>';
2075  }
2076  if ( $state === 'bi' ) {
2077  $output .= '</b>';
2078  }
2079  // There might be lonely ''''', so make sure we have a buffer
2080  if ( $state === 'both' && $buffer ) {
2081  $output .= '<b><i>' . $buffer . '</i></b>';
2082  }
2083  return $output;
2084  }
2085 
2096  private function handleExternalLinks( $text ) {
2097  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2098  // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2099  if ( $bits === false ) {
2100  throw new MWException( "PCRE needs to be compiled with "
2101  . "--enable-unicode-properties in order for MediaWiki to function" );
2102  }
2103  $s = array_shift( $bits );
2104 
2105  $i = 0;
2106  while ( $i < count( $bits ) ) {
2107  $url = $bits[$i++];
2108  $i++; // protocol
2109  $text = $bits[$i++];
2110  $trail = $bits[$i++];
2111 
2112  # The characters '<' and '>' (which were escaped by
2113  # removeHTMLtags()) should not be included in
2114  # URLs, per RFC 2396.
2115  $m2 = [];
2116  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2117  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2118  $url = substr( $url, 0, $m2[0][1] );
2119  }
2120 
2121  # If the link text is an image URL, replace it with an <img> tag
2122  # This happened by accident in the original parser, but some people used it extensively
2123  $img = $this->maybeMakeExternalImage( $text );
2124  if ( $img !== false ) {
2125  $text = $img;
2126  }
2127 
2128  $dtrail = '';
2129 
2130  # Set linktype for CSS
2131  $linktype = 'text';
2132 
2133  # No link text, e.g. [http://domain.tld/some.link]
2134  if ( $text == '' ) {
2135  # Autonumber
2136  $langObj = $this->getTargetLanguage();
2137  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2138  $linktype = 'autonumber';
2139  } else {
2140  # Have link text, e.g. [http://domain.tld/some.link text]s
2141  # Check for trail
2142  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2143  }
2144 
2145  // Excluding protocol-relative URLs may avoid many false positives.
2146  if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
2147  $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2148  }
2149 
2150  $url = Sanitizer::cleanUrl( $url );
2151 
2152  # Use the encoded URL
2153  # This means that users can paste URLs directly into the text
2154  # Funny characters like ö aren't valid in URLs anyway
2155  # This was changed in August 2004
2156  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2157  $this->getExternalLinkAttribs( $url ), $this->getTitle() ) . $dtrail . $trail;
2158 
2159  # Register link in the output object.
2160  $this->mOutput->addExternalLink( $url );
2161  }
2162 
2163  return $s;
2164  }
2165 
2176  public static function getExternalLinkRel( $url = false, LinkTarget $title = null ) {
2178  $ns = $title ? $title->getNamespace() : false;
2179  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2181  ) {
2182  return 'nofollow';
2183  }
2184  return null;
2185  }
2186 
2198  public function getExternalLinkAttribs( $url ) {
2199  $attribs = [];
2200  $rel = self::getExternalLinkRel( $url, $this->getTitle() );
2201 
2202  $target = $this->mOptions->getExternalLinkTarget();
2203  if ( $target ) {
2204  $attribs['target'] = $target;
2205  if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2206  // T133507. New windows can navigate parent cross-origin.
2207  // Including noreferrer due to lacking browser
2208  // support of noopener. Eventually noreferrer should be removed.
2209  if ( $rel !== '' ) {
2210  $rel .= ' ';
2211  }
2212  $rel .= 'noreferrer noopener';
2213  }
2214  }
2215  $attribs['rel'] = $rel;
2216  return $attribs;
2217  }
2218 
2229  public static function normalizeLinkUrl( $url ) {
2230  # Test for RFC 3986 IPv6 syntax
2231  $scheme = '[a-z][a-z0-9+.-]*:';
2232  $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2233  $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2234  if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2235  IPUtils::isValid( rawurldecode( $m[1] ) )
2236  ) {
2237  $isIPv6 = rawurldecode( $m[1] );
2238  } else {
2239  $isIPv6 = false;
2240  }
2241 
2242  # Make sure unsafe characters are encoded
2243  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2244  function ( $m ) {
2245  return rawurlencode( $m[0] );
2246  },
2247  $url
2248  );
2249 
2250  $ret = '';
2251  $end = strlen( $url );
2252 
2253  # Fragment part - 'fragment'
2254  $start = strpos( $url, '#' );
2255  if ( $start !== false && $start < $end ) {
2257  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2258  $end = $start;
2259  }
2260 
2261  # Query part - 'query' minus &=+;
2262  $start = strpos( $url, '?' );
2263  if ( $start !== false && $start < $end ) {
2265  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2266  $end = $start;
2267  }
2268 
2269  # Scheme and path part - 'pchar'
2270  # (we assume no userinfo or encoded colons in the host)
2272  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2273 
2274  # Fix IPv6 syntax
2275  if ( $isIPv6 !== false ) {
2276  $ipv6Host = "%5B({$isIPv6})%5D";
2277  $ret = preg_replace(
2278  "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2279  "$1[$2]",
2280  $ret
2281  );
2282  }
2283 
2284  return $ret;
2285  }
2286 
2287  private static function normalizeUrlComponent( $component, $unsafe ) {
2288  $callback = function ( $matches ) use ( $unsafe ) {
2289  $char = urldecode( $matches[0] );
2290  $ord = ord( $char );
2291  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2292  # Unescape it
2293  return $char;
2294  } else {
2295  # Leave it escaped, but use uppercase for a-f
2296  return strtoupper( $matches[0] );
2297  }
2298  };
2299  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2300  }
2301 
2310  private function maybeMakeExternalImage( $url ) {
2311  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2312  $imagesexception = !empty( $imagesfrom );
2313  $text = false;
2314  # $imagesfrom could be either a single string or an array of strings, parse out the latter
2315  if ( $imagesexception && is_array( $imagesfrom ) ) {
2316  $imagematch = false;
2317  foreach ( $imagesfrom as $match ) {
2318  if ( strpos( $url, $match ) === 0 ) {
2319  $imagematch = true;
2320  break;
2321  }
2322  }
2323  } elseif ( $imagesexception ) {
2324  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2325  } else {
2326  $imagematch = false;
2327  }
2328 
2329  if ( $this->mOptions->getAllowExternalImages()
2330  || ( $imagesexception && $imagematch )
2331  ) {
2332  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2333  # Image found
2334  $text = Linker::makeExternalImage( $url );
2335  }
2336  }
2337  if ( !$text && $this->mOptions->getEnableImageWhitelist()
2338  && preg_match( self::EXT_IMAGE_REGEX, $url )
2339  ) {
2340  $whitelist = explode(
2341  "\n",
2342  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2343  );
2344 
2345  foreach ( $whitelist as $entry ) {
2346  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2347  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2348  continue;
2349  }
2350  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2351  # Image matches a whitelist entry
2352  $text = Linker::makeExternalImage( $url );
2353  break;
2354  }
2355  }
2356  }
2357  return $text;
2358  }
2359 
2367  private function handleInternalLinks( $text ) {
2368  $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2369  return $text;
2370  }
2371 
2377  private function handleInternalLinks2( &$s ) {
2378  static $tc = false, $e1, $e1_img;
2379  # the % is needed to support urlencoded titles as well
2380  if ( !$tc ) {
2381  $tc = Title::legalChars() . '#%';
2382  # Match a link having the form [[namespace:link|alternate]]trail
2383  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2384  # Match cases where there is no "]]", which might still be images
2385  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2386  }
2387 
2388  $holders = new LinkHolderArray(
2389  $this,
2390  $this->getContentLanguageConverter(),
2391  $this->getHookContainer() );
2392 
2393  # split the entire text string on occurrences of [[
2394  $a = StringUtils::explode( '[[', ' ' . $s );
2395  # get the first element (all text up to first [[), and remove the space we added
2396  $s = $a->current();
2397  $a->next();
2398  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2399  $s = substr( $s, 1 );
2400 
2401  $nottalk = !$this->getTitle()->isTalkPage();
2402 
2403  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2404  $e2 = null;
2405  if ( $useLinkPrefixExtension ) {
2406  # Match the end of a line for a word that's not followed by whitespace,
2407  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2408  $charset = $this->contLang->linkPrefixCharset();
2409  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2410  $m = [];
2411  if ( preg_match( $e2, $s, $m ) ) {
2412  $first_prefix = $m[2];
2413  } else {
2414  $first_prefix = false;
2415  }
2416  } else {
2417  $prefix = '';
2418  }
2419 
2420  # Some namespaces don't allow subpages
2421  $useSubpages = $this->nsInfo->hasSubpages(
2422  $this->getTitle()->getNamespace()
2423  );
2424 
2425  # Loop for each link
2426  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2427  # Check for excessive memory usage
2428  if ( $holders->isBig() ) {
2429  # Too big
2430  # Do the existence check, replace the link holders and clear the array
2431  $holders->replace( $s );
2432  $holders->clear();
2433  }
2434 
2435  if ( $useLinkPrefixExtension ) {
2436  if ( preg_match( $e2, $s, $m ) ) {
2437  list( , $s, $prefix ) = $m;
2438  } else {
2439  $prefix = '';
2440  }
2441  # first link
2442  if ( $first_prefix ) {
2443  $prefix = $first_prefix;
2444  $first_prefix = false;
2445  }
2446  }
2447 
2448  $might_be_img = false;
2449 
2450  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2451  $text = $m[2];
2452  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2453  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2454  # the real problem is with the $e1 regex
2455  # See T1500.
2456  # Still some problems for cases where the ] is meant to be outside punctuation,
2457  # and no image is in sight. See T4095.
2458  if ( $text !== ''
2459  && substr( $m[3], 0, 1 ) === ']'
2460  && strpos( $text, '[' ) !== false
2461  ) {
2462  $text .= ']'; # so that handleExternalLinks($text) works later
2463  $m[3] = substr( $m[3], 1 );
2464  }
2465  # fix up urlencoded title texts
2466  if ( strpos( $m[1], '%' ) !== false ) {
2467  # Should anchors '#' also be rejected?
2468  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2469  }
2470  $trail = $m[3];
2471  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2472  # Invalid, but might be an image with a link in its caption
2473  $might_be_img = true;
2474  $text = $m[2];
2475  if ( strpos( $m[1], '%' ) !== false ) {
2476  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2477  }
2478  $trail = "";
2479  } else { # Invalid form; output directly
2480  $s .= $prefix . '[[' . $line;
2481  continue;
2482  }
2483 
2484  $origLink = ltrim( $m[1], ' ' );
2485 
2486  # Don't allow internal links to pages containing
2487  # PROTO: where PROTO is a valid URL protocol; these
2488  # should be external links.
2489  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2490  $s .= $prefix . '[[' . $line;
2491  continue;
2492  }
2493 
2494  # Make subpage if necessary
2495  if ( $useSubpages ) {
2497  $this->getTitle(), $origLink, $text
2498  );
2499  } else {
2500  $link = $origLink;
2501  }
2502 
2503  // \x7f isn't a default legal title char, so most likely strip
2504  // markers will force us into the "invalid form" path above. But,
2505  // just in case, let's assert that xmlish tags aren't valid in
2506  // the title position.
2507  $unstrip = $this->mStripState->killMarkers( $link );
2508  $noMarkers = ( $unstrip === $link );
2509 
2510  $nt = $noMarkers ? Title::newFromText( $link ) : null;
2511  if ( $nt === null ) {
2512  $s .= $prefix . '[[' . $line;
2513  continue;
2514  }
2515 
2516  $ns = $nt->getNamespace();
2517  $iw = $nt->getInterwiki();
2518 
2519  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2520 
2521  if ( $might_be_img ) { # if this is actually an invalid link
2522  if ( $ns === NS_FILE && $noforce ) { # but might be an image
2523  $found = false;
2524  while ( true ) {
2525  # look at the next 'line' to see if we can close it there
2526  $a->next();
2527  $next_line = $a->current();
2528  if ( $next_line === false || $next_line === null ) {
2529  break;
2530  }
2531  $m = explode( ']]', $next_line, 3 );
2532  if ( count( $m ) == 3 ) {
2533  # the first ]] closes the inner link, the second the image
2534  $found = true;
2535  $text .= "[[{$m[0]}]]{$m[1]}";
2536  $trail = $m[2];
2537  break;
2538  } elseif ( count( $m ) == 2 ) {
2539  # if there's exactly one ]] that's fine, we'll keep looking
2540  $text .= "[[{$m[0]}]]{$m[1]}";
2541  } else {
2542  # if $next_line is invalid too, we need look no further
2543  $text .= '[[' . $next_line;
2544  break;
2545  }
2546  }
2547  if ( !$found ) {
2548  # we couldn't find the end of this imageLink, so output it raw
2549  # but don't ignore what might be perfectly normal links in the text we've examined
2550  $holders->merge( $this->handleInternalLinks2( $text ) );
2551  $s .= "{$prefix}[[$link|$text";
2552  # note: no $trail, because without an end, there *is* no trail
2553  continue;
2554  }
2555  } else { # it's not an image, so output it raw
2556  $s .= "{$prefix}[[$link|$text";
2557  # note: no $trail, because without an end, there *is* no trail
2558  continue;
2559  }
2560  }
2561 
2562  $wasblank = ( $text == '' );
2563  if ( $wasblank ) {
2564  $text = $link;
2565  if ( !$noforce ) {
2566  # Strip off leading ':'
2567  $text = substr( $text, 1 );
2568  }
2569  } else {
2570  # T6598 madness. Handle the quotes only if they come from the alternate part
2571  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2572  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2573  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2574  $text = $this->doQuotes( $text );
2575  }
2576 
2577  # Link not escaped by : , create the various objects
2578  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2579  # Interwikis
2580  if (
2581  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2582  MediaWikiServices::getInstance()->getLanguageNameUtils()
2583  ->getLanguageName(
2584  $iw,
2585  LanguageNameUtils::AUTONYMS,
2586  LanguageNameUtils::DEFINED
2587  )
2588  || in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes' ) )
2589  )
2590  ) {
2591  # T26502: filter duplicates
2592  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2593  $this->mLangLinkLanguages[$iw] = true;
2594  $this->mOutput->addLanguageLink( $nt->getFullText() );
2595  }
2596 
2600  $s = rtrim( $s . $prefix ) . $trail; # T175416
2601  continue;
2602  }
2603 
2604  if ( $ns === NS_FILE ) {
2605  if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->getTitle() ) ) {
2606  if ( $wasblank ) {
2607  # if no parameters were passed, $text
2608  # becomes something like "File:Foo.png",
2609  # which we don't want to pass on to the
2610  # image generator
2611  $text = '';
2612  } else {
2613  # recursively parse links inside the image caption
2614  # actually, this will parse them in any other parameters, too,
2615  # but it might be hard to fix that, and it doesn't matter ATM
2616  $text = $this->handleExternalLinks( $text );
2617  $holders->merge( $this->handleInternalLinks2( $text ) );
2618  }
2619  # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2620  $s .= $prefix . $this->armorLinks(
2621  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2622  continue;
2623  }
2624  } elseif ( $ns === NS_CATEGORY ) {
2628  $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2629 
2630  if ( $wasblank ) {
2631  $sortkey = $this->getDefaultSort();
2632  } else {
2633  $sortkey = $text;
2634  }
2635  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2636  $sortkey = str_replace( "\n", '', $sortkey );
2637  $sortkey = $this->getTargetLanguageConverter()->convertCategoryKey( $sortkey );
2638  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2639 
2640  continue;
2641  }
2642  }
2643 
2644  # Self-link checking. For some languages, variants of the title are checked in
2645  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2646  # for linking to a different variant.
2647  if ( $ns !== NS_SPECIAL && $nt->equals( $this->getTitle() ) && !$nt->hasFragment() ) {
2648  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2649  continue;
2650  }
2651 
2652  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2653  # @todo FIXME: Should do batch file existence checks, see comment below
2654  if ( $ns === NS_MEDIA ) {
2655  # Give extensions a chance to select the file revision for us
2656  $options = [];
2657  $descQuery = false;
2658  $this->hookRunner->onBeforeParserFetchFileAndTitle(
2659  $this, $nt, $options, $descQuery );
2660  # Fetch and register the file (file title may be different via hooks)
2661  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2662  # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2663  $s .= $prefix . $this->armorLinks(
2664  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2665  continue;
2666  }
2667 
2668  # Some titles, such as valid special pages or files in foreign repos, should
2669  # be shown as bluelinks even though they're not included in the page table
2670  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2671  # batch file existence checks for NS_FILE and NS_MEDIA
2672  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2673  $this->mOutput->addLink( $nt );
2674  $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2675  } else {
2676  # Links will be added to the output link list after checking
2677  $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2678  }
2679  }
2680  return $holders;
2681  }
2682 
2696  private function makeKnownLinkHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
2697  list( $inside, $trail ) = Linker::splitTrail( $trail );
2698 
2699  if ( $text == '' ) {
2700  $text = htmlspecialchars( $nt->getPrefixedText() );
2701  }
2702 
2703  $link = $this->getLinkRenderer()->makeKnownLink(
2704  $nt, new HtmlArmor( "$prefix$text$inside" )
2705  );
2706 
2707  return $this->armorLinks( $link ) . $trail;
2708  }
2709 
2720  private function armorLinks( $text ) {
2721  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2722  self::MARKER_PREFIX . "NOPARSE$1", $text );
2723  }
2724 
2734  public function doBlockLevels( $text, $linestart ) {
2735  wfDeprecated( __METHOD__, '1.35' );
2736  return BlockLevelPass::doBlockLevels( $text, $linestart );
2737  }
2738 
2747  private function expandMagicVariable( $index, $frame = false ) {
2752  if (
2753  $this->hookRunner->onParserGetVariableValueVarCache( $this, $this->mVarCache ) &&
2754  isset( $this->mVarCache[$index] )
2755  ) {
2756  return $this->mVarCache[$index];
2757  }
2758 
2759  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2760  $this->hookRunner->onParserGetVariableValueTs( $this, $ts );
2761 
2762  $value = CoreMagicVariables::expand(
2763  $this, $index, $ts, $this->nsInfo, $this->svcOptions, $this->logger
2764  );
2765 
2766  if ( $value === null ) {
2767  // Not a defined core magic word
2768  $ret = null;
2769  $originalIndex = $index;
2770  $this->hookRunner->onParserGetVariableValueSwitch( $this,
2771  $this->mVarCache, $index, $ret, $frame );
2772  if ( $index !== $originalIndex ) {
2774  'A ParserGetVariableValueSwitch hook handler modified $index, ' .
2775  'this is deprecated since MediaWiki 1.35',
2776  '1.35', false, false
2777  );
2778  }
2779  if ( !isset( $this->mVarCache[$originalIndex] ) ||
2780  $this->mVarCache[$originalIndex] !== $ret ) {
2782  'A ParserGetVariableValueSwitch hook handler bypassed the cache, ' .
2783  'this is deprecated since MediaWiki 1.35', '1.35', false, false
2784  );
2785  }// FIXME: in the future, don't give this hook unrestricted
2786  // access to mVarCache; we can cache it ourselves by falling
2787  // through here.
2788  return $ret;
2789  }
2790 
2791  $this->mVarCache[$index] = $value;
2792 
2793  return $value;
2794  }
2795 
2800  private function initializeVariables() {
2801  $variableIDs = $this->magicWordFactory->getVariableIDs();
2802  $substIDs = $this->magicWordFactory->getSubstIDs();
2803 
2804  $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2805  $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2806  }
2807 
2830  public function preprocessToDom( $text, $flags = 0 ) {
2831  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2832  return $dom;
2833  }
2834 
2855  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2856  # Is there any text? Also, Prevent too big inclusions!
2857  $textSize = strlen( $text );
2858  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2859  return $text;
2860  }
2861 
2862  if ( $frame === false ) {
2863  $frame = $this->getPreprocessor()->newFrame();
2864  } elseif ( !( $frame instanceof PPFrame ) ) {
2865  $this->logger->debug(
2866  __METHOD__ . " called using plain parameters instead of " .
2867  "a PPFrame instance. Creating custom frame."
2868  );
2869  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2870  }
2871 
2872  $dom = $this->preprocessToDom( $text );
2873  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2874  $text = $frame->expand( $dom, $flags );
2875 
2876  return $text;
2877  }
2878 
2906  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2907  # does no harm if $current and $max are present but are unnecessary for the message
2908  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2909  # only during preview, and that would split the parser cache unnecessarily.
2910  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2911  ->text();
2912  $this->mOutput->addWarning( $warning );
2913  $this->addTrackingCategory( "$limitationType-category" );
2914  }
2915 
2929  public function braceSubstitution( array $piece, PPFrame $frame ) {
2930  // Flags
2931 
2932  // $text has been filled
2933  $found = false;
2934  // wiki markup in $text should be escaped
2935  $nowiki = false;
2936  // $text is HTML, armour it against wikitext transformation
2937  $isHTML = false;
2938  // Force interwiki transclusion to be done in raw mode not rendered
2939  $forceRawInterwiki = false;
2940  // $text is a DOM node needing expansion in a child frame
2941  $isChildObj = false;
2942  // $text is a DOM node needing expansion in the current frame
2943  $isLocalObj = false;
2944 
2945  # Title object, where $text came from
2946  $title = false;
2947 
2948  # $part1 is the bit before the first |, and must contain only title characters.
2949  # Various prefixes will be stripped from it later.
2950  $titleWithSpaces = $frame->expand( $piece['title'] );
2951  $part1 = trim( $titleWithSpaces );
2952  $titleText = false;
2953 
2954  # Original title text preserved for various purposes
2955  $originalTitle = $part1;
2956 
2957  # $args is a list of argument nodes, starting from index 0, not including $part1
2958  # @todo FIXME: If piece['parts'] is null then the call to getLength()
2959  # below won't work b/c this $args isn't an object
2960  $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
2961 
2962  $profileSection = null; // profile templates
2963 
2964  $sawDeprecatedTemplateEquals = false; // T91154
2965 
2966  # SUBST
2967  // @phan-suppress-next-line PhanRedundantCondition
2968  if ( !$found ) {
2969  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2970 
2971  # Possibilities for substMatch: "subst", "safesubst" or FALSE
2972  # Decide whether to expand template or keep wikitext as-is.
2973  if ( $this->ot['wiki'] ) {
2974  if ( $substMatch === false ) {
2975  $literal = true; # literal when in PST with no prefix
2976  } else {
2977  $literal = false; # expand when in PST with subst: or safesubst:
2978  }
2979  } else {
2980  if ( $substMatch == 'subst' ) {
2981  $literal = true; # literal when not in PST with plain subst:
2982  } else {
2983  $literal = false; # expand when not in PST with safesubst: or no prefix
2984  }
2985  }
2986  if ( $literal ) {
2987  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
2988  $isLocalObj = true;
2989  $found = true;
2990  }
2991  }
2992 
2993  # Variables
2994  if ( !$found && $args->getLength() == 0 ) {
2995  $id = $this->mVariables->matchStartToEnd( $part1 );
2996  if ( $id !== false ) {
2997  $text = $this->expandMagicVariable( $id, $frame );
2998  if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
2999  $this->mOutput->updateCacheExpiry(
3000  $this->magicWordFactory->getCacheTTL( $id ) );
3001  }
3002  $found = true;
3003  }
3004  }
3005 
3006  # MSG, MSGNW and RAW
3007  if ( !$found ) {
3008  # Check for MSGNW:
3009  $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3010  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3011  $nowiki = true;
3012  } else {
3013  # Remove obsolete MSG:
3014  $mwMsg = $this->magicWordFactory->get( 'msg' );
3015  $mwMsg->matchStartAndRemove( $part1 );
3016  }
3017 
3018  # Check for RAW:
3019  $mwRaw = $this->magicWordFactory->get( 'raw' );
3020  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3021  $forceRawInterwiki = true;
3022  }
3023  }
3024 
3025  # Parser functions
3026  if ( !$found ) {
3027  $colonPos = strpos( $part1, ':' );
3028  if ( $colonPos !== false ) {
3029  $func = substr( $part1, 0, $colonPos );
3030  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3031  $argsLength = $args->getLength();
3032  for ( $i = 0; $i < $argsLength; $i++ ) {
3033  $funcArgs[] = $args->item( $i );
3034  }
3035 
3036  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3037 
3038  // Extract any forwarded flags
3039  if ( isset( $result['title'] ) ) {
3040  $title = $result['title'];
3041  }
3042  if ( isset( $result['found'] ) ) {
3043  $found = $result['found'];
3044  }
3045  if ( array_key_exists( 'text', $result ) ) {
3046  // a string or null
3047  $text = $result['text'];
3048  }
3049  if ( isset( $result['nowiki'] ) ) {
3050  $nowiki = $result['nowiki'];
3051  }
3052  if ( isset( $result['isHTML'] ) ) {
3053  $isHTML = $result['isHTML'];
3054  }
3055  if ( isset( $result['forceRawInterwiki'] ) ) {
3056  $forceRawInterwiki = $result['forceRawInterwiki'];
3057  }
3058  if ( isset( $result['isChildObj'] ) ) {
3059  $isChildObj = $result['isChildObj'];
3060  }
3061  if ( isset( $result['isLocalObj'] ) ) {
3062  $isLocalObj = $result['isLocalObj'];
3063  }
3064  }
3065  }
3066 
3067  # Finish mangling title and then check for loops.
3068  # Set $title to a Title object and $titleText to the PDBK
3069  if ( !$found ) {
3070  $ns = NS_TEMPLATE;
3071  # Split the title into page and subpage
3072  $subpage = '';
3073  $relative = Linker::normalizeSubpageLink(
3074  $this->getTitle(), $part1, $subpage
3075  );
3076  if ( $part1 !== $relative ) {
3077  $part1 = $relative;
3078  $ns = $this->getTitle()->getNamespace();
3079  }
3080  $title = Title::newFromText( $part1, $ns );
3081  if ( $title ) {
3082  $titleText = $title->getPrefixedText();
3083  # Check for language variants if the template is not found
3084  if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3085  $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3086  }
3087  # Do recursion depth check
3088  $limit = $this->mOptions->getMaxTemplateDepth();
3089  if ( $frame->depth >= $limit ) {
3090  $found = true;
3091  $text = '<span class="error">'
3092  . wfMessage( 'parser-template-recursion-depth-warning' )
3093  ->numParams( $limit )->inContentLanguage()->text()
3094  . '</span>';
3095  }
3096  }
3097  }
3098 
3099  # Load from database
3100  if ( !$found && $title ) {
3101  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3102  if ( !$title->isExternal() ) {
3103  if ( $title->isSpecialPage()
3104  && $this->mOptions->getAllowSpecialInclusion()
3105  && $this->ot['html']
3106  ) {
3107  $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3108  // Pass the template arguments as URL parameters.
3109  // "uselang" will have no effect since the Language object
3110  // is forced to the one defined in ParserOptions.
3111  $pageArgs = [];
3112  $argsLength = $args->getLength();
3113  for ( $i = 0; $i < $argsLength; $i++ ) {
3114  $bits = $args->item( $i )->splitArg();
3115  if ( strval( $bits['index'] ) === '' ) {
3116  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3117  $value = trim( $frame->expand( $bits['value'] ) );
3118  $pageArgs[$name] = $value;
3119  }
3120  }
3121 
3122  // Create a new context to execute the special page
3123  $context = new RequestContext;
3124  $context->setTitle( $title );
3125  $context->setRequest( new FauxRequest( $pageArgs ) );
3126  if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3127  $context->setUser( $this->getUser() );
3128  } else {
3129  // If this page is cached, then we better not be per user.
3130  $context->setUser( User::newFromName( '127.0.0.1', false ) );
3131  }
3132  $context->setLanguage( $this->mOptions->getUserLangObj() );
3133  $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3134  if ( $ret ) {
3135  $text = $context->getOutput()->getHTML();
3136  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3137  $found = true;
3138  $isHTML = true;
3139  if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3140  $this->mOutput->updateRuntimeAdaptiveExpiry(
3141  $specialPage->maxIncludeCacheTime()
3142  );
3143  }
3144  }
3145  } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3146  $found = false; # access denied
3147  $this->logger->debug(
3148  __METHOD__ .
3149  ": template inclusion denied for " . $title->getPrefixedDBkey()
3150  );
3151  } else {
3152  list( $text, $title ) = $this->getTemplateDom( $title );
3153  if ( $text !== false ) {
3154  $found = true;
3155  $isChildObj = true;
3156  if (
3157  $title->getNamespace() === NS_TEMPLATE &&
3158  $title->getDBkey() === '=' &&
3159  $originalTitle === '='
3160  ) {
3161  // Note that we won't get here if `=` is evaluated
3162  // (in the future) as a parser function, nor if
3163  // the Template namespace is given explicitly,
3164  // ie `{{Template:=}}`. Only `{{=}}` triggers.
3165  $sawDeprecatedTemplateEquals = true; // T91154
3166  }
3167  }
3168  }
3169 
3170  # If the title is valid but undisplayable, make a link to it
3171  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3172  $text = "[[:$titleText]]";
3173  $found = true;
3174  }
3175  } elseif ( $title->isTrans() ) {
3176  # Interwiki transclusion
3177  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3178  $text = $this->interwikiTransclude( $title, 'render' );
3179  $isHTML = true;
3180  } else {
3181  $text = $this->interwikiTransclude( $title, 'raw' );
3182  # Preprocess it like a template
3183  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3184  $isChildObj = true;
3185  }
3186  $found = true;
3187  }
3188 
3189  # Do infinite loop check
3190  # This has to be done after redirect resolution to avoid infinite loops via redirects
3191  if ( !$frame->loopCheck( $title ) ) {
3192  $found = true;
3193  $text = '<span class="error">'
3194  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3195  . '</span>';
3196  $this->addTrackingCategory( 'template-loop-category' );
3197  $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3198  wfEscapeWikiText( $titleText ) )->text() );
3199  $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3200  }
3201  }
3202 
3203  # If we haven't found text to substitute by now, we're done
3204  # Recover the source wikitext and return it
3205  if ( !$found ) {
3206  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3207  if ( $profileSection ) {
3208  $this->mProfiler->scopedProfileOut( $profileSection );
3209  }
3210  return [ 'object' => $text ];
3211  }
3212 
3213  # Expand DOM-style return values in a child frame
3214  if ( $isChildObj ) {
3215  # Clean up argument array
3216  $newFrame = $frame->newChild( $args, $title );
3217 
3218  if ( $nowiki ) {
3219  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3220  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3221  # Expansion is eligible for the empty-frame cache
3222  $text = $newFrame->cachedExpand( $titleText, $text );
3223  } else {
3224  # Uncached expansion
3225  $text = $newFrame->expand( $text );
3226  }
3227  }
3228  if ( $isLocalObj && $nowiki ) {
3229  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3230  $isLocalObj = false;
3231  }
3232 
3233  if ( $profileSection ) {
3234  $this->mProfiler->scopedProfileOut( $profileSection );
3235  }
3236  if (
3237  $sawDeprecatedTemplateEquals &&
3238  $this->mStripState->unstripBoth( $text ) !== '='
3239  ) {
3240  // T91154: {{=}} is deprecated when it doesn't expand to `=`;
3241  // use {{Template:=}} if you must.
3242  $this->addTrackingCategory( 'template-equals-category' );
3243  $this->mOutput->addWarning( wfMessage( 'template-equals-warning' )->text() );
3244  }
3245 
3246  # Replace raw HTML by a placeholder
3247  if ( $isHTML ) {
3248  $text = $this->insertStripItem( $text );
3249  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3250  # Escape nowiki-style return values
3251  $text = wfEscapeWikiText( $text );
3252  } elseif ( is_string( $text )
3253  && !$piece['lineStart']
3254  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3255  ) {
3256  # T2529: if the template begins with a table or block-level
3257  # element, it should be treated as beginning a new line.
3258  # This behavior is somewhat controversial.
3259  $text = "\n" . $text;
3260  }
3261 
3262  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3263  # Error, oversize inclusion
3264  if ( $titleText !== false ) {
3265  # Make a working, properly escaped link if possible (T25588)
3266  $text = "[[:$titleText]]";
3267  } else {
3268  # This will probably not be a working link, but at least it may
3269  # provide some hint of where the problem is
3270  preg_replace( '/^:/', '', $originalTitle );
3271  $text = "[[:$originalTitle]]";
3272  }
3273  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3274  . 'post-expand include size too large -->' );
3275  $this->limitationWarn( 'post-expand-template-inclusion' );
3276  }
3277 
3278  if ( $isLocalObj ) {
3279  $ret = [ 'object' => $text ];
3280  } else {
3281  $ret = [ 'text' => $text ];
3282  }
3283 
3284  return $ret;
3285  }
3286 
3305  public function callParserFunction( PPFrame $frame, $function, array $args = [] ) {
3306  # Case sensitive functions
3307  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3308  $function = $this->mFunctionSynonyms[1][$function];
3309  } else {
3310  # Case insensitive functions
3311  $function = $this->contLang->lc( $function );
3312  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3313  $function = $this->mFunctionSynonyms[0][$function];
3314  } else {
3315  return [ 'found' => false ];
3316  }
3317  }
3318 
3319  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3320 
3321  $allArgs = [ $this ];
3322  if ( $flags & self::SFH_OBJECT_ARGS ) {
3323  # Convert arguments to PPNodes and collect for appending to $allArgs
3324  $funcArgs = [];
3325  foreach ( $args as $k => $v ) {
3326  if ( $v instanceof PPNode || $k === 0 ) {
3327  $funcArgs[] = $v;
3328  } else {
3329  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3330  }
3331  }
3332 
3333  # Add a frame parameter, and pass the arguments as an array
3334  $allArgs[] = $frame;
3335  $allArgs[] = $funcArgs;
3336  } else {
3337  # Convert arguments to plain text and append to $allArgs
3338  foreach ( $args as $k => $v ) {
3339  if ( $v instanceof PPNode ) {
3340  $allArgs[] = trim( $frame->expand( $v ) );
3341  } elseif ( is_int( $k ) && $k >= 0 ) {
3342  $allArgs[] = trim( $v );
3343  } else {
3344  $allArgs[] = trim( "$k=$v" );
3345  }
3346  }
3347  }
3348 
3349  $result = $callback( ...$allArgs );
3350 
3351  # The interface for function hooks allows them to return a wikitext
3352  # string or an array containing the string and any flags. This mungs
3353  # things around to match what this method should return.
3354  if ( !is_array( $result ) ) {
3355  $result = [
3356  'found' => true,
3357  'text' => $result,
3358  ];
3359  } else {
3360  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3361  $result['text'] = $result[0];
3362  }
3363  unset( $result[0] );
3364  $result += [
3365  'found' => true,
3366  ];
3367  }
3368 
3369  $noparse = true;
3370  $preprocessFlags = 0;
3371  if ( isset( $result['noparse'] ) ) {
3372  $noparse = $result['noparse'];
3373  }
3374  if ( isset( $result['preprocessFlags'] ) ) {
3375  $preprocessFlags = $result['preprocessFlags'];
3376  }
3377 
3378  if ( !$noparse ) {
3379  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3380  $result['isChildObj'] = true;
3381  }
3382 
3383  return $result;
3384  }
3385 
3394  public function getTemplateDom( Title $title ) {
3395  $cacheTitle = $title;
3396  $titleText = $title->getPrefixedDBkey();
3397 
3398  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3399  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3400  $title = Title::makeTitle( $ns, $dbk );
3401  $titleText = $title->getPrefixedDBkey();
3402  }
3403  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3404  return [ $this->mTplDomCache[$titleText], $title ];
3405  }
3406 
3407  # Cache miss, go to the database
3408  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3409 
3410  if ( $text === false ) {
3411  $this->mTplDomCache[$titleText] = false;
3412  return [ false, $title ];
3413  }
3414 
3415  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3416  $this->mTplDomCache[$titleText] = $dom;
3417 
3418  if ( !$title->equals( $cacheTitle ) ) {
3419  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3420  [ $title->getNamespace(), $title->getDBkey() ];
3421  }
3422 
3423  return [ $dom, $title ];
3424  }
3425 
3441  wfDeprecated( __METHOD__, '1.35' );
3442  $revisionRecord = $this->fetchCurrentRevisionRecordOfTitle( $title );
3443  if ( $revisionRecord ) {
3444  return new Revision( $revisionRecord );
3445  }
3446  return $revisionRecord;
3447  }
3448 
3463  $cacheKey = $title->getPrefixedDBkey();
3464  if ( !$this->currentRevisionCache ) {
3465  $this->currentRevisionCache = new MapCacheLRU( 100 );
3466  }
3467  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3468  $revisionRecord =
3469  // Defaults to Parser::statelessFetchRevisionRecord()
3470  call_user_func(
3471  $this->mOptions->getCurrentRevisionRecordCallback(),
3472  $title,
3473  $this
3474  );
3475  if ( !$revisionRecord ) {
3476  // Parser::statelessFetchRevisionRecord() can return false;
3477  // normalize it to null.
3478  $revisionRecord = null;
3479  }
3480  $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3481  }
3482  return $this->currentRevisionCache->get( $cacheKey );
3483  }
3484 
3492  return (
3493  $this->currentRevisionCache &&
3494  $this->currentRevisionCache->has( $title->getPrefixedText() )
3495  );
3496  }
3497 
3508  public static function statelessFetchRevision( Title $title, $parser = false ) {
3509  wfDeprecated( __METHOD__, '1.35' );
3510  $revRecord = MediaWikiServices::getInstance()
3511  ->getRevisionLookup()
3512  ->getKnownCurrentRevision( $title );
3513  return $revRecord ? new Revision( $revRecord ) : false;
3514  }
3515 
3525  public static function statelessFetchRevisionRecord( Title $title, $parser = null ) {
3526  $revRecord = MediaWikiServices::getInstance()
3527  ->getRevisionLookup()
3528  ->getKnownCurrentRevision( $title );
3529  return $revRecord;
3530  }
3531 
3537  public function fetchTemplateAndTitle( Title $title ) {
3538  // Defaults to Parser::statelessFetchTemplate()
3539  $templateCb = $this->mOptions->getTemplateCallback();
3540  $stuff = call_user_func( $templateCb, $title, $this );
3541  if ( isset( $stuff['revision-record'] ) ) {
3542  $revRecord = $stuff['revision-record'];
3543  } else {
3544  // Triggers deprecation warnings via DeprecatablePropertyArray
3545  $rev = $stuff['revision'] ?? null;
3546  if ( $rev instanceof Revision ) {
3547  $revRecord = $rev->getRevisionRecord();
3548  } else {
3549  $revRecord = null;
3550  }
3551  }
3552 
3553  $text = $stuff['text'];
3554  if ( is_string( $stuff['text'] ) ) {
3555  // We use U+007F DELETE to distinguish strip markers from regular text
3556  $text = strtr( $text, "\x7f", "?" );
3557  }
3558  $finalTitle = $stuff['finalTitle'] ?? $title;
3559  foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3560  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3561  if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3562  // Self-transclusion; final result may change based on the new page version
3563  try {
3564  $sha1 = $revRecord->getSha1();
3565  } catch ( RevisionAccessException $e ) {
3566  $sha1 = null;
3567  }
3568  $this->setOutputFlag( 'vary-revision-sha1', 'Self transclusion' );
3569  $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3570  }
3571  }
3572 
3573  return [ $text, $finalTitle ];
3574  }
3575 
3582  public function fetchTemplate( Title $title ) {
3583  wfDeprecated( __METHOD__, '1.35' );
3584  return $this->fetchTemplateAndTitle( $title )[0];
3585  }
3586 
3596  public static function statelessFetchTemplate( $title, $parser = false ) {
3597  $text = $skip = false;
3598  $finalTitle = $title;
3599  $deps = [];
3600  $revRecord = null;
3601 
3602  # Loop to fetch the article, with up to 1 redirect
3603  $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
3604  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3605  # Give extensions a chance to select the revision instead
3606  $id = false; # Assume current
3607  Hooks::runner()->onBeforeParserFetchTemplateAndtitle(
3608  $parser, $title, $skip, $id );
3609 
3610  if ( $skip ) {
3611  $text = false;
3612  $deps[] = [
3613  'title' => $title,
3614  'page_id' => $title->getArticleID(),
3615  'rev_id' => null
3616  ];
3617  break;
3618  }
3619  # Get the revision
3620  # TODO rewrite using only RevisionRecord objects
3621  if ( $id ) {
3622  $revRecord = $revLookup->getRevisionById( $id );
3623  } elseif ( $parser ) {
3624  $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3625  } else {
3626  $revRecord = $revLookup->getRevisionByTitle( $title );
3627  }
3628  $rev_id = $revRecord ? $revRecord->getId() : 0;
3629  # If there is no current revision, there is no page
3630  if ( $id === false && !$revRecord ) {
3631  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3632  $linkCache->addBadLinkObj( $title );
3633  }
3634 
3635  $deps[] = [
3636  'title' => $title,
3637  'page_id' => $title->getArticleID(),
3638  'rev_id' => $rev_id
3639  ];
3640  if ( $revRecord ) {
3641  $revTitle = Title::newFromLinkTarget(
3642  $revRecord->getPageAsLinkTarget()
3643  );
3644  if ( !$title->equals( $revTitle ) ) {
3645  # We fetched a rev from a different title; register it too...
3646  $deps[] = [
3647  'title' => $revTitle,
3648  'page_id' => $revRecord->getPageId(),
3649  'rev_id' => $rev_id
3650  ];
3651  }
3652  }
3653 
3654  if ( $revRecord ) {
3655  $content = $revRecord->getContent( SlotRecord::MAIN );
3656  $text = $content ? $content->getWikitextForTransclusion() : null;
3657 
3658  // Hook is hard deprecated since 1.35
3659  if ( Hooks::isRegistered( 'ParserFetchTemplate' ) ) {
3660  // Only create the Revision object if needed
3661  $legacyRevision = new Revision( $revRecord );
3662  Hooks::runner()->onParserFetchTemplate(
3663  $parser,
3664  $title,
3665  $legacyRevision,
3666  $text,
3667  $deps
3668  );
3669  }
3670 
3671  if ( $text === false || $text === null ) {
3672  $text = false;
3673  break;
3674  }
3675  } elseif ( $title->getNamespace() === NS_MEDIAWIKI ) {
3676  $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3677  lcfirst( $title->getText() ) )->inContentLanguage();
3678  if ( !$message->exists() ) {
3679  $text = false;
3680  break;
3681  }
3682  $content = $message->content();
3683  $text = $message->plain();
3684  } else {
3685  break;
3686  }
3687  if ( !$content ) {
3688  break;
3689  }
3690  # Redirect?
3691  $finalTitle = $title;
3692  $title = $content->getRedirectTarget();
3693  }
3694 
3695  $legacyRevision = function () use ( $revRecord ) {
3696  return $revRecord ? new Revision( $revRecord ) : null;
3697  };
3698  $retValues = [
3699  'revision' => $legacyRevision,
3700  'revision-record' => $revRecord ?: false, // So isset works
3701  'text' => $text,
3702  'finalTitle' => $finalTitle,
3703  'deps' => $deps
3704  ];
3705  $propertyArray = new DeprecatablePropertyArray(
3706  $retValues,
3707  [ 'revision' => '1.35' ],
3708  __METHOD__
3709  );
3710  return $propertyArray;
3711  }
3712 
3720  public function fetchFileAndTitle( Title $title, array $options = [] ) {
3721  $file = $this->fetchFileNoRegister( $title, $options );
3722 
3723  $time = $file ? $file->getTimestamp() : false;
3724  $sha1 = $file ? $file->getSha1() : false;
3725  # Register the file as a dependency...
3726  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3727  if ( $file && !$title->equals( $file->getTitle() ) ) {
3728  # Update fetched file title
3729  $title = $file->getTitle();
3730  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3731  }
3732  return [ $file, $title ];
3733  }
3734 
3745  protected function fetchFileNoRegister( Title $title, array $options = [] ) {
3746  if ( isset( $options['broken'] ) ) {
3747  $file = false; // broken thumbnail forced by hook
3748  } else {
3749  $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
3750  if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3751  $file = $repoGroup->findFileFromKey( $options['sha1'], $options );
3752  } else { // get by (name,timestamp)
3753  $file = $repoGroup->findFile( $title, $options );
3754  }
3755  }
3756  return $file;
3757  }
3758 
3768  public function interwikiTransclude( Title $title, $action ) {
3769  if ( !$this->svcOptions->get( 'EnableScaryTranscluding' ) ) {
3770  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3771  }
3772 
3773  $url = $title->getFullURL( [ 'action' => $action ] );
3774  if ( strlen( $url ) > 1024 ) {
3775  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3776  }
3777 
3778  $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3779 
3780  $fname = __METHOD__;
3781  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3782 
3783  $data = $cache->getWithSetCallback(
3784  $cache->makeGlobalKey(
3785  'interwiki-transclude',
3786  ( $wikiId !== false ) ? $wikiId : 'external',
3787  sha1( $url )
3788  ),
3789  $this->svcOptions->get( 'TranscludeCacheExpiry' ),
3790  function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3791  $req = MWHttpRequest::factory( $url, [], $fname );
3792 
3793  $status = $req->execute(); // Status object
3794  if ( !$status->isOK() ) {
3795  $ttl = $cache::TTL_UNCACHEABLE;
3796  } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3797  $ttl = min( $cache::TTL_LAGGED, $ttl );
3798  }
3799 
3800  return [
3801  'text' => $status->isOK() ? $req->getContent() : null,
3802  'code' => $req->getStatus()
3803  ];
3804  },
3805  [
3806  'checkKeys' => ( $wikiId !== false )
3807  ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3808  : [],
3809  'pcGroup' => 'interwiki-transclude:5',
3810  'pcTTL' => $cache::TTL_PROC_LONG
3811  ]
3812  );
3813 
3814  if ( is_string( $data['text'] ) ) {
3815  $text = $data['text'];
3816  } elseif ( $data['code'] != 200 ) {
3817  // Though we failed to fetch the content, this status is useless.
3818  $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3819  ->params( $url, $data['code'] )->inContentLanguage()->text();
3820  } else {
3821  $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3822  }
3823 
3824  return $text;
3825  }
3826 
3837  public function argSubstitution( array $piece, PPFrame $frame ) {
3838  $error = false;
3839  $parts = $piece['parts'];
3840  $nameWithSpaces = $frame->expand( $piece['title'] );
3841  $argName = trim( $nameWithSpaces );
3842  $object = false;
3843  $text = $frame->getArgument( $argName );
3844  if ( $text === false && $parts->getLength() > 0
3845  && ( $this->ot['html']
3846  || $this->ot['pre']
3847  || ( $this->ot['wiki'] && $frame->isTemplate() )
3848  )
3849  ) {
3850  # No match in frame, use the supplied default
3851  $object = $parts->item( 0 )->getChildren();
3852  }
3853  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3854  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3855  $this->limitationWarn( 'post-expand-template-argument' );
3856  }
3857 
3858  if ( $text === false && $object === false ) {
3859  # No match anywhere
3860  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3861  }
3862  if ( $error !== false ) {
3863  $text .= $error;
3864  }
3865  if ( $object !== false ) {
3866  $ret = [ 'object' => $object ];
3867  } else {
3868  $ret = [ 'text' => $text ];
3869  }
3870 
3871  return $ret;
3872  }
3873 
3890  public function extensionSubstitution( array $params, PPFrame $frame ) {
3891  static $errorStr = '<span class="error">';
3892  static $errorLen = 20;
3893 
3894  $name = $frame->expand( $params['name'] );
3895  if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3896  // Probably expansion depth or node count exceeded. Just punt the
3897  // error up.
3898  return $name;
3899  }
3900 
3901  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3902  if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3903  // See above
3904  return $attrText;
3905  }
3906 
3907  // We can't safely check if the expansion for $content resulted in an
3908  // error, because the content could happen to be the error string
3909  // (T149622).
3910  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3911 
3912  $marker = self::MARKER_PREFIX . "-$name-"
3913  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3914 
3915  $markerType = 'general';
3916  if ( $this->ot['html'] ) {
3917  $name = strtolower( $name );
3918  $attributes = Sanitizer::decodeTagAttributes( $attrText );
3919  if ( isset( $params['attributes'] ) ) {
3920  $attributes += $params['attributes'];
3921  }
3922 
3923  if ( isset( $this->mTagHooks[$name] ) ) {
3924  $output = call_user_func_array( $this->mTagHooks[$name],
3925  [ $content, $attributes, $this, $frame ] );
3926  } else {
3927  $output = '<span class="error">Invalid tag extension name: ' .
3928  htmlspecialchars( $name ) . '</span>';
3929  }
3930 
3931  if ( is_array( $output ) ) {
3932  // Extract flags
3933  $flags = $output;
3934  $output = $flags[0];
3935  if ( isset( $flags['markerType'] ) ) {
3936  $markerType = $flags['markerType'];
3937  }
3938  }
3939  } else {
3940  if ( $attrText === null ) {
3941  $attrText = '';
3942  }
3943  if ( isset( $params['attributes'] ) ) {
3944  foreach ( $params['attributes'] as $attrName => $attrValue ) {
3945  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3946  htmlspecialchars( $attrValue ) . '"';
3947  }
3948  }
3949  if ( $content === null ) {
3950  $output = "<$name$attrText/>";
3951  } else {
3952  $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
3953  if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3954  // See above
3955  return $close;
3956  }
3957  $output = "<$name$attrText>$content$close";
3958  }
3959  }
3960 
3961  if ( $markerType === 'none' ) {
3962  return $output;
3963  } elseif ( $markerType === 'nowiki' ) {
3964  $this->mStripState->addNoWiki( $marker, $output );
3965  } elseif ( $markerType === 'general' ) {
3966  $this->mStripState->addGeneral( $marker, $output );
3967  } else {
3968  throw new MWException( __METHOD__ . ': invalid marker type' );
3969  }
3970  return $marker;
3971  }
3972 
3980  private function incrementIncludeSize( $type, $size ) {
3981  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3982  return false;
3983  } else {
3984  $this->mIncludeSizes[$type] += $size;
3985  return true;
3986  }
3987  }
3988 
3995  $this->mExpensiveFunctionCount++;
3996  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3997  }
3998 
4006  private function handleDoubleUnderscore( $text ) {
4007  # The position of __TOC__ needs to be recorded
4008  $mw = $this->magicWordFactory->get( 'toc' );
4009  if ( $mw->match( $text ) ) {
4010  $this->mShowToc = true;
4011  $this->mForceTocPosition = true;
4012 
4013  # Set a placeholder. At the end we'll fill it in with the TOC.
4014  $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4015 
4016  # Only keep the first one.
4017  $text = $mw->replace( '', $text );
4018  }
4019 
4020  # Now match and remove the rest of them
4021  $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4022  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4023 
4024  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4025  $this->getOutput()->setNoGallery( true );
4026  }
4027  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4028  $this->mShowToc = false;
4029  }
4030  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4031  && $this->getTitle()->getNamespace() === NS_CATEGORY
4032  ) {
4033  $this->addTrackingCategory( 'hidden-category-category' );
4034  }
4035  # (T10068) Allow control over whether robots index a page.
4036  # __INDEX__ always overrides __NOINDEX__, see T16899
4037  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->getTitle()->canUseNoindex() ) {
4038  $this->mOutput->setIndexPolicy( 'noindex' );
4039  $this->addTrackingCategory( 'noindex-category' );
4040  }
4041  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->getTitle()->canUseNoindex() ) {
4042  $this->mOutput->setIndexPolicy( 'index' );
4043  $this->addTrackingCategory( 'index-category' );
4044  }
4045 
4046  # Cache all double underscores in the database
4047  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4048  $this->mOutput->setProperty( $key, '' );
4049  }
4050 
4051  return $text;
4052  }
4053 
4059  public function addTrackingCategory( $msg ) {
4060  return $this->mOutput->addTrackingCategory( $msg, $this->getTitle() );
4061  }
4062 
4078  private function finalizeHeadings( $text, $origText, $isMain = true ) {
4079  # Inhibit editsection links if requested in the page
4080  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4081  $maybeShowEditLink = false;
4082  } else {
4083  $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4084  }
4085 
4086  # Get all headlines for numbering them and adding funky stuff like [edit]
4087  # links - this is for later, but we need the number of headlines right now
4088  # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4089  # be trimmed here since whitespace in HTML headings is significant.
4090  $matches = [];
4091  $numMatches = preg_match_all(
4092  '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4093  $text,
4094  $matches
4095  );
4096 
4097  # if there are fewer than 4 headlines in the article, do not show TOC
4098  # unless it's been explicitly enabled.
4099  $enoughToc = $this->mShowToc &&
4100  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4101 
4102  # Allow user to stipulate that a page should have a "new section"
4103  # link added via __NEWSECTIONLINK__
4104  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4105  $this->mOutput->setNewSection( true );
4106  }
4107 
4108  # Allow user to remove the "new section"
4109  # link via __NONEWSECTIONLINK__
4110  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4111  $this->mOutput->hideNewSection( true );
4112  }
4113 
4114  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4115  # override above conditions and always show TOC above first header
4116  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4117  $this->mShowToc = true;
4118  $enoughToc = true;
4119  }
4120 
4121  # headline counter
4122  $headlineCount = 0;
4123  $numVisible = 0;
4124 
4125  # Ugh .. the TOC should have neat indentation levels which can be
4126  # passed to the skin functions. These are determined here
4127  $toc = '';
4128  $full = '';
4129  $head = [];
4130  $sublevelCount = [];
4131  $levelCount = [];
4132  $level = 0;
4133  $prevlevel = 0;
4134  $toclevel = 0;
4135  $prevtoclevel = 0;
4136  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4137  $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4138  $oldType = $this->mOutputType;
4139  $this->setOutputType( self::OT_WIKI );
4140  $frame = $this->getPreprocessor()->newFrame();
4141  $root = $this->preprocessToDom( $origText );
4142  $node = $root->getFirstChild();
4143  $byteOffset = 0;
4144  $tocraw = [];
4145  $refers = [];
4146 
4147  $headlines = $numMatches !== false ? $matches[3] : [];
4148 
4149  $maxTocLevel = $this->svcOptions->get( 'MaxTocLevel' );
4150  foreach ( $headlines as $headline ) {
4151  $isTemplate = false;
4152  $titleText = false;
4153  $sectionIndex = false;
4154  $numbering = '';
4155  $markerMatches = [];
4156  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4157  $serial = $markerMatches[1];
4158  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4159  $isTemplate = ( $titleText != $baseTitleText );
4160  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4161  }
4162 
4163  if ( $toclevel ) {
4164  $prevlevel = $level;
4165  }
4166  $level = $matches[1][$headlineCount];
4167 
4168  if ( $level > $prevlevel ) {
4169  # Increase TOC level
4170  $toclevel++;
4171  $sublevelCount[$toclevel] = 0;
4172  if ( $toclevel < $maxTocLevel ) {
4173  $prevtoclevel = $toclevel;
4174  $toc .= Linker::tocIndent();
4175  $numVisible++;
4176  }
4177  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4178  # Decrease TOC level, find level to jump to
4179 
4180  for ( $i = $toclevel; $i > 0; $i-- ) {
4181  // @phan-suppress-next-line PhanTypeInvalidDimOffset
4182  if ( $levelCount[$i] == $level ) {
4183  # Found last matching level
4184  $toclevel = $i;
4185  break;
4186  } elseif ( $levelCount[$i] < $level ) {
4187  // @phan-suppress-previous-line PhanTypeInvalidDimOffset
4188  # Found first matching level below current level
4189  $toclevel = $i + 1;
4190  break;
4191  }
4192  }
4193  if ( $i == 0 ) {
4194  $toclevel = 1;
4195  }
4196  if ( $toclevel < $maxTocLevel ) {
4197  if ( $prevtoclevel < $maxTocLevel ) {
4198  # Unindent only if the previous toc level was shown :p
4199  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4200  $prevtoclevel = $toclevel;
4201  } else {
4202  $toc .= Linker::tocLineEnd();
4203  }
4204  }
4205  } else {
4206  # No change in level, end TOC line
4207  if ( $toclevel < $maxTocLevel ) {
4208  $toc .= Linker::tocLineEnd();
4209  }
4210  }
4211 
4212  $levelCount[$toclevel] = $level;
4213 
4214  # count number of headlines for each level
4215  $sublevelCount[$toclevel]++;
4216  $dot = 0;
4217  for ( $i = 1; $i <= $toclevel; $i++ ) {
4218  if ( !empty( $sublevelCount[$i] ) ) {
4219  if ( $dot ) {
4220  $numbering .= '.';
4221  }
4222  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4223  $dot = 1;
4224  }
4225  }
4226 
4227  # The safe header is a version of the header text safe to use for links
4228 
4229  # Remove link placeholders by the link text.
4230  # <!--LINK number-->
4231  # turns into
4232  # link text with suffix
4233  # Do this before unstrip since link text can contain strip markers
4234  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4235 
4236  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4237  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4238 
4239  # Remove any <style> or <script> tags (T198618)
4240  $safeHeadline = preg_replace(
4241  '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4242  '',
4243  $safeHeadline
4244  );
4245 
4246  # Strip out HTML (first regex removes any tag not allowed)
4247  # Allowed tags are:
4248  # * <sup> and <sub> (T10393)
4249  # * <i> (T28375)
4250  # * <b> (r105284)
4251  # * <bdi> (T74884)
4252  # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4253  # * <s> and <strike> (T35715)
4254  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4255  # to allow setting directionality in toc items.
4256  $tocline = preg_replace(
4257  [
4258  '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4259  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4260  ],
4261  [ '', '<$1>' ],
4262  $safeHeadline
4263  );
4264 
4265  # Strip '<span></span>', which is the result from the above if
4266  # <span id="foo"></span> is used to produce an additional anchor
4267  # for a section.
4268  $tocline = str_replace( '<span></span>', '', $tocline );
4269 
4270  $tocline = trim( $tocline );
4271 
4272  # For the anchor, strip out HTML-y stuff period
4273  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4274  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4275 
4276  # Save headline for section edit hint before it's escaped
4277  $headlineHint = $safeHeadline;
4278 
4279  # Decode HTML entities
4280  $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4281 
4282  $safeHeadline = self::normalizeSectionName( $safeHeadline );
4283 
4284  $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4285  $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4286  $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4287  if ( $fallbackHeadline === $safeHeadline ) {
4288  # No reason to have both (in fact, we can't)
4289  $fallbackHeadline = false;
4290  }
4291 
4292  # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4293  # @todo FIXME: We may be changing them depending on the current locale.
4294  $arrayKey = strtolower( $safeHeadline );
4295  if ( $fallbackHeadline === false ) {
4296  $fallbackArrayKey = false;
4297  } else {
4298  $fallbackArrayKey = strtolower( $fallbackHeadline );
4299  }
4300 
4301  # Create the anchor for linking from the TOC to the section
4302  $anchor = $safeHeadline;
4303  $fallbackAnchor = $fallbackHeadline;
4304  if ( isset( $refers[$arrayKey] ) ) {
4305  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4306  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4307  $anchor .= "_$i";
4308  $linkAnchor .= "_$i";
4309  $refers["${arrayKey}_$i"] = true;
4310  } else {
4311  $refers[$arrayKey] = true;
4312  }
4313  if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4314  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4315  for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4316  $fallbackAnchor .= "_$i";
4317  $refers["${fallbackArrayKey}_$i"] = true;
4318  } else {
4319  $refers[$fallbackArrayKey] = true;
4320  }
4321 
4322  # Don't number the heading if it is the only one (looks silly)
4323  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4324  # the two are different if the line contains a link
4325  $headline = Html::element(
4326  'span',
4327  [ 'class' => 'mw-headline-number' ],
4328  $numbering
4329  ) . ' ' . $headline;
4330  }
4331 
4332  if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4333  $toc .= Linker::tocLine( $linkAnchor, $tocline,
4334  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4335  }
4336 
4337  # Add the section to the section tree
4338  # Find the DOM node for this header
4339  $noOffset = ( $isTemplate || $sectionIndex === false );
4340  while ( $node && !$noOffset ) {
4341  if ( $node->getName() === 'h' ) {
4342  $bits = $node->splitHeading();
4343  if ( $bits['i'] == $sectionIndex ) {
4344  break;
4345  }
4346  }
4347  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4348  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4349  $node = $node->getNextSibling();
4350  }
4351  $tocraw[] = [
4352  'toclevel' => $toclevel,
4353  'level' => $level,
4354  'line' => $tocline,
4355  'number' => $numbering,
4356  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4357  'fromtitle' => $titleText,
4358  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4359  'anchor' => $anchor,
4360  ];
4361 
4362  # give headline the correct <h#> tag
4363  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4364  // Output edit section links as markers with styles that can be customized by skins
4365  if ( $isTemplate ) {
4366  # Put a T flag in the section identifier, to indicate to extractSections()
4367  # that sections inside <includeonly> should be counted.
4368  $editsectionPage = $titleText;
4369  $editsectionSection = "T-$sectionIndex";
4370  $editsectionContent = null;
4371  } else {
4372  $editsectionPage = $this->getTitle()->getPrefixedText();
4373  $editsectionSection = $sectionIndex;
4374  $editsectionContent = $headlineHint;
4375  }
4376  // We use a bit of pesudo-xml for editsection markers. The
4377  // language converter is run later on. Using a UNIQ style marker
4378  // leads to the converter screwing up the tokens when it
4379  // converts stuff. And trying to insert strip tags fails too. At
4380  // this point all real inputted tags have already been escaped,
4381  // so we don't have to worry about a user trying to input one of
4382  // these markers directly. We use a page and section attribute
4383  // to stop the language converter from converting these
4384  // important bits of data, but put the headline hint inside a
4385  // content block because the language converter is supposed to
4386  // be able to convert that piece of data.
4387  // Gets replaced with html in ParserOutput::getText
4388  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4389  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4390  if ( $editsectionContent !== null ) {
4391  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4392  } else {
4393  $editlink .= '/>';
4394  }
4395  } else {
4396  $editlink = '';
4397  }
4398  $head[$headlineCount] = Linker::makeHeadline( $level,
4399  $matches['attrib'][$headlineCount], $anchor, $headline,
4400  $editlink, $fallbackAnchor );
4401 
4402  $headlineCount++;
4403  }
4404 
4405  $this->setOutputType( $oldType );
4406 
4407  # Never ever show TOC if no headers
4408  if ( $numVisible < 1 ) {
4409  $enoughToc = false;
4410  }
4411 
4412  if ( $enoughToc ) {
4413  if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4414  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4415  }
4416  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4417  $this->mOutput->setTOCHTML( $toc );
4418  $toc = self::TOC_START . $toc . self::TOC_END;
4419  }
4420 
4421  if ( $isMain ) {
4422  $this->mOutput->setSections( $tocraw );
4423  }
4424 
4425  # split up and insert constructed headlines
4426  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4427  $i = 0;
4428 
4429  // build an array of document sections
4430  $sections = [];
4431  foreach ( $blocks as $block ) {
4432  // $head is zero-based, sections aren't.
4433  if ( empty( $head[$i - 1] ) ) {
4434  $sections[$i] = $block;
4435  } else {
4436  $sections[$i] = $head[$i - 1] . $block;
4437  }
4438 
4449  $this->hookRunner->onParserSectionCreate( $this, $i, $sections[$i], $maybeShowEditLink );
4450 
4451  $i++;
4452  }
4453 
4454  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4455  // append the TOC at the beginning
4456  // Top anchor now in skin
4457  $sections[0] .= $toc . "\n";
4458  }
4459 
4460  $full .= implode( '', $sections );
4461 
4462  if ( $this->mForceTocPosition ) {
4463  return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4464  } else {
4465  return $full;
4466  }
4467  }
4468 
4480  public function preSaveTransform( $text, Title $title, User $user,
4481  ParserOptions $options, $clearState = true
4482  ) {
4483  if ( $clearState ) {
4484  $magicScopeVariable = $this->lock();
4485  }
4486  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4487  $this->setUser( $user );
4488 
4489  // Strip U+0000 NULL (T159174)
4490  $text = str_replace( "\000", '', $text );
4491 
4492  // We still normalize line endings for backwards-compatibility
4493  // with other code that just calls PST, but this should already
4494  // be handled in TextContent subclasses
4495  $text = TextContent::normalizeLineEndings( $text );
4496 
4497  if ( $options->getPreSaveTransform() ) {
4498  $text = $this->pstPass2( $text, $user );
4499  }
4500  $text = $this->mStripState->unstripBoth( $text );
4501 
4502  $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4503 
4504  $this->setUser( null ); # Reset
4505 
4506  return $text;
4507  }
4508 
4517  private function pstPass2( $text, User $user ) {
4518  # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4519  # $this->contLang here in order to give everyone the same signature and use the default one
4520  # rather than the one selected in each user's preferences. (see also T14815)
4521  $ts = $this->mOptions->getTimestamp();
4522  $timestamp = MWTimestamp::getLocalInstance( $ts );
4523  $ts = $timestamp->format( 'YmdHis' );
4524  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4525 
4526  $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4527 
4528  # Variable replacement
4529  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4530  $text = $this->replaceVariables( $text );
4531 
4532  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4533  # which may corrupt this parser instance via its wfMessage()->text() call-
4534 
4535  # Signatures
4536  if ( strpos( $text, '~~~' ) !== false ) {
4537  $sigText = $this->getUserSig( $user );
4538  $text = strtr( $text, [
4539  '~~~~~' => $d,
4540  '~~~~' => "$sigText $d",
4541  '~~~' => $sigText
4542  ] );
4543  # The main two signature forms used above are time-sensitive
4544  $this->setOutputFlag( 'user-signature', 'User signature detected' );
4545  }
4546 
4547  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4548  $tc = '[' . Title::legalChars() . ']';
4549  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4550 
4551  // [[ns:page (context)|]]
4552  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4553  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4554  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4555  // [[ns:page (context), context|]] (using either single or double-width comma)
4556  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4557  // [[|page]] (reverse pipe trick: add context from page title)
4558  $p2 = "/\[\[\\|($tc+)]]/";
4559 
4560  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4561  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4562  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4563  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4564 
4565  $t = $this->getTitle()->getText();
4566  $m = [];
4567  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4568  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4569  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4570  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4571  } else {
4572  # if there's no context, don't bother duplicating the title
4573  $text = preg_replace( $p2, '[[\\1]]', $text );
4574  }
4575 
4576  return $text;
4577  }
4578 
4593  public function getUserSig( User $user, $nickname = false, $fancySig = null ) {
4594  $username = $user->getName();
4595 
4596  # If not given, retrieve from the user object.
4597  if ( $nickname === false ) {
4598  $nickname = $user->getOption( 'nickname' );
4599  }
4600 
4601  if ( $fancySig === null ) {
4602  $fancySig = $user->getBoolOption( 'fancysig' );
4603  }
4604 
4605  if ( $nickname === null || $nickname === '' ) {
4606  $nickname = $username;
4607  } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) {
4608  $nickname = $username;
4609  $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4610  } elseif ( $fancySig !== false ) {
4611  # Sig. might contain markup; validate this
4612  $isValid = $this->validateSig( $nickname ) !== false;
4613 
4614  # New validator
4615  $sigValidation = $this->svcOptions->get( 'SignatureValidation' );
4616  if ( $isValid && $sigValidation === 'disallow' ) {
4617  $validator = new SignatureValidator(
4618  $user,
4619  null,
4620  $this->mOptions
4621  );
4622  $isValid = !$validator->validateSignature( $nickname );
4623  }
4624 
4625  if ( $isValid ) {
4626  # Validated; clean up (if needed) and return it
4627  return $this->cleanSig( $nickname, true );
4628  } else {
4629  # Failed to validate; fall back to the default
4630  $nickname = $username;
4631  $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4632  }
4633  }
4634 
4635  # Make sure nickname doesnt get a sig in a sig
4636  $nickname = self::cleanSigInSig( $nickname );
4637 
4638  # If we're still here, make it a link to the user page
4639  $userText = wfEscapeWikiText( $username );
4640  $nickText = wfEscapeWikiText( $nickname );
4641  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4642 
4643  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4644  ->title( $this->getTitle() )->text();
4645  }
4646 
4653  public function validateSig( $text ) {
4654  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4655  }
4656 
4667  public function cleanSig( $text, $parsing = false ) {
4668  if ( !$parsing ) {
4669  global $wgTitle;
4670  $magicScopeVariable = $this->lock();
4671  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4672  }
4673 
4674  # Option to disable this feature
4675  if ( !$this->mOptions->getCleanSignatures() ) {
4676  return $text;
4677  }
4678 
4679  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4680  # => Move this logic to braceSubstitution()
4681  $substWord = $this->magicWordFactory->get( 'subst' );
4682  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4683  $substText = '{{' . $substWord->getSynonym( 0 );
4684 
4685  $text = preg_replace( $substRegex, $substText, $text );
4686  $text = self::cleanSigInSig( $text );
4687  $dom = $this->preprocessToDom( $text );
4688  $frame = $this->getPreprocessor()->newFrame();
4689  $text = $frame->expand( $dom );
4690 
4691  if ( !$parsing ) {
4692  $text = $this->mStripState->unstripBoth( $text );
4693  }
4694 
4695  return $text;
4696  }
4697 
4704  public static function cleanSigInSig( $text ) {
4705  $text = preg_replace( '/~{3,5}/', '', $text );
4706  return $text;
4707  }
4708 
4719  public function startExternalParse( ?Title $title, ParserOptions $options,
4720  $outputType, $clearState = true, $revId = null
4721  ) {
4722  $this->startParse( $title, $options, $outputType, $clearState );
4723  if ( $revId !== null ) {
4724  $this->mRevisionId = $revId;
4725  }
4726  }
4727 
4734  private function startParse( ?Title $title, ParserOptions $options,
4735  $outputType, $clearState = true
4736  ) {
4737  $this->setTitle( $title );
4738  $this->mOptions = $options;
4739  $this->setOutputType( $outputType );
4740  if ( $clearState ) {
4741  $this->clearState();
4742  }
4743  }
4744 
4753  public function transformMsg( $text, ParserOptions $options, Title $title = null ) {
4754  static $executing = false;
4755 
4756  # Guard against infinite recursion
4757  if ( $executing ) {
4758  return $text;
4759  }
4760  $executing = true;
4761 
4762  if ( !$title ) {
4763  global $wgTitle;
4764  $title = $wgTitle;
4765  }
4766 
4767  $text = $this->preprocess( $text, $title, $options );
4768 
4769  $executing = false;
4770  return $text;
4771  }
4772 
4797  public function setHook( $tag, callable $callback ) {
4798  $tag = strtolower( $tag );
4799  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4800  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4801  }
4802  $oldVal = $this->mTagHooks[$tag] ?? null;
4803  $this->mTagHooks[$tag] = $callback;
4804  if ( !in_array( $tag, $this->mStripList ) ) {
4805  $this->mStripList[] = $tag;
4806  }
4807 
4808  return $oldVal;
4809  }
4810 
4814  public function clearTagHooks() {
4815  $this->mTagHooks = [];
4816  $this->mStripList = [];
4817  }
4818 
4862  public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4863  $oldVal = $this->mFunctionHooks[$id][0] ?? null;
4864  $this->mFunctionHooks[$id] = [ $callback, $flags ];
4865 
4866  # Add to function cache
4867  $mw = $this->magicWordFactory->get( $id );
4868  if ( !$mw ) {
4869  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4870  }
4871 
4872  $synonyms = $mw->getSynonyms();
4873  $sensitive = intval( $mw->isCaseSensitive() );
4874 
4875  foreach ( $synonyms as $syn ) {
4876  # Case
4877  if ( !$sensitive ) {
4878  $syn = $this->contLang->lc( $syn );
4879  }
4880  # Add leading hash
4881  if ( !( $flags & self::SFH_NO_HASH ) ) {
4882  $syn = '#' . $syn;
4883  }
4884  # Remove trailing colon
4885  if ( substr( $syn, -1, 1 ) === ':' ) {
4886  $syn = substr( $syn, 0, -1 );
4887  }
4888  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4889  }
4890  return $oldVal;
4891  }
4892 
4898  public function getFunctionHooks() {
4899  $this->firstCallInit();
4900  return array_keys( $this->mFunctionHooks );
4901  }
4902 
4911  public function replaceLinkHolders( &$text, $options = 0 ) {
4912  $this->replaceLinkHoldersPrivate( $text, $options );
4913  }
4914 
4922  private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
4923  $this->mLinkHolders->replace( $text );
4924  }
4925 
4933  private function replaceLinkHoldersText( $text ) {
4934  return $this->mLinkHolders->replaceText( $text );
4935  }
4936 
4951  public function renderImageGallery( $text, array $params ) {
4952  $mode = false;
4953  if ( isset( $params['mode'] ) ) {
4954  $mode = $params['mode'];
4955  }
4956 
4957  try {
4958  $ig = ImageGalleryBase::factory( $mode );
4959  } catch ( Exception $e ) {
4960  // If invalid type set, fallback to default.
4961  $ig = ImageGalleryBase::factory( false );
4962  }
4963 
4964  $ig->setContextTitle( $this->getTitle() );
4965  $ig->setShowBytes( false );
4966  $ig->setShowDimensions( false );
4967  $ig->setShowFilename( false );
4968  $ig->setParser( $this );
4969  $ig->setHideBadImages();
4970  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
4971 
4972  if ( isset( $params['showfilename'] ) ) {
4973  $ig->setShowFilename( true );
4974  } else {
4975  $ig->setShowFilename( false );
4976  }
4977  if ( isset( $params['caption'] ) ) {
4978  // NOTE: We aren't passing a frame here or below. Frame info
4979  // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
4980  // See T107332#4030581
4981  $caption = $this->recursiveTagParse( $params['caption'] );
4982  $ig->setCaptionHtml( $caption );
4983  }
4984  if ( isset( $params['perrow'] ) ) {
4985  $ig->setPerRow( $params['perrow'] );
4986  }
4987  if ( isset( $params['widths'] ) ) {
4988  $ig->setWidths( $params['widths'] );
4989  }
4990  if ( isset( $params['heights'] ) ) {
4991  $ig->setHeights( $params['heights'] );
4992  }
4993  $ig->setAdditionalOptions( $params );
4994 
4995  $this->hookRunner->onBeforeParserrenderImageGallery( $this, $ig );
4996 
4997  $lines = StringUtils::explode( "\n", $text );
4998  foreach ( $lines as $line ) {
4999  # match lines like these:
5000  # Image:someimage.jpg|This is some image
5001  $matches = [];
5002  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5003  # Skip empty lines
5004  if ( count( $matches ) == 0 ) {
5005  continue;
5006  }
5007 
5008  if ( strpos( $matches[0], '%' ) !== false ) {
5009  $matches[1] = rawurldecode( $matches[1] );
5010  }
5012  if ( $title === null ) {
5013  # Bogus title. Ignore these so we don't bomb out later.
5014  continue;
5015  }
5016 
5017  # We need to get what handler the file uses, to figure out parameters.
5018  # Note, a hook can overide the file name, and chose an entirely different
5019  # file (which potentially could be of a different type and have different handler).
5020  $options = [];
5021  $descQuery = false;
5022  $this->hookRunner->onBeforeParserFetchFileAndTitle(
5023  $this, $title, $options, $descQuery );
5024  # Don't register it now, as TraditionalImageGallery does that later.
5025  $file = $this->fetchFileNoRegister( $title, $options );
5026  $handler = $file ? $file->getHandler() : false;
5027 
5028  $paramMap = [
5029  'img_alt' => 'gallery-internal-alt',
5030  'img_link' => 'gallery-internal-link',
5031  ];
5032  if ( $handler ) {
5033  $paramMap += $handler->getParamMap();
5034  // We don't want people to specify per-image widths.
5035  // Additionally the width parameter would need special casing anyhow.
5036  unset( $paramMap['img_width'] );
5037  }
5038 
5039  $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5040 
5041  $label = '';
5042  $alt = '';
5043  $link = '';
5044  $handlerOptions = [];
5045  if ( isset( $matches[3] ) ) {
5046  // look for an |alt= definition while trying not to break existing
5047  // captions with multiple pipes (|) in it, until a more sensible grammar
5048  // is defined for images in galleries
5049 
5050  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5051  // splitting on '|' is a bit odd, and different from makeImage.
5052  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5053  // Protect LanguageConverter markup
5054  $parameterMatches = StringUtils::delimiterExplode(
5055  '-{', '}-', '|', $matches[3], true /* nested */
5056  );
5057 
5058  foreach ( $parameterMatches as $parameterMatch ) {
5059  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5060  if ( $magicName ) {
5061  $paramName = $paramMap[$magicName];
5062 
5063  switch ( $paramName ) {
5064  case 'gallery-internal-alt':
5065  $alt = $this->stripAltText( $match, false );
5066  break;
5067  case 'gallery-internal-link':
5068  $linkValue = $this->stripAltText( $match, false );
5069  if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5070  // Result of LanguageConverter::markNoConversion
5071  // invoked on an external link.
5072  $linkValue = substr( $linkValue, 4, -2 );
5073  }
5074  list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5075  if ( $type === 'link-url' ) {
5076  $link = $target;
5077  $this->mOutput->addExternalLink( $target );
5078  } elseif ( $type === 'link-title' ) {
5079  $link = $target->getLinkURL();
5080  $this->mOutput->addLink( $target );
5081  }
5082  break;
5083  default:
5084  // Must be a handler specific parameter.
5085  if ( $handler->validateParam( $paramName, $match ) ) {
5086  $handlerOptions[$paramName] = $match;
5087  } else {
5088  // Guess not, consider it as caption.
5089  $this->logger->debug(
5090  "$parameterMatch failed parameter validation" );
5091  $label = $parameterMatch;
5092  }
5093  }
5094 
5095  } else {
5096  // Last pipe wins.
5097  $label = $parameterMatch;
5098  }
5099  }
5100  }
5101 
5102  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5103  }
5104  $html = $ig->toHTML();
5105  $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5106  return $html;
5107  }
5108 
5113  private function getImageParams( $handler ) {
5114  if ( $handler ) {
5115  $handlerClass = get_class( $handler );
5116  } else {
5117  $handlerClass = '';
5118  }
5119  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5120  # Initialise static lists
5121  static $internalParamNames = [
5122  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5123  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5124  'bottom', 'text-bottom' ],
5125  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5126  'upright', 'border', 'link', 'alt', 'class' ],
5127  ];
5128  static $internalParamMap;
5129  if ( !$internalParamMap ) {
5130  $internalParamMap = [];
5131  foreach ( $internalParamNames as $type => $names ) {
5132  foreach ( $names as $name ) {
5133  // For grep: img_left, img_right, img_center, img_none,
5134  // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5135  // img_bottom, img_text_bottom,
5136  // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5137  // img_border, img_link, img_alt, img_class
5138  $magicName = str_replace( '-', '_', "img_$name" );
5139  $internalParamMap[$magicName] = [ $type, $name ];
5140  }
5141  }
5142  }
5143 
5144  # Add handler params
5145  $paramMap = $internalParamMap;
5146  if ( $handler ) {
5147  $handlerParamMap = $handler->getParamMap();
5148  foreach ( $handlerParamMap as $magic => $paramName ) {
5149  $paramMap[$magic] = [ 'handler', $paramName ];
5150  }
5151  }
5152  $this->mImageParams[$handlerClass] = $paramMap;
5153  $this->mImageParamsMagicArray[$handlerClass] =
5154  $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5155  }
5156  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5157  }
5158 
5167  public function makeImage( Title $title, $options, $holders = false ) {
5168  # Check if the options text is of the form "options|alt text"
5169  # Options are:
5170  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5171  # * left no resizing, just left align. label is used for alt= only
5172  # * right same, but right aligned
5173  # * none same, but not aligned
5174  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5175  # * center center the image
5176  # * frame Keep original image size, no magnify-button.
5177  # * framed Same as "frame"
5178  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5179  # * upright reduce width for upright images, rounded to full __0 px
5180  # * border draw a 1px border around the image
5181  # * alt Text for HTML alt attribute (defaults to empty)
5182  # * class Set a class for img node
5183  # * link Set the target of the image link. Can be external, interwiki, or local
5184  # vertical-align values (no % or length right now):
5185  # * baseline
5186  # * sub
5187  # * super
5188  # * top
5189  # * text-top
5190  # * middle
5191  # * bottom
5192  # * text-bottom
5193 
5194  # Protect LanguageConverter markup when splitting into parts
5196  '-{', '}-', '|', $options, true /* allow nesting */
5197  );
5198 
5199  # Give extensions a chance to select the file revision for us
5200  $options = [];
5201  $descQuery = false;
5202  $this->hookRunner->onBeforeParserFetchFileAndTitle(
5203  $this, $title, $options, $descQuery );
5204  # Fetch and register the file (file title may be different via hooks)
5205  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5206 
5207  # Get parameter map
5208  $handler = $file ? $file->getHandler() : false;
5209 
5210  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5211 
5212  if ( !$file ) {
5213  $this->addTrackingCategory( 'broken-file-category' );
5214  }
5215 
5216  # Process the input parameters
5217  $caption = '';
5218  $params = [ 'frame' => [], 'handler' => [],
5219  'horizAlign' => [], 'vertAlign' => [] ];
5220  $seenformat = false;
5221  foreach ( $parts as $part ) {
5222  $part = trim( $part );
5223  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5224  $validated = false;
5225  if ( isset( $paramMap[$magicName] ) ) {
5226  list( $type, $paramName ) = $paramMap[$magicName];
5227 
5228  # Special case; width and height come in one variable together
5229  if ( $type === 'handler' && $paramName === 'width' ) {
5230  $parsedWidthParam = self::parseWidthParam( $value );
5231  if ( isset( $parsedWidthParam['width'] ) ) {
5232  $width = $parsedWidthParam['width'];
5233  if ( $handler->validateParam( 'width', $width ) ) {
5234  $params[$type]['width'] = $width;
5235  $validated = true;
5236  }
5237  }
5238  if ( isset( $parsedWidthParam['height'] ) ) {
5239  $height = $parsedWidthParam['height'];
5240  if ( $handler->validateParam( 'height', $height ) ) {
5241  $params[$type]['height'] = $height;
5242  $validated = true;
5243  }
5244  }
5245  # else no validation -- T15436
5246  } else {
5247  if ( $type === 'handler' ) {
5248  # Validate handler parameter
5249  $validated = $handler->validateParam( $paramName, $value );
5250  } else {
5251  # Validate internal parameters
5252  switch ( $paramName ) {
5253  case 'manualthumb':
5254  case 'alt':
5255  case 'class':
5256  # @todo FIXME: Possibly check validity here for
5257  # manualthumb? downstream behavior seems odd with
5258  # missing manual thumbs.
5259  $validated = true;
5260  $value = $this->stripAltText( $value, $holders );
5261  break;
5262  case 'link':
5263  list( $paramName, $value ) =
5264  $this->parseLinkParameter(
5265  $this->stripAltText( $value, $holders )
5266  );
5267  if ( $paramName ) {
5268  $validated = true;
5269  if ( $paramName === 'no-link' ) {
5270  $value = true;
5271  }
5272  if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5273  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5274  }
5275  }
5276  break;
5277  case 'frameless':
5278  case 'framed':
5279  case 'thumbnail':
5280  // use first appearing option, discard others.
5281  $validated = !$seenformat;
5282  $seenformat = true;
5283  break;
5284  default:
5285  # Most other things appear to be empty or numeric...
5286  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5287  }
5288  }
5289 
5290  if ( $validated ) {
5291  $params[$type][$paramName] = $value;
5292  }
5293  }
5294  }
5295  if ( !$validated ) {
5296  $caption = $part;
5297  }
5298  }
5299 
5300  # Process alignment parameters
5301  // @phan-suppress-next-line PhanImpossibleCondition
5302  if ( $params['horizAlign'] ) {
5303  $params['frame']['align'] = key( $params['horizAlign'] );
5304  }
5305  // @phan-suppress-next-line PhanImpossibleCondition
5306  if ( $params['vertAlign'] ) {
5307  $params['frame']['valign'] = key( $params['vertAlign'] );
5308  }
5309 
5310  $params['frame']['caption'] = $caption;
5311 
5312  # Will the image be presented in a frame, with the caption below?
5313  $imageIsFramed = isset( $params['frame']['frame'] )
5314  || isset( $params['frame']['framed'] )
5315  || isset( $params['frame']['thumbnail'] )
5316  || isset( $params['frame']['manualthumb'] );
5317 
5318  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5319  # came to also set the caption, ordinary text after the image -- which
5320  # makes no sense, because that just repeats the text multiple times in
5321  # screen readers. It *also* came to set the title attribute.
5322  # Now that we have an alt attribute, we should not set the alt text to
5323  # equal the caption: that's worse than useless, it just repeats the
5324  # text. This is the framed/thumbnail case. If there's no caption, we
5325  # use the unnamed parameter for alt text as well, just for the time be-
5326  # ing, if the unnamed param is set and the alt param is not.
5327  # For the future, we need to figure out if we want to tweak this more,
5328  # e.g., introducing a title= parameter for the title; ignoring the un-
5329  # named parameter entirely for images without a caption; adding an ex-
5330  # plicit caption= parameter and preserving the old magic unnamed para-
5331  # meter for BC; ...
5332  if ( $imageIsFramed ) { # Framed image
5333  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5334  # No caption or alt text, add the filename as the alt text so
5335  # that screen readers at least get some description of the image
5336  $params['frame']['alt'] = $title->getText();
5337  }
5338  # Do not set $params['frame']['title'] because tooltips don't make sense
5339  # for framed images
5340  } else { # Inline image
5341  if ( !isset( $params['frame']['alt'] ) ) {
5342  # No alt text, use the "caption" for the alt text
5343  if ( $caption !== '' ) {
5344  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5345  } else {
5346  # No caption, fall back to using the filename for the
5347  # alt text
5348  $params['frame']['alt'] = $title->getText();
5349  }
5350  }
5351  # Use the "caption" for the tooltip text
5352  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5353  }
5354  $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5355 
5356  $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5357 
5358  # Linker does the rest
5359  $time = $options['time'] ?? false;
5360  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5361  $time, $descQuery, $this->mOptions->getThumbSize() );
5362 
5363  # Give the handler a chance to modify the parser object
5364  if ( $handler ) {
5365  $handler->parserTransformHook( $this, $file );
5366  }
5367 
5368  return $ret;
5369  }
5370 
5389  private function parseLinkParameter( $value ) {
5390  $chars = self::EXT_LINK_URL_CLASS;
5391  $addr = self::EXT_LINK_ADDR;
5392  $prots = $this->mUrlProtocols;
5393  $type = null;
5394  $target = false;
5395  if ( $value === '' ) {
5396  $type = 'no-link';
5397  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5398  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5399  $this->mOutput->addExternalLink( $value );
5400  $type = 'link-url';
5401  $target = $value;
5402  }
5403  } else {
5404  $linkTitle = Title::newFromText( $value );
5405  if ( $linkTitle ) {
5406  $this->mOutput->addLink( $linkTitle );
5407  $type = 'link-title';
5408  $target = $linkTitle;
5409  }
5410  }
5411  return [ $type, $target ];
5412  }
5413 
5419  private function stripAltText( $caption, $holders ) {
5420  # Strip bad stuff out of the title (tooltip). We can't just use
5421  # replaceLinkHoldersText() here, because if this function is called
5422  # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5423  if ( $holders ) {
5424  $tooltip = $holders->replaceText( $caption );
5425  } else {
5426  $tooltip = $this->replaceLinkHoldersText( $caption );
5427  }
5428 
5429  # make sure there are no placeholders in thumbnail attributes
5430  # that are later expanded to html- so expand them now and
5431  # remove the tags
5432  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5433  # Compatibility hack! In HTML certain entity references not terminated
5434  # by a semicolon are decoded (but not if we're in an attribute; that's
5435  # how link URLs get away without properly escaping & in queries).
5436  # But wikitext has always required semicolon-termination of entities,
5437  # so encode & where needed to avoid decode of semicolon-less entities.
5438  # See T209236 and
5439  # https://www.w3.org/TR/html5/syntax.html#named-character-references
5440  # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5441  $tooltip = preg_replace( "/
5442  & # 1. entity prefix
5443  (?= # 2. followed by:
5444  (?: # a. one of the legacy semicolon-less named entities
5445  A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5446  C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5447  GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5448  O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5449  U(?:acute|circ|grave|uml)|Yacute|
5450  a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5451  c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5452  divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5453  frac(?:1(?:2|4)|34)|
5454  gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5455  i(?:acute|circ|excl|grave|quest|uml)|laquo|
5456  lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5457  m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5458  not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5459  o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5460  p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5461  s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5462  u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5463  )
5464  (?:[^;]|$)) # b. and not followed by a semicolon
5465  # S = study, for efficiency
5466  /Sx", '&amp;', $tooltip );
5467  $tooltip = Sanitizer::stripAllTags( $tooltip );
5468 
5469  return $tooltip;
5470  }
5471 
5481  public function attributeStripCallback( &$text, $frame = false ) {
5482  wfDeprecated( __METHOD__, '1.35' );
5483  $text = $this->replaceVariables( $text, $frame );
5484  $text = $this->mStripState->unstripBoth( $text );
5485  return $text;
5486  }
5487 
5493  public function getTags() {
5494  $this->firstCallInit();
5495  return array_keys( $this->mTagHooks );
5496  }
5497 
5502  public function getFunctionSynonyms() {
5503  $this->firstCallInit();
5504  return $this->mFunctionSynonyms;
5505  }
5506 
5511  public function getUrlProtocols() {
5512  return $this->mUrlProtocols;
5513  }
5514 
5544  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5545  global $wgTitle; # not generally used but removes an ugly failure mode
5546 
5547  $magicScopeVariable = $this->lock();
5548  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5549  $outText = '';
5550  $frame = $this->getPreprocessor()->newFrame();
5551 
5552  # Process section extraction flags
5553  $flags = 0;
5554  $sectionParts = explode( '-', $sectionId );
5555  $sectionIndex = array_pop( $sectionParts );
5556  foreach ( $sectionParts as $part ) {
5557  if ( $part === 'T' ) {
5558  $flags |= self::PTD_FOR_INCLUSION;
5559  }
5560  }
5561 
5562  # Check for empty input
5563  if ( strval( $text ) === '' ) {
5564  # Only sections 0 and T-0 exist in an empty document
5565  if ( $sectionIndex == 0 ) {
5566  if ( $mode === 'get' ) {
5567  return '';
5568  }
5569 
5570  return $newText;
5571  } else {
5572  if ( $mode === 'get' ) {
5573  return $newText;
5574  }
5575 
5576  return $text;
5577  }
5578  }
5579 
5580  # Preprocess the text
5581  $root = $this->preprocessToDom( $text, $flags );
5582 
5583  # <h> nodes indicate section breaks
5584  # They can only occur at the top level, so we can find them by iterating the root's children
5585  $node = $root->getFirstChild();
5586 
5587  # Find the target section
5588  if ( $sectionIndex == 0 ) {
5589  # Section zero doesn't nest, level=big
5590  $targetLevel = 1000;
5591  } else {
5592  while ( $node ) {
5593  if ( $node->getName() === 'h' ) {
5594  $bits = $node->splitHeading();
5595  if ( $bits['i'] == $sectionIndex ) {
5596  $targetLevel = $bits['level'];
5597  break;
5598  }
5599  }
5600  if ( $mode === 'replace' ) {
5601  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5602  }
5603  $node = $node->getNextSibling();
5604  }
5605  }
5606 
5607  if ( !$node ) {
5608  # Not found
5609  if ( $mode === 'get' ) {
5610  return $newText;
5611  } else {
5612  return $text;
5613  }
5614  }
5615 
5616  # Find the end of the section, including nested sections
5617  do {
5618  if ( $node->getName() === 'h' ) {
5619  $bits = $node->splitHeading();
5620  $curLevel = $bits['level'];
5621  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5622  break;
5623  }
5624  }
5625  if ( $mode === 'get' ) {
5626  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5627  }
5628  $node = $node->getNextSibling();
5629  } while ( $node );
5630 
5631  # Write out the remainder (in replace mode only)
5632  if ( $mode === 'replace' ) {
5633  # Output the replacement text
5634  # Add two newlines on -- trailing whitespace in $newText is conventionally
5635  # stripped by the editor, so we need both newlines to restore the paragraph gap
5636  # Only add trailing whitespace if there is newText
5637  if ( $newText != "" ) {
5638  $outText .= $newText . "\n\n";
5639  }
5640 
5641  while ( $node ) {
5642  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5643  $node = $node->getNextSibling();
5644  }
5645  }
5646 
5647  # Re-insert stripped tags
5648  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5649 
5650  return $outText;
5651  }
5652 
5667  public function getSection( $text, $sectionId, $defaultText = '' ) {
5668  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5669  }
5670 
5683  public function replaceSection( $oldText, $sectionId, $newText ) {
5684  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5685  }
5686 
5716  public function getFlatSectionInfo( $text ) {
5717  $magicScopeVariable = $this->lock();
5718  $this->startParse( null, new ParserOptions, self::OT_PLAIN, true );
5719  $frame = $this->getPreprocessor()->newFrame();
5720  $root = $this->preprocessToDom( $text, 0 );
5721  $node = $root->getFirstChild();
5722  $offset = 0;
5723  $currentSection = [
5724  'index' => 0,
5725  'level' => 0,
5726  'offset' => 0,
5727  'heading' => '',
5728  'text' => ''
5729  ];
5730  $sections = [];
5731 
5732  while ( $node ) {
5733  $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
5734  if ( $node->getName() === 'h' ) {
5735  $bits = $node->splitHeading();
5736  $sections[] = $currentSection;
5737  $currentSection = [
5738  'index' => $bits['i'],
5739  'level' => $bits['level'],
5740  'offset' => $offset,
5741  'heading' => $nodeText,
5742  'text' => $nodeText
5743  ];
5744  } else {
5745  $currentSection['text'] .= $nodeText;
5746  }
5747  $offset += strlen( $nodeText );
5748  $node = $node->getNextSibling();
5749  }
5750  $sections[] = $currentSection;
5751  return $sections;
5752  }
5753 
5764  public function getRevisionId() {
5765  return $this->mRevisionId;
5766  }
5767 
5775  public function getRevisionObject() {
5776  wfDeprecated( __METHOD__, '1.35' );
5777 
5778  if ( $this->mRevisionObject ) {
5779  return $this->mRevisionObject;
5780  }
5781 
5782  $this->mRevisionObject = null;
5783 
5784  $revRecord = $this->getRevisionRecordObject();
5785  if ( $revRecord ) {
5786  $this->mRevisionObject = new Revision( $revRecord );
5787  }
5788 
5789  return $this->mRevisionObject;
5790  }
5791 
5798  public function getRevisionRecordObject() {
5799  if ( $this->mRevisionRecordObject ) {
5800  return $this->mRevisionRecordObject;
5801  }
5802 
5803  // NOTE: try to get the RevisionObject even if mRevisionId is null.
5804  // This is useful when parsing a revision that has not yet been saved.
5805  // However, if we get back a saved revision even though we are in
5806  // preview mode, we'll have to ignore it, see below.
5807  // NOTE: This callback may be used to inject an OLD revision that was
5808  // already loaded, so "current" is a bit of a misnomer. We can't just
5809  // skip it if mRevisionId is set.
5810  $rev = call_user_func(
5811  $this->mOptions->getCurrentRevisionRecordCallback(),
5812  $this->getTitle(),
5813  $this
5814  );
5815 
5816  if ( $rev === false ) {
5817  // The revision record callback returns `false` (not null) to
5818  // indicate that the revision is missing. (See for example
5819  // Parser::statelessFetchRevisionRecord(), the default callback.)
5820  // This API expects `null` instead. (T251952)
5821  $rev = null;
5822  }
5823 
5824  if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
5825  // We are in preview mode (mRevisionId is null), and the current revision callback
5826  // returned an existing revision. Ignore it and return null, it's probably the page's
5827  // current revision, which is not what we want here. Note that we do want to call the
5828  // callback to allow the unsaved revision to be injected here, e.g. for
5829  // self-transclusion previews.
5830  return null;
5831  }
5832 
5833  // If the parse is for a new revision, then the callback should have
5834  // already been set to force the object and should match mRevisionId.
5835  // If not, try to fetch by mRevisionId for sanity.
5836  if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
5837  $rev = MediaWikiServices::getInstance()
5838  ->getRevisionLookup()
5839  ->getRevisionById( $this->mRevisionId );
5840  }
5841 
5842  $this->mRevisionRecordObject = $rev;
5843 
5844  return $this->mRevisionRecordObject;
5845  }
5846 
5852  public function getRevisionTimestamp() {
5853  if ( $this->mRevisionTimestamp !== null ) {
5854  return $this->mRevisionTimestamp;
5855  }
5856 
5857  # Use specified revision timestamp, falling back to the current timestamp
5858  $revObject = $this->getRevisionRecordObject();
5859  $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
5860  $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
5861 
5862  # The cryptic '' timezone parameter tells to use the site-default
5863  # timezone offset instead of the user settings.
5864  # Since this value will be saved into the parser cache, served
5865  # to other users, and potentially even used inside links and such,
5866  # it needs to be consistent for all visitors.
5867  $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
5868 
5869  return $this->mRevisionTimestamp;
5870  }
5871 
5877  public function getRevisionUser(): ?string {
5878  if ( $this->mRevisionUser === null ) {
5879  $revObject = $this->getRevisionRecordObject();
5880 
5881  # if this template is subst: the revision id will be blank,
5882  # so just use the current user's name
5883  if ( $revObject && $revObject->getUser() ) {
5884  $this->mRevisionUser = $revObject->getUser()->getName();
5885  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5886  $this->mRevisionUser = $this->getUser()->getName();
5887  } else {
5888  # Note that we fall through here with
5889  # $this->mRevisionUser still null
5890  }
5891  }
5892  return $this->mRevisionUser;
5893  }
5894 
5900  public function getRevisionSize() {
5901  if ( $this->mRevisionSize === null ) {
5902  $revObject = $this->getRevisionRecordObject();
5903 
5904  # if this variable is subst: the revision id will be blank,
5905  # so just use the parser input size, because the own substituation
5906  # will change the size.
5907  if ( $revObject ) {
5908  $this->mRevisionSize = $revObject->getSize();
5909  } else {
5910  $this->mRevisionSize = $this->mInputSize;
5911  }
5912  }
5913  return $this->mRevisionSize;
5914  }
5915 
5921  public function setDefaultSort( $sort ) {
5922  $this->mDefaultSort = $sort;
5923  $this->mOutput->setProperty( 'defaultsort', $sort );
5924  }
5925 
5936  public function getDefaultSort() {
5937  if ( $this->mDefaultSort !== false ) {
5938  return $this->mDefaultSort;
5939  } else {
5940  return '';
5941  }
5942  }
5943 
5950  public function getCustomDefaultSort() {
5951  return $this->mDefaultSort;
5952  }
5953 
5954  private static function getSectionNameFromStrippedText( $text ) {
5956  $text = Sanitizer::decodeCharReferences( $text );
5957  $text = self::normalizeSectionName( $text );
5958  return $text;
5959  }
5960 
5961  private static function makeAnchor( $sectionName ) {
5962  return '#' . Sanitizer::escapeIdForLink( $sectionName );
5963  }
5964 
5965  private function makeLegacyAnchor( $sectionName ) {
5966  $fragmentMode = $this->svcOptions->get( 'FragmentMode' );
5967  if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
5968  // ForAttribute() and ForLink() are the same for legacy encoding
5970  } else {
5971  $id = Sanitizer::escapeIdForLink( $sectionName );
5972  }
5973 
5974  return "#$id";
5975  }
5976 
5985  public function guessSectionNameFromWikiText( $text ) {
5986  # Strip out wikitext links(they break the anchor)
5987  $text = $this->stripSectionName( $text );
5988  $sectionName = self::getSectionNameFromStrippedText( $text );
5989  return self::makeAnchor( $sectionName );
5990  }
5991 
6001  public function guessLegacySectionNameFromWikiText( $text ) {
6002  # Strip out wikitext links(they break the anchor)
6003  $text = $this->stripSectionName( $text );
6004  $sectionName = self::getSectionNameFromStrippedText( $text );
6005  return $this->makeLegacyAnchor( $sectionName );
6006  }
6007 
6013  public static function guessSectionNameFromStrippedText( $text ) {
6014  $sectionName = self::getSectionNameFromStrippedText( $text );
6015  return self::makeAnchor( $sectionName );
6016  }
6017 
6024  private static function normalizeSectionName( $text ) {
6025  # T90902: ensure the same normalization is applied for IDs as to links
6026 
6027  $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6028  '@phan-var MediaWikiTitleCodec $titleParser';
6029  try {
6030 
6031  $parts = $titleParser->splitTitleString( "#$text" );
6032  } catch ( MalformedTitleException $ex ) {
6033  return $text;
6034  }
6035  return $parts['fragment'];
6036  }
6037 
6052  public function stripSectionName( $text ) {
6053  # Strip internal link markup
6054  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6055  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6056 
6057  # Strip external link markup
6058  # @todo FIXME: Not tolerant to blank link text
6059  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6060  # on how many empty links there are on the page - need to figure that out.
6061  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6062 
6063  # Parse wikitext quotes (italics & bold)
6064  $text = $this->doQuotes( $text );
6065 
6066  # Strip HTML tags
6067  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6068  return $text;
6069  }
6070 
6081  private function fuzzTestSrvus( $text, Title $title, ParserOptions $options,
6082  $outputType = self::OT_HTML
6083  ) {
6084  $magicScopeVariable = $this->lock();
6085  $this->startParse( $title, $options, $outputType, true );
6086 
6087  $text = $this->replaceVariables( $text );
6088  $text = $this->mStripState->unstripBoth( $text );
6089  $text = Sanitizer::removeHTMLtags( $text );
6090  return $text;
6091  }
6092 
6099  private function fuzzTestPst( $text, Title $title, ParserOptions $options ) {
6100  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6101  }
6102 
6109  private function fuzzTestPreprocess( $text, Title $title, ParserOptions $options ) {
6110  return $this->fuzzTestSrvus( $text, $title, $options, self::OT_PREPROCESS );
6111  }
6112 
6130  public function markerSkipCallback( $s, callable $callback ) {
6131  $i = 0;
6132  $out = '';
6133  while ( $i < strlen( $s ) ) {
6134  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6135  if ( $markerStart === false ) {
6136  $out .= call_user_func( $callback, substr( $s, $i ) );
6137  break;
6138  } else {
6139  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6140  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6141  if ( $markerEnd === false ) {
6142  $out .= substr( $s, $markerStart );
6143  break;
6144  } else {
6145  $markerEnd += strlen( self::MARKER_SUFFIX );
6146  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6147  $i = $markerEnd;
6148  }
6149  }
6150  }
6151  return $out;
6152  }
6153 
6160  public function killMarkers( $text ) {
6161  return $this->mStripState->killMarkers( $text );
6162  }
6163 
6174  public static function parseWidthParam( $value, $parseHeight = true ) {
6175  $parsedWidthParam = [];
6176  if ( $value === '' ) {
6177  return $parsedWidthParam;
6178  }
6179  $m = [];
6180  # (T15500) In both cases (width/height and width only),
6181  # permit trailing "px" for backward compatibility.
6182  if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6183  $width = intval( $m[1] );
6184  $height = intval( $m[2] );
6185  $parsedWidthParam['width'] = $width;
6186  $parsedWidthParam['height'] = $height;
6187  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6188  $width = intval( $value );
6189  $parsedWidthParam['width'] = $width;
6190  }
6191  return $parsedWidthParam;
6192  }
6193 
6203  protected function lock() {
6204  if ( $this->mInParse ) {
6205  throw new MWException( "Parser state cleared while parsing. "
6206  . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6207  }
6208 
6209  // Save the backtrace when locking, so that if some code tries locking again,
6210  // we can print the lock owner's backtrace for easier debugging
6211  $e = new Exception;
6212  $this->mInParse = $e->getTraceAsString();
6213 
6214  $recursiveCheck = new ScopedCallback( function () {
6215  $this->mInParse = false;
6216  } );
6217 
6218  return $recursiveCheck;
6219  }
6220 
6231  public static function stripOuterParagraph( $html ) {
6232  $m = [];
6233  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6234  $html = $m[1];
6235  }
6236 
6237  return $html;
6238  }
6239 
6250  public function getFreshParser() {
6251  if ( $this->mInParse ) {
6252  return $this->factory->create();
6253  } else {
6254  return $this;
6255  }
6256  }
6257 
6265  public function enableOOUI() {
6266  wfDeprecated( __METHOD__, '1.35' );
6268  $this->mOutput->setEnableOOUI( true );
6269  }
6270 
6277  private function setOutputFlag( string $flag, string $reason ): void {
6278  $this->mOutput->setFlag( $flag );
6279  $name = $this->getTitle()->getPrefixedText();
6280  $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6281  }
6282 }
Parser\$badFileLookup
BadFileLookup $badFileLookup
Definition: Parser.php:353
Parser\getFunctionHooks
getFunctionHooks()
Get all registered function hook identifiers.
Definition: Parser.php:4898
Parser\$mLinkRenderer
LinkRenderer $mLinkRenderer
Definition: Parser.php:317
Parser\$mForceTocPosition
$mForceTocPosition
Definition: Parser.php:242
Parser\recursivePreprocess
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition: Parser.php:932
Parser\getContentLanguageConverter
getContentLanguageConverter()
Shorthand for getting a Language Converter for Content language.
Definition: Parser.php:1606
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
Parser\attributeStripCallback
attributeStripCallback(&$text, $frame=false)
Callback from the Sanitizer for expanding items found in HTML attribute values, so they can be safely...
Definition: Parser.php:5481
PPFrame\loopCheck
loopCheck( $title)
Returns true if the infinite loop check is OK, false if a loop is detected.
Parser\$mSubstWords
MagicWordArray $mSubstWords
Definition: Parser.php:179
Parser\$linkRendererFactory
LinkRendererFactory $linkRendererFactory
Definition: Parser.php:344
Sanitizer\ID_FALLBACK
const ID_FALLBACK
Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false if no fallback...
Definition: Sanitizer.php:78
Parser\maybeMakeExternalImage
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition: Parser.php:2310
MagicWordArray
Class for handling an array of magic words.
Definition: MagicWordArray.php:32
Parser\EXT_LINK_ADDR
const EXT_LINK_ADDR
Definition: Parser.php:106
Revision\RevisionAccessException
Exception representing a failure to look up a revision.
Definition: RevisionAccessException.php:34
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:35
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:328
Parser\$mInputSize
$mInputSize
Definition: Parser.php:283
PPFrame\STRIP_COMMENTS
const STRIP_COMMENTS
Definition: PPFrame.php:31
Parser\SPACE_NOT_NL
const SPACE_NOT_NL
Definition: Parser.php:113
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
Parser\__destruct
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: Parser.php:481
ParserOutput
Definition: ParserOutput.php:27
Parser\$mLinkHolders
LinkHolderArray $mLinkHolders
Definition: Parser.php:214
Parser\braceSubstitution
braceSubstitution(array $piece, PPFrame $frame)
Return the text of a template, after recursively replacing any variables or templates within the temp...
Definition: Parser.php:2929
Parser\makeLimitReport
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition: Parser.php:715
MagicWordFactory
A factory that stores information about MagicWords, and creates them on demand with caching.
Definition: MagicWordFactory.php:37
Parser\internalParseHalfParsed
internalParseHalfParsed( $text, $isMain=true, $linestart=true)
Helper function for parse() that transforms half-parsed HTML into fully parsed HTML.
Definition: Parser.php:1644
Parser\stripAltText
stripAltText( $caption, $holders)
Definition: Parser.php:5419
Parser\killMarkers
killMarkers( $text)
Remove any strip markers found in the given text.
Definition: Parser.php:6160
User\isAnon
isAnon()
Get whether the user is anonymous.
Definition: User.php:2980
Sanitizer\stripAllTags
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1574
Parser\$mTagHooks
$mTagHooks
Definition: Parser.php:153
Parser\OutputType
OutputType( $x=null)
Accessor/mutator for the output type.
Definition: Parser.php:1044
Parser\$currentRevisionCache
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:301
Parser\setOutputFlag
setOutputFlag(string $flag, string $reason)
Sets the flag on the parser output but also does some debug logging.
Definition: Parser.php:6277
Parser\Title
Title(Title $x=null)
Accessor/mutator for the Title object.
Definition: Parser.php:1008
Parser\enableOOUI
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition: Parser.php:6265
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:160
Linker\makeSelfLinkObj
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:164
Parser\$mTplDomCache
array $mTplDomCache
Definition: Parser.php:244
MediaWiki\BadFileLookup
Definition: BadFileLookup.php:13
PPFrame\NO_ARGS
const NO_ARGS
Definition: PPFrame.php:29
wfSetVar
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
Definition: GlobalFunctions.php:1550
Parser\parseExtensionTagAsTopLevelDoc
parseExtensionTagAsTopLevelDoc( $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition: Parser.php:891
Parser\$mDoubleUnderscores
$mDoubleUnderscores
Definition: Parser.php:237
Linker\tocIndent
static tocIndent()
Add another level to the Table of Contents.
Definition: Linker.php:1665
Parser\getRevisionSize
getRevisionSize()
Get the size of the revision.
Definition: Parser.php:5900
Sanitizer\escapeIdForAttribute
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:815
Sanitizer\removeHTMLtags
static removeHTMLtags( $text, $processCallback=null, $args=[], $extratags=[], $removetags=[])
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments.
Definition: Sanitizer.php:239
Parser\handleExternalLinks
handleExternalLinks( $text)
Replace external links (REL)
Definition: Parser.php:2096
Parser\$mOutputType
$mOutputType
Definition: Parser.php:269
Parser\setUser
setUser(?User $user)
Set the current user.
Definition: Parser.php:972
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:41
ParserOptions\getDisableTitleConversion
getDisableTitleConversion()
Whether title conversion should be disabled.
Definition: ParserOptions.php:513
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1815
Parser\handleHeadings
handleHeadings( $text)
Parse headers and return html.
Definition: Parser.php:1878
getUser
getUser()
MediaWiki\SpecialPage\SpecialPageFactory
Factory for handling the special page list and generating SpecialPage objects.
Definition: SpecialPageFactory.php:65
$wgNoFollowDomainExceptions
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
Definition: DefaultSettings.php:4692
Parser\handleAllQuotes
handleAllQuotes( $text)
Replace single quotes with HTML markup.
Definition: Parser.php:1895
Parser\$mUrlProtocols
$mUrlProtocols
Definition: Parser.php:188
Parser\extractTagsAndParams
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:1218
Parser\$mLinkID
int $mLinkID
Definition: Parser.php:220
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1852
Parser\handleDoubleUnderscore
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition: Parser.php:4006
SFH_OBJECT_ARGS
const SFH_OBJECT_ARGS
Definition: Defines.php:187
Parser\fetchCurrentRevisionRecordOfTitle
fetchCurrentRevisionRecordOfTitle(Title $title)
Fetch the current revision of a given title as a RevisionRecord.
Definition: Parser.php:3462
Sanitizer\normalizeSectionNameWhitespace
static normalizeSectionNameWhitespace( $section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(),...
Definition: Sanitizer.php:1108
OT_PREPROCESS
const OT_PREPROCESS
Definition: Defines.php:175
Parser\normalizeSectionName
static normalizeSectionName( $text)
Apply the same normalization as code making links to this section would.
Definition: Parser.php:6024
NS_FILE
const NS_FILE
Definition: Defines.php:75
OT_PLAIN
const OT_PLAIN
Definition: Defines.php:177
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1283
Parser\recursiveTagParseFully
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition: Parser.php:866
NS_TEMPLATE
const NS_TEMPLATE
Definition: Defines.php:79
Parser\$specialPageFactory
SpecialPageFactory $specialPageFactory
Definition: Parser.php:332
Parser\nextLinkID
nextLinkID()
Definition: Parser.php:1087
Parser\fuzzTestPreprocess
fuzzTestPreprocess( $text, Title $title, ParserOptions $options)
Definition: Parser.php:6109
Parser\fuzzTestPst
fuzzTestPst( $text, Title $title, ParserOptions $options)
Definition: Parser.php:6099
Parser\getTargetLanguage
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:1114
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:540
Parser\$mStripList
$mStripList
Definition: Parser.php:157
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
MediaWiki\Linker\LinkRendererFactory
Factory to create LinkRender objects.
Definition: LinkRendererFactory.php:34
$s
$s
Definition: mergeMessageFileList.php:185
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:92
Parser\getRevisionObject
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition: Parser.php:5775
Parser\guessSectionNameFromWikiText
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition: Parser.php:5985
Parser\setDefaultSort
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition: Parser.php:5921
Preprocessor_Hash
Differences from DOM schema:
Definition: Preprocessor_Hash.php:43
Sanitizer\armorFrenchSpaces
static armorFrenchSpaces( $text, $space='&#160;')
Armor French spaces with a replacement character.
Definition: Sanitizer.php:746
Parser\VERSION
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way,...
Definition: Parser.php:90
StripState
Definition: StripState.php:29
Parser\getExternalLinkRel
static getExternalLinkRel( $url=false, LinkTarget $title=null)
Get the rel attribute for a particular external link.
Definition: Parser.php:2176
Parser\replaceVariables
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:2855
Parser\MARKER_PREFIX
const MARKER_PREFIX
Definition: Parser.php:145
Parser\getFunctionSynonyms
getFunctionSynonyms()
Definition: Parser.php:5502
Parser\$mInParse
bool string $mInParse
Recursive call protection.
Definition: Parser.php:309
Parser\transformMsg
transformMsg( $text, ParserOptions $options, Title $title=null)
Wrapper for preprocess()
Definition: Parser.php:4753
$wgTitle
$wgTitle
Definition: Setup.php:794
Parser\doQuotes
doQuotes( $text)
Helper function for handleAllQuotes()
Definition: Parser.php:1913
Linker\tocLine
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1691
MediaWiki\Languages\LanguageConverterFactory
An interface for creating language converters.
Definition: LanguageConverterFactory.php:44
Parser\$svcOptions
ServiceOptions $svcOptions
This is called $svcOptions instead of $options like elsewhere to avoid confusion with $mOptions,...
Definition: Parser.php:341
Linker\tocList
static tocList( $toc, Language $lang=null)
Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript.
Definition: Linker.php:1727
Parser\SFH_OBJECT_ARGS
const SFH_OBJECT_ARGS
Definition: Parser.php:94
Parser\OT_WIKI
const OT_WIKI
Definition: Parser.php:121
Parser\getTags
getTags()
Accessor.
Definition: Parser.php:5493
Parser\getStripList
getStripList()
Get a list of strippable XML-like elements.
Definition: Parser.php:1281
Parser\initializeVariables
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
Definition: Parser.php:2800
PPFrame\NO_TEMPLATES
const NO_TEMPLATES
Definition: PPFrame.php:30
Preprocessor
Definition: Preprocessor.php:30
Parser\getOptions
getOptions()
Definition: Parser.php:1059
MediaWiki\Languages\LanguageNameUtils
A service that provides utilities to do with language names and codes.
Definition: LanguageNameUtils.php:42
PPFrame\newChild
newChild( $args=false, $title=false, $indexOffset=0)
Create a child frame.
Parser\getFunctionLang
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition: Parser.php:1102
StringUtils\replaceMarkup
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <....
Definition: StringUtils.php:268
Parser\$mRevisionRecordObject
RevisionRecord null $mRevisionRecordObject
Definition: Parser.php:286
Parser\Options
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:1079
Parser\lock
lock()
Lock the current instance of the parser.
Definition: Parser.php:6203
Parser\statelessFetchRevision
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition: Parser.php:3508
Revision
Definition: Revision.php:40
Parser\getDefaultSort
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition: Parser.php:5936
Parser\$mFunctionSynonyms
$mFunctionSynonyms
Definition: Parser.php:156
Parser\$hookRunner
HookRunner $hookRunner
Definition: Parser.php:359
Parser\$nsInfo
NamespaceInfo $nsInfo
Definition: Parser.php:347
Parser\makeLegacyAnchor
makeLegacyAnchor( $sectionName)
Definition: Parser.php:5965
Parser\setHook
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition: Parser.php:4797
Parser\$mHeadings
$mHeadings
Definition: Parser.php:235
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:58
Parser\getTitle
getTitle()
Definition: Parser.php:997
Parser\$mVariables
MagicWordArray $mVariables
Definition: Parser.php:174
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1059
MWException
MediaWiki exception.
Definition: MWException.php:29
Parser\TOC_START
const TOC_START
Definition: Parser.php:148
Parser\statelessFetchTemplate
static statelessFetchTemplate( $title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition: Parser.php:3596
Parser\$mConf
array $mConf
Definition: Parser.php:185
Parser\$ot
$ot
Definition: Parser.php:271
Parser\getRevisionRecordObject
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition: Parser.php:5798
Parser\getUserSig
getUserSig(User $user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext.
Definition: Parser.php:4593
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1027
Parser\OT_MSG
const OT_MSG
Definition: Parser.php:123
Parser\makeKnownLinkHolder
makeKnownLinkHolder(Title $nt, $text='', $trail='', $prefix='')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
Definition: Parser.php:2696
Parser\firstCallInit
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition: Parser.php:520
Parser\$mProfiler
SectionProfiler $mProfiler
Definition: Parser.php:312
Parser\preprocess
preprocess( $text, ?Title $title, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition: Parser.php:909
Parser\getFlatSectionInfo
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition: Parser.php:5716
Parser\$mMarkerIndex
$mMarkerIndex
Definition: Parser.php:162
BlockLevelPass\doBlockLevels
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: BlockLevelPass.php:52
Parser\getCustomDefaultSort
getCustomDefaultSort()
Accessor for $mDefaultSort Unlike getDefaultSort(), will return false if none is set.
Definition: Parser.php:5950
wfUrlProtocolsWithoutProtRel
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
Definition: GlobalFunctions.php:763
Parser\handleTables
handleTables( $text)
Parse the wiki syntax used to render tables.
Definition: Parser.php:1316
$matches
$matches
Definition: NoLocalSettings.php:24
CoreTagHooks\register
static register( $parser)
Definition: CoreTagHooks.php:33
Parser\$contLang
Language $contLang
Definition: Parser.php:323
Parser\makeAnchor
static makeAnchor( $sectionName)
Definition: Parser.php:5961
StringUtils\explode
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
Definition: StringUtils.php:326
PPNode
There are three types of nodes:
Definition: PPNode.php:35
Parser\$factory
ParserFactory $factory
Definition: Parser.php:329
Parser\replaceLinkHoldersPrivate
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:4922
LinkHolderArray
Definition: LinkHolderArray.php:33
Parser\__clone
__clone()
Allow extensions to clean up when the parser is cloned.
Definition: Parser.php:495
PPFrame\RECOVER_ORIG
const RECOVER_ORIG
Definition: PPFrame.php:36
Linker\makeHeadline
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1802
Parser\getHookContainer
getHookContainer()
Get a HookContainer capable of returning metadata about hooks or running extension hooks.
Definition: Parser.php:1619
Parser\callParserFunction
callParserFunction(PPFrame $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition: Parser.php:3305
Parser\extensionSubstitution
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition: Parser.php:3890
Linker\tocLineEnd
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1715
$args
if( $line===false) $args
Definition: mcc.php:124
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:38
Parser\$mLangLinkLanguages
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition: Parser.php:293
Parser\markerSkipCallback
markerSkipCallback( $s, callable $callback)
Call a callback function on all regions of the given text that are not inside strip markers,...
Definition: Parser.php:6130
Parser\fetchFileNoRegister
fetchFileNoRegister(Title $title, array $options=[])
Helper function for fetchFileAndTitle.
Definition: Parser.php:3745
Parser\fetchTemplate
fetchTemplate(Title $title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3582
Parser\limitationWarn
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:2906
Parser\TOC_END
const TOC_END
Definition: Parser.php:149
Parser\PTD_FOR_INCLUSION
const PTD_FOR_INCLUSION
Definition: Parser.php:116
$title
$title
Definition: testCompression.php:38
Parser\recursiveTagParse
recursiveTagParse( $text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition: Parser.php:842
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:846
Parser\fetchFileAndTitle
fetchFileAndTitle(Title $title, array $options=[])
Fetch a file and its title and register a reference to it.
Definition: Parser.php:3720
Parser\finalizeHeadings
finalizeHeadings( $text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition: Parser.php:4078
OT_WIKI
const OT_WIKI
Definition: Defines.php:174
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:591
Parser\$mHighestExpansionDepth
$mHighestExpansionDepth
Definition: Parser.php:231
SectionProfiler
Arbitrary section name based PHP profiling.
Definition: SectionProfiler.php:33
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
Parser\cleanSig
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition: Parser.php:4667
RequestContext
Group all the pieces relevant to the context of a request into one instance @newable.
Definition: RequestContext.php:39
Parser\$mUser
User $mUser
Definition: Parser.php:250
Parser\$mImageParamsMagicArray
$mImageParamsMagicArray
Definition: Parser.php:160
Parser\handleInternalLinks
handleInternalLinks( $text)
Process [[ ]] wikilinks.
Definition: Parser.php:2367
Parser\$mTplRedirCache
$mTplRedirCache
Definition: Parser.php:233
Parser\$mFirstCall
bool $mFirstCall
Whether firstCallInit still needs to be called.
Definition: Parser.php:167
ParserOptions\getPreSaveTransform
getPreSaveTransform()
Transform wiki markup when saving the page?
Definition: ParserOptions.php:615
Parser\getStripState
getStripState()
Get the StripState.
Definition: Parser.php:1290
Parser\getContentLanguage
getContentLanguage()
Get the content language that this Parser is using.
Definition: Parser.php:1185
Parser\OT_PLAIN
const OT_PLAIN
Definition: Parser.php:125
Parser\handleMagicLinks
handleMagicLinks( $text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition: Parser.php:1696
Linker\splitTrail
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition: Linker.php:1823
Parser\__construct
__construct( $svcOptions=null, MagicWordFactory $magicWordFactory=null, Language $contLang=null, ParserFactory $factory=null, $urlProtocols=null, SpecialPageFactory $spFactory=null, $linkRendererFactory=null, $nsInfo=null, $logger=null, BadFileLookup $badFileLookup=null, LanguageConverterFactory $languageConverterFactory=null, HookContainer $hookContainer=null)
Constructing parsers directly is deprecated! Use a ParserFactory.
Definition: Parser.php:403
Parser\insertStripItem
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:1303
Parser\fuzzTestSrvus
fuzzTestSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition: Parser.php:6081
Parser\isCurrentRevisionOfTitleCached
isCurrentRevisionOfTitleCached(Title $title)
Definition: Parser.php:3491
Parser\getFreshParser
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition: Parser.php:6250
Parser\getRevisionUser
getRevisionUser()
Get the name of the user that edited the last revision.
Definition: Parser.php:5877
Parser\setOptions
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition: Parser.php:1068
Parser\getImageParams
getImageParams( $handler)
Definition: Parser.php:5113
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:718
Parser\$mAutonumber
$mAutonumber
Definition: Parser.php:203
Parser\replaceLinkHolders
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:4911
Parser\addTrackingCategory
addTrackingCategory( $msg)
Definition: Parser.php:4059
Parser\getUrlProtocols
getUrlProtocols()
Definition: Parser.php:5511
Parser\incrementIncludeSize
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition: Parser.php:3980
Parser\getTargetLanguageConverter
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition: Parser.php:1595
ParserFactory
Definition: ParserFactory.php:33
Parser\startExternalParse
startExternalParse(?Title $title, ParserOptions $options, $outputType, $clearState=true, $revId=null)
Set up some variables which are usually set up in parse() so that an external function can call some ...
Definition: Parser.php:4719
$content
$content
Definition: router.php:76
CoreParserFunctions\register
static register( $parser)
Definition: CoreParserFunctions.php:37
Parser\makeFreeExternalLink
makeFreeExternalLink( $url, $numPostProto)
Make a free external link, given a user-supplied URL.
Definition: Parser.php:1806
Parser\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: Parser.php:364
PPFrame\expand
expand( $root, $flags=0)
Expand a document tree node.
ILanguageConverter
The shared interface for all language converters.
Definition: ILanguageConverter.php:28
$wgNoFollowNsExceptions
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
Definition: DefaultSettings.php:4677
$wgNoFollowLinks
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
Definition: DefaultSettings.php:4671
Parser\$mOutput
ParserOutput $mOutput
Definition: Parser.php:202
Parser\$mFunctionHooks
$mFunctionHooks
Definition: Parser.php:155
ParserFactory\$inParserFactory
static int $inParserFactory
Track calls to Parser constructor to aid in deprecation of direct Parser invocation.
Definition: ParserFactory.php:72
Parser\$mOptions
ParserOptions null $mOptions
Definition: Parser.php:259
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:57
Parser\$mRevisionUser
$mRevisionUser
Definition: Parser.php:279
User\getOption
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2560
Sanitizer\validateTagAttributes
static validateTagAttributes( $attribs, $element)
Take an array of attribute names and values and normalize or discard illegal values for the given ele...
Definition: Sanitizer.php:390
Parser\extractSections
extractSections( $text, $sectionId, $mode, $newText='')
Break wikitext input into sections, and either pull or replace some particular section's text.
Definition: Parser.php:5544
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
Parser\OT_HTML
const OT_HTML
Definition: Parser.php:120
PPFrame
Definition: PPFrame.php:28
$line
$line
Definition: mcc.php:119
Parser\EXT_LINK_URL_CLASS
const EXT_LINK_URL_CLASS
Definition: Parser.php:102
Parser\renderImageGallery
renderImageGallery( $text, array $params)
Renders an image gallery from a text with one line per image.
Definition: Parser.php:4951
StringUtils\delimiterExplode
static delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
Definition: StringUtils.php:59
OutputPage\setupOOUI
static setupOOUI( $skinName='default', $dir='ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
Definition: OutputPage.php:4110
Parser\magicLinkCallback
magicLinkCallback(array $m)
Definition: Parser.php:1727
Parser\getUser
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition: Parser.php:1132
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1494
Parser\incrementExpensiveFunctionCount
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition: Parser.php:3994
Parser\$mImageParams
$mImageParams
Definition: Parser.php:159
Parser\setFunctionHook
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition: Parser.php:4862
Parser\setLinkID
setLinkID( $id)
Definition: Parser.php:1094
Sanitizer\cleanUrl
static cleanUrl( $url)
Definition: Sanitizer.php:1625
Parser\$magicWordFactory
MagicWordFactory $magicWordFactory
Definition: Parser.php:320
Parser\preprocessToDom
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:2830
Parser
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:84
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:280
Parser\getMagicWordFactory
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition: Parser.php:1175
Parser\argSubstitution
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition: Parser.php:3837
Linker\makeMediaLinkFile
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition: Linker.php:778
Sanitizer\fixTagAttributes
static fixTagAttributes( $text, $element, $sorted=false)
Take a tag soup fragment listing an HTML element's attributes and normalize it to well-formed XML,...
Definition: Sanitizer.php:703
Parser\getPreloadText
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:951
Parser\setOutputType
setOutputType( $ot)
Mutator for the output type.
Definition: Parser.php:1026
Parser\getTemplateDom
getTemplateDom(Title $title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition: Parser.php:3394
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:49
Parser\OT_PREPROCESS
const OT_PREPROCESS
Definition: Parser.php:122
Parser\getExternalLinkAttribs
getExternalLinkAttribs( $url)
Get an associative array of additional HTML attributes appropriate for a particular external link.
Definition: Parser.php:2198
SFH_NO_HASH
const SFH_NO_HASH
Definition: Defines.php:186
Hooks\isRegistered
static isRegistered( $name)
Returns true if a hook has a function registered to it.
Definition: Hooks.php:88
Parser\$mStripState
StripState $mStripState
Definition: Parser.php:209
Parser\internalParse
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1526
OT_HTML
const OT_HTML
Definition: Defines.php:173
Parser\validateSig
validateSig( $text)
Check that the user's signature contains no bad XML.
Definition: Parser.php:4653
Parser\$mPPNodeCount
$mPPNodeCount
Definition: Parser.php:224
Title
Represents a title within MediaWiki.
Definition: Title.php:41
Parser\resetOutput
resetOutput()
Reset the ParserOutput.
Definition: Parser.php:588
Parser\stripOuterParagraph
static stripOuterParagraph( $html)
Strip outer.
Definition: Parser.php:6231
Parser\$mVarCache
$mVarCache
Definition: Parser.php:158
Parser\$mDefaultSort
$mDefaultSort
Definition: Parser.php:232
Parser\$mExpensiveFunctionCount
$mExpensiveFunctionCount
Definition: Parser.php:239
Parser\normalizeLinkUrl
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition: Parser.php:2229
MediaWiki\Preferences\SignatureValidator
Definition: SignatureValidator.php:37
Parser\interwikiTransclude
interwikiTransclude(Title $title, $action)
Transclude an interwiki link.
Definition: Parser.php:3768
Parser\$mExtLinkBracketedRegex
$mExtLinkBracketedRegex
Definition: Parser.php:188
wfMatchesDomainList
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
Definition: GlobalFunctions.php:876
Parser\$mIncludeSizes
$mIncludeSizes
Definition: Parser.php:222
$cache
$cache
Definition: mcc.php:33
MalformedTitleException
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Definition: MalformedTitleException.php:26
Parser\getSection
getSection( $text, $sectionId, $defaultText='')
This function returns the text of a section, specified by a number ($section).
Definition: Parser.php:5667
Xml\isWellFormedXmlFragment
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:737
Parser\$mRevisionTimestamp
$mRevisionTimestamp
Definition: Parser.php:277
Parser\replaceSection
replaceSection( $oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition: Parser.php:5683
Sanitizer\ID_PRIMARY
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
Definition: Sanitizer.php:70
Parser\$logger
LoggerInterface $logger
Definition: Parser.php:350
ParserOptions\getUser
getUser()
Current user.
Definition: ParserOptions.php:1029
PPFrame\virtualBracketedImplode
virtualBracketedImplode( $start, $sep, $end,... $params)
Virtual implode with brackets.
Parser\armorLinks
armorLinks( $text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition: Parser.php:2720
Linker\tocUnindent
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1676
Linker\makeImageLink
static makeImageLink(Parser $parser, LinkTarget $title, $file, $frameParams=[], $handlerParams=[], $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
Definition: Linker.php:299
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
Parser\getBadFileLookup
getBadFileLookup()
Get the BadFileLookup instance that this Parser is using.
Definition: Parser.php:1195
Parser\getOutput
getOutput()
Definition: Parser.php:1052
StringUtils\delimiterReplace
static delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
Definition: StringUtils.php:248
Parser\handleInternalLinks2
handleInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition: Parser.php:2377
Parser\preSaveTransform
preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition: Parser.php:4480
Parser\getOutputType
getOutputType()
Accessor for the output type.
Definition: Parser.php:1018
Parser\$mGeneratedPPNodeCount
$mGeneratedPPNodeCount
Definition: Parser.php:229
Parser\statelessFetchRevisionRecord
static statelessFetchRevisionRecord(Title $title, $parser=null)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition: Parser.php:3525
Parser\getHookRunner
getHookRunner()
Get a HookRunner for calling core hooks.
Definition: Parser.php:1631
PPFrame\getArgument
getArgument( $name)
Get an argument to this frame by name.
Sanitizer\normalizeCharReferences
static normalizeCharReferences( $text)
Ensure that any entities and character references are legal for XML and XHTML specifically.
Definition: Sanitizer.php:1127
TextContent\normalizeLineEndings
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Definition: TextContent.php:203
Parser\getSectionNameFromStrippedText
static getSectionNameFromStrippedText( $text)
Definition: Parser.php:5954
Sanitizer\escapeIdForLink
static escapeIdForLink( $id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
Definition: Sanitizer.php:842
Linker\normalizeSubpageLink
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition: Linker.php:1481
MediaWiki\Config\ServiceOptions\get
get( $key)
Definition: ServiceOptions.php:84
Parser\startParse
startParse(?Title $title, ParserOptions $options, $outputType, $clearState=true)
Definition: Parser.php:4734
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
Parser\SFH_NO_HASH
const SFH_NO_HASH
Definition: Parser.php:93
CoreMagicVariables\expand
static expand(Parser $parser, string $id, int $ts, NamespaceInfo $nsInfo, ServiceOptions $svcOptions, LoggerInterface $logger)
Expand the magic variable given by $index.
Definition: CoreMagicVariables.php:48
Parser\$mShowToc
$mShowToc
Definition: Parser.php:241
ImageGalleryBase\factory
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
Definition: ImageGalleryBase.php:116
Sanitizer\decodeTagAttributes
static decodeTagAttributes( $text)
Return an associative array of attribute names and values from a partial tag string.
Definition: Sanitizer.php:1008
Parser\guessSectionNameFromStrippedText
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition: Parser.php:6013
Parser\fetchTemplateAndTitle
fetchTemplateAndTitle(Title $title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3537
Parser\$languageConverterFactory
LanguageConverterFactory $languageConverterFactory
Definition: Parser.php:326
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
PPFrame\isTemplate
isTemplate()
Return true if the frame is a template frame.
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
Parser\parseLinkParameter
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition: Parser.php:5389
$t
$t
Definition: testCompression.php:74
Title\legalChars
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:702
Parser\$mRevisionObject
$mRevisionObject
Definition: Parser.php:273
Parser\fetchCurrentRevisionOfTitle
fetchCurrentRevisionOfTitle(Title $title)
Fetch the current revision of a given title.
Definition: Parser.php:3440
Sanitizer\decodeCharReferences
static decodeCharReferences( $text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string.
Definition: Sanitizer.php:1232
Parser\getRevisionTimestamp
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition: Parser.php:5852
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
Parser\expandMagicVariable
expandMagicVariable( $index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition: Parser.php:2747
User\getBoolOption
getBoolOption( $oname)
Get the user's current setting for a given option, as a boolean value.
Definition: User.php:2592
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Parser\$mPreprocessor
Preprocessor $mPreprocessor
Definition: Parser.php:195
Parser\parseWidthParam
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition: Parser.php:6174
RawMessage
Variant of the Message class.
Definition: RawMessage.php:35
Parser\cleanSigInSig
static cleanSigInSig( $text)
Strip 3, 4 or 5 tildes out of signatures.
Definition: Parser.php:4704
Parser\setTitle
setTitle(Title $t=null)
Set the context title.
Definition: Parser.php:981
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
Parser\replaceLinkHoldersText
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition: Parser.php:4933
Parser\normalizeUrlComponent
static normalizeUrlComponent( $component, $unsafe)
Definition: Parser.php:2287
Parser\clearTagHooks
clearTagHooks()
Remove all tag hooks.
Definition: Parser.php:4814
MWTimestamp\getLocalInstance
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Definition: MWTimestamp.php:205
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2045
Linker\makeExternalImage
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition: Linker.php:243
Parser\$mTitle
Title null $mTitle
Since 1.34, leaving mTitle uninitialized or setting mTitle to null is deprecated.
Definition: Parser.php:268
Parser\getLinkRenderer
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition: Parser.php:1157
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:41
MediaWiki\Debug\DeprecatablePropertyArray
ArrayAccess implementation that supports deprecating access to certain properties.
Definition: DeprecatablePropertyArray.php:16
Parser\parse
parse( $text, Title $title, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition: Parser.php:610
Parser\$mRevisionId
$mRevisionId
Definition: Parser.php:275
RequestContext\setTitle
setTitle(Title $title=null)
Definition: RequestContext.php:166
Parser\$mRevisionSize
$mRevisionSize
Definition: Parser.php:281
Parser\getRevisionId
getRevisionId()
Get the ID of the revision we are parsing.
Definition: Parser.php:5764
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Parser\pstPass2
pstPass2( $text, User $user)
Pre-save transform helper function.
Definition: Parser.php:4517
Parser\clearState
clearState()
Clear Parser state.
Definition: Parser.php:538
Parser\guessLegacySectionNameFromWikiText
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition: Parser.php:6001
MWHttpRequest\factory
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
Definition: MWHttpRequest.php:195
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:62
Parser\makeImage
makeImage(Title $title, $options, $holders=false)
Parse image options text and use it to make an image.
Definition: Parser.php:5167
Parser\stripSectionName
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition: Parser.php:6052
Parser\EXT_IMAGE_REGEX
const EXT_IMAGE_REGEX
Definition: Parser.php:109
Parser\getPreprocessor
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:1144
Parser\doBlockLevels
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: Parser.php:2734
Parser\$hookContainer
HookContainer $hookContainer
Definition: Parser.php:356
$type
$type
Definition: testCompression.php:52
MWTidy\tidy
static tidy( $text)
Interface with Remex tidy.
Definition: MWTidy.php:42