MediaWiki  1.34.0
Parser.php
Go to the documentation of this file.
1 <?php
30 use Psr\Log\NullLogger;
31 use Wikimedia\ScopedCallback;
32 use Psr\Log\LoggerInterface;
33 
74 class Parser {
80  const VERSION = '1.6.4';
81 
86  const HALF_PARSED_VERSION = 2;
87 
88  # Flags for Parser::setFunctionHook
89  const SFH_NO_HASH = 1;
90  const SFH_OBJECT_ARGS = 2;
91 
92  # Constants needed for external link processing
93  # Everything except bracket, space, or control characters
94  # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
95  # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
96  # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
97  # uses to replace invalid HTML characters.
98  const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
99  # Simplified expression to match an IPv4 or IPv6 address, or
100  # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
101  const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
102  # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
103  // phpcs:ignore Generic.Files.LineLength
104  const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
105  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
106 
107  # Regular expression for a non-newline space
108  const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
109 
110  # Flags for preprocessToDom
111  const PTD_FOR_INCLUSION = 1;
112 
113  # Allowed values for $this->mOutputType
114  # Parameter to startExternalParse().
115  const OT_HTML = 1; # like parse()
116  const OT_WIKI = 2; # like preSaveTransform()
117  const OT_PREPROCESS = 3; # like preprocess()
118  const OT_MSG = 3;
119  const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
120 
138  const MARKER_SUFFIX = "-QINU`\"'\x7f";
139  const MARKER_PREFIX = "\x7f'\"`UNIQ-";
140 
141  # Markers used for wrapping the table of contents
142  const TOC_START = '<mw:toc>';
143  const TOC_END = '</mw:toc>';
144 
146  const MAX_TTS = 900;
147 
148  # Persistent:
149  public $mTagHooks = [];
150  public $mTransparentTagHooks = [];
151  public $mFunctionHooks = [];
152  public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
153  public $mFunctionTagHooks = [];
154  public $mStripList = [];
155  public $mDefaultStripList = [];
156  public $mVarCache = [];
157  public $mImageParams = [];
158  public $mImageParamsMagicArray = [];
159  public $mMarkerIndex = 0;
163  public $mFirstCall = true;
164 
165  # Initialised by initializeVariables()
166 
170  public $mVariables;
171 
175  public $mSubstWords;
176 
181  public $mConf;
182 
183  # Initialised in constructor
184  public $mExtLinkBracketedRegex, $mUrlProtocols;
185 
186  # Initialized in getPreprocessor()
187 
188  public $mPreprocessor;
189 
190  # Cleared with clearState():
191 
194  public $mOutput;
195  public $mAutonumber;
196 
200  public $mStripState;
201 
202  public $mIncludeCount;
206  public $mLinkHolders;
207 
208  public $mLinkID;
209  public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
210  public $mDefaultSort;
211  public $mTplRedirCache, $mHeadings, $mDoubleUnderscores;
212  public $mExpensiveFunctionCount; # number of expensive parser function calls
213  public $mShowToc, $mForceTocPosition;
215  public $mTplDomCache;
216 
220  public $mUser; # User object; only used when doing pre-save transform
221 
222  # Temporary
223  # These are variables reset at least once per parse regardless of $clearState
224 
228  public $mOptions;
229 
237  public $mTitle; # Title context, used for self-link rendering and similar things
238  public $mOutputType; # Output type, one of the OT_xxx constants
239  public $ot; # Shortcut alias, see setOutputType()
240  public $mRevisionObject; # The revision object of the specified revision ID
241  public $mRevisionId; # ID to display in {{REVISIONID}} tags
242  public $mRevisionTimestamp; # The timestamp of the specified revision ID
243  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
244  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
245  public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
246  public $mInputSize = false; # For {{PAGESIZE}} on current page.
247 
253  public $mLangLinkLanguages;
254 
261  public $currentRevisionCache;
262 
267  public $mInParse = false;
268 
270  protected $mProfiler;
271 
275  protected $mLinkRenderer;
276 
278  private $magicWordFactory;
279 
281  private $contLang;
282 
284  private $factory;
285 
287  private $specialPageFactory;
288 
296  private $svcOptions;
297 
299  private $linkRendererFactory;
300 
302  private $nsInfo;
303 
305  private $logger;
306 
308  private $badFileLookup;
309 
316  public static $constructorOptions = [
317  // See $wgParserConf documentation
318  'class',
319  'preprocessorClass',
320  // See documentation for the corresponding config options
321  'ArticlePath',
322  'EnableScaryTranscluding',
323  'ExtraInterlanguageLinkPrefixes',
324  'FragmentMode',
325  'LanguageCode',
326  'MaxSigChars',
327  'MaxTocLevel',
328  'MiserMode',
329  'ScriptPath',
330  'Server',
331  'ServerName',
332  'ShowHostnames',
333  'Sitename',
334  'StylePath',
335  'TranscludeCacheExpiry',
336  ];
337 
352  public function __construct(
353  $svcOptions = null,
354  MagicWordFactory $magicWordFactory = null,
355  Language $contLang = null,
356  ParserFactory $factory = null,
357  $urlProtocols = null,
358  SpecialPageFactory $spFactory = null,
359  $linkRendererFactory = null,
360  $nsInfo = null,
361  $logger = null,
362  BadFileLookup $badFileLookup = null
363  ) {
364  if ( !$svcOptions || is_array( $svcOptions ) ) {
365  // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is
366  // Config, and the eighth is LinkRendererFactory.
367  $this->mConf = (array)$svcOptions;
368  if ( empty( $this->mConf['class'] ) ) {
369  $this->mConf['class'] = self::class;
370  }
371  if ( empty( $this->mConf['preprocessorClass'] ) ) {
372  $this->mConf['preprocessorClass'] = self::getDefaultPreprocessorClass();
373  }
374  $this->svcOptions = new ServiceOptions( self::$constructorOptions,
375  $this->mConf, func_num_args() > 6
376  ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig()
377  );
378  $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null;
379  $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null;
380  } else {
381  // New calling convention
382  $svcOptions->assertRequiredOptions( self::$constructorOptions );
383  // $this->mConf is public, so we'll keep those two options there as well for
384  // compatibility until it's removed
385  $this->mConf = [
386  'class' => $svcOptions->get( 'class' ),
387  'preprocessorClass' => $svcOptions->get( 'preprocessorClass' ),
388  ];
389  $this->svcOptions = $svcOptions;
390  }
391 
392  $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
393  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
394  self::EXT_LINK_ADDR .
395  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
396 
397  $this->magicWordFactory = $magicWordFactory ??
398  MediaWikiServices::getInstance()->getMagicWordFactory();
399 
400  $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
401 
402  $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory();
403  $this->specialPageFactory = $spFactory ??
404  MediaWikiServices::getInstance()->getSpecialPageFactory();
405  $this->linkRendererFactory = $linkRendererFactory ??
406  MediaWikiServices::getInstance()->getLinkRendererFactory();
407  $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
408  $this->logger = $logger ?: new NullLogger();
409  $this->badFileLookup = $badFileLookup ??
410  MediaWikiServices::getInstance()->getBadFileLookup();
411  }
412 
416  public function __destruct() {
417  if ( isset( $this->mLinkHolders ) ) {
418  // @phan-suppress-next-line PhanTypeObjectUnsetDeclaredProperty
419  unset( $this->mLinkHolders );
420  }
421  // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach
422  foreach ( $this as $name => $value ) {
423  unset( $this->$name );
424  }
425  }
426 
430  public function __clone() {
431  $this->mInParse = false;
432 
433  // T58226: When you create a reference "to" an object field, that
434  // makes the object field itself be a reference too (until the other
435  // reference goes out of scope). When cloning, any field that's a
436  // reference is copied as a reference in the new object. Both of these
437  // are defined PHP5 behaviors, as inconvenient as it is for us when old
438  // hooks from PHP4 days are passing fields by reference.
439  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
440  // Make a non-reference copy of the field, then rebind the field to
441  // reference the new copy.
442  $tmp = $this->$k;
443  $this->$k =& $tmp;
444  unset( $tmp );
445  }
446 
447  Hooks::run( 'ParserCloned', [ $this ] );
448  }
449 
457  public static function getDefaultPreprocessorClass() {
458  return Preprocessor_Hash::class;
459  }
460 
464  public function firstCallInit() {
465  if ( !$this->mFirstCall ) {
466  return;
467  }
468  $this->mFirstCall = false;
469 
471  CoreTagHooks::register( $this );
472  $this->initializeVariables();
473 
474  // Avoid PHP 7.1 warning from passing $this by reference
475  $parser = $this;
476  Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
477  }
478 
484  public function clearState() {
485  $this->firstCallInit();
486  $this->resetOutput();
487  $this->mAutonumber = 0;
488  $this->mIncludeCount = [];
489  $this->mLinkHolders = new LinkHolderArray( $this );
490  $this->mLinkID = 0;
491  $this->mRevisionObject = $this->mRevisionTimestamp =
492  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
493  $this->mVarCache = [];
494  $this->mUser = null;
495  $this->mLangLinkLanguages = [];
496  $this->currentRevisionCache = null;
497 
498  $this->mStripState = new StripState( $this );
499 
500  # Clear these on every parse, T6549
501  $this->mTplRedirCache = $this->mTplDomCache = [];
502 
503  $this->mShowToc = true;
504  $this->mForceTocPosition = false;
505  $this->mIncludeSizes = [
506  'post-expand' => 0,
507  'arg' => 0,
508  ];
509  $this->mPPNodeCount = 0;
510  $this->mGeneratedPPNodeCount = 0;
511  $this->mHighestExpansionDepth = 0;
512  $this->mDefaultSort = false;
513  $this->mHeadings = [];
514  $this->mDoubleUnderscores = [];
515  $this->mExpensiveFunctionCount = 0;
516 
517  # Fix cloning
518  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
519  $this->mPreprocessor = null;
520  }
521 
522  $this->mProfiler = new SectionProfiler();
523 
524  // Avoid PHP 7.1 warning from passing $this by reference
525  $parser = $this;
526  Hooks::run( 'ParserClearState', [ &$parser ] );
527  }
528 
532  public function resetOutput() {
533  $this->mOutput = new ParserOutput;
534  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
535  }
536 
554  public function parse(
555  $text, Title $title, ParserOptions $options,
556  $linestart = true, $clearState = true, $revid = null
557  ) {
558  if ( $clearState ) {
559  // We use U+007F DELETE to construct strip markers, so we have to make
560  // sure that this character does not occur in the input text.
561  $text = strtr( $text, "\x7f", "?" );
562  $magicScopeVariable = $this->lock();
563  }
564  // Strip U+0000 NULL (T159174)
565  $text = str_replace( "\000", '', $text );
566 
567  $this->startParse( $title, $options, self::OT_HTML, $clearState );
568 
569  $this->currentRevisionCache = null;
570  $this->mInputSize = strlen( $text );
571  if ( $this->mOptions->getEnableLimitReport() ) {
572  $this->mOutput->resetParseStartTime();
573  }
574 
575  $oldRevisionId = $this->mRevisionId;
576  $oldRevisionObject = $this->mRevisionObject;
577  $oldRevisionTimestamp = $this->mRevisionTimestamp;
578  $oldRevisionUser = $this->mRevisionUser;
579  $oldRevisionSize = $this->mRevisionSize;
580  if ( $revid !== null ) {
581  $this->mRevisionId = $revid;
582  $this->mRevisionObject = null;
583  $this->mRevisionTimestamp = null;
584  $this->mRevisionUser = null;
585  $this->mRevisionSize = null;
586  }
587 
588  // Avoid PHP 7.1 warning from passing $this by reference
589  $parser = $this;
590  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
591  # No more strip!
592  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
593  $text = $this->internalParse( $text );
594  Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
595 
596  $text = $this->internalParseHalfParsed( $text, true, $linestart );
597 
605  if ( !( $options->getDisableTitleConversion()
606  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
607  || isset( $this->mDoubleUnderscores['notitleconvert'] )
608  || $this->mOutput->getDisplayTitle() !== false )
609  ) {
610  $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
611  if ( $convruletitle ) {
612  $this->mOutput->setTitleText( $convruletitle );
613  } else {
614  $titleText = $this->getTargetLanguage()->convertTitle( $title );
615  $this->mOutput->setTitleText( $titleText );
616  }
617  }
618 
619  # Compute runtime adaptive expiry if set
620  $this->mOutput->finalizeAdaptiveCacheExpiry();
621 
622  # Warn if too many heavyweight parser functions were used
623  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
624  $this->limitationWarn( 'expensive-parserfunction',
625  $this->mExpensiveFunctionCount,
626  $this->mOptions->getExpensiveParserFunctionLimit()
627  );
628  }
629 
630  # Information on limits, for the benefit of users who try to skirt them
631  if ( $this->mOptions->getEnableLimitReport() ) {
632  $text .= $this->makeLimitReport();
633  }
634 
635  # Wrap non-interface parser output in a <div> so it can be targeted
636  # with CSS (T37247)
637  $class = $this->mOptions->getWrapOutputClass();
638  if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
639  $this->mOutput->addWrapperDivClass( $class );
640  }
641 
642  $this->mOutput->setText( $text );
643 
644  $this->mRevisionId = $oldRevisionId;
645  $this->mRevisionObject = $oldRevisionObject;
646  $this->mRevisionTimestamp = $oldRevisionTimestamp;
647  $this->mRevisionUser = $oldRevisionUser;
648  $this->mRevisionSize = $oldRevisionSize;
649  $this->mInputSize = false;
650  $this->currentRevisionCache = null;
651 
652  return $this->mOutput;
653  }
654 
661  protected function makeLimitReport() {
662  $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
663 
664  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
665  if ( $cpuTime !== null ) {
666  $this->mOutput->setLimitReportData( 'limitreport-cputime',
667  sprintf( "%.3f", $cpuTime )
668  );
669  }
670 
671  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
672  $this->mOutput->setLimitReportData( 'limitreport-walltime',
673  sprintf( "%.3f", $wallTime )
674  );
675 
676  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
677  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
678  );
679  $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
680  [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
681  );
682  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
683  [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
684  );
685  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
686  [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
687  );
688  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
689  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
690  );
691  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
692  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
693  );
694 
695  foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
696  $this->mOutput->setLimitReportData( $key, $value );
697  }
698 
699  Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
700 
701  $limitReport = "NewPP limit report\n";
702  if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
703  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
704  }
705  $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
706  $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
707  $limitReport .= 'Dynamic content: ' .
708  ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
709  "\n";
710  $limitReport .= 'Complications: [' . implode( ', ', $this->mOutput->getAllFlags() ) . "]\n";
711 
712  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
713  if ( Hooks::run( 'ParserLimitReportFormat',
714  [ $key, &$value, &$limitReport, false, false ]
715  ) ) {
716  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
717  $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
718  ->inLanguage( 'en' )->useDatabase( false );
719  if ( !$valueMsg->exists() ) {
720  $valueMsg = new RawMessage( '$1' );
721  }
722  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
723  $valueMsg->params( $value );
724  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
725  }
726  }
727  }
728  // Since we're not really outputting HTML, decode the entities and
729  // then re-encode the things that need hiding inside HTML comments.
730  $limitReport = htmlspecialchars_decode( $limitReport );
731 
732  // Sanitize for comment. Note '‐' in the replacement is U+2010,
733  // which looks much like the problematic '-'.
734  $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
735  $text = "\n<!-- \n$limitReport-->\n";
736 
737  // Add on template profiling data in human/machine readable way
738  $dataByFunc = $this->mProfiler->getFunctionStats();
739  uasort( $dataByFunc, function ( $a, $b ) {
740  return $b['real'] <=> $a['real']; // descending order
741  } );
742  $profileReport = [];
743  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
744  $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
745  $item['%real'], $item['real'], $item['calls'],
746  htmlspecialchars( $item['name'] ) );
747  }
748  $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
749  $text .= implode( "\n", $profileReport ) . "\n-->\n";
750 
751  $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
752 
753  // Add other cache related metadata
754  if ( $this->svcOptions->get( 'ShowHostnames' ) ) {
755  $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
756  }
757  $this->mOutput->setLimitReportData( 'cachereport-timestamp',
758  $this->mOutput->getCacheTime() );
759  $this->mOutput->setLimitReportData( 'cachereport-ttl',
760  $this->mOutput->getCacheExpiry() );
761  $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
762  $this->mOutput->hasDynamicContent() );
763 
764  if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
765  wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
766  $this->mTitle->getPrefixedDBkey() );
767  }
768  return $text;
769  }
770 
795  public function recursiveTagParse( $text, $frame = false ) {
796  // Avoid PHP 7.1 warning from passing $this by reference
797  $parser = $this;
798  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
799  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
800  $text = $this->internalParse( $text, false, $frame );
801  return $text;
802  }
803 
823  public function recursiveTagParseFully( $text, $frame = false ) {
824  $text = $this->recursiveTagParse( $text, $frame );
825  $text = $this->internalParseHalfParsed( $text, false );
826  return $text;
827  }
828 
840  public function preprocess( $text, Title $title = null,
841  ParserOptions $options, $revid = null, $frame = false
842  ) {
843  $magicScopeVariable = $this->lock();
844  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
845  if ( $revid !== null ) {
846  $this->mRevisionId = $revid;
847  }
848  // Avoid PHP 7.1 warning from passing $this by reference
849  $parser = $this;
850  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
851  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
852  $text = $this->replaceVariables( $text, $frame );
853  $text = $this->mStripState->unstripBoth( $text );
854  return $text;
855  }
856 
866  public function recursivePreprocess( $text, $frame = false ) {
867  $text = $this->replaceVariables( $text, $frame );
868  $text = $this->mStripState->unstripBoth( $text );
869  return $text;
870  }
871 
885  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
886  $msg = new RawMessage( $text );
887  $text = $msg->params( $params )->plain();
888 
889  # Parser (re)initialisation
890  $magicScopeVariable = $this->lock();
891  $this->startParse( $title, $options, self::OT_PLAIN, true );
892 
894  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
895  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
896  $text = $this->mStripState->unstripBoth( $text );
897  return $text;
898  }
899 
906  public function setUser( $user ) {
907  $this->mUser = $user;
908  }
909 
915  public function setTitle( Title $t = null ) {
916  if ( !$t ) {
917  $t = Title::makeTitle( NS_SPECIAL, 'Badtitle/Parser' );
918  }
919 
920  if ( $t->hasFragment() ) {
921  # Strip the fragment to avoid various odd effects
922  $this->mTitle = $t->createFragmentTarget( '' );
923  } else {
924  $this->mTitle = $t;
925  }
926  }
927 
935  public function getTitle() : ?Title {
936  if ( $this->mTitle === null ) {
937  wfDeprecated( 'Parser title should never be null', '1.34' );
938  }
939  return $this->mTitle;
940  }
941 
948  public function Title( Title $x = null ) : ?Title {
949  return wfSetVar( $this->mTitle, $x );
950  }
951 
957  public function setOutputType( $ot ) {
958  $this->mOutputType = $ot;
959  # Shortcut alias
960  $this->ot = [
961  'html' => $ot == self::OT_HTML,
962  'wiki' => $ot == self::OT_WIKI,
963  'pre' => $ot == self::OT_PREPROCESS,
964  'plain' => $ot == self::OT_PLAIN,
965  ];
966  }
967 
974  public function OutputType( $x = null ) {
975  return wfSetVar( $this->mOutputType, $x );
976  }
977 
983  public function getOutput() {
984  return $this->mOutput;
985  }
986 
992  public function getOptions() {
993  return $this->mOptions;
994  }
995 
1002  public function Options( $x = null ) {
1003  return wfSetVar( $this->mOptions, $x );
1004  }
1005 
1009  public function nextLinkID() {
1010  return $this->mLinkID++;
1011  }
1012 
1016  public function setLinkID( $id ) {
1017  $this->mLinkID = $id;
1018  }
1019 
1024  public function getFunctionLang() {
1025  return $this->getTargetLanguage();
1026  }
1027 
1037  public function getTargetLanguage() {
1038  $target = $this->mOptions->getTargetLanguage();
1039 
1040  if ( $target !== null ) {
1041  return $target;
1042  } elseif ( $this->mOptions->getInterfaceMessage() ) {
1043  return $this->mOptions->getUserLangObj();
1044  } elseif ( is_null( $this->mTitle ) ) {
1045  throw new MWException( __METHOD__ . ': $this->mTitle is null' );
1046  }
1047 
1048  return $this->mTitle->getPageLanguage();
1049  }
1050 
1056  public function getConverterLanguage() {
1057  wfDeprecated( __METHOD__, '1.32' );
1058  return $this->getTargetLanguage();
1059  }
1060 
1067  public function getUser() {
1068  if ( !is_null( $this->mUser ) ) {
1069  return $this->mUser;
1070  }
1071  return $this->mOptions->getUser();
1072  }
1073 
1079  public function getPreprocessor() {
1080  if ( !isset( $this->mPreprocessor ) ) {
1081  $class = $this->svcOptions->get( 'preprocessorClass' );
1082  $this->mPreprocessor = new $class( $this );
1083  }
1084  return $this->mPreprocessor;
1085  }
1086 
1093  public function getLinkRenderer() {
1094  // XXX We make the LinkRenderer with current options and then cache it forever
1095  if ( !$this->mLinkRenderer ) {
1096  $this->mLinkRenderer = $this->linkRendererFactory->create();
1097  $this->mLinkRenderer->setStubThreshold(
1098  $this->getOptions()->getStubThreshold()
1099  );
1100  }
1101 
1102  return $this->mLinkRenderer;
1103  }
1104 
1111  public function getMagicWordFactory() {
1112  return $this->magicWordFactory;
1113  }
1114 
1121  public function getContentLanguage() {
1122  return $this->contLang;
1123  }
1124 
1144  public static function extractTagsAndParams( $elements, $text, &$matches ) {
1145  static $n = 1;
1146  $stripped = '';
1147  $matches = [];
1148 
1149  $taglist = implode( '|', $elements );
1150  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1151 
1152  while ( $text != '' ) {
1153  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1154  $stripped .= $p[0];
1155  if ( count( $p ) < 5 ) {
1156  break;
1157  }
1158  if ( count( $p ) > 5 ) {
1159  # comment
1160  $element = $p[4];
1161  $attributes = '';
1162  $close = '';
1163  $inside = $p[5];
1164  } else {
1165  # tag
1166  list( , $element, $attributes, $close, $inside ) = $p;
1167  }
1168 
1169  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1170  $stripped .= $marker;
1171 
1172  if ( $close === '/>' ) {
1173  # Empty element tag, <tag />
1174  $content = null;
1175  $text = $inside;
1176  $tail = null;
1177  } else {
1178  if ( $element === '!--' ) {
1179  $end = '/(-->)/';
1180  } else {
1181  $end = "/(<\\/$element\\s*>)/i";
1182  }
1183  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1184  $content = $q[0];
1185  if ( count( $q ) < 3 ) {
1186  # No end tag -- let it run out to the end of the text.
1187  $tail = '';
1188  $text = '';
1189  } else {
1190  list( , $tail, $text ) = $q;
1191  }
1192  }
1193 
1194  $matches[$marker] = [ $element,
1195  $content,
1196  Sanitizer::decodeTagAttributes( $attributes ),
1197  "<$element$attributes$close$content$tail" ];
1198  }
1199  return $stripped;
1200  }
1201 
1207  public function getStripList() {
1208  return $this->mStripList;
1209  }
1210 
1216  public function getStripState() {
1217  return $this->mStripState;
1218  }
1219 
1229  public function insertStripItem( $text ) {
1230  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1231  $this->mMarkerIndex++;
1232  $this->mStripState->addGeneral( $marker, $text );
1233  return $marker;
1234  }
1235 
1244  public function doTableStuff( $text ) {
1245  wfDeprecated( __METHOD__, '1.34' );
1246  return $this->handleTables( $text );
1247  }
1248 
1255  private function handleTables( $text ) {
1256  $lines = StringUtils::explode( "\n", $text );
1257  $out = '';
1258  $td_history = []; # Is currently a td tag open?
1259  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1260  $tr_history = []; # Is currently a tr tag open?
1261  $tr_attributes = []; # history of tr attributes
1262  $has_opened_tr = []; # Did this table open a <tr> element?
1263  $indent_level = 0; # indent level of the table
1264 
1265  foreach ( $lines as $outLine ) {
1266  $line = trim( $outLine );
1267 
1268  if ( $line === '' ) { # empty line, go to next line
1269  $out .= $outLine . "\n";
1270  continue;
1271  }
1272 
1273  $first_character = $line[0];
1274  $first_two = substr( $line, 0, 2 );
1275  $matches = [];
1276 
1277  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1278  # First check if we are starting a new table
1279  $indent_level = strlen( $matches[1] );
1280 
1281  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1282  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1283 
1284  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1285  array_push( $td_history, false );
1286  array_push( $last_tag_history, '' );
1287  array_push( $tr_history, false );
1288  array_push( $tr_attributes, '' );
1289  array_push( $has_opened_tr, false );
1290  } elseif ( count( $td_history ) == 0 ) {
1291  # Don't do any of the following
1292  $out .= $outLine . "\n";
1293  continue;
1294  } elseif ( $first_two === '|}' ) {
1295  # We are ending a table
1296  $line = '</table>' . substr( $line, 2 );
1297  $last_tag = array_pop( $last_tag_history );
1298 
1299  if ( !array_pop( $has_opened_tr ) ) {
1300  $line = "<tr><td></td></tr>{$line}";
1301  }
1302 
1303  if ( array_pop( $tr_history ) ) {
1304  $line = "</tr>{$line}";
1305  }
1306 
1307  if ( array_pop( $td_history ) ) {
1308  $line = "</{$last_tag}>{$line}";
1309  }
1310  array_pop( $tr_attributes );
1311  if ( $indent_level > 0 ) {
1312  $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1313  } else {
1314  $outLine = $line;
1315  }
1316  } elseif ( $first_two === '|-' ) {
1317  # Now we have a table row
1318  $line = preg_replace( '#^\|-+#', '', $line );
1319 
1320  # Whats after the tag is now only attributes
1321  $attributes = $this->mStripState->unstripBoth( $line );
1322  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1323  array_pop( $tr_attributes );
1324  array_push( $tr_attributes, $attributes );
1325 
1326  $line = '';
1327  $last_tag = array_pop( $last_tag_history );
1328  array_pop( $has_opened_tr );
1329  array_push( $has_opened_tr, true );
1330 
1331  if ( array_pop( $tr_history ) ) {
1332  $line = '</tr>';
1333  }
1334 
1335  if ( array_pop( $td_history ) ) {
1336  $line = "</{$last_tag}>{$line}";
1337  }
1338 
1339  $outLine = $line;
1340  array_push( $tr_history, false );
1341  array_push( $td_history, false );
1342  array_push( $last_tag_history, '' );
1343  } elseif ( $first_character === '|'
1344  || $first_character === '!'
1345  || $first_two === '|+'
1346  ) {
1347  # This might be cell elements, td, th or captions
1348  if ( $first_two === '|+' ) {
1349  $first_character = '+';
1350  $line = substr( $line, 2 );
1351  } else {
1352  $line = substr( $line, 1 );
1353  }
1354 
1355  // Implies both are valid for table headings.
1356  if ( $first_character === '!' ) {
1357  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1358  }
1359 
1360  # Split up multiple cells on the same line.
1361  # FIXME : This can result in improper nesting of tags processed
1362  # by earlier parser steps.
1363  $cells = explode( '||', $line );
1364 
1365  $outLine = '';
1366 
1367  # Loop through each table cell
1368  foreach ( $cells as $cell ) {
1369  $previous = '';
1370  if ( $first_character !== '+' ) {
1371  $tr_after = array_pop( $tr_attributes );
1372  if ( !array_pop( $tr_history ) ) {
1373  $previous = "<tr{$tr_after}>\n";
1374  }
1375  array_push( $tr_history, true );
1376  array_push( $tr_attributes, '' );
1377  array_pop( $has_opened_tr );
1378  array_push( $has_opened_tr, true );
1379  }
1380 
1381  $last_tag = array_pop( $last_tag_history );
1382 
1383  if ( array_pop( $td_history ) ) {
1384  $previous = "</{$last_tag}>\n{$previous}";
1385  }
1386 
1387  if ( $first_character === '|' ) {
1388  $last_tag = 'td';
1389  } elseif ( $first_character === '!' ) {
1390  $last_tag = 'th';
1391  } elseif ( $first_character === '+' ) {
1392  $last_tag = 'caption';
1393  } else {
1394  $last_tag = '';
1395  }
1396 
1397  array_push( $last_tag_history, $last_tag );
1398 
1399  # A cell could contain both parameters and data
1400  $cell_data = explode( '|', $cell, 2 );
1401 
1402  # T2553: Note that a '|' inside an invalid link should not
1403  # be mistaken as delimiting cell parameters
1404  # Bug T153140: Neither should language converter markup.
1405  if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1406  $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1407  } elseif ( count( $cell_data ) == 1 ) {
1408  // Whitespace in cells is trimmed
1409  $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1410  } else {
1411  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1412  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1413  // Whitespace in cells is trimmed
1414  $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1415  }
1416 
1417  $outLine .= $cell;
1418  array_push( $td_history, true );
1419  }
1420  }
1421  $out .= $outLine . "\n";
1422  }
1423 
1424  # Closing open td, tr && table
1425  while ( count( $td_history ) > 0 ) {
1426  if ( array_pop( $td_history ) ) {
1427  $out .= "</td>\n";
1428  }
1429  if ( array_pop( $tr_history ) ) {
1430  $out .= "</tr>\n";
1431  }
1432  if ( !array_pop( $has_opened_tr ) ) {
1433  $out .= "<tr><td></td></tr>\n";
1434  }
1435 
1436  $out .= "</table>\n";
1437  }
1438 
1439  # Remove trailing line-ending (b/c)
1440  if ( substr( $out, -1 ) === "\n" ) {
1441  $out = substr( $out, 0, -1 );
1442  }
1443 
1444  # special case: don't return empty table
1445  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1446  $out = '';
1447  }
1448 
1449  return $out;
1450  }
1451 
1465  public function internalParse( $text, $isMain = true, $frame = false ) {
1466  $origText = $text;
1467 
1468  // Avoid PHP 7.1 warning from passing $this by reference
1469  $parser = $this;
1470 
1471  # Hook to suspend the parser in this state
1472  if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1473  return $text;
1474  }
1475 
1476  # if $frame is provided, then use $frame for replacing any variables
1477  if ( $frame ) {
1478  # use frame depth to infer how include/noinclude tags should be handled
1479  # depth=0 means this is the top-level document; otherwise it's an included document
1480  if ( !$frame->depth ) {
1481  $flag = 0;
1482  } else {
1483  $flag = self::PTD_FOR_INCLUSION;
1484  }
1485  $dom = $this->preprocessToDom( $text, $flag );
1486  $text = $frame->expand( $dom );
1487  } else {
1488  # if $frame is not provided, then use old-style replaceVariables
1489  $text = $this->replaceVariables( $text );
1490  }
1491 
1492  Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1493  $text = Sanitizer::removeHTMLtags(
1494  $text,
1495  [ $this, 'attributeStripCallback' ],
1496  false,
1497  array_keys( $this->mTransparentTagHooks ),
1498  [],
1499  [ $this, 'addTrackingCategory' ]
1500  );
1501  Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1502 
1503  # Tables need to come after variable replacement for things to work
1504  # properly; putting them before other transformations should keep
1505  # exciting things like link expansions from showing up in surprising
1506  # places.
1507  $text = $this->handleTables( $text );
1508 
1509  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1510 
1511  $text = $this->handleDoubleUnderscore( $text );
1512 
1513  $text = $this->handleHeadings( $text );
1514  $text = $this->handleInternalLinks( $text );
1515  $text = $this->handleAllQuotes( $text );
1516  $text = $this->handleExternalLinks( $text );
1517 
1518  # handleInternalLinks may sometimes leave behind
1519  # absolute URLs, which have to be masked to hide them from handleExternalLinks
1520  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1521 
1522  $text = $this->handleMagicLinks( $text );
1523  $text = $this->finalizeHeadings( $text, $origText, $isMain );
1524 
1525  return $text;
1526  }
1527 
1537  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1538  $text = $this->mStripState->unstripGeneral( $text );
1539 
1540  // Avoid PHP 7.1 warning from passing $this by reference
1541  $parser = $this;
1542 
1543  if ( $isMain ) {
1544  Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1545  }
1546 
1547  # Clean up special characters, only run once, next-to-last before doBlockLevels
1548  $text = Sanitizer::armorFrenchSpaces( $text );
1549 
1550  $text = $this->doBlockLevels( $text, $linestart );
1551 
1552  $this->replaceLinkHoldersPrivate( $text );
1553 
1561  if ( !( $this->mOptions->getDisableContentConversion()
1562  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1563  && !$this->mOptions->getInterfaceMessage()
1564  ) {
1565  # The position of the convert() call should not be changed. it
1566  # assumes that the links are all replaced and the only thing left
1567  # is the <nowiki> mark.
1568  $text = $this->getTargetLanguage()->convert( $text );
1569  }
1570 
1571  $text = $this->mStripState->unstripNoWiki( $text );
1572 
1573  if ( $isMain ) {
1574  Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1575  }
1576 
1577  $text = $this->replaceTransparentTags( $text );
1578  $text = $this->mStripState->unstripGeneral( $text );
1579 
1580  $text = Sanitizer::normalizeCharReferences( $text );
1581 
1582  if ( MWTidy::isEnabled() ) {
1583  if ( $this->mOptions->getTidy() ) {
1584  $text = MWTidy::tidy( $text );
1585  }
1586  } else {
1587  # attempt to sanitize at least some nesting problems
1588  # (T4702 and quite a few others)
1589  # This code path is buggy and deprecated!
1590  wfDeprecated( 'disabling tidy', '1.33' );
1591  $tidyregs = [
1592  # ''Something [http://www.cool.com cool''] -->
1593  # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1594  '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1595  '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1596  # fix up an anchor inside another anchor, only
1597  # at least for a single single nested link (T5695)
1598  '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1599  '\\1\\2</a>\\3</a>\\1\\4</a>',
1600  # fix div inside inline elements- doBlockLevels won't wrap a line which
1601  # contains a div, so fix it up here; replace
1602  # div with escaped text
1603  '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1604  '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1605  # remove empty italic or bold tag pairs, some
1606  # introduced by rules above
1607  '/<([bi])><\/\\1>/' => '',
1608  ];
1609 
1610  $text = preg_replace(
1611  array_keys( $tidyregs ),
1612  array_values( $tidyregs ),
1613  $text );
1614  }
1615 
1616  if ( $isMain ) {
1617  Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1618  }
1619 
1620  return $text;
1621  }
1622 
1633  public function doMagicLinks( $text ) {
1634  wfDeprecated( __METHOD__, '1.34' );
1635  return $this->handleMagicLinks( $text );
1636  }
1637 
1648  private function handleMagicLinks( $text ) {
1649  $prots = wfUrlProtocolsWithoutProtRel();
1650  $urlChar = self::EXT_LINK_URL_CLASS;
1651  $addr = self::EXT_LINK_ADDR;
1652  $space = self::SPACE_NOT_NL; # non-newline space
1653  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1654  $spaces = "$space++"; # possessive match of 1 or more spaces
1655  $text = preg_replace_callback(
1656  '!(?: # Start cases
1657  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1658  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1659  (\b # m[3]: Free external links
1660  (?i:$prots)
1661  ($addr$urlChar*) # m[4]: Post-protocol path
1662  ) |
1663  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1664  ([0-9]+)\b |
1665  \bISBN $spaces ( # m[6]: ISBN, capture number
1666  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1667  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1668  [0-9Xx] # check digit
1669  )\b
1670  )!xu", [ $this, 'magicLinkCallback' ], $text );
1671  return $text;
1672  }
1673 
1679  public function magicLinkCallback( $m ) {
1680  if ( isset( $m[1] ) && $m[1] !== '' ) {
1681  # Skip anchor
1682  return $m[0];
1683  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1684  # Skip HTML element
1685  return $m[0];
1686  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1687  # Free external link
1688  return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1689  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1690  # RFC or PMID
1691  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1692  if ( !$this->mOptions->getMagicRFCLinks() ) {
1693  return $m[0];
1694  }
1695  $keyword = 'RFC';
1696  $urlmsg = 'rfcurl';
1697  $cssClass = 'mw-magiclink-rfc';
1698  $trackingCat = 'magiclink-tracking-rfc';
1699  $id = $m[5];
1700  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1701  if ( !$this->mOptions->getMagicPMIDLinks() ) {
1702  return $m[0];
1703  }
1704  $keyword = 'PMID';
1705  $urlmsg = 'pubmedurl';
1706  $cssClass = 'mw-magiclink-pmid';
1707  $trackingCat = 'magiclink-tracking-pmid';
1708  $id = $m[5];
1709  } else {
1710  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1711  substr( $m[0], 0, 20 ) . '"' );
1712  }
1713  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1714  $this->addTrackingCategory( $trackingCat );
1715  return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1716  } elseif ( isset( $m[6] ) && $m[6] !== ''
1717  && $this->mOptions->getMagicISBNLinks()
1718  ) {
1719  # ISBN
1720  $isbn = $m[6];
1721  $space = self::SPACE_NOT_NL; # non-newline space
1722  $isbn = preg_replace( "/$space/", ' ', $isbn );
1723  $num = strtr( $isbn, [
1724  '-' => '',
1725  ' ' => '',
1726  'x' => 'X',
1727  ] );
1728  $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1729  return $this->getLinkRenderer()->makeKnownLink(
1730  SpecialPage::getTitleFor( 'Booksources', $num ),
1731  "ISBN $isbn",
1732  [
1733  'class' => 'internal mw-magiclink-isbn',
1734  'title' => false // suppress title attribute
1735  ]
1736  );
1737  } else {
1738  return $m[0];
1739  }
1740  }
1741 
1751  public function makeFreeExternalLink( $url, $numPostProto ) {
1752  $trail = '';
1753 
1754  # The characters '<' and '>' (which were escaped by
1755  # removeHTMLtags()) should not be included in
1756  # URLs, per RFC 2396.
1757  # Make &nbsp; terminate a URL as well (bug T84937)
1758  $m2 = [];
1759  if ( preg_match(
1760  '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1761  $url,
1762  $m2,
1763  PREG_OFFSET_CAPTURE
1764  ) ) {
1765  $trail = substr( $url, $m2[0][1] ) . $trail;
1766  $url = substr( $url, 0, $m2[0][1] );
1767  }
1768 
1769  # Move trailing punctuation to $trail
1770  $sep = ',;\.:!?';
1771  # If there is no left bracket, then consider right brackets fair game too
1772  if ( strpos( $url, '(' ) === false ) {
1773  $sep .= ')';
1774  }
1775 
1776  $urlRev = strrev( $url );
1777  $numSepChars = strspn( $urlRev, $sep );
1778  # Don't break a trailing HTML entity by moving the ; into $trail
1779  # This is in hot code, so use substr_compare to avoid having to
1780  # create a new string object for the comparison
1781  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1782  # more optimization: instead of running preg_match with a $
1783  # anchor, which can be slow, do the match on the reversed
1784  # string starting at the desired offset.
1785  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1786  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1787  $numSepChars--;
1788  }
1789  }
1790  if ( $numSepChars ) {
1791  $trail = substr( $url, -$numSepChars ) . $trail;
1792  $url = substr( $url, 0, -$numSepChars );
1793  }
1794 
1795  # Verify that we still have a real URL after trail removal, and
1796  # not just lone protocol
1797  if ( strlen( $trail ) >= $numPostProto ) {
1798  return $url . $trail;
1799  }
1800 
1801  $url = Sanitizer::cleanUrl( $url );
1802 
1803  # Is this an external image?
1804  $text = $this->maybeMakeExternalImage( $url );
1805  if ( $text === false ) {
1806  # Not an image, make a link
1807  $text = Linker::makeExternalLink( $url,
1808  $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1809  true, 'free',
1810  $this->getExternalLinkAttribs( $url ), $this->mTitle );
1811  # Register it in the output object...
1812  $this->mOutput->addExternalLink( $url );
1813  }
1814  return $text . $trail;
1815  }
1816 
1825  public function doHeadings( $text ) {
1826  wfDeprecated( __METHOD__, '1.34' );
1827  return $this->handleHeadings( $text );
1828  }
1829 
1836  private function handleHeadings( $text ) {
1837  for ( $i = 6; $i >= 1; --$i ) {
1838  $h = str_repeat( '=', $i );
1839  // Trim non-newline whitespace from headings
1840  // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1841  $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1842  }
1843  return $text;
1844  }
1845 
1855  public function doAllQuotes( $text ) {
1856  wfDeprecated( __METHOD__, '1.34' );
1857  return $this->handleAllQuotes( $text );
1858  }
1859 
1867  private function handleAllQuotes( $text ) {
1868  $outtext = '';
1869  $lines = StringUtils::explode( "\n", $text );
1870  foreach ( $lines as $line ) {
1871  $outtext .= $this->doQuotes( $line ) . "\n";
1872  }
1873  $outtext = substr( $outtext, 0, -1 );
1874  return $outtext;
1875  }
1876 
1885  public function doQuotes( $text ) {
1886  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1887  $countarr = count( $arr );
1888  if ( $countarr == 1 ) {
1889  return $text;
1890  }
1891 
1892  // First, do some preliminary work. This may shift some apostrophes from
1893  // being mark-up to being text. It also counts the number of occurrences
1894  // of bold and italics mark-ups.
1895  $numbold = 0;
1896  $numitalics = 0;
1897  for ( $i = 1; $i < $countarr; $i += 2 ) {
1898  $thislen = strlen( $arr[$i] );
1899  // If there are ever four apostrophes, assume the first is supposed to
1900  // be text, and the remaining three constitute mark-up for bold text.
1901  // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1902  if ( $thislen == 4 ) {
1903  $arr[$i - 1] .= "'";
1904  $arr[$i] = "'''";
1905  $thislen = 3;
1906  } elseif ( $thislen > 5 ) {
1907  // If there are more than 5 apostrophes in a row, assume they're all
1908  // text except for the last 5.
1909  // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1910  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1911  $arr[$i] = "'''''";
1912  $thislen = 5;
1913  }
1914  // Count the number of occurrences of bold and italics mark-ups.
1915  if ( $thislen == 2 ) {
1916  $numitalics++;
1917  } elseif ( $thislen == 3 ) {
1918  $numbold++;
1919  } elseif ( $thislen == 5 ) {
1920  $numitalics++;
1921  $numbold++;
1922  }
1923  }
1924 
1925  // If there is an odd number of both bold and italics, it is likely
1926  // that one of the bold ones was meant to be an apostrophe followed
1927  // by italics. Which one we cannot know for certain, but it is more
1928  // likely to be one that has a single-letter word before it.
1929  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1930  $firstsingleletterword = -1;
1931  $firstmultiletterword = -1;
1932  $firstspace = -1;
1933  for ( $i = 1; $i < $countarr; $i += 2 ) {
1934  if ( strlen( $arr[$i] ) == 3 ) {
1935  $x1 = substr( $arr[$i - 1], -1 );
1936  $x2 = substr( $arr[$i - 1], -2, 1 );
1937  if ( $x1 === ' ' ) {
1938  if ( $firstspace == -1 ) {
1939  $firstspace = $i;
1940  }
1941  } elseif ( $x2 === ' ' ) {
1942  $firstsingleletterword = $i;
1943  // if $firstsingleletterword is set, we don't
1944  // look at the other options, so we can bail early.
1945  break;
1946  } elseif ( $firstmultiletterword == -1 ) {
1947  $firstmultiletterword = $i;
1948  }
1949  }
1950  }
1951 
1952  // If there is a single-letter word, use it!
1953  if ( $firstsingleletterword > -1 ) {
1954  $arr[$firstsingleletterword] = "''";
1955  $arr[$firstsingleletterword - 1] .= "'";
1956  } elseif ( $firstmultiletterword > -1 ) {
1957  // If not, but there's a multi-letter word, use that one.
1958  $arr[$firstmultiletterword] = "''";
1959  $arr[$firstmultiletterword - 1] .= "'";
1960  } elseif ( $firstspace > -1 ) {
1961  // ... otherwise use the first one that has neither.
1962  // (notice that it is possible for all three to be -1 if, for example,
1963  // there is only one pentuple-apostrophe in the line)
1964  $arr[$firstspace] = "''";
1965  $arr[$firstspace - 1] .= "'";
1966  }
1967  }
1968 
1969  // Now let's actually convert our apostrophic mush to HTML!
1970  $output = '';
1971  $buffer = '';
1972  $state = '';
1973  $i = 0;
1974  foreach ( $arr as $r ) {
1975  if ( ( $i % 2 ) == 0 ) {
1976  if ( $state === 'both' ) {
1977  $buffer .= $r;
1978  } else {
1979  $output .= $r;
1980  }
1981  } else {
1982  $thislen = strlen( $r );
1983  if ( $thislen == 2 ) {
1984  if ( $state === 'i' ) {
1985  $output .= '</i>';
1986  $state = '';
1987  } elseif ( $state === 'bi' ) {
1988  $output .= '</i>';
1989  $state = 'b';
1990  } elseif ( $state === 'ib' ) {
1991  $output .= '</b></i><b>';
1992  $state = 'b';
1993  } elseif ( $state === 'both' ) {
1994  $output .= '<b><i>' . $buffer . '</i>';
1995  $state = 'b';
1996  } else { // $state can be 'b' or ''
1997  $output .= '<i>';
1998  $state .= 'i';
1999  }
2000  } elseif ( $thislen == 3 ) {
2001  if ( $state === 'b' ) {
2002  $output .= '</b>';
2003  $state = '';
2004  } elseif ( $state === 'bi' ) {
2005  $output .= '</i></b><i>';
2006  $state = 'i';
2007  } elseif ( $state === 'ib' ) {
2008  $output .= '</b>';
2009  $state = 'i';
2010  } elseif ( $state === 'both' ) {
2011  $output .= '<i><b>' . $buffer . '</b>';
2012  $state = 'i';
2013  } else { // $state can be 'i' or ''
2014  $output .= '<b>';
2015  $state .= 'b';
2016  }
2017  } elseif ( $thislen == 5 ) {
2018  if ( $state === 'b' ) {
2019  $output .= '</b><i>';
2020  $state = 'i';
2021  } elseif ( $state === 'i' ) {
2022  $output .= '</i><b>';
2023  $state = 'b';
2024  } elseif ( $state === 'bi' ) {
2025  $output .= '</i></b>';
2026  $state = '';
2027  } elseif ( $state === 'ib' ) {
2028  $output .= '</b></i>';
2029  $state = '';
2030  } elseif ( $state === 'both' ) {
2031  $output .= '<i><b>' . $buffer . '</b></i>';
2032  $state = '';
2033  } else { // ($state == '')
2034  $buffer = '';
2035  $state = 'both';
2036  }
2037  }
2038  }
2039  $i++;
2040  }
2041  // Now close all remaining tags. Notice that the order is important.
2042  if ( $state === 'b' || $state === 'ib' ) {
2043  $output .= '</b>';
2044  }
2045  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
2046  $output .= '</i>';
2047  }
2048  if ( $state === 'bi' ) {
2049  $output .= '</b>';
2050  }
2051  // There might be lonely ''''', so make sure we have a buffer
2052  if ( $state === 'both' && $buffer ) {
2053  $output .= '<b><i>' . $buffer . '</i></b>';
2054  }
2055  return $output;
2056  }
2057 
2071  public function replaceExternalLinks( $text ) {
2072  wfDeprecated( __METHOD__, '1.34' );
2073  return $this->handleExternalLinks( $text );
2074  }
2075 
2086  private function handleExternalLinks( $text ) {
2087  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
2088  // @phan-suppress-next-line PhanTypeComparisonFromArray See phan issue #3161
2089  if ( $bits === false ) {
2090  throw new MWException( "PCRE needs to be compiled with "
2091  . "--enable-unicode-properties in order for MediaWiki to function" );
2092  }
2093  $s = array_shift( $bits );
2094 
2095  $i = 0;
2096  while ( $i < count( $bits ) ) {
2097  $url = $bits[$i++];
2098  $i++; // protocol
2099  $text = $bits[$i++];
2100  $trail = $bits[$i++];
2101 
2102  # The characters '<' and '>' (which were escaped by
2103  # removeHTMLtags()) should not be included in
2104  # URLs, per RFC 2396.
2105  $m2 = [];
2106  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
2107  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
2108  $url = substr( $url, 0, $m2[0][1] );
2109  }
2110 
2111  # If the link text is an image URL, replace it with an <img> tag
2112  # This happened by accident in the original parser, but some people used it extensively
2113  $img = $this->maybeMakeExternalImage( $text );
2114  if ( $img !== false ) {
2115  $text = $img;
2116  }
2117 
2118  $dtrail = '';
2119 
2120  # Set linktype for CSS
2121  $linktype = 'text';
2122 
2123  # No link text, e.g. [http://domain.tld/some.link]
2124  if ( $text == '' ) {
2125  # Autonumber
2126  $langObj = $this->getTargetLanguage();
2127  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
2128  $linktype = 'autonumber';
2129  } else {
2130  # Have link text, e.g. [http://domain.tld/some.link text]s
2131  # Check for trail
2132  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
2133  }
2134 
2135  // Excluding protocol-relative URLs may avoid many false positives.
2136  if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
2137  $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
2138  }
2139 
2140  $url = Sanitizer::cleanUrl( $url );
2141 
2142  # Use the encoded URL
2143  # This means that users can paste URLs directly into the text
2144  # Funny characters like ö aren't valid in URLs anyway
2145  # This was changed in August 2004
2146  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
2147  $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
2148 
2149  # Register link in the output object.
2150  $this->mOutput->addExternalLink( $url );
2151  }
2152 
2153  return $s;
2154  }
2155 
2166  public static function getExternalLinkRel( $url = false, $title = null ) {
2168  $ns = $title ? $title->getNamespace() : false;
2169  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
2171  ) {
2172  return 'nofollow';
2173  }
2174  return null;
2175  }
2176 
2188  public function getExternalLinkAttribs( $url ) {
2189  $attribs = [];
2190  $rel = self::getExternalLinkRel( $url, $this->mTitle );
2191 
2192  $target = $this->mOptions->getExternalLinkTarget();
2193  if ( $target ) {
2194  $attribs['target'] = $target;
2195  if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2196  // T133507. New windows can navigate parent cross-origin.
2197  // Including noreferrer due to lacking browser
2198  // support of noopener. Eventually noreferrer should be removed.
2199  if ( $rel !== '' ) {
2200  $rel .= ' ';
2201  }
2202  $rel .= 'noreferrer noopener';
2203  }
2204  }
2205  $attribs['rel'] = $rel;
2206  return $attribs;
2207  }
2208 
2219  public static function normalizeLinkUrl( $url ) {
2220  # Test for RFC 3986 IPv6 syntax
2221  $scheme = '[a-z][a-z0-9+.-]*:';
2222  $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2223  $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2224  if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2225  IP::isValid( rawurldecode( $m[1] ) )
2226  ) {
2227  $isIPv6 = rawurldecode( $m[1] );
2228  } else {
2229  $isIPv6 = false;
2230  }
2231 
2232  # Make sure unsafe characters are encoded
2233  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2234  function ( $m ) {
2235  return rawurlencode( $m[0] );
2236  },
2237  $url
2238  );
2239 
2240  $ret = '';
2241  $end = strlen( $url );
2242 
2243  # Fragment part - 'fragment'
2244  $start = strpos( $url, '#' );
2245  if ( $start !== false && $start < $end ) {
2246  $ret = self::normalizeUrlComponent(
2247  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2248  $end = $start;
2249  }
2250 
2251  # Query part - 'query' minus &=+;
2252  $start = strpos( $url, '?' );
2253  if ( $start !== false && $start < $end ) {
2254  $ret = self::normalizeUrlComponent(
2255  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2256  $end = $start;
2257  }
2258 
2259  # Scheme and path part - 'pchar'
2260  # (we assume no userinfo or encoded colons in the host)
2261  $ret = self::normalizeUrlComponent(
2262  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2263 
2264  # Fix IPv6 syntax
2265  if ( $isIPv6 !== false ) {
2266  $ipv6Host = "%5B({$isIPv6})%5D";
2267  $ret = preg_replace(
2268  "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2269  "$1[$2]",
2270  $ret
2271  );
2272  }
2273 
2274  return $ret;
2275  }
2276 
2277  private static function normalizeUrlComponent( $component, $unsafe ) {
2278  $callback = function ( $matches ) use ( $unsafe ) {
2279  $char = urldecode( $matches[0] );
2280  $ord = ord( $char );
2281  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2282  # Unescape it
2283  return $char;
2284  } else {
2285  # Leave it escaped, but use uppercase for a-f
2286  return strtoupper( $matches[0] );
2287  }
2288  };
2289  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2290  }
2291 
2300  private function maybeMakeExternalImage( $url ) {
2301  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2302  $imagesexception = !empty( $imagesfrom );
2303  $text = false;
2304  # $imagesfrom could be either a single string or an array of strings, parse out the latter
2305  if ( $imagesexception && is_array( $imagesfrom ) ) {
2306  $imagematch = false;
2307  foreach ( $imagesfrom as $match ) {
2308  if ( strpos( $url, $match ) === 0 ) {
2309  $imagematch = true;
2310  break;
2311  }
2312  }
2313  } elseif ( $imagesexception ) {
2314  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2315  } else {
2316  $imagematch = false;
2317  }
2318 
2319  if ( $this->mOptions->getAllowExternalImages()
2320  || ( $imagesexception && $imagematch )
2321  ) {
2322  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2323  # Image found
2324  $text = Linker::makeExternalImage( $url );
2325  }
2326  }
2327  if ( !$text && $this->mOptions->getEnableImageWhitelist()
2328  && preg_match( self::EXT_IMAGE_REGEX, $url )
2329  ) {
2330  $whitelist = explode(
2331  "\n",
2332  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2333  );
2334 
2335  foreach ( $whitelist as $entry ) {
2336  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2337  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2338  continue;
2339  }
2340  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2341  # Image matches a whitelist entry
2342  $text = Linker::makeExternalImage( $url );
2343  break;
2344  }
2345  }
2346  }
2347  return $text;
2348  }
2349 
2360  public function replaceInternalLinks( $text ) {
2361  wfDeprecated( __METHOD__, '1.34' );
2362  return $this->handleInternalLinks( $text );
2363  }
2364 
2372  private function handleInternalLinks( $text ) {
2373  $this->mLinkHolders->merge( $this->handleInternalLinks2( $text ) );
2374  return $text;
2375  }
2376 
2386  public function replaceInternalLinks2( &$text ) {
2387  wfDeprecated( __METHOD__, '1.34' );
2388  return $this->handleInternalLinks2( $text );
2389  }
2390 
2397  private function handleInternalLinks2( &$s ) {
2398  static $tc = false, $e1, $e1_img;
2399  # the % is needed to support urlencoded titles as well
2400  if ( !$tc ) {
2401  $tc = Title::legalChars() . '#%';
2402  # Match a link having the form [[namespace:link|alternate]]trail
2403  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2404  # Match cases where there is no "]]", which might still be images
2405  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2406  }
2407 
2408  $holders = new LinkHolderArray( $this );
2409 
2410  # split the entire text string on occurrences of [[
2411  $a = StringUtils::explode( '[[', ' ' . $s );
2412  # get the first element (all text up to first [[), and remove the space we added
2413  $s = $a->current();
2414  $a->next();
2415  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2416  $s = substr( $s, 1 );
2417 
2418  if ( is_null( $this->mTitle ) ) {
2419  throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2420  }
2421  $nottalk = !$this->mTitle->isTalkPage();
2422 
2423  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2424  $e2 = null;
2425  if ( $useLinkPrefixExtension ) {
2426  # Match the end of a line for a word that's not followed by whitespace,
2427  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2428  $charset = $this->contLang->linkPrefixCharset();
2429  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2430  $m = [];
2431  if ( preg_match( $e2, $s, $m ) ) {
2432  $first_prefix = $m[2];
2433  } else {
2434  $first_prefix = false;
2435  }
2436  } else {
2437  $prefix = '';
2438  }
2439 
2440  # Some namespaces don't allow subpages
2441  $useSubpages = $this->nsInfo->hasSubpages(
2442  $this->mTitle->getNamespace()
2443  );
2444 
2445  # Loop for each link
2446  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2447  # Check for excessive memory usage
2448  if ( $holders->isBig() ) {
2449  # Too big
2450  # Do the existence check, replace the link holders and clear the array
2451  $holders->replace( $s );
2452  $holders->clear();
2453  }
2454 
2455  if ( $useLinkPrefixExtension ) {
2456  if ( preg_match( $e2, $s, $m ) ) {
2457  list( , $s, $prefix ) = $m;
2458  } else {
2459  $prefix = '';
2460  }
2461  # first link
2462  if ( $first_prefix ) {
2463  $prefix = $first_prefix;
2464  $first_prefix = false;
2465  }
2466  }
2467 
2468  $might_be_img = false;
2469 
2470  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2471  $text = $m[2];
2472  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2473  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2474  # the real problem is with the $e1 regex
2475  # See T1500.
2476  # Still some problems for cases where the ] is meant to be outside punctuation,
2477  # and no image is in sight. See T4095.
2478  if ( $text !== ''
2479  && substr( $m[3], 0, 1 ) === ']'
2480  && strpos( $text, '[' ) !== false
2481  ) {
2482  $text .= ']'; # so that handleExternalLinks($text) works later
2483  $m[3] = substr( $m[3], 1 );
2484  }
2485  # fix up urlencoded title texts
2486  if ( strpos( $m[1], '%' ) !== false ) {
2487  # Should anchors '#' also be rejected?
2488  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2489  }
2490  $trail = $m[3];
2491  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2492  # Invalid, but might be an image with a link in its caption
2493  $might_be_img = true;
2494  $text = $m[2];
2495  if ( strpos( $m[1], '%' ) !== false ) {
2496  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2497  }
2498  $trail = "";
2499  } else { # Invalid form; output directly
2500  $s .= $prefix . '[[' . $line;
2501  continue;
2502  }
2503 
2504  $origLink = ltrim( $m[1], ' ' );
2505 
2506  # Don't allow internal links to pages containing
2507  # PROTO: where PROTO is a valid URL protocol; these
2508  # should be external links.
2509  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2510  $s .= $prefix . '[[' . $line;
2511  continue;
2512  }
2513 
2514  # Make subpage if necessary
2515  if ( $useSubpages ) {
2517  $this->mTitle, $origLink, $text
2518  );
2519  } else {
2520  $link = $origLink;
2521  }
2522 
2523  // \x7f isn't a default legal title char, so most likely strip
2524  // markers will force us into the "invalid form" path above. But,
2525  // just in case, let's assert that xmlish tags aren't valid in
2526  // the title position.
2527  $unstrip = $this->mStripState->killMarkers( $link );
2528  $noMarkers = ( $unstrip === $link );
2529 
2530  $nt = $noMarkers ? Title::newFromText( $link ) : null;
2531  if ( $nt === null ) {
2532  $s .= $prefix . '[[' . $line;
2533  continue;
2534  }
2535 
2536  $ns = $nt->getNamespace();
2537  $iw = $nt->getInterwiki();
2538 
2539  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2540 
2541  if ( $might_be_img ) { # if this is actually an invalid link
2542  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2543  $found = false;
2544  while ( true ) {
2545  # look at the next 'line' to see if we can close it there
2546  $a->next();
2547  $next_line = $a->current();
2548  if ( $next_line === false || $next_line === null ) {
2549  break;
2550  }
2551  $m = explode( ']]', $next_line, 3 );
2552  if ( count( $m ) == 3 ) {
2553  # the first ]] closes the inner link, the second the image
2554  $found = true;
2555  $text .= "[[{$m[0]}]]{$m[1]}";
2556  $trail = $m[2];
2557  break;
2558  } elseif ( count( $m ) == 2 ) {
2559  # if there's exactly one ]] that's fine, we'll keep looking
2560  $text .= "[[{$m[0]}]]{$m[1]}";
2561  } else {
2562  # if $next_line is invalid too, we need look no further
2563  $text .= '[[' . $next_line;
2564  break;
2565  }
2566  }
2567  if ( !$found ) {
2568  # we couldn't find the end of this imageLink, so output it raw
2569  # but don't ignore what might be perfectly normal links in the text we've examined
2570  $holders->merge( $this->handleInternalLinks2( $text ) );
2571  $s .= "{$prefix}[[$link|$text";
2572  # note: no $trail, because without an end, there *is* no trail
2573  continue;
2574  }
2575  } else { # it's not an image, so output it raw
2576  $s .= "{$prefix}[[$link|$text";
2577  # note: no $trail, because without an end, there *is* no trail
2578  continue;
2579  }
2580  }
2581 
2582  $wasblank = ( $text == '' );
2583  if ( $wasblank ) {
2584  $text = $link;
2585  if ( !$noforce ) {
2586  # Strip off leading ':'
2587  $text = substr( $text, 1 );
2588  }
2589  } else {
2590  # T6598 madness. Handle the quotes only if they come from the alternate part
2591  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2592  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2593  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2594  $text = $this->doQuotes( $text );
2595  }
2596 
2597  # Link not escaped by : , create the various objects
2598  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2599  # Interwikis
2600  if (
2601  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2602  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2603  in_array( $iw, $this->svcOptions->get( 'ExtraInterlanguageLinkPrefixes' ) )
2604  )
2605  ) {
2606  # T26502: filter duplicates
2607  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2608  $this->mLangLinkLanguages[$iw] = true;
2609  $this->mOutput->addLanguageLink( $nt->getFullText() );
2610  }
2611 
2615  $s = rtrim( $s . $prefix ) . $trail; # T175416
2616  continue;
2617  }
2618 
2619  if ( $ns == NS_FILE ) {
2620  if ( !$this->badFileLookup->isBadFile( $nt->getDBkey(), $this->mTitle ) ) {
2621  if ( $wasblank ) {
2622  # if no parameters were passed, $text
2623  # becomes something like "File:Foo.png",
2624  # which we don't want to pass on to the
2625  # image generator
2626  $text = '';
2627  } else {
2628  # recursively parse links inside the image caption
2629  # actually, this will parse them in any other parameters, too,
2630  # but it might be hard to fix that, and it doesn't matter ATM
2631  $text = $this->handleExternalLinks( $text );
2632  $holders->merge( $this->handleInternalLinks2( $text ) );
2633  }
2634  # cloak any absolute URLs inside the image markup, so handleExternalLinks() won't touch them
2635  $s .= $prefix . $this->armorLinksPrivate(
2636  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2637  continue;
2638  }
2639  } elseif ( $ns == NS_CATEGORY ) {
2643  $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2644 
2645  if ( $wasblank ) {
2646  $sortkey = $this->getDefaultSort();
2647  } else {
2648  $sortkey = $text;
2649  }
2650  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2651  $sortkey = str_replace( "\n", '', $sortkey );
2652  $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2653  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2654 
2655  continue;
2656  }
2657  }
2658 
2659  # Self-link checking. For some languages, variants of the title are checked in
2660  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2661  # for linking to a different variant.
2662  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2663  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2664  continue;
2665  }
2666 
2667  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2668  # @todo FIXME: Should do batch file existence checks, see comment below
2669  if ( $ns == NS_MEDIA ) {
2670  # Give extensions a chance to select the file revision for us
2671  $options = [];
2672  $descQuery = false;
2673  Hooks::run( 'BeforeParserFetchFileAndTitle',
2674  [ $this, $nt, &$options, &$descQuery ] );
2675  # Fetch and register the file (file title may be different via hooks)
2676  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2677  # Cloak with NOPARSE to avoid replacement in handleExternalLinks
2678  $s .= $prefix . $this->armorLinksPrivate(
2679  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2680  continue;
2681  }
2682 
2683  # Some titles, such as valid special pages or files in foreign repos, should
2684  # be shown as bluelinks even though they're not included in the page table
2685  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2686  # batch file existence checks for NS_FILE and NS_MEDIA
2687  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2688  $this->mOutput->addLink( $nt );
2689  $s .= $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2690  } else {
2691  # Links will be added to the output link list after checking
2692  $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2693  }
2694  }
2695  return $holders;
2696  }
2697 
2712  protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2713  wfDeprecated( __METHOD__, '1.34' );
2714  return $this->makeKnownLinkHolderPrivate( $nt, $text, $trail, $prefix );
2715  }
2716 
2730  private function makeKnownLinkHolderPrivate( $nt, $text = '', $trail = '', $prefix = '' ) {
2731  list( $inside, $trail ) = Linker::splitTrail( $trail );
2732 
2733  if ( $text == '' ) {
2734  $text = htmlspecialchars( $nt->getPrefixedText() );
2735  }
2736 
2737  $link = $this->getLinkRenderer()->makeKnownLink(
2738  $nt, new HtmlArmor( "$prefix$text$inside" )
2739  );
2740 
2741  return $this->armorLinksPrivate( $link ) . $trail;
2742  }
2743 
2755  public function armorLinks( $text ) {
2756  wfDeprecated( __METHOD__, '1.34' );
2757  return $this->armorLinksPrivate( $text );
2758  }
2759 
2770  private function armorLinksPrivate( $text ) {
2771  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2772  self::MARKER_PREFIX . "NOPARSE$1", $text );
2773  }
2774 
2780  public function areSubpagesAllowed() {
2781  # Some namespaces don't allow subpages
2782  wfDeprecated( __METHOD__, '1.34' );
2783  return $this->nsInfo->hasSubpages( $this->mTitle->getNamespace() );
2784  }
2785 
2795  public function maybeDoSubpageLink( $target, &$text ) {
2796  wfDeprecated( __METHOD__, '1.34' );
2797  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2798  }
2799 
2808  public function doBlockLevels( $text, $linestart ) {
2809  return BlockLevelPass::doBlockLevels( $text, $linestart );
2810  }
2811 
2824  public function getVariableValue( $index, $frame = false ) {
2825  wfDeprecated( __METHOD__, '1.34' );
2826  return $this->expandMagicVariable( $index, $frame );
2827  }
2828 
2838  private function expandMagicVariable( $index, $frame = false ) {
2839  // XXX This function should be moved out of Parser class for
2840  // reuse by Parsoid/etc.
2841  if ( is_null( $this->mTitle ) ) {
2842  // If no title set, bad things are going to happen
2843  // later. Title should always be set since this
2844  // should only be called in the middle of a parse
2845  // operation (but the unit-tests do funky stuff)
2846  throw new MWException( __METHOD__ . ' Should only be '
2847  . ' called while parsing (no title set)' );
2848  }
2849 
2850  // Avoid PHP 7.1 warning from passing $this by reference
2851  $parser = $this;
2852 
2857  if (
2858  Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) &&
2859  isset( $this->mVarCache[$index] )
2860  ) {
2861  return $this->mVarCache[$index];
2862  }
2863 
2864  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2865  Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2866 
2867  $pageLang = $this->getFunctionLang();
2868 
2869  switch ( $index ) {
2870  case '!':
2871  $value = '|';
2872  break;
2873  case 'currentmonth':
2874  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2875  break;
2876  case 'currentmonth1':
2877  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2878  break;
2879  case 'currentmonthname':
2880  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2881  break;
2882  case 'currentmonthnamegen':
2883  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2884  break;
2885  case 'currentmonthabbrev':
2886  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2887  break;
2888  case 'currentday':
2889  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2890  break;
2891  case 'currentday2':
2892  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2893  break;
2894  case 'localmonth':
2895  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2896  break;
2897  case 'localmonth1':
2898  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2899  break;
2900  case 'localmonthname':
2901  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2902  break;
2903  case 'localmonthnamegen':
2904  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2905  break;
2906  case 'localmonthabbrev':
2907  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2908  break;
2909  case 'localday':
2910  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2911  break;
2912  case 'localday2':
2913  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2914  break;
2915  case 'pagename':
2916  $value = wfEscapeWikiText( $this->mTitle->getText() );
2917  break;
2918  case 'pagenamee':
2919  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2920  break;
2921  case 'fullpagename':
2922  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2923  break;
2924  case 'fullpagenamee':
2925  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2926  break;
2927  case 'subpagename':
2928  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2929  break;
2930  case 'subpagenamee':
2931  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2932  break;
2933  case 'rootpagename':
2934  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2935  break;
2936  case 'rootpagenamee':
2937  $value = wfEscapeWikiText( wfUrlencode( str_replace(
2938  ' ',
2939  '_',
2940  $this->mTitle->getRootText()
2941  ) ) );
2942  break;
2943  case 'basepagename':
2944  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2945  break;
2946  case 'basepagenamee':
2947  $value = wfEscapeWikiText( wfUrlencode( str_replace(
2948  ' ',
2949  '_',
2950  $this->mTitle->getBaseText()
2951  ) ) );
2952  break;
2953  case 'talkpagename':
2954  if ( $this->mTitle->canHaveTalkPage() ) {
2955  $talkPage = $this->mTitle->getTalkPage();
2956  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2957  } else {
2958  $value = '';
2959  }
2960  break;
2961  case 'talkpagenamee':
2962  if ( $this->mTitle->canHaveTalkPage() ) {
2963  $talkPage = $this->mTitle->getTalkPage();
2964  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2965  } else {
2966  $value = '';
2967  }
2968  break;
2969  case 'subjectpagename':
2970  $subjPage = $this->mTitle->getSubjectPage();
2971  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2972  break;
2973  case 'subjectpagenamee':
2974  $subjPage = $this->mTitle->getSubjectPage();
2975  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2976  break;
2977  case 'pageid': // requested in T25427
2978  # Inform the edit saving system that getting the canonical output
2979  # after page insertion requires a parse that used that exact page ID
2980  $this->setOutputFlag( 'vary-page-id', '{{PAGEID}} used' );
2981  $value = $this->mTitle->getArticleID();
2982  if ( !$value ) {
2983  $value = $this->mOptions->getSpeculativePageId();
2984  if ( $value ) {
2985  $this->mOutput->setSpeculativePageIdUsed( $value );
2986  }
2987  }
2988  break;
2989  case 'revisionid':
2990  if (
2991  $this->svcOptions->get( 'MiserMode' ) &&
2992  !$this->mOptions->getInterfaceMessage() &&
2993  // @TODO: disallow this word on all namespaces
2994  $this->nsInfo->isContent( $this->mTitle->getNamespace() )
2995  ) {
2996  // Use a stub result instead of the actual revision ID in order to avoid
2997  // double parses on page save but still allow preview detection (T137900)
2998  if ( $this->getRevisionId() || $this->mOptions->getSpeculativeRevId() ) {
2999  $value = '-';
3000  } else {
3001  $this->setOutputFlag( 'vary-revision-exists', '{{REVISIONID}} used' );
3002  $value = '';
3003  }
3004  } else {
3005  # Inform the edit saving system that getting the canonical output after
3006  # revision insertion requires a parse that used that exact revision ID
3007  $this->setOutputFlag( 'vary-revision-id', '{{REVISIONID}} used' );
3008  $value = $this->getRevisionId();
3009  if ( $value === 0 ) {
3010  $rev = $this->getRevisionObject();
3011  $value = $rev ? $rev->getId() : $value;
3012  }
3013  if ( !$value ) {
3014  $value = $this->mOptions->getSpeculativeRevId();
3015  if ( $value ) {
3016  $this->mOutput->setSpeculativeRevIdUsed( $value );
3017  }
3018  }
3019  }
3020  break;
3021  case 'revisionday':
3022  $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
3023  break;
3024  case 'revisionday2':
3025  $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
3026  break;
3027  case 'revisionmonth':
3028  $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
3029  break;
3030  case 'revisionmonth1':
3031  $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
3032  break;
3033  case 'revisionyear':
3034  $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
3035  break;
3036  case 'revisiontimestamp':
3037  $value = $this->getRevisionTimestampSubstring( 0, 14, self::MAX_TTS, $index );
3038  break;
3039  case 'revisionuser':
3040  # Inform the edit saving system that getting the canonical output after
3041  # revision insertion requires a parse that used the actual user ID
3042  $this->setOutputFlag( 'vary-user', '{{REVISIONUSER}} used' );
3043  $value = $this->getRevisionUser();
3044  break;
3045  case 'revisionsize':
3046  $value = $this->getRevisionSize();
3047  break;
3048  case 'namespace':
3049  $value = str_replace( '_', ' ',
3050  $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
3051  break;
3052  case 'namespacee':
3053  $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
3054  break;
3055  case 'namespacenumber':
3056  $value = $this->mTitle->getNamespace();
3057  break;
3058  case 'talkspace':
3059  $value = $this->mTitle->canHaveTalkPage()
3060  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
3061  : '';
3062  break;
3063  case 'talkspacee':
3064  $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
3065  break;
3066  case 'subjectspace':
3067  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
3068  break;
3069  case 'subjectspacee':
3070  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3071  break;
3072  case 'currentdayname':
3073  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
3074  break;
3075  case 'currentyear':
3076  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
3077  break;
3078  case 'currenttime':
3079  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
3080  break;
3081  case 'currenthour':
3082  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
3083  break;
3084  case 'currentweek':
3085  # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
3086  # int to remove the padding
3087  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
3088  break;
3089  case 'currentdow':
3090  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
3091  break;
3092  case 'localdayname':
3093  $value = $pageLang->getWeekdayName(
3094  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
3095  );
3096  break;
3097  case 'localyear':
3098  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
3099  break;
3100  case 'localtime':
3101  $value = $pageLang->time(
3102  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
3103  false,
3104  false
3105  );
3106  break;
3107  case 'localhour':
3108  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
3109  break;
3110  case 'localweek':
3111  # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
3112  # int to remove the padding
3113  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
3114  break;
3115  case 'localdow':
3116  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
3117  break;
3118  case 'numberofarticles':
3119  $value = $pageLang->formatNum( SiteStats::articles() );
3120  break;
3121  case 'numberoffiles':
3122  $value = $pageLang->formatNum( SiteStats::images() );
3123  break;
3124  case 'numberofusers':
3125  $value = $pageLang->formatNum( SiteStats::users() );
3126  break;
3127  case 'numberofactiveusers':
3128  $value = $pageLang->formatNum( SiteStats::activeUsers() );
3129  break;
3130  case 'numberofpages':
3131  $value = $pageLang->formatNum( SiteStats::pages() );
3132  break;
3133  case 'numberofadmins':
3134  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
3135  break;
3136  case 'numberofedits':
3137  $value = $pageLang->formatNum( SiteStats::edits() );
3138  break;
3139  case 'currenttimestamp':
3140  $value = wfTimestamp( TS_MW, $ts );
3141  break;
3142  case 'localtimestamp':
3143  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
3144  break;
3145  case 'currentversion':
3146  $value = SpecialVersion::getVersion();
3147  break;
3148  case 'articlepath':
3149  return $this->svcOptions->get( 'ArticlePath' );
3150  case 'sitename':
3151  return $this->svcOptions->get( 'Sitename' );
3152  case 'server':
3153  return $this->svcOptions->get( 'Server' );
3154  case 'servername':
3155  return $this->svcOptions->get( 'ServerName' );
3156  case 'scriptpath':
3157  return $this->svcOptions->get( 'ScriptPath' );
3158  case 'stylepath':
3159  return $this->svcOptions->get( 'StylePath' );
3160  case 'directionmark':
3161  return $pageLang->getDirMark();
3162  case 'contentlanguage':
3163  return $this->svcOptions->get( 'LanguageCode' );
3164  case 'pagelanguage':
3165  $value = $pageLang->getCode();
3166  break;
3167  case 'cascadingsources':
3168  $value = CoreParserFunctions::cascadingsources( $this );
3169  break;
3170  default:
3171  $ret = null;
3172  Hooks::run(
3173  'ParserGetVariableValueSwitch',
3174  [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
3175  );
3176 
3177  return $ret;
3178  }
3179 
3180  if ( $index ) {
3181  $this->mVarCache[$index] = $value;
3182  }
3183 
3184  return $value;
3185  }
3186 
3194  private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
3195  # Get the timezone-adjusted timestamp to be used for this revision
3196  $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
3197  # Possibly set vary-revision if there is not yet an associated revision
3198  if ( !$this->getRevisionObject() ) {
3199  # Get the timezone-adjusted timestamp $mtts seconds in the future.
3200  # This future is relative to the current time and not that of the
3201  # parser options. The rendered timestamp can be compared to that
3202  # of the timestamp specified by the parser options.
3203  $resThen = substr(
3204  $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
3205  $start,
3206  $len
3207  );
3208 
3209  if ( $resNow !== $resThen ) {
3210  # Inform the edit saving system that getting the canonical output after
3211  # revision insertion requires a parse that used an actual revision timestamp
3212  $this->setOutputFlag( 'vary-revision-timestamp', "$variable used" );
3213  }
3214  }
3215 
3216  return $resNow;
3217  }
3218 
3225  public function initialiseVariables() {
3226  wfDeprecated( __METHOD__, '1.34' );
3227  $this->initializeVariables();
3228  }
3229 
3234  private function initializeVariables() {
3235  $variableIDs = $this->magicWordFactory->getVariableIDs();
3236  $substIDs = $this->magicWordFactory->getSubstIDs();
3237 
3238  $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
3239  $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
3240  }
3241 
3264  public function preprocessToDom( $text, $flags = 0 ) {
3265  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3266  return $dom;
3267  }
3268 
3277  public static function splitWhitespace( $s ) {
3278  wfDeprecated( __METHOD__, '1.34' );
3279  $ltrimmed = ltrim( $s );
3280  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3281  $trimmed = rtrim( $ltrimmed );
3282  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3283  if ( $diff > 0 ) {
3284  $w2 = substr( $ltrimmed, -$diff );
3285  } else {
3286  $w2 = '';
3287  }
3288  return [ $w1, $trimmed, $w2 ];
3289  }
3290 
3311  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3312  # Is there any text? Also, Prevent too big inclusions!
3313  $textSize = strlen( $text );
3314  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3315  return $text;
3316  }
3317 
3318  if ( $frame === false ) {
3319  $frame = $this->getPreprocessor()->newFrame();
3320  } elseif ( !( $frame instanceof PPFrame ) ) {
3321  $this->logger->debug(
3322  __METHOD__ . " called using plain parameters instead of " .
3323  "a PPFrame instance. Creating custom frame."
3324  );
3325  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3326  }
3327 
3328  $dom = $this->preprocessToDom( $text );
3329  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3330  $text = $frame->expand( $dom, $flags );
3331 
3332  return $text;
3333  }
3334 
3343  public static function createAssocArgs( $args ) {
3344  wfDeprecated( __METHOD__, '1.34' );
3345  $assocArgs = [];
3346  $index = 1;
3347  foreach ( $args as $arg ) {
3348  $eqpos = strpos( $arg, '=' );
3349  if ( $eqpos === false ) {
3350  $assocArgs[$index++] = $arg;
3351  } else {
3352  $name = trim( substr( $arg, 0, $eqpos ) );
3353  $value = trim( substr( $arg, $eqpos + 1 ) );
3354  if ( $value === false ) {
3355  $value = '';
3356  }
3357  if ( $name !== false ) {
3358  $assocArgs[$name] = $value;
3359  }
3360  }
3361  }
3362 
3363  return $assocArgs;
3364  }
3365 
3392  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3393  # does no harm if $current and $max are present but are unnecessary for the message
3394  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3395  # only during preview, and that would split the parser cache unnecessarily.
3396  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3397  ->text();
3398  $this->mOutput->addWarning( $warning );
3399  $this->addTrackingCategory( "$limitationType-category" );
3400  }
3401 
3415  public function braceSubstitution( $piece, $frame ) {
3416  // Flags
3417 
3418  // $text has been filled
3419  $found = false;
3420  // wiki markup in $text should be escaped
3421  $nowiki = false;
3422  // $text is HTML, armour it against wikitext transformation
3423  $isHTML = false;
3424  // Force interwiki transclusion to be done in raw mode not rendered
3425  $forceRawInterwiki = false;
3426  // $text is a DOM node needing expansion in a child frame
3427  $isChildObj = false;
3428  // $text is a DOM node needing expansion in the current frame
3429  $isLocalObj = false;
3430 
3431  # Title object, where $text came from
3432  $title = false;
3433 
3434  # $part1 is the bit before the first |, and must contain only title characters.
3435  # Various prefixes will be stripped from it later.
3436  $titleWithSpaces = $frame->expand( $piece['title'] );
3437  $part1 = trim( $titleWithSpaces );
3438  $titleText = false;
3439 
3440  # Original title text preserved for various purposes
3441  $originalTitle = $part1;
3442 
3443  # $args is a list of argument nodes, starting from index 0, not including $part1
3444  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3445  # below won't work b/c this $args isn't an object
3446  $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
3447 
3448  $profileSection = null; // profile templates
3449 
3450  # SUBST
3451  if ( !$found ) {
3452  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3453 
3454  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3455  # Decide whether to expand template or keep wikitext as-is.
3456  if ( $this->ot['wiki'] ) {
3457  if ( $substMatch === false ) {
3458  $literal = true; # literal when in PST with no prefix
3459  } else {
3460  $literal = false; # expand when in PST with subst: or safesubst:
3461  }
3462  } else {
3463  if ( $substMatch == 'subst' ) {
3464  $literal = true; # literal when not in PST with plain subst:
3465  } else {
3466  $literal = false; # expand when not in PST with safesubst: or no prefix
3467  }
3468  }
3469  if ( $literal ) {
3470  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3471  $isLocalObj = true;
3472  $found = true;
3473  }
3474  }
3475 
3476  # Variables
3477  if ( !$found && $args->getLength() == 0 ) {
3478  $id = $this->mVariables->matchStartToEnd( $part1 );
3479  if ( $id !== false ) {
3480  $text = $this->expandMagicVariable( $id, $frame );
3481  if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3482  $this->mOutput->updateCacheExpiry(
3483  $this->magicWordFactory->getCacheTTL( $id ) );
3484  }
3485  $found = true;
3486  }
3487  }
3488 
3489  # MSG, MSGNW and RAW
3490  if ( !$found ) {
3491  # Check for MSGNW:
3492  $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3493  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3494  $nowiki = true;
3495  } else {
3496  # Remove obsolete MSG:
3497  $mwMsg = $this->magicWordFactory->get( 'msg' );
3498  $mwMsg->matchStartAndRemove( $part1 );
3499  }
3500 
3501  # Check for RAW:
3502  $mwRaw = $this->magicWordFactory->get( 'raw' );
3503  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3504  $forceRawInterwiki = true;
3505  }
3506  }
3507 
3508  # Parser functions
3509  if ( !$found ) {
3510  $colonPos = strpos( $part1, ':' );
3511  if ( $colonPos !== false ) {
3512  $func = substr( $part1, 0, $colonPos );
3513  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3514  $argsLength = $args->getLength();
3515  for ( $i = 0; $i < $argsLength; $i++ ) {
3516  $funcArgs[] = $args->item( $i );
3517  }
3518 
3519  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3520 
3521  // Extract any forwarded flags
3522  if ( isset( $result['title'] ) ) {
3523  $title = $result['title'];
3524  }
3525  if ( isset( $result['found'] ) ) {
3526  $found = $result['found'];
3527  }
3528  if ( array_key_exists( 'text', $result ) ) {
3529  // a string or null
3530  $text = $result['text'];
3531  }
3532  if ( isset( $result['nowiki'] ) ) {
3533  $nowiki = $result['nowiki'];
3534  }
3535  if ( isset( $result['isHTML'] ) ) {
3536  $isHTML = $result['isHTML'];
3537  }
3538  if ( isset( $result['forceRawInterwiki'] ) ) {
3539  $forceRawInterwiki = $result['forceRawInterwiki'];
3540  }
3541  if ( isset( $result['isChildObj'] ) ) {
3542  $isChildObj = $result['isChildObj'];
3543  }
3544  if ( isset( $result['isLocalObj'] ) ) {
3545  $isLocalObj = $result['isLocalObj'];
3546  }
3547  }
3548  }
3549 
3550  # Finish mangling title and then check for loops.
3551  # Set $title to a Title object and $titleText to the PDBK
3552  if ( !$found ) {
3553  $ns = NS_TEMPLATE;
3554  # Split the title into page and subpage
3555  $subpage = '';
3556  $relative = Linker::normalizeSubpageLink(
3557  $this->mTitle, $part1, $subpage
3558  );
3559  if ( $part1 !== $relative ) {
3560  $part1 = $relative;
3561  $ns = $this->mTitle->getNamespace();
3562  }
3563  $title = Title::newFromText( $part1, $ns );
3564  if ( $title ) {
3565  $titleText = $title->getPrefixedText();
3566  # Check for language variants if the template is not found
3567  if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3568  $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
3569  }
3570  # Do recursion depth check
3571  $limit = $this->mOptions->getMaxTemplateDepth();
3572  if ( $frame->depth >= $limit ) {
3573  $found = true;
3574  $text = '<span class="error">'
3575  . wfMessage( 'parser-template-recursion-depth-warning' )
3576  ->numParams( $limit )->inContentLanguage()->text()
3577  . '</span>';
3578  }
3579  }
3580  }
3581 
3582  # Load from database
3583  if ( !$found && $title ) {
3584  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3585  if ( !$title->isExternal() ) {
3586  if ( $title->isSpecialPage()
3587  && $this->mOptions->getAllowSpecialInclusion()
3588  && $this->ot['html']
3589  ) {
3590  $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3591  // Pass the template arguments as URL parameters.
3592  // "uselang" will have no effect since the Language object
3593  // is forced to the one defined in ParserOptions.
3594  $pageArgs = [];
3595  $argsLength = $args->getLength();
3596  for ( $i = 0; $i < $argsLength; $i++ ) {
3597  $bits = $args->item( $i )->splitArg();
3598  if ( strval( $bits['index'] ) === '' ) {
3599  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3600  $value = trim( $frame->expand( $bits['value'] ) );
3601  $pageArgs[$name] = $value;
3602  }
3603  }
3604 
3605  // Create a new context to execute the special page
3606  $context = new RequestContext;
3607  $context->setTitle( $title );
3608  $context->setRequest( new FauxRequest( $pageArgs ) );
3609  if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3610  $context->setUser( $this->getUser() );
3611  } else {
3612  // If this page is cached, then we better not be per user.
3613  $context->setUser( User::newFromName( '127.0.0.1', false ) );
3614  }
3615  $context->setLanguage( $this->mOptions->getUserLangObj() );
3616  $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3617  if ( $ret ) {
3618  $text = $context->getOutput()->getHTML();
3619  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3620  $found = true;
3621  $isHTML = true;
3622  if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3623  $this->mOutput->updateRuntimeAdaptiveExpiry(
3624  $specialPage->maxIncludeCacheTime()
3625  );
3626  }
3627  }
3628  } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
3629  $found = false; # access denied
3630  $this->logger->debug(
3631  __METHOD__ .
3632  ": template inclusion denied for " . $title->getPrefixedDBkey()
3633  );
3634  } else {
3635  list( $text, $title ) = $this->getTemplateDom( $title );
3636  if ( $text !== false ) {
3637  $found = true;
3638  $isChildObj = true;
3639  }
3640  }
3641 
3642  # If the title is valid but undisplayable, make a link to it
3643  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3644  $text = "[[:$titleText]]";
3645  $found = true;
3646  }
3647  } elseif ( $title->isTrans() ) {
3648  # Interwiki transclusion
3649  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3650  $text = $this->interwikiTransclude( $title, 'render' );
3651  $isHTML = true;
3652  } else {
3653  $text = $this->interwikiTransclude( $title, 'raw' );
3654  # Preprocess it like a template
3655  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3656  $isChildObj = true;
3657  }
3658  $found = true;
3659  }
3660 
3661  # Do infinite loop check
3662  # This has to be done after redirect resolution to avoid infinite loops via redirects
3663  if ( !$frame->loopCheck( $title ) ) {
3664  $found = true;
3665  $text = '<span class="error">'
3666  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3667  . '</span>';
3668  $this->addTrackingCategory( 'template-loop-category' );
3669  $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3670  wfEscapeWikiText( $titleText ) )->text() );
3671  $this->logger->debug( __METHOD__ . ": template loop broken at '$titleText'" );
3672  }
3673  }
3674 
3675  # If we haven't found text to substitute by now, we're done
3676  # Recover the source wikitext and return it
3677  if ( !$found ) {
3678  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3679  if ( $profileSection ) {
3680  $this->mProfiler->scopedProfileOut( $profileSection );
3681  }
3682  return [ 'object' => $text ];
3683  }
3684 
3685  # Expand DOM-style return values in a child frame
3686  if ( $isChildObj ) {
3687  # Clean up argument array
3688  $newFrame = $frame->newChild( $args, $title );
3689 
3690  if ( $nowiki ) {
3691  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3692  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3693  # Expansion is eligible for the empty-frame cache
3694  $text = $newFrame->cachedExpand( $titleText, $text );
3695  } else {
3696  # Uncached expansion
3697  $text = $newFrame->expand( $text );
3698  }
3699  }
3700  if ( $isLocalObj && $nowiki ) {
3701  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3702  $isLocalObj = false;
3703  }
3704 
3705  if ( $profileSection ) {
3706  $this->mProfiler->scopedProfileOut( $profileSection );
3707  }
3708 
3709  # Replace raw HTML by a placeholder
3710  if ( $isHTML ) {
3711  $text = $this->insertStripItem( $text );
3712  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3713  # Escape nowiki-style return values
3714  $text = wfEscapeWikiText( $text );
3715  } elseif ( is_string( $text )
3716  && !$piece['lineStart']
3717  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3718  ) {
3719  # T2529: if the template begins with a table or block-level
3720  # element, it should be treated as beginning a new line.
3721  # This behavior is somewhat controversial.
3722  $text = "\n" . $text;
3723  }
3724 
3725  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3726  # Error, oversize inclusion
3727  if ( $titleText !== false ) {
3728  # Make a working, properly escaped link if possible (T25588)
3729  $text = "[[:$titleText]]";
3730  } else {
3731  # This will probably not be a working link, but at least it may
3732  # provide some hint of where the problem is
3733  preg_replace( '/^:/', '', $originalTitle );
3734  $text = "[[:$originalTitle]]";
3735  }
3736  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3737  . 'post-expand include size too large -->' );
3738  $this->limitationWarn( 'post-expand-template-inclusion' );
3739  }
3740 
3741  if ( $isLocalObj ) {
3742  $ret = [ 'object' => $text ];
3743  } else {
3744  $ret = [ 'text' => $text ];
3745  }
3746 
3747  return $ret;
3748  }
3749 
3769  public function callParserFunction( $frame, $function, array $args = [] ) {
3770  # Case sensitive functions
3771  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3772  $function = $this->mFunctionSynonyms[1][$function];
3773  } else {
3774  # Case insensitive functions
3775  $function = $this->contLang->lc( $function );
3776  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3777  $function = $this->mFunctionSynonyms[0][$function];
3778  } else {
3779  return [ 'found' => false ];
3780  }
3781  }
3782 
3783  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3784 
3785  // Avoid PHP 7.1 warning from passing $this by reference
3786  $parser = $this;
3787 
3788  $allArgs = [ &$parser ];
3789  if ( $flags & self::SFH_OBJECT_ARGS ) {
3790  # Convert arguments to PPNodes and collect for appending to $allArgs
3791  $funcArgs = [];
3792  foreach ( $args as $k => $v ) {
3793  if ( $v instanceof PPNode || $k === 0 ) {
3794  $funcArgs[] = $v;
3795  } else {
3796  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3797  }
3798  }
3799 
3800  # Add a frame parameter, and pass the arguments as an array
3801  $allArgs[] = $frame;
3802  $allArgs[] = $funcArgs;
3803  } else {
3804  # Convert arguments to plain text and append to $allArgs
3805  foreach ( $args as $k => $v ) {
3806  if ( $v instanceof PPNode ) {
3807  $allArgs[] = trim( $frame->expand( $v ) );
3808  } elseif ( is_int( $k ) && $k >= 0 ) {
3809  $allArgs[] = trim( $v );
3810  } else {
3811  $allArgs[] = trim( "$k=$v" );
3812  }
3813  }
3814  }
3815 
3816  $result = $callback( ...$allArgs );
3817 
3818  # The interface for function hooks allows them to return a wikitext
3819  # string or an array containing the string and any flags. This mungs
3820  # things around to match what this method should return.
3821  if ( !is_array( $result ) ) {
3822  $result = [
3823  'found' => true,
3824  'text' => $result,
3825  ];
3826  } else {
3827  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3828  $result['text'] = $result[0];
3829  }
3830  unset( $result[0] );
3831  $result += [
3832  'found' => true,
3833  ];
3834  }
3835 
3836  $noparse = true;
3837  $preprocessFlags = 0;
3838  if ( isset( $result['noparse'] ) ) {
3839  $noparse = $result['noparse'];
3840  }
3841  if ( isset( $result['preprocessFlags'] ) ) {
3842  $preprocessFlags = $result['preprocessFlags'];
3843  }
3844 
3845  if ( !$noparse ) {
3846  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3847  $result['isChildObj'] = true;
3848  }
3849 
3850  return $result;
3851  }
3852 
3861  public function getTemplateDom( $title ) {
3862  $cacheTitle = $title;
3863  $titleText = $title->getPrefixedDBkey();
3864 
3865  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3866  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3867  $title = Title::makeTitle( $ns, $dbk );
3868  $titleText = $title->getPrefixedDBkey();
3869  }
3870  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3871  return [ $this->mTplDomCache[$titleText], $title ];
3872  }
3873 
3874  # Cache miss, go to the database
3875  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3876 
3877  if ( $text === false ) {
3878  $this->mTplDomCache[$titleText] = false;
3879  return [ false, $title ];
3880  }
3881 
3882  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3883  $this->mTplDomCache[$titleText] = $dom;
3884 
3885  if ( !$title->equals( $cacheTitle ) ) {
3886  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3887  [ $title->getNamespace(), $title->getDBkey() ];
3888  }
3889 
3890  return [ $dom, $title ];
3891  }
3892 
3904  public function fetchCurrentRevisionOfTitle( $title ) {
3905  $cacheKey = $title->getPrefixedDBkey();
3906  if ( !$this->currentRevisionCache ) {
3907  $this->currentRevisionCache = new MapCacheLRU( 100 );
3908  }
3909  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3910  $this->currentRevisionCache->set( $cacheKey,
3911  // Defaults to Parser::statelessFetchRevision()
3912  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3913  );
3914  }
3915  return $this->currentRevisionCache->get( $cacheKey );
3916  }
3917 
3923  public function isCurrentRevisionOfTitleCached( $title ) {
3924  return (
3925  $this->currentRevisionCache &&
3926  $this->currentRevisionCache->has( $title->getPrefixedText() )
3927  );
3928  }
3929 
3939  public static function statelessFetchRevision( Title $title, $parser = false ) {
3941 
3942  return $rev;
3943  }
3944 
3950  public function fetchTemplateAndTitle( $title ) {
3951  // Defaults to Parser::statelessFetchTemplate()
3952  $templateCb = $this->mOptions->getTemplateCallback();
3953  $stuff = call_user_func( $templateCb, $title, $this );
3954  $rev = $stuff['revision'] ?? null;
3955  $text = $stuff['text'];
3956  if ( is_string( $stuff['text'] ) ) {
3957  // We use U+007F DELETE to distinguish strip markers from regular text
3958  $text = strtr( $text, "\x7f", "?" );
3959  }
3960  $finalTitle = $stuff['finalTitle'] ?? $title;
3961  foreach ( ( $stuff['deps'] ?? [] ) as $dep ) {
3962  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3963  if ( $dep['title']->equals( $this->getTitle() ) && $rev instanceof Revision ) {
3964  // Self-transclusion; final result may change based on the new page version
3965  $this->setOutputFlag( 'vary-revision-sha1', 'Self transclusion' );
3966  $this->getOutput()->setRevisionUsedSha1Base36( $rev->getSha1() );
3967  }
3968  }
3969 
3970  return [ $text, $finalTitle ];
3971  }
3972 
3978  public function fetchTemplate( $title ) {
3979  return $this->fetchTemplateAndTitle( $title )[0];
3980  }
3981 
3991  public static function statelessFetchTemplate( $title, $parser = false ) {
3992  $text = $skip = false;
3993  $finalTitle = $title;
3994  $deps = [];
3995  $rev = null;
3996 
3997  # Loop to fetch the article, with up to 1 redirect
3998  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3999  # Give extensions a chance to select the revision instead
4000  $id = false; # Assume current
4001  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
4002  [ $parser, $title, &$skip, &$id ] );
4003 
4004  if ( $skip ) {
4005  $text = false;
4006  $deps[] = [
4007  'title' => $title,
4008  'page_id' => $title->getArticleID(),
4009  'rev_id' => null
4010  ];
4011  break;
4012  }
4013  # Get the revision
4014  if ( $id ) {
4015  $rev = Revision::newFromId( $id );
4016  } elseif ( $parser ) {
4017  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
4018  } else {
4019  $rev = Revision::newFromTitle( $title );
4020  }
4021  $rev_id = $rev ? $rev->getId() : 0;
4022  # If there is no current revision, there is no page
4023  if ( $id === false && !$rev ) {
4024  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
4025  $linkCache->addBadLinkObj( $title );
4026  }
4027 
4028  $deps[] = [
4029  'title' => $title,
4030  'page_id' => $title->getArticleID(),
4031  'rev_id' => $rev_id
4032  ];
4033  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
4034  # We fetched a rev from a different title; register it too...
4035  $deps[] = [
4036  'title' => $rev->getTitle(),
4037  'page_id' => $rev->getPage(),
4038  'rev_id' => $rev_id
4039  ];
4040  }
4041 
4042  if ( $rev ) {
4043  $content = $rev->getContent();
4044  $text = $content ? $content->getWikitextForTransclusion() : null;
4045 
4046  Hooks::run( 'ParserFetchTemplate',
4047  [ $parser, $title, $rev, &$text, &$deps ] );
4048 
4049  if ( $text === false || $text === null ) {
4050  $text = false;
4051  break;
4052  }
4053  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
4054  $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
4055  lcfirst( $title->getText() ) )->inContentLanguage();
4056  if ( !$message->exists() ) {
4057  $text = false;
4058  break;
4059  }
4060  $content = $message->content();
4061  $text = $message->plain();
4062  } else {
4063  break;
4064  }
4065  if ( !$content ) {
4066  break;
4067  }
4068  # Redirect?
4069  $finalTitle = $title;
4070  $title = $content->getRedirectTarget();
4071  }
4072  return [
4073  'revision' => $rev,
4074  'text' => $text,
4075  'finalTitle' => $finalTitle,
4076  'deps' => $deps
4077  ];
4078  }
4079 
4087  public function fetchFileAndTitle( $title, $options = [] ) {
4088  $file = $this->fetchFileNoRegister( $title, $options );
4089 
4090  $time = $file ? $file->getTimestamp() : false;
4091  $sha1 = $file ? $file->getSha1() : false;
4092  # Register the file as a dependency...
4093  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4094  if ( $file && !$title->equals( $file->getTitle() ) ) {
4095  # Update fetched file title
4096  $title = $file->getTitle();
4097  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4098  }
4099  return [ $file, $title ];
4100  }
4101 
4112  protected function fetchFileNoRegister( $title, $options = [] ) {
4113  if ( isset( $options['broken'] ) ) {
4114  $file = false; // broken thumbnail forced by hook
4115  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
4116  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
4117  } else { // get by (name,timestamp)
4118  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title, $options );
4119  }
4120  return $file;
4121  }
4122 
4131  public function interwikiTransclude( $title, $action ) {
4132  if ( !$this->svcOptions->get( 'EnableScaryTranscluding' ) ) {
4133  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
4134  }
4135 
4136  $url = $title->getFullURL( [ 'action' => $action ] );
4137  if ( strlen( $url ) > 1024 ) {
4138  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
4139  }
4140 
4141  $wikiId = $title->getTransWikiID(); // remote wiki ID or false
4142 
4143  $fname = __METHOD__;
4144  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
4145 
4146  $data = $cache->getWithSetCallback(
4147  $cache->makeGlobalKey(
4148  'interwiki-transclude',
4149  ( $wikiId !== false ) ? $wikiId : 'external',
4150  sha1( $url )
4151  ),
4152  $this->svcOptions->get( 'TranscludeCacheExpiry' ),
4153  function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
4154  $req = MWHttpRequest::factory( $url, [], $fname );
4155 
4156  $status = $req->execute(); // Status object
4157  if ( !$status->isOK() ) {
4158  $ttl = $cache::TTL_UNCACHEABLE;
4159  } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
4160  $ttl = min( $cache::TTL_LAGGED, $ttl );
4161  }
4162 
4163  return [
4164  'text' => $status->isOK() ? $req->getContent() : null,
4165  'code' => $req->getStatus()
4166  ];
4167  },
4168  [
4169  'checkKeys' => ( $wikiId !== false )
4170  ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
4171  : [],
4172  'pcGroup' => 'interwiki-transclude:5',
4173  'pcTTL' => $cache::TTL_PROC_LONG
4174  ]
4175  );
4176 
4177  if ( is_string( $data['text'] ) ) {
4178  $text = $data['text'];
4179  } elseif ( $data['code'] != 200 ) {
4180  // Though we failed to fetch the content, this status is useless.
4181  $text = wfMessage( 'scarytranscludefailed-httpstatus' )
4182  ->params( $url, $data['code'] )->inContentLanguage()->text();
4183  } else {
4184  $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
4185  }
4186 
4187  return $text;
4188  }
4189 
4199  public function argSubstitution( $piece, $frame ) {
4200  $error = false;
4201  $parts = $piece['parts'];
4202  $nameWithSpaces = $frame->expand( $piece['title'] );
4203  $argName = trim( $nameWithSpaces );
4204  $object = false;
4205  $text = $frame->getArgument( $argName );
4206  if ( $text === false && $parts->getLength() > 0
4207  && ( $this->ot['html']
4208  || $this->ot['pre']
4209  || ( $this->ot['wiki'] && $frame->isTemplate() )
4210  )
4211  ) {
4212  # No match in frame, use the supplied default
4213  $object = $parts->item( 0 )->getChildren();
4214  }
4215  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
4216  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
4217  $this->limitationWarn( 'post-expand-template-argument' );
4218  }
4219 
4220  if ( $text === false && $object === false ) {
4221  # No match anywhere
4222  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
4223  }
4224  if ( $error !== false ) {
4225  $text .= $error;
4226  }
4227  if ( $object !== false ) {
4228  $ret = [ 'object' => $object ];
4229  } else {
4230  $ret = [ 'text' => $text ];
4231  }
4232 
4233  return $ret;
4234  }
4235 
4252  public function extensionSubstitution( $params, $frame ) {
4253  static $errorStr = '<span class="error">';
4254  static $errorLen = 20;
4255 
4256  $name = $frame->expand( $params['name'] );
4257  if ( substr( $name, 0, $errorLen ) === $errorStr ) {
4258  // Probably expansion depth or node count exceeded. Just punt the
4259  // error up.
4260  return $name;
4261  }
4262 
4263  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
4264  if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
4265  // See above
4266  return $attrText;
4267  }
4268 
4269  // We can't safely check if the expansion for $content resulted in an
4270  // error, because the content could happen to be the error string
4271  // (T149622).
4272  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4273 
4274  $marker = self::MARKER_PREFIX . "-$name-"
4275  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4276 
4277  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4278  ( $this->ot['html'] || $this->ot['pre'] );
4279  if ( $isFunctionTag ) {
4280  $markerType = 'none';
4281  } else {
4282  $markerType = 'general';
4283  }
4284  if ( $this->ot['html'] || $isFunctionTag ) {
4285  $name = strtolower( $name );
4286  $attributes = Sanitizer::decodeTagAttributes( $attrText );
4287  if ( isset( $params['attributes'] ) ) {
4288  $attributes += $params['attributes'];
4289  }
4290 
4291  if ( isset( $this->mTagHooks[$name] ) ) {
4292  $output = call_user_func_array( $this->mTagHooks[$name],
4293  [ $content, $attributes, $this, $frame ] );
4294  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4295  list( $callback, ) = $this->mFunctionTagHooks[$name];
4296 
4297  // Avoid PHP 7.1 warning from passing $this by reference
4298  $parser = $this;
4299  $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
4300  } else {
4301  $output = '<span class="error">Invalid tag extension name: ' .
4302  htmlspecialchars( $name ) . '</span>';
4303  }
4304 
4305  if ( is_array( $output ) ) {
4306  // Extract flags
4307  $flags = $output;
4308  $output = $flags[0];
4309  if ( isset( $flags['markerType'] ) ) {
4310  $markerType = $flags['markerType'];
4311  }
4312  }
4313  } else {
4314  if ( is_null( $attrText ) ) {
4315  $attrText = '';
4316  }
4317  if ( isset( $params['attributes'] ) ) {
4318  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4319  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4320  htmlspecialchars( $attrValue ) . '"';
4321  }
4322  }
4323  if ( $content === null ) {
4324  $output = "<$name$attrText/>";
4325  } else {
4326  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4327  if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4328  // See above
4329  return $close;
4330  }
4331  $output = "<$name$attrText>$content$close";
4332  }
4333  }
4334 
4335  if ( $markerType === 'none' ) {
4336  return $output;
4337  } elseif ( $markerType === 'nowiki' ) {
4338  $this->mStripState->addNoWiki( $marker, $output );
4339  } elseif ( $markerType === 'general' ) {
4340  $this->mStripState->addGeneral( $marker, $output );
4341  } else {
4342  throw new MWException( __METHOD__ . ': invalid marker type' );
4343  }
4344  return $marker;
4345  }
4346 
4354  public function incrementIncludeSize( $type, $size ) {
4355  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4356  return false;
4357  } else {
4358  $this->mIncludeSizes[$type] += $size;
4359  return true;
4360  }
4361  }
4362 
4368  public function incrementExpensiveFunctionCount() {
4369  $this->mExpensiveFunctionCount++;
4370  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4371  }
4372 
4381  public function doDoubleUnderscore( $text ) {
4382  wfDeprecated( __METHOD__, '1.34' );
4383  return $this->handleDoubleUnderscore( $text );
4384  }
4385 
4393  private function handleDoubleUnderscore( $text ) {
4394  # The position of __TOC__ needs to be recorded
4395  $mw = $this->magicWordFactory->get( 'toc' );
4396  if ( $mw->match( $text ) ) {
4397  $this->mShowToc = true;
4398  $this->mForceTocPosition = true;
4399 
4400  # Set a placeholder. At the end we'll fill it in with the TOC.
4401  $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4402 
4403  # Only keep the first one.
4404  $text = $mw->replace( '', $text );
4405  }
4406 
4407  # Now match and remove the rest of them
4408  $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4409  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4410 
4411  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4412  $this->mOutput->mNoGallery = true;
4413  }
4414  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4415  $this->mShowToc = false;
4416  }
4417  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4418  && $this->mTitle->getNamespace() == NS_CATEGORY
4419  ) {
4420  $this->addTrackingCategory( 'hidden-category-category' );
4421  }
4422  # (T10068) Allow control over whether robots index a page.
4423  # __INDEX__ always overrides __NOINDEX__, see T16899
4424  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4425  $this->mOutput->setIndexPolicy( 'noindex' );
4426  $this->addTrackingCategory( 'noindex-category' );
4427  }
4428  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4429  $this->mOutput->setIndexPolicy( 'index' );
4430  $this->addTrackingCategory( 'index-category' );
4431  }
4432 
4433  # Cache all double underscores in the database
4434  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4435  $this->mOutput->setProperty( $key, '' );
4436  }
4437 
4438  return $text;
4439  }
4440 
4446  public function addTrackingCategory( $msg ) {
4447  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4448  }
4449 
4467  public function formatHeadings( $text, $origText, $isMain = true ) {
4468  wfDeprecated( __METHOD__, '1.34' );
4469  return $this->finalizeHeadings( $text, $origText, $isMain );
4470  }
4471 
4487  private function finalizeHeadings( $text, $origText, $isMain = true ) {
4488  # Inhibit editsection links if requested in the page
4489  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4490  $maybeShowEditLink = false;
4491  } else {
4492  $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4493  }
4494 
4495  # Get all headlines for numbering them and adding funky stuff like [edit]
4496  # links - this is for later, but we need the number of headlines right now
4497  # NOTE: white space in headings have been trimmed in handleHeadings. They shouldn't
4498  # be trimmed here since whitespace in HTML headings is significant.
4499  $matches = [];
4500  $numMatches = preg_match_all(
4501  '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4502  $text,
4503  $matches
4504  );
4505 
4506  # if there are fewer than 4 headlines in the article, do not show TOC
4507  # unless it's been explicitly enabled.
4508  $enoughToc = $this->mShowToc &&
4509  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4510 
4511  # Allow user to stipulate that a page should have a "new section"
4512  # link added via __NEWSECTIONLINK__
4513  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4514  $this->mOutput->setNewSection( true );
4515  }
4516 
4517  # Allow user to remove the "new section"
4518  # link via __NONEWSECTIONLINK__
4519  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4520  $this->mOutput->hideNewSection( true );
4521  }
4522 
4523  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4524  # override above conditions and always show TOC above first header
4525  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4526  $this->mShowToc = true;
4527  $enoughToc = true;
4528  }
4529 
4530  # headline counter
4531  $headlineCount = 0;
4532  $numVisible = 0;
4533 
4534  # Ugh .. the TOC should have neat indentation levels which can be
4535  # passed to the skin functions. These are determined here
4536  $toc = '';
4537  $full = '';
4538  $head = [];
4539  $sublevelCount = [];
4540  $levelCount = [];
4541  $level = 0;
4542  $prevlevel = 0;
4543  $toclevel = 0;
4544  $prevtoclevel = 0;
4545  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4546  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4547  $oldType = $this->mOutputType;
4548  $this->setOutputType( self::OT_WIKI );
4549  $frame = $this->getPreprocessor()->newFrame();
4550  $root = $this->preprocessToDom( $origText );
4551  $node = $root->getFirstChild();
4552  $byteOffset = 0;
4553  $tocraw = [];
4554  $refers = [];
4555 
4556  $headlines = $numMatches !== false ? $matches[3] : [];
4557 
4558  $maxTocLevel = $this->svcOptions->get( 'MaxTocLevel' );
4559  foreach ( $headlines as $headline ) {
4560  $isTemplate = false;
4561  $titleText = false;
4562  $sectionIndex = false;
4563  $numbering = '';
4564  $markerMatches = [];
4565  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4566  $serial = $markerMatches[1];
4567  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4568  $isTemplate = ( $titleText != $baseTitleText );
4569  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4570  }
4571 
4572  if ( $toclevel ) {
4573  $prevlevel = $level;
4574  }
4575  $level = $matches[1][$headlineCount];
4576 
4577  if ( $level > $prevlevel ) {
4578  # Increase TOC level
4579  $toclevel++;
4580  $sublevelCount[$toclevel] = 0;
4581  if ( $toclevel < $maxTocLevel ) {
4582  $prevtoclevel = $toclevel;
4583  $toc .= Linker::tocIndent();
4584  $numVisible++;
4585  }
4586  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4587  # Decrease TOC level, find level to jump to
4588 
4589  for ( $i = $toclevel; $i > 0; $i-- ) {
4590  if ( $levelCount[$i] == $level ) {
4591  # Found last matching level
4592  $toclevel = $i;
4593  break;
4594  } elseif ( $levelCount[$i] < $level ) {
4595  # Found first matching level below current level
4596  $toclevel = $i + 1;
4597  break;
4598  }
4599  }
4600  if ( $i == 0 ) {
4601  $toclevel = 1;
4602  }
4603  if ( $toclevel < $maxTocLevel ) {
4604  if ( $prevtoclevel < $maxTocLevel ) {
4605  # Unindent only if the previous toc level was shown :p
4606  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4607  $prevtoclevel = $toclevel;
4608  } else {
4609  $toc .= Linker::tocLineEnd();
4610  }
4611  }
4612  } else {
4613  # No change in level, end TOC line
4614  if ( $toclevel < $maxTocLevel ) {
4615  $toc .= Linker::tocLineEnd();
4616  }
4617  }
4618 
4619  $levelCount[$toclevel] = $level;
4620 
4621  # count number of headlines for each level
4622  $sublevelCount[$toclevel]++;
4623  $dot = 0;
4624  for ( $i = 1; $i <= $toclevel; $i++ ) {
4625  if ( !empty( $sublevelCount[$i] ) ) {
4626  if ( $dot ) {
4627  $numbering .= '.';
4628  }
4629  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4630  $dot = 1;
4631  }
4632  }
4633 
4634  # The safe header is a version of the header text safe to use for links
4635 
4636  # Remove link placeholders by the link text.
4637  # <!--LINK number-->
4638  # turns into
4639  # link text with suffix
4640  # Do this before unstrip since link text can contain strip markers
4641  $safeHeadline = $this->replaceLinkHoldersTextPrivate( $headline );
4642 
4643  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4644  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4645 
4646  # Remove any <style> or <script> tags (T198618)
4647  $safeHeadline = preg_replace(
4648  '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4649  '',
4650  $safeHeadline
4651  );
4652 
4653  # Strip out HTML (first regex removes any tag not allowed)
4654  # Allowed tags are:
4655  # * <sup> and <sub> (T10393)
4656  # * <i> (T28375)
4657  # * <b> (r105284)
4658  # * <bdi> (T74884)
4659  # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4660  # * <s> and <strike> (T35715)
4661  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4662  # to allow setting directionality in toc items.
4663  $tocline = preg_replace(
4664  [
4665  '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4666  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4667  ],
4668  [ '', '<$1>' ],
4669  $safeHeadline
4670  );
4671 
4672  # Strip '<span></span>', which is the result from the above if
4673  # <span id="foo"></span> is used to produce an additional anchor
4674  # for a section.
4675  $tocline = str_replace( '<span></span>', '', $tocline );
4676 
4677  $tocline = trim( $tocline );
4678 
4679  # For the anchor, strip out HTML-y stuff period
4680  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4681  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4682 
4683  # Save headline for section edit hint before it's escaped
4684  $headlineHint = $safeHeadline;
4685 
4686  # Decode HTML entities
4687  $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4688 
4689  $safeHeadline = self::normalizeSectionName( $safeHeadline );
4690 
4691  $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4692  $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4693  $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4694  if ( $fallbackHeadline === $safeHeadline ) {
4695  # No reason to have both (in fact, we can't)
4696  $fallbackHeadline = false;
4697  }
4698 
4699  # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4700  # @todo FIXME: We may be changing them depending on the current locale.
4701  $arrayKey = strtolower( $safeHeadline );
4702  if ( $fallbackHeadline === false ) {
4703  $fallbackArrayKey = false;
4704  } else {
4705  $fallbackArrayKey = strtolower( $fallbackHeadline );
4706  }
4707 
4708  # Create the anchor for linking from the TOC to the section
4709  $anchor = $safeHeadline;
4710  $fallbackAnchor = $fallbackHeadline;
4711  if ( isset( $refers[$arrayKey] ) ) {
4712  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4713  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4714  $anchor .= "_$i";
4715  $linkAnchor .= "_$i";
4716  $refers["${arrayKey}_$i"] = true;
4717  } else {
4718  $refers[$arrayKey] = true;
4719  }
4720  if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4721  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4722  for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4723  $fallbackAnchor .= "_$i";
4724  $refers["${fallbackArrayKey}_$i"] = true;
4725  } else {
4726  $refers[$fallbackArrayKey] = true;
4727  }
4728 
4729  # Don't number the heading if it is the only one (looks silly)
4730  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4731  # the two are different if the line contains a link
4732  $headline = Html::element(
4733  'span',
4734  [ 'class' => 'mw-headline-number' ],
4735  $numbering
4736  ) . ' ' . $headline;
4737  }
4738 
4739  if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4740  $toc .= Linker::tocLine( $linkAnchor, $tocline,
4741  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4742  }
4743 
4744  # Add the section to the section tree
4745  # Find the DOM node for this header
4746  $noOffset = ( $isTemplate || $sectionIndex === false );
4747  while ( $node && !$noOffset ) {
4748  if ( $node->getName() === 'h' ) {
4749  $bits = $node->splitHeading();
4750  if ( $bits['i'] == $sectionIndex ) {
4751  break;
4752  }
4753  }
4754  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4755  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4756  $node = $node->getNextSibling();
4757  }
4758  $tocraw[] = [
4759  'toclevel' => $toclevel,
4760  'level' => $level,
4761  'line' => $tocline,
4762  'number' => $numbering,
4763  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4764  'fromtitle' => $titleText,
4765  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4766  'anchor' => $anchor,
4767  ];
4768 
4769  # give headline the correct <h#> tag
4770  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4771  // Output edit section links as markers with styles that can be customized by skins
4772  if ( $isTemplate ) {
4773  # Put a T flag in the section identifier, to indicate to extractSections()
4774  # that sections inside <includeonly> should be counted.
4775  $editsectionPage = $titleText;
4776  $editsectionSection = "T-$sectionIndex";
4777  $editsectionContent = null;
4778  } else {
4779  $editsectionPage = $this->mTitle->getPrefixedText();
4780  $editsectionSection = $sectionIndex;
4781  $editsectionContent = $headlineHint;
4782  }
4783  // We use a bit of pesudo-xml for editsection markers. The
4784  // language converter is run later on. Using a UNIQ style marker
4785  // leads to the converter screwing up the tokens when it
4786  // converts stuff. And trying to insert strip tags fails too. At
4787  // this point all real inputted tags have already been escaped,
4788  // so we don't have to worry about a user trying to input one of
4789  // these markers directly. We use a page and section attribute
4790  // to stop the language converter from converting these
4791  // important bits of data, but put the headline hint inside a
4792  // content block because the language converter is supposed to
4793  // be able to convert that piece of data.
4794  // Gets replaced with html in ParserOutput::getText
4795  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4796  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4797  if ( $editsectionContent !== null ) {
4798  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4799  } else {
4800  $editlink .= '/>';
4801  }
4802  } else {
4803  $editlink = '';
4804  }
4805  $head[$headlineCount] = Linker::makeHeadline( $level,
4806  $matches['attrib'][$headlineCount], $anchor, $headline,
4807  $editlink, $fallbackAnchor );
4808 
4809  $headlineCount++;
4810  }
4811 
4812  $this->setOutputType( $oldType );
4813 
4814  # Never ever show TOC if no headers
4815  if ( $numVisible < 1 ) {
4816  $enoughToc = false;
4817  }
4818 
4819  if ( $enoughToc ) {
4820  if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4821  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4822  }
4823  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4824  $this->mOutput->setTOCHTML( $toc );
4825  $toc = self::TOC_START . $toc . self::TOC_END;
4826  }
4827 
4828  if ( $isMain ) {
4829  $this->mOutput->setSections( $tocraw );
4830  }
4831 
4832  # split up and insert constructed headlines
4833  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4834  $i = 0;
4835 
4836  // build an array of document sections
4837  $sections = [];
4838  foreach ( $blocks as $block ) {
4839  // $head is zero-based, sections aren't.
4840  if ( empty( $head[$i - 1] ) ) {
4841  $sections[$i] = $block;
4842  } else {
4843  $sections[$i] = $head[$i - 1] . $block;
4844  }
4845 
4856  Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4857 
4858  $i++;
4859  }
4860 
4861  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4862  // append the TOC at the beginning
4863  // Top anchor now in skin
4864  $sections[0] .= $toc . "\n";
4865  }
4866 
4867  $full .= implode( '', $sections );
4868 
4869  if ( $this->mForceTocPosition ) {
4870  return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4871  } else {
4872  return $full;
4873  }
4874  }
4875 
4887  public function preSaveTransform( $text, Title $title, User $user,
4888  ParserOptions $options, $clearState = true
4889  ) {
4890  if ( $clearState ) {
4891  $magicScopeVariable = $this->lock();
4892  }
4893  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4894  $this->setUser( $user );
4895 
4896  // Strip U+0000 NULL (T159174)
4897  $text = str_replace( "\000", '', $text );
4898 
4899  // We still normalize line endings for backwards-compatibility
4900  // with other code that just calls PST, but this should already
4901  // be handled in TextContent subclasses
4902  $text = TextContent::normalizeLineEndings( $text );
4903 
4904  if ( $options->getPreSaveTransform() ) {
4905  $text = $this->pstPass2( $text, $user );
4906  }
4907  $text = $this->mStripState->unstripBoth( $text );
4908 
4909  $this->setUser( null ); # Reset
4910 
4911  return $text;
4912  }
4913 
4922  private function pstPass2( $text, $user ) {
4923  # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4924  # $this->contLang here in order to give everyone the same signature and use the default one
4925  # rather than the one selected in each user's preferences. (see also T14815)
4926  $ts = $this->mOptions->getTimestamp();
4927  $timestamp = MWTimestamp::getLocalInstance( $ts );
4928  $ts = $timestamp->format( 'YmdHis' );
4929  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4930 
4931  $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4932 
4933  # Variable replacement
4934  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4935  $text = $this->replaceVariables( $text );
4936 
4937  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4938  # which may corrupt this parser instance via its wfMessage()->text() call-
4939 
4940  # Signatures
4941  if ( strpos( $text, '~~~' ) !== false ) {
4942  $sigText = $this->getUserSig( $user );
4943  $text = strtr( $text, [
4944  '~~~~~' => $d,
4945  '~~~~' => "$sigText $d",
4946  '~~~' => $sigText
4947  ] );
4948  # The main two signature forms used above are time-sensitive
4949  $this->setOutputFlag( 'user-signature', 'User signature detected' );
4950  }
4951 
4952  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4953  $tc = '[' . Title::legalChars() . ']';
4954  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4955 
4956  // [[ns:page (context)|]]
4957  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4958  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4959  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4960  // [[ns:page (context), context|]] (using either single or double-width comma)
4961  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4962  // [[|page]] (reverse pipe trick: add context from page title)
4963  $p2 = "/\[\[\\|($tc+)]]/";
4964 
4965  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4966  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4967  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4968  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4969 
4970  $t = $this->mTitle->getText();
4971  $m = [];
4972  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4973  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4974  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4975  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4976  } else {
4977  # if there's no context, don't bother duplicating the title
4978  $text = preg_replace( $p2, '[[\\1]]', $text );
4979  }
4980 
4981  return $text;
4982  }
4983 
4998  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4999  $username = $user->getName();
5000 
5001  # If not given, retrieve from the user object.
5002  if ( $nickname === false ) {
5003  $nickname = $user->getOption( 'nickname' );
5004  }
5005 
5006  if ( is_null( $fancySig ) ) {
5007  $fancySig = $user->getBoolOption( 'fancysig' );
5008  }
5009 
5010  $nickname = $nickname == null ? $username : $nickname;
5011 
5012  if ( mb_strlen( $nickname ) > $this->svcOptions->get( 'MaxSigChars' ) ) {
5013  $nickname = $username;
5014  $this->logger->debug( __METHOD__ . ": $username has overlong signature." );
5015  } elseif ( $fancySig !== false ) {
5016  # Sig. might contain markup; validate this
5017  if ( $this->validateSig( $nickname ) !== false ) {
5018  # Validated; clean up (if needed) and return it
5019  return $this->cleanSig( $nickname, true );
5020  } else {
5021  # Failed to validate; fall back to the default
5022  $nickname = $username;
5023  $this->logger->debug( __METHOD__ . ": $username has bad XML tags in signature." );
5024  }
5025  }
5026 
5027  # Make sure nickname doesnt get a sig in a sig
5028  $nickname = self::cleanSigInSig( $nickname );
5029 
5030  # If we're still here, make it a link to the user page
5031  $userText = wfEscapeWikiText( $username );
5032  $nickText = wfEscapeWikiText( $nickname );
5033  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
5034 
5035  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
5036  ->title( $this->getTitle() )->text();
5037  }
5038 
5045  public function validateSig( $text ) {
5046  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
5047  }
5048 
5059  public function cleanSig( $text, $parsing = false ) {
5060  if ( !$parsing ) {
5061  global $wgTitle;
5062  $magicScopeVariable = $this->lock();
5063  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
5064  }
5065 
5066  # Option to disable this feature
5067  if ( !$this->mOptions->getCleanSignatures() ) {
5068  return $text;
5069  }
5070 
5071  # @todo FIXME: Regex doesn't respect extension tags or nowiki
5072  # => Move this logic to braceSubstitution()
5073  $substWord = $this->magicWordFactory->get( 'subst' );
5074  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
5075  $substText = '{{' . $substWord->getSynonym( 0 );
5076 
5077  $text = preg_replace( $substRegex, $substText, $text );
5078  $text = self::cleanSigInSig( $text );
5079  $dom = $this->preprocessToDom( $text );
5080  $frame = $this->getPreprocessor()->newFrame();
5081  $text = $frame->expand( $dom );
5082 
5083  if ( !$parsing ) {
5084  $text = $this->mStripState->unstripBoth( $text );
5085  }
5086 
5087  return $text;
5088  }
5089 
5096  public static function cleanSigInSig( $text ) {
5097  $text = preg_replace( '/~{3,5}/', '', $text );
5098  return $text;
5099  }
5100 
5111  public function startExternalParse( Title $title = null, ParserOptions $options,
5112  $outputType, $clearState = true, $revId = null
5113  ) {
5114  $this->startParse( $title, $options, $outputType, $clearState );
5115  if ( $revId !== null ) {
5116  $this->mRevisionId = $revId;
5117  }
5118  }
5119 
5126  private function startParse( Title $title = null, ParserOptions $options,
5127  $outputType, $clearState = true
5128  ) {
5129  $this->setTitle( $title );
5130  $this->mOptions = $options;
5131  $this->setOutputType( $outputType );
5132  if ( $clearState ) {
5133  $this->clearState();
5134  }
5135  }
5136 
5145  public function transformMsg( $text, $options, $title = null ) {
5146  static $executing = false;
5147 
5148  # Guard against infinite recursion
5149  if ( $executing ) {
5150  return $text;
5151  }
5152  $executing = true;
5153 
5154  if ( !$title ) {
5155  global $wgTitle;
5156  $title = $wgTitle;
5157  }
5158 
5159  $text = $this->preprocess( $text, $title, $options );
5160 
5161  $executing = false;
5162  return $text;
5163  }
5164 
5189  public function setHook( $tag, callable $callback ) {
5190  $tag = strtolower( $tag );
5191  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5192  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5193  }
5194  $oldVal = $this->mTagHooks[$tag] ?? null;
5195  $this->mTagHooks[$tag] = $callback;
5196  if ( !in_array( $tag, $this->mStripList ) ) {
5197  $this->mStripList[] = $tag;
5198  }
5199 
5200  return $oldVal;
5201  }
5202 
5220  public function setTransparentTagHook( $tag, callable $callback ) {
5221  $tag = strtolower( $tag );
5222  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5223  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5224  }
5225  $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
5226  $this->mTransparentTagHooks[$tag] = $callback;
5227 
5228  return $oldVal;
5229  }
5230 
5234  public function clearTagHooks() {
5235  $this->mTagHooks = [];
5236  $this->mFunctionTagHooks = [];
5237  $this->mStripList = $this->mDefaultStripList;
5238  }
5239 
5283  public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
5284  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5285  $this->mFunctionHooks[$id] = [ $callback, $flags ];
5286 
5287  # Add to function cache
5288  $mw = $this->magicWordFactory->get( $id );
5289  if ( !$mw ) {
5290  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5291  }
5292 
5293  $synonyms = $mw->getSynonyms();
5294  $sensitive = intval( $mw->isCaseSensitive() );
5295 
5296  foreach ( $synonyms as $syn ) {
5297  # Case
5298  if ( !$sensitive ) {
5299  $syn = $this->contLang->lc( $syn );
5300  }
5301  # Add leading hash
5302  if ( !( $flags & self::SFH_NO_HASH ) ) {
5303  $syn = '#' . $syn;
5304  }
5305  # Remove trailing colon
5306  if ( substr( $syn, -1, 1 ) === ':' ) {
5307  $syn = substr( $syn, 0, -1 );
5308  }
5309  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5310  }
5311  return $oldVal;
5312  }
5313 
5319  public function getFunctionHooks() {
5320  $this->firstCallInit();
5321  return array_keys( $this->mFunctionHooks );
5322  }
5323 
5334  public function setFunctionTagHook( $tag, callable $callback, $flags ) {
5335  $tag = strtolower( $tag );
5336  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5337  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5338  }
5339  $old = $this->mFunctionTagHooks[$tag] ?? null;
5340  $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5341 
5342  if ( !in_array( $tag, $this->mStripList ) ) {
5343  $this->mStripList[] = $tag;
5344  }
5345 
5346  return $old;
5347  }
5348 
5357  public function replaceLinkHolders( &$text, $options = 0 ) {
5358  $this->replaceLinkHoldersPrivate( $text, $options );
5359  }
5360 
5368  private function replaceLinkHoldersPrivate( &$text, $options = 0 ) {
5369  $this->mLinkHolders->replace( $text );
5370  }
5371 
5380  public function replaceLinkHoldersText( $text ) {
5381  wfDeprecated( __METHOD__, '1.34' );
5382  return $this->replaceLinkHoldersTextPrivate( $text );
5383  }
5384 
5392  private function replaceLinkHoldersTextPrivate( $text ) {
5393  return $this->mLinkHolders->replaceText( $text );
5394  }
5395 
5409  public function renderImageGallery( $text, $params ) {
5410  $mode = false;
5411  if ( isset( $params['mode'] ) ) {
5412  $mode = $params['mode'];
5413  }
5414 
5415  try {
5416  $ig = ImageGalleryBase::factory( $mode );
5417  } catch ( Exception $e ) {
5418  // If invalid type set, fallback to default.
5419  $ig = ImageGalleryBase::factory( false );
5420  }
5421 
5422  $ig->setContextTitle( $this->mTitle );
5423  $ig->setShowBytes( false );
5424  $ig->setShowDimensions( false );
5425  $ig->setShowFilename( false );
5426  $ig->setParser( $this );
5427  $ig->setHideBadImages();
5428  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5429 
5430  if ( isset( $params['showfilename'] ) ) {
5431  $ig->setShowFilename( true );
5432  } else {
5433  $ig->setShowFilename( false );
5434  }
5435  if ( isset( $params['caption'] ) ) {
5436  // NOTE: We aren't passing a frame here or below. Frame info
5437  // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5438  // See T107332#4030581
5439  $caption = $this->recursiveTagParse( $params['caption'] );
5440  $ig->setCaptionHtml( $caption );
5441  }
5442  if ( isset( $params['perrow'] ) ) {
5443  $ig->setPerRow( $params['perrow'] );
5444  }
5445  if ( isset( $params['widths'] ) ) {
5446  $ig->setWidths( $params['widths'] );
5447  }
5448  if ( isset( $params['heights'] ) ) {
5449  $ig->setHeights( $params['heights'] );
5450  }
5451  $ig->setAdditionalOptions( $params );
5452 
5453  // Avoid PHP 7.1 warning from passing $this by reference
5454  $parser = $this;
5455  Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5456 
5457  $lines = StringUtils::explode( "\n", $text );
5458  foreach ( $lines as $line ) {
5459  # match lines like these:
5460  # Image:someimage.jpg|This is some image
5461  $matches = [];
5462  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5463  # Skip empty lines
5464  if ( count( $matches ) == 0 ) {
5465  continue;
5466  }
5467 
5468  if ( strpos( $matches[0], '%' ) !== false ) {
5469  $matches[1] = rawurldecode( $matches[1] );
5470  }
5472  if ( is_null( $title ) ) {
5473  # Bogus title. Ignore these so we don't bomb out later.
5474  continue;
5475  }
5476 
5477  # We need to get what handler the file uses, to figure out parameters.
5478  # Note, a hook can overide the file name, and chose an entirely different
5479  # file (which potentially could be of a different type and have different handler).
5480  $options = [];
5481  $descQuery = false;
5482  Hooks::run( 'BeforeParserFetchFileAndTitle',
5483  [ $this, $title, &$options, &$descQuery ] );
5484  # Don't register it now, as TraditionalImageGallery does that later.
5485  $file = $this->fetchFileNoRegister( $title, $options );
5486  $handler = $file ? $file->getHandler() : false;
5487 
5488  $paramMap = [
5489  'img_alt' => 'gallery-internal-alt',
5490  'img_link' => 'gallery-internal-link',
5491  ];
5492  if ( $handler ) {
5493  $paramMap += $handler->getParamMap();
5494  // We don't want people to specify per-image widths.
5495  // Additionally the width parameter would need special casing anyhow.
5496  unset( $paramMap['img_width'] );
5497  }
5498 
5499  $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5500 
5501  $label = '';
5502  $alt = '';
5503  $link = '';
5504  $handlerOptions = [];
5505  if ( isset( $matches[3] ) ) {
5506  // look for an |alt= definition while trying not to break existing
5507  // captions with multiple pipes (|) in it, until a more sensible grammar
5508  // is defined for images in galleries
5509 
5510  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5511  // splitting on '|' is a bit odd, and different from makeImage.
5512  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5513  // Protect LanguageConverter markup
5514  $parameterMatches = StringUtils::delimiterExplode(
5515  '-{', '}-', '|', $matches[3], true /* nested */
5516  );
5517 
5518  foreach ( $parameterMatches as $parameterMatch ) {
5519  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5520  if ( $magicName ) {
5521  $paramName = $paramMap[$magicName];
5522 
5523  switch ( $paramName ) {
5524  case 'gallery-internal-alt':
5525  $alt = $this->stripAltTextPrivate( $match, false );
5526  break;
5527  case 'gallery-internal-link':
5528  $linkValue = $this->stripAltTextPrivate( $match, false );
5529  if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5530  // Result of LanguageConverter::markNoConversion
5531  // invoked on an external link.
5532  $linkValue = substr( $linkValue, 4, -2 );
5533  }
5534  list( $type, $target ) = $this->parseLinkParameterPrivate( $linkValue );
5535  if ( $type === 'link-url' ) {
5536  $link = $target;
5537  $this->mOutput->addExternalLink( $target );
5538  } elseif ( $type === 'link-title' ) {
5539  $link = $target->getLinkURL();
5540  $this->mOutput->addLink( $target );
5541  }
5542  break;
5543  default:
5544  // Must be a handler specific parameter.
5545  if ( $handler->validateParam( $paramName, $match ) ) {
5546  $handlerOptions[$paramName] = $match;
5547  } else {
5548  // Guess not, consider it as caption.
5549  $this->logger->debug(
5550  "$parameterMatch failed parameter validation" );
5551  $label = $parameterMatch;
5552  }
5553  }
5554 
5555  } else {
5556  // Last pipe wins.
5557  $label = $parameterMatch;
5558  }
5559  }
5560  }
5561 
5562  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5563  }
5564  $html = $ig->toHTML();
5565  Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5566  return $html;
5567  }
5568 
5574  public function getImageParams( $handler ) {
5575  wfDeprecated( __METHOD__, '1.34' );
5576  return $this->getImageParamsPrivate( $handler );
5577  }
5578 
5583  private function getImageParamsPrivate( $handler ) {
5584  if ( $handler ) {
5585  $handlerClass = get_class( $handler );
5586  } else {
5587  $handlerClass = '';
5588  }
5589  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5590  # Initialise static lists
5591  static $internalParamNames = [
5592  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5593  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5594  'bottom', 'text-bottom' ],
5595  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5596  'upright', 'border', 'link', 'alt', 'class' ],
5597  ];
5598  static $internalParamMap;
5599  if ( !$internalParamMap ) {
5600  $internalParamMap = [];
5601  foreach ( $internalParamNames as $type => $names ) {
5602  foreach ( $names as $name ) {
5603  // For grep: img_left, img_right, img_center, img_none,
5604  // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5605  // img_bottom, img_text_bottom,
5606  // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5607  // img_border, img_link, img_alt, img_class
5608  $magicName = str_replace( '-', '_', "img_$name" );
5609  $internalParamMap[$magicName] = [ $type, $name ];
5610  }
5611  }
5612  }
5613 
5614  # Add handler params
5615  $paramMap = $internalParamMap;
5616  if ( $handler ) {
5617  $handlerParamMap = $handler->getParamMap();
5618  foreach ( $handlerParamMap as $magic => $paramName ) {
5619  $paramMap[$magic] = [ 'handler', $paramName ];
5620  }
5621  }
5622  $this->mImageParams[$handlerClass] = $paramMap;
5623  $this->mImageParamsMagicArray[$handlerClass] =
5624  $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5625  }
5626  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5627  }
5628 
5637  public function makeImage( $title, $options, $holders = false ) {
5638  # Check if the options text is of the form "options|alt text"
5639  # Options are:
5640  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5641  # * left no resizing, just left align. label is used for alt= only
5642  # * right same, but right aligned
5643  # * none same, but not aligned
5644  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5645  # * center center the image
5646  # * frame Keep original image size, no magnify-button.
5647  # * framed Same as "frame"
5648  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5649  # * upright reduce width for upright images, rounded to full __0 px
5650  # * border draw a 1px border around the image
5651  # * alt Text for HTML alt attribute (defaults to empty)
5652  # * class Set a class for img node
5653  # * link Set the target of the image link. Can be external, interwiki, or local
5654  # vertical-align values (no % or length right now):
5655  # * baseline
5656  # * sub
5657  # * super
5658  # * top
5659  # * text-top
5660  # * middle
5661  # * bottom
5662  # * text-bottom
5663 
5664  # Protect LanguageConverter markup when splitting into parts
5666  '-{', '}-', '|', $options, true /* allow nesting */
5667  );
5668 
5669  # Give extensions a chance to select the file revision for us
5670  $options = [];
5671  $descQuery = false;
5672  Hooks::run( 'BeforeParserFetchFileAndTitle',
5673  [ $this, $title, &$options, &$descQuery ] );
5674  # Fetch and register the file (file title may be different via hooks)
5675  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5676 
5677  # Get parameter map
5678  $handler = $file ? $file->getHandler() : false;
5679 
5680  list( $paramMap, $mwArray ) = $this->getImageParamsPrivate( $handler );
5681 
5682  if ( !$file ) {
5683  $this->addTrackingCategory( 'broken-file-category' );
5684  }
5685 
5686  # Process the input parameters
5687  $caption = '';
5688  $params = [ 'frame' => [], 'handler' => [],
5689  'horizAlign' => [], 'vertAlign' => [] ];
5690  $seenformat = false;
5691  foreach ( $parts as $part ) {
5692  $part = trim( $part );
5693  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5694  $validated = false;
5695  if ( isset( $paramMap[$magicName] ) ) {
5696  list( $type, $paramName ) = $paramMap[$magicName];
5697 
5698  # Special case; width and height come in one variable together
5699  if ( $type === 'handler' && $paramName === 'width' ) {
5700  $parsedWidthParam = self::parseWidthParam( $value );
5701  if ( isset( $parsedWidthParam['width'] ) ) {
5702  $width = $parsedWidthParam['width'];
5703  if ( $handler->validateParam( 'width', $width ) ) {
5704  $params[$type]['width'] = $width;
5705  $validated = true;
5706  }
5707  }
5708  if ( isset( $parsedWidthParam['height'] ) ) {
5709  $height = $parsedWidthParam['height'];
5710  if ( $handler->validateParam( 'height', $height ) ) {
5711  $params[$type]['height'] = $height;
5712  $validated = true;
5713  }
5714  }
5715  # else no validation -- T15436
5716  } else {
5717  if ( $type === 'handler' ) {
5718  # Validate handler parameter
5719  $validated = $handler->validateParam( $paramName, $value );
5720  } else {
5721  # Validate internal parameters
5722  switch ( $paramName ) {
5723  case 'manualthumb':
5724  case 'alt':
5725  case 'class':
5726  # @todo FIXME: Possibly check validity here for
5727  # manualthumb? downstream behavior seems odd with
5728  # missing manual thumbs.
5729  $validated = true;
5730  $value = $this->stripAltTextPrivate( $value, $holders );
5731  break;
5732  case 'link':
5733  list( $paramName, $value ) =
5734  $this->parseLinkParameterPrivate(
5735  $this->stripAltTextPrivate( $value, $holders )
5736  );
5737  if ( $paramName ) {
5738  $validated = true;
5739  if ( $paramName === 'no-link' ) {
5740  $value = true;
5741  }
5742  if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5743  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5744  }
5745  }
5746  break;
5747  case 'frameless':
5748  case 'framed':
5749  case 'thumbnail':
5750  // use first appearing option, discard others.
5751  $validated = !$seenformat;
5752  $seenformat = true;
5753  break;
5754  default:
5755  # Most other things appear to be empty or numeric...
5756  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5757  }
5758  }
5759 
5760  if ( $validated ) {
5761  $params[$type][$paramName] = $value;
5762  }
5763  }
5764  }
5765  if ( !$validated ) {
5766  $caption = $part;
5767  }
5768  }
5769 
5770  # Process alignment parameters
5771  if ( $params['horizAlign'] ) {
5772  $params['frame']['align'] = key( $params['horizAlign'] );
5773  }
5774  if ( $params['vertAlign'] ) {
5775  $params['frame']['valign'] = key( $params['vertAlign'] );
5776  }
5777 
5778  $params['frame']['caption'] = $caption;
5779 
5780  # Will the image be presented in a frame, with the caption below?
5781  $imageIsFramed = isset( $params['frame']['frame'] )
5782  || isset( $params['frame']['framed'] )
5783  || isset( $params['frame']['thumbnail'] )
5784  || isset( $params['frame']['manualthumb'] );
5785 
5786  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5787  # came to also set the caption, ordinary text after the image -- which
5788  # makes no sense, because that just repeats the text multiple times in
5789  # screen readers. It *also* came to set the title attribute.
5790  # Now that we have an alt attribute, we should not set the alt text to
5791  # equal the caption: that's worse than useless, it just repeats the
5792  # text. This is the framed/thumbnail case. If there's no caption, we
5793  # use the unnamed parameter for alt text as well, just for the time be-
5794  # ing, if the unnamed param is set and the alt param is not.
5795  # For the future, we need to figure out if we want to tweak this more,
5796  # e.g., introducing a title= parameter for the title; ignoring the un-
5797  # named parameter entirely for images without a caption; adding an ex-
5798  # plicit caption= parameter and preserving the old magic unnamed para-
5799  # meter for BC; ...
5800  if ( $imageIsFramed ) { # Framed image
5801  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5802  # No caption or alt text, add the filename as the alt text so
5803  # that screen readers at least get some description of the image
5804  $params['frame']['alt'] = $title->getText();
5805  }
5806  # Do not set $params['frame']['title'] because tooltips don't make sense
5807  # for framed images
5808  } else { # Inline image
5809  if ( !isset( $params['frame']['alt'] ) ) {
5810  # No alt text, use the "caption" for the alt text
5811  if ( $caption !== '' ) {
5812  $params['frame']['alt'] = $this->stripAltTextPrivate( $caption, $holders );
5813  } else {
5814  # No caption, fall back to using the filename for the
5815  # alt text
5816  $params['frame']['alt'] = $title->getText();
5817  }
5818  }
5819  # Use the "caption" for the tooltip text
5820  $params['frame']['title'] = $this->stripAltTextPrivate( $caption, $holders );
5821  }
5822  $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5823 
5824  Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5825 
5826  # Linker does the rest
5827  $time = $options['time'] ?? false;
5828  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5829  $time, $descQuery, $this->mOptions->getThumbSize() );
5830 
5831  # Give the handler a chance to modify the parser object
5832  if ( $handler ) {
5833  $handler->parserTransformHook( $this, $file );
5834  }
5835 
5836  return $ret;
5837  }
5838 
5858  public function parseLinkParameter( $value ) {
5859  wfDeprecated( __METHOD__, '1.34' );
5860  return $this->parseLinkParameterPrivate( $value );
5861  }
5862 
5881  private function parseLinkParameterPrivate( $value ) {
5882  $chars = self::EXT_LINK_URL_CLASS;
5883  $addr = self::EXT_LINK_ADDR;
5884  $prots = $this->mUrlProtocols;
5885  $type = null;
5886  $target = false;
5887  if ( $value === '' ) {
5888  $type = 'no-link';
5889  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5890  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5891  $this->mOutput->addExternalLink( $value );
5892  $type = 'link-url';
5893  $target = $value;
5894  }
5895  } else {
5896  $linkTitle = Title::newFromText( $value );
5897  if ( $linkTitle ) {
5898  $this->mOutput->addLink( $linkTitle );
5899  $type = 'link-title';
5900  $target = $linkTitle;
5901  }
5902  }
5903  return [ $type, $target ];
5904  }
5905 
5912  protected function stripAltText( $caption, $holders ) {
5913  wfDeprecated( __METHOD__, '1.34' );
5914  return $this->stripAltTextPrivate( $caption, $holders );
5915  }
5916 
5922  private function stripAltTextPrivate( $caption, $holders ) {
5923  # Strip bad stuff out of the title (tooltip). We can't just use
5924  # replaceLinkHoldersText() here, because if this function is called
5925  # from handleInternalLinks2(), mLinkHolders won't be up-to-date.
5926  if ( $holders ) {
5927  $tooltip = $holders->replaceText( $caption );
5928  } else {
5929  $tooltip = $this->replaceLinkHoldersTextPrivate( $caption );
5930  }
5931 
5932  # make sure there are no placeholders in thumbnail attributes
5933  # that are later expanded to html- so expand them now and
5934  # remove the tags
5935  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5936  # Compatibility hack! In HTML certain entity references not terminated
5937  # by a semicolon are decoded (but not if we're in an attribute; that's
5938  # how link URLs get away without properly escaping & in queries).
5939  # But wikitext has always required semicolon-termination of entities,
5940  # so encode & where needed to avoid decode of semicolon-less entities.
5941  # See T209236 and
5942  # https://www.w3.org/TR/html5/syntax.html#named-character-references
5943  # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5944  $tooltip = preg_replace( "/
5945  & # 1. entity prefix
5946  (?= # 2. followed by:
5947  (?: # a. one of the legacy semicolon-less named entities
5948  A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5949  C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5950  GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5951  O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5952  U(?:acute|circ|grave|uml)|Yacute|
5953  a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5954  c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5955  divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5956  frac(?:1(?:2|4)|34)|
5957  gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5958  i(?:acute|circ|excl|grave|quest|uml)|laquo|
5959  lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5960  m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5961  not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5962  o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5963  p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5964  s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5965  u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5966  )
5967  (?:[^;]|$)) # b. and not followed by a semicolon
5968  # S = study, for efficiency
5969  /Sx", '&amp;', $tooltip );
5970  $tooltip = Sanitizer::stripAllTags( $tooltip );
5971 
5972  return $tooltip;
5973  }
5974 
5980  public function disableCache() {
5981  wfDeprecated( __METHOD__, '1.28' );
5982  $this->logger->debug( "Parser output marked as uncacheable." );
5983  if ( !$this->mOutput ) {
5984  throw new MWException( __METHOD__ .
5985  " can only be called when actually parsing something" );
5986  }
5987  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5988  }
5989 
5998  public function attributeStripCallback( &$text, $frame = false ) {
5999  $text = $this->replaceVariables( $text, $frame );
6000  $text = $this->mStripState->unstripBoth( $text );
6001  return $text;
6002  }
6003 
6009  public function getTags() {
6010  $this->firstCallInit();
6011  return array_merge(
6012  array_keys( $this->mTransparentTagHooks ),
6013  array_keys( $this->mTagHooks ),
6014  array_keys( $this->mFunctionTagHooks )
6015  );
6016  }
6017 
6022  public function getFunctionSynonyms() {
6023  $this->firstCallInit();
6024  return $this->mFunctionSynonyms;
6025  }
6026 
6031  public function getUrlProtocols() {
6032  return $this->mUrlProtocols;
6033  }
6034 
6045  public function replaceTransparentTags( $text ) {
6046  $matches = [];
6047  $elements = array_keys( $this->mTransparentTagHooks );
6048  $text = self::extractTagsAndParams( $elements, $text, $matches );
6049  $replacements = [];
6050 
6051  foreach ( $matches as $marker => $data ) {
6052  list( $element, $content, $params, $tag ) = $data;
6053  $tagName = strtolower( $element );
6054  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
6055  $output = call_user_func_array(
6056  $this->mTransparentTagHooks[$tagName],
6057  [ $content, $params, $this ]
6058  );
6059  } else {
6060  $output = $tag;
6061  }
6062  $replacements[$marker] = $output;
6063  }
6064  return strtr( $text, $replacements );
6065  }
6066 
6096  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
6097  global $wgTitle; # not generally used but removes an ugly failure mode
6098 
6099  $magicScopeVariable = $this->lock();
6100  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
6101  $outText = '';
6102  $frame = $this->getPreprocessor()->newFrame();
6103 
6104  # Process section extraction flags
6105  $flags = 0;
6106  $sectionParts = explode( '-', $sectionId );
6107  $sectionIndex = array_pop( $sectionParts );
6108  foreach ( $sectionParts as $part ) {
6109  if ( $part === 'T' ) {
6110  $flags |= self::PTD_FOR_INCLUSION;
6111  }
6112  }
6113 
6114  # Check for empty input
6115  if ( strval( $text ) === '' ) {
6116  # Only sections 0 and T-0 exist in an empty document
6117  if ( $sectionIndex == 0 ) {
6118  if ( $mode === 'get' ) {
6119  return '';
6120  }
6121 
6122  return $newText;
6123  } else {
6124  if ( $mode === 'get' ) {
6125  return $newText;
6126  }
6127 
6128  return $text;
6129  }
6130  }
6131 
6132  # Preprocess the text
6133  $root = $this->preprocessToDom( $text, $flags );
6134 
6135  # <h> nodes indicate section breaks
6136  # They can only occur at the top level, so we can find them by iterating the root's children
6137  $node = $root->getFirstChild();
6138 
6139  # Find the target section
6140  if ( $sectionIndex == 0 ) {
6141  # Section zero doesn't nest, level=big
6142  $targetLevel = 1000;
6143  } else {
6144  while ( $node ) {
6145  if ( $node->getName() === 'h' ) {
6146  $bits = $node->splitHeading();
6147  if ( $bits['i'] == $sectionIndex ) {
6148  $targetLevel = $bits['level'];
6149  break;
6150  }
6151  }
6152  if ( $mode === 'replace' ) {
6153  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6154  }
6155  $node = $node->getNextSibling();
6156  }
6157  }
6158 
6159  if ( !$node ) {
6160  # Not found
6161  if ( $mode === 'get' ) {
6162  return $newText;
6163  } else {
6164  return $text;
6165  }
6166  }
6167 
6168  # Find the end of the section, including nested sections
6169  do {
6170  if ( $node->getName() === 'h' ) {
6171  $bits = $node->splitHeading();
6172  $curLevel = $bits['level'];
6173  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
6174  break;
6175  }
6176  }
6177  if ( $mode === 'get' ) {
6178  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6179  }
6180  $node = $node->getNextSibling();
6181  } while ( $node );
6182 
6183  # Write out the remainder (in replace mode only)
6184  if ( $mode === 'replace' ) {
6185  # Output the replacement text
6186  # Add two newlines on -- trailing whitespace in $newText is conventionally
6187  # stripped by the editor, so we need both newlines to restore the paragraph gap
6188  # Only add trailing whitespace if there is newText
6189  if ( $newText != "" ) {
6190  $outText .= $newText . "\n\n";
6191  }
6192 
6193  while ( $node ) {
6194  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
6195  $node = $node->getNextSibling();
6196  }
6197  }
6198 
6199  if ( is_string( $outText ) ) {
6200  # Re-insert stripped tags
6201  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
6202  }
6203 
6204  return $outText;
6205  }
6206 
6221  public function getSection( $text, $sectionId, $defaultText = '' ) {
6222  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
6223  }
6224 
6237  public function replaceSection( $oldText, $sectionId, $newText ) {
6238  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
6239  }
6240 
6251  public function getRevisionId() {
6252  return $this->mRevisionId;
6253  }
6254 
6261  public function getRevisionObject() {
6262  if ( $this->mRevisionObject ) {
6263  return $this->mRevisionObject;
6264  }
6265 
6266  // NOTE: try to get the RevisionObject even if mRevisionId is null.
6267  // This is useful when parsing a revision that has not yet been saved.
6268  // However, if we get back a saved revision even though we are in
6269  // preview mode, we'll have to ignore it, see below.
6270  // NOTE: This callback may be used to inject an OLD revision that was
6271  // already loaded, so "current" is a bit of a misnomer. We can't just
6272  // skip it if mRevisionId is set.
6273  $rev = call_user_func(
6274  $this->mOptions->getCurrentRevisionCallback(),
6275  $this->getTitle(),
6276  $this
6277  );
6278 
6279  if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
6280  // We are in preview mode (mRevisionId is null), and the current revision callback
6281  // returned an existing revision. Ignore it and return null, it's probably the page's
6282  // current revision, which is not what we want here. Note that we do want to call the
6283  // callback to allow the unsaved revision to be injected here, e.g. for
6284  // self-transclusion previews.
6285  return null;
6286  }
6287 
6288  // If the parse is for a new revision, then the callback should have
6289  // already been set to force the object and should match mRevisionId.
6290  // If not, try to fetch by mRevisionId for sanity.
6291  if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
6292  $rev = Revision::newFromId( $this->mRevisionId );
6293  }
6294 
6295  $this->mRevisionObject = $rev;
6296 
6297  return $this->mRevisionObject;
6298  }
6299 
6305  public function getRevisionTimestamp() {
6306  if ( $this->mRevisionTimestamp !== null ) {
6307  return $this->mRevisionTimestamp;
6308  }
6309 
6310  # Use specified revision timestamp, falling back to the current timestamp
6311  $revObject = $this->getRevisionObject();
6312  $timestamp = $revObject ? $revObject->getTimestamp() : $this->mOptions->getTimestamp();
6313  $this->mOutput->setRevisionTimestampUsed( $timestamp ); // unadjusted time zone
6314 
6315  # The cryptic '' timezone parameter tells to use the site-default
6316  # timezone offset instead of the user settings.
6317  # Since this value will be saved into the parser cache, served
6318  # to other users, and potentially even used inside links and such,
6319  # it needs to be consistent for all visitors.
6320  $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
6321 
6322  return $this->mRevisionTimestamp;
6323  }
6324 
6330  public function getRevisionUser() {
6331  if ( is_null( $this->mRevisionUser ) ) {
6332  $revObject = $this->getRevisionObject();
6333 
6334  # if this template is subst: the revision id will be blank,
6335  # so just use the current user's name
6336  if ( $revObject ) {
6337  $this->mRevisionUser = $revObject->getUserText();
6338  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6339  $this->mRevisionUser = $this->getUser()->getName();
6340  }
6341  }
6342  return $this->mRevisionUser;
6343  }
6344 
6350  public function getRevisionSize() {
6351  if ( is_null( $this->mRevisionSize ) ) {
6352  $revObject = $this->getRevisionObject();
6353 
6354  # if this variable is subst: the revision id will be blank,
6355  # so just use the parser input size, because the own substituation
6356  # will change the size.
6357  if ( $revObject ) {
6358  $this->mRevisionSize = $revObject->getSize();
6359  } else {
6360  $this->mRevisionSize = $this->mInputSize;
6361  }
6362  }
6363  return $this->mRevisionSize;
6364  }
6365 
6371  public function setDefaultSort( $sort ) {
6372  $this->mDefaultSort = $sort;
6373  $this->mOutput->setProperty( 'defaultsort', $sort );
6374  }
6375 
6386  public function getDefaultSort() {
6387  if ( $this->mDefaultSort !== false ) {
6388  return $this->mDefaultSort;
6389  } else {
6390  return '';
6391  }
6392  }
6393 
6400  public function getCustomDefaultSort() {
6401  return $this->mDefaultSort;
6402  }
6403 
6404  private static function getSectionNameFromStrippedText( $text ) {
6405  $text = Sanitizer::normalizeSectionNameWhitespace( $text );
6406  $text = Sanitizer::decodeCharReferences( $text );
6407  $text = self::normalizeSectionName( $text );
6408  return $text;
6409  }
6410 
6411  private static function makeAnchor( $sectionName ) {
6412  return '#' . Sanitizer::escapeIdForLink( $sectionName );
6413  }
6414 
6415  private function makeLegacyAnchor( $sectionName ) {
6416  $fragmentMode = $this->svcOptions->get( 'FragmentMode' );
6417  if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6418  // ForAttribute() and ForLink() are the same for legacy encoding
6419  $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6420  } else {
6421  $id = Sanitizer::escapeIdForLink( $sectionName );
6422  }
6423 
6424  return "#$id";
6425  }
6426 
6435  public function guessSectionNameFromWikiText( $text ) {
6436  # Strip out wikitext links(they break the anchor)
6437  $text = $this->stripSectionName( $text );
6438  $sectionName = self::getSectionNameFromStrippedText( $text );
6439  return self::makeAnchor( $sectionName );
6440  }
6441 
6451  public function guessLegacySectionNameFromWikiText( $text ) {
6452  # Strip out wikitext links(they break the anchor)
6453  $text = $this->stripSectionName( $text );
6454  $sectionName = self::getSectionNameFromStrippedText( $text );
6455  return $this->makeLegacyAnchor( $sectionName );
6456  }
6457 
6463  public static function guessSectionNameFromStrippedText( $text ) {
6464  $sectionName = self::getSectionNameFromStrippedText( $text );
6465  return self::makeAnchor( $sectionName );
6466  }
6467 
6474  private static function normalizeSectionName( $text ) {
6475  # T90902: ensure the same normalization is applied for IDs as to links
6476 
6477  $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6478  '@phan-var MediaWikiTitleCodec $titleParser';
6479  try {
6480 
6481  $parts = $titleParser->splitTitleString( "#$text" );
6482  } catch ( MalformedTitleException $ex ) {
6483  return $text;
6484  }
6485  return $parts['fragment'];
6486  }
6487 
6502  public function stripSectionName( $text ) {
6503  # Strip internal link markup
6504  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6505  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6506 
6507  # Strip external link markup
6508  # @todo FIXME: Not tolerant to blank link text
6509  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6510  # on how many empty links there are on the page - need to figure that out.
6511  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6512 
6513  # Parse wikitext quotes (italics & bold)
6514  $text = $this->doQuotes( $text );
6515 
6516  # Strip HTML tags
6517  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6518  return $text;
6519  }
6520 
6532  public function testSrvus( $text, Title $title, ParserOptions $options,
6533  $outputType = self::OT_HTML
6534  ) {
6535  wfDeprecated( __METHOD__, '1.34' );
6536  return $this->fuzzTestSrvus( $text, $title, $options, $outputType );
6537  }
6538 
6549  private function fuzzTestSrvus( $text, Title $title, ParserOptions $options,
6550  $outputType = self::OT_HTML
6551  ) {
6552  $magicScopeVariable = $this->lock();
6553  $this->startParse( $title, $options, $outputType, true );
6554 
6555  $text = $this->replaceVariables( $text );
6556  $text = $this->mStripState->unstripBoth( $text );
6557  $text = Sanitizer::removeHTMLtags( $text );
6558  return $text;
6559  }
6560 
6568  public function testPst( $text, Title $title, ParserOptions $options ) {
6569  wfDeprecated( __METHOD__, '1.34' );
6570  return $this->fuzzTestPst( $text, $title, $options );
6571  }
6572 
6579  private function fuzzTestPst( $text, Title $title, ParserOptions $options ) {
6580  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6581  }
6582 
6590  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6591  wfDeprecated( __METHOD__, '1.34' );
6592  return $this->fuzzTestPreprocess( $text, $title, $options );
6593  }
6594 
6601  private function fuzzTestPreprocess( $text, Title $title, ParserOptions $options ) {
6602  return $this->fuzzTestSrvus( $text, $title, $options, self::OT_PREPROCESS );
6603  }
6604 
6621  public function markerSkipCallback( $s, $callback ) {
6622  $i = 0;
6623  $out = '';
6624  while ( $i < strlen( $s ) ) {
6625  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6626  if ( $markerStart === false ) {
6627  $out .= call_user_func( $callback, substr( $s, $i ) );
6628  break;
6629  } else {
6630  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6631  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6632  if ( $markerEnd === false ) {
6633  $out .= substr( $s, $markerStart );
6634  break;
6635  } else {
6636  $markerEnd += strlen( self::MARKER_SUFFIX );
6637  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6638  $i = $markerEnd;
6639  }
6640  }
6641  }
6642  return $out;
6643  }
6644 
6651  public function killMarkers( $text ) {
6652  return $this->mStripState->killMarkers( $text );
6653  }
6654 
6672  public function serializeHalfParsedText( $text ) {
6673  wfDeprecated( __METHOD__, '1.31' );
6674  $data = [
6675  'text' => $text,
6676  'version' => self::HALF_PARSED_VERSION,
6677  'stripState' => $this->mStripState->getSubState( $text ),
6678  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6679  ];
6680  return $data;
6681  }
6682 
6699  public function unserializeHalfParsedText( $data ) {
6700  wfDeprecated( __METHOD__, '1.31' );
6701  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6702  throw new MWException( __METHOD__ . ': invalid version' );
6703  }
6704 
6705  # First, extract the strip state.
6706  $texts = [ $data['text'] ];
6707  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6708 
6709  # Now renumber links
6710  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6711 
6712  # Should be good to go.
6713  return $texts[0];
6714  }
6715 
6726  public function isValidHalfParsedText( $data ) {
6727  wfDeprecated( __METHOD__, '1.31' );
6728  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6729  }
6730 
6740  public static function parseWidthParam( $value, $parseHeight = true ) {
6741  $parsedWidthParam = [];
6742  if ( $value === '' ) {
6743  return $parsedWidthParam;
6744  }
6745  $m = [];
6746  # (T15500) In both cases (width/height and width only),
6747  # permit trailing "px" for backward compatibility.
6748  if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6749  $width = intval( $m[1] );
6750  $height = intval( $m[2] );
6751  $parsedWidthParam['width'] = $width;
6752  $parsedWidthParam['height'] = $height;
6753  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6754  $width = intval( $value );
6755  $parsedWidthParam['width'] = $width;
6756  }
6757  return $parsedWidthParam;
6758  }
6759 
6769  protected function lock() {
6770  if ( $this->mInParse ) {
6771  throw new MWException( "Parser state cleared while parsing. "
6772  . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6773  }
6774 
6775  // Save the backtrace when locking, so that if some code tries locking again,
6776  // we can print the lock owner's backtrace for easier debugging
6777  $e = new Exception;
6778  $this->mInParse = $e->getTraceAsString();
6779 
6780  $recursiveCheck = new ScopedCallback( function () {
6781  $this->mInParse = false;
6782  } );
6783 
6784  return $recursiveCheck;
6785  }
6786 
6797  public static function stripOuterParagraph( $html ) {
6798  $m = [];
6799  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6800  $html = $m[1];
6801  }
6802 
6803  return $html;
6804  }
6805 
6816  public function getFreshParser() {
6817  if ( $this->mInParse ) {
6818  return $this->factory->create();
6819  } else {
6820  return $this;
6821  }
6822  }
6823 
6830  public function enableOOUI() {
6831  OutputPage::setupOOUI();
6832  $this->mOutput->setEnableOOUI( true );
6833  }
6834 
6839  protected function setOutputFlag( $flag, $reason ) {
6840  $this->mOutput->setFlag( $flag );
6841  $name = $this->mTitle->getPrefixedText();
6842  $this->logger->debug( __METHOD__ . ": set $flag flag on '$name'; $reason" );
6843  }
6844 }
OT_MSG
const OT_MSG
Definition: Defines.php:167
SiteStats\articles
static articles()
Definition: SiteStats.php:103
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
MagicWordArray
Class for handling an array of magic words.
Definition: MagicWordArray.php:32
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
Revision\newKnownCurrent
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: Revision.php:1129
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:316
PPFrame\STRIP_COMMENTS
const STRIP_COMMENTS
Definition: PPFrame.php:31
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:28
RepoGroup\singleton
static singleton()
Definition: RepoGroup.php:60
ParserOutput
Definition: ParserOutput.php:25
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
SiteStats\users
static users()
Definition: SiteStats.php:121
MagicWordFactory
A factory that stores information about MagicWords, and creates them on demand with caching.
Definition: MagicWordFactory.php:34
User\isAnon
isAnon()
Get whether the user is anonymous.
Definition: User.php:3532
SiteStats\activeUsers
static activeUsers()
Definition: SiteStats.php:130
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
Linker\makeSelfLinkObj
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:163
MediaWiki\BadFileLookup
Definition: BadFileLookup.php:12
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:1607
Linker\tocIndent
static tocIndent()
Add another level to the Table of Contents.
Definition: Linker.php:1617
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:41
ParserOptions\getDisableTitleConversion
getDisableTitleConversion()
Whether title conversion should be disabled.
Definition: ParserOptions.php:531
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1869
getUser
getUser()
SiteStats\pages
static pages()
Definition: SiteStats.php:112
$wgNoFollowDomainExceptions
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
Definition: DefaultSettings.php:4305
wfUrlencode
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
Definition: GlobalFunctions.php:309
SiteStats\numberingroup
static numberingroup( $group)
Find the number of users in a given user group.
Definition: SiteStats.php:150
SFH_OBJECT_ARGS
const SFH_OBJECT_ARGS
Definition: Defines.php:178
OT_PREPROCESS
const OT_PREPROCESS
Definition: Defines.php:166
NS_FILE
const NS_FILE
Definition: Defines.php:66
OT_PLAIN
const OT_PLAIN
Definition: Defines.php:168
$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()
Fetch server name for use in error reporting etc.
Definition: GlobalFunctions.php:1326
NS_TEMPLATE
const NS_TEMPLATE
Definition: Defines.php:70
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
MediaWiki\Linker\LinkRendererFactory
Factory to create LinkRender objects.
Definition: LinkRendererFactory.php:32
$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:83
$wgTitle
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition: api.php:58
MWTidy\isEnabled
static isEnabled()
Definition: MWTidy.php:54
StripState
Definition: StripState.php:28
Linker\tocLine
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1643
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:1007
Linker\tocList
static tocList( $toc, Language $lang=null)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition: Linker.php:1679
PPFrame\NO_TEMPLATES
const NO_TEMPLATES
Definition: PPFrame.php:30
Preprocessor
Definition: Preprocessor.php:30
SiteStats\images
static images()
Definition: SiteStats.php:139
StringUtils\replaceMarkup
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <....
Definition: StringUtils.php:297
Revision
Definition: Revision.php:40
Revision\newFromTitle
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: Revision.php:138
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:49
MWException
MediaWiki exception.
Definition: MWException.php:26
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1044
BlockLevelPass\doBlockLevels
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: BlockLevelPass.php:50
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
wfUrlProtocolsWithoutProtRel
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
Definition: GlobalFunctions.php:764
$matches
$matches
Definition: NoLocalSettings.php:24
CoreTagHooks\register
static register( $parser)
Definition: CoreTagHooks.php:33
StringUtils\explode
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
Definition: StringUtils.php:356
PPNode
There are three types of nodes:
Definition: PPNode.php:35
$chars
if(PHP_SAPI !=='cli' &&PHP_SAPI !=='phpdbg') $chars
Definition: make-tables.php:8
LinkHolderArray
Definition: LinkHolderArray.php:29
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:1754
Linker\tocLineEnd
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1667
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:37
$t
$t
Definition: make-normalization-table.php:143
$lines
$lines
Definition: router.php:61
MWTimestamp\getInstance
static getInstance( $ts=false)
Get a timestamp instance in GMT.
Definition: MWTimestamp.php:39
$title
$title
Definition: testCompression.php:34
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:848
OT_WIKI
const OT_WIKI
Definition: Defines.php:165
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
$output
$output
Definition: SyntaxHighlight.php:335
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SectionProfiler
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
Definition: SectionProfiler.php:30
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:74
RequestContext
Group all the pieces relevant to the context of a request into one instance.
Definition: RequestContext.php:33
ParserOptions\getPreSaveTransform
getPreSaveTransform()
Transform wiki markup when saving the page?
Definition: ParserOptions.php:633
$sort
$sort
Definition: profileinfo.php:331
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:1775
MediaWiki\Special\SpecialPageFactory
Factory for handling the special page list and generating SpecialPage objects.
Definition: SpecialPageFactory.php:64
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:719
SpecialVersion\getVersion
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
Definition: SpecialVersion.php:295
$line
$line
Definition: cdb.php:59
ParserFactory
Definition: ParserFactory.php:33
$content
$content
Definition: router.php:78
CoreParserFunctions\register
static register( $parser)
Definition: CoreParserFunctions.php:34
$wgNoFollowNsExceptions
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
Definition: DefaultSettings.php:4290
$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:4284
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:48
User\getOption
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2918
PPFrame
Definition: PPFrame.php:28
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
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1551
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
Linker\makeMediaLinkFile
static makeMediaLinkFile(LinkTarget $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition: Linker.php:781
$context
$context
Definition: load.php:45
SFH_NO_HASH
const SFH_NO_HASH
Definition: Defines.php:177
$args
if( $line===false) $args
Definition: cdb.php:64
OT_HTML
const OT_HTML
Definition: Defines.php:164
Title
Represents a title within MediaWiki.
Definition: Title.php:42
$status
return $status
Definition: SyntaxHighlight.php:347
CoreParserFunctions\cascadingsources
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
Definition: CoreParserFunctions.php:1373
wfMatchesDomainList
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
Definition: GlobalFunctions.php:879
$cache
$cache
Definition: mcc.php:33
MalformedTitleException
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Definition: MalformedTitleException.php:25
Xml\isWellFormedXmlFragment
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:730
ParserOptions\getUser
getUser()
Current user.
Definition: ParserOptions.php:977
Linker\tocUnindent
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1628
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:303
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:74
StringUtils\delimiterReplace
static delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
Definition: StringUtils.php:248
TextContent\normalizeLineEndings
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Definition: TextContent.php:182
Linker\normalizeSubpageLink
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition: Linker.php:1455
MediaWiki\Config\ServiceOptions\get
get( $key)
Definition: ServiceOptions.php:84
ImageGalleryBase\factory
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
Definition: ImageGalleryBase.php:112
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:33
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:68
Title\legalChars
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:695
User\getBoolOption
getBoolOption( $oname)
Get the user's current setting for a given option, as a boolean value.
Definition: User.php:2977
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
RawMessage
Variant of the Message class.
Definition: RawMessage.php:34
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MWTimestamp\getLocalInstance
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Definition: MWTimestamp.php:204
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2232
Linker\makeExternalImage
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition: Linker.php:247
SiteStats\edits
static edits()
Definition: SiteStats.php:94
Language
Internationalisation code.
Definition: Language.php:37
MWHttpRequest\factory
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
Definition: MWHttpRequest.php:189
if
if($IP===false)
Definition: initImageData.php:4
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:62
$mTitle
Title $mTitle
Definition: RevisionSearchResultTrait.php:26
$type
$type
Definition: testCompression.php:48
MWTidy\tidy
static tidy( $text)
Interface with Remex tidy.
Definition: MWTidy.php:42