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 $mFunctionTagHooks = [];
158  private $mStripList = [];
159  private $mVarCache = [];
160  private $mImageParams = [];
163  public $mMarkerIndex = 0;
168  public $mFirstCall = true;
169 
170  # Initialised by initializeVariables()
171 
175  private $mVariables;
176 
180  private $mSubstWords;
181 
186  private $mConf;
187 
188  # Initialised in constructor
190 
191  # Initialized in getPreprocessor()
192 
197 
198  # Cleared with clearState():
199 
203  public $mOutput;
204  private $mAutonumber;
205 
210  public $mStripState;
211 
215  private $mLinkHolders;
216 
221  public $mLinkID;
233  private $mDefaultSort;
236  public $mHeadings;
240  public $mExpensiveFunctionCount; # number of expensive parser function calls
242  public $mShowToc;
245  private $mTplDomCache;
246 
251  public $mUser; # User object; only used when doing pre-save transform
252 
253  # Temporary
254  # These are variables reset at least once per parse regardless of $clearState
255 
260  public $mOptions;
261 
269  public $mTitle; # Title context, used for self-link rendering and similar things
270  private $mOutputType; # Output type, one of the OT_xxx constants
272  public $ot; # Shortcut alias, see setOutputType()
274  public $mRevisionObject; # The revision object of the specified revision ID
275 
276  public $mRevisionId; # ID to display in {{REVISIONID}} tags
278  public $mRevisionTimestamp; # The timestamp of the specified revision ID
280  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
282  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
284  public $mInputSize = false; # For {{PAGESIZE}} on current page.
285 
288 
295 
303 
310  public $mInParse = false;
311 
313  private $mProfiler;
314 
318  private $mLinkRenderer;
319 
322 
324  private $contLang;
325 
328 
330  private $factory;
331 
334 
342  private $svcOptions;
343 
346 
348  private $nsInfo;
349 
351  private $logger;
352 
354  private $badFileLookup;
355 
357  private $hookContainer;
358 
360  private $hookRunner;
361 
366  public const CONSTRUCTOR_OPTIONS = [
367  // Deprecated and unused; from $wgParserConf
368  'class',
369  // See documentation for the corresponding config options
370  'ArticlePath',
371  'EnableScaryTranscluding',
372  'ExtraInterlanguageLinkPrefixes',
373  'FragmentMode',
374  'LanguageCode',
375  'MaxSigChars',
376  'MaxTocLevel',
377  'MiserMode',
378  'ScriptPath',
379  'Server',
380  'ServerName',
381  'ShowHostnames',
382  'SignatureValidation',
383  'Sitename',
384  'StylePath',
385  'TranscludeCacheExpiry',
386  ];
387 
405  public function __construct(
406  $svcOptions = null,
408  Language $contLang = null,
409  ParserFactory $factory = null,
410  $urlProtocols = null,
411  SpecialPageFactory $spFactory = null,
412  $linkRendererFactory = null,
413  $nsInfo = null,
414  $logger = null,
418  ) {
419  if ( ParserFactory::$inParserFactory === 0 ) {
420  // Direct construction of Parser is deprecated; use a ParserFactory
421  wfDeprecated( __METHOD__, '1.34' );
422  }
423  if ( !$svcOptions || is_array( $svcOptions ) ) {
424  wfDeprecated( 'old calling convention for ' . __METHOD__, '1.34' );
425  // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is
426  // Config, and the eighth is LinkRendererFactory.
427  $this->mConf = (array)$svcOptions;
428  if ( empty( $this->mConf['class'] ) ) {
429  $this->mConf['class'] = self::class;
430  }
431  $this->svcOptions = new ServiceOptions( self::CONSTRUCTOR_OPTIONS,
432  $this->mConf, func_num_args() > 6
433  ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
434  );
435  $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null;
436  $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null;
437  } else {
438  // New calling convention
439  $svcOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
440  // $this->mConf is public, so we'll keep the option there for
441  // compatibility until it's removed
442  $this->mConf = [
443  'class' => $svcOptions->get( 'class' ),
444  ];
445  $this->svcOptions = $svcOptions;
446  }
447 
448  $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
449  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
450  self::EXT_LINK_ADDR .
451  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
452 
453  $this->magicWordFactory = $magicWordFactory ??
454  MediaWikiServices::getInstance()->getMagicWordFactory();
455 
456  $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
457 
458  $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory();
459  $this->specialPageFactory = $spFactory ??
460  MediaWikiServices::getInstance()->getSpecialPageFactory();
461  $this->linkRendererFactory = $linkRendererFactory ??
462  MediaWikiServices::getInstance()->getLinkRendererFactory();
463  $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
464  $this->logger = $logger ?: new NullLogger();
465  $this->badFileLookup = $badFileLookup ??
466  MediaWikiServices::getInstance()->getBadFileLookup();
467 
468  $this->languageConverterFactory = $languageConverterFactory ??
469  MediaWikiServices::getInstance()->getLanguageConverterFactory();
470 
471  $this->hookContainer = $hookContainer ??
472  MediaWikiServices::getInstance()->getHookContainer();
473  $this->hookRunner = new HookRunner( $this->hookContainer );
474 
475  // T250444: This will eventually be inlined here and the
476  // standalone method removed.
477  $this->firstCallInit();
478  }
479 
483  public function __destruct() {
484  if ( isset( $this->mLinkHolders ) ) {
485  // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
486  unset( $this->mLinkHolders );
487  }
488  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
489  foreach ( $this as $name => $value ) {
490  unset( $this->$name );
491  }
492  }
493 
497  public function __clone() {
498  $this->mInParse = false;
499 
500  // T58226: When you create a reference "to" an object field, that
501  // makes the object field itself be a reference too (until the other
502  // reference goes out of scope). When cloning, any field that's a
503  // reference is copied as a reference in the new object. Both of these
504  // are defined PHP5 behaviors, as inconvenient as it is for us when old
505  // hooks from PHP4 days are passing fields by reference.
506  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
507  // Make a non-reference copy of the field, then rebind the field to
508  // reference the new copy.
509  $tmp = $this->$k;
510  $this->$k =& $tmp;
511  unset( $tmp );
512  }
513 
514  $this->hookRunner->onParserCloned( $this );
515  }
516 
522  public function firstCallInit() {
523  if ( !$this->mFirstCall ) {
524  return;
525  }
526  $this->mFirstCall = false;
527 
529  CoreTagHooks::register( $this );
530  $this->initializeVariables();
531 
532  $this->hookRunner->onParserFirstCallInit( $this );
533  }
534 
540  public function clearState() {
541  $this->firstCallInit();
542  $this->resetOutput();
543  $this->mAutonumber = 0;
544  $this->mLinkHolders = new LinkHolderArray(
545  $this,
547  $this->getHookContainer()
548  );
549  $this->mLinkID = 0;
550  $this->mRevisionObject = $this->mRevisionTimestamp =
551  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
552  $this->mRevisionRecordObject = null;
553  $this->mVarCache = [];
554  $this->mUser = null;
555  $this->mLangLinkLanguages = [];
556  $this->currentRevisionCache = null;
557 
558  $this->mStripState = new StripState( $this );
559 
560  # Clear these on every parse, T6549
561  $this->mTplRedirCache = $this->mTplDomCache = [];
562 
563  $this->mShowToc = true;
564  $this->mForceTocPosition = false;
565  $this->mIncludeSizes = [
566  'post-expand' => 0,
567  'arg' => 0,
568  ];
569  $this->mPPNodeCount = 0;
570  $this->mGeneratedPPNodeCount = 0;
571  $this->mHighestExpansionDepth = 0;
572  $this->mDefaultSort = false;
573  $this->mHeadings = [];
574  $this->mDoubleUnderscores = [];
575  $this->mExpensiveFunctionCount = 0;
576 
577  # Fix cloning
578  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
579  $this->mPreprocessor = null;
580  }
581 
582  $this->mProfiler = new SectionProfiler();
583 
584  $this->hookRunner->onParserClearState( $this );
585  }
586 
590  public function resetOutput() {
591  $this->mOutput = new ParserOutput;
592  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
593  }
594 
612  public function parse(
613  $text, Title $title, ParserOptions $options,
614  $linestart = true, $clearState = true, $revid = null
615  ) {
616  if ( $clearState ) {
617  // We use U+007F DELETE to construct strip markers, so we have to make
618  // sure that this character does not occur in the input text.
619  $text = strtr( $text, "\x7f", "?" );
620  $magicScopeVariable = $this->lock();
621  }
622  // Strip U+0000 NULL (T159174)
623  $text = str_replace( "\000", '', $text );
624 
625  $this->startParse( $title, $options, self::OT_HTML, $clearState );
626 
627  $this->currentRevisionCache = null;
628  $this->mInputSize = strlen( $text );
629  if ( $this->mOptions->getEnableLimitReport() ) {
630  $this->mOutput->resetParseStartTime();
631  }
632 
633  $oldRevisionId = $this->mRevisionId;
634  $oldRevisionObject = $this->mRevisionObject;
635  $oldRevisionRecordObject = $this->mRevisionRecordObject;
636  $oldRevisionTimestamp = $this->mRevisionTimestamp;
637  $oldRevisionUser = $this->mRevisionUser;
638  $oldRevisionSize = $this->mRevisionSize;
639  if ( $revid !== null ) {
640  $this->mRevisionId = $revid;
641  $this->mRevisionObject = null;
642  $this->mRevisionRecordObject = null;
643  $this->mRevisionTimestamp = null;
644  $this->mRevisionUser = null;
645  $this->mRevisionSize = null;
646  }
647 
648  $this->hookRunner->onParserBeforeStrip( $this, $text, $this->mStripState );
649  # No more strip!
650  $this->hookRunner->onParserAfterStrip( $this, $text, $this->mStripState );
651  $text = $this->internalParse( $text );
652  $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
653 
654  $text = $this->internalParseHalfParsed( $text, true, $linestart );
655 
663  if ( !( $options->getDisableTitleConversion()
664  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
665  || isset( $this->mDoubleUnderscores['notitleconvert'] )
666  || $this->mOutput->getDisplayTitle() !== false )
667  ) {
668  $convruletitle = $this->getTargetLanguageConverter()->getConvRuleTitle();
669  if ( $convruletitle ) {
670  $this->mOutput->setTitleText( $convruletitle );
671  } else {
672  $titleText = $this->getTargetLanguageConverter()->convertTitle( $title );
673  $this->mOutput->setTitleText( $titleText );
674  }
675  }
676 
677  # Compute runtime adaptive expiry if set
678  $this->mOutput->finalizeAdaptiveCacheExpiry();
679 
680  # Warn if too many heavyweight parser functions were used
681  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
682  $this->limitationWarn( 'expensive-parserfunction',
683  $this->mExpensiveFunctionCount,
684  $this->mOptions->getExpensiveParserFunctionLimit()
685  );
686  }
687 
688  # Information on limits, for the benefit of users who try to skirt them
689  if ( $this->mOptions->getEnableLimitReport() ) {
690  $text .= $this->makeLimitReport();
691  }
692 
693  # Wrap non-interface parser output in a <div> so it can be targeted
694  # with CSS (T37247)
695  $class = $this->mOptions->getWrapOutputClass();
696  if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
697  $this->mOutput->addWrapperDivClass( $class );
698  }
699 
700  $this->mOutput->setText( $text );
701 
702  $this->mRevisionId = $oldRevisionId;
703  $this->mRevisionObject = $oldRevisionObject;
704  $this->mRevisionRecordObject = $oldRevisionRecordObject;
705  $this->mRevisionTimestamp = $oldRevisionTimestamp;
706  $this->mRevisionUser = $oldRevisionUser;
707  $this->mRevisionSize = $oldRevisionSize;
708  $this->mInputSize = false;
709  $this->currentRevisionCache = null;
710 
711  return $this->mOutput;
712  }
713 
720  protected function makeLimitReport() {
721  $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
722 
723  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
724  if ( $cpuTime !== null ) {
725  $this->mOutput->setLimitReportData( 'limitreport-cputime',
726  sprintf( "%.3f", $cpuTime )
727  );
728  }
729 
730  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
731  $this->mOutput->setLimitReportData( 'limitreport-walltime',
732  sprintf( "%.3f", $wallTime )
733  );
734 
735  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
736  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
737  );
738  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
739  [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
740  );
741  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
742  [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
743  );
744  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
745  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
746  );
747  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
748  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
749  );
750 
751  foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
752  $this->mOutput->setLimitReportData( $key, $value );
753  }
754 
755  $this->hookRunner->onParserLimitReportPrepare( $this, $this->mOutput );
756 
757  $limitReport = "NewPP limit report\n";
758  if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
759  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
760  }
761  $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
762  $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
763  $limitReport .= 'Dynamic content: ' .
764  ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
765  "\n";
766  $limitReport .= 'Complications: [' . implode( ', ', $this->mOutput->getAllFlags() ) . "]\n";
767 
768  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
769  if ( $this->hookRunner->onParserLimitReportFormat(
770  $key, $value, $limitReport, false, false )
771  ) {
772  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
773  $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
774  ->inLanguage( 'en' )->useDatabase( false );
775  if ( !$valueMsg->exists() ) {
776  $valueMsg = new RawMessage( '$1' );
777  }
778  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
779  $valueMsg->params( $value );
780  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
781  }
782  }
783  }
784  // Since we're not really outputting HTML, decode the entities and
785  // then re-encode the things that need hiding inside HTML comments.
786  $limitReport = htmlspecialchars_decode( $limitReport );
787 
788  // Sanitize for comment. Note '‐' in the replacement is U+2010,
789  // which looks much like the problematic '-'.
790  $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
791  $text = "\n<!-- \n$limitReport-->\n";
792 
793  // Add on template profiling data in human/machine readable way
794  $dataByFunc = $this->mProfiler->getFunctionStats();
795  uasort( $dataByFunc, function ( $a, $b ) {
796  return $b['real'] <=> $a['real']; // descending order
797  } );
798  $profileReport = [];
799  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
800  $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
801  $item['%real'], $item['real'], $item['calls'],
802  htmlspecialchars( $item['name'] ) );
803  }
804  $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
805  $text .= implode( "\n", $profileReport ) . "\n-->\n";
806 
807  $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
808 
809  // Add other cache related metadata
810  if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
811  $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
812  }
813  $this->mOutput->setLimitReportData( 'cachereport-timestamp',
814  $this->mOutput->getCacheTime() );
815  $this->mOutput->setLimitReportData( 'cachereport-ttl',
816  $this->mOutput->getCacheExpiry() );
817  $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
818  $this->mOutput->hasDynamicContent() );
819 
820  return $text;
821  }
822 
847  public function recursiveTagParse( $text, $frame = false ) {
848  $this->hookRunner->onParserBeforeStrip( $this, $text, $this->mStripState );
849  $this->hookRunner->onParserAfterStrip( $this, $text, $this->mStripState );
850  $text = $this->internalParse( $text, false, $frame );
851  return $text;
852  }
853 
873  public function recursiveTagParseFully( $text, $frame = false ) {
874  $text = $this->recursiveTagParse( $text, $frame );
875  $text = $this->internalParseHalfParsed( $text, false );
876  return $text;
877  }
878 
898  public function parseExtensionTagAsTopLevelDoc( $text ) {
899  $text = $this->recursiveTagParse( $text );
900  $this->hookRunner->onParserAfterParse( $this, $text, $this->mStripState );
901  $text = $this->internalParseHalfParsed( $text, true );
902  return $text;
903  }
904 
916  public function preprocess( $text, ?Title $title,
917  ParserOptions $options, $revid = null, $frame = false
918  ) {
919  $magicScopeVariable = $this->lock();
920  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
921  if ( $revid !== null ) {
922  $this->mRevisionId = $revid;
923  }
924  $this->hookRunner->onParserBeforeStrip( $this, $text, $this->mStripState );
925  $this->hookRunner->onParserAfterStrip( $this, $text, $this->mStripState );
926  $this->hookRunner->onParserBeforePreprocess( $this, $text, $this->mStripState );
927  $text = $this->replaceVariables( $text, $frame );
928  $text = $this->mStripState->unstripBoth( $text );
929  return $text;
930  }
931 
941  public function recursivePreprocess( $text, $frame = false ) {
942  $text = $this->replaceVariables( $text, $frame );
943  $text = $this->mStripState->unstripBoth( $text );
944  return $text;
945  }
946 
960  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
961  $msg = new RawMessage( $text );
962  $text = $msg->params( $params )->plain();
963 
964  # Parser (re)initialisation
965  $magicScopeVariable = $this->lock();
966  $this->startParse( $title, $options, self::OT_PLAIN, true );
967 
969  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
970  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
971  $text = $this->mStripState->unstripBoth( $text );
972  return $text;
973  }
974 
981  public function setUser( ?User $user ) {
982  $this->mUser = $user;
983  }
984 
990  public function setTitle( Title $t = null ) {
991  if ( !$t ) {
992  $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
993  }
994 
995  if ( $t->hasFragment() ) {
996  # Strip the fragment to avoid various odd effects
997  $this->mTitle = $t->createFragmentTarget( '' );
998  } else {
999  $this->mTitle = $t;
1000  }
1001  }
1002 
1006  public function getTitle() : Title {
1007  return $this->mTitle;
1008  }
1009 
1017  public function Title( Title $x = null ) : ?Title {
1018  wfDeprecated( __METHOD__, '1.35' );
1019  return wfSetVar( $this->mTitle, $x );
1020  }
1021 
1027  public function getOutputType(): int {
1028  return $this->mOutputType;
1029  }
1030 
1035  public function setOutputType( $ot ): void {
1036  $this->mOutputType = $ot;
1037  # Shortcut alias
1038  $this->ot = [
1039  'html' => $ot == self::OT_HTML,
1040  'wiki' => $ot == self::OT_WIKI,
1041  'pre' => $ot == self::OT_PREPROCESS,
1042  'plain' => $ot == self::OT_PLAIN,
1043  ];
1044  }
1045 
1053  public function OutputType( $x = null ) {
1054  wfDeprecated( __METHOD__, '1.35' );
1055  return wfSetVar( $this->mOutputType, $x );
1056  }
1057 
1061  public function getOutput() {
1062  return $this->mOutput;
1063  }
1064 
1068  public function getOptions() {
1069  return $this->mOptions;
1070  }
1071 
1077  public function setOptions( ParserOptions $options ): void {
1078  $this->mOptions = $options;
1079  }
1080 
1088  public function Options( $x = null ) {
1089  wfDeprecated( __METHOD__, '1.35' );
1090  return wfSetVar( $this->mOptions, $x );
1091  }
1092 
1096  public function nextLinkID() {
1097  return $this->mLinkID++;
1098  }
1099 
1103  public function setLinkID( $id ) {
1104  $this->mLinkID = $id;
1105  }
1106 
1111  public function getFunctionLang() {
1112  return $this->getTargetLanguage();
1113  }
1114 
1123  public function getTargetLanguage() {
1124  $target = $this->mOptions->getTargetLanguage();
1125 
1126  if ( $target !== null ) {
1127  return $target;
1128  } elseif ( $this->mOptions->getInterfaceMessage() ) {
1129  return $this->mOptions->getUserLangObj();
1130  }
1131 
1132  return $this->getTitle()->getPageLanguage();
1133  }
1134 
1141  public function getUser() {
1142  if ( $this->mUser !== null ) {
1143  return $this->mUser;
1144  }
1145  return $this->mOptions->getUser();
1146  }
1147 
1153  public function getPreprocessor() {
1154  if ( !isset( $this->mPreprocessor ) ) {
1155  $this->mPreprocessor = new Preprocessor_Hash( $this );
1156  }
1157  return $this->mPreprocessor;
1158  }
1159 
1166  public function getLinkRenderer() {
1167  // XXX We make the LinkRenderer with current options and then cache it forever
1168  if ( !$this->mLinkRenderer ) {
1169  $this->mLinkRenderer = $this->linkRendererFactory->create();
1170  $this->mLinkRenderer->setStubThreshold(
1171  $this->getOptions()->getStubThreshold()
1172  );
1173  }
1174 
1175  return $this->mLinkRenderer;
1176  }
1177 
1184  public function getMagicWordFactory() {
1185  return $this->magicWordFactory;
1186  }
1187 
1194  public function getContentLanguage() {
1195  return $this->contLang;
1196  }
1197 
1204  public function getBadFileLookup() {
1205  return $this->badFileLookup;
1206  }
1207 
1227  public static function extractTagsAndParams( array $elements, $text, &$matches ) {
1228  static $n = 1;
1229  $stripped = '';
1230  $matches = [];
1231 
1232  $taglist = implode( '|', $elements );
1233  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1234 
1235  while ( $text != '' ) {
1236  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1237  $stripped .= $p[0];
1238  if ( count( $p ) < 5 ) {
1239  break;
1240  }
1241  if ( count( $p ) > 5 ) {
1242  # comment
1243  $element = $p[4];
1244  $attributes = '';
1245  $close = '';
1246  $inside = $p[5];
1247  } else {
1248  # tag
1249  list( , $element, $attributes, $close, $inside ) = $p;
1250  }
1251 
1252  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1253  $stripped .= $marker;
1254 
1255  if ( $close === '/>' ) {
1256  # Empty element tag, <tag />
1257  $content = null;
1258  $text = $inside;
1259  $tail = null;
1260  } else {
1261  if ( $element === '!--' ) {
1262  $end = '/(-->)/';
1263  } else {
1264  $end = "/(<\\/$element\\s*>)/i";
1265  }
1266  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1267  $content = $q[0];
1268  if ( count( $q ) < 3 ) {
1269  # No end tag -- let it run out to the end of the text.
1270  $tail = '';
1271  $text = '';
1272  } else {
1273  list( , $tail, $text ) = $q;
1274  }
1275  }
1276 
1277  $matches[$marker] = [ $element,
1278  $content,
1279  Sanitizer::decodeTagAttributes( $attributes ),
1280  "<$element$attributes$close$content$tail" ];
1281  }
1282  return $stripped;
1283  }
1284 
1290  public function getStripList() {
1291  return $this->mStripList;
1292  }
1293 
1299  public function getStripState() {
1300  return $this->mStripState;
1301  }
1302 
1312  public function insertStripItem( $text ) {
1313  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1314  $this->mMarkerIndex++;
1315  $this->mStripState->addGeneral( $marker, $text );
1316  return $marker;
1317  }
1318 
1325  private function handleTables( $text ) {
1326  $lines = StringUtils::explode( "\n", $text );
1327  $out = '';
1328  $td_history = []; # Is currently a td tag open?
1329  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1330  $tr_history = []; # Is currently a tr tag open?
1331  $tr_attributes = []; # history of tr attributes
1332  $has_opened_tr = []; # Did this table open a <tr> element?
1333  $indent_level = 0; # indent level of the table
1334 
1335  foreach ( $lines as $outLine ) {
1336  $line = trim( $outLine );
1337 
1338  if ( $line === '' ) { # empty line, go to next line
1339  $out .= $outLine . "\n";
1340  continue;
1341  }
1342 
1343  $first_character = $line[0];
1344  $first_two = substr( $line, 0, 2 );
1345  $matches = [];
1346 
1347  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1348  # First check if we are starting a new table
1349  $indent_level = strlen( $matches[1] );
1350 
1351  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1352  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1353 
1354  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1355  array_push( $td_history, false );
1356  array_push( $last_tag_history, '' );
1357  array_push( $tr_history, false );
1358  array_push( $tr_attributes, '' );
1359  array_push( $has_opened_tr, false );
1360  } elseif ( count( $td_history ) == 0 ) {
1361  # Don't do any of the following
1362  $out .= $outLine . "\n";
1363  continue;
1364  } elseif ( $first_two === '|}' ) {
1365  # We are ending a table
1366  $line = '</table>' . substr( $line, 2 );
1367  $last_tag = array_pop( $last_tag_history );
1368 
1369  if ( !array_pop( $has_opened_tr ) ) {
1370  $line = "<tr><td></td></tr>{$line}";
1371  }
1372 
1373  if ( array_pop( $tr_history ) ) {
1374  $line = "</tr>{$line}";
1375  }
1376 
1377  if ( array_pop( $td_history ) ) {
1378  $line = "</{$last_tag}>{$line}";
1379  }
1380  array_pop( $tr_attributes );
1381  if ( $indent_level > 0 ) {
1382  $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1383  } else {
1384  $outLine = $line;
1385  }
1386  } elseif ( $first_two === '|-' ) {
1387  # Now we have a table row
1388  $line = preg_replace( '#^\|-+#', '', $line );
1389 
1390  # Whats after the tag is now only attributes
1391  $attributes = $this->mStripState->unstripBoth( $line );
1392  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1393  array_pop( $tr_attributes );
1394  array_push( $tr_attributes, $attributes );
1395 
1396  $line = '';
1397  $last_tag = array_pop( $last_tag_history );
1398  array_pop( $has_opened_tr );
1399  array_push( $has_opened_tr, true );
1400 
1401  if ( array_pop( $tr_history ) ) {
1402  $line = '</tr>';
1403  }
1404 
1405  if ( array_pop( $td_history ) ) {
1406  $line = "</{$last_tag}>{$line}";
1407  }
1408 
1409  $outLine = $line;
1410  array_push( $tr_history, false );
1411  array_push( $td_history, false );
1412  array_push( $last_tag_history, '' );
1413  } elseif ( $first_character === '|'
1414  || $first_character === '!'
1415  || $first_two === '|+'
1416  ) {
1417  # This might be cell elements, td, th or captions
1418  if ( $first_two === '|+' ) {
1419  $first_character = '+';
1420  $line = substr( $line, 2 );
1421  } else {
1422  $line = substr( $line, 1 );
1423  }
1424 
1425  // Implies both are valid for table headings.
1426  if ( $first_character === '!' ) {
1427  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1428  }
1429 
1430  # Split up multiple cells on the same line.
1431  # FIXME : This can result in improper nesting of tags processed
1432  # by earlier parser steps.
1433  $cells = explode( '||', $line );
1434 
1435  $outLine = '';
1436 
1437  # Loop through each table cell
1438  foreach ( $cells as $cell ) {
1439  $previous = '';
1440  if ( $first_character !== '+' ) {
1441  $tr_after = array_pop( $tr_attributes );
1442  if ( !array_pop( $tr_history ) ) {
1443  $previous = "<tr{$tr_after}>\n";
1444  }
1445  array_push( $tr_history, true );
1446  array_push( $tr_attributes, '' );
1447  array_pop( $has_opened_tr );
1448  array_push( $has_opened_tr, true );
1449  }
1450 
1451  $last_tag = array_pop( $last_tag_history );
1452 
1453  if ( array_pop( $td_history ) ) {
1454  $previous = "</{$last_tag}>\n{$previous}";
1455  }
1456 
1457  if ( $first_character === '|' ) {
1458  $last_tag = 'td';
1459  } elseif ( $first_character === '!' ) {
1460  $last_tag = 'th';
1461  } elseif ( $first_character === '+' ) {
1462  $last_tag = 'caption';
1463  } else {
1464  $last_tag = '';
1465  }
1466 
1467  array_push( $last_tag_history, $last_tag );
1468 
1469  # A cell could contain both parameters and data
1470  $cell_data = explode( '|', $cell, 2 );
1471 
1472  # T2553: Note that a '|' inside an invalid link should not
1473  # be mistaken as delimiting cell parameters
1474  # Bug T153140: Neither should language converter markup.
1475  if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1476  $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1477  } elseif ( count( $cell_data ) == 1 ) {
1478  // Whitespace in cells is trimmed
1479  $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1480  } else {
1481  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1482  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1483  // Whitespace in cells is trimmed
1484  $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1485  }
1486 
1487  $outLine .= $cell;
1488  array_push( $td_history, true );
1489  }
1490  }
1491  $out .= $outLine . "\n";
1492  }
1493 
1494  # Closing open td, tr && table
1495  while ( count( $td_history ) > 0 ) {
1496  if ( array_pop( $td_history ) ) {
1497  $out .= "</td>\n";
1498  }
1499  if ( array_pop( $tr_history ) ) {
1500  $out .= "</tr>\n";
1501  }
1502  if ( !array_pop( $has_opened_tr ) ) {
1503  $out .= "<tr><td></td></tr>\n";
1504  }
1505 
1506  $out .= "</table>\n";
1507  }
1508 
1509  # Remove trailing line-ending (b/c)
1510  if ( substr( $out, -1 ) === "\n" ) {
1511  $out = substr( $out, 0, -1 );
1512  }
1513 
1514  # special case: don't return empty table
1515  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1516  $out = '';
1517  }
1518 
1519  return $out;
1520  }
1521 
1535  public function internalParse( $text, $isMain = true, $frame = false ) {
1536  $origText = $text;
1537 
1538  # Hook to suspend the parser in this state
1539  if ( !$this->hookRunner->onParserBeforeInternalParse( $this, $text, $this->mStripState ) ) {
1540  return $text;
1541  }
1542 
1543  # if $frame is provided, then use $frame for replacing any variables
1544  if ( $frame ) {
1545  # use frame depth to infer how include/noinclude tags should be handled
1546  # depth=0 means this is the top-level document; otherwise it's an included document
1547  if ( !$frame->depth ) {
1548  $flag = 0;
1549  } else {
1550  $flag = self::PTD_FOR_INCLUSION;
1551  }
1552  $dom = $this->preprocessToDom( $text, $flag );
1553  $text = $frame->expand( $dom );
1554  } else {
1555  # if $frame is not provided, then use old-style replaceVariables
1556  $text = $this->replaceVariables( $text );
1557  }
1558 
1559  $this->hookRunner->onInternalParseBeforeSanitize( $this, $text, $this->mStripState );
1560  $text = Sanitizer::removeHTMLtags(
1561  $text,
1562  // Callback from the Sanitizer for expanding items found in
1563  // HTML attribute values, so they can be safely tested and escaped.
1564  function ( &$text, $frame = false ) {
1565  $text = $this->replaceVariables( $text, $frame );
1566  $text = $this->mStripState->unstripBoth( $text );
1567  },
1568  false,
1569  [],
1570  []
1571  );
1572  $this->hookRunner->onInternalParseBeforeLinks( $this, $text, $this->mStripState );
1573 
1574  # Tables need to come after variable replacement for things to work
1575  # properly; putting them before other transformations should keep
1576  # exciting things like link expansions from showing up in surprising
1577  # places.
1578  $text = $this->handleTables( $text );
1579 
1580  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1581 
1582  $text = $this->handleDoubleUnderscore( $text );
1583 
1584  $text = $this->handleHeadings( $text );
1585  $text = $this->handleInternalLinks( $text );
1586  $text = $this->handleAllQuotes( $text );
1587  $text = $this->handleExternalLinks( $text );
1588 
1589  # handleInternalLinks may sometimes leave behind
1590  # absolute URLs, which have to be masked to hide them from handleExternalLinks
1591  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1592 
1593  $text = $this->handleMagicLinks( $text );
1594  $text = $this->finalizeHeadings( $text, $origText, $isMain );
1595 
1596  return $text;
1597  }
1598 
1605  return $this->languageConverterFactory->getLanguageConverter(
1606  $this->getTargetLanguage()
1607  );
1608  }
1609 
1616  return $this->languageConverterFactory->getLanguageConverter(
1617  $this->getContentLanguage()
1618  );
1619  }
1620 
1628  protected function getHookContainer() {
1629  return $this->hookContainer;
1630  }
1631 
1640  protected function getHookRunner() {
1641  return $this->hookRunner;
1642  }
1643 
1653  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1654  $text = $this->mStripState->unstripGeneral( $text );
1655 
1656  $text = BlockLevelPass::doBlockLevels( $text, $linestart );
1657 
1658  $this->replaceLinkHoldersPrivate( $text );
1659 
1667  if ( !( $this->mOptions->getDisableContentConversion()
1668  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1669  && !$this->mOptions->getInterfaceMessage()
1670  ) {
1671  # The position of the convert() call should not be changed. it
1672  # assumes that the links are all replaced and the only thing left
1673  # is the <nowiki> mark.
1674  $text = $this->getTargetLanguageConverter()->convert( $text );
1675  }
1676 
1677  $text = $this->mStripState->unstripNoWiki( $text );
1678 
1679  if ( $isMain ) {
1680  $this->hookRunner->onParserBeforeTidy( $this, $text );
1681  }
1682 
1683  $text = $this->mStripState->unstripGeneral( $text );
1684 
1685  # Clean up special characters, only run once, after doBlockLevels
1686  $text = Sanitizer::armorFrenchSpaces( $text );
1687 
1688  $text = Sanitizer::normalizeCharReferences( $text );
1689 
1690  $text = MWTidy::tidy( $text );
1691 
1692  if ( $isMain ) {
1693  $this->hookRunner->onParserAfterTidy( $this, $text );
1694  }
1695 
1696  return $text;
1697  }
1698 
1709  private function handleMagicLinks( $text ) {
1710  $prots = wfUrlProtocolsWithoutProtRel();
1711  $urlChar = self::EXT_LINK_URL_CLASS;
1712  $addr = self::EXT_LINK_ADDR;
1713  $space = self::SPACE_NOT_NL; # non-newline space
1714  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1715  $spaces = "$space++"; # possessive match of 1 or more spaces
1716  $text = preg_replace_callback(
1717  '!(?: # Start cases
1718  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1719  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1720  (\b # m[3]: Free external links
1721  (?i:$prots)
1722  ($addr$urlChar*) # m[4]: Post-protocol path
1723  ) |
1724  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1725  ([0-9]+)\b |
1726  \bISBN $spaces ( # m[6]: ISBN, capture number
1727  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1728  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1729  [0-9Xx] # check digit
1730  )\b
1731  )!xu", [ $this, 'magicLinkCallback' ], $text );
1732  return $text;
1733  }
1734 
1740  private function magicLinkCallback( array $m ) {
1741  if ( isset( $m[1] ) && $m[1] !== '' ) {
1742  # Skip anchor
1743  return $m[0];
1744  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1745  # Skip HTML element
1746  return $m[0];
1747  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1748  # Free external link
1749  return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1750  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1751  # RFC or PMID
1752  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1753  if ( !$this->mOptions->getMagicRFCLinks() ) {
1754  return $m[0];
1755  }
1756  $keyword = 'RFC';
1757  $urlmsg = 'rfcurl';
1758  $cssClass = 'mw-magiclink-rfc';
1759  $trackingCat = 'magiclink-tracking-rfc';
1760  $id = $m[5];
1761  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1762  if ( !$this->mOptions->getMagicPMIDLinks() ) {
1763  return $m[0];
1764  }
1765  $keyword = 'PMID';
1766  $urlmsg = 'pubmedurl';
1767  $cssClass = 'mw-magiclink-pmid';
1768  $trackingCat = 'magiclink-tracking-pmid';
1769  $id = $m[5];
1770  } else {
1771  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1772  substr( $m[0], 0, 20 ) . '"' );
1773  }
1774  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1775  $this->addTrackingCategory( $trackingCat );
1776  return Linker::makeExternalLink(
1777  $url,
1778  "{$keyword} {$id}",
1779  true,
1780  $cssClass,
1781  [],
1782  $this->getTitle()
1783  );
1784  } elseif ( isset( $m[6] ) && $m[6] !== ''
1785  && $this->mOptions->getMagicISBNLinks()
1786  ) {
1787  # ISBN
1788  $isbn = $m[6];
1789  $space = self::SPACE_NOT_NL; # non-newline space
1790  $isbn = preg_replace( "/$space/", ' ', $isbn );
1791  $num = strtr( $isbn, [
1792  '-' => '',
1793  ' ' => '',
1794  'x' => 'X',
1795  ] );
1796  $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1797  return $this->getLinkRenderer()->makeKnownLink(
1798  SpecialPage::getTitleFor( 'Booksources', $num ),
1799  "ISBN $isbn",
1800  [
1801  'class' => 'internal mw-magiclink-isbn',
1802  'title' => false // suppress title attribute
1803  ]
1804  );
1805  } else {
1806  return $m[0];
1807  }
1808  }
1809 
1819  private function makeFreeExternalLink( $url, $numPostProto ) {
1820  $trail = '';
1821 
1822  # The characters '<' and '>' (which were escaped by
1823  # removeHTMLtags()) should not be included in
1824  # URLs, per RFC 2396.
1825  # Make &nbsp; terminate a URL as well (bug T84937)
1826  $m2 = [];
1827  if ( preg_match(
1828  '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1829  $url,
1830  $m2,
1831  PREG_OFFSET_CAPTURE
1832  ) ) {
1833  $trail = substr( $url, $m2[0][1] ) . $trail;
1834  $url = substr( $url, 0, $m2[0][1] );
1835  }
1836 
1837  # Move trailing punctuation to $trail
1838  $sep = ',;\.:!?';
1839  # If there is no left bracket, then consider right brackets fair game too
1840  if ( strpos( $url, '(' ) === false ) {
1841  $sep .= ')';
1842  }
1843 
1844  $urlRev = strrev( $url );
1845  $numSepChars = strspn( $urlRev, $sep );
1846  # Don't break a trailing HTML entity by moving the ; into $trail
1847  # This is in hot code, so use substr_compare to avoid having to
1848  # create a new string object for the comparison
1849  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1850  # more optimization: instead of running preg_match with a $
1851  # anchor, which can be slow, do the match on the reversed
1852  # string starting at the desired offset.
1853  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1854  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1855  $numSepChars--;
1856  }
1857  }
1858  if ( $numSepChars ) {
1859  $trail = substr( $url, -$numSepChars ) . $trail;
1860  $url = substr( $url, 0, -$numSepChars );
1861  }
1862 
1863  # Verify that we still have a real URL after trail removal, and
1864  # not just lone protocol
1865  if ( strlen( $trail ) >= $numPostProto ) {
1866  return $url . $trail;
1867  }
1868 
1869  $url = Sanitizer::cleanUrl( $url );
1870 
1871  # Is this an external image?
1872  $text = $this->maybeMakeExternalImage( $url );
1873  if ( $text === false ) {
1874  # Not an image, make a link
1875  $text = Linker::makeExternalLink( $url,
1876  $this->getTargetLanguageConverter()->markNoConversion( $url ),
1877  true, 'free',
1878  $this->getExternalLinkAttribs( $url ), $this->getTitle() );
1879  # Register it in the output object...
1880  $this->mOutput->addExternalLink( $url );
1881  }
1882  return $text . $trail;
1883  }
1884 
1891  private function handleHeadings( $text ) {
1892  for ( $i = 6; $i >= 1; --$i ) {
1893  $h = str_repeat( '=', $i );
1894  // Trim non-newline whitespace from headings
1895  // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1896  $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1897  }
1898  return $text;
1899  }
1900 
1908  private function handleAllQuotes( $text ) {
1909  $outtext = '';
1910  $lines = StringUtils::explode( "\n", $text );
1911  foreach ( $lines as $line ) {
1912  $outtext .= $this->doQuotes( $line ) . "\n";
1913  }
1914  $outtext = substr( $outtext, 0, -1 );
1915  return $outtext;
1916  }
1917 
1926  public function doQuotes( $text ) {
1927  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1928  $countarr = count( $arr );
1929  if ( $countarr == 1 ) {
1930  return $text;
1931  }
1932 
1933  // First, do some preliminary work. This may shift some apostrophes from
1934  // being mark-up to being text. It also counts the number of occurrences
1935  // of bold and italics mark-ups.
1936  $numbold = 0;
1937  $numitalics = 0;
1938  for ( $i = 1; $i < $countarr; $i += 2 ) {
1939  $thislen = strlen( $arr[$i] );
1940  // If there are ever four apostrophes, assume the first is supposed to
1941  // be text, and the remaining three constitute mark-up for bold text.
1942  // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1943  if ( $thislen == 4 ) {
1944  $arr[$i - 1] .= "'";
1945  $arr[$i] = "'''";
1946  $thislen = 3;
1947  } elseif ( $thislen > 5 ) {
1948  // If there are more than 5 apostrophes in a row, assume they're all
1949  // text except for the last 5.
1950  // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1951  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1952  $arr[$i] = "'''''";
1953  $thislen = 5;
1954  }
1955  // Count the number of occurrences of bold and italics mark-ups.
1956  if ( $thislen == 2 ) {
1957  $numitalics++;
1958  } elseif ( $thislen == 3 ) {
1959  $numbold++;
1960  } elseif ( $thislen == 5 ) {
1961  $numitalics++;
1962  $numbold++;
1963  }
1964  }
1965 
1966  // If there is an odd number of both bold and italics, it is likely
1967  // that one of the bold ones was meant to be an apostrophe followed
1968  // by italics. Which one we cannot know for certain, but it is more
1969  // likely to be one that has a single-letter word before it.
1970  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1971  $firstsingleletterword = -1;
1972  $firstmultiletterword = -1;
1973  $firstspace = -1;
1974  for ( $i = 1; $i < $countarr; $i += 2 ) {
1975  if ( strlen( $arr[$i] ) == 3 ) {
1976  $x1 = substr( $arr[$i - 1], -1 );
1977  $x2 = substr( $arr[$i - 1], -2, 1 );
1978  if ( $x1 === ' ' ) {
1979  if ( $firstspace == -1 ) {
1980  $firstspace = $i;
1981  }
1982  } elseif ( $x2 === ' ' ) {
1983  $firstsingleletterword = $i;
1984  // if $firstsingleletterword is set, we don't
1985  // look at the other options, so we can bail early.
1986  break;
1987  } elseif ( $firstmultiletterword == -1 ) {
1988  $firstmultiletterword = $i;
1989  }
1990  }
1991  }
1992 
1993  // If there is a single-letter word, use it!
1994  if ( $firstsingleletterword > -1 ) {
1995  $arr[$firstsingleletterword] = "''";
1996  $arr[$firstsingleletterword - 1] .= "'";
1997  } elseif ( $firstmultiletterword > -1 ) {
1998  // If not, but there's a multi-letter word, use that one.
1999  $arr[$firstmultiletterword] = "''";
2000  $arr[$firstmultiletterword - 1] .= "'";
2001  } elseif ( $firstspace > -1 ) {
2002  // ... otherwise use the first one that has neither.
2003  // (notice that it is possible for all three to be -1 if, for example,
2004  // there is only one pentuple-apostrophe in the line)
2005  $arr[$firstspace] = "''";
2006  $arr[$firstspace - 1] .= "'";
2007  }
2008  }
2009 
2010  // Now let's actually convert our apostrophic mush to HTML!
2011  $output = '';
2012  $buffer = '';
2013  $state = '';
2014  $i = 0;
2015  foreach ( $arr as $r ) {
2016  if ( ( $i % 2 ) == 0 ) {
2017  if ( $state === 'both' ) {
2018  $buffer .= $r;
2019  } else {
2020  $output .= $r;
2021  }
2022  } else {
2023  $thislen = strlen( $r );
2024  if ( $thislen == 2 ) {
2025  if ( $state === 'i' ) {
2026  $output .= '</i>';
2027  $state = '';
2028  } elseif ( $state === 'bi' ) {
2029  $output .= '</i>';
2030  $state = 'b';
2031  } elseif ( $state === 'ib' ) {
2032  $output .= '</b></i><b>';
2033  $state = 'b';
2034  } elseif ( $state === 'both' ) {
2035  $output .= '<b><i>' . $buffer . '</i>';
2036  $state = 'b';
2037  } else { // $state can be 'b' or ''
2038  $output .= '<i>';
2039  $state .= 'i';
2040  }
2041  } elseif ( $thislen == 3 ) {
2042  if ( $state === 'b' ) {
2043  $output .= '</b>';
2044  $state = '';
2045  } elseif ( $state === 'bi' ) {
2046  $output .= '</i></b><i>';
2047  $state = 'i';
2048  } elseif ( $state === 'ib' ) {
2049  $output .= '</b>';
2050  $state = 'i';
2051  } elseif ( $state === 'both' ) {
2052  $output .= '<i><b>' . $buffer . '</b>';
2053  $state = 'i';
2054  } else { // $state can be 'i' or ''
2055  $output .= '<b>';
2056  $state .= 'b';
2057  }
2058  } elseif ( $thislen == 5 ) {
2059  if ( $state === 'b' ) {
2060  $output .= '</b><i>';
2061  $state = 'i';
2062  } elseif ( $state === 'i' ) {
2063  $output .= '</i><b>';
2064  $state = 'b';
2065  } elseif ( $state === 'bi' ) {
2066  $output .= '</i></b>';
2067  $state = '';
2068  } elseif ( $state === 'ib' ) {
2069  $output .= '</b></i>';
2070  $state = '';
2071  } elseif ( $state === 'both' ) {
2072  $output .= '<i><b>' . $buffer . '</b></i>';
2073  $state = '';
2074  } else { // ($state == '')
2075  $buffer = '';
2076  $state = 'both';
2077  }
2078  }
2079  }
2080  $i++;
2081  }
2082  // Now close all remaining tags. Notice that the order is important.
2083  if ( $state === 'b' || $state === 'ib' ) {
2084  $output .= '</b>';
2085  }
2086  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2087  $output .= '</i>';
2088  }
2089  if ( $state === 'bi' ) {
2090  $output .= '</b>';
2091  }
2092  // There might be lonely ''''', so make sure we have a buffer
2093  if ( $state === 'both' && $buffer ) {
2094  $output .= '<b><i>' . $buffer . '</i></b>';
2095  }
2096  return $output;
2097  }
2098 
2109  private function handleExternalLinks( $text ) {
2110  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2111  // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2112  if ( $bits === false ) {
2113  throw new MWException( "PCRE needs to be compiled with "
2114  . "--enable-unicode-properties in order for MediaWiki to function" );
2115  }
2116  $s = array_shift( $bits );
2117 
2118  $i = 0;
2119  while ( $i < count( $bits ) ) {
2120  $url = $bits[$i++];
2121  $i++; // protocol
2122  $text = $bits[$i++];
2123  $trail = $bits[$i++];
2124 
2125  # The characters '<' and '>' (which were escaped by
2126  # removeHTMLtags()) should not be included in
2127  # URLs, per RFC 2396.
2128  $m2 = [];
2129  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2130  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2131  $url = substr( $url, 0, $m2[0][1] );
2132  }
2133 
2134  # If the link text is an image URL, replace it with an <img> tag
2135  # This happened by accident in the original parser, but some people used it extensively
2136  $img = $this->maybeMakeExternalImage( $text );
2137  if ( $img !== false ) {
2138  $text = $img;
2139  }
2140 
2141  $dtrail = '';
2142 
2143  # Set linktype for CSS
2144  $linktype = 'text';
2145 
2146  # No link text, e.g. [http://domain.tld/some.link]
2147  if ( $text == '' ) {
2148  # Autonumber
2149  $langObj = $this->getTargetLanguage();
2150  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2151  $linktype = 'autonumber';
2152  } else {
2153  # Have link text, e.g. [http://domain.tld/some.link text]s
2154  # Check for trail
2155  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2156  }
2157 
2158  // Excluding protocol-relative URLs may avoid many false positives.
2159  if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
2160  $text = $this->getTargetLanguageConverter()->markNoConversion( $text );
2161  }
2162 
2163  $url = Sanitizer::cleanUrl( $url );
2164 
2165  # Use the encoded URL
2166  # This means that users can paste URLs directly into the text
2167  # Funny characters like ö aren't valid in URLs anyway
2168  # This was changed in August 2004
2169  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2170  $this->getExternalLinkAttribs( $url ), $this->getTitle() ) . $dtrail . $trail;
2171 
2172  # Register link in the output object.
2173  $this->mOutput->addExternalLink( $url );
2174  }
2175 
2176  return $s;
2177  }
2178 
2189  public static function getExternalLinkRel( $url = false, LinkTarget $title = null ) {
2191  $ns = $title ? $title->getNamespace() : false;
2192  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2194  ) {
2195  return 'nofollow';
2196  }
2197  return null;
2198  }
2199 
2211  public function getExternalLinkAttribs( $url ) {
2212  $attribs = [];
2213  $rel = self::getExternalLinkRel( $url, $this->getTitle() );
2214 
2215  $target = $this->mOptions->getExternalLinkTarget();
2216  if ( $target ) {
2217  $attribs['target'] = $target;
2218  if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2219  // T133507. New windows can navigate parent cross-origin.
2220  // Including noreferrer due to lacking browser
2221  // support of noopener. Eventually noreferrer should be removed.
2222  if ( $rel !== '' ) {
2223  $rel .= ' ';
2224  }
2225  $rel .= 'noreferrer noopener';
2226  }
2227  }
2228  $attribs['rel'] = $rel;
2229  return $attribs;
2230  }
2231 
2242  public static function normalizeLinkUrl( $url ) {
2243  # Test for RFC 3986 IPv6 syntax
2244  $scheme = '[a-z][a-z0-9+.-]*:';
2245  $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2246  $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2247  if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2248  IPUtils::isValid( rawurldecode( $m[1] ) )
2249  ) {
2250  $isIPv6 = rawurldecode( $m[1] );
2251  } else {
2252  $isIPv6 = false;
2253  }
2254 
2255  # Make sure unsafe characters are encoded
2256  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2257  function ( $m ) {
2258  return rawurlencode( $m[0] );
2259  },
2260  $url
2261  );
2262 
2263  $ret = '';
2264  $end = strlen( $url );
2265 
2266  # Fragment part - 'fragment'
2267  $start = strpos( $url, '#' );
2268  if ( $start !== false && $start < $end ) {
2270  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2271  $end = $start;
2272  }
2273 
2274  # Query part - 'query' minus &=+;
2275  $start = strpos( $url, '?' );
2276  if ( $start !== false && $start < $end ) {
2278  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2279  $end = $start;
2280  }
2281 
2282  # Scheme and path part - 'pchar'
2283  # (we assume no userinfo or encoded colons in the host)
2285  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2286 
2287  # Fix IPv6 syntax
2288  if ( $isIPv6 !== false ) {
2289  $ipv6Host = "%5B({$isIPv6})%5D";
2290  $ret = preg_replace(
2291  "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2292  "$1[$2]",
2293  $ret
2294  );
2295  }
2296 
2297  return $ret;
2298  }
2299 
2300  private static function normalizeUrlComponent( $component, $unsafe ) {
2301  $callback = function ( $matches ) use ( $unsafe ) {
2302  $char = urldecode( $matches[0] );
2303  $ord = ord( $char );
2304  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2305  # Unescape it
2306  return $char;
2307  } else {
2308  # Leave it escaped, but use uppercase for a-f
2309  return strtoupper( $matches[0] );
2310  }
2311  };
2312  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2313  }
2314 
2323  private function maybeMakeExternalImage( $url ) {
2324  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2325  $imagesexception = !empty( $imagesfrom );
2326  $text = false;
2327  # $imagesfrom could be either a single string or an array of strings, parse out the latter
2328  if ( $imagesexception && is_array( $imagesfrom ) ) {
2329  $imagematch = false;
2330  foreach ( $imagesfrom as $match ) {
2331  if ( strpos( $url, $match ) === 0 ) {
2332  $imagematch = true;
2333  break;
2334  }
2335  }
2336  } elseif ( $imagesexception ) {
2337  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2338  } else {
2339  $imagematch = false;
2340  }
2341 
2342  if ( $this->mOptions->getAllowExternalImages()
2343  || ( $imagesexception && $imagematch )
2344  ) {
2345  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2346  # Image found
2347  $text = Linker::makeExternalImage( $url );
2348  }
2349  }
2350  if ( !$text && $this->mOptions->getEnableImageWhitelist()
2351  && preg_match( self::EXT_IMAGE_REGEX, $url )
2352  ) {
2353  $whitelist = explode(
2354  "\n",
2355  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2356  );
2357 
2358  foreach ( $whitelist as $entry ) {
2359  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2360  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2361  continue;
2362  }
2363  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2364  # Image matches a whitelist entry
2365  $text = Linker::makeExternalImage( $url );
2366  break;
2367  }
2368  }
2369  }
2370  return $text;
2371  }
2372 
2380  private function handleInternalLinks( $text ) {
2381  $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2382  return $text;
2383  }
2384 
2390  private function handleInternalLinks2( &$s ) {
2391  static $tc = false, $e1, $e1_img;
2392  # the % is needed to support urlencoded titles as well
2393  if ( !$tc ) {
2394  $tc = Title::legalChars() . '#%';
2395  # Match a link having the form [[namespace:link|alternate]]trail
2396  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2397  # Match cases where there is no "]]", which might still be images
2398  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2399  }
2400 
2401  $holders = new LinkHolderArray(
2402  $this,
2403  $this->getContentLanguageConverter(),
2404  $this->getHookContainer() );
2405 
2406  # split the entire text string on occurrences of [[
2407  $a = StringUtils::explode( '[[', ' ' . $s );
2408  # get the first element (all text up to first [[), and remove the space we added
2409  $s = $a->current();
2410  $a->next();
2411  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2412  $s = substr( $s, 1 );
2413 
2414  $nottalk = !$this->getTitle()->isTalkPage();
2415 
2416  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2417  $e2 = null;
2418  if ( $useLinkPrefixExtension ) {
2419  # Match the end of a line for a word that's not followed by whitespace,
2420  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2421  $charset = $this->contLang->linkPrefixCharset();
2422  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2423  $m = [];
2424  if ( preg_match( $e2, $s, $m ) ) {
2425  $first_prefix = $m[2];
2426  } else {
2427  $first_prefix = false;
2428  }
2429  } else {
2430  $prefix = '';
2431  }
2432 
2433  # Some namespaces don't allow subpages
2434  $useSubpages = $this->nsInfo->hasSubpages(
2435  $this->getTitle()->getNamespace()
2436  );
2437 
2438  # Loop for each link
2439  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2440  # Check for excessive memory usage
2441  if ( $holders->isBig() ) {
2442  # Too big
2443  # Do the existence check, replace the link holders and clear the array
2444  $holders->replace( $s );
2445  $holders->clear();
2446  }
2447 
2448  if ( $useLinkPrefixExtension ) {
2449  if ( preg_match( $e2, $s, $m ) ) {
2450  list( , $s, $prefix ) = $m;
2451  } else {
2452  $prefix = '';
2453  }
2454  # first link
2455  if ( $first_prefix ) {
2456  $prefix = $first_prefix;
2457  $first_prefix = false;
2458  }
2459  }
2460 
2461  $might_be_img = false;
2462 
2463  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2464  $text = $m[2];
2465  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2466  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2467  # the real problem is with the $e1 regex
2468  # See T1500.
2469  # Still some problems for cases where the ] is meant to be outside punctuation,
2470  # and no image is in sight. See T4095.
2471  if ( $text !== ''
2472  && substr( $m[3], 0, 1 ) === ']'
2473  && strpos( $text, '[' ) !== false
2474  ) {
2475  $text .= ']'; # so that handleExternalLinks($text) works later
2476  $m[3] = substr( $m[3], 1 );
2477  }
2478  # fix up urlencoded title texts
2479  if ( strpos( $m[1], '%' ) !== false ) {
2480  # Should anchors '#' also be rejected?
2481  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2482  }
2483  $trail = $m[3];
2484  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2485  # Invalid, but might be an image with a link in its caption
2486  $might_be_img = true;
2487  $text = $m[2];
2488  if ( strpos( $m[1], '%' ) !== false ) {
2489  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2490  }
2491  $trail = "";
2492  } else { # Invalid form; output directly
2493  $s .= $prefix . '[[' . $line;
2494  continue;
2495  }
2496 
2497  $origLink = ltrim( $m[1], ' ' );
2498 
2499  # Don't allow internal links to pages containing
2500  # PROTO: where PROTO is a valid URL protocol; these
2501  # should be external links.
2502  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2503  $s .= $prefix . '[[' . $line;
2504  continue;
2505  }
2506 
2507  # Make subpage if necessary
2508  if ( $useSubpages ) {
2510  $this->getTitle(), $origLink, $text
2511  );
2512  } else {
2513  $link = $origLink;
2514  }
2515 
2516  // \x7f isn't a default legal title char, so most likely strip
2517  // markers will force us into the "invalid form" path above. But,
2518  // just in case, let's assert that xmlish tags aren't valid in
2519  // the title position.
2520  $unstrip = $this->mStripState->killMarkers( $link );
2521  $noMarkers = ( $unstrip === $link );
2522 
2523  $nt = $noMarkers ? Title::newFromText( $link ) : null;
2524  if ( $nt === null ) {
2525  $s .= $prefix . '[[' . $line;
2526  continue;
2527  }
2528 
2529  $ns = $nt->getNamespace();
2530  $iw = $nt->getInterwiki();
2531 
2532  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2533 
2534  if ( $might_be_img ) { # if this is actually an invalid link
2535  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2536  $found = false;
2537  while ( true ) {
2538  # look at the next 'line' to see if we can close it there
2539  $a->next();
2540  $next_line = $a->current();
2541  if ( $next_line === false || $next_line === null ) {
2542  break;
2543  }
2544  $m = explode( ']]', $next_line, 3 );
2545  if ( count( $m ) == 3 ) {
2546  # the first ]] closes the inner link, the second the image
2547  $found = true;
2548  $text .= "[[{$m[0]}]]{$m[1]}";
2549  $trail = $m[2];
2550  break;
2551  } elseif ( count( $m ) == 2 ) {
2552  # if there's exactly one ]] that's fine, we'll keep looking
2553  $text .= "[[{$m[0]}]]{$m[1]}";
2554  } else {
2555  # if $next_line is invalid too, we need look no further
2556  $text .= '[[' . $next_line;
2557  break;
2558  }
2559  }
2560  if ( !$found ) {
2561  # we couldn't find the end of this imageLink, so output it raw
2562  # but don't ignore what might be perfectly normal links in the text we've examined
2563  $holders->merge( $this->handleInternalLinks2( $text ) );
2564  $s .= "{$prefix}[[$link|$text";
2565  # note: no $trail, because without an end, there *is* no trail
2566  continue;
2567  }
2568  } else { # it's not an image, so output it raw
2569  $s .= "{$prefix}[[$link|$text";
2570  # note: no $trail, because without an end, there *is* no trail
2571  continue;
2572  }
2573  }
2574 
2575  $wasblank = ( $text == '' );
2576  if ( $wasblank ) {
2577  $text = $link;
2578  if ( !$noforce ) {
2579  # Strip off leading ':'
2580  $text = substr( $text, 1 );
2581  }
2582  } else {
2583  # T6598 madness. Handle the quotes only if they come from the alternate part
2584  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2585  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2586  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2587  $text = $this->doQuotes( $text );
2588  }
2589 
2590  # Link not escaped by : , create the various objects
2591  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2592  # Interwikis
2593  if (
2594  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2595  MediaWikiServices::getInstance()->getLanguageNameUtils()
2596  ->getLanguageName(
2597  $iw,
2598  LanguageNameUtils::AUTONYMS,
2599  LanguageNameUtils::DEFINED
2600  )
2601  || in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes' ) )
2602  )
2603  ) {
2604  # T26502: filter duplicates
2605  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2606  $this->mLangLinkLanguages[$iw] = true;
2607  $this->mOutput->addLanguageLink( $nt->getFullText() );
2608  }
2609 
2613  $s = rtrim( $s . $prefix ) . $trail; # T175416
2614  continue;
2615  }
2616 
2617  if ( $ns == NS_FILE ) {
2618  if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->getTitle() ) ) {
2619  if ( $wasblank ) {
2620  # if no parameters were passed, $text
2621  # becomes something like "File:Foo.png",
2622  # which we don't want to pass on to the
2623  # image generator
2624  $text = '';
2625  } else {
2626  # recursively parse links inside the image caption
2627  # actually, this will parse them in any other parameters, too,
2628  # but it might be hard to fix that, and it doesn't matter ATM
2629  $text = $this->handleExternalLinks( $text );
2630  $holders->merge( $this->handleInternalLinks2( $text ) );
2631  }
2632  # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2633  $s .= $prefix . $this->armorLinks(
2634  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2635  continue;
2636  }
2637  } elseif ( $ns == NS_CATEGORY ) {
2641  $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2642 
2643  if ( $wasblank ) {
2644  $sortkey = $this->getDefaultSort();
2645  } else {
2646  $sortkey = $text;
2647  }
2648  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2649  $sortkey = str_replace( "\n", '', $sortkey );
2650  $sortkey = $this->getTargetLanguageConverter()->convertCategoryKey( $sortkey );
2651  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2652 
2653  continue;
2654  }
2655  }
2656 
2657  # Self-link checking. For some languages, variants of the title are checked in
2658  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2659  # for linking to a different variant.
2660  if ( $ns != NS_SPECIAL && $nt->equals( $this->getTitle() ) && !$nt->hasFragment() ) {
2661  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2662  continue;
2663  }
2664 
2665  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2666  # @todo FIXME: Should do batch file existence checks, see comment below
2667  if ( $ns == NS_MEDIA ) {
2668  # Give extensions a chance to select the file revision for us
2669  $options = [];
2670  $descQuery = false;
2671  $this->hookRunner->onBeforeParserFetchFileAndTitle(
2672  $this, $nt, $options, $descQuery );
2673  # Fetch and register the file (file title may be different via hooks)
2674  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2675  # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2676  $s .= $prefix . $this->armorLinks(
2677  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2678  continue;
2679  }
2680 
2681  # Some titles, such as valid special pages or files in foreign repos, should
2682  # be shown as bluelinks even though they're not included in the page table
2683  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2684  # batch file existence checks for NS_FILE and NS_MEDIA
2685  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2686  $this->mOutput->addLink( $nt );
2687  $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2688  } else {
2689  # Links will be added to the output link list after checking
2690  $s .= $holders->makeHolder( $nt, $text, $trail, $prefix );
2691  }
2692  }
2693  return $holders;
2694  }
2695 
2709  private function makeKnownLinkHolder( Title $nt, $text = '', $trail = '', $prefix = '' ) {
2710  list( $inside, $trail ) = Linker::splitTrail( $trail );
2711 
2712  if ( $text == '' ) {
2713  $text = htmlspecialchars( $nt->getPrefixedText() );
2714  }
2715 
2716  $link = $this->getLinkRenderer()->makeKnownLink(
2717  $nt, new HtmlArmor( "$prefix$text$inside" )
2718  );
2719 
2720  return $this->armorLinks( $link ) . $trail;
2721  }
2722 
2733  private function armorLinks( $text ) {
2734  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2735  self::MARKER_PREFIX . "NOPARSE$1", $text );
2736  }
2737 
2747  public function doBlockLevels( $text, $linestart ) {
2748  wfDeprecated( __METHOD__, '1.35' );
2749  return BlockLevelPass::doBlockLevels( $text, $linestart );
2750  }
2751 
2760  private function expandMagicVariable( $index, $frame = false ) {
2765  if (
2766  $this->hookRunner->onParserGetVariableValueVarCache( $this, $this->mVarCache ) &&
2767  isset( $this->mVarCache[$index] )
2768  ) {
2769  return $this->mVarCache[$index];
2770  }
2771 
2772  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2773  $this->hookRunner->onParserGetVariableValueTs( $this, $ts );
2774 
2775  $value = CoreMagicVariables::expand(
2776  $this, $index, $ts, $this->nsInfo, $this->svcOptions, $this->logger
2777  );
2778 
2779  if ( $value === null ) {
2780  // Not a defined core magic word
2781  $ret = null;
2782  $originalIndex = $index;
2783  $this->hookRunner->onParserGetVariableValueSwitch( $this,
2784  $this->mVarCache, $index, $ret, $frame );
2785  if ( $index !== $originalIndex ) {
2787  'A ParserGetVariableValueSwitch hook handler modified $index, ' .
2788  'this is deprecated since MediaWiki 1.35',
2789  '1.35', false, false
2790  );
2791  }
2792  if ( !isset( $this->mVarCache[$originalIndex] ) ||
2793  $this->mVarCache[$originalIndex] !== $ret ) {
2795  'A ParserGetVariableValueSwitch hook handler bypassed the cache, ' .
2796  'this is deprecated since MediaWiki 1.35', '1.35', false, false
2797  );
2798  }// FIXME: in the future, don't give this hook unrestricted
2799  // access to mVarCache; we can cache it ourselves by falling
2800  // through here.
2801  return $ret;
2802  }
2803 
2804  $this->mVarCache[$index] = $value;
2805 
2806  return $value;
2807  }
2808 
2813  private function initializeVariables() {
2814  $variableIDs = $this->magicWordFactory->getVariableIDs();
2815  $substIDs = $this->magicWordFactory->getSubstIDs();
2816 
2817  $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2818  $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2819  }
2820 
2843  public function preprocessToDom( $text, $flags = 0 ) {
2844  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2845  return $dom;
2846  }
2847 
2868  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2869  # Is there any text? Also, Prevent too big inclusions!
2870  $textSize = strlen( $text );
2871  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2872  return $text;
2873  }
2874 
2875  if ( $frame === false ) {
2876  $frame = $this->getPreprocessor()->newFrame();
2877  } elseif ( !( $frame instanceof PPFrame ) ) {
2878  $this->logger->debug(
2879  __METHOD__ . " called using plain parameters instead of " .
2880  "a PPFrame instance. Creating custom frame."
2881  );
2882  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2883  }
2884 
2885  $dom = $this->preprocessToDom( $text );
2886  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2887  $text = $frame->expand( $dom, $flags );
2888 
2889  return $text;
2890  }
2891 
2919  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
2920  # does no harm if $current and $max are present but are unnecessary for the message
2921  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
2922  # only during preview, and that would split the parser cache unnecessarily.
2923  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
2924  ->text();
2925  $this->mOutput->addWarning( $warning );
2926  $this->addTrackingCategory( "$limitationType-category" );
2927  }
2928 
2942  public function braceSubstitution( array $piece, PPFrame $frame ) {
2943  // Flags
2944 
2945  // $text has been filled
2946  $found = false;
2947  // wiki markup in $text should be escaped
2948  $nowiki = false;
2949  // $text is HTML, armour it against wikitext transformation
2950  $isHTML = false;
2951  // Force interwiki transclusion to be done in raw mode not rendered
2952  $forceRawInterwiki = false;
2953  // $text is a DOM node needing expansion in a child frame
2954  $isChildObj = false;
2955  // $text is a DOM node needing expansion in the current frame
2956  $isLocalObj = false;
2957 
2958  # Title object, where $text came from
2959  $title = false;
2960 
2961  # $part1 is the bit before the first |, and must contain only title characters.
2962  # Various prefixes will be stripped from it later.
2963  $titleWithSpaces = $frame->expand( $piece['title'] );
2964  $part1 = trim( $titleWithSpaces );
2965  $titleText = false;
2966 
2967  # Original title text preserved for various purposes
2968  $originalTitle = $part1;
2969 
2970  # $args is a list of argument nodes, starting from index 0, not including $part1
2971  # @todo FIXME: If piece['parts'] is null then the call to getLength()
2972  # below won't work b/c this $args isn't an object
2973  $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
2974 
2975  $profileSection = null; // profile templates
2976 
2977  # SUBST
2978  // @phan-suppress-next-line PhanRedundantCondition
2979  if ( !$found ) {
2980  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
2981 
2982  # Possibilities for substMatch: "subst", "safesubst" or FALSE
2983  # Decide whether to expand template or keep wikitext as-is.
2984  if ( $this->ot['wiki'] ) {
2985  if ( $substMatch === false ) {
2986  $literal = true; # literal when in PST with no prefix
2987  } else {
2988  $literal = false; # expand when in PST with subst: or safesubst:
2989  }
2990  } else {
2991  if ( $substMatch == 'subst' ) {
2992  $literal = true; # literal when not in PST with plain subst:
2993  } else {
2994  $literal = false; # expand when not in PST with safesubst: or no prefix
2995  }
2996  }
2997  if ( $literal ) {
2998  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
2999  $isLocalObj = true;
3000  $found = true;
3001  }
3002  }
3003 
3004  # Variables
3005  if ( !$found && $args->getLength() == 0 ) {
3006  $id = $this->mVariables->matchStartToEnd( $part1 );
3007  if ( $id !== false ) {
3008  $text = $this->expandMagicVariable( $id, $frame );
3009  if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3010  $this->mOutput->updateCacheExpiry(
3011  $this->magicWordFactory->getCacheTTL( $id ) );
3012  }
3013  $found = true;
3014  }
3015  }
3016 
3017  # MSG, MSGNW and RAW
3018  if ( !$found ) {
3019  # Check for MSGNW:
3020  $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3021  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3022  $nowiki = true;
3023  } else {
3024  # Remove obsolete MSG:
3025  $mwMsg = $this->magicWordFactory->get( 'msg' );
3026  $mwMsg->matchStartAndRemove( $part1 );
3027  }
3028 
3029  # Check for RAW:
3030  $mwRaw = $this->magicWordFactory->get( 'raw' );
3031  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3032  $forceRawInterwiki = true;
3033  }
3034  }
3035 
3036  # Parser functions
3037  if ( !$found ) {
3038  $colonPos = strpos( $part1, ':' );
3039  if ( $colonPos !== false ) {
3040  $func = substr( $part1, 0, $colonPos );
3041  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3042  $argsLength = $args->getLength();
3043  for ( $i = 0; $i < $argsLength; $i++ ) {
3044  $funcArgs[] = $args->item( $i );
3045  }
3046 
3047  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3048 
3049  // Extract any forwarded flags
3050  if ( isset( $result['title'] ) ) {
3051  $title = $result['title'];
3052  }
3053  if ( isset( $result['found'] ) ) {
3054  $found = $result['found'];
3055  }
3056  if ( array_key_exists( 'text', $result ) ) {
3057  // a string or null
3058  $text = $result['text'];
3059  }
3060  if ( isset( $result['nowiki'] ) ) {
3061  $nowiki = $result['nowiki'];
3062  }
3063  if ( isset( $result['isHTML'] ) ) {
3064  $isHTML = $result['isHTML'];
3065  }
3066  if ( isset( $result['forceRawInterwiki'] ) ) {
3067  $forceRawInterwiki = $result['forceRawInterwiki'];
3068  }
3069  if ( isset( $result['isChildObj'] ) ) {
3070  $isChildObj = $result['isChildObj'];
3071  }
3072  if ( isset( $result['isLocalObj'] ) ) {
3073  $isLocalObj = $result['isLocalObj'];
3074  }
3075  }
3076  }
3077 
3078  # Finish mangling title and then check for loops.
3079  # Set $title to a Title object and $titleText to the PDBK
3080  if ( !$found ) {
3081  $ns = NS_TEMPLATE;
3082  # Split the title into page and subpage
3083  $subpage = '';
3084  $relative = Linker::normalizeSubpageLink(
3085  $this->getTitle(), $part1, $subpage
3086  );
3087  if ( $part1 !== $relative ) {
3088  $part1 = $relative;
3089  $ns = $this->getTitle()->getNamespace();
3090  }
3091  $title = Title::newFromText( $part1, $ns );
3092  if ( $title ) {
3093  $titleText = $title->getPrefixedText();
3094  # Check for language variants if the template is not found
3095  if ( $this->getTargetLanguageConverter()->hasVariants() && $title->getArticleID() == 0 ) {
3096  $this->getTargetLanguageConverter()->findVariantLink( $part1, $title, true );
3097  }
3098  # Do recursion depth check
3099  $limit = $this->mOptions->getMaxTemplateDepth();
3100  if ( $frame->depth >= $limit ) {
3101  $found = true;
3102  $text = '<span class="error">'
3103  . wfMessage( 'parser-template-recursion-depth-warning' )
3104  ->numParams( $limit )->inContentLanguage()->text()
3105  . '</span>';
3106  }
3107  }
3108  }
3109 
3110  # Load from database
3111  if ( !$found && $title ) {
3112  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3113  if ( !$title->isExternal() ) {
3114  if ( $title->isSpecialPage()
3115  && $this->mOptions->getAllowSpecialInclusion()
3116  && $this->ot['html']
3117  ) {
3118  $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3119  // Pass the template arguments as URL parameters.
3120  // "uselang" will have no effect since the Language object
3121  // is forced to the one defined in ParserOptions.
3122  $pageArgs = [];
3123  $argsLength = $args->getLength();
3124  for ( $i = 0; $i < $argsLength; $i++ ) {
3125  $bits = $args->item( $i )->splitArg();
3126  if ( strval( $bits['index'] ) === '' ) {
3127  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3128  $value = trim( $frame->expand( $bits['value'] ) );
3129  $pageArgs[$name] = $value;
3130  }
3131  }
3132 
3133  // Create a new context to execute the special page
3134  $context = new RequestContext;
3135  $context->setTitle( $title );
3136  $context->setRequest( new FauxRequest( $pageArgs ) );
3137  if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3138  $context->setUser( $this->getUser() );
3139  } else {
3140  // If this page is cached, then we better not be per user.
3141  $context->setUser( User::newFromName( '127.0.0.1', false ) );
3142  }
3143  $context->setLanguage( $this->mOptions->getUserLangObj() );
3144  $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3145  if ( $ret ) {
3146  $text = $context->getOutput()->getHTML();
3147  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3148  $found = true;
3149  $isHTML = true;
3150  if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3151  $this->mOutput->updateRuntimeAdaptiveExpiry(
3152  $specialPage->maxIncludeCacheTime()
3153  );
3154  }
3155  }
3156  } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3157  $found = false; # access denied
3158  $this->logger->debug(
3159  __METHOD__ .
3160  ": template inclusion denied for " . $title->getPrefixedDBkey()
3161  );
3162  } else {
3163  list( $text, $title ) = $this->getTemplateDom( $title );
3164  if ( $text !== false ) {
3165  $found = true;
3166  $isChildObj = true;
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 
3237  # Replace raw HTML by a placeholder
3238  if ( $isHTML ) {
3239  $text = $this->insertStripItem( $text );
3240  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3241  # Escape nowiki-style return values
3242  $text = wfEscapeWikiText( $text );
3243  } elseif ( is_string( $text )
3244  && !$piece['lineStart']
3245  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3246  ) {
3247  # T2529: if the template begins with a table or block-level
3248  # element, it should be treated as beginning a new line.
3249  # This behavior is somewhat controversial.
3250  $text = "\n" . $text;
3251  }
3252 
3253  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3254  # Error, oversize inclusion
3255  if ( $titleText !== false ) {
3256  # Make a working, properly escaped link if possible (T25588)
3257  $text = "[[:$titleText]]";
3258  } else {
3259  # This will probably not be a working link, but at least it may
3260  # provide some hint of where the problem is
3261  preg_replace( '/^:/', '', $originalTitle );
3262  $text = "[[:$originalTitle]]";
3263  }
3264  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3265  . 'post-expand include size too large -->' );
3266  $this->limitationWarn( 'post-expand-template-inclusion' );
3267  }
3268 
3269  if ( $isLocalObj ) {
3270  $ret = [ 'object' => $text ];
3271  } else {
3272  $ret = [ 'text' => $text ];
3273  }
3274 
3275  return $ret;
3276  }
3277 
3296  public function callParserFunction( PPFrame $frame, $function, array $args = [] ) {
3297  # Case sensitive functions
3298  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3299  $function = $this->mFunctionSynonyms[1][$function];
3300  } else {
3301  # Case insensitive functions
3302  $function = $this->contLang->lc( $function );
3303  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3304  $function = $this->mFunctionSynonyms[0][$function];
3305  } else {
3306  return [ 'found' => false ];
3307  }
3308  }
3309 
3310  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3311 
3312  $allArgs = [ $this ];
3313  if ( $flags & self::SFH_OBJECT_ARGS ) {
3314  # Convert arguments to PPNodes and collect for appending to $allArgs
3315  $funcArgs = [];
3316  foreach ( $args as $k => $v ) {
3317  if ( $v instanceof PPNode || $k === 0 ) {
3318  $funcArgs[] = $v;
3319  } else {
3320  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3321  }
3322  }
3323 
3324  # Add a frame parameter, and pass the arguments as an array
3325  $allArgs[] = $frame;
3326  $allArgs[] = $funcArgs;
3327  } else {
3328  # Convert arguments to plain text and append to $allArgs
3329  foreach ( $args as $k => $v ) {
3330  if ( $v instanceof PPNode ) {
3331  $allArgs[] = trim( $frame->expand( $v ) );
3332  } elseif ( is_int( $k ) && $k >= 0 ) {
3333  $allArgs[] = trim( $v );
3334  } else {
3335  $allArgs[] = trim( "$k=$v" );
3336  }
3337  }
3338  }
3339 
3340  $result = $callback( ...$allArgs );
3341 
3342  # The interface for function hooks allows them to return a wikitext
3343  # string or an array containing the string and any flags. This mungs
3344  # things around to match what this method should return.
3345  if ( !is_array( $result ) ) {
3346  $result = [
3347  'found' => true,
3348  'text' => $result,
3349  ];
3350  } else {
3351  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3352  $result['text'] = $result[0];
3353  }
3354  unset( $result[0] );
3355  $result += [
3356  'found' => true,
3357  ];
3358  }
3359 
3360  $noparse = true;
3361  $preprocessFlags = 0;
3362  if ( isset( $result['noparse'] ) ) {
3363  $noparse = $result['noparse'];
3364  }
3365  if ( isset( $result['preprocessFlags'] ) ) {
3366  $preprocessFlags = $result['preprocessFlags'];
3367  }
3368 
3369  if ( !$noparse ) {
3370  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3371  $result['isChildObj'] = true;
3372  }
3373 
3374  return $result;
3375  }
3376 
3385  public function getTemplateDom( Title $title ) {
3386  $cacheTitle = $title;
3387  $titleText = $title->getPrefixedDBkey();
3388 
3389  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3390  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3391  $title = Title::makeTitle( $ns, $dbk );
3392  $titleText = $title->getPrefixedDBkey();
3393  }
3394  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3395  return [ $this->mTplDomCache[$titleText], $title ];
3396  }
3397 
3398  # Cache miss, go to the database
3399  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3400 
3401  if ( $text === false ) {
3402  $this->mTplDomCache[$titleText] = false;
3403  return [ false, $title ];
3404  }
3405 
3406  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3407  $this->mTplDomCache[$titleText] = $dom;
3408 
3409  if ( !$title->equals( $cacheTitle ) ) {
3410  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3411  [ $title->getNamespace(), $title->getDBkey() ];
3412  }
3413 
3414  return [ $dom, $title ];
3415  }
3416 
3432  wfDeprecated( __METHOD__, '1.35' );
3433  $revisionRecord = $this->fetchCurrentRevisionRecordOfTitle( $title );
3434  if ( $revisionRecord ) {
3435  return new Revision( $revisionRecord );
3436  }
3437  return $revisionRecord;
3438  }
3439 
3454  $cacheKey = $title->getPrefixedDBkey();
3455  if ( !$this->currentRevisionCache ) {
3456  $this->currentRevisionCache = new MapCacheLRU( 100 );
3457  }
3458  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3459  $revisionRecord =
3460  // Defaults to Parser::statelessFetchRevisionRecord()
3461  call_user_func(
3462  $this->mOptions->getCurrentRevisionRecordCallback(),
3463  $title,
3464  $this
3465  );
3466  if ( !$revisionRecord ) {
3467  // Parser::statelessFetchRevisionRecord() can return false;
3468  // normalize it to null.
3469  $revisionRecord = null;
3470  }
3471  $this->currentRevisionCache->set( $cacheKey, $revisionRecord );
3472  }
3473  return $this->currentRevisionCache->get( $cacheKey );
3474  }
3475 
3483  return (
3484  $this->currentRevisionCache &&
3485  $this->currentRevisionCache->has( $title->getPrefixedText() )
3486  );
3487  }
3488 
3499  public static function statelessFetchRevision( Title $title, $parser = false ) {
3500  wfDeprecated( __METHOD__, '1.35' );
3501  $revRecord = MediaWikiServices::getInstance()
3502  ->getRevisionLookup()
3503  ->getKnownCurrentRevision( $title );
3504  return $revRecord ? new Revision( $revRecord ) : false;
3505  }
3506 
3516  public static function statelessFetchRevisionRecord( Title $title, $parser = null ) {
3517  $revRecord = MediaWikiServices::getInstance()
3518  ->getRevisionLookup()
3519  ->getKnownCurrentRevision( $title );
3520  return $revRecord;
3521  }
3522 
3528  public function fetchTemplateAndTitle( Title $title ) {
3529  // Defaults to Parser::statelessFetchTemplate()
3530  $templateCb = $this->mOptions->getTemplateCallback();
3531  $stuff = call_user_func( $templateCb, $title, $this );
3532  if ( isset( $stuff['revision-record'] ) ) {
3533  $revRecord = $stuff['revision-record'];
3534  } else {
3535  // Triggers deprecation warnings via DeprecatablePropertyArray
3536  $rev = $stuff['revision'] ?? null;
3537  if ( $rev instanceof Revision ) {
3538  $revRecord = $rev->getRevisionRecord();
3539  } else {
3540  $revRecord = null;
3541  }
3542  }
3543 
3544  $text = $stuff['text'];
3545  if ( is_string( $stuff['text'] ) ) {
3546  // We use U+007F DELETE to distinguish strip markers from regular text
3547  $text = strtr( $text, "\x7f", "?" );
3548  }
3549  $finalTitle = $stuff['finalTitle'] ?? $title;
3550  foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3551  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3552  if ( $dep['title']->equals( $this->getTitle() ) && $revRecord instanceof RevisionRecord ) {
3553  // Self-transclusion; final result may change based on the new page version
3554  try {
3555  $sha1 = $revRecord->getSha1();
3556  } catch ( RevisionAccessException $e ) {
3557  $sha1 = null;
3558  }
3559  $this->setOutputFlag( 'vary-revision-sha1', 'Self transclusion' );
3560  $this->getOutput()->setRevisionUsedSha1Base36( $sha1 );
3561  }
3562  }
3563 
3564  return [ $text, $finalTitle ];
3565  }
3566 
3573  public function fetchTemplate( Title $title ) {
3574  wfDeprecated( __METHOD__, '1.35' );
3575  return $this->fetchTemplateAndTitle( $title )[0];
3576  }
3577 
3587  public static function statelessFetchTemplate( $title, $parser = false ) {
3588  $text = $skip = false;
3589  $finalTitle = $title;
3590  $deps = [];
3591  $revRecord = null;
3592 
3593  # Loop to fetch the article, with up to 1 redirect
3594  $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
3595  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3596  # Give extensions a chance to select the revision instead
3597  $id = false; # Assume current
3598  Hooks::runner()->onBeforeParserFetchTemplateAndtitle(
3599  $parser, $title, $skip, $id );
3600 
3601  if ( $skip ) {
3602  $text = false;
3603  $deps[] = [
3604  'title' => $title,
3605  'page_id' => $title->getArticleID(),
3606  'rev_id' => null
3607  ];
3608  break;
3609  }
3610  # Get the revision
3611  # TODO rewrite using only RevisionRecord objects
3612  if ( $id ) {
3613  $revRecord = $revLookup->getRevisionById( $id );
3614  } elseif ( $parser ) {
3615  $revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
3616  } else {
3617  $revRecord = $revLookup->getRevisionByTitle( $title );
3618  }
3619  $rev_id = $revRecord ? $revRecord->getId() : 0;
3620  # If there is no current revision, there is no page
3621  if ( $id === false && !$revRecord ) {
3622  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3623  $linkCache->addBadLinkObj( $title );
3624  }
3625 
3626  $deps[] = [
3627  'title' => $title,
3628  'page_id' => $title->getArticleID(),
3629  'rev_id' => $rev_id
3630  ];
3631  if ( $revRecord ) {
3632  $revTitle = Title::newFromLinkTarget(
3633  $revRecord->getPageAsLinkTarget()
3634  );
3635  if ( !$title->equals( $revTitle ) ) {
3636  # We fetched a rev from a different title; register it too...
3637  $deps[] = [
3638  'title' => $revTitle,
3639  'page_id' => $revRecord->getPageId(),
3640  'rev_id' => $rev_id
3641  ];
3642  }
3643  }
3644 
3645  if ( $revRecord ) {
3646  $content = $revRecord->getContent( SlotRecord::MAIN );
3647  $text = $content ? $content->getWikitextForTransclusion() : null;
3648 
3649  // Hook is hard deprecated since 1.35
3650  if ( Hooks::isRegistered( 'ParserFetchTemplate' ) ) {
3651  // Only create the Revision object if needed
3652  $legacyRevision = new Revision( $revRecord );
3653  Hooks::runner()->onParserFetchTemplate(
3654  $parser,
3655  $title,
3656  $legacyRevision,
3657  $text,
3658  $deps
3659  );
3660  }
3661 
3662  if ( $text === false || $text === null ) {
3663  $text = false;
3664  break;
3665  }
3666  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3667  $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3668  lcfirst( $title->getText() ) )->inContentLanguage();
3669  if ( !$message->exists() ) {
3670  $text = false;
3671  break;
3672  }
3673  $content = $message->content();
3674  $text = $message->plain();
3675  } else {
3676  break;
3677  }
3678  if ( !$content ) {
3679  break;
3680  }
3681  # Redirect?
3682  $finalTitle = $title;
3683  $title = $content->getRedirectTarget();
3684  }
3685 
3686  $legacyRevision = function () use ( $revRecord ) {
3687  return $revRecord ? new Revision( $revRecord ) : null;
3688  };
3689  $retValues = [
3690  'revision' => $legacyRevision,
3691  'revision-record' => $revRecord ?: false, // So isset works
3692  'text' => $text,
3693  'finalTitle' => $finalTitle,
3694  'deps' => $deps
3695  ];
3696  $propertyArray = new DeprecatablePropertyArray(
3697  $retValues,
3698  [ 'revision' => '1.35' ],
3699  __METHOD__
3700  );
3701  return $propertyArray;
3702  }
3703 
3711  public function fetchFileAndTitle( Title $title, array $options = [] ) {
3712  $file = $this->fetchFileNoRegister( $title, $options );
3713 
3714  $time = $file ? $file->getTimestamp() : false;
3715  $sha1 = $file ? $file->getSha1() : false;
3716  # Register the file as a dependency...
3717  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3718  if ( $file && !$title->equals( $file->getTitle() ) ) {
3719  # Update fetched file title
3720  $title = $file->getTitle();
3721  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3722  }
3723  return [ $file, $title ];
3724  }
3725 
3736  protected function fetchFileNoRegister( Title $title, array $options = [] ) {
3737  if ( isset( $options['broken'] ) ) {
3738  $file = false; // broken thumbnail forced by hook
3739  } else {
3740  $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
3741  if ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3742  $file = $repoGroup->findFileFromKey( $options['sha1'], $options );
3743  } else { // get by (name,timestamp)
3744  $file = $repoGroup->findFile( $title, $options );
3745  }
3746  }
3747  return $file;
3748  }
3749 
3759  public function interwikiTransclude( Title $title, $action ) {
3760  if ( !$this->svcOptions->get( 'EnableScaryTranscluding' ) ) {
3761  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3762  }
3763 
3764  $url = $title->getFullURL( [ 'action' => $action ] );
3765  if ( strlen( $url ) > 1024 ) {
3766  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3767  }
3768 
3769  $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3770 
3771  $fname = __METHOD__;
3772  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3773 
3774  $data = $cache->getWithSetCallback(
3775  $cache->makeGlobalKey(
3776  'interwiki-transclude',
3777  ( $wikiId !== false ) ? $wikiId : 'external',
3778  sha1( $url )
3779  ),
3780  $this->svcOptions->get( 'TranscludeCacheExpiry' ),
3781  function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3782  $req = MWHttpRequest::factory( $url, [], $fname );
3783 
3784  $status = $req->execute(); // Status object
3785  if ( !$status->isOK() ) {
3786  $ttl = $cache::TTL_UNCACHEABLE;
3787  } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3788  $ttl = min( $cache::TTL_LAGGED, $ttl );
3789  }
3790 
3791  return [
3792  'text' => $status->isOK() ? $req->getContent() : null,
3793  'code' => $req->getStatus()
3794  ];
3795  },
3796  [
3797  'checkKeys' => ( $wikiId !== false )
3798  ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3799  : [],
3800  'pcGroup' => 'interwiki-transclude:5',
3801  'pcTTL' => $cache::TTL_PROC_LONG
3802  ]
3803  );
3804 
3805  if ( is_string( $data['text'] ) ) {
3806  $text = $data['text'];
3807  } elseif ( $data['code'] != 200 ) {
3808  // Though we failed to fetch the content, this status is useless.
3809  $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3810  ->params( $url, $data['code'] )->inContentLanguage()->text();
3811  } else {
3812  $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3813  }
3814 
3815  return $text;
3816  }
3817 
3828  public function argSubstitution( array $piece, PPFrame $frame ) {
3829  $error = false;
3830  $parts = $piece['parts'];
3831  $nameWithSpaces = $frame->expand( $piece['title'] );
3832  $argName = trim( $nameWithSpaces );
3833  $object = false;
3834  $text = $frame->getArgument( $argName );
3835  if ( $text === false && $parts->getLength() > 0
3836  && ( $this->ot['html']
3837  || $this->ot['pre']
3838  || ( $this->ot['wiki'] && $frame->isTemplate() )
3839  )
3840  ) {
3841  # No match in frame, use the supplied default
3842  $object = $parts->item( 0 )->getChildren();
3843  }
3844  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3845  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3846  $this->limitationWarn( 'post-expand-template-argument' );
3847  }
3848 
3849  if ( $text === false && $object === false ) {
3850  # No match anywhere
3851  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3852  }
3853  if ( $error !== false ) {
3854  $text .= $error;
3855  }
3856  if ( $object !== false ) {
3857  $ret = [ 'object' => $object ];
3858  } else {
3859  $ret = [ 'text' => $text ];
3860  }
3861 
3862  return $ret;
3863  }
3864 
3881  public function extensionSubstitution( array $params, PPFrame $frame ) {
3882  static $errorStr = '<span class="error">';
3883  static $errorLen = 20;
3884 
3885  $name = $frame->expand( $params['name'] );
3886  if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3887  // Probably expansion depth or node count exceeded. Just punt the
3888  // error up.
3889  return $name;
3890  }
3891 
3892  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3893  if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3894  // See above
3895  return $attrText;
3896  }
3897 
3898  // We can't safely check if the expansion for $content resulted in an
3899  // error, because the content could happen to be the error string
3900  // (T149622).
3901  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3902 
3903  $marker = self::MARKER_PREFIX . "-$name-"
3904  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3905 
3906  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3907  ( $this->ot['html'] || $this->ot['pre'] );
3908  if ( $isFunctionTag ) {
3909  $markerType = 'none';
3910  } else {
3911  $markerType = 'general';
3912  }
3913  if ( $this->ot['html'] || $isFunctionTag ) {
3914  $name = strtolower( $name );
3915  $attributes = Sanitizer::decodeTagAttributes( $attrText );
3916  if ( isset( $params['attributes'] ) ) {
3917  $attributes += $params['attributes'];
3918  }
3919 
3920  if ( isset( $this->mTagHooks[$name] ) ) {
3921  $output = call_user_func_array( $this->mTagHooks[$name],
3922  [ $content, $attributes, $this, $frame ] );
3923  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3924  list( $callback, ) = $this->mFunctionTagHooks[$name];
3925 
3926  $output = $callback( $this, $frame, $content, $attributes );
3927  } else {
3928  $output = '<span class="error">Invalid tag extension name: ' .
3929  htmlspecialchars( $name ) . '</span>';
3930  }
3931 
3932  if ( is_array( $output ) ) {
3933  // Extract flags
3934  $flags = $output;
3935  $output = $flags[0];
3936  if ( isset( $flags['markerType'] ) ) {
3937  $markerType = $flags['markerType'];
3938  }
3939  }
3940  } else {
3941  if ( $attrText === null ) {
3942  $attrText = '';
3943  }
3944  if ( isset( $params['attributes'] ) ) {
3945  foreach ( $params['attributes'] as $attrName => $attrValue ) {
3946  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3947  htmlspecialchars( $attrValue ) . '"';
3948  }
3949  }
3950  if ( $content === null ) {
3951  $output = "<$name$attrText/>";
3952  } else {
3953  $close = $params['close'] === null ? '' : $frame->expand( $params['close'] );
3954  if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3955  // See above
3956  return $close;
3957  }
3958  $output = "<$name$attrText>$content$close";
3959  }
3960  }
3961 
3962  if ( $markerType === 'none' ) {
3963  return $output;
3964  } elseif ( $markerType === 'nowiki' ) {
3965  $this->mStripState->addNoWiki( $marker, $output );
3966  } elseif ( $markerType === 'general' ) {
3967  $this->mStripState->addGeneral( $marker, $output );
3968  } else {
3969  throw new MWException( __METHOD__ . ': invalid marker type' );
3970  }
3971  return $marker;
3972  }
3973 
3981  private function incrementIncludeSize( $type, $size ) {
3982  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3983  return false;
3984  } else {
3985  $this->mIncludeSizes[$type] += $size;
3986  return true;
3987  }
3988  }
3989 
3996  $this->mExpensiveFunctionCount++;
3997  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3998  }
3999 
4007  private function handleDoubleUnderscore( $text ) {
4008  # The position of __TOC__ needs to be recorded
4009  $mw = $this->magicWordFactory->get( 'toc' );
4010  if ( $mw->match( $text ) ) {
4011  $this->mShowToc = true;
4012  $this->mForceTocPosition = true;
4013 
4014  # Set a placeholder. At the end we'll fill it in with the TOC.
4015  $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4016 
4017  # Only keep the first one.
4018  $text = $mw->replace( '', $text );
4019  }
4020 
4021  # Now match and remove the rest of them
4022  $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4023  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4024 
4025  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4026  $this->mOutput->mNoGallery = true;
4027  }
4028  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4029  $this->mShowToc = false;
4030  }
4031  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4032  && $this->getTitle()->getNamespace() == NS_CATEGORY
4033  ) {
4034  $this->addTrackingCategory( 'hidden-category-category' );
4035  }
4036  # (T10068) Allow control over whether robots index a page.
4037  # __INDEX__ always overrides __NOINDEX__, see T16899
4038  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->getTitle()->canUseNoindex() ) {
4039  $this->mOutput->setIndexPolicy( 'noindex' );
4040  $this->addTrackingCategory( 'noindex-category' );
4041  }
4042  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->getTitle()->canUseNoindex() ) {
4043  $this->mOutput->setIndexPolicy( 'index' );
4044  $this->addTrackingCategory( 'index-category' );
4045  }
4046 
4047  # Cache all double underscores in the database
4048  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4049  $this->mOutput->setProperty( $key, '' );
4050  }
4051 
4052  return $text;
4053  }
4054 
4060  public function addTrackingCategory( $msg ) {
4061  return $this->mOutput->addTrackingCategory( $msg, $this->getTitle() );
4062  }
4063 
4079  private function finalizeHeadings( $text, $origText, $isMain = true ) {
4080  # Inhibit editsection links if requested in the page
4081  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4082  $maybeShowEditLink = false;
4083  } else {
4084  $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4085  }
4086 
4087  # Get all headlines for numbering them and adding funky stuff like [edit]
4088  # links - this is for later, but we need the number of headlines right now
4089  # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4090  # be trimmed here since whitespace in HTML headings is significant.
4091  $matches = [];
4092  $numMatches = preg_match_all(
4093  '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4094  $text,
4095  $matches
4096  );
4097 
4098  # if there are fewer than 4 headlines in the article, do not show TOC
4099  # unless it's been explicitly enabled.
4100  $enoughToc = $this->mShowToc &&
4101  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4102 
4103  # Allow user to stipulate that a page should have a "new section"
4104  # link added via __NEWSECTIONLINK__
4105  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4106  $this->mOutput->setNewSection( true );
4107  }
4108 
4109  # Allow user to remove the "new section"
4110  # link via __NONEWSECTIONLINK__
4111  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4112  $this->mOutput->hideNewSection( true );
4113  }
4114 
4115  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4116  # override above conditions and always show TOC above first header
4117  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4118  $this->mShowToc = true;
4119  $enoughToc = true;
4120  }
4121 
4122  # headline counter
4123  $headlineCount = 0;
4124  $numVisible = 0;
4125 
4126  # Ugh .. the TOC should have neat indentation levels which can be
4127  # passed to the skin functions. These are determined here
4128  $toc = '';
4129  $full = '';
4130  $head = [];
4131  $sublevelCount = [];
4132  $levelCount = [];
4133  $level = 0;
4134  $prevlevel = 0;
4135  $toclevel = 0;
4136  $prevtoclevel = 0;
4137  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4138  $baseTitleText = $this->getTitle()->getPrefixedDBkey();
4139  $oldType = $this->mOutputType;
4140  $this->setOutputType( self::OT_WIKI );
4141  $frame = $this->getPreprocessor()->newFrame();
4142  $root = $this->preprocessToDom( $origText );
4143  $node = $root->getFirstChild();
4144  $byteOffset = 0;
4145  $tocraw = [];
4146  $refers = [];
4147 
4148  $headlines = $numMatches !== false ? $matches[3] : [];
4149 
4150  $maxTocLevel = $this->svcOptions->get( 'MaxTocLevel' );
4151  foreach ( $headlines as $headline ) {
4152  $isTemplate = false;
4153  $titleText = false;
4154  $sectionIndex = false;
4155  $numbering = '';
4156  $markerMatches = [];
4157  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4158  $serial = $markerMatches[1];
4159  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4160  $isTemplate = ( $titleText != $baseTitleText );
4161  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4162  }
4163 
4164  if ( $toclevel ) {
4165  $prevlevel = $level;
4166  }
4167  $level = $matches[1][$headlineCount];
4168 
4169  if ( $level > $prevlevel ) {
4170  # Increase TOC level
4171  $toclevel++;
4172  $sublevelCount[$toclevel] = 0;
4173  if ( $toclevel < $maxTocLevel ) {
4174  $prevtoclevel = $toclevel;
4175  $toc .= Linker::tocIndent();
4176  $numVisible++;
4177  }
4178  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4179  # Decrease TOC level, find level to jump to
4180 
4181  for ( $i = $toclevel; $i > 0; $i-- ) {
4182  // @phan-suppress-next-line PhanTypeInvalidDimOffset
4183  if ( $levelCount[$i] == $level ) {
4184  # Found last matching level
4185  $toclevel = $i;
4186  break;
4187  } elseif ( $levelCount[$i] < $level ) {
4188  // @phan-suppress-previous-line PhanTypeInvalidDimOffset
4189  # Found first matching level below current level
4190  $toclevel = $i + 1;
4191  break;
4192  }
4193  }
4194  if ( $i == 0 ) {
4195  $toclevel = 1;
4196  }
4197  if ( $toclevel < $maxTocLevel ) {
4198  if ( $prevtoclevel < $maxTocLevel ) {
4199  # Unindent only if the previous toc level was shown :p
4200  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4201  $prevtoclevel = $toclevel;
4202  } else {
4203  $toc .= Linker::tocLineEnd();
4204  }
4205  }
4206  } else {
4207  # No change in level, end TOC line
4208  if ( $toclevel < $maxTocLevel ) {
4209  $toc .= Linker::tocLineEnd();
4210  }
4211  }
4212 
4213  $levelCount[$toclevel] = $level;
4214 
4215  # count number of headlines for each level
4216  $sublevelCount[$toclevel]++;
4217  $dot = 0;
4218  for ( $i = 1; $i <= $toclevel; $i++ ) {
4219  if ( !empty( $sublevelCount[$i] ) ) {
4220  if ( $dot ) {
4221  $numbering .= '.';
4222  }
4223  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4224  $dot = 1;
4225  }
4226  }
4227 
4228  # The safe header is a version of the header text safe to use for links
4229 
4230  # Remove link placeholders by the link text.
4231  # <!--LINK number-->
4232  # turns into
4233  # link text with suffix
4234  # Do this before unstrip since link text can contain strip markers
4235  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4236 
4237  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4238  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4239 
4240  # Remove any <style> or <script> tags (T198618)
4241  $safeHeadline = preg_replace(
4242  '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4243  '',
4244  $safeHeadline
4245  );
4246 
4247  # Strip out HTML (first regex removes any tag not allowed)
4248  # Allowed tags are:
4249  # * <sup> and <sub> (T10393)
4250  # * <i> (T28375)
4251  # * <b> (r105284)
4252  # * <bdi> (T74884)
4253  # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4254  # * <s> and <strike> (T35715)
4255  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4256  # to allow setting directionality in toc items.
4257  $tocline = preg_replace(
4258  [
4259  '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4260  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4261  ],
4262  [ '', '<$1>' ],
4263  $safeHeadline
4264  );
4265 
4266  # Strip '<span></span>', which is the result from the above if
4267  # <span id="foo"></span> is used to produce an additional anchor
4268  # for a section.
4269  $tocline = str_replace( '<span></span>', '', $tocline );
4270 
4271  $tocline = trim( $tocline );
4272 
4273  # For the anchor, strip out HTML-y stuff period
4274  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4275  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4276 
4277  # Save headline for section edit hint before it's escaped
4278  $headlineHint = $safeHeadline;
4279 
4280  # Decode HTML entities
4281  $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4282 
4283  $safeHeadline = self::normalizeSectionName( $safeHeadline );
4284 
4285  $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4286  $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4287  $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4288  if ( $fallbackHeadline === $safeHeadline ) {
4289  # No reason to have both (in fact, we can't)
4290  $fallbackHeadline = false;
4291  }
4292 
4293  # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4294  # @todo FIXME: We may be changing them depending on the current locale.
4295  $arrayKey = strtolower( $safeHeadline );
4296  if ( $fallbackHeadline === false ) {
4297  $fallbackArrayKey = false;
4298  } else {
4299  $fallbackArrayKey = strtolower( $fallbackHeadline );
4300  }
4301 
4302  # Create the anchor for linking from the TOC to the section
4303  $anchor = $safeHeadline;
4304  $fallbackAnchor = $fallbackHeadline;
4305  if ( isset( $refers[$arrayKey] ) ) {
4306  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4307  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4308  $anchor .= "_$i";
4309  $linkAnchor .= "_$i";
4310  $refers["${arrayKey}_$i"] = true;
4311  } else {
4312  $refers[$arrayKey] = true;
4313  }
4314  if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4315  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4316  for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4317  $fallbackAnchor .= "_$i";
4318  $refers["${fallbackArrayKey}_$i"] = true;
4319  } else {
4320  $refers[$fallbackArrayKey] = true;
4321  }
4322 
4323  # Don't number the heading if it is the only one (looks silly)
4324  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4325  # the two are different if the line contains a link
4326  $headline = Html::element(
4327  'span',
4328  [ 'class' => 'mw-headline-number' ],
4329  $numbering
4330  ) . ' ' . $headline;
4331  }
4332 
4333  if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4334  $toc .= Linker::tocLine( $linkAnchor, $tocline,
4335  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4336  }
4337 
4338  # Add the section to the section tree
4339  # Find the DOM node for this header
4340  $noOffset = ( $isTemplate || $sectionIndex === false );
4341  while ( $node && !$noOffset ) {
4342  if ( $node->getName() === 'h' ) {
4343  $bits = $node->splitHeading();
4344  if ( $bits['i'] == $sectionIndex ) {
4345  break;
4346  }
4347  }
4348  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4349  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4350  $node = $node->getNextSibling();
4351  }
4352  $tocraw[] = [
4353  'toclevel' => $toclevel,
4354  'level' => $level,
4355  'line' => $tocline,
4356  'number' => $numbering,
4357  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4358  'fromtitle' => $titleText,
4359  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4360  'anchor' => $anchor,
4361  ];
4362 
4363  # give headline the correct <h#> tag
4364  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4365  // Output edit section links as markers with styles that can be customized by skins
4366  if ( $isTemplate ) {
4367  # Put a T flag in the section identifier, to indicate to extractSections()
4368  # that sections inside <includeonly> should be counted.
4369  $editsectionPage = $titleText;
4370  $editsectionSection = "T-$sectionIndex";
4371  $editsectionContent = null;
4372  } else {
4373  $editsectionPage = $this->getTitle()->getPrefixedText();
4374  $editsectionSection = $sectionIndex;
4375  $editsectionContent = $headlineHint;
4376  }
4377  // We use a bit of pesudo-xml for editsection markers. The
4378  // language converter is run later on. Using a UNIQ style marker
4379  // leads to the converter screwing up the tokens when it
4380  // converts stuff. And trying to insert strip tags fails too. At
4381  // this point all real inputted tags have already been escaped,
4382  // so we don't have to worry about a user trying to input one of
4383  // these markers directly. We use a page and section attribute
4384  // to stop the language converter from converting these
4385  // important bits of data, but put the headline hint inside a
4386  // content block because the language converter is supposed to
4387  // be able to convert that piece of data.
4388  // Gets replaced with html in ParserOutput::getText
4389  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4390  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4391  if ( $editsectionContent !== null ) {
4392  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4393  } else {
4394  $editlink .= '/>';
4395  }
4396  } else {
4397  $editlink = '';
4398  }
4399  $head[$headlineCount] = Linker::makeHeadline( $level,
4400  $matches['attrib'][$headlineCount], $anchor, $headline,
4401  $editlink, $fallbackAnchor );
4402 
4403  $headlineCount++;
4404  }
4405 
4406  $this->setOutputType( $oldType );
4407 
4408  # Never ever show TOC if no headers
4409  if ( $numVisible < 1 ) {
4410  $enoughToc = false;
4411  }
4412 
4413  if ( $enoughToc ) {
4414  if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4415  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4416  }
4417  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4418  $this->mOutput->setTOCHTML( $toc );
4419  $toc = self::TOC_START . $toc . self::TOC_END;
4420  }
4421 
4422  if ( $isMain ) {
4423  $this->mOutput->setSections( $tocraw );
4424  }
4425 
4426  # split up and insert constructed headlines
4427  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4428  $i = 0;
4429 
4430  // build an array of document sections
4431  $sections = [];
4432  foreach ( $blocks as $block ) {
4433  // $head is zero-based, sections aren't.
4434  if ( empty( $head[$i - 1] ) ) {
4435  $sections[$i] = $block;
4436  } else {
4437  $sections[$i] = $head[$i - 1] . $block;
4438  }
4439 
4450  $this->hookRunner->onParserSectionCreate( $this, $i, $sections[$i], $maybeShowEditLink );
4451 
4452  $i++;
4453  }
4454 
4455  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4456  // append the TOC at the beginning
4457  // Top anchor now in skin
4458  $sections[0] .= $toc . "\n";
4459  }
4460 
4461  $full .= implode( '', $sections );
4462 
4463  if ( $this->mForceTocPosition ) {
4464  return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4465  } else {
4466  return $full;
4467  }
4468  }
4469 
4481  public function preSaveTransform( $text, Title $title, User $user,
4482  ParserOptions $options, $clearState = true
4483  ) {
4484  if ( $clearState ) {
4485  $magicScopeVariable = $this->lock();
4486  }
4487  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4488  $this->setUser( $user );
4489 
4490  // Strip U+0000 NULL (T159174)
4491  $text = str_replace( "\000", '', $text );
4492 
4493  // We still normalize line endings for backwards-compatibility
4494  // with other code that just calls PST, but this should already
4495  // be handled in TextContent subclasses
4496  $text = TextContent::normalizeLineEndings( $text );
4497 
4498  if ( $options->getPreSaveTransform() ) {
4499  $text = $this->pstPass2( $text, $user );
4500  }
4501  $text = $this->mStripState->unstripBoth( $text );
4502 
4503  $this->hookRunner->onParserPreSaveTransformComplete( $this, $text );
4504 
4505  $this->setUser( null ); # Reset
4506 
4507  return $text;
4508  }
4509 
4518  private function pstPass2( $text, User $user ) {
4519  # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4520  # $this->contLang here in order to give everyone the same signature and use the default one
4521  # rather than the one selected in each user's preferences. (see also T14815)
4522  $ts = $this->mOptions->getTimestamp();
4523  $timestamp = MWTimestamp::getLocalInstance( $ts );
4524  $ts = $timestamp->format( 'YmdHis' );
4525  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4526 
4527  $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4528 
4529  # Variable replacement
4530  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4531  $text = $this->replaceVariables( $text );
4532 
4533  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4534  # which may corrupt this parser instance via its wfMessage()->text() call-
4535 
4536  # Signatures
4537  if ( strpos( $text, '~~~' ) !== false ) {
4538  $sigText = $this->getUserSig( $user );
4539  $text = strtr( $text, [
4540  '~~~~~' => $d,
4541  '~~~~' => "$sigText $d",
4542  '~~~' => $sigText
4543  ] );
4544  # The main two signature forms used above are time-sensitive
4545  $this->setOutputFlag( 'user-signature', 'User signature detected' );
4546  }
4547 
4548  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4549  $tc = '[' . Title::legalChars() . ']';
4550  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4551 
4552  // [[ns:page (context)|]]
4553  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4554  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4555  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4556  // [[ns:page (context), context|]] (using either single or double-width comma)
4557  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4558  // [[|page]] (reverse pipe trick: add context from page title)
4559  $p2 = "/\[\[\\|($tc+)]]/";
4560 
4561  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4562  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4563  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4564  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4565 
4566  $t = $this->getTitle()->getText();
4567  $m = [];
4568  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4569  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4570  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4571  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4572  } else {
4573  # if there's no context, don't bother duplicating the title
4574  $text = preg_replace( $p2, '[[\\1]]', $text );
4575  }
4576 
4577  return $text;
4578  }
4579 
4594  public function getUserSig( User $user, $nickname = false, $fancySig = null ) {
4595  $username = $user->getName();
4596 
4597  # If not given, retrieve from the user object.
4598  if ( $nickname === false ) {
4599  $nickname = $user->getOption( 'nickname' );
4600  }
4601 
4602  if ( $fancySig === null ) {
4603  $fancySig = $user->getBoolOption( 'fancysig' );
4604  }
4605 
4606  if ( $nickname === null || $nickname === '' ) {
4607  $nickname = $username;
4608  } elseif ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) {
4609  $nickname = $username;
4610  $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
4611  } elseif ( $fancySig !== false ) {
4612  # Sig. might contain markup; validate this
4613  $isValid = $this->validateSig( $nickname ) !== false;
4614 
4615  # New validator
4616  $sigValidation = $this->svcOptions->get( 'SignatureValidation' );
4617  if ( $isValid && $sigValidation === 'disallow' ) {
4618  $validator = new SignatureValidator(
4619  $user,
4620  null,
4621  $this->mOptions
4622  );
4623  $isValid = !$validator->validateSignature( $nickname );
4624  }
4625 
4626  if ( $isValid ) {
4627  # Validated; clean up (if needed) and return it
4628  return $this->cleanSig( $nickname, true );
4629  } else {
4630  # Failed to validate; fall back to the default
4631  $nickname = $username;
4632  $this->logger->debug( __METHOD__ . ": $username has invalid signature." );
4633  }
4634  }
4635 
4636  # Make sure nickname doesnt get a sig in a sig
4637  $nickname = self::cleanSigInSig( $nickname );
4638 
4639  # If we're still here, make it a link to the user page
4640  $userText = wfEscapeWikiText( $username );
4641  $nickText = wfEscapeWikiText( $nickname );
4642  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4643 
4644  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4645  ->title( $this->getTitle() )->text();
4646  }
4647 
4654  public function validateSig( $text ) {
4655  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4656  }
4657 
4668  public function cleanSig( $text, $parsing = false ) {
4669  if ( !$parsing ) {
4670  global $wgTitle;
4671  $magicScopeVariable = $this->lock();
4672  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4673  }
4674 
4675  # Option to disable this feature
4676  if ( !$this->mOptions->getCleanSignatures() ) {
4677  return $text;
4678  }
4679 
4680  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4681  # => Move this logic to braceSubstitution()
4682  $substWord = $this->magicWordFactory->get( 'subst' );
4683  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4684  $substText = '{{' . $substWord->getSynonym( 0 );
4685 
4686  $text = preg_replace( $substRegex, $substText, $text );
4687  $text = self::cleanSigInSig( $text );
4688  $dom = $this->preprocessToDom( $text );
4689  $frame = $this->getPreprocessor()->newFrame();
4690  $text = $frame->expand( $dom );
4691 
4692  if ( !$parsing ) {
4693  $text = $this->mStripState->unstripBoth( $text );
4694  }
4695 
4696  return $text;
4697  }
4698 
4705  public static function cleanSigInSig( $text ) {
4706  $text = preg_replace( '/~{3,5}/', '', $text );
4707  return $text;
4708  }
4709 
4720  public function startExternalParse( ?Title $title, ParserOptions $options,
4721  $outputType, $clearState = true, $revId = null
4722  ) {
4723  $this->startParse( $title, $options, $outputType, $clearState );
4724  if ( $revId !== null ) {
4725  $this->mRevisionId = $revId;
4726  }
4727  }
4728 
4735  private function startParse( ?Title $title, ParserOptions $options,
4736  $outputType, $clearState = true
4737  ) {
4738  $this->setTitle( $title );
4739  $this->mOptions = $options;
4740  $this->setOutputType( $outputType );
4741  if ( $clearState ) {
4742  $this->clearState();
4743  }
4744  }
4745 
4754  public function transformMsg( $text, ParserOptions $options, Title $title = null ) {
4755  static $executing = false;
4756 
4757  # Guard against infinite recursion
4758  if ( $executing ) {
4759  return $text;
4760  }
4761  $executing = true;
4762 
4763  if ( !$title ) {
4764  global $wgTitle;
4765  $title = $wgTitle;
4766  }
4767 
4768  $text = $this->preprocess( $text, $title, $options );
4769 
4770  $executing = false;
4771  return $text;
4772  }
4773 
4798  public function setHook( $tag, callable $callback ) {
4799  $tag = strtolower( $tag );
4800  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4801  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4802  }
4803  $oldVal = $this->mTagHooks[$tag] ?? null;
4804  $this->mTagHooks[$tag] = $callback;
4805  if ( !in_array( $tag, $this->mStripList ) ) {
4806  $this->mStripList[] = $tag;
4807  }
4808 
4809  return $oldVal;
4810  }
4811 
4815  public function clearTagHooks() {
4816  $this->mTagHooks = [];
4817  $this->mFunctionTagHooks = [];
4818  $this->mStripList = [];
4819  }
4820 
4864  public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4865  $oldVal = $this->mFunctionHooks[$id][0] ?? null;
4866  $this->mFunctionHooks[$id] = [ $callback, $flags ];
4867 
4868  # Add to function cache
4869  $mw = $this->magicWordFactory->get( $id );
4870  if ( !$mw ) {
4871  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4872  }
4873 
4874  $synonyms = $mw->getSynonyms();
4875  $sensitive = intval( $mw->isCaseSensitive() );
4876 
4877  foreach ( $synonyms as $syn ) {
4878  # Case
4879  if ( !$sensitive ) {
4880  $syn = $this->contLang->lc( $syn );
4881  }
4882  # Add leading hash
4883  if ( !( $flags & self::SFH_NO_HASH ) ) {
4884  $syn = '#' . $syn;
4885  }
4886  # Remove trailing colon
4887  if ( substr( $syn, -1, 1 ) === ':' ) {
4888  $syn = substr( $syn, 0, -1 );
4889  }
4890  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4891  }
4892  return $oldVal;
4893  }
4894 
4900  public function getFunctionHooks() {
4901  $this->firstCallInit();
4902  return array_keys( $this->mFunctionHooks );
4903  }
4904 
4916  public function setFunctionTagHook( $tag, callable $callback, $flags ) {
4917  wfDeprecated( __METHOD__, '1.35' );
4918  $tag = strtolower( $tag );
4919  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4920  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4921  }
4922  $old = $this->mFunctionTagHooks[$tag] ?? null;
4923  $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4924 
4925  if ( !in_array( $tag, $this->mStripList ) ) {
4926  $this->mStripList[] = $tag;
4927  }
4928 
4929  return $old;
4930  }
4931 
4940  public function replaceLinkHolders( &$text, $options = 0 ) {
4941  $this->replaceLinkHoldersPrivate( $text, $options );
4942  }
4943 
4951  private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
4952  $this->mLinkHolders->replace( $text );
4953  }
4954 
4962  private function replaceLinkHoldersText( $text ) {
4963  return $this->mLinkHolders->replaceText( $text );
4964  }
4965 
4980  public function renderImageGallery( $text, array $params ) {
4981  $mode = false;
4982  if ( isset( $params['mode'] ) ) {
4983  $mode = $params['mode'];
4984  }
4985 
4986  try {
4987  $ig = ImageGalleryBase::factory( $mode );
4988  } catch ( Exception $e ) {
4989  // If invalid type set, fallback to default.
4990  $ig = ImageGalleryBase::factory( false );
4991  }
4992 
4993  $ig->setContextTitle( $this->getTitle() );
4994  $ig->setShowBytes( false );
4995  $ig->setShowDimensions( false );
4996  $ig->setShowFilename( false );
4997  $ig->setParser( $this );
4998  $ig->setHideBadImages();
4999  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5000 
5001  if ( isset( $params['showfilename'] ) ) {
5002  $ig->setShowFilename( true );
5003  } else {
5004  $ig->setShowFilename( false );
5005  }
5006  if ( isset( $params['caption'] ) ) {
5007  // NOTE: We aren't passing a frame here or below. Frame info
5008  // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5009  // See T107332#4030581
5010  $caption = $this->recursiveTagParse( $params['caption'] );
5011  $ig->setCaptionHtml( $caption );
5012  }
5013  if ( isset( $params['perrow'] ) ) {
5014  $ig->setPerRow( $params['perrow'] );
5015  }
5016  if ( isset( $params['widths'] ) ) {
5017  $ig->setWidths( $params['widths'] );
5018  }
5019  if ( isset( $params['heights'] ) ) {
5020  $ig->setHeights( $params['heights'] );
5021  }
5022  $ig->setAdditionalOptions( $params );
5023 
5024  $this->hookRunner->onBeforeParserrenderImageGallery( $this, $ig );
5025 
5026  $lines = StringUtils::explode( "\n", $text );
5027  foreach ( $lines as $line ) {
5028  # match lines like these:
5029  # Image:someimage.jpg|This is some image
5030  $matches = [];
5031  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5032  # Skip empty lines
5033  if ( count( $matches ) == 0 ) {
5034  continue;
5035  }
5036 
5037  if ( strpos( $matches[0], '%' ) !== false ) {
5038  $matches[1] = rawurldecode( $matches[1] );
5039  }
5041  if ( $title === null ) {
5042  # Bogus title. Ignore these so we don't bomb out later.
5043  continue;
5044  }
5045 
5046  # We need to get what handler the file uses, to figure out parameters.
5047  # Note, a hook can overide the file name, and chose an entirely different
5048  # file (which potentially could be of a different type and have different handler).
5049  $options = [];
5050  $descQuery = false;
5051  $this->hookRunner->onBeforeParserFetchFileAndTitle(
5052  $this, $title, $options, $descQuery );
5053  # Don't register it now, as TraditionalImageGallery does that later.
5054  $file = $this->fetchFileNoRegister( $title, $options );
5055  $handler = $file ? $file->getHandler() : false;
5056 
5057  $paramMap = [
5058  'img_alt' => 'gallery-internal-alt',
5059  'img_link' => 'gallery-internal-link',
5060  ];
5061  if ( $handler ) {
5062  $paramMap += $handler->getParamMap();
5063  // We don't want people to specify per-image widths.
5064  // Additionally the width parameter would need special casing anyhow.
5065  unset( $paramMap['img_width'] );
5066  }
5067 
5068  $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5069 
5070  $label = '';
5071  $alt = '';
5072  $link = '';
5073  $handlerOptions = [];
5074  if ( isset( $matches[3] ) ) {
5075  // look for an |alt= definition while trying not to break existing
5076  // captions with multiple pipes (|) in it, until a more sensible grammar
5077  // is defined for images in galleries
5078 
5079  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5080  // splitting on '|' is a bit odd, and different from makeImage.
5081  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5082  // Protect LanguageConverter markup
5083  $parameterMatches = StringUtils::delimiterExplode(
5084  '-{', '}-', '|', $matches[3], true /* nested */
5085  );
5086 
5087  foreach ( $parameterMatches as $parameterMatch ) {
5088  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5089  if ( $magicName ) {
5090  $paramName = $paramMap[$magicName];
5091 
5092  switch ( $paramName ) {
5093  case 'gallery-internal-alt':
5094  $alt = $this->stripAltText( $match, false );
5095  break;
5096  case 'gallery-internal-link':
5097  $linkValue = $this->stripAltText( $match, false );
5098  if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5099  // Result of LanguageConverter::markNoConversion
5100  // invoked on an external link.
5101  $linkValue = substr( $linkValue, 4, -2 );
5102  }
5103  list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5104  if ( $type === 'link-url' ) {
5105  $link = $target;
5106  $this->mOutput->addExternalLink( $target );
5107  } elseif ( $type === 'link-title' ) {
5108  $link = $target->getLinkURL();
5109  $this->mOutput->addLink( $target );
5110  }
5111  break;
5112  default:
5113  // Must be a handler specific parameter.
5114  if ( $handler->validateParam( $paramName, $match ) ) {
5115  $handlerOptions[$paramName] = $match;
5116  } else {
5117  // Guess not, consider it as caption.
5118  $this->logger->debug(
5119  "$parameterMatch failed parameter validation" );
5120  $label = $parameterMatch;
5121  }
5122  }
5123 
5124  } else {
5125  // Last pipe wins.
5126  $label = $parameterMatch;
5127  }
5128  }
5129  }
5130 
5131  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5132  }
5133  $html = $ig->toHTML();
5134  $this->hookRunner->onAfterParserFetchFileAndTitle( $this, $ig, $html );
5135  return $html;
5136  }
5137 
5142  private function getImageParams( $handler ) {
5143  if ( $handler ) {
5144  $handlerClass = get_class( $handler );
5145  } else {
5146  $handlerClass = '';
5147  }
5148  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5149  # Initialise static lists
5150  static $internalParamNames = [
5151  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5152  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5153  'bottom', 'text-bottom' ],
5154  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5155  'upright', 'border', 'link', 'alt', 'class' ],
5156  ];
5157  static $internalParamMap;
5158  if ( !$internalParamMap ) {
5159  $internalParamMap = [];
5160  foreach ( $internalParamNames as $type => $names ) {
5161  foreach ( $names as $name ) {
5162  // For grep: img_left, img_right, img_center, img_none,
5163  // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5164  // img_bottom, img_text_bottom,
5165  // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5166  // img_border, img_link, img_alt, img_class
5167  $magicName = str_replace( '-', '_', "img_$name" );
5168  $internalParamMap[$magicName] = [ $type, $name ];
5169  }
5170  }
5171  }
5172 
5173  # Add handler params
5174  $paramMap = $internalParamMap;
5175  if ( $handler ) {
5176  $handlerParamMap = $handler->getParamMap();
5177  foreach ( $handlerParamMap as $magic => $paramName ) {
5178  $paramMap[$magic] = [ 'handler', $paramName ];
5179  }
5180  }
5181  $this->mImageParams[$handlerClass] = $paramMap;
5182  $this->mImageParamsMagicArray[$handlerClass] =
5183  $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5184  }
5185  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5186  }
5187 
5196  public function makeImage( Title $title, $options, $holders = false ) {
5197  # Check if the options text is of the form "options|alt text"
5198  # Options are:
5199  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5200  # * left no resizing, just left align. label is used for alt= only
5201  # * right same, but right aligned
5202  # * none same, but not aligned
5203  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5204  # * center center the image
5205  # * frame Keep original image size, no magnify-button.
5206  # * framed Same as "frame"
5207  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5208  # * upright reduce width for upright images, rounded to full __0 px
5209  # * border draw a 1px border around the image
5210  # * alt Text for HTML alt attribute (defaults to empty)
5211  # * class Set a class for img node
5212  # * link Set the target of the image link. Can be external, interwiki, or local
5213  # vertical-align values (no % or length right now):
5214  # * baseline
5215  # * sub
5216  # * super
5217  # * top
5218  # * text-top
5219  # * middle
5220  # * bottom
5221  # * text-bottom
5222 
5223  # Protect LanguageConverter markup when splitting into parts
5225  '-{', '}-', '|', $options, true /* allow nesting */
5226  );
5227 
5228  # Give extensions a chance to select the file revision for us
5229  $options = [];
5230  $descQuery = false;
5231  $this->hookRunner->onBeforeParserFetchFileAndTitle(
5232  $this, $title, $options, $descQuery );
5233  # Fetch and register the file (file title may be different via hooks)
5234  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5235 
5236  # Get parameter map
5237  $handler = $file ? $file->getHandler() : false;
5238 
5239  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5240 
5241  if ( !$file ) {
5242  $this->addTrackingCategory( 'broken-file-category' );
5243  }
5244 
5245  # Process the input parameters
5246  $caption = '';
5247  $params = [ 'frame' => [], 'handler' => [],
5248  'horizAlign' => [], 'vertAlign' => [] ];
5249  $seenformat = false;
5250  foreach ( $parts as $part ) {
5251  $part = trim( $part );
5252  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5253  $validated = false;
5254  if ( isset( $paramMap[$magicName] ) ) {
5255  list( $type, $paramName ) = $paramMap[$magicName];
5256 
5257  # Special case; width and height come in one variable together
5258  if ( $type === 'handler' && $paramName === 'width' ) {
5259  $parsedWidthParam = self::parseWidthParam( $value );
5260  if ( isset( $parsedWidthParam['width'] ) ) {
5261  $width = $parsedWidthParam['width'];
5262  if ( $handler->validateParam( 'width', $width ) ) {
5263  $params[$type]['width'] = $width;
5264  $validated = true;
5265  }
5266  }
5267  if ( isset( $parsedWidthParam['height'] ) ) {
5268  $height = $parsedWidthParam['height'];
5269  if ( $handler->validateParam( 'height', $height ) ) {
5270  $params[$type]['height'] = $height;
5271  $validated = true;
5272  }
5273  }
5274  # else no validation -- T15436
5275  } else {
5276  if ( $type === 'handler' ) {
5277  # Validate handler parameter
5278  $validated = $handler->validateParam( $paramName, $value );
5279  } else {
5280  # Validate internal parameters
5281  switch ( $paramName ) {
5282  case 'manualthumb':
5283  case 'alt':
5284  case 'class':
5285  # @todo FIXME: Possibly check validity here for
5286  # manualthumb? downstream behavior seems odd with
5287  # missing manual thumbs.
5288  $validated = true;
5289  $value = $this->stripAltText( $value, $holders );
5290  break;
5291  case 'link':
5292  list( $paramName, $value ) =
5293  $this->parseLinkParameter(
5294  $this->stripAltText( $value, $holders )
5295  );
5296  if ( $paramName ) {
5297  $validated = true;
5298  if ( $paramName === 'no-link' ) {
5299  $value = true;
5300  }
5301  if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5302  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5303  }
5304  }
5305  break;
5306  case 'frameless':
5307  case 'framed':
5308  case 'thumbnail':
5309  // use first appearing option, discard others.
5310  $validated = !$seenformat;
5311  $seenformat = true;
5312  break;
5313  default:
5314  # Most other things appear to be empty or numeric...
5315  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5316  }
5317  }
5318 
5319  if ( $validated ) {
5320  $params[$type][$paramName] = $value;
5321  }
5322  }
5323  }
5324  if ( !$validated ) {
5325  $caption = $part;
5326  }
5327  }
5328 
5329  # Process alignment parameters
5330  // @phan-suppress-next-line PhanImpossibleCondition
5331  if ( $params['horizAlign'] ) {
5332  $params['frame']['align'] = key( $params['horizAlign'] );
5333  }
5334  // @phan-suppress-next-line PhanImpossibleCondition
5335  if ( $params['vertAlign'] ) {
5336  $params['frame']['valign'] = key( $params['vertAlign'] );
5337  }
5338 
5339  $params['frame']['caption'] = $caption;
5340 
5341  # Will the image be presented in a frame, with the caption below?
5342  $imageIsFramed = isset( $params['frame']['frame'] )
5343  || isset( $params['frame']['framed'] )
5344  || isset( $params['frame']['thumbnail'] )
5345  || isset( $params['frame']['manualthumb'] );
5346 
5347  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5348  # came to also set the caption, ordinary text after the image -- which
5349  # makes no sense, because that just repeats the text multiple times in
5350  # screen readers. It *also* came to set the title attribute.
5351  # Now that we have an alt attribute, we should not set the alt text to
5352  # equal the caption: that's worse than useless, it just repeats the
5353  # text. This is the framed/thumbnail case. If there's no caption, we
5354  # use the unnamed parameter for alt text as well, just for the time be-
5355  # ing, if the unnamed param is set and the alt param is not.
5356  # For the future, we need to figure out if we want to tweak this more,
5357  # e.g., introducing a title= parameter for the title; ignoring the un-
5358  # named parameter entirely for images without a caption; adding an ex-
5359  # plicit caption= parameter and preserving the old magic unnamed para-
5360  # meter for BC; ...
5361  if ( $imageIsFramed ) { # Framed image
5362  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5363  # No caption or alt text, add the filename as the alt text so
5364  # that screen readers at least get some description of the image
5365  $params['frame']['alt'] = $title->getText();
5366  }
5367  # Do not set $params['frame']['title'] because tooltips don't make sense
5368  # for framed images
5369  } else { # Inline image
5370  if ( !isset( $params['frame']['alt'] ) ) {
5371  # No alt text, use the "caption" for the alt text
5372  if ( $caption !== '' ) {
5373  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5374  } else {
5375  # No caption, fall back to using the filename for the
5376  # alt text
5377  $params['frame']['alt'] = $title->getText();
5378  }
5379  }
5380  # Use the "caption" for the tooltip text
5381  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5382  }
5383  $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5384 
5385  $this->hookRunner->onParserMakeImageParams( $title, $file, $params, $this );
5386 
5387  # Linker does the rest
5388  $time = $options['time'] ?? false;
5389  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5390  $time, $descQuery, $this->mOptions->getThumbSize() );
5391 
5392  # Give the handler a chance to modify the parser object
5393  if ( $handler ) {
5394  $handler->parserTransformHook( $this, $file );
5395  }
5396 
5397  return $ret;
5398  }
5399 
5418  private function parseLinkParameter( $value ) {
5419  $chars = self::EXT_LINK_URL_CLASS;
5420  $addr = self::EXT_LINK_ADDR;
5421  $prots = $this->mUrlProtocols;
5422  $type = null;
5423  $target = false;
5424  if ( $value === '' ) {
5425  $type = 'no-link';
5426  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5427  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5428  $this->mOutput->addExternalLink( $value );
5429  $type = 'link-url';
5430  $target = $value;
5431  }
5432  } else {
5433  $linkTitle = Title::newFromText( $value );
5434  if ( $linkTitle ) {
5435  $this->mOutput->addLink( $linkTitle );
5436  $type = 'link-title';
5437  $target = $linkTitle;
5438  }
5439  }
5440  return [ $type, $target ];
5441  }
5442 
5448  private function stripAltText( $caption, $holders ) {
5449  # Strip bad stuff out of the title (tooltip). We can't just use
5450  # replaceLinkHoldersText() here, because if this function is called
5451  # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5452  if ( $holders ) {
5453  $tooltip = $holders->replaceText( $caption );
5454  } else {
5455  $tooltip = $this->replaceLinkHoldersText( $caption );
5456  }
5457 
5458  # make sure there are no placeholders in thumbnail attributes
5459  # that are later expanded to html- so expand them now and
5460  # remove the tags
5461  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5462  # Compatibility hack! In HTML certain entity references not terminated
5463  # by a semicolon are decoded (but not if we're in an attribute; that's
5464  # how link URLs get away without properly escaping & in queries).
5465  # But wikitext has always required semicolon-termination of entities,
5466  # so encode & where needed to avoid decode of semicolon-less entities.
5467  # See T209236 and
5468  # https://www.w3.org/TR/html5/syntax.html#named-character-references
5469  # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5470  $tooltip = preg_replace( "/
5471  & # 1. entity prefix
5472  (?= # 2. followed by:
5473  (?: # a. one of the legacy semicolon-less named entities
5474  A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5475  C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5476  GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5477  O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5478  U(?:acute|circ|grave|uml)|Yacute|
5479  a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5480  c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5481  divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5482  frac(?:1(?:2|4)|34)|
5483  gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5484  i(?:acute|circ|excl|grave|quest|uml)|laquo|
5485  lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5486  m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5487  not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5488  o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5489  p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5490  s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5491  u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5492  )
5493  (?:[^;]|$)) # b. and not followed by a semicolon
5494  # S = study, for efficiency
5495  /Sx", '&amp;', $tooltip );
5496  $tooltip = Sanitizer::stripAllTags( $tooltip );
5497 
5498  return $tooltip;
5499  }
5500 
5510  public function attributeStripCallback( &$text, $frame = false ) {
5511  wfDeprecated( __METHOD__, '1.35' );
5512  $text = $this->replaceVariables( $text, $frame );
5513  $text = $this->mStripState->unstripBoth( $text );
5514  return $text;
5515  }
5516 
5522  public function getTags() {
5523  $this->firstCallInit();
5524  return array_merge(
5525  array_keys( $this->mTagHooks ),
5526  array_keys( $this->mFunctionTagHooks )
5527  );
5528  }
5529 
5534  public function getFunctionSynonyms() {
5535  $this->firstCallInit();
5536  return $this->mFunctionSynonyms;
5537  }
5538 
5543  public function getUrlProtocols() {
5544  return $this->mUrlProtocols;
5545  }
5546 
5576  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5577  global $wgTitle; # not generally used but removes an ugly failure mode
5578 
5579  $magicScopeVariable = $this->lock();
5580  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5581  $outText = '';
5582  $frame = $this->getPreprocessor()->newFrame();
5583 
5584  # Process section extraction flags
5585  $flags = 0;
5586  $sectionParts = explode( '-', $sectionId );
5587  $sectionIndex = array_pop( $sectionParts );
5588  foreach ( $sectionParts as $part ) {
5589  if ( $part === 'T' ) {
5590  $flags |= self::PTD_FOR_INCLUSION;
5591  }
5592  }
5593 
5594  # Check for empty input
5595  if ( strval( $text ) === '' ) {
5596  # Only sections 0 and T-0 exist in an empty document
5597  if ( $sectionIndex == 0 ) {
5598  if ( $mode === 'get' ) {
5599  return '';
5600  }
5601 
5602  return $newText;
5603  } else {
5604  if ( $mode === 'get' ) {
5605  return $newText;
5606  }
5607 
5608  return $text;
5609  }
5610  }
5611 
5612  # Preprocess the text
5613  $root = $this->preprocessToDom( $text, $flags );
5614 
5615  # <h> nodes indicate section breaks
5616  # They can only occur at the top level, so we can find them by iterating the root's children
5617  $node = $root->getFirstChild();
5618 
5619  # Find the target section
5620  if ( $sectionIndex == 0 ) {
5621  # Section zero doesn't nest, level=big
5622  $targetLevel = 1000;
5623  } else {
5624  while ( $node ) {
5625  if ( $node->getName() === 'h' ) {
5626  $bits = $node->splitHeading();
5627  if ( $bits['i'] == $sectionIndex ) {
5628  $targetLevel = $bits['level'];
5629  break;
5630  }
5631  }
5632  if ( $mode === 'replace' ) {
5633  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5634  }
5635  $node = $node->getNextSibling();
5636  }
5637  }
5638 
5639  if ( !$node ) {
5640  # Not found
5641  if ( $mode === 'get' ) {
5642  return $newText;
5643  } else {
5644  return $text;
5645  }
5646  }
5647 
5648  # Find the end of the section, including nested sections
5649  do {
5650  if ( $node->getName() === 'h' ) {
5651  $bits = $node->splitHeading();
5652  $curLevel = $bits['level'];
5653  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5654  break;
5655  }
5656  }
5657  if ( $mode === 'get' ) {
5658  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5659  }
5660  $node = $node->getNextSibling();
5661  } while ( $node );
5662 
5663  # Write out the remainder (in replace mode only)
5664  if ( $mode === 'replace' ) {
5665  # Output the replacement text
5666  # Add two newlines on -- trailing whitespace in $newText is conventionally
5667  # stripped by the editor, so we need both newlines to restore the paragraph gap
5668  # Only add trailing whitespace if there is newText
5669  if ( $newText != "" ) {
5670  $outText .= $newText . "\n\n";
5671  }
5672 
5673  while ( $node ) {
5674  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5675  $node = $node->getNextSibling();
5676  }
5677  }
5678 
5679  # Re-insert stripped tags
5680  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5681 
5682  return $outText;
5683  }
5684 
5699  public function getSection( $text, $sectionId, $defaultText = '' ) {
5700  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5701  }
5702 
5715  public function replaceSection( $oldText, $sectionId, $newText ) {
5716  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5717  }
5718 
5748  public function getFlatSectionInfo( $text ) {
5749  $magicScopeVariable = $this->lock();
5750  $this->startParse( null, new ParserOptions, self::OT_PLAIN, true );
5751  $frame = $this->getPreprocessor()->newFrame();
5752  $root = $this->preprocessToDom( $text, 0 );
5753  $node = $root->getFirstChild();
5754  $offset = 0;
5755  $currentSection = [
5756  'index' => 0,
5757  'level' => 0,
5758  'offset' => 0,
5759  'heading' => '',
5760  'text' => ''
5761  ];
5762  $sections = [];
5763 
5764  while ( $node ) {
5765  $nodeText = $frame->expand( $node, PPFrame::RECOVER_ORIG );
5766  if ( $node->getName() === 'h' ) {
5767  $bits = $node->splitHeading();
5768  $sections[] = $currentSection;
5769  $currentSection = [
5770  'index' => $bits['i'],
5771  'level' => $bits['level'],
5772  'offset' => $offset,
5773  'heading' => $nodeText,
5774  'text' => $nodeText
5775  ];
5776  } else {
5777  $currentSection['text'] .= $nodeText;
5778  }
5779  $offset += strlen( $nodeText );
5780  $node = $node->getNextSibling();
5781  }
5782  $sections[] = $currentSection;
5783  return $sections;
5784  }
5785 
5796  public function getRevisionId() {
5797  return $this->mRevisionId;
5798  }
5799 
5807  public function getRevisionObject() {
5808  wfDeprecated( __METHOD__, '1.35' );
5809 
5810  if ( $this->mRevisionObject ) {
5811  return $this->mRevisionObject;
5812  }
5813 
5814  $this->mRevisionObject = null;
5815 
5816  $revRecord = $this->getRevisionRecordObject();
5817  if ( $revRecord ) {
5818  $this->mRevisionObject = new Revision( $revRecord );
5819  }
5820 
5821  return $this->mRevisionObject;
5822  }
5823 
5830  public function getRevisionRecordObject() {
5831  if ( $this->mRevisionRecordObject ) {
5832  return $this->mRevisionRecordObject;
5833  }
5834 
5835  // NOTE: try to get the RevisionObject even if mRevisionId is null.
5836  // This is useful when parsing a revision that has not yet been saved.
5837  // However, if we get back a saved revision even though we are in
5838  // preview mode, we'll have to ignore it, see below.
5839  // NOTE: This callback may be used to inject an OLD revision that was
5840  // already loaded, so "current" is a bit of a misnomer. We can't just
5841  // skip it if mRevisionId is set.
5842  $rev = call_user_func(
5843  $this->mOptions->getCurrentRevisionRecordCallback(),
5844  $this->getTitle(),
5845  $this
5846  );
5847 
5848  if ( $rev === false ) {
5849  // The revision record callback returns `false` (not null) to
5850  // indicate that the revision is missing. (See for example
5851  // Parser::statelessFetchRevisionRecord(), the default callback.)
5852  // This API expects `null` instead. (T251952)
5853  $rev = null;
5854  }
5855 
5856  if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
5857  // We are in preview mode (mRevisionId is null), and the current revision callback
5858  // returned an existing revision. Ignore it and return null, it's probably the page's
5859  // current revision, which is not what we want here. Note that we do want to call the
5860  // callback to allow the unsaved revision to be injected here, e.g. for
5861  // self-transclusion previews.
5862  return null;
5863  }
5864 
5865  // If the parse is for a new revision, then the callback should have
5866  // already been set to force the object and should match mRevisionId.
5867  // If not, try to fetch by mRevisionId for sanity.
5868  if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
5869  $rev = MediaWikiServices::getInstance()
5870  ->getRevisionLookup()
5871  ->getRevisionById( $this->mRevisionId );
5872  }
5873 
5874  $this->mRevisionRecordObject = $rev;
5875 
5876  return $this->mRevisionRecordObject;
5877  }
5878 
5884  public function getRevisionTimestamp() {
5885  if ( $this->mRevisionTimestamp !== null ) {
5886  return $this->mRevisionTimestamp;
5887  }
5888 
5889  # Use specified revision timestamp, falling back to the current timestamp
5890  $revObject = $this->getRevisionRecordObject();
5891  $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
5892  $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
5893 
5894  # The cryptic '' timezone parameter tells to use the site-default
5895  # timezone offset instead of the user settings.
5896  # Since this value will be saved into the parser cache, served
5897  # to other users, and potentially even used inside links and such,
5898  # it needs to be consistent for all visitors.
5899  $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
5900 
5901  return $this->mRevisionTimestamp;
5902  }
5903 
5909  public function getRevisionUser(): ?string {
5910  if ( $this->mRevisionUser === null ) {
5911  $revObject = $this->getRevisionRecordObject();
5912 
5913  # if this template is subst: the revision id will be blank,
5914  # so just use the current user's name
5915  if ( $revObject && $revObject->getUser() ) {
5916  $this->mRevisionUser = $revObject->getUser()->getName();
5917  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5918  $this->mRevisionUser = $this->getUser()->getName();
5919  } else {
5920  # Note that we fall through here with
5921  # $this->mRevisionUser still null
5922  }
5923  }
5924  return $this->mRevisionUser;
5925  }
5926 
5932  public function getRevisionSize() {
5933  if ( $this->mRevisionSize === null ) {
5934  $revObject = $this->getRevisionRecordObject();
5935 
5936  # if this variable is subst: the revision id will be blank,
5937  # so just use the parser input size, because the own substituation
5938  # will change the size.
5939  if ( $revObject ) {
5940  $this->mRevisionSize = $revObject->getSize();
5941  } else {
5942  $this->mRevisionSize = $this->mInputSize;
5943  }
5944  }
5945  return $this->mRevisionSize;
5946  }
5947 
5953  public function setDefaultSort( $sort ) {
5954  $this->mDefaultSort = $sort;
5955  $this->mOutput->setProperty( 'defaultsort', $sort );
5956  }
5957 
5968  public function getDefaultSort() {
5969  if ( $this->mDefaultSort !== false ) {
5970  return $this->mDefaultSort;
5971  } else {
5972  return '';
5973  }
5974  }
5975 
5982  public function getCustomDefaultSort() {
5983  return $this->mDefaultSort;
5984  }
5985 
5986  private static function getSectionNameFromStrippedText( $text ) {
5988  $text = Sanitizer::decodeCharReferences( $text );
5989  $text = self::normalizeSectionName( $text );
5990  return $text;
5991  }
5992 
5993  private static function makeAnchor( $sectionName ) {
5994  return '#' . Sanitizer::escapeIdForLink( $sectionName );
5995  }
5996 
5997  private function makeLegacyAnchor( $sectionName ) {
5998  $fragmentMode = $this->svcOptions->get( 'FragmentMode' );
5999  if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6000  // ForAttribute() and ForLink() are the same for legacy encoding
6002  } else {
6003  $id = Sanitizer::escapeIdForLink( $sectionName );
6004  }
6005 
6006  return "#$id";
6007  }
6008 
6017  public function guessSectionNameFromWikiText( $text ) {
6018  # Strip out wikitext links(they break the anchor)
6019  $text = $this->stripSectionName( $text );
6020  $sectionName = self::getSectionNameFromStrippedText( $text );
6021  return self::makeAnchor( $sectionName );
6022  }
6023 
6033  public function guessLegacySectionNameFromWikiText( $text ) {
6034  # Strip out wikitext links(they break the anchor)
6035  $text = $this->stripSectionName( $text );
6036  $sectionName = self::getSectionNameFromStrippedText( $text );
6037  return $this->makeLegacyAnchor( $sectionName );
6038  }
6039 
6045  public static function guessSectionNameFromStrippedText( $text ) {
6046  $sectionName = self::getSectionNameFromStrippedText( $text );
6047  return self::makeAnchor( $sectionName );
6048  }
6049 
6056  private static function normalizeSectionName( $text ) {
6057  # T90902: ensure the same normalization is applied for IDs as to links
6058 
6059  $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6060  '@phan-var MediaWikiTitleCodec $titleParser';
6061  try {
6062 
6063  $parts = $titleParser->splitTitleString( "#$text" );
6064  } catch ( MalformedTitleException $ex ) {
6065  return $text;
6066  }
6067  return $parts['fragment'];
6068  }
6069 
6084  public function stripSectionName( $text ) {
6085  # Strip internal link markup
6086  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6087  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6088 
6089  # Strip external link markup
6090  # @todo FIXME: Not tolerant to blank link text
6091  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6092  # on how many empty links there are on the page - need to figure that out.
6093  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6094 
6095  # Parse wikitext quotes (italics & bold)
6096  $text = $this->doQuotes( $text );
6097 
6098  # Strip HTML tags
6099  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6100  return $text;
6101  }
6102 
6113  private function fuzzTestSrvus( $text, Title $title, ParserOptions $options,
6114  $outputType = self::OT_HTML
6115  ) {
6116  $magicScopeVariable = $this->lock();
6117  $this->startParse( $title, $options, $outputType, true );
6118 
6119  $text = $this->replaceVariables( $text );
6120  $text = $this->mStripState->unstripBoth( $text );
6121  $text = Sanitizer::removeHTMLtags( $text );
6122  return $text;
6123  }
6124 
6131  private function fuzzTestPst( $text, Title $title, ParserOptions $options ) {
6132  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6133  }
6134 
6141  private function fuzzTestPreprocess( $text, Title $title, ParserOptions $options ) {
6142  return $this->fuzzTestSrvus( $text, $title, $options, self::OT_PREPROCESS );
6143  }
6144 
6162  public function markerSkipCallback( $s, callable $callback ) {
6163  $i = 0;
6164  $out = '';
6165  while ( $i < strlen( $s ) ) {
6166  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6167  if ( $markerStart === false ) {
6168  $out .= call_user_func( $callback, substr( $s, $i ) );
6169  break;
6170  } else {
6171  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6172  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6173  if ( $markerEnd === false ) {
6174  $out .= substr( $s, $markerStart );
6175  break;
6176  } else {
6177  $markerEnd += strlen( self::MARKER_SUFFIX );
6178  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6179  $i = $markerEnd;
6180  }
6181  }
6182  }
6183  return $out;
6184  }
6185 
6192  public function killMarkers( $text ) {
6193  return $this->mStripState->killMarkers( $text );
6194  }
6195 
6206  public static function parseWidthParam( $value, $parseHeight = true ) {
6207  $parsedWidthParam = [];
6208  if ( $value === '' ) {
6209  return $parsedWidthParam;
6210  }
6211  $m = [];
6212  # (T15500) In both cases (width/height and width only),
6213  # permit trailing "px" for backward compatibility.
6214  if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6215  $width = intval( $m[1] );
6216  $height = intval( $m[2] );
6217  $parsedWidthParam['width'] = $width;
6218  $parsedWidthParam['height'] = $height;
6219  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6220  $width = intval( $value );
6221  $parsedWidthParam['width'] = $width;
6222  }
6223  return $parsedWidthParam;
6224  }
6225 
6235  protected function lock() {
6236  if ( $this->mInParse ) {
6237  throw new MWException( "Parser state cleared while parsing. "
6238  . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6239  }
6240 
6241  // Save the backtrace when locking, so that if some code tries locking again,
6242  // we can print the lock owner's backtrace for easier debugging
6243  $e = new Exception;
6244  $this->mInParse = $e->getTraceAsString();
6245 
6246  $recursiveCheck = new ScopedCallback( function () {
6247  $this->mInParse = false;
6248  } );
6249 
6250  return $recursiveCheck;
6251  }
6252 
6263  public static function stripOuterParagraph( $html ) {
6264  $m = [];
6265  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6266  $html = $m[1];
6267  }
6268 
6269  return $html;
6270  }
6271 
6282  public function getFreshParser() {
6283  if ( $this->mInParse ) {
6284  return $this->factory->create();
6285  } else {
6286  return $this;
6287  }
6288  }
6289 
6297  public function enableOOUI() {
6298  wfDeprecated( __METHOD__, '1.35' );
6300  $this->mOutput->setEnableOOUI( true );
6301  }
6302 
6309  private function setOutputFlag( string $flag, string $reason ): void {
6310  $this->mOutput->setFlag( $flag );
6311  $name = $this->getTitle()->getPrefixedText();
6312  $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6313  }
6314 }
Parser\$badFileLookup
BadFileLookup $badFileLookup
Definition: Parser.php:354
Parser\getFunctionHooks
getFunctionHooks()
Get all registered function hook identifiers.
Definition: Parser.php:4900
Parser\$mLinkRenderer
LinkRenderer $mLinkRenderer
Definition: Parser.php:318
Parser\$mForceTocPosition
$mForceTocPosition
Definition: Parser.php:243
Parser\recursivePreprocess
recursivePreprocess( $text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition: Parser.php:941
Parser\getContentLanguageConverter
getContentLanguageConverter()
Shorthand for getting a Language Converter for Content language.
Definition: Parser.php:1615
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:5510
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:180
Parser\$linkRendererFactory
LinkRendererFactory $linkRendererFactory
Definition: Parser.php:345
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:75
Parser\maybeMakeExternalImage
maybeMakeExternalImage( $url)
make an image if it's allowed, either through the global option, through the exception,...
Definition: Parser.php:2323
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:329
Parser\$mInputSize
$mInputSize
Definition: Parser.php:284
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:483
ParserOutput
Definition: ParserOutput.php:25
Parser\$mLinkHolders
LinkHolderArray $mLinkHolders
Definition: Parser.php:215
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:2942
Parser\makeLimitReport
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment.
Definition: Parser.php:720
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:1653
Parser\stripAltText
stripAltText( $caption, $holders)
Definition: Parser.php:5448
Parser\killMarkers
killMarkers( $text)
Remove any strip markers found in the given text.
Definition: Parser.php:6192
User\isAnon
isAnon()
Get whether the user is anonymous.
Definition: User.php:3104
Sanitizer\stripAllTags
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1860
Parser\$mTagHooks
$mTagHooks
Definition: Parser.php:153
Parser\OutputType
OutputType( $x=null)
Accessor/mutator for the output type.
Definition: Parser.php:1053
Parser\$currentRevisionCache
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:302
Parser\setOutputFlag
setOutputFlag(string $flag, string $reason)
Sets the flag on the parser output but also does some debug logging.
Definition: Parser.php:6309
Parser\Title
Title(Title $x=null)
Accessor/mutator for the Title object.
Definition: Parser.php:1017
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:6297
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
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:245
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:1543
Parser\parseExtensionTagAsTopLevelDoc
parseExtensionTagAsTopLevelDoc( $text)
Needed by Parsoid/PHP to ensure all the hooks for extensions are run in the right order.
Definition: Parser.php:898
Parser\$mDoubleUnderscores
$mDoubleUnderscores
Definition: Parser.php:238
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:5932
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:1120
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:496
Parser\handleExternalLinks
handleExternalLinks( $text)
Replace external links (REL)
Definition: Parser.php:2109
Parser\$mOutputType
$mOutputType
Definition: Parser.php:270
Parser\setUser
setUser(?User $user)
Set the current user.
Definition: Parser.php:981
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:43
ParserOptions\getDisableTitleConversion
getDisableTitleConversion()
Whether title conversion should be disabled.
Definition: ParserOptions.php:524
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1808
Parser\handleHeadings
handleHeadings( $text)
Parse headers and return html.
Definition: Parser.php:1891
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:4698
Parser\handleAllQuotes
handleAllQuotes( $text)
Replace single quotes with HTML markup.
Definition: Parser.php:1908
Parser\$mUrlProtocols
$mUrlProtocols
Definition: Parser.php:189
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:1227
Parser\$mLinkID
int $mLinkID
Definition: Parser.php:221
Title\getPrefixedText
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1850
Parser\handleDoubleUnderscore
handleDoubleUnderscore( $text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores,...
Definition: Parser.php:4007
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:3453
Sanitizer\normalizeSectionNameWhitespace
static normalizeSectionNameWhitespace( $section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(),...
Definition: Sanitizer.php:1396
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:6056
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:1282
Parser\recursiveTagParseFully
recursiveTagParseFully( $text, $frame=false)
Fully parse wikitext to fully parsed HTML.
Definition: Parser.php:873
NS_TEMPLATE
const NS_TEMPLATE
Definition: Defines.php:79
Parser\$specialPageFactory
SpecialPageFactory $specialPageFactory
Definition: Parser.php:333
Parser\nextLinkID
nextLinkID()
Definition: Parser.php:1096
Parser\fuzzTestPreprocess
fuzzTestPreprocess( $text, Title $title, ParserOptions $options)
Definition: Parser.php:6141
Parser\fuzzTestPst
fuzzTestPst( $text, Title $title, ParserOptions $options)
Definition: Parser.php:6131
Parser\getTargetLanguage
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:1123
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:540
Parser\$mStripList
$mStripList
Definition: Parser.php:158
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1219
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:90
Parser\getRevisionObject
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition: Parser.php:5807
Parser\guessSectionNameFromWikiText
guessSectionNameFromWikiText( $text)
Try to guess the section anchor name based on a wikitext fragment presumably extracted from a heading...
Definition: Parser.php:6017
Parser\setDefaultSort
setDefaultSort( $sort)
Mutator for $mDefaultSort.
Definition: Parser.php:5953
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:1003
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:2189
Parser\replaceVariables
replaceVariables( $text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:2868
Parser\MARKER_PREFIX
const MARKER_PREFIX
Definition: Parser.php:145
Parser\getFunctionSynonyms
getFunctionSynonyms()
Definition: Parser.php:5534
Parser\$mInParse
bool string $mInParse
Recursive call protection.
Definition: Parser.php:310
Parser\transformMsg
transformMsg( $text, ParserOptions $options, Title $title=null)
Wrapper for preprocess()
Definition: Parser.php:4754
$wgTitle
$wgTitle
Definition: Setup.php:800
Parser\doQuotes
doQuotes( $text)
Helper function for handleAllQuotes()
Definition: Parser.php:1926
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:342
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:5522
Parser\getStripList
getStripList()
Get a list of strippable XML-like elements.
Definition: Parser.php:1290
Parser\initializeVariables
initializeVariables()
Initialize the magic variables (like CURRENTMONTHNAME) and substitution modifiers.
Definition: Parser.php:2813
PPFrame\NO_TEMPLATES
const NO_TEMPLATES
Definition: PPFrame.php:30
Preprocessor
Definition: Preprocessor.php:30
Parser\getOptions
getOptions()
Definition: Parser.php:1068
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:1111
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:287
Parser\Options
Options( $x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:1088
Parser\lock
lock()
Lock the current instance of the parser.
Definition: Parser.php:6235
Parser\statelessFetchRevision
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition: Parser.php:3499
Revision
Definition: Revision.php:40
Parser\getDefaultSort
getDefaultSort()
Accessor for $mDefaultSort Will use the empty string if none is set.
Definition: Parser.php:5968
Parser\$mFunctionSynonyms
$mFunctionSynonyms
Definition: Parser.php:156
Parser\$hookRunner
HookRunner $hookRunner
Definition: Parser.php:360
Parser\$nsInfo
NamespaceInfo $nsInfo
Definition: Parser.php:348
Parser\makeLegacyAnchor
makeLegacyAnchor( $sectionName)
Definition: Parser.php:5997
Parser\setHook
setHook( $tag, callable $callback)
Create an HTML-style tag, e.g.
Definition: Parser.php:4798
Parser\$mHeadings
$mHeadings
Definition: Parser.php:236
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:58
Parser\getTitle
getTitle()
Definition: Parser.php:1006
Parser\$mVariables
MagicWordArray $mVariables
Definition: Parser.php:175
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1058
MWException
MediaWiki exception.
Definition: MWException.php:28
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:3587
Parser\$mConf
array $mConf
Definition: Parser.php:186
Parser\$ot
$ot
Definition: Parser.php:272
Parser\getRevisionRecordObject
getRevisionRecordObject()
Get the revision record object for $this->mRevisionId.
Definition: Parser.php:5830
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:4594
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:1026
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:2709
Parser\firstCallInit
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition: Parser.php:522
Parser\$mProfiler
SectionProfiler $mProfiler
Definition: Parser.php:313
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:916
Parser\getFlatSectionInfo
getFlatSectionInfo( $text)
Get an array of preprocessor section information.
Definition: Parser.php:5748
Parser\$mMarkerIndex
$mMarkerIndex
Definition: Parser.php:163
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:5982
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:1325
$matches
$matches
Definition: NoLocalSettings.php:24
CoreTagHooks\register
static register( $parser)
Definition: CoreTagHooks.php:33
Parser\$contLang
Language $contLang
Definition: Parser.php:324
Parser\makeAnchor
static makeAnchor( $sectionName)
Definition: Parser.php:5993
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:330
Parser\replaceLinkHoldersPrivate
replaceLinkHoldersPrivate(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:4951
LinkHolderArray
Definition: LinkHolderArray.php:33
Parser\__clone
__clone()
Allow extensions to clean up when the parser is cloned.
Definition: Parser.php:497
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:1628
Parser\callParserFunction
callParserFunction(PPFrame $frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition: Parser.php:3296
Parser\extensionSubstitution
extensionSubstitution(array $params, PPFrame $frame)
Return the text to be used for a given extension tag.
Definition: Parser.php:3881
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:294
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:6162
Parser\fetchFileNoRegister
fetchFileNoRegister(Title $title, array $options=[])
Helper function for fetchFileAndTitle.
Definition: Parser.php:3736
Parser\fetchTemplate
fetchTemplate(Title $title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3573
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:2919
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:847
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:3711
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:4079
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:592
Parser\$mHighestExpansionDepth
$mHighestExpansionDepth
Definition: Parser.php:232
SectionProfiler
Arbitrary section name based PHP profiling.
Definition: SectionProfiler.php:30
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:83
Parser\cleanSig
cleanSig( $text, $parsing=false)
Clean up signature text.
Definition: Parser.php:4668
RequestContext
Group all the pieces relevant to the context of a request into one instance @newable.
Definition: RequestContext.php:38
Parser\$mUser
User $mUser
Definition: Parser.php:251
Parser\$mImageParamsMagicArray
$mImageParamsMagicArray
Definition: Parser.php:161
Parser\handleInternalLinks
handleInternalLinks( $text)
Process [[ ]] wikilinks.
Definition: Parser.php:2380
Parser\$mTplRedirCache
$mTplRedirCache
Definition: Parser.php:234
Parser\$mFirstCall
bool $mFirstCall
Whether firstCallInit still needs to be called.
Definition: Parser.php:168
ParserOptions\getPreSaveTransform
getPreSaveTransform()
Transform wiki markup when saving the page?
Definition: ParserOptions.php:626
Parser\getStripState
getStripState()
Get the StripState.
Definition: Parser.php:1299
Parser\getContentLanguage
getContentLanguage()
Get the content language that this Parser is using.
Definition: Parser.php:1194
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:1709
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:405
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:1312
Parser\fuzzTestSrvus
fuzzTestSrvus( $text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
Strip/replaceVariables/unstrip for preprocessor regression testing.
Definition: Parser.php:6113
Parser\isCurrentRevisionOfTitleCached
isCurrentRevisionOfTitleCached(Title $title)
Definition: Parser.php:3482
Parser\getFreshParser
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition: Parser.php:6282
Parser\getRevisionUser
getRevisionUser()
Get the name of the user that edited the last revision.
Definition: Parser.php:5909
Parser\setOptions
setOptions(ParserOptions $options)
Mutator for the ParserOptions object.
Definition: Parser.php:1077
Parser\getImageParams
getImageParams( $handler)
Definition: Parser.php:5142
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:718
Parser\$mAutonumber
$mAutonumber
Definition: Parser.php:204
Parser\replaceLinkHolders
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:4940
Parser\addTrackingCategory
addTrackingCategory( $msg)
Definition: Parser.php:4060
Parser\getUrlProtocols
getUrlProtocols()
Definition: Parser.php:5543
Parser\incrementIncludeSize
incrementIncludeSize( $type, $size)
Increment an include size counter.
Definition: Parser.php:3981
Parser\getTargetLanguageConverter
getTargetLanguageConverter()
Shorthand for getting a Language Converter for Target language.
Definition: Parser.php:1604
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:4720
$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:1819
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:4683
$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:4677
Parser\$mOutput
ParserOutput $mOutput
Definition: Parser.php:203
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:260
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:57
Parser\$mRevisionUser
$mRevisionUser
Definition: Parser.php:280
User\getOption
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2684
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:647
Parser\setFunctionTagHook
setFunctionTagHook( $tag, callable $callback, $flags)
Create a tag function, e.g.
Definition: Parser.php:4916
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:5576
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:4980
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:4118
Parser\magicLinkCallback
magicLinkCallback(array $m)
Definition: Parser.php:1740
Parser\$mFunctionTagHooks
$mFunctionTagHooks
Definition: Parser.php:157
Parser\getUser
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise.
Definition: Parser.php:1141
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1487
Parser\incrementExpensiveFunctionCount
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition: Parser.php:3995
Parser\$mImageParams
$mImageParams
Definition: Parser.php:160
Parser\setFunctionHook
setFunctionHook( $id, callable $callback, $flags=0)
Create a function, e.g.
Definition: Parser.php:4864
Parser\setLinkID
setLinkID( $id)
Definition: Parser.php:1103
Sanitizer\cleanUrl
static cleanUrl( $url)
Definition: Sanitizer.php:1898
Parser\$magicWordFactory
MagicWordFactory $magicWordFactory
Definition: Parser.php:321
Parser\preprocessToDom
preprocessToDom( $text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:2843
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:281
Parser\getMagicWordFactory
getMagicWordFactory()
Get the MagicWordFactory that this Parser is using.
Definition: Parser.php:1184
Parser\argSubstitution
argSubstitution(array $piece, PPFrame $frame)
Triple brace replacement – used for template arguments.
Definition: Parser.php:3828
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:960
Parser\getPreloadText
getPreloadText( $text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:960
Parser\setOutputType
setOutputType( $ot)
Mutator for the output type.
Definition: Parser.php:1035
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:3385
$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:2211
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:210
Parser\internalParse
internalParse( $text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1535
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:4654
Parser\$mPPNodeCount
$mPPNodeCount
Definition: Parser.php:225
Title
Represents a title within MediaWiki.
Definition: Title.php:42
Parser\resetOutput
resetOutput()
Reset the ParserOutput.
Definition: Parser.php:590
Parser\stripOuterParagraph
static stripOuterParagraph( $html)
Strip outer.
Definition: Parser.php:6263
Parser\$mVarCache
$mVarCache
Definition: Parser.php:159
Parser\$mDefaultSort
$mDefaultSort
Definition: Parser.php:233
Parser\$mExpensiveFunctionCount
$mExpensiveFunctionCount
Definition: Parser.php:240
Parser\normalizeLinkUrl
static normalizeLinkUrl( $url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition: Parser.php:2242
MediaWiki\Preferences\SignatureValidator
Definition: SignatureValidator.php:37
Parser\interwikiTransclude
interwikiTransclude(Title $title, $action)
Transclude an interwiki link.
Definition: Parser.php:3759
Parser\$mExtLinkBracketedRegex
$mExtLinkBracketedRegex
Definition: Parser.php:189
wfMatchesDomainList
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
Definition: GlobalFunctions.php:875
Parser\$mIncludeSizes
$mIncludeSizes
Definition: Parser.php:223
$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:5699
Xml\isWellFormedXmlFragment
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:734
Parser\$mRevisionTimestamp
$mRevisionTimestamp
Definition: Parser.php:278
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:5715
Sanitizer\ID_PRIMARY
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
Definition: Sanitizer.php:67
Parser\$logger
LoggerInterface $logger
Definition: Parser.php:351
ParserOptions\getUser
getUser()
Current user.
Definition: ParserOptions.php:1040
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:2733
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:1204
Parser\getOutput
getOutput()
Definition: Parser.php:1061
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:2390
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:4481
Parser\getOutputType
getOutputType()
Accessor for the output type.
Definition: Parser.php:1027
Parser\$mGeneratedPPNodeCount
$mGeneratedPPNodeCount
Definition: Parser.php:230
Parser\statelessFetchRevisionRecord
static statelessFetchRevisionRecord(Title $title, $parser=null)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition: Parser.php:3516
Parser\getHookRunner
getHookRunner()
Get a HookRunner for calling core hooks.
Definition: Parser.php:1640
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:1415
TextContent\normalizeLineEndings
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Definition: TextContent.php:184
Parser\getSectionNameFromStrippedText
static getSectionNameFromStrippedText( $text)
Definition: Parser.php:5986
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:1147
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:4735
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
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:242
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:1296
Parser\guessSectionNameFromStrippedText
static guessSectionNameFromStrippedText( $text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition: Parser.php:6045
Parser\fetchTemplateAndTitle
fetchTemplateAndTitle(Title $title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3528
Parser\$languageConverterFactory
LanguageConverterFactory $languageConverterFactory
Definition: Parser.php:327
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:571
Parser\parseLinkParameter
parseLinkParameter( $value)
Parse the value of 'link' parameter in image syntax ([[File:Foo.jpg|link=<value>]]).
Definition: Parser.php:5418
$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:700
Parser\$mRevisionObject
$mRevisionObject
Definition: Parser.php:274
Parser\fetchCurrentRevisionOfTitle
fetchCurrentRevisionOfTitle(Title $title)
Fetch the current revision of a given title.
Definition: Parser.php:3431
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:1515
Parser\getRevisionTimestamp
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition: Parser.php:5884
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:2760
User\getBoolOption
getBoolOption( $oname)
Get the user's current setting for a given option, as a boolean value.
Definition: User.php:2716
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
Parser\$mPreprocessor
Preprocessor $mPreprocessor
Definition: Parser.php:196
Parser\parseWidthParam
static parseWidthParam( $value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition: Parser.php:6206
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:4705
Parser\setTitle
setTitle(Title $t=null)
Set the context title.
Definition: Parser.php:990
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:59
Parser\replaceLinkHoldersText
replaceLinkHoldersText( $text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition: Parser.php:4962
Parser\normalizeUrlComponent
static normalizeUrlComponent( $component, $unsafe)
Definition: Parser.php:2300
Parser\clearTagHooks
clearTagHooks()
Remove all tag hooks.
Definition: Parser.php:4815
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:2081
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:269
Parser\getLinkRenderer
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition: Parser.php:1166
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:612
Parser\$mRevisionId
$mRevisionId
Definition: Parser.php:276
RequestContext\setTitle
setTitle(Title $title=null)
Definition: RequestContext.php:163
Parser\$mRevisionSize
$mRevisionSize
Definition: Parser.php:282
Parser\getRevisionId
getRevisionId()
Get the ID of the revision we are parsing.
Definition: Parser.php:5796
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
Parser\pstPass2
pstPass2( $text, User $user)
Pre-save transform helper function.
Definition: Parser.php:4518
Parser\clearState
clearState()
Clear Parser state.
Definition: Parser.php:540
Parser\guessLegacySectionNameFromWikiText
guessLegacySectionNameFromWikiText( $text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition: Parser.php:6033
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:5196
Parser\stripSectionName
stripSectionName( $text)
Strips a text string of wikitext for use in a section anchor.
Definition: Parser.php:6084
Parser\EXT_IMAGE_REGEX
const EXT_IMAGE_REGEX
Definition: Parser.php:109
Parser\getPreprocessor
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:1153
Parser\doBlockLevels
doBlockLevels( $text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: Parser.php:2747
Parser\$hookContainer
HookContainer $hookContainer
Definition: Parser.php:357
$type
$type
Definition: testCompression.php:52
MWTidy\tidy
static tidy( $text)
Interface with Remex tidy.
Definition: MWTidy.php:42