MediaWiki  master
Parser.php
Go to the documentation of this file.
1 <?php
26 
70 class Parser {
76  const VERSION = '1.6.4';
77 
83 
84  # Flags for Parser::setFunctionHook
85  const SFH_NO_HASH = 1;
86  const SFH_OBJECT_ARGS = 2;
87 
88  # Constants needed for external link processing
89  # Everything except bracket, space, or control characters
90  # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
91  # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
92  # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
93  # uses to replace invalid HTML characters.
94  const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
95  # Simplified expression to match an IPv4 or IPv6 address, or
96  # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
97  const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
98  # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
99  // phpcs:ignore Generic.Files.LineLength
100  const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
101  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
102 
103  # Regular expression for a non-newline space
104  const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
105 
106  # Flags for preprocessToDom
107  const PTD_FOR_INCLUSION = 1;
108 
109  # Allowed values for $this->mOutputType
110  # Parameter to startExternalParse().
111  const OT_HTML = 1; # like parse()
112  const OT_WIKI = 2; # like preSaveTransform()
114  const OT_MSG = 3;
115  const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
116 
134  const MARKER_SUFFIX = "-QINU`\"'\x7f";
135  const MARKER_PREFIX = "\x7f'\"`UNIQ-";
136 
137  # Markers used for wrapping the table of contents
138  const TOC_START = '<mw:toc>';
139  const TOC_END = '</mw:toc>';
140 
142  const MAX_TTS = 900;
143 
144  # Persistent:
145  public $mTagHooks = [];
147  public $mFunctionHooks = [];
148  public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
149  public $mFunctionTagHooks = [];
150  public $mStripList = [];
151  public $mDefaultStripList = [];
152  public $mVarCache = [];
153  public $mImageParams = [];
155  public $mMarkerIndex = 0;
156  public $mFirstCall = true;
157 
158  # Initialised by initialiseVariables()
159 
163  public $mVariables;
164 
168  public $mSubstWords;
169  # Initialised in constructor
171 
172  # Initialized in getPreprocessor()
173 
175 
176  # Cleared with clearState():
177 
180  public $mOutput;
181  public $mAutonumber;
182 
186  public $mStripState;
187 
193 
194  public $mLinkID;
198  public $mExpensiveFunctionCount; # number of expensive parser function calls
200 
204  public $mUser; # User object; only used when doing pre-save transform
205 
206  # Temporary
207  # These are variables reset at least once per parse regardless of $clearState
208 
212  public $mOptions;
213 
217  public $mTitle; # Title context, used for self-link rendering and similar things
218  public $mOutputType; # Output type, one of the OT_xxx constants
219  public $ot; # Shortcut alias, see setOutputType()
220  public $mRevisionObject; # The revision object of the specified revision ID
221  public $mRevisionId; # ID to display in {{REVISIONID}} tags
222  public $mRevisionTimestamp; # The timestamp of the specified revision ID
223  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
224  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
225  public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
226  public $mInputSize = false; # For {{PAGESIZE}} on current page.
227 
232  public $mUniqPrefix = self::MARKER_PREFIX;
233 
240 
248 
253  public $mInParse = false;
254 
256  protected $mProfiler;
257 
261  protected $mLinkRenderer;
262 
266  public function __construct( $conf = [] ) {
267  $this->mConf = $conf;
268  $this->mUrlProtocols = wfUrlProtocols();
269  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
270  self::EXT_LINK_ADDR .
271  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
272  if ( isset( $conf['preprocessorClass'] ) ) {
273  $this->mPreprocessorClass = $conf['preprocessorClass'];
274  } elseif ( defined( 'HPHP_VERSION' ) ) {
275  # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
276  $this->mPreprocessorClass = Preprocessor_Hash::class;
277  } elseif ( extension_loaded( 'domxml' ) ) {
278  # PECL extension that conflicts with the core DOM extension (T15770)
279  wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
280  $this->mPreprocessorClass = Preprocessor_Hash::class;
281  } elseif ( extension_loaded( 'dom' ) ) {
282  $this->mPreprocessorClass = Preprocessor_DOM::class;
283  } else {
284  $this->mPreprocessorClass = Preprocessor_Hash::class;
285  }
286  wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
287  }
288 
292  public function __destruct() {
293  if ( isset( $this->mLinkHolders ) ) {
294  unset( $this->mLinkHolders );
295  }
296  foreach ( $this as $name => $value ) {
297  unset( $this->$name );
298  }
299  }
300 
304  public function __clone() {
305  $this->mInParse = false;
306 
307  // T58226: When you create a reference "to" an object field, that
308  // makes the object field itself be a reference too (until the other
309  // reference goes out of scope). When cloning, any field that's a
310  // reference is copied as a reference in the new object. Both of these
311  // are defined PHP5 behaviors, as inconvenient as it is for us when old
312  // hooks from PHP4 days are passing fields by reference.
313  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
314  // Make a non-reference copy of the field, then rebind the field to
315  // reference the new copy.
316  $tmp = $this->$k;
317  $this->$k =& $tmp;
318  unset( $tmp );
319  }
320 
321  Hooks::run( 'ParserCloned', [ $this ] );
322  }
323 
327  public function firstCallInit() {
328  if ( !$this->mFirstCall ) {
329  return;
330  }
331  $this->mFirstCall = false;
332 
334  CoreTagHooks::register( $this );
335  $this->initialiseVariables();
336 
337  // Avoid PHP 7.1 warning from passing $this by reference
338  $parser = $this;
339  Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
340  }
341 
347  public function clearState() {
348  if ( $this->mFirstCall ) {
349  $this->firstCallInit();
350  }
351  $this->mOutput = new ParserOutput;
352  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
353  $this->mAutonumber = 0;
354  $this->mIncludeCount = [];
355  $this->mLinkHolders = new LinkHolderArray( $this );
356  $this->mLinkID = 0;
357  $this->mRevisionObject = $this->mRevisionTimestamp =
358  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
359  $this->mVarCache = [];
360  $this->mUser = null;
361  $this->mLangLinkLanguages = [];
362  $this->currentRevisionCache = null;
363 
364  $this->mStripState = new StripState( $this );
365 
366  # Clear these on every parse, T6549
367  $this->mTplRedirCache = $this->mTplDomCache = [];
368 
369  $this->mShowToc = true;
370  $this->mForceTocPosition = false;
371  $this->mIncludeSizes = [
372  'post-expand' => 0,
373  'arg' => 0,
374  ];
375  $this->mPPNodeCount = 0;
376  $this->mGeneratedPPNodeCount = 0;
377  $this->mHighestExpansionDepth = 0;
378  $this->mDefaultSort = false;
379  $this->mHeadings = [];
380  $this->mDoubleUnderscores = [];
381  $this->mExpensiveFunctionCount = 0;
382 
383  # Fix cloning
384  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
385  $this->mPreprocessor = null;
386  }
387 
388  $this->mProfiler = new SectionProfiler();
389 
390  // Avoid PHP 7.1 warning from passing $this by reference
391  $parser = $this;
392  Hooks::run( 'ParserClearState', [ &$parser ] );
393  }
394 
407  public function parse(
409  $linestart = true, $clearState = true, $revid = null
410  ) {
411  if ( $clearState ) {
412  // We use U+007F DELETE to construct strip markers, so we have to make
413  // sure that this character does not occur in the input text.
414  $text = strtr( $text, "\x7f", "?" );
415  $magicScopeVariable = $this->lock();
416  }
417  // Strip U+0000 NULL (T159174)
418  $text = str_replace( "\000", '', $text );
419 
420  $this->startParse( $title, $options, self::OT_HTML, $clearState );
421 
422  $this->currentRevisionCache = null;
423  $this->mInputSize = strlen( $text );
424  if ( $this->mOptions->getEnableLimitReport() ) {
425  $this->mOutput->resetParseStartTime();
426  }
427 
428  $oldRevisionId = $this->mRevisionId;
429  $oldRevisionObject = $this->mRevisionObject;
430  $oldRevisionTimestamp = $this->mRevisionTimestamp;
431  $oldRevisionUser = $this->mRevisionUser;
432  $oldRevisionSize = $this->mRevisionSize;
433  if ( $revid !== null ) {
434  $this->mRevisionId = $revid;
435  $this->mRevisionObject = null;
436  $this->mRevisionTimestamp = null;
437  $this->mRevisionUser = null;
438  $this->mRevisionSize = null;
439  }
440 
441  // Avoid PHP 7.1 warning from passing $this by reference
442  $parser = $this;
443  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
444  # No more strip!
445  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
446  $text = $this->internalParse( $text );
447  Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
448 
449  $text = $this->internalParseHalfParsed( $text, true, $linestart );
450 
458  if ( !( $options->getDisableTitleConversion()
459  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
460  || isset( $this->mDoubleUnderscores['notitleconvert'] )
461  || $this->mOutput->getDisplayTitle() !== false )
462  ) {
463  $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
464  if ( $convruletitle ) {
465  $this->mOutput->setTitleText( $convruletitle );
466  } else {
467  $titleText = $this->getConverterLanguage()->convertTitle( $title );
468  $this->mOutput->setTitleText( $titleText );
469  }
470  }
471 
472  # Compute runtime adaptive expiry if set
473  $this->mOutput->finalizeAdaptiveCacheExpiry();
474 
475  # Warn if too many heavyweight parser functions were used
476  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
477  $this->limitationWarn( 'expensive-parserfunction',
478  $this->mExpensiveFunctionCount,
479  $this->mOptions->getExpensiveParserFunctionLimit()
480  );
481  }
482 
483  # Information on limits, for the benefit of users who try to skirt them
484  if ( $this->mOptions->getEnableLimitReport() ) {
485  $text .= $this->makeLimitReport();
486  }
487 
488  # Wrap non-interface parser output in a <div> so it can be targeted
489  # with CSS (T37247)
490  $class = $this->mOptions->getWrapOutputClass();
491  if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
492  $text = Html::rawElement( 'div', [ 'class' => $class ], $text );
493  }
494 
495  $this->mOutput->setText( $text );
496 
497  $this->mRevisionId = $oldRevisionId;
498  $this->mRevisionObject = $oldRevisionObject;
499  $this->mRevisionTimestamp = $oldRevisionTimestamp;
500  $this->mRevisionUser = $oldRevisionUser;
501  $this->mRevisionSize = $oldRevisionSize;
502  $this->mInputSize = false;
503  $this->currentRevisionCache = null;
504 
505  return $this->mOutput;
506  }
507 
514  protected function makeLimitReport() {
515  global $wgShowHostnames;
516 
517  $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
518 
519  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
520  if ( $cpuTime !== null ) {
521  $this->mOutput->setLimitReportData( 'limitreport-cputime',
522  sprintf( "%.3f", $cpuTime )
523  );
524  }
525 
526  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
527  $this->mOutput->setLimitReportData( 'limitreport-walltime',
528  sprintf( "%.3f", $wallTime )
529  );
530 
531  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
532  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
533  );
534  $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
535  [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
536  );
537  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
538  [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
539  );
540  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
541  [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
542  );
543  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
544  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
545  );
546  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
547  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
548  );
549 
550  foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
551  $this->mOutput->setLimitReportData( $key, $value );
552  }
553 
554  Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
555 
556  $limitReport = "NewPP limit report\n";
557  if ( $wgShowHostnames ) {
558  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
559  }
560  $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
561  $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
562  $limitReport .= 'Dynamic content: ' .
563  ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
564  "\n";
565 
566  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
567  if ( Hooks::run( 'ParserLimitReportFormat',
568  [ $key, &$value, &$limitReport, false, false ]
569  ) ) {
570  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
571  $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
572  ->inLanguage( 'en' )->useDatabase( false );
573  if ( !$valueMsg->exists() ) {
574  $valueMsg = new RawMessage( '$1' );
575  }
576  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
577  $valueMsg->params( $value );
578  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
579  }
580  }
581  }
582  // Since we're not really outputting HTML, decode the entities and
583  // then re-encode the things that need hiding inside HTML comments.
584  $limitReport = htmlspecialchars_decode( $limitReport );
585  // Run deprecated hook
586  Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
587 
588  // Sanitize for comment. Note '‐' in the replacement is U+2010,
589  // which looks much like the problematic '-'.
590  $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
591  $text = "\n<!-- \n$limitReport-->\n";
592 
593  // Add on template profiling data in human/machine readable way
594  $dataByFunc = $this->mProfiler->getFunctionStats();
595  uasort( $dataByFunc, function ( $a, $b ) {
596  return $a['real'] < $b['real']; // descending order
597  } );
598  $profileReport = [];
599  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
600  $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
601  $item['%real'], $item['real'], $item['calls'],
602  htmlspecialchars( $item['name'] ) );
603  }
604  $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
605  $text .= implode( "\n", $profileReport ) . "\n-->\n";
606 
607  $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
608 
609  // Add other cache related metadata
610  if ( $wgShowHostnames ) {
611  $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
612  }
613  $this->mOutput->setLimitReportData( 'cachereport-timestamp',
614  $this->mOutput->getCacheTime() );
615  $this->mOutput->setLimitReportData( 'cachereport-ttl',
616  $this->mOutput->getCacheExpiry() );
617  $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
618  $this->mOutput->hasDynamicContent() );
619 
620  if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
621  wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
622  $this->mTitle->getPrefixedDBkey() );
623  }
624  return $text;
625  }
626 
649  public function recursiveTagParse( $text, $frame = false ) {
650  // Avoid PHP 7.1 warning from passing $this by reference
651  $parser = $this;
652  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
653  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
654  $text = $this->internalParse( $text, false, $frame );
655  return $text;
656  }
657 
675  public function recursiveTagParseFully( $text, $frame = false ) {
676  $text = $this->recursiveTagParse( $text, $frame );
677  $text = $this->internalParseHalfParsed( $text, false );
678  return $text;
679  }
680 
692  public function preprocess( $text, Title $title = null,
693  ParserOptions $options, $revid = null, $frame = false
694  ) {
695  $magicScopeVariable = $this->lock();
696  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
697  if ( $revid !== null ) {
698  $this->mRevisionId = $revid;
699  }
700  // Avoid PHP 7.1 warning from passing $this by reference
701  $parser = $this;
702  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
703  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
704  $text = $this->replaceVariables( $text, $frame );
705  $text = $this->mStripState->unstripBoth( $text );
706  return $text;
707  }
708 
718  public function recursivePreprocess( $text, $frame = false ) {
719  $text = $this->replaceVariables( $text, $frame );
720  $text = $this->mStripState->unstripBoth( $text );
721  return $text;
722  }
723 
737  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
738  $msg = new RawMessage( $text );
739  $text = $msg->params( $params )->plain();
740 
741  # Parser (re)initialisation
742  $magicScopeVariable = $this->lock();
743  $this->startParse( $title, $options, self::OT_PLAIN, true );
744 
746  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
747  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
748  $text = $this->mStripState->unstripBoth( $text );
749  return $text;
750  }
751 
758  public function setUser( $user ) {
759  $this->mUser = $user;
760  }
761 
767  public function setTitle( $t ) {
768  if ( !$t ) {
769  $t = Title::newFromText( 'NO TITLE' );
770  }
771 
772  if ( $t->hasFragment() ) {
773  # Strip the fragment to avoid various odd effects
774  $this->mTitle = $t->createFragmentTarget( '' );
775  } else {
776  $this->mTitle = $t;
777  }
778  }
779 
785  public function getTitle() {
786  return $this->mTitle;
787  }
788 
795  public function Title( $x = null ) {
796  return wfSetVar( $this->mTitle, $x );
797  }
798 
804  public function setOutputType( $ot ) {
805  $this->mOutputType = $ot;
806  # Shortcut alias
807  $this->ot = [
808  'html' => $ot == self::OT_HTML,
809  'wiki' => $ot == self::OT_WIKI,
810  'pre' => $ot == self::OT_PREPROCESS,
811  'plain' => $ot == self::OT_PLAIN,
812  ];
813  }
814 
821  public function OutputType( $x = null ) {
822  return wfSetVar( $this->mOutputType, $x );
823  }
824 
830  public function getOutput() {
831  return $this->mOutput;
832  }
833 
839  public function getOptions() {
840  return $this->mOptions;
841  }
842 
849  public function Options( $x = null ) {
850  return wfSetVar( $this->mOptions, $x );
851  }
852 
856  public function nextLinkID() {
857  return $this->mLinkID++;
858  }
859 
863  public function setLinkID( $id ) {
864  $this->mLinkID = $id;
865  }
866 
871  public function getFunctionLang() {
872  return $this->getTargetLanguage();
873  }
874 
884  public function getTargetLanguage() {
885  $target = $this->mOptions->getTargetLanguage();
886 
887  if ( $target !== null ) {
888  return $target;
889  } elseif ( $this->mOptions->getInterfaceMessage() ) {
890  return $this->mOptions->getUserLangObj();
891  } elseif ( is_null( $this->mTitle ) ) {
892  throw new MWException( __METHOD__ . ': $this->mTitle is null' );
893  }
894 
895  return $this->mTitle->getPageLanguage();
896  }
897 
902  public function getConverterLanguage() {
903  return $this->getTargetLanguage();
904  }
905 
912  public function getUser() {
913  if ( !is_null( $this->mUser ) ) {
914  return $this->mUser;
915  }
916  return $this->mOptions->getUser();
917  }
918 
924  public function getPreprocessor() {
925  if ( !isset( $this->mPreprocessor ) ) {
926  $class = $this->mPreprocessorClass;
927  $this->mPreprocessor = new $class( $this );
928  }
929  return $this->mPreprocessor;
930  }
931 
938  public function getLinkRenderer() {
939  if ( !$this->mLinkRenderer ) {
940  $this->mLinkRenderer = MediaWikiServices::getInstance()
941  ->getLinkRendererFactory()->create();
942  $this->mLinkRenderer->setStubThreshold(
943  $this->getOptions()->getStubThreshold()
944  );
945  }
946 
947  return $this->mLinkRenderer;
948  }
949 
969  public static function extractTagsAndParams( $elements, $text, &$matches ) {
970  static $n = 1;
971  $stripped = '';
972  $matches = [];
973 
974  $taglist = implode( '|', $elements );
975  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
976 
977  while ( $text != '' ) {
978  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
979  $stripped .= $p[0];
980  if ( count( $p ) < 5 ) {
981  break;
982  }
983  if ( count( $p ) > 5 ) {
984  # comment
985  $element = $p[4];
986  $attributes = '';
987  $close = '';
988  $inside = $p[5];
989  } else {
990  # tag
991  $element = $p[1];
992  $attributes = $p[2];
993  $close = $p[3];
994  $inside = $p[4];
995  }
996 
997  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
998  $stripped .= $marker;
999 
1000  if ( $close === '/>' ) {
1001  # Empty element tag, <tag />
1002  $content = null;
1003  $text = $inside;
1004  $tail = null;
1005  } else {
1006  if ( $element === '!--' ) {
1007  $end = '/(-->)/';
1008  } else {
1009  $end = "/(<\\/$element\\s*>)/i";
1010  }
1011  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1012  $content = $q[0];
1013  if ( count( $q ) < 3 ) {
1014  # No end tag -- let it run out to the end of the text.
1015  $tail = '';
1016  $text = '';
1017  } else {
1018  $tail = $q[1];
1019  $text = $q[2];
1020  }
1021  }
1022 
1023  $matches[$marker] = [ $element,
1024  $content,
1025  Sanitizer::decodeTagAttributes( $attributes ),
1026  "<$element$attributes$close$content$tail" ];
1027  }
1028  return $stripped;
1029  }
1030 
1036  public function getStripList() {
1037  return $this->mStripList;
1038  }
1039 
1049  public function insertStripItem( $text ) {
1050  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1051  $this->mMarkerIndex++;
1052  $this->mStripState->addGeneral( $marker, $text );
1053  return $marker;
1054  }
1055 
1063  public function doTableStuff( $text ) {
1064  $lines = StringUtils::explode( "\n", $text );
1065  $out = '';
1066  $td_history = []; # Is currently a td tag open?
1067  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1068  $tr_history = []; # Is currently a tr tag open?
1069  $tr_attributes = []; # history of tr attributes
1070  $has_opened_tr = []; # Did this table open a <tr> element?
1071  $indent_level = 0; # indent level of the table
1072 
1073  foreach ( $lines as $outLine ) {
1074  $line = trim( $outLine );
1075 
1076  if ( $line === '' ) { # empty line, go to next line
1077  $out .= $outLine . "\n";
1078  continue;
1079  }
1080 
1081  $first_character = $line[0];
1082  $first_two = substr( $line, 0, 2 );
1083  $matches = [];
1084 
1085  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1086  # First check if we are starting a new table
1087  $indent_level = strlen( $matches[1] );
1088 
1089  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1090  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1091 
1092  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1093  array_push( $td_history, false );
1094  array_push( $last_tag_history, '' );
1095  array_push( $tr_history, false );
1096  array_push( $tr_attributes, '' );
1097  array_push( $has_opened_tr, false );
1098  } elseif ( count( $td_history ) == 0 ) {
1099  # Don't do any of the following
1100  $out .= $outLine . "\n";
1101  continue;
1102  } elseif ( $first_two === '|}' ) {
1103  # We are ending a table
1104  $line = '</table>' . substr( $line, 2 );
1105  $last_tag = array_pop( $last_tag_history );
1106 
1107  if ( !array_pop( $has_opened_tr ) ) {
1108  $line = "<tr><td></td></tr>{$line}";
1109  }
1110 
1111  if ( array_pop( $tr_history ) ) {
1112  $line = "</tr>{$line}";
1113  }
1114 
1115  if ( array_pop( $td_history ) ) {
1116  $line = "</{$last_tag}>{$line}";
1117  }
1118  array_pop( $tr_attributes );
1119  if ( $indent_level > 0 ) {
1120  $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1121  } else {
1122  $outLine = $line;
1123  }
1124  } elseif ( $first_two === '|-' ) {
1125  # Now we have a table row
1126  $line = preg_replace( '#^\|-+#', '', $line );
1127 
1128  # Whats after the tag is now only attributes
1129  $attributes = $this->mStripState->unstripBoth( $line );
1130  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1131  array_pop( $tr_attributes );
1132  array_push( $tr_attributes, $attributes );
1133 
1134  $line = '';
1135  $last_tag = array_pop( $last_tag_history );
1136  array_pop( $has_opened_tr );
1137  array_push( $has_opened_tr, true );
1138 
1139  if ( array_pop( $tr_history ) ) {
1140  $line = '</tr>';
1141  }
1142 
1143  if ( array_pop( $td_history ) ) {
1144  $line = "</{$last_tag}>{$line}";
1145  }
1146 
1147  $outLine = $line;
1148  array_push( $tr_history, false );
1149  array_push( $td_history, false );
1150  array_push( $last_tag_history, '' );
1151  } elseif ( $first_character === '|'
1152  || $first_character === '!'
1153  || $first_two === '|+'
1154  ) {
1155  # This might be cell elements, td, th or captions
1156  if ( $first_two === '|+' ) {
1157  $first_character = '+';
1158  $line = substr( $line, 2 );
1159  } else {
1160  $line = substr( $line, 1 );
1161  }
1162 
1163  // Implies both are valid for table headings.
1164  if ( $first_character === '!' ) {
1165  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1166  }
1167 
1168  # Split up multiple cells on the same line.
1169  # FIXME : This can result in improper nesting of tags processed
1170  # by earlier parser steps.
1171  $cells = explode( '||', $line );
1172 
1173  $outLine = '';
1174 
1175  # Loop through each table cell
1176  foreach ( $cells as $cell ) {
1177  $previous = '';
1178  if ( $first_character !== '+' ) {
1179  $tr_after = array_pop( $tr_attributes );
1180  if ( !array_pop( $tr_history ) ) {
1181  $previous = "<tr{$tr_after}>\n";
1182  }
1183  array_push( $tr_history, true );
1184  array_push( $tr_attributes, '' );
1185  array_pop( $has_opened_tr );
1186  array_push( $has_opened_tr, true );
1187  }
1188 
1189  $last_tag = array_pop( $last_tag_history );
1190 
1191  if ( array_pop( $td_history ) ) {
1192  $previous = "</{$last_tag}>\n{$previous}";
1193  }
1194 
1195  if ( $first_character === '|' ) {
1196  $last_tag = 'td';
1197  } elseif ( $first_character === '!' ) {
1198  $last_tag = 'th';
1199  } elseif ( $first_character === '+' ) {
1200  $last_tag = 'caption';
1201  } else {
1202  $last_tag = '';
1203  }
1204 
1205  array_push( $last_tag_history, $last_tag );
1206 
1207  # A cell could contain both parameters and data
1208  $cell_data = explode( '|', $cell, 2 );
1209 
1210  # T2553: Note that a '|' inside an invalid link should not
1211  # be mistaken as delimiting cell parameters
1212  # Bug T153140: Neither should language converter markup.
1213  if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1214  $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1215  } elseif ( count( $cell_data ) == 1 ) {
1216  // Whitespace in cells is trimmed
1217  $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1218  } else {
1219  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1220  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1221  // Whitespace in cells is trimmed
1222  $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1223  }
1224 
1225  $outLine .= $cell;
1226  array_push( $td_history, true );
1227  }
1228  }
1229  $out .= $outLine . "\n";
1230  }
1231 
1232  # Closing open td, tr && table
1233  while ( count( $td_history ) > 0 ) {
1234  if ( array_pop( $td_history ) ) {
1235  $out .= "</td>\n";
1236  }
1237  if ( array_pop( $tr_history ) ) {
1238  $out .= "</tr>\n";
1239  }
1240  if ( !array_pop( $has_opened_tr ) ) {
1241  $out .= "<tr><td></td></tr>\n";
1242  }
1243 
1244  $out .= "</table>\n";
1245  }
1246 
1247  # Remove trailing line-ending (b/c)
1248  if ( substr( $out, -1 ) === "\n" ) {
1249  $out = substr( $out, 0, -1 );
1250  }
1251 
1252  # special case: don't return empty table
1253  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1254  $out = '';
1255  }
1256 
1257  return $out;
1258  }
1259 
1272  public function internalParse( $text, $isMain = true, $frame = false ) {
1273  $origText = $text;
1274 
1275  // Avoid PHP 7.1 warning from passing $this by reference
1276  $parser = $this;
1277 
1278  # Hook to suspend the parser in this state
1279  if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1280  return $text;
1281  }
1282 
1283  # if $frame is provided, then use $frame for replacing any variables
1284  if ( $frame ) {
1285  # use frame depth to infer how include/noinclude tags should be handled
1286  # depth=0 means this is the top-level document; otherwise it's an included document
1287  if ( !$frame->depth ) {
1288  $flag = 0;
1289  } else {
1290  $flag = self::PTD_FOR_INCLUSION;
1291  }
1292  $dom = $this->preprocessToDom( $text, $flag );
1293  $text = $frame->expand( $dom );
1294  } else {
1295  # if $frame is not provided, then use old-style replaceVariables
1296  $text = $this->replaceVariables( $text );
1297  }
1298 
1299  Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1300  $text = Sanitizer::removeHTMLtags(
1301  $text,
1302  [ $this, 'attributeStripCallback' ],
1303  false,
1304  array_keys( $this->mTransparentTagHooks ),
1305  [],
1306  [ $this, 'addTrackingCategory' ]
1307  );
1308  Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1309 
1310  # Tables need to come after variable replacement for things to work
1311  # properly; putting them before other transformations should keep
1312  # exciting things like link expansions from showing up in surprising
1313  # places.
1314  $text = $this->doTableStuff( $text );
1315 
1316  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1317 
1318  $text = $this->doDoubleUnderscore( $text );
1319 
1320  $text = $this->doHeadings( $text );
1321  $text = $this->replaceInternalLinks( $text );
1322  $text = $this->doAllQuotes( $text );
1323  $text = $this->replaceExternalLinks( $text );
1324 
1325  # replaceInternalLinks may sometimes leave behind
1326  # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1327  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1328 
1329  $text = $this->doMagicLinks( $text );
1330  $text = $this->formatHeadings( $text, $origText, $isMain );
1331 
1332  return $text;
1333  }
1334 
1344  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1345  $text = $this->mStripState->unstripGeneral( $text );
1346 
1347  // Avoid PHP 7.1 warning from passing $this by reference
1348  $parser = $this;
1349 
1350  if ( $isMain ) {
1351  Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1352  }
1353 
1354  # Clean up special characters, only run once, next-to-last before doBlockLevels
1355  $fixtags = [
1356  # French spaces, last one Guillemet-left
1357  # only if there is something before the space
1358  '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1359  # french spaces, Guillemet-right
1360  '/(\\302\\253) /' => '\\1&#160;',
1361  '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, T13874.
1362  ];
1363  $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1364 
1365  $text = $this->doBlockLevels( $text, $linestart );
1366 
1367  $this->replaceLinkHolders( $text );
1368 
1376  if ( !( $this->mOptions->getDisableContentConversion()
1377  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1378  ) {
1379  if ( !$this->mOptions->getInterfaceMessage() ) {
1380  # The position of the convert() call should not be changed. it
1381  # assumes that the links are all replaced and the only thing left
1382  # is the <nowiki> mark.
1383  $text = $this->getConverterLanguage()->convert( $text );
1384  }
1385  }
1386 
1387  $text = $this->mStripState->unstripNoWiki( $text );
1388 
1389  if ( $isMain ) {
1390  Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1391  }
1392 
1393  $text = $this->replaceTransparentTags( $text );
1394  $text = $this->mStripState->unstripGeneral( $text );
1395 
1396  $text = Sanitizer::normalizeCharReferences( $text );
1397 
1398  if ( MWTidy::isEnabled() ) {
1399  if ( $this->mOptions->getTidy() ) {
1400  $text = MWTidy::tidy( $text );
1401  }
1402  } else {
1403  # attempt to sanitize at least some nesting problems
1404  # (T4702 and quite a few others)
1405  $tidyregs = [
1406  # ''Something [http://www.cool.com cool''] -->
1407  # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1408  '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1409  '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1410  # fix up an anchor inside another anchor, only
1411  # at least for a single single nested link (T5695)
1412  '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1413  '\\1\\2</a>\\3</a>\\1\\4</a>',
1414  # fix div inside inline elements- doBlockLevels won't wrap a line which
1415  # contains a div, so fix it up here; replace
1416  # div with escaped text
1417  '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1418  '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1419  # remove empty italic or bold tag pairs, some
1420  # introduced by rules above
1421  '/<([bi])><\/\\1>/' => '',
1422  ];
1423 
1424  $text = preg_replace(
1425  array_keys( $tidyregs ),
1426  array_values( $tidyregs ),
1427  $text );
1428  }
1429 
1430  if ( $isMain ) {
1431  Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1432  }
1433 
1434  return $text;
1435  }
1436 
1448  public function doMagicLinks( $text ) {
1449  $prots = wfUrlProtocolsWithoutProtRel();
1450  $urlChar = self::EXT_LINK_URL_CLASS;
1451  $addr = self::EXT_LINK_ADDR;
1452  $space = self::SPACE_NOT_NL; # non-newline space
1453  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1454  $spaces = "$space++"; # possessive match of 1 or more spaces
1455  $text = preg_replace_callback(
1456  '!(?: # Start cases
1457  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1458  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1459  (\b # m[3]: Free external links
1460  (?i:$prots)
1461  ($addr$urlChar*) # m[4]: Post-protocol path
1462  ) |
1463  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1464  ([0-9]+)\b |
1465  \bISBN $spaces ( # m[6]: ISBN, capture number
1466  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1467  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1468  [0-9Xx] # check digit
1469  )\b
1470  )!xu", [ $this, 'magicLinkCallback' ], $text );
1471  return $text;
1472  }
1473 
1479  public function magicLinkCallback( $m ) {
1480  if ( isset( $m[1] ) && $m[1] !== '' ) {
1481  # Skip anchor
1482  return $m[0];
1483  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1484  # Skip HTML element
1485  return $m[0];
1486  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1487  # Free external link
1488  return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1489  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1490  # RFC or PMID
1491  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1492  if ( !$this->mOptions->getMagicRFCLinks() ) {
1493  return $m[0];
1494  }
1495  $keyword = 'RFC';
1496  $urlmsg = 'rfcurl';
1497  $cssClass = 'mw-magiclink-rfc';
1498  $trackingCat = 'magiclink-tracking-rfc';
1499  $id = $m[5];
1500  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1501  if ( !$this->mOptions->getMagicPMIDLinks() ) {
1502  return $m[0];
1503  }
1504  $keyword = 'PMID';
1505  $urlmsg = 'pubmedurl';
1506  $cssClass = 'mw-magiclink-pmid';
1507  $trackingCat = 'magiclink-tracking-pmid';
1508  $id = $m[5];
1509  } else {
1510  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1511  substr( $m[0], 0, 20 ) . '"' );
1512  }
1513  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1514  $this->addTrackingCategory( $trackingCat );
1515  return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1516  } elseif ( isset( $m[6] ) && $m[6] !== ''
1517  && $this->mOptions->getMagicISBNLinks()
1518  ) {
1519  # ISBN
1520  $isbn = $m[6];
1521  $space = self::SPACE_NOT_NL; # non-newline space
1522  $isbn = preg_replace( "/$space/", ' ', $isbn );
1523  $num = strtr( $isbn, [
1524  '-' => '',
1525  ' ' => '',
1526  'x' => 'X',
1527  ] );
1528  $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1529  return $this->getLinkRenderer()->makeKnownLink(
1530  SpecialPage::getTitleFor( 'Booksources', $num ),
1531  "ISBN $isbn",
1532  [
1533  'class' => 'internal mw-magiclink-isbn',
1534  'title' => false // suppress title attribute
1535  ]
1536  );
1537  } else {
1538  return $m[0];
1539  }
1540  }
1541 
1551  public function makeFreeExternalLink( $url, $numPostProto ) {
1552  $trail = '';
1553 
1554  # The characters '<' and '>' (which were escaped by
1555  # removeHTMLtags()) should not be included in
1556  # URLs, per RFC 2396.
1557  # Make &nbsp; terminate a URL as well (bug T84937)
1558  $m2 = [];
1559  if ( preg_match(
1560  '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1561  $url,
1562  $m2,
1563  PREG_OFFSET_CAPTURE
1564  ) ) {
1565  $trail = substr( $url, $m2[0][1] ) . $trail;
1566  $url = substr( $url, 0, $m2[0][1] );
1567  }
1568 
1569  # Move trailing punctuation to $trail
1570  $sep = ',;\.:!?';
1571  # If there is no left bracket, then consider right brackets fair game too
1572  if ( strpos( $url, '(' ) === false ) {
1573  $sep .= ')';
1574  }
1575 
1576  $urlRev = strrev( $url );
1577  $numSepChars = strspn( $urlRev, $sep );
1578  # Don't break a trailing HTML entity by moving the ; into $trail
1579  # This is in hot code, so use substr_compare to avoid having to
1580  # create a new string object for the comparison
1581  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1582  # more optimization: instead of running preg_match with a $
1583  # anchor, which can be slow, do the match on the reversed
1584  # string starting at the desired offset.
1585  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1586  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1587  $numSepChars--;
1588  }
1589  }
1590  if ( $numSepChars ) {
1591  $trail = substr( $url, -$numSepChars ) . $trail;
1592  $url = substr( $url, 0, -$numSepChars );
1593  }
1594 
1595  # Verify that we still have a real URL after trail removal, and
1596  # not just lone protocol
1597  if ( strlen( $trail ) >= $numPostProto ) {
1598  return $url . $trail;
1599  }
1600 
1601  $url = Sanitizer::cleanUrl( $url );
1602 
1603  # Is this an external image?
1604  $text = $this->maybeMakeExternalImage( $url );
1605  if ( $text === false ) {
1606  # Not an image, make a link
1607  $text = Linker::makeExternalLink( $url,
1608  $this->getConverterLanguage()->markNoConversion( $url, true ),
1609  true, 'free',
1610  $this->getExternalLinkAttribs( $url ), $this->mTitle );
1611  # Register it in the output object...
1612  $this->mOutput->addExternalLink( $url );
1613  }
1614  return $text . $trail;
1615  }
1616 
1626  public function doHeadings( $text ) {
1627  for ( $i = 6; $i >= 1; --$i ) {
1628  $h = str_repeat( '=', $i );
1629  // Trim non-newline whitespace from headings
1630  // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1631  $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1632  }
1633  return $text;
1634  }
1635 
1644  public function doAllQuotes( $text ) {
1645  $outtext = '';
1646  $lines = StringUtils::explode( "\n", $text );
1647  foreach ( $lines as $line ) {
1648  $outtext .= $this->doQuotes( $line ) . "\n";
1649  }
1650  $outtext = substr( $outtext, 0, -1 );
1651  return $outtext;
1652  }
1653 
1661  public function doQuotes( $text ) {
1662  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1663  $countarr = count( $arr );
1664  if ( $countarr == 1 ) {
1665  return $text;
1666  }
1667 
1668  // First, do some preliminary work. This may shift some apostrophes from
1669  // being mark-up to being text. It also counts the number of occurrences
1670  // of bold and italics mark-ups.
1671  $numbold = 0;
1672  $numitalics = 0;
1673  for ( $i = 1; $i < $countarr; $i += 2 ) {
1674  $thislen = strlen( $arr[$i] );
1675  // If there are ever four apostrophes, assume the first is supposed to
1676  // be text, and the remaining three constitute mark-up for bold text.
1677  // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1678  if ( $thislen == 4 ) {
1679  $arr[$i - 1] .= "'";
1680  $arr[$i] = "'''";
1681  $thislen = 3;
1682  } elseif ( $thislen > 5 ) {
1683  // If there are more than 5 apostrophes in a row, assume they're all
1684  // text except for the last 5.
1685  // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1686  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1687  $arr[$i] = "'''''";
1688  $thislen = 5;
1689  }
1690  // Count the number of occurrences of bold and italics mark-ups.
1691  if ( $thislen == 2 ) {
1692  $numitalics++;
1693  } elseif ( $thislen == 3 ) {
1694  $numbold++;
1695  } elseif ( $thislen == 5 ) {
1696  $numitalics++;
1697  $numbold++;
1698  }
1699  }
1700 
1701  // If there is an odd number of both bold and italics, it is likely
1702  // that one of the bold ones was meant to be an apostrophe followed
1703  // by italics. Which one we cannot know for certain, but it is more
1704  // likely to be one that has a single-letter word before it.
1705  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1706  $firstsingleletterword = -1;
1707  $firstmultiletterword = -1;
1708  $firstspace = -1;
1709  for ( $i = 1; $i < $countarr; $i += 2 ) {
1710  if ( strlen( $arr[$i] ) == 3 ) {
1711  $x1 = substr( $arr[$i - 1], -1 );
1712  $x2 = substr( $arr[$i - 1], -2, 1 );
1713  if ( $x1 === ' ' ) {
1714  if ( $firstspace == -1 ) {
1715  $firstspace = $i;
1716  }
1717  } elseif ( $x2 === ' ' ) {
1718  $firstsingleletterword = $i;
1719  // if $firstsingleletterword is set, we don't
1720  // look at the other options, so we can bail early.
1721  break;
1722  } else {
1723  if ( $firstmultiletterword == -1 ) {
1724  $firstmultiletterword = $i;
1725  }
1726  }
1727  }
1728  }
1729 
1730  // If there is a single-letter word, use it!
1731  if ( $firstsingleletterword > -1 ) {
1732  $arr[$firstsingleletterword] = "''";
1733  $arr[$firstsingleletterword - 1] .= "'";
1734  } elseif ( $firstmultiletterword > -1 ) {
1735  // If not, but there's a multi-letter word, use that one.
1736  $arr[$firstmultiletterword] = "''";
1737  $arr[$firstmultiletterword - 1] .= "'";
1738  } elseif ( $firstspace > -1 ) {
1739  // ... otherwise use the first one that has neither.
1740  // (notice that it is possible for all three to be -1 if, for example,
1741  // there is only one pentuple-apostrophe in the line)
1742  $arr[$firstspace] = "''";
1743  $arr[$firstspace - 1] .= "'";
1744  }
1745  }
1746 
1747  // Now let's actually convert our apostrophic mush to HTML!
1748  $output = '';
1749  $buffer = '';
1750  $state = '';
1751  $i = 0;
1752  foreach ( $arr as $r ) {
1753  if ( ( $i % 2 ) == 0 ) {
1754  if ( $state === 'both' ) {
1755  $buffer .= $r;
1756  } else {
1757  $output .= $r;
1758  }
1759  } else {
1760  $thislen = strlen( $r );
1761  if ( $thislen == 2 ) {
1762  if ( $state === 'i' ) {
1763  $output .= '</i>';
1764  $state = '';
1765  } elseif ( $state === 'bi' ) {
1766  $output .= '</i>';
1767  $state = 'b';
1768  } elseif ( $state === 'ib' ) {
1769  $output .= '</b></i><b>';
1770  $state = 'b';
1771  } elseif ( $state === 'both' ) {
1772  $output .= '<b><i>' . $buffer . '</i>';
1773  $state = 'b';
1774  } else { // $state can be 'b' or ''
1775  $output .= '<i>';
1776  $state .= 'i';
1777  }
1778  } elseif ( $thislen == 3 ) {
1779  if ( $state === 'b' ) {
1780  $output .= '</b>';
1781  $state = '';
1782  } elseif ( $state === 'bi' ) {
1783  $output .= '</i></b><i>';
1784  $state = 'i';
1785  } elseif ( $state === 'ib' ) {
1786  $output .= '</b>';
1787  $state = 'i';
1788  } elseif ( $state === 'both' ) {
1789  $output .= '<i><b>' . $buffer . '</b>';
1790  $state = 'i';
1791  } else { // $state can be 'i' or ''
1792  $output .= '<b>';
1793  $state .= 'b';
1794  }
1795  } elseif ( $thislen == 5 ) {
1796  if ( $state === 'b' ) {
1797  $output .= '</b><i>';
1798  $state = 'i';
1799  } elseif ( $state === 'i' ) {
1800  $output .= '</i><b>';
1801  $state = 'b';
1802  } elseif ( $state === 'bi' ) {
1803  $output .= '</i></b>';
1804  $state = '';
1805  } elseif ( $state === 'ib' ) {
1806  $output .= '</b></i>';
1807  $state = '';
1808  } elseif ( $state === 'both' ) {
1809  $output .= '<i><b>' . $buffer . '</b></i>';
1810  $state = '';
1811  } else { // ($state == '')
1812  $buffer = '';
1813  $state = 'both';
1814  }
1815  }
1816  }
1817  $i++;
1818  }
1819  // Now close all remaining tags. Notice that the order is important.
1820  if ( $state === 'b' || $state === 'ib' ) {
1821  $output .= '</b>';
1822  }
1823  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1824  $output .= '</i>';
1825  }
1826  if ( $state === 'bi' ) {
1827  $output .= '</b>';
1828  }
1829  // There might be lonely ''''', so make sure we have a buffer
1830  if ( $state === 'both' && $buffer ) {
1831  $output .= '<b><i>' . $buffer . '</i></b>';
1832  }
1833  return $output;
1834  }
1835 
1849  public function replaceExternalLinks( $text ) {
1850  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1851  if ( $bits === false ) {
1852  throw new MWException( "PCRE needs to be compiled with "
1853  . "--enable-unicode-properties in order for MediaWiki to function" );
1854  }
1855  $s = array_shift( $bits );
1856 
1857  $i = 0;
1858  while ( $i < count( $bits ) ) {
1859  $url = $bits[$i++];
1860  $i++; // protocol
1861  $text = $bits[$i++];
1862  $trail = $bits[$i++];
1863 
1864  # The characters '<' and '>' (which were escaped by
1865  # removeHTMLtags()) should not be included in
1866  # URLs, per RFC 2396.
1867  $m2 = [];
1868  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1869  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1870  $url = substr( $url, 0, $m2[0][1] );
1871  }
1872 
1873  # If the link text is an image URL, replace it with an <img> tag
1874  # This happened by accident in the original parser, but some people used it extensively
1875  $img = $this->maybeMakeExternalImage( $text );
1876  if ( $img !== false ) {
1877  $text = $img;
1878  }
1879 
1880  $dtrail = '';
1881 
1882  # Set linktype for CSS
1883  $linktype = 'text';
1884 
1885  # No link text, e.g. [http://domain.tld/some.link]
1886  if ( $text == '' ) {
1887  # Autonumber
1888  $langObj = $this->getTargetLanguage();
1889  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1890  $linktype = 'autonumber';
1891  } else {
1892  # Have link text, e.g. [http://domain.tld/some.link text]s
1893  # Check for trail
1894  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1895  }
1896 
1897  $text = $this->getConverterLanguage()->markNoConversion( $text );
1898 
1899  $url = Sanitizer::cleanUrl( $url );
1900 
1901  # Use the encoded URL
1902  # This means that users can paste URLs directly into the text
1903  # Funny characters like ö aren't valid in URLs anyway
1904  # This was changed in August 2004
1905  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1906  $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1907 
1908  # Register link in the output object.
1909  $this->mOutput->addExternalLink( $url );
1910  }
1911 
1912  return $s;
1913  }
1914 
1924  public static function getExternalLinkRel( $url = false, $title = null ) {
1925  global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions;
1926  $ns = $title ? $title->getNamespace() : false;
1927  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1928  && !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
1929  ) {
1930  return 'nofollow';
1931  }
1932  return null;
1933  }
1934 
1945  public function getExternalLinkAttribs( $url ) {
1946  $attribs = [];
1947  $rel = self::getExternalLinkRel( $url, $this->mTitle );
1948 
1949  $target = $this->mOptions->getExternalLinkTarget();
1950  if ( $target ) {
1951  $attribs['target'] = $target;
1952  if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
1953  // T133507. New windows can navigate parent cross-origin.
1954  // Including noreferrer due to lacking browser
1955  // support of noopener. Eventually noreferrer should be removed.
1956  if ( $rel !== '' ) {
1957  $rel .= ' ';
1958  }
1959  $rel .= 'noreferrer noopener';
1960  }
1961  }
1962  $attribs['rel'] = $rel;
1963  return $attribs;
1964  }
1965 
1975  public static function normalizeLinkUrl( $url ) {
1976  # First, make sure unsafe characters are encoded
1977  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1978  function ( $m ) {
1979  return rawurlencode( $m[0] );
1980  },
1981  $url
1982  );
1983 
1984  $ret = '';
1985  $end = strlen( $url );
1986 
1987  # Fragment part - 'fragment'
1988  $start = strpos( $url, '#' );
1989  if ( $start !== false && $start < $end ) {
1990  $ret = self::normalizeUrlComponent(
1991  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1992  $end = $start;
1993  }
1994 
1995  # Query part - 'query' minus &=+;
1996  $start = strpos( $url, '?' );
1997  if ( $start !== false && $start < $end ) {
1998  $ret = self::normalizeUrlComponent(
1999  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2000  $end = $start;
2001  }
2002 
2003  # Scheme and path part - 'pchar'
2004  # (we assume no userinfo or encoded colons in the host)
2005  $ret = self::normalizeUrlComponent(
2006  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2007 
2008  return $ret;
2009  }
2010 
2011  private static function normalizeUrlComponent( $component, $unsafe ) {
2012  $callback = function ( $matches ) use ( $unsafe ) {
2013  $char = urldecode( $matches[0] );
2014  $ord = ord( $char );
2015  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2016  # Unescape it
2017  return $char;
2018  } else {
2019  # Leave it escaped, but use uppercase for a-f
2020  return strtoupper( $matches[0] );
2021  }
2022  };
2023  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2024  }
2025 
2034  private function maybeMakeExternalImage( $url ) {
2035  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2036  $imagesexception = !empty( $imagesfrom );
2037  $text = false;
2038  # $imagesfrom could be either a single string or an array of strings, parse out the latter
2039  if ( $imagesexception && is_array( $imagesfrom ) ) {
2040  $imagematch = false;
2041  foreach ( $imagesfrom as $match ) {
2042  if ( strpos( $url, $match ) === 0 ) {
2043  $imagematch = true;
2044  break;
2045  }
2046  }
2047  } elseif ( $imagesexception ) {
2048  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2049  } else {
2050  $imagematch = false;
2051  }
2052 
2053  if ( $this->mOptions->getAllowExternalImages()
2054  || ( $imagesexception && $imagematch )
2055  ) {
2056  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2057  # Image found
2058  $text = Linker::makeExternalImage( $url );
2059  }
2060  }
2061  if ( !$text && $this->mOptions->getEnableImageWhitelist()
2062  && preg_match( self::EXT_IMAGE_REGEX, $url )
2063  ) {
2064  $whitelist = explode(
2065  "\n",
2066  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2067  );
2068 
2069  foreach ( $whitelist as $entry ) {
2070  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2071  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2072  continue;
2073  }
2074  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2075  # Image matches a whitelist entry
2076  $text = Linker::makeExternalImage( $url );
2077  break;
2078  }
2079  }
2080  }
2081  return $text;
2082  }
2083 
2093  public function replaceInternalLinks( $s ) {
2094  $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2095  return $s;
2096  }
2097 
2106  public function replaceInternalLinks2( &$s ) {
2108 
2109  static $tc = false, $e1, $e1_img;
2110  # the % is needed to support urlencoded titles as well
2111  if ( !$tc ) {
2112  $tc = Title::legalChars() . '#%';
2113  # Match a link having the form [[namespace:link|alternate]]trail
2114  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2115  # Match cases where there is no "]]", which might still be images
2116  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2117  }
2118 
2119  $holders = new LinkHolderArray( $this );
2120 
2121  # split the entire text string on occurrences of [[
2122  $a = StringUtils::explode( '[[', ' ' . $s );
2123  # get the first element (all text up to first [[), and remove the space we added
2124  $s = $a->current();
2125  $a->next();
2126  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2127  $s = substr( $s, 1 );
2128 
2129  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2130  $e2 = null;
2131  if ( $useLinkPrefixExtension ) {
2132  # Match the end of a line for a word that's not followed by whitespace,
2133  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2135  $charset = $wgContLang->linkPrefixCharset();
2136  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2137  }
2138 
2139  if ( is_null( $this->mTitle ) ) {
2140  throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2141  }
2142  $nottalk = !$this->mTitle->isTalkPage();
2143 
2144  if ( $useLinkPrefixExtension ) {
2145  $m = [];
2146  if ( preg_match( $e2, $s, $m ) ) {
2147  $first_prefix = $m[2];
2148  } else {
2149  $first_prefix = false;
2150  }
2151  } else {
2152  $prefix = '';
2153  }
2154 
2155  $useSubpages = $this->areSubpagesAllowed();
2156 
2157  # Loop for each link
2158  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2159  # Check for excessive memory usage
2160  if ( $holders->isBig() ) {
2161  # Too big
2162  # Do the existence check, replace the link holders and clear the array
2163  $holders->replace( $s );
2164  $holders->clear();
2165  }
2166 
2167  if ( $useLinkPrefixExtension ) {
2168  if ( preg_match( $e2, $s, $m ) ) {
2169  $prefix = $m[2];
2170  $s = $m[1];
2171  } else {
2172  $prefix = '';
2173  }
2174  # first link
2175  if ( $first_prefix ) {
2176  $prefix = $first_prefix;
2177  $first_prefix = false;
2178  }
2179  }
2180 
2181  $might_be_img = false;
2182 
2183  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2184  $text = $m[2];
2185  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2186  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2187  # the real problem is with the $e1 regex
2188  # See T1500.
2189  # Still some problems for cases where the ] is meant to be outside punctuation,
2190  # and no image is in sight. See T4095.
2191  if ( $text !== ''
2192  && substr( $m[3], 0, 1 ) === ']'
2193  && strpos( $text, '[' ) !== false
2194  ) {
2195  $text .= ']'; # so that replaceExternalLinks($text) works later
2196  $m[3] = substr( $m[3], 1 );
2197  }
2198  # fix up urlencoded title texts
2199  if ( strpos( $m[1], '%' ) !== false ) {
2200  # Should anchors '#' also be rejected?
2201  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2202  }
2203  $trail = $m[3];
2204  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2205  # Invalid, but might be an image with a link in its caption
2206  $might_be_img = true;
2207  $text = $m[2];
2208  if ( strpos( $m[1], '%' ) !== false ) {
2209  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2210  }
2211  $trail = "";
2212  } else { # Invalid form; output directly
2213  $s .= $prefix . '[[' . $line;
2214  continue;
2215  }
2216 
2217  $origLink = ltrim( $m[1], ' ' );
2218 
2219  # Don't allow internal links to pages containing
2220  # PROTO: where PROTO is a valid URL protocol; these
2221  # should be external links.
2222  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2223  $s .= $prefix . '[[' . $line;
2224  continue;
2225  }
2226 
2227  # Make subpage if necessary
2228  if ( $useSubpages ) {
2229  $link = $this->maybeDoSubpageLink( $origLink, $text );
2230  } else {
2231  $link = $origLink;
2232  }
2233 
2234  // \x7f isn't a default legal title char, so most likely strip
2235  // markers will force us into the "invalid form" path above. But,
2236  // just in case, let's assert that xmlish tags aren't valid in
2237  // the title position.
2238  $unstrip = $this->mStripState->killMarkers( $link );
2239  $noMarkers = ( $unstrip === $link );
2240 
2241  $nt = $noMarkers ? Title::newFromText( $link ) : null;
2242  if ( $nt === null ) {
2243  $s .= $prefix . '[[' . $line;
2244  continue;
2245  }
2246 
2247  $ns = $nt->getNamespace();
2248  $iw = $nt->getInterwiki();
2249 
2250  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2251 
2252  if ( $might_be_img ) { # if this is actually an invalid link
2253  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2254  $found = false;
2255  while ( true ) {
2256  # look at the next 'line' to see if we can close it there
2257  $a->next();
2258  $next_line = $a->current();
2259  if ( $next_line === false || $next_line === null ) {
2260  break;
2261  }
2262  $m = explode( ']]', $next_line, 3 );
2263  if ( count( $m ) == 3 ) {
2264  # the first ]] closes the inner link, the second the image
2265  $found = true;
2266  $text .= "[[{$m[0]}]]{$m[1]}";
2267  $trail = $m[2];
2268  break;
2269  } elseif ( count( $m ) == 2 ) {
2270  # if there's exactly one ]] that's fine, we'll keep looking
2271  $text .= "[[{$m[0]}]]{$m[1]}";
2272  } else {
2273  # if $next_line is invalid too, we need look no further
2274  $text .= '[[' . $next_line;
2275  break;
2276  }
2277  }
2278  if ( !$found ) {
2279  # we couldn't find the end of this imageLink, so output it raw
2280  # but don't ignore what might be perfectly normal links in the text we've examined
2281  $holders->merge( $this->replaceInternalLinks2( $text ) );
2282  $s .= "{$prefix}[[$link|$text";
2283  # note: no $trail, because without an end, there *is* no trail
2284  continue;
2285  }
2286  } else { # it's not an image, so output it raw
2287  $s .= "{$prefix}[[$link|$text";
2288  # note: no $trail, because without an end, there *is* no trail
2289  continue;
2290  }
2291  }
2292 
2293  $wasblank = ( $text == '' );
2294  if ( $wasblank ) {
2295  $text = $link;
2296  if ( !$noforce ) {
2297  # Strip off leading ':'
2298  $text = substr( $text, 1 );
2299  }
2300  } else {
2301  # T6598 madness. Handle the quotes only if they come from the alternate part
2302  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2303  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2304  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2305  $text = $this->doQuotes( $text );
2306  }
2307 
2308  # Link not escaped by : , create the various objects
2309  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2310  # Interwikis
2311  if (
2312  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2313  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2314  in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2315  )
2316  ) {
2317  # T26502: filter duplicates
2318  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2319  $this->mLangLinkLanguages[$iw] = true;
2320  $this->mOutput->addLanguageLink( $nt->getFullText() );
2321  }
2322 
2326  $s = rtrim( $s . $prefix ) . $trail; # T175416
2327  continue;
2328  }
2329 
2330  if ( $ns == NS_FILE ) {
2331  if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2332  if ( $wasblank ) {
2333  # if no parameters were passed, $text
2334  # becomes something like "File:Foo.png",
2335  # which we don't want to pass on to the
2336  # image generator
2337  $text = '';
2338  } else {
2339  # recursively parse links inside the image caption
2340  # actually, this will parse them in any other parameters, too,
2341  # but it might be hard to fix that, and it doesn't matter ATM
2342  $text = $this->replaceExternalLinks( $text );
2343  $holders->merge( $this->replaceInternalLinks2( $text ) );
2344  }
2345  # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2346  $s .= $prefix . $this->armorLinks(
2347  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2348  continue;
2349  }
2350  } elseif ( $ns == NS_CATEGORY ) {
2354  $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2355 
2356  if ( $wasblank ) {
2357  $sortkey = $this->getDefaultSort();
2358  } else {
2359  $sortkey = $text;
2360  }
2361  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2362  $sortkey = str_replace( "\n", '', $sortkey );
2363  $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2364  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2365 
2366  continue;
2367  }
2368  }
2369 
2370  # Self-link checking. For some languages, variants of the title are checked in
2371  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2372  # for linking to a different variant.
2373  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2374  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2375  continue;
2376  }
2377 
2378  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2379  # @todo FIXME: Should do batch file existence checks, see comment below
2380  if ( $ns == NS_MEDIA ) {
2381  # Give extensions a chance to select the file revision for us
2382  $options = [];
2383  $descQuery = false;
2384  Hooks::run( 'BeforeParserFetchFileAndTitle',
2385  [ $this, $nt, &$options, &$descQuery ] );
2386  # Fetch and register the file (file title may be different via hooks)
2387  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2388  # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2389  $s .= $prefix . $this->armorLinks(
2390  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2391  continue;
2392  }
2393 
2394  # Some titles, such as valid special pages or files in foreign repos, should
2395  # be shown as bluelinks even though they're not included in the page table
2396  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2397  # batch file existence checks for NS_FILE and NS_MEDIA
2398  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2399  $this->mOutput->addLink( $nt );
2400  $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2401  } else {
2402  # Links will be added to the output link list after checking
2403  $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2404  }
2405  }
2406  return $holders;
2407  }
2408 
2422  protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2423  list( $inside, $trail ) = Linker::splitTrail( $trail );
2424 
2425  if ( $text == '' ) {
2426  $text = htmlspecialchars( $nt->getPrefixedText() );
2427  }
2428 
2429  $link = $this->getLinkRenderer()->makeKnownLink(
2430  $nt, new HtmlArmor( "$prefix$text$inside" )
2431  );
2432 
2433  return $this->armorLinks( $link ) . $trail;
2434  }
2435 
2446  public function armorLinks( $text ) {
2447  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2448  self::MARKER_PREFIX . "NOPARSE$1", $text );
2449  }
2450 
2455  public function areSubpagesAllowed() {
2456  # Some namespaces don't allow subpages
2457  return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2458  }
2459 
2468  public function maybeDoSubpageLink( $target, &$text ) {
2469  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2470  }
2471 
2480  public function doBlockLevels( $text, $linestart ) {
2481  return BlockLevelPass::doBlockLevels( $text, $linestart );
2482  }
2483 
2495  public function getVariableValue( $index, $frame = false ) {
2498 
2499  if ( is_null( $this->mTitle ) ) {
2500  // If no title set, bad things are going to happen
2501  // later. Title should always be set since this
2502  // should only be called in the middle of a parse
2503  // operation (but the unit-tests do funky stuff)
2504  throw new MWException( __METHOD__ . ' Should only be '
2505  . ' called while parsing (no title set)' );
2506  }
2507 
2508  // Avoid PHP 7.1 warning from passing $this by reference
2509  $parser = $this;
2510 
2515  if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) ) {
2516  if ( isset( $this->mVarCache[$index] ) ) {
2517  return $this->mVarCache[$index];
2518  }
2519  }
2520 
2521  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2522  Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2523 
2524  $pageLang = $this->getFunctionLang();
2525 
2526  switch ( $index ) {
2527  case '!':
2528  $value = '|';
2529  break;
2530  case 'currentmonth':
2531  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2532  break;
2533  case 'currentmonth1':
2534  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2535  break;
2536  case 'currentmonthname':
2537  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2538  break;
2539  case 'currentmonthnamegen':
2540  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2541  break;
2542  case 'currentmonthabbrev':
2543  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2544  break;
2545  case 'currentday':
2546  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2547  break;
2548  case 'currentday2':
2549  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2550  break;
2551  case 'localmonth':
2552  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2553  break;
2554  case 'localmonth1':
2555  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2556  break;
2557  case 'localmonthname':
2558  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2559  break;
2560  case 'localmonthnamegen':
2561  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2562  break;
2563  case 'localmonthabbrev':
2564  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2565  break;
2566  case 'localday':
2567  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2568  break;
2569  case 'localday2':
2570  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2571  break;
2572  case 'pagename':
2573  $value = wfEscapeWikiText( $this->mTitle->getText() );
2574  break;
2575  case 'pagenamee':
2576  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2577  break;
2578  case 'fullpagename':
2579  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2580  break;
2581  case 'fullpagenamee':
2582  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2583  break;
2584  case 'subpagename':
2585  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2586  break;
2587  case 'subpagenamee':
2588  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2589  break;
2590  case 'rootpagename':
2591  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2592  break;
2593  case 'rootpagenamee':
2594  $value = wfEscapeWikiText( wfUrlencode( str_replace(
2595  ' ',
2596  '_',
2597  $this->mTitle->getRootText()
2598  ) ) );
2599  break;
2600  case 'basepagename':
2601  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2602  break;
2603  case 'basepagenamee':
2604  $value = wfEscapeWikiText( wfUrlencode( str_replace(
2605  ' ',
2606  '_',
2607  $this->mTitle->getBaseText()
2608  ) ) );
2609  break;
2610  case 'talkpagename':
2611  if ( $this->mTitle->canHaveTalkPage() ) {
2612  $talkPage = $this->mTitle->getTalkPage();
2613  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2614  } else {
2615  $value = '';
2616  }
2617  break;
2618  case 'talkpagenamee':
2619  if ( $this->mTitle->canHaveTalkPage() ) {
2620  $talkPage = $this->mTitle->getTalkPage();
2621  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2622  } else {
2623  $value = '';
2624  }
2625  break;
2626  case 'subjectpagename':
2627  $subjPage = $this->mTitle->getSubjectPage();
2628  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2629  break;
2630  case 'subjectpagenamee':
2631  $subjPage = $this->mTitle->getSubjectPage();
2632  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2633  break;
2634  case 'pageid': // requested in T25427
2635  $pageid = $this->getTitle()->getArticleID();
2636  if ( $pageid == 0 ) {
2637  # 0 means the page doesn't exist in the database,
2638  # which means the user is previewing a new page.
2639  # The vary-revision flag must be set, because the magic word
2640  # will have a different value once the page is saved.
2641  $this->mOutput->setFlag( 'vary-revision' );
2642  wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2643  }
2644  $value = $pageid ? $pageid : null;
2645  break;
2646  case 'revisionid':
2647  # Let the edit saving system know we should parse the page
2648  # *after* a revision ID has been assigned.
2649  $this->mOutput->setFlag( 'vary-revision-id' );
2650  wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2651  $value = $this->mRevisionId;
2652  if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
2653  $value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
2654  $this->mOutput->setSpeculativeRevIdUsed( $value );
2655  }
2656  break;
2657  case 'revisionday':
2658  $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2659  break;
2660  case 'revisionday2':
2661  $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2662  break;
2663  case 'revisionmonth':
2664  $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2665  break;
2666  case 'revisionmonth1':
2667  $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2668  break;
2669  case 'revisionyear':
2670  $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
2671  break;
2672  case 'revisiontimestamp':
2673  # Let the edit saving system know we should parse the page
2674  # *after* a revision ID has been assigned. This is for null edits.
2675  $this->mOutput->setFlag( 'vary-revision' );
2676  wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2677  $value = $this->getRevisionTimestamp();
2678  break;
2679  case 'revisionuser':
2680  # Let the edit saving system know we should parse the page
2681  # *after* a revision ID has been assigned for null edits.
2682  $this->mOutput->setFlag( 'vary-user' );
2683  wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2684  $value = $this->getRevisionUser();
2685  break;
2686  case 'revisionsize':
2687  $value = $this->getRevisionSize();
2688  break;
2689  case 'namespace':
2690  $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2691  break;
2692  case 'namespacee':
2693  $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
2694  break;
2695  case 'namespacenumber':
2696  $value = $this->mTitle->getNamespace();
2697  break;
2698  case 'talkspace':
2699  $value = $this->mTitle->canHaveTalkPage()
2700  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2701  : '';
2702  break;
2703  case 'talkspacee':
2704  $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2705  break;
2706  case 'subjectspace':
2707  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2708  break;
2709  case 'subjectspacee':
2710  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2711  break;
2712  case 'currentdayname':
2713  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2714  break;
2715  case 'currentyear':
2716  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2717  break;
2718  case 'currenttime':
2719  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2720  break;
2721  case 'currenthour':
2722  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2723  break;
2724  case 'currentweek':
2725  # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2726  # int to remove the padding
2727  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2728  break;
2729  case 'currentdow':
2730  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2731  break;
2732  case 'localdayname':
2733  $value = $pageLang->getWeekdayName(
2734  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2735  );
2736  break;
2737  case 'localyear':
2738  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2739  break;
2740  case 'localtime':
2741  $value = $pageLang->time(
2742  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2743  false,
2744  false
2745  );
2746  break;
2747  case 'localhour':
2748  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2749  break;
2750  case 'localweek':
2751  # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2752  # int to remove the padding
2753  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2754  break;
2755  case 'localdow':
2756  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2757  break;
2758  case 'numberofarticles':
2759  $value = $pageLang->formatNum( SiteStats::articles() );
2760  break;
2761  case 'numberoffiles':
2762  $value = $pageLang->formatNum( SiteStats::images() );
2763  break;
2764  case 'numberofusers':
2765  $value = $pageLang->formatNum( SiteStats::users() );
2766  break;
2767  case 'numberofactiveusers':
2768  $value = $pageLang->formatNum( SiteStats::activeUsers() );
2769  break;
2770  case 'numberofpages':
2771  $value = $pageLang->formatNum( SiteStats::pages() );
2772  break;
2773  case 'numberofadmins':
2774  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2775  break;
2776  case 'numberofedits':
2777  $value = $pageLang->formatNum( SiteStats::edits() );
2778  break;
2779  case 'currenttimestamp':
2780  $value = wfTimestamp( TS_MW, $ts );
2781  break;
2782  case 'localtimestamp':
2783  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2784  break;
2785  case 'currentversion':
2787  break;
2788  case 'articlepath':
2789  return $wgArticlePath;
2790  case 'sitename':
2791  return $wgSitename;
2792  case 'server':
2793  return $wgServer;
2794  case 'servername':
2795  return $wgServerName;
2796  case 'scriptpath':
2797  return $wgScriptPath;
2798  case 'stylepath':
2799  return $wgStylePath;
2800  case 'directionmark':
2801  return $pageLang->getDirMark();
2802  case 'contentlanguage':
2804  return $wgLanguageCode;
2805  case 'pagelanguage':
2806  $value = $pageLang->getCode();
2807  break;
2808  case 'cascadingsources':
2810  break;
2811  default:
2812  $ret = null;
2813  Hooks::run(
2814  'ParserGetVariableValueSwitch',
2815  [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2816  );
2817 
2818  return $ret;
2819  }
2820 
2821  if ( $index ) {
2822  $this->mVarCache[$index] = $value;
2823  }
2824 
2825  return $value;
2826  }
2827 
2835  private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
2837 
2838  # Get the timezone-adjusted timestamp to be used for this revision
2839  $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
2840  # Possibly set vary-revision if there is not yet an associated revision
2841  if ( !$this->getRevisionObject() ) {
2842  # Get the timezone-adjusted timestamp $mtts seconds in the future
2843  $resThen = substr(
2844  $wgContLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
2845  $start,
2846  $len
2847  );
2848 
2849  if ( $resNow !== $resThen ) {
2850  # Let the edit saving system know we should parse the page
2851  # *after* a revision ID has been assigned. This is for null edits.
2852  $this->mOutput->setFlag( 'vary-revision' );
2853  wfDebug( __METHOD__ . ": $variable used, setting vary-revision...\n" );
2854  }
2855  }
2856 
2857  return $resNow;
2858  }
2859 
2865  public function initialiseVariables() {
2866  $variableIDs = MagicWord::getVariableIDs();
2867  $substIDs = MagicWord::getSubstIDs();
2868 
2869  $this->mVariables = new MagicWordArray( $variableIDs );
2870  $this->mSubstWords = new MagicWordArray( $substIDs );
2871  }
2872 
2895  public function preprocessToDom( $text, $flags = 0 ) {
2896  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2897  return $dom;
2898  }
2899 
2907  public static function splitWhitespace( $s ) {
2908  $ltrimmed = ltrim( $s );
2909  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
2910  $trimmed = rtrim( $ltrimmed );
2911  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
2912  if ( $diff > 0 ) {
2913  $w2 = substr( $ltrimmed, -$diff );
2914  } else {
2915  $w2 = '';
2916  }
2917  return [ $w1, $trimmed, $w2 ];
2918  }
2919 
2940  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
2941  # Is there any text? Also, Prevent too big inclusions!
2942  $textSize = strlen( $text );
2943  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
2944  return $text;
2945  }
2946 
2947  if ( $frame === false ) {
2948  $frame = $this->getPreprocessor()->newFrame();
2949  } elseif ( !( $frame instanceof PPFrame ) ) {
2950  wfDebug( __METHOD__ . " called using plain parameters instead of "
2951  . "a PPFrame instance. Creating custom frame.\n" );
2952  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
2953  }
2954 
2955  $dom = $this->preprocessToDom( $text );
2956  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
2957  $text = $frame->expand( $dom, $flags );
2958 
2959  return $text;
2960  }
2961 
2969  public static function createAssocArgs( $args ) {
2970  $assocArgs = [];
2971  $index = 1;
2972  foreach ( $args as $arg ) {
2973  $eqpos = strpos( $arg, '=' );
2974  if ( $eqpos === false ) {
2975  $assocArgs[$index++] = $arg;
2976  } else {
2977  $name = trim( substr( $arg, 0, $eqpos ) );
2978  $value = trim( substr( $arg, $eqpos + 1 ) );
2979  if ( $value === false ) {
2980  $value = '';
2981  }
2982  if ( $name !== false ) {
2983  $assocArgs[$name] = $value;
2984  }
2985  }
2986  }
2987 
2988  return $assocArgs;
2989  }
2990 
3017  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3018  # does no harm if $current and $max are present but are unnecessary for the message
3019  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3020  # only during preview, and that would split the parser cache unnecessarily.
3021  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3022  ->text();
3023  $this->mOutput->addWarning( $warning );
3024  $this->addTrackingCategory( "$limitationType-category" );
3025  }
3026 
3039  public function braceSubstitution( $piece, $frame ) {
3040  // Flags
3041 
3042  // $text has been filled
3043  $found = false;
3044  // wiki markup in $text should be escaped
3045  $nowiki = false;
3046  // $text is HTML, armour it against wikitext transformation
3047  $isHTML = false;
3048  // Force interwiki transclusion to be done in raw mode not rendered
3049  $forceRawInterwiki = false;
3050  // $text is a DOM node needing expansion in a child frame
3051  $isChildObj = false;
3052  // $text is a DOM node needing expansion in the current frame
3053  $isLocalObj = false;
3054 
3055  # Title object, where $text came from
3056  $title = false;
3057 
3058  # $part1 is the bit before the first |, and must contain only title characters.
3059  # Various prefixes will be stripped from it later.
3060  $titleWithSpaces = $frame->expand( $piece['title'] );
3061  $part1 = trim( $titleWithSpaces );
3062  $titleText = false;
3063 
3064  # Original title text preserved for various purposes
3065  $originalTitle = $part1;
3066 
3067  # $args is a list of argument nodes, starting from index 0, not including $part1
3068  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3069  # below won't work b/c this $args isn't an object
3070  $args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3071 
3072  $profileSection = null; // profile templates
3073 
3074  # SUBST
3075  if ( !$found ) {
3076  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3077 
3078  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3079  # Decide whether to expand template or keep wikitext as-is.
3080  if ( $this->ot['wiki'] ) {
3081  if ( $substMatch === false ) {
3082  $literal = true; # literal when in PST with no prefix
3083  } else {
3084  $literal = false; # expand when in PST with subst: or safesubst:
3085  }
3086  } else {
3087  if ( $substMatch == 'subst' ) {
3088  $literal = true; # literal when not in PST with plain subst:
3089  } else {
3090  $literal = false; # expand when not in PST with safesubst: or no prefix
3091  }
3092  }
3093  if ( $literal ) {
3094  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3095  $isLocalObj = true;
3096  $found = true;
3097  }
3098  }
3099 
3100  # Variables
3101  if ( !$found && $args->getLength() == 0 ) {
3102  $id = $this->mVariables->matchStartToEnd( $part1 );
3103  if ( $id !== false ) {
3104  $text = $this->getVariableValue( $id, $frame );
3105  if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3106  $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3107  }
3108  $found = true;
3109  }
3110  }
3111 
3112  # MSG, MSGNW and RAW
3113  if ( !$found ) {
3114  # Check for MSGNW:
3115  $mwMsgnw = MagicWord::get( 'msgnw' );
3116  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3117  $nowiki = true;
3118  } else {
3119  # Remove obsolete MSG:
3120  $mwMsg = MagicWord::get( 'msg' );
3121  $mwMsg->matchStartAndRemove( $part1 );
3122  }
3123 
3124  # Check for RAW:
3125  $mwRaw = MagicWord::get( 'raw' );
3126  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3127  $forceRawInterwiki = true;
3128  }
3129  }
3130 
3131  # Parser functions
3132  if ( !$found ) {
3133  $colonPos = strpos( $part1, ':' );
3134  if ( $colonPos !== false ) {
3135  $func = substr( $part1, 0, $colonPos );
3136  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3137  $argsLength = $args->getLength();
3138  for ( $i = 0; $i < $argsLength; $i++ ) {
3139  $funcArgs[] = $args->item( $i );
3140  }
3141  try {
3142  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3143  } catch ( Exception $ex ) {
3144  throw $ex;
3145  }
3146 
3147  // Extract any forwarded flags
3148  if ( isset( $result['title'] ) ) {
3149  $title = $result['title'];
3150  }
3151  if ( isset( $result['found'] ) ) {
3152  $found = $result['found'];
3153  }
3154  if ( array_key_exists( 'text', $result ) ) {
3155  // a string or null
3156  $text = $result['text'];
3157  }
3158  if ( isset( $result['nowiki'] ) ) {
3159  $nowiki = $result['nowiki'];
3160  }
3161  if ( isset( $result['isHTML'] ) ) {
3162  $isHTML = $result['isHTML'];
3163  }
3164  if ( isset( $result['forceRawInterwiki'] ) ) {
3165  $forceRawInterwiki = $result['forceRawInterwiki'];
3166  }
3167  if ( isset( $result['isChildObj'] ) ) {
3168  $isChildObj = $result['isChildObj'];
3169  }
3170  if ( isset( $result['isLocalObj'] ) ) {
3171  $isLocalObj = $result['isLocalObj'];
3172  }
3173  }
3174  }
3175 
3176  # Finish mangling title and then check for loops.
3177  # Set $title to a Title object and $titleText to the PDBK
3178  if ( !$found ) {
3179  $ns = NS_TEMPLATE;
3180  # Split the title into page and subpage
3181  $subpage = '';
3182  $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3183  if ( $part1 !== $relative ) {
3184  $part1 = $relative;
3185  $ns = $this->mTitle->getNamespace();
3186  }
3187  $title = Title::newFromText( $part1, $ns );
3188  if ( $title ) {
3189  $titleText = $title->getPrefixedText();
3190  # Check for language variants if the template is not found
3191  if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3192  $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3193  }
3194  # Do recursion depth check
3195  $limit = $this->mOptions->getMaxTemplateDepth();
3196  if ( $frame->depth >= $limit ) {
3197  $found = true;
3198  $text = '<span class="error">'
3199  . wfMessage( 'parser-template-recursion-depth-warning' )
3200  ->numParams( $limit )->inContentLanguage()->text()
3201  . '</span>';
3202  }
3203  }
3204  }
3205 
3206  # Load from database
3207  if ( !$found && $title ) {
3208  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3209  if ( !$title->isExternal() ) {
3210  if ( $title->isSpecialPage()
3211  && $this->mOptions->getAllowSpecialInclusion()
3212  && $this->ot['html']
3213  ) {
3214  $specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
3215  // Pass the template arguments as URL parameters.
3216  // "uselang" will have no effect since the Language object
3217  // is forced to the one defined in ParserOptions.
3218  $pageArgs = [];
3219  $argsLength = $args->getLength();
3220  for ( $i = 0; $i < $argsLength; $i++ ) {
3221  $bits = $args->item( $i )->splitArg();
3222  if ( strval( $bits['index'] ) === '' ) {
3223  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3224  $value = trim( $frame->expand( $bits['value'] ) );
3225  $pageArgs[$name] = $value;
3226  }
3227  }
3228 
3229  // Create a new context to execute the special page
3230  $context = new RequestContext;
3231  $context->setTitle( $title );
3232  $context->setRequest( new FauxRequest( $pageArgs ) );
3233  if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3234  $context->setUser( $this->getUser() );
3235  } else {
3236  // If this page is cached, then we better not be per user.
3237  $context->setUser( User::newFromName( '127.0.0.1', false ) );
3238  }
3239  $context->setLanguage( $this->mOptions->getUserLangObj() );
3241  $title, $context, $this->getLinkRenderer() );
3242  if ( $ret ) {
3243  $text = $context->getOutput()->getHTML();
3244  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3245  $found = true;
3246  $isHTML = true;
3247  if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3248  $this->mOutput->updateRuntimeAdaptiveExpiry(
3249  $specialPage->maxIncludeCacheTime()
3250  );
3251  }
3252  }
3253  } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3254  $found = false; # access denied
3255  wfDebug( __METHOD__ . ": template inclusion denied for " .
3256  $title->getPrefixedDBkey() . "\n" );
3257  } else {
3258  list( $text, $title ) = $this->getTemplateDom( $title );
3259  if ( $text !== false ) {
3260  $found = true;
3261  $isChildObj = true;
3262  }
3263  }
3264 
3265  # If the title is valid but undisplayable, make a link to it
3266  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3267  $text = "[[:$titleText]]";
3268  $found = true;
3269  }
3270  } elseif ( $title->isTrans() ) {
3271  # Interwiki transclusion
3272  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3273  $text = $this->interwikiTransclude( $title, 'render' );
3274  $isHTML = true;
3275  } else {
3276  $text = $this->interwikiTransclude( $title, 'raw' );
3277  # Preprocess it like a template
3278  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3279  $isChildObj = true;
3280  }
3281  $found = true;
3282  }
3283 
3284  # Do infinite loop check
3285  # This has to be done after redirect resolution to avoid infinite loops via redirects
3286  if ( !$frame->loopCheck( $title ) ) {
3287  $found = true;
3288  $text = '<span class="error">'
3289  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3290  . '</span>';
3291  $this->addTrackingCategory( 'template-loop-category' );
3292  $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3293  wfEscapeWikiText( $titleText ) )->text() );
3294  wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3295  }
3296  }
3297 
3298  # If we haven't found text to substitute by now, we're done
3299  # Recover the source wikitext and return it
3300  if ( !$found ) {
3301  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3302  if ( $profileSection ) {
3303  $this->mProfiler->scopedProfileOut( $profileSection );
3304  }
3305  return [ 'object' => $text ];
3306  }
3307 
3308  # Expand DOM-style return values in a child frame
3309  if ( $isChildObj ) {
3310  # Clean up argument array
3311  $newFrame = $frame->newChild( $args, $title );
3312 
3313  if ( $nowiki ) {
3314  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3315  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3316  # Expansion is eligible for the empty-frame cache
3317  $text = $newFrame->cachedExpand( $titleText, $text );
3318  } else {
3319  # Uncached expansion
3320  $text = $newFrame->expand( $text );
3321  }
3322  }
3323  if ( $isLocalObj && $nowiki ) {
3324  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3325  $isLocalObj = false;
3326  }
3327 
3328  if ( $profileSection ) {
3329  $this->mProfiler->scopedProfileOut( $profileSection );
3330  }
3331 
3332  # Replace raw HTML by a placeholder
3333  if ( $isHTML ) {
3334  $text = $this->insertStripItem( $text );
3335  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3336  # Escape nowiki-style return values
3337  $text = wfEscapeWikiText( $text );
3338  } elseif ( is_string( $text )
3339  && !$piece['lineStart']
3340  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3341  ) {
3342  # T2529: if the template begins with a table or block-level
3343  # element, it should be treated as beginning a new line.
3344  # This behavior is somewhat controversial.
3345  $text = "\n" . $text;
3346  }
3347 
3348  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3349  # Error, oversize inclusion
3350  if ( $titleText !== false ) {
3351  # Make a working, properly escaped link if possible (T25588)
3352  $text = "[[:$titleText]]";
3353  } else {
3354  # This will probably not be a working link, but at least it may
3355  # provide some hint of where the problem is
3356  preg_replace( '/^:/', '', $originalTitle );
3357  $text = "[[:$originalTitle]]";
3358  }
3359  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3360  . 'post-expand include size too large -->' );
3361  $this->limitationWarn( 'post-expand-template-inclusion' );
3362  }
3363 
3364  if ( $isLocalObj ) {
3365  $ret = [ 'object' => $text ];
3366  } else {
3367  $ret = [ 'text' => $text ];
3368  }
3369 
3370  return $ret;
3371  }
3372 
3392  public function callParserFunction( $frame, $function, array $args = [] ) {
3394 
3395  # Case sensitive functions
3396  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3397  $function = $this->mFunctionSynonyms[1][$function];
3398  } else {
3399  # Case insensitive functions
3400  $function = $wgContLang->lc( $function );
3401  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3402  $function = $this->mFunctionSynonyms[0][$function];
3403  } else {
3404  return [ 'found' => false ];
3405  }
3406  }
3407 
3408  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3409 
3410  // Avoid PHP 7.1 warning from passing $this by reference
3411  $parser = $this;
3412 
3413  $allArgs = [ &$parser ];
3414  if ( $flags & self::SFH_OBJECT_ARGS ) {
3415  # Convert arguments to PPNodes and collect for appending to $allArgs
3416  $funcArgs = [];
3417  foreach ( $args as $k => $v ) {
3418  if ( $v instanceof PPNode || $k === 0 ) {
3419  $funcArgs[] = $v;
3420  } else {
3421  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3422  }
3423  }
3424 
3425  # Add a frame parameter, and pass the arguments as an array
3426  $allArgs[] = $frame;
3427  $allArgs[] = $funcArgs;
3428  } else {
3429  # Convert arguments to plain text and append to $allArgs
3430  foreach ( $args as $k => $v ) {
3431  if ( $v instanceof PPNode ) {
3432  $allArgs[] = trim( $frame->expand( $v ) );
3433  } elseif ( is_int( $k ) && $k >= 0 ) {
3434  $allArgs[] = trim( $v );
3435  } else {
3436  $allArgs[] = trim( "$k=$v" );
3437  }
3438  }
3439  }
3440 
3441  $result = call_user_func_array( $callback, $allArgs );
3442 
3443  # The interface for function hooks allows them to return a wikitext
3444  # string or an array containing the string and any flags. This mungs
3445  # things around to match what this method should return.
3446  if ( !is_array( $result ) ) {
3447  $result = [
3448  'found' => true,
3449  'text' => $result,
3450  ];
3451  } else {
3452  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3453  $result['text'] = $result[0];
3454  }
3455  unset( $result[0] );
3456  $result += [
3457  'found' => true,
3458  ];
3459  }
3460 
3461  $noparse = true;
3462  $preprocessFlags = 0;
3463  if ( isset( $result['noparse'] ) ) {
3464  $noparse = $result['noparse'];
3465  }
3466  if ( isset( $result['preprocessFlags'] ) ) {
3467  $preprocessFlags = $result['preprocessFlags'];
3468  }
3469 
3470  if ( !$noparse ) {
3471  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3472  $result['isChildObj'] = true;
3473  }
3474 
3475  return $result;
3476  }
3477 
3486  public function getTemplateDom( $title ) {
3487  $cacheTitle = $title;
3488  $titleText = $title->getPrefixedDBkey();
3489 
3490  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3491  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3492  $title = Title::makeTitle( $ns, $dbk );
3493  $titleText = $title->getPrefixedDBkey();
3494  }
3495  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3496  return [ $this->mTplDomCache[$titleText], $title ];
3497  }
3498 
3499  # Cache miss, go to the database
3500  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3501 
3502  if ( $text === false ) {
3503  $this->mTplDomCache[$titleText] = false;
3504  return [ false, $title ];
3505  }
3506 
3507  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3508  $this->mTplDomCache[$titleText] = $dom;
3509 
3510  if ( !$title->equals( $cacheTitle ) ) {
3511  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3512  [ $title->getNamespace(), $title->getDBkey() ];
3513  }
3514 
3515  return [ $dom, $title ];
3516  }
3517 
3530  $cacheKey = $title->getPrefixedDBkey();
3531  if ( !$this->currentRevisionCache ) {
3532  $this->currentRevisionCache = new MapCacheLRU( 100 );
3533  }
3534  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3535  $this->currentRevisionCache->set( $cacheKey,
3536  // Defaults to Parser::statelessFetchRevision()
3537  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3538  );
3539  }
3540  return $this->currentRevisionCache->get( $cacheKey );
3541  }
3542 
3552  public static function statelessFetchRevision( Title $title, $parser = false ) {
3554 
3555  return $rev;
3556  }
3557 
3563  public function fetchTemplateAndTitle( $title ) {
3564  // Defaults to Parser::statelessFetchTemplate()
3565  $templateCb = $this->mOptions->getTemplateCallback();
3566  $stuff = call_user_func( $templateCb, $title, $this );
3567  // We use U+007F DELETE to distinguish strip markers from regular text.
3568  $text = $stuff['text'];
3569  if ( is_string( $stuff['text'] ) ) {
3570  $text = strtr( $text, "\x7f", "?" );
3571  }
3572  $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3573  if ( isset( $stuff['deps'] ) ) {
3574  foreach ( $stuff['deps'] as $dep ) {
3575  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3576  if ( $dep['title']->equals( $this->getTitle() ) ) {
3577  // If we transclude ourselves, the final result
3578  // will change based on the new version of the page
3579  $this->mOutput->setFlag( 'vary-revision' );
3580  }
3581  }
3582  }
3583  return [ $text, $finalTitle ];
3584  }
3585 
3591  public function fetchTemplate( $title ) {
3592  return $this->fetchTemplateAndTitle( $title )[0];
3593  }
3594 
3604  public static function statelessFetchTemplate( $title, $parser = false ) {
3605  $text = $skip = false;
3606  $finalTitle = $title;
3607  $deps = [];
3608 
3609  # Loop to fetch the article, with up to 1 redirect
3610  // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
3611  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3612  # Give extensions a chance to select the revision instead
3613  $id = false; # Assume current
3614  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3615  [ $parser, $title, &$skip, &$id ] );
3616 
3617  if ( $skip ) {
3618  $text = false;
3619  $deps[] = [
3620  'title' => $title,
3621  'page_id' => $title->getArticleID(),
3622  'rev_id' => null
3623  ];
3624  break;
3625  }
3626  # Get the revision
3627  if ( $id ) {
3628  $rev = Revision::newFromId( $id );
3629  } elseif ( $parser ) {
3630  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3631  } else {
3633  }
3634  $rev_id = $rev ? $rev->getId() : 0;
3635  # If there is no current revision, there is no page
3636  if ( $id === false && !$rev ) {
3637  $linkCache = LinkCache::singleton();
3638  $linkCache->addBadLinkObj( $title );
3639  }
3640 
3641  $deps[] = [
3642  'title' => $title,
3643  'page_id' => $title->getArticleID(),
3644  'rev_id' => $rev_id ];
3645  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3646  # We fetched a rev from a different title; register it too...
3647  $deps[] = [
3648  'title' => $rev->getTitle(),
3649  'page_id' => $rev->getPage(),
3650  'rev_id' => $rev_id ];
3651  }
3652 
3653  if ( $rev ) {
3654  $content = $rev->getContent();
3655  $text = $content ? $content->getWikitextForTransclusion() : null;
3656 
3657  Hooks::run( 'ParserFetchTemplate',
3658  [ $parser, $title, $rev, &$text, &$deps ] );
3659 
3660  if ( $text === false || $text === null ) {
3661  $text = false;
3662  break;
3663  }
3664  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3666  $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3667  if ( !$message->exists() ) {
3668  $text = false;
3669  break;
3670  }
3671  $content = $message->content();
3672  $text = $message->plain();
3673  } else {
3674  break;
3675  }
3676  if ( !$content ) {
3677  break;
3678  }
3679  # Redirect?
3680  $finalTitle = $title;
3681  $title = $content->getRedirectTarget();
3682  }
3683  return [
3684  'text' => $text,
3685  'finalTitle' => $finalTitle,
3686  'deps' => $deps ];
3687  }
3688 
3696  public function fetchFile( $title, $options = [] ) {
3697  return $this->fetchFileAndTitle( $title, $options )[0];
3698  }
3699 
3707  public function fetchFileAndTitle( $title, $options = [] ) {
3708  $file = $this->fetchFileNoRegister( $title, $options );
3709 
3710  $time = $file ? $file->getTimestamp() : false;
3711  $sha1 = $file ? $file->getSha1() : false;
3712  # Register the file as a dependency...
3713  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3714  if ( $file && !$title->equals( $file->getTitle() ) ) {
3715  # Update fetched file title
3716  $title = $file->getTitle();
3717  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3718  }
3719  return [ $file, $title ];
3720  }
3721 
3732  protected function fetchFileNoRegister( $title, $options = [] ) {
3733  if ( isset( $options['broken'] ) ) {
3734  $file = false; // broken thumbnail forced by hook
3735  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3736  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3737  } else { // get by (name,timestamp)
3738  $file = wfFindFile( $title, $options );
3739  }
3740  return $file;
3741  }
3742 
3751  public function interwikiTransclude( $title, $action ) {
3752  global $wgEnableScaryTranscluding;
3753 
3754  if ( !$wgEnableScaryTranscluding ) {
3755  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3756  }
3757 
3758  $url = $title->getFullURL( [ 'action' => $action ] );
3759 
3760  if ( strlen( $url ) > 255 ) {
3761  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3762  }
3763  return $this->fetchScaryTemplateMaybeFromCache( $url );
3764  }
3765 
3770  public function fetchScaryTemplateMaybeFromCache( $url ) {
3771  global $wgTranscludeCacheExpiry;
3772  $dbr = wfGetDB( DB_REPLICA );
3773  $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
3774  $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
3775  [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
3776  if ( $obj ) {
3777  return $obj->tc_contents;
3778  }
3779 
3780  $req = MWHttpRequest::factory( $url, [], __METHOD__ );
3781  $status = $req->execute(); // Status object
3782  if ( $status->isOK() ) {
3783  $text = $req->getContent();
3784  } elseif ( $req->getStatus() != 200 ) {
3785  // Though we failed to fetch the content, this status is useless.
3786  return wfMessage( 'scarytranscludefailed-httpstatus' )
3787  ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
3788  } else {
3789  return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3790  }
3791 
3792  $dbw = wfGetDB( DB_MASTER );
3793  $dbw->replace( 'transcache', [ 'tc_url' ], [
3794  'tc_url' => $url,
3795  'tc_time' => $dbw->timestamp( time() ),
3796  'tc_contents' => $text
3797  ] );
3798  return $text;
3799  }
3800 
3810  public function argSubstitution( $piece, $frame ) {
3811  $error = false;
3812  $parts = $piece['parts'];
3813  $nameWithSpaces = $frame->expand( $piece['title'] );
3814  $argName = trim( $nameWithSpaces );
3815  $object = false;
3816  $text = $frame->getArgument( $argName );
3817  if ( $text === false && $parts->getLength() > 0
3818  && ( $this->ot['html']
3819  || $this->ot['pre']
3820  || ( $this->ot['wiki'] && $frame->isTemplate() )
3821  )
3822  ) {
3823  # No match in frame, use the supplied default
3824  $object = $parts->item( 0 )->getChildren();
3825  }
3826  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3827  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3828  $this->limitationWarn( 'post-expand-template-argument' );
3829  }
3830 
3831  if ( $text === false && $object === false ) {
3832  # No match anywhere
3833  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3834  }
3835  if ( $error !== false ) {
3836  $text .= $error;
3837  }
3838  if ( $object !== false ) {
3839  $ret = [ 'object' => $object ];
3840  } else {
3841  $ret = [ 'text' => $text ];
3842  }
3843 
3844  return $ret;
3845  }
3846 
3862  public function extensionSubstitution( $params, $frame ) {
3863  static $errorStr = '<span class="error">';
3864  static $errorLen = 20;
3865 
3866  $name = $frame->expand( $params['name'] );
3867  if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3868  // Probably expansion depth or node count exceeded. Just punt the
3869  // error up.
3870  return $name;
3871  }
3872 
3873  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3874  if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3875  // See above
3876  return $attrText;
3877  }
3878 
3879  // We can't safely check if the expansion for $content resulted in an
3880  // error, because the content could happen to be the error string
3881  // (T149622).
3882  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3883 
3884  $marker = self::MARKER_PREFIX . "-$name-"
3885  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3886 
3887  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3888  ( $this->ot['html'] || $this->ot['pre'] );
3889  if ( $isFunctionTag ) {
3890  $markerType = 'none';
3891  } else {
3892  $markerType = 'general';
3893  }
3894  if ( $this->ot['html'] || $isFunctionTag ) {
3895  $name = strtolower( $name );
3896  $attributes = Sanitizer::decodeTagAttributes( $attrText );
3897  if ( isset( $params['attributes'] ) ) {
3898  $attributes = $attributes + $params['attributes'];
3899  }
3900 
3901  if ( isset( $this->mTagHooks[$name] ) ) {
3902  $output = call_user_func_array( $this->mTagHooks[$name],
3903  [ $content, $attributes, $this, $frame ] );
3904  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
3905  list( $callback, ) = $this->mFunctionTagHooks[$name];
3906 
3907  // Avoid PHP 7.1 warning from passing $this by reference
3908  $parser = $this;
3909  $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
3910  } else {
3911  $output = '<span class="error">Invalid tag extension name: ' .
3912  htmlspecialchars( $name ) . '</span>';
3913  }
3914 
3915  if ( is_array( $output ) ) {
3916  // Extract flags
3917  $flags = $output;
3918  $output = $flags[0];
3919  if ( isset( $flags['markerType'] ) ) {
3920  $markerType = $flags['markerType'];
3921  }
3922  }
3923  } else {
3924  if ( is_null( $attrText ) ) {
3925  $attrText = '';
3926  }
3927  if ( isset( $params['attributes'] ) ) {
3928  foreach ( $params['attributes'] as $attrName => $attrValue ) {
3929  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
3930  htmlspecialchars( $attrValue ) . '"';
3931  }
3932  }
3933  if ( $content === null ) {
3934  $output = "<$name$attrText/>";
3935  } else {
3936  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
3937  if ( substr( $close, 0, $errorLen ) === $errorStr ) {
3938  // See above
3939  return $close;
3940  }
3941  $output = "<$name$attrText>$content$close";
3942  }
3943  }
3944 
3945  if ( $markerType === 'none' ) {
3946  return $output;
3947  } elseif ( $markerType === 'nowiki' ) {
3948  $this->mStripState->addNoWiki( $marker, $output );
3949  } elseif ( $markerType === 'general' ) {
3950  $this->mStripState->addGeneral( $marker, $output );
3951  } else {
3952  throw new MWException( __METHOD__ . ': invalid marker type' );
3953  }
3954  return $marker;
3955  }
3956 
3964  public function incrementIncludeSize( $type, $size ) {
3965  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
3966  return false;
3967  } else {
3968  $this->mIncludeSizes[$type] += $size;
3969  return true;
3970  }
3971  }
3972 
3979  $this->mExpensiveFunctionCount++;
3980  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
3981  }
3982 
3991  public function doDoubleUnderscore( $text ) {
3992  # The position of __TOC__ needs to be recorded
3993  $mw = MagicWord::get( 'toc' );
3994  if ( $mw->match( $text ) ) {
3995  $this->mShowToc = true;
3996  $this->mForceTocPosition = true;
3997 
3998  # Set a placeholder. At the end we'll fill it in with the TOC.
3999  $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4000 
4001  # Only keep the first one.
4002  $text = $mw->replace( '', $text );
4003  }
4004 
4005  # Now match and remove the rest of them
4007  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4008 
4009  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4010  $this->mOutput->mNoGallery = true;
4011  }
4012  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4013  $this->mShowToc = false;
4014  }
4015  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4016  && $this->mTitle->getNamespace() == NS_CATEGORY
4017  ) {
4018  $this->addTrackingCategory( 'hidden-category-category' );
4019  }
4020  # (T10068) Allow control over whether robots index a page.
4021  # __INDEX__ always overrides __NOINDEX__, see T16899
4022  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4023  $this->mOutput->setIndexPolicy( 'noindex' );
4024  $this->addTrackingCategory( 'noindex-category' );
4025  }
4026  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4027  $this->mOutput->setIndexPolicy( 'index' );
4028  $this->addTrackingCategory( 'index-category' );
4029  }
4030 
4031  # Cache all double underscores in the database
4032  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4033  $this->mOutput->setProperty( $key, '' );
4034  }
4035 
4036  return $text;
4037  }
4038 
4044  public function addTrackingCategory( $msg ) {
4045  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4046  }
4047 
4064  public function formatHeadings( $text, $origText, $isMain = true ) {
4065  global $wgMaxTocLevel;
4066 
4067  # Inhibit editsection links if requested in the page
4068  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4069  $maybeShowEditLink = false;
4070  } else {
4071  $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4072  }
4073 
4074  # Get all headlines for numbering them and adding funky stuff like [edit]
4075  # links - this is for later, but we need the number of headlines right now
4076  # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
4077  # be trimmed here since whitespace in HTML headings is significant.
4078  $matches = [];
4079  $numMatches = preg_match_all(
4080  '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4081  $text,
4082  $matches
4083  );
4084 
4085  # if there are fewer than 4 headlines in the article, do not show TOC
4086  # unless it's been explicitly enabled.
4087  $enoughToc = $this->mShowToc &&
4088  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4089 
4090  # Allow user to stipulate that a page should have a "new section"
4091  # link added via __NEWSECTIONLINK__
4092  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4093  $this->mOutput->setNewSection( true );
4094  }
4095 
4096  # Allow user to remove the "new section"
4097  # link via __NONEWSECTIONLINK__
4098  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4099  $this->mOutput->hideNewSection( true );
4100  }
4101 
4102  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4103  # override above conditions and always show TOC above first header
4104  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4105  $this->mShowToc = true;
4106  $enoughToc = true;
4107  }
4108 
4109  # headline counter
4110  $headlineCount = 0;
4111  $numVisible = 0;
4112 
4113  # Ugh .. the TOC should have neat indentation levels which can be
4114  # passed to the skin functions. These are determined here
4115  $toc = '';
4116  $full = '';
4117  $head = [];
4118  $sublevelCount = [];
4119  $levelCount = [];
4120  $level = 0;
4121  $prevlevel = 0;
4122  $toclevel = 0;
4123  $prevtoclevel = 0;
4124  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4125  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4126  $oldType = $this->mOutputType;
4127  $this->setOutputType( self::OT_WIKI );
4128  $frame = $this->getPreprocessor()->newFrame();
4129  $root = $this->preprocessToDom( $origText );
4130  $node = $root->getFirstChild();
4131  $byteOffset = 0;
4132  $tocraw = [];
4133  $refers = [];
4134 
4135  $headlines = $numMatches !== false ? $matches[3] : [];
4136 
4137  foreach ( $headlines as $headline ) {
4138  $isTemplate = false;
4139  $titleText = false;
4140  $sectionIndex = false;
4141  $numbering = '';
4142  $markerMatches = [];
4143  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4144  $serial = $markerMatches[1];
4145  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4146  $isTemplate = ( $titleText != $baseTitleText );
4147  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4148  }
4149 
4150  if ( $toclevel ) {
4151  $prevlevel = $level;
4152  }
4153  $level = $matches[1][$headlineCount];
4154 
4155  if ( $level > $prevlevel ) {
4156  # Increase TOC level
4157  $toclevel++;
4158  $sublevelCount[$toclevel] = 0;
4159  if ( $toclevel < $wgMaxTocLevel ) {
4160  $prevtoclevel = $toclevel;
4161  $toc .= Linker::tocIndent();
4162  $numVisible++;
4163  }
4164  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4165  # Decrease TOC level, find level to jump to
4166 
4167  for ( $i = $toclevel; $i > 0; $i-- ) {
4168  if ( $levelCount[$i] == $level ) {
4169  # Found last matching level
4170  $toclevel = $i;
4171  break;
4172  } elseif ( $levelCount[$i] < $level ) {
4173  # Found first matching level below current level
4174  $toclevel = $i + 1;
4175  break;
4176  }
4177  }
4178  if ( $i == 0 ) {
4179  $toclevel = 1;
4180  }
4181  if ( $toclevel < $wgMaxTocLevel ) {
4182  if ( $prevtoclevel < $wgMaxTocLevel ) {
4183  # Unindent only if the previous toc level was shown :p
4184  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4185  $prevtoclevel = $toclevel;
4186  } else {
4187  $toc .= Linker::tocLineEnd();
4188  }
4189  }
4190  } else {
4191  # No change in level, end TOC line
4192  if ( $toclevel < $wgMaxTocLevel ) {
4193  $toc .= Linker::tocLineEnd();
4194  }
4195  }
4196 
4197  $levelCount[$toclevel] = $level;
4198 
4199  # count number of headlines for each level
4200  $sublevelCount[$toclevel]++;
4201  $dot = 0;
4202  for ( $i = 1; $i <= $toclevel; $i++ ) {
4203  if ( !empty( $sublevelCount[$i] ) ) {
4204  if ( $dot ) {
4205  $numbering .= '.';
4206  }
4207  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4208  $dot = 1;
4209  }
4210  }
4211 
4212  # The safe header is a version of the header text safe to use for links
4213 
4214  # Remove link placeholders by the link text.
4215  # <!--LINK number-->
4216  # turns into
4217  # link text with suffix
4218  # Do this before unstrip since link text can contain strip markers
4219  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4220 
4221  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4222  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4223 
4224  # Strip out HTML (first regex removes any tag not allowed)
4225  # Allowed tags are:
4226  # * <sup> and <sub> (T10393)
4227  # * <i> (T28375)
4228  # * <b> (r105284)
4229  # * <bdi> (T74884)
4230  # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4231  # * <s> and <strike> (T35715)
4232  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4233  # to allow setting directionality in toc items.
4234  $tocline = preg_replace(
4235  [
4236  '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4237  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4238  ],
4239  [ '', '<$1>' ],
4240  $safeHeadline
4241  );
4242 
4243  # Strip '<span></span>', which is the result from the above if
4244  # <span id="foo"></span> is used to produce an additional anchor
4245  # for a section.
4246  $tocline = str_replace( '<span></span>', '', $tocline );
4247 
4248  $tocline = trim( $tocline );
4249 
4250  # For the anchor, strip out HTML-y stuff period
4251  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4252  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4253 
4254  # Save headline for section edit hint before it's escaped
4255  $headlineHint = $safeHeadline;
4256 
4257  # Decode HTML entities
4258  $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4259 
4260  $safeHeadline = self::normalizeSectionName( $safeHeadline );
4261 
4262  $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4263  $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4264  $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4265  if ( $fallbackHeadline === $safeHeadline ) {
4266  # No reason to have both (in fact, we can't)
4267  $fallbackHeadline = false;
4268  }
4269 
4270  # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4271  # @todo FIXME: We may be changing them depending on the current locale.
4272  $arrayKey = strtolower( $safeHeadline );
4273  if ( $fallbackHeadline === false ) {
4274  $fallbackArrayKey = false;
4275  } else {
4276  $fallbackArrayKey = strtolower( $fallbackHeadline );
4277  }
4278 
4279  # Create the anchor for linking from the TOC to the section
4280  $anchor = $safeHeadline;
4281  $fallbackAnchor = $fallbackHeadline;
4282  if ( isset( $refers[$arrayKey] ) ) {
4283  // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
4284  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4285  $anchor .= "_$i";
4286  $linkAnchor .= "_$i";
4287  $refers["${arrayKey}_$i"] = true;
4288  } else {
4289  $refers[$arrayKey] = true;
4290  }
4291  if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4292  // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
4293  for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4294  $fallbackAnchor .= "_$i";
4295  $refers["${fallbackArrayKey}_$i"] = true;
4296  } else {
4297  $refers[$fallbackArrayKey] = true;
4298  }
4299 
4300  # Don't number the heading if it is the only one (looks silly)
4301  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4302  # the two are different if the line contains a link
4303  $headline = Html::element(
4304  'span',
4305  [ 'class' => 'mw-headline-number' ],
4306  $numbering
4307  ) . ' ' . $headline;
4308  }
4309 
4310  if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4311  $toc .= Linker::tocLine( $linkAnchor, $tocline,
4312  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4313  }
4314 
4315  # Add the section to the section tree
4316  # Find the DOM node for this header
4317  $noOffset = ( $isTemplate || $sectionIndex === false );
4318  while ( $node && !$noOffset ) {
4319  if ( $node->getName() === 'h' ) {
4320  $bits = $node->splitHeading();
4321  if ( $bits['i'] == $sectionIndex ) {
4322  break;
4323  }
4324  }
4325  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4326  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4327  $node = $node->getNextSibling();
4328  }
4329  $tocraw[] = [
4330  'toclevel' => $toclevel,
4331  'level' => $level,
4332  'line' => $tocline,
4333  'number' => $numbering,
4334  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4335  'fromtitle' => $titleText,
4336  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4337  'anchor' => $anchor,
4338  ];
4339 
4340  # give headline the correct <h#> tag
4341  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4342  // Output edit section links as markers with styles that can be customized by skins
4343  if ( $isTemplate ) {
4344  # Put a T flag in the section identifier, to indicate to extractSections()
4345  # that sections inside <includeonly> should be counted.
4346  $editsectionPage = $titleText;
4347  $editsectionSection = "T-$sectionIndex";
4348  $editsectionContent = null;
4349  } else {
4350  $editsectionPage = $this->mTitle->getPrefixedText();
4351  $editsectionSection = $sectionIndex;
4352  $editsectionContent = $headlineHint;
4353  }
4354  // We use a bit of pesudo-xml for editsection markers. The
4355  // language converter is run later on. Using a UNIQ style marker
4356  // leads to the converter screwing up the tokens when it
4357  // converts stuff. And trying to insert strip tags fails too. At
4358  // this point all real inputted tags have already been escaped,
4359  // so we don't have to worry about a user trying to input one of
4360  // these markers directly. We use a page and section attribute
4361  // to stop the language converter from converting these
4362  // important bits of data, but put the headline hint inside a
4363  // content block because the language converter is supposed to
4364  // be able to convert that piece of data.
4365  // Gets replaced with html in ParserOutput::getText
4366  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4367  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4368  if ( $editsectionContent !== null ) {
4369  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4370  } else {
4371  $editlink .= '/>';
4372  }
4373  } else {
4374  $editlink = '';
4375  }
4376  $head[$headlineCount] = Linker::makeHeadline( $level,
4377  $matches['attrib'][$headlineCount], $anchor, $headline,
4378  $editlink, $fallbackAnchor );
4379 
4380  $headlineCount++;
4381  }
4382 
4383  $this->setOutputType( $oldType );
4384 
4385  # Never ever show TOC if no headers
4386  if ( $numVisible < 1 ) {
4387  $enoughToc = false;
4388  }
4389 
4390  if ( $enoughToc ) {
4391  if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4392  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4393  }
4394  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4395  $this->mOutput->setTOCHTML( $toc );
4396  $toc = self::TOC_START . $toc . self::TOC_END;
4397  }
4398 
4399  if ( $isMain ) {
4400  $this->mOutput->setSections( $tocraw );
4401  }
4402 
4403  # split up and insert constructed headlines
4404  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4405  $i = 0;
4406 
4407  // build an array of document sections
4408  $sections = [];
4409  foreach ( $blocks as $block ) {
4410  // $head is zero-based, sections aren't.
4411  if ( empty( $head[$i - 1] ) ) {
4412  $sections[$i] = $block;
4413  } else {
4414  $sections[$i] = $head[$i - 1] . $block;
4415  }
4416 
4427  Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4428 
4429  $i++;
4430  }
4431 
4432  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4433  // append the TOC at the beginning
4434  // Top anchor now in skin
4435  $sections[0] = $sections[0] . $toc . "\n";
4436  }
4437 
4438  $full .= implode( '', $sections );
4439 
4440  if ( $this->mForceTocPosition ) {
4441  return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4442  } else {
4443  return $full;
4444  }
4445  }
4446 
4458  public function preSaveTransform( $text, Title $title, User $user,
4459  ParserOptions $options, $clearState = true
4460  ) {
4461  if ( $clearState ) {
4462  $magicScopeVariable = $this->lock();
4463  }
4464  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4465  $this->setUser( $user );
4466 
4467  // Strip U+0000 NULL (T159174)
4468  $text = str_replace( "\000", '', $text );
4469 
4470  // We still normalize line endings for backwards-compatibility
4471  // with other code that just calls PST, but this should already
4472  // be handled in TextContent subclasses
4473  $text = TextContent::normalizeLineEndings( $text );
4474 
4475  if ( $options->getPreSaveTransform() ) {
4476  $text = $this->pstPass2( $text, $user );
4477  }
4478  $text = $this->mStripState->unstripBoth( $text );
4479 
4480  $this->setUser( null ); # Reset
4481 
4482  return $text;
4483  }
4484 
4493  private function pstPass2( $text, $user ) {
4495 
4496  # Note: This is the timestamp saved as hardcoded wikitext to
4497  # the database, we use $wgContLang here in order to give
4498  # everyone the same signature and use the default one rather
4499  # than the one selected in each user's preferences.
4500  # (see also T14815)
4501  $ts = $this->mOptions->getTimestamp();
4502  $timestamp = MWTimestamp::getLocalInstance( $ts );
4503  $ts = $timestamp->format( 'YmdHis' );
4504  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4505 
4506  $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4507 
4508  # Variable replacement
4509  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4510  $text = $this->replaceVariables( $text );
4511 
4512  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4513  # which may corrupt this parser instance via its wfMessage()->text() call-
4514 
4515  # Signatures
4516  if ( strpos( $text, '~~~' ) !== false ) {
4517  $sigText = $this->getUserSig( $user );
4518  $text = strtr( $text, [
4519  '~~~~~' => $d,
4520  '~~~~' => "$sigText $d",
4521  '~~~' => $sigText
4522  ] );
4523  # The main two signature forms used above are time-sensitive
4524  $this->mOutput->setFlag( 'user-signature' );
4525  }
4526 
4527  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4528  $tc = '[' . Title::legalChars() . ']';
4529  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4530 
4531  // [[ns:page (context)|]]
4532  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4533  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4534  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4535  // [[ns:page (context), context|]] (using either single or double-width comma)
4536  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4537  // [[|page]] (reverse pipe trick: add context from page title)
4538  $p2 = "/\[\[\\|($tc+)]]/";
4539 
4540  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4541  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4542  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4543  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4544 
4545  $t = $this->mTitle->getText();
4546  $m = [];
4547  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4548  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4549  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4550  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4551  } else {
4552  # if there's no context, don't bother duplicating the title
4553  $text = preg_replace( $p2, '[[\\1]]', $text );
4554  }
4555 
4556  return $text;
4557  }
4558 
4573  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4574  global $wgMaxSigChars;
4575 
4576  $username = $user->getName();
4577 
4578  # If not given, retrieve from the user object.
4579  if ( $nickname === false ) {
4580  $nickname = $user->getOption( 'nickname' );
4581  }
4582 
4583  if ( is_null( $fancySig ) ) {
4584  $fancySig = $user->getBoolOption( 'fancysig' );
4585  }
4586 
4587  $nickname = $nickname == null ? $username : $nickname;
4588 
4589  if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4590  $nickname = $username;
4591  wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4592  } elseif ( $fancySig !== false ) {
4593  # Sig. might contain markup; validate this
4594  if ( $this->validateSig( $nickname ) !== false ) {
4595  # Validated; clean up (if needed) and return it
4596  return $this->cleanSig( $nickname, true );
4597  } else {
4598  # Failed to validate; fall back to the default
4599  $nickname = $username;
4600  wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4601  }
4602  }
4603 
4604  # Make sure nickname doesnt get a sig in a sig
4605  $nickname = self::cleanSigInSig( $nickname );
4606 
4607  # If we're still here, make it a link to the user page
4608  $userText = wfEscapeWikiText( $username );
4609  $nickText = wfEscapeWikiText( $nickname );
4610  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4611 
4612  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4613  ->title( $this->getTitle() )->text();
4614  }
4615 
4622  public function validateSig( $text ) {
4623  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4624  }
4625 
4636  public function cleanSig( $text, $parsing = false ) {
4637  if ( !$parsing ) {
4638  global $wgTitle;
4639  $magicScopeVariable = $this->lock();
4640  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4641  }
4642 
4643  # Option to disable this feature
4644  if ( !$this->mOptions->getCleanSignatures() ) {
4645  return $text;
4646  }
4647 
4648  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4649  # => Move this logic to braceSubstitution()
4650  $substWord = MagicWord::get( 'subst' );
4651  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4652  $substText = '{{' . $substWord->getSynonym( 0 );
4653 
4654  $text = preg_replace( $substRegex, $substText, $text );
4655  $text = self::cleanSigInSig( $text );
4656  $dom = $this->preprocessToDom( $text );
4657  $frame = $this->getPreprocessor()->newFrame();
4658  $text = $frame->expand( $dom );
4659 
4660  if ( !$parsing ) {
4661  $text = $this->mStripState->unstripBoth( $text );
4662  }
4663 
4664  return $text;
4665  }
4666 
4673  public static function cleanSigInSig( $text ) {
4674  $text = preg_replace( '/~{3,5}/', '', $text );
4675  return $text;
4676  }
4677 
4688  $outputType, $clearState = true
4689  ) {
4690  $this->startParse( $title, $options, $outputType, $clearState );
4691  }
4692 
4699  private function startParse( Title $title = null, ParserOptions $options,
4700  $outputType, $clearState = true
4701  ) {
4702  $this->setTitle( $title );
4703  $this->mOptions = $options;
4704  $this->setOutputType( $outputType );
4705  if ( $clearState ) {
4706  $this->clearState();
4707  }
4708  }
4709 
4718  public function transformMsg( $text, $options, $title = null ) {
4719  static $executing = false;
4720 
4721  # Guard against infinite recursion
4722  if ( $executing ) {
4723  return $text;
4724  }
4725  $executing = true;
4726 
4727  if ( !$title ) {
4728  global $wgTitle;
4729  $title = $wgTitle;
4730  }
4731 
4732  $text = $this->preprocess( $text, $title, $options );
4733 
4734  $executing = false;
4735  return $text;
4736  }
4737 
4762  public function setHook( $tag, callable $callback ) {
4763  $tag = strtolower( $tag );
4764  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4765  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4766  }
4767  $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
4768  $this->mTagHooks[$tag] = $callback;
4769  if ( !in_array( $tag, $this->mStripList ) ) {
4770  $this->mStripList[] = $tag;
4771  }
4772 
4773  return $oldVal;
4774  }
4775 
4793  public function setTransparentTagHook( $tag, callable $callback ) {
4794  $tag = strtolower( $tag );
4795  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4796  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4797  }
4798  $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
4799  $this->mTransparentTagHooks[$tag] = $callback;
4800 
4801  return $oldVal;
4802  }
4803 
4807  public function clearTagHooks() {
4808  $this->mTagHooks = [];
4809  $this->mFunctionTagHooks = [];
4810  $this->mStripList = $this->mDefaultStripList;
4811  }
4812 
4856  public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4858 
4859  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4860  $this->mFunctionHooks[$id] = [ $callback, $flags ];
4861 
4862  # Add to function cache
4863  $mw = MagicWord::get( $id );
4864  if ( !$mw ) {
4865  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4866  }
4867 
4868  $synonyms = $mw->getSynonyms();
4869  $sensitive = intval( $mw->isCaseSensitive() );
4870 
4871  foreach ( $synonyms as $syn ) {
4872  # Case
4873  if ( !$sensitive ) {
4874  $syn = $wgContLang->lc( $syn );
4875  }
4876  # Add leading hash
4877  if ( !( $flags & self::SFH_NO_HASH ) ) {
4878  $syn = '#' . $syn;
4879  }
4880  # Remove trailing colon
4881  if ( substr( $syn, -1, 1 ) === ':' ) {
4882  $syn = substr( $syn, 0, -1 );
4883  }
4884  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4885  }
4886  return $oldVal;
4887  }
4888 
4894  public function getFunctionHooks() {
4895  return array_keys( $this->mFunctionHooks );
4896  }
4897 
4908  public function setFunctionTagHook( $tag, callable $callback, $flags ) {
4909  $tag = strtolower( $tag );
4910  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4911  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
4912  }
4913  $old = isset( $this->mFunctionTagHooks[$tag] ) ?
4914  $this->mFunctionTagHooks[$tag] : null;
4915  $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
4916 
4917  if ( !in_array( $tag, $this->mStripList ) ) {
4918  $this->mStripList[] = $tag;
4919  }
4920 
4921  return $old;
4922  }
4923 
4931  public function replaceLinkHolders( &$text, $options = 0 ) {
4932  $this->mLinkHolders->replace( $text );
4933  }
4934 
4942  public function replaceLinkHoldersText( $text ) {
4943  return $this->mLinkHolders->replaceText( $text );
4944  }
4945 
4959  public function renderImageGallery( $text, $params ) {
4960  $mode = false;
4961  if ( isset( $params['mode'] ) ) {
4962  $mode = $params['mode'];
4963  }
4964 
4965  try {
4966  $ig = ImageGalleryBase::factory( $mode );
4967  } catch ( Exception $e ) {
4968  // If invalid type set, fallback to default.
4969  $ig = ImageGalleryBase::factory( false );
4970  }
4971 
4972  $ig->setContextTitle( $this->mTitle );
4973  $ig->setShowBytes( false );
4974  $ig->setShowDimensions( false );
4975  $ig->setShowFilename( false );
4976  $ig->setParser( $this );
4977  $ig->setHideBadImages();
4978  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
4979 
4980  if ( isset( $params['showfilename'] ) ) {
4981  $ig->setShowFilename( true );
4982  } else {
4983  $ig->setShowFilename( false );
4984  }
4985  if ( isset( $params['caption'] ) ) {
4986  $caption = $params['caption'];
4987  $caption = htmlspecialchars( $caption );
4988  $caption = $this->replaceInternalLinks( $caption );
4989  $ig->setCaptionHtml( $caption );
4990  }
4991  if ( isset( $params['perrow'] ) ) {
4992  $ig->setPerRow( $params['perrow'] );
4993  }
4994  if ( isset( $params['widths'] ) ) {
4995  $ig->setWidths( $params['widths'] );
4996  }
4997  if ( isset( $params['heights'] ) ) {
4998  $ig->setHeights( $params['heights'] );
4999  }
5000  $ig->setAdditionalOptions( $params );
5001 
5002  // Avoid PHP 7.1 warning from passing $this by reference
5003  $parser = $this;
5004  Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5005 
5006  $lines = StringUtils::explode( "\n", $text );
5007  foreach ( $lines as $line ) {
5008  # match lines like these:
5009  # Image:someimage.jpg|This is some image
5010  $matches = [];
5011  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5012  # Skip empty lines
5013  if ( count( $matches ) == 0 ) {
5014  continue;
5015  }
5016 
5017  if ( strpos( $matches[0], '%' ) !== false ) {
5018  $matches[1] = rawurldecode( $matches[1] );
5019  }
5021  if ( is_null( $title ) ) {
5022  # Bogus title. Ignore these so we don't bomb out later.
5023  continue;
5024  }
5025 
5026  # We need to get what handler the file uses, to figure out parameters.
5027  # Note, a hook can overide the file name, and chose an entirely different
5028  # file (which potentially could be of a different type and have different handler).
5029  $options = [];
5030  $descQuery = false;
5031  Hooks::run( 'BeforeParserFetchFileAndTitle',
5032  [ $this, $title, &$options, &$descQuery ] );
5033  # Don't register it now, as TraditionalImageGallery does that later.
5034  $file = $this->fetchFileNoRegister( $title, $options );
5035  $handler = $file ? $file->getHandler() : false;
5036 
5037  $paramMap = [
5038  'img_alt' => 'gallery-internal-alt',
5039  'img_link' => 'gallery-internal-link',
5040  ];
5041  if ( $handler ) {
5042  $paramMap = $paramMap + $handler->getParamMap();
5043  // We don't want people to specify per-image widths.
5044  // Additionally the width parameter would need special casing anyhow.
5045  unset( $paramMap['img_width'] );
5046  }
5047 
5048  $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5049 
5050  $label = '';
5051  $alt = '';
5052  $link = '';
5053  $handlerOptions = [];
5054  if ( isset( $matches[3] ) ) {
5055  // look for an |alt= definition while trying not to break existing
5056  // captions with multiple pipes (|) in it, until a more sensible grammar
5057  // is defined for images in galleries
5058 
5059  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5060  // splitting on '|' is a bit odd, and different from makeImage.
5061  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5062  // Protect LanguageConverter markup
5063  $parameterMatches = StringUtils::delimiterExplode(
5064  '-{', '}-', '|', $matches[3], true /* nested */
5065  );
5066 
5067  foreach ( $parameterMatches as $parameterMatch ) {
5068  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5069  if ( $magicName ) {
5070  $paramName = $paramMap[$magicName];
5071 
5072  switch ( $paramName ) {
5073  case 'gallery-internal-alt':
5074  $alt = $this->stripAltText( $match, false );
5075  break;
5076  case 'gallery-internal-link':
5077  $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5078  $chars = self::EXT_LINK_URL_CLASS;
5079  $addr = self::EXT_LINK_ADDR;
5080  $prots = $this->mUrlProtocols;
5081  // check to see if link matches an absolute url, if not then it must be a wiki link.
5082  if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5083  // Result of LanguageConverter::markNoConversion
5084  // invoked on an external link.
5085  $linkValue = substr( $linkValue, 4, -2 );
5086  }
5087  if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
5088  $link = $linkValue;
5089  $this->mOutput->addExternalLink( $link );
5090  } else {
5091  $localLinkTitle = Title::newFromText( $linkValue );
5092  if ( $localLinkTitle !== null ) {
5093  $this->mOutput->addLink( $localLinkTitle );
5094  $link = $localLinkTitle->getLinkURL();
5095  }
5096  }
5097  break;
5098  default:
5099  // Must be a handler specific parameter.
5100  if ( $handler->validateParam( $paramName, $match ) ) {
5101  $handlerOptions[$paramName] = $match;
5102  } else {
5103  // Guess not, consider it as caption.
5104  wfDebug( "$parameterMatch failed parameter validation\n" );
5105  $label = $parameterMatch;
5106  }
5107  }
5108 
5109  } else {
5110  // Last pipe wins.
5111  $label = $parameterMatch;
5112  }
5113  }
5114  }
5115 
5116  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5117  }
5118  $html = $ig->toHTML();
5119  Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5120  return $html;
5121  }
5122 
5127  public function getImageParams( $handler ) {
5128  if ( $handler ) {
5129  $handlerClass = get_class( $handler );
5130  } else {
5131  $handlerClass = '';
5132  }
5133  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5134  # Initialise static lists
5135  static $internalParamNames = [
5136  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5137  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5138  'bottom', 'text-bottom' ],
5139  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5140  'upright', 'border', 'link', 'alt', 'class' ],
5141  ];
5142  static $internalParamMap;
5143  if ( !$internalParamMap ) {
5144  $internalParamMap = [];
5145  foreach ( $internalParamNames as $type => $names ) {
5146  foreach ( $names as $name ) {
5147  // For grep: img_left, img_right, img_center, img_none,
5148  // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5149  // img_bottom, img_text_bottom,
5150  // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5151  // img_border, img_link, img_alt, img_class
5152  $magicName = str_replace( '-', '_', "img_$name" );
5153  $internalParamMap[$magicName] = [ $type, $name ];
5154  }
5155  }
5156  }
5157 
5158  # Add handler params
5159  $paramMap = $internalParamMap;
5160  if ( $handler ) {
5161  $handlerParamMap = $handler->getParamMap();
5162  foreach ( $handlerParamMap as $magic => $paramName ) {
5163  $paramMap[$magic] = [ 'handler', $paramName ];
5164  }
5165  }
5166  $this->mImageParams[$handlerClass] = $paramMap;
5167  $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5168  }
5169  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5170  }
5171 
5180  public function makeImage( $title, $options, $holders = false ) {
5181  # Check if the options text is of the form "options|alt text"
5182  # Options are:
5183  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5184  # * left no resizing, just left align. label is used for alt= only
5185  # * right same, but right aligned
5186  # * none same, but not aligned
5187  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5188  # * center center the image
5189  # * frame Keep original image size, no magnify-button.
5190  # * framed Same as "frame"
5191  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5192  # * upright reduce width for upright images, rounded to full __0 px
5193  # * border draw a 1px border around the image
5194  # * alt Text for HTML alt attribute (defaults to empty)
5195  # * class Set a class for img node
5196  # * link Set the target of the image link. Can be external, interwiki, or local
5197  # vertical-align values (no % or length right now):
5198  # * baseline
5199  # * sub
5200  # * super
5201  # * top
5202  # * text-top
5203  # * middle
5204  # * bottom
5205  # * text-bottom
5206 
5207  # Protect LanguageConverter markup when splitting into parts
5209  '-{', '}-', '|', $options, true /* allow nesting */
5210  );
5211 
5212  # Give extensions a chance to select the file revision for us
5213  $options = [];
5214  $descQuery = false;
5215  Hooks::run( 'BeforeParserFetchFileAndTitle',
5216  [ $this, $title, &$options, &$descQuery ] );
5217  # Fetch and register the file (file title may be different via hooks)
5218  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5219 
5220  # Get parameter map
5221  $handler = $file ? $file->getHandler() : false;
5222 
5223  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5224 
5225  if ( !$file ) {
5226  $this->addTrackingCategory( 'broken-file-category' );
5227  }
5228 
5229  # Process the input parameters
5230  $caption = '';
5231  $params = [ 'frame' => [], 'handler' => [],
5232  'horizAlign' => [], 'vertAlign' => [] ];
5233  $seenformat = false;
5234  foreach ( $parts as $part ) {
5235  $part = trim( $part );
5236  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5237  $validated = false;
5238  if ( isset( $paramMap[$magicName] ) ) {
5239  list( $type, $paramName ) = $paramMap[$magicName];
5240 
5241  # Special case; width and height come in one variable together
5242  if ( $type === 'handler' && $paramName === 'width' ) {
5243  $parsedWidthParam = self::parseWidthParam( $value );
5244  if ( isset( $parsedWidthParam['width'] ) ) {
5245  $width = $parsedWidthParam['width'];
5246  if ( $handler->validateParam( 'width', $width ) ) {
5247  $params[$type]['width'] = $width;
5248  $validated = true;
5249  }
5250  }
5251  if ( isset( $parsedWidthParam['height'] ) ) {
5252  $height = $parsedWidthParam['height'];
5253  if ( $handler->validateParam( 'height', $height ) ) {
5254  $params[$type]['height'] = $height;
5255  $validated = true;
5256  }
5257  }
5258  # else no validation -- T15436
5259  } else {
5260  if ( $type === 'handler' ) {
5261  # Validate handler parameter
5262  $validated = $handler->validateParam( $paramName, $value );
5263  } else {
5264  # Validate internal parameters
5265  switch ( $paramName ) {
5266  case 'manualthumb':
5267  case 'alt':
5268  case 'class':
5269  # @todo FIXME: Possibly check validity here for
5270  # manualthumb? downstream behavior seems odd with
5271  # missing manual thumbs.
5272  $validated = true;
5273  $value = $this->stripAltText( $value, $holders );
5274  break;
5275  case 'link':
5276  $chars = self::EXT_LINK_URL_CLASS;
5277  $addr = self::EXT_LINK_ADDR;
5278  $prots = $this->mUrlProtocols;
5279  if ( $value === '' ) {
5280  $paramName = 'no-link';
5281  $value = true;
5282  $validated = true;
5283  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5284  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5285  $paramName = 'link-url';
5286  $this->mOutput->addExternalLink( $value );
5287  if ( $this->mOptions->getExternalLinkTarget() ) {
5288  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5289  }
5290  $validated = true;
5291  }
5292  } else {
5293  $linkTitle = Title::newFromText( $value );
5294  if ( $linkTitle ) {
5295  $paramName = 'link-title';
5296  $value = $linkTitle;
5297  $this->mOutput->addLink( $linkTitle );
5298  $validated = true;
5299  }
5300  }
5301  break;
5302  case 'frameless':
5303  case 'framed':
5304  case 'thumbnail':
5305  // use first appearing option, discard others.
5306  $validated = !$seenformat;
5307  $seenformat = true;
5308  break;
5309  default:
5310  # Most other things appear to be empty or numeric...
5311  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5312  }
5313  }
5314 
5315  if ( $validated ) {
5316  $params[$type][$paramName] = $value;
5317  }
5318  }
5319  }
5320  if ( !$validated ) {
5321  $caption = $part;
5322  }
5323  }
5324 
5325  # Process alignment parameters
5326  if ( $params['horizAlign'] ) {
5327  $params['frame']['align'] = key( $params['horizAlign'] );
5328  }
5329  if ( $params['vertAlign'] ) {
5330  $params['frame']['valign'] = key( $params['vertAlign'] );
5331  }
5332 
5333  $params['frame']['caption'] = $caption;
5334 
5335  # Will the image be presented in a frame, with the caption below?
5336  $imageIsFramed = isset( $params['frame']['frame'] )
5337  || isset( $params['frame']['framed'] )
5338  || isset( $params['frame']['thumbnail'] )
5339  || isset( $params['frame']['manualthumb'] );
5340 
5341  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5342  # came to also set the caption, ordinary text after the image -- which
5343  # makes no sense, because that just repeats the text multiple times in
5344  # screen readers. It *also* came to set the title attribute.
5345  # Now that we have an alt attribute, we should not set the alt text to
5346  # equal the caption: that's worse than useless, it just repeats the
5347  # text. This is the framed/thumbnail case. If there's no caption, we
5348  # use the unnamed parameter for alt text as well, just for the time be-
5349  # ing, if the unnamed param is set and the alt param is not.
5350  # For the future, we need to figure out if we want to tweak this more,
5351  # e.g., introducing a title= parameter for the title; ignoring the un-
5352  # named parameter entirely for images without a caption; adding an ex-
5353  # plicit caption= parameter and preserving the old magic unnamed para-
5354  # meter for BC; ...
5355  if ( $imageIsFramed ) { # Framed image
5356  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5357  # No caption or alt text, add the filename as the alt text so
5358  # that screen readers at least get some description of the image
5359  $params['frame']['alt'] = $title->getText();
5360  }
5361  # Do not set $params['frame']['title'] because tooltips don't make sense
5362  # for framed images
5363  } else { # Inline image
5364  if ( !isset( $params['frame']['alt'] ) ) {
5365  # No alt text, use the "caption" for the alt text
5366  if ( $caption !== '' ) {
5367  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5368  } else {
5369  # No caption, fall back to using the filename for the
5370  # alt text
5371  $params['frame']['alt'] = $title->getText();
5372  }
5373  }
5374  # Use the "caption" for the tooltip text
5375  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5376  }
5377 
5378  Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5379 
5380  # Linker does the rest
5381  $time = isset( $options['time'] ) ? $options['time'] : false;
5382  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5383  $time, $descQuery, $this->mOptions->getThumbSize() );
5384 
5385  # Give the handler a chance to modify the parser object
5386  if ( $handler ) {
5387  $handler->parserTransformHook( $this, $file );
5388  }
5389 
5390  return $ret;
5391  }
5392 
5398  protected function stripAltText( $caption, $holders ) {
5399  # Strip bad stuff out of the title (tooltip). We can't just use
5400  # replaceLinkHoldersText() here, because if this function is called
5401  # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5402  if ( $holders ) {
5403  $tooltip = $holders->replaceText( $caption );
5404  } else {
5405  $tooltip = $this->replaceLinkHoldersText( $caption );
5406  }
5407 
5408  # make sure there are no placeholders in thumbnail attributes
5409  # that are later expanded to html- so expand them now and
5410  # remove the tags
5411  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5412  $tooltip = Sanitizer::stripAllTags( $tooltip );
5413 
5414  return $tooltip;
5415  }
5416 
5422  public function disableCache() {
5423  wfDebug( "Parser output marked as uncacheable.\n" );
5424  if ( !$this->mOutput ) {
5425  throw new MWException( __METHOD__ .
5426  " can only be called when actually parsing something" );
5427  }
5428  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5429  }
5430 
5439  public function attributeStripCallback( &$text, $frame = false ) {
5440  $text = $this->replaceVariables( $text, $frame );
5441  $text = $this->mStripState->unstripBoth( $text );
5442  return $text;
5443  }
5444 
5450  public function getTags() {
5451  return array_merge(
5452  array_keys( $this->mTransparentTagHooks ),
5453  array_keys( $this->mTagHooks ),
5454  array_keys( $this->mFunctionTagHooks )
5455  );
5456  }
5457 
5468  public function replaceTransparentTags( $text ) {
5469  $matches = [];
5470  $elements = array_keys( $this->mTransparentTagHooks );
5471  $text = self::extractTagsAndParams( $elements, $text, $matches );
5472  $replacements = [];
5473 
5474  foreach ( $matches as $marker => $data ) {
5475  list( $element, $content, $params, $tag ) = $data;
5476  $tagName = strtolower( $element );
5477  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5478  $output = call_user_func_array(
5479  $this->mTransparentTagHooks[$tagName],
5480  [ $content, $params, $this ]
5481  );
5482  } else {
5483  $output = $tag;
5484  }
5485  $replacements[$marker] = $output;
5486  }
5487  return strtr( $text, $replacements );
5488  }
5489 
5519  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5520  global $wgTitle; # not generally used but removes an ugly failure mode
5521 
5522  $magicScopeVariable = $this->lock();
5523  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5524  $outText = '';
5525  $frame = $this->getPreprocessor()->newFrame();
5526 
5527  # Process section extraction flags
5528  $flags = 0;
5529  $sectionParts = explode( '-', $sectionId );
5530  $sectionIndex = array_pop( $sectionParts );
5531  foreach ( $sectionParts as $part ) {
5532  if ( $part === 'T' ) {
5533  $flags |= self::PTD_FOR_INCLUSION;
5534  }
5535  }
5536 
5537  # Check for empty input
5538  if ( strval( $text ) === '' ) {
5539  # Only sections 0 and T-0 exist in an empty document
5540  if ( $sectionIndex == 0 ) {
5541  if ( $mode === 'get' ) {
5542  return '';
5543  } else {
5544  return $newText;
5545  }
5546  } else {
5547  if ( $mode === 'get' ) {
5548  return $newText;
5549  } else {
5550  return $text;
5551  }
5552  }
5553  }
5554 
5555  # Preprocess the text
5556  $root = $this->preprocessToDom( $text, $flags );
5557 
5558  # <h> nodes indicate section breaks
5559  # They can only occur at the top level, so we can find them by iterating the root's children
5560  $node = $root->getFirstChild();
5561 
5562  # Find the target section
5563  if ( $sectionIndex == 0 ) {
5564  # Section zero doesn't nest, level=big
5565  $targetLevel = 1000;
5566  } else {
5567  while ( $node ) {
5568  if ( $node->getName() === 'h' ) {
5569  $bits = $node->splitHeading();
5570  if ( $bits['i'] == $sectionIndex ) {
5571  $targetLevel = $bits['level'];
5572  break;
5573  }
5574  }
5575  if ( $mode === 'replace' ) {
5576  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5577  }
5578  $node = $node->getNextSibling();
5579  }
5580  }
5581 
5582  if ( !$node ) {
5583  # Not found
5584  if ( $mode === 'get' ) {
5585  return $newText;
5586  } else {
5587  return $text;
5588  }
5589  }
5590 
5591  # Find the end of the section, including nested sections
5592  do {
5593  if ( $node->getName() === 'h' ) {
5594  $bits = $node->splitHeading();
5595  $curLevel = $bits['level'];
5596  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5597  break;
5598  }
5599  }
5600  if ( $mode === 'get' ) {
5601  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5602  }
5603  $node = $node->getNextSibling();
5604  } while ( $node );
5605 
5606  # Write out the remainder (in replace mode only)
5607  if ( $mode === 'replace' ) {
5608  # Output the replacement text
5609  # Add two newlines on -- trailing whitespace in $newText is conventionally
5610  # stripped by the editor, so we need both newlines to restore the paragraph gap
5611  # Only add trailing whitespace if there is newText
5612  if ( $newText != "" ) {
5613  $outText .= $newText . "\n\n";
5614  }
5615 
5616  while ( $node ) {
5617  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5618  $node = $node->getNextSibling();
5619  }
5620  }
5621 
5622  if ( is_string( $outText ) ) {
5623  # Re-insert stripped tags
5624  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5625  }
5626 
5627  return $outText;
5628  }
5629 
5644  public function getSection( $text, $sectionId, $defaultText = '' ) {
5645  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5646  }
5647 
5660  public function replaceSection( $oldText, $sectionId, $newText ) {
5661  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5662  }
5663 
5669  public function getRevisionId() {
5670  return $this->mRevisionId;
5671  }
5672 
5679  public function getRevisionObject() {
5680  if ( !is_null( $this->mRevisionObject ) ) {
5681  return $this->mRevisionObject;
5682  }
5683  if ( is_null( $this->mRevisionId ) ) {
5684  return null;
5685  }
5686 
5687  $rev = call_user_func(
5688  $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5689  );
5690 
5691  # If the parse is for a new revision, then the callback should have
5692  # already been set to force the object and should match mRevisionId.
5693  # If not, try to fetch by mRevisionId for sanity.
5694  if ( $rev && $rev->getId() != $this->mRevisionId ) {
5695  $rev = Revision::newFromId( $this->mRevisionId );
5696  }
5697 
5698  $this->mRevisionObject = $rev;
5699 
5700  return $this->mRevisionObject;
5701  }
5702 
5708  public function getRevisionTimestamp() {
5709  if ( is_null( $this->mRevisionTimestamp ) ) {
5711 
5712  $revObject = $this->getRevisionObject();
5713  $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5714 
5715  # The cryptic '' timezone parameter tells to use the site-default
5716  # timezone offset instead of the user settings.
5717  # Since this value will be saved into the parser cache, served
5718  # to other users, and potentially even used inside links and such,
5719  # it needs to be consistent for all visitors.
5720  $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
5721 
5722  }
5723  return $this->mRevisionTimestamp;
5724  }
5725 
5731  public function getRevisionUser() {
5732  if ( is_null( $this->mRevisionUser ) ) {
5733  $revObject = $this->getRevisionObject();
5734 
5735  # if this template is subst: the revision id will be blank,
5736  # so just use the current user's name
5737  if ( $revObject ) {
5738  $this->mRevisionUser = $revObject->getUserText();
5739  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5740  $this->mRevisionUser = $this->getUser()->getName();
5741  }
5742  }
5743  return $this->mRevisionUser;
5744  }
5745 
5751  public function getRevisionSize() {
5752  if ( is_null( $this->mRevisionSize ) ) {
5753  $revObject = $this->getRevisionObject();
5754 
5755  # if this variable is subst: the revision id will be blank,
5756  # so just use the parser input size, because the own substituation
5757  # will change the size.
5758  if ( $revObject ) {
5759  $this->mRevisionSize = $revObject->getSize();
5760  } else {
5761  $this->mRevisionSize = $this->mInputSize;
5762  }
5763  }
5764  return $this->mRevisionSize;
5765  }
5766 
5772  public function setDefaultSort( $sort ) {
5773  $this->mDefaultSort = $sort;
5774  $this->mOutput->setProperty( 'defaultsort', $sort );
5775  }
5776 
5787  public function getDefaultSort() {
5788  if ( $this->mDefaultSort !== false ) {
5789  return $this->mDefaultSort;
5790  } else {
5791  return '';
5792  }
5793  }
5794 
5801  public function getCustomDefaultSort() {
5802  return $this->mDefaultSort;
5803  }
5804 
5805  private static function getSectionNameFromStrippedText( $text ) {
5807  $text = Sanitizer::decodeCharReferences( $text );
5808  $text = self::normalizeSectionName( $text );
5809  return $text;
5810  }
5811 
5812  private static function makeAnchor( $sectionName ) {
5813  return '#' . Sanitizer::escapeIdForLink( $sectionName );
5814  }
5815 
5816  private static function makeLegacyAnchor( $sectionName ) {
5818  if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
5819  // ForAttribute() and ForLink() are the same for legacy encoding
5821  } else {
5822  $id = Sanitizer::escapeIdForLink( $sectionName );
5823  }
5824 
5825  return "#$id";
5826  }
5827 
5836  public function guessSectionNameFromWikiText( $text ) {
5837  # Strip out wikitext links(they break the anchor)
5838  $text = $this->stripSectionName( $text );
5839  $sectionName = self::getSectionNameFromStrippedText( $text );
5840  return self::makeAnchor( $sectionName );
5841  }
5842 
5852  public function guessLegacySectionNameFromWikiText( $text ) {
5853  # Strip out wikitext links(they break the anchor)
5854  $text = $this->stripSectionName( $text );
5855  $sectionName = self::getSectionNameFromStrippedText( $text );
5856  return self::makeLegacyAnchor( $sectionName );
5857  }
5858 
5864  public static function guessSectionNameFromStrippedText( $text ) {
5865  $sectionName = self::getSectionNameFromStrippedText( $text );
5866  return self::makeAnchor( $sectionName );
5867  }
5868 
5875  private static function normalizeSectionName( $text ) {
5876  # T90902: ensure the same normalization is applied for IDs as to links
5877  $titleParser = MediaWikiServices::getInstance()->getTitleParser();
5878  try {
5879 
5880  $parts = $titleParser->splitTitleString( "#$text" );
5881  } catch ( MalformedTitleException $ex ) {
5882  return $text;
5883  }
5884  return $parts['fragment'];
5885  }
5886 
5901  public function stripSectionName( $text ) {
5902  # Strip internal link markup
5903  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
5904  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
5905 
5906  # Strip external link markup
5907  # @todo FIXME: Not tolerant to blank link text
5908  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
5909  # on how many empty links there are on the page - need to figure that out.
5910  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
5911 
5912  # Parse wikitext quotes (italics & bold)
5913  $text = $this->doQuotes( $text );
5914 
5915  # Strip HTML tags
5916  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
5917  return $text;
5918  }
5919 
5930  public function testSrvus( $text, Title $title, ParserOptions $options,
5931  $outputType = self::OT_HTML
5932  ) {
5933  $magicScopeVariable = $this->lock();
5934  $this->startParse( $title, $options, $outputType, true );
5935 
5936  $text = $this->replaceVariables( $text );
5937  $text = $this->mStripState->unstripBoth( $text );
5938  $text = Sanitizer::removeHTMLtags( $text );
5939  return $text;
5940  }
5941 
5948  public function testPst( $text, Title $title, ParserOptions $options ) {
5949  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
5950  }
5951 
5958  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
5959  return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
5960  }
5961 
5978  public function markerSkipCallback( $s, $callback ) {
5979  $i = 0;
5980  $out = '';
5981  while ( $i < strlen( $s ) ) {
5982  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
5983  if ( $markerStart === false ) {
5984  $out .= call_user_func( $callback, substr( $s, $i ) );
5985  break;
5986  } else {
5987  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
5988  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
5989  if ( $markerEnd === false ) {
5990  $out .= substr( $s, $markerStart );
5991  break;
5992  } else {
5993  $markerEnd += strlen( self::MARKER_SUFFIX );
5994  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
5995  $i = $markerEnd;
5996  }
5997  }
5998  }
5999  return $out;
6000  }
6001 
6008  public function killMarkers( $text ) {
6009  return $this->mStripState->killMarkers( $text );
6010  }
6011 
6029  public function serializeHalfParsedText( $text ) {
6030  wfDeprecated( __METHOD__, '1.31' );
6031  $data = [
6032  'text' => $text,
6033  'version' => self::HALF_PARSED_VERSION,
6034  'stripState' => $this->mStripState->getSubState( $text ),
6035  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6036  ];
6037  return $data;
6038  }
6039 
6056  public function unserializeHalfParsedText( $data ) {
6057  wfDeprecated( __METHOD__, '1.31' );
6058  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6059  throw new MWException( __METHOD__ . ': invalid version' );
6060  }
6061 
6062  # First, extract the strip state.
6063  $texts = [ $data['text'] ];
6064  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6065 
6066  # Now renumber links
6067  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6068 
6069  # Should be good to go.
6070  return $texts[0];
6071  }
6072 
6083  public function isValidHalfParsedText( $data ) {
6084  wfDeprecated( __METHOD__, '1.31' );
6085  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6086  }
6087 
6097  public static function parseWidthParam( $value, $parseHeight = true ) {
6098  $parsedWidthParam = [];
6099  if ( $value === '' ) {
6100  return $parsedWidthParam;
6101  }
6102  $m = [];
6103  # (T15500) In both cases (width/height and width only),
6104  # permit trailing "px" for backward compatibility.
6105  if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6106  $width = intval( $m[1] );
6107  $height = intval( $m[2] );
6108  $parsedWidthParam['width'] = $width;
6109  $parsedWidthParam['height'] = $height;
6110  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6111  $width = intval( $value );
6112  $parsedWidthParam['width'] = $width;
6113  }
6114  return $parsedWidthParam;
6115  }
6116 
6126  protected function lock() {
6127  if ( $this->mInParse ) {
6128  throw new MWException( "Parser state cleared while parsing. "
6129  . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6130  }
6131 
6132  // Save the backtrace when locking, so that if some code tries locking again,
6133  // we can print the lock owner's backtrace for easier debugging
6134  $e = new Exception;
6135  $this->mInParse = $e->getTraceAsString();
6136 
6137  $recursiveCheck = new ScopedCallback( function () {
6138  $this->mInParse = false;
6139  } );
6140 
6141  return $recursiveCheck;
6142  }
6143 
6154  public static function stripOuterParagraph( $html ) {
6155  $m = [];
6156  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6157  if ( strpos( $m[1], '</p>' ) === false ) {
6158  $html = $m[1];
6159  }
6160  }
6161 
6162  return $html;
6163  }
6164 
6175  public function getFreshParser() {
6176  global $wgParserConf;
6177  if ( $this->mInParse ) {
6178  return new $wgParserConf['class']( $wgParserConf );
6179  } else {
6180  return $this;
6181  }
6182  }
6183 
6190  public function enableOOUI() {
6192  $this->mOutput->setEnableOOUI( true );
6193  }
6194 }
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition: Parser.php:5679
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:585
setTitle($t)
Set the context title.
Definition: Parser.php:767
$mAutonumber
Definition: Parser.php:181
markerSkipCallback($s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers...
Definition: Parser.php:5978
$mPPNodeCount
Definition: Parser.php:195
getRevisionTimestampSubstring($start, $len, $mtts, $variable)
Definition: Parser.php:2835
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition: Parser.php:2106
static getVariableIDs()
Get an array of parser variable IDs.
Definition: MagicWord.php:291
getExternalLinkAttribs($url)
Get an associative array of additional HTML attributes appropriate for a particular external link...
Definition: Parser.php:1945
const MARKER_PREFIX
Definition: Parser.php:135
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global then executing the whole list after the page is displayed We don t do anything smart like collating updates to the same table or such because the list is almost always going to have just one item on if that
Definition: deferred.txt:11
isValidHalfParsedText($data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(), is compatible with the current version of the parser.
Definition: Parser.php:6083
null means default in associative array form
Definition: hooks.txt:2019
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:2019
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1569
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
getSection($text, $sectionId, $defaultText= '')
This function returns the text of a section, specified by a number ($section).
Definition: Parser.php:5644
static decodeTagAttributes($text)
Return an associative array of attribute names and values from a partial tag string.
Definition: Sanitizer.php:1404
$mTplRedirCache
Definition: Parser.php:197
killMarkers($text)
Remove any strip markers found in the given text.
Definition: Parser.php:6008
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
static tocList($toc, $lang=false)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition: Linker.php:1581
LinkRenderer $mLinkRenderer
Definition: Parser.php:261
fetchTemplateAndTitle($title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3563
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:790
getRevisionUser()
Get the name of the user that edited the last revision.
Definition: Parser.php:5731
the array() calling protocol came about after MediaWiki 1.4rc1.
stripSectionName($text)
Strips a text string of wikitext for use in a section anchor.
Definition: Parser.php:5901
const OT_PREPROCESS
Definition: Defines.php:187
static parseWidthParam($value, $parseHeight=true)
Parsed a width param of imagelink like 300px or 200x300px.
Definition: Parser.php:6097
either a plain
Definition: hooks.txt:2080
$mDoubleUnderscores
Definition: Parser.php:197
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2610
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Group all the pieces relevant to the context of a request into one instance.
getPreloadText($text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:737
setTransparentTagHook($tag, callable $callback)
As setHook(), but letting the contents be parsed.
Definition: Parser.php:4793
validateSig($text)
Check that the user's signature contains no bad XML.
Definition: Parser.php:4622
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:247
$wgSitename
Name of the site.
renderImageGallery($text, $params)
Renders an image gallery from a text with one line per image.
Definition: Parser.php:4959
recursivePreprocess($text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition: Parser.php:718
replaceExternalLinks($text)
Replace external links (REL)
Definition: Parser.php:1849
static isNonincludable($index)
It is not possible to use pages from this namespace as template?
const ID_PRIMARY
Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
Definition: Sanitizer.php:64
nextLinkID()
Definition: Parser.php:856
const SPACE_NOT_NL
Definition: Parser.php:104
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:2019
getImageParams($handler)
Definition: Parser.php:5127
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
doHeadings($text)
Parse headers and return html.
Definition: Parser.php:1626
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:82
const OT_PLAIN
Definition: Parser.php:115
getTags()
Accessor.
Definition: Parser.php:5450
static isWellFormedXmlFragment($text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:731
const OT_WIKI
Definition: Parser.php:112
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:2195
fetchFileAndTitle($title, $options=[])
Fetch a file and its title and register a reference to it.
Definition: Parser.php:3707
User $mUser
Definition: Parser.php:204
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers ...
Definition: Parser.php:2865
static isEnabled()
Definition: MWTidy.php:58
static tidy($text)
Interface with html tidy.
Definition: MWTidy.php:46
getFunctionHooks()
Get all registered function hook identifiers.
Definition: Parser.php:4894
static fixTagAttributes($text, $element, $sorted=false)
Take a tag soup fragment listing an HTML element's attributes and normalize it to well-formed XML...
Definition: Sanitizer.php:1109
static makeLegacyAnchor($sectionName)
Definition: Parser.php:5816
static rawElement($element, $attribs=[], $contents= '')
Returns an HTML element in a string.
Definition: Html.php:209
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition: globals.txt:10
wfHostname()
Fetch server name for use in error reporting etc.
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition: Parser.php:871
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition: hooks.txt:2252
setFunctionTagHook($tag, callable $callback, $flags)
Create a tag function, e.g.
Definition: Parser.php:4908
argSubstitution($piece, $frame)
Triple brace replacement – used for template arguments.
Definition: Parser.php:3810
testSrvus($text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition: Parser.php:5930
const TOC_START
Definition: Parser.php:138
Title($x=null)
Accessor/mutator for the Title object.
Definition: Parser.php:795
SectionProfiler $mProfiler
Definition: Parser.php:256
$sort
you don t have to do a grep find to see where the $wgReverseTitle variable is used
Definition: hooks.txt:115
fetchFileNoRegister($title, $options=[])
Helper function for fetchFileAndTitle.
Definition: Parser.php:3732
null for the local wiki Added in
Definition: hooks.txt:1623
There are three types of nodes:
$mHeadings
Definition: Parser.php:197
$value
clearTagHooks()
Remove all tag hooks.
Definition: Parser.php:4807
static makeSelfLinkObj($nt, $html= '', $query= '', $trail= '', $prefix= '')
Make appropriate markup for a link to the current article.
Definition: Linker.php:186
const NS_SPECIAL
Definition: Defines.php:54
clearState()
Clear Parser state.
Definition: Parser.php:347
__construct($conf=[])
Definition: Parser.php:266
const EXT_LINK_ADDR
Definition: Parser.php:97
$mFirstCall
Definition: Parser.php:156
interwikiTransclude($title, $action)
Transclude an interwiki link.
Definition: Parser.php:3751
pstPass2($text, $user)
Pre-save transform helper function.
Definition: Parser.php:4493
guessLegacySectionNameFromWikiText($text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead, if possible.
Definition: Parser.php:5852
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
Options($x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:849
serializeHalfParsedText($text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition: Parser.php:6029
replaceLinkHolders(&$text, $options=0)
Replace "" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:4931
static statelessFetchRevision(Title $title, $parser=false)
Wrapper around Revision::newFromTitle to allow passing additional parameters without passing them on ...
Definition: Parser.php:3552
static activeUsers()
Definition: SiteStats.php:130
$mLinkID
Definition: Parser.php:194
doQuotes($text)
Helper function for doAllQuotes()
Definition: Parser.php:1661
preprocessToDom($text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:2895
limitationWarn($limitationType, $current= '', $max= '')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
Definition: Parser.php:3017
static cleanUrl($url)
Definition: Sanitizer.php:1989
wfUrlencode($s)
We want some things to be included as literal characters in our title URLs for prettiness, which urlencode encodes by default.
static newFromText($text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:273
$mGeneratedPPNodeCount
Definition: Parser.php:195
$mRevisionId
Definition: Parser.php:221
target page
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
doBlockLevels($text, $linestart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: Parser.php:2480
$wgArticlePath
Definition: img_auth.php:45
OutputType($x=null)
Accessor/mutator for the output type.
Definition: Parser.php:821
getLinkRenderer()
Get a LinkRenderer instance to make links with.
Definition: Parser.php:938
const NS_TEMPLATE
Definition: Defines.php:75
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:133
getVariableValue($index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition: Parser.php:2495
recursiveTagParse($text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition: Parser.php:649
const NO_ARGS
magic word & $parser
Definition: hooks.txt:2610
MagicWordArray $mVariables
Definition: Parser.php:163
static validateTagAttributes($attribs, $element)
Take an array of attribute names and values and normalize or discard illegal values for the given ele...
Definition: Sanitizer.php:772
const SFH_NO_HASH
Definition: Parser.php:85
const DB_MASTER
Definition: defines.php:26
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php For a description of the see design txt $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition: globals.txt:25
$mForceTocPosition
Definition: Parser.php:199
preprocess($text, Title $title=null, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition: Parser.php:692
static getCacheTTL($id)
Allow external reads of TTL array.
Definition: MagicWord.php:314
getRevisionId()
Get the ID of the revision we are parsing.
Definition: Parser.php:5669
const OT_PREPROCESS
Definition: Parser.php:113
maybeDoSubpageLink($target, &$text)
Handle link to subpage if necessary.
Definition: Parser.php:2468
$mFunctionSynonyms
Definition: Parser.php:148
If you want to remove the page from your watchlist later
getPreSaveTransform()
Transform wiki markup when saving the page?
replaceLinkHoldersText($text)
Replace "" link placeholders with plain text of links (not HTML-formatted).
Definition: Parser.php:4942
setLinkID($id)
Definition: Parser.php:863
$mOutputType
Definition: Parser.php:218
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
$mDefaultStripList
Definition: Parser.php:151
static createAssocArgs($args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition: Parser.php:2969
$mExtLinkBracketedRegex
Definition: Parser.php:170
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page.Return false to stop further processing of the tag $reader:XMLReader object &$pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn't exist locally, this hook is called to give extensions an opportunity to auto-create it.If the auto-creation is successful, return false.$name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Array with elements of the form"language:title"in the order that they will be output.&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED!Use HtmlPageLinkRendererBegin instead.Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:2017
if($line===false) $args
Definition: cdb.php:64
static getLocalInstance($ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
const ID_FALLBACK
Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false if no fallback...
Definition: Sanitizer.php:72
static getDoubleUnderscoreArray()
Get a MagicWordArray of double-underscore entities.
Definition: MagicWord.php:327
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:1665
getTemplateDom($title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition: Parser.php:3486
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3038
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:46
static decodeCharReferences($text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string...
Definition: Sanitizer.php:1617
cleanSig($text, $parsing=false)
Clean up signature text.
Definition: Parser.php:4636
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static factory($mode=false, IContextSource $context=null)
Get a new image gallery.
$wgLanguageCode
Site language code.
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static getPage($name)
Find the object with a given name and return it (or NULL)
static edits()
Definition: SiteStats.php:94
$wgExtraInterlanguageLinkPrefixes
List of additional interwiki prefixes that should be treated as interlanguage links (i...
startExternalParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Set up some variables which are usually set up in parse() so that an external function can call some ...
Definition: Parser.php:4687
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
const NO_TEMPLATES
addTrackingCategory($msg)
Definition: Parser.php:4044
replaceInternalLinks($s)
Process [[ ]] wikilinks.
Definition: Parser.php:2093
$mVarCache
Definition: Parser.php:152
$wgStylePath
The URL path of the skins directory.
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached...
Definition: Parser.php:5422
$mRevisionObject
Definition: Parser.php:220
static normalizeSectionNameWhitespace($section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(), for use in the id's that are used for section links.
Definition: Sanitizer.php:1498
internalParse($text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1272
Title $mTitle
Definition: Parser.php:217
static delimiterReplace($startDelim, $endDelim, $replace, $subject, $flags= '')
Perform an operation equivalent to preg_replace() with flags.
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: Parser.php:292
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition: Parser.php:5708
static stripOuterParagraph($html)
Strip outer.
Definition: Parser.php:6154
static register($parser)
$mRevIdForTs
Definition: Parser.php:225
static singleton()
Get an instance of this class.
Definition: LinkCache.php:67
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition: design.txt:25
static normalizeSubpageLink($contextTitle, $target, &$text)
Definition: Linker.php:1369
$mStripList
Definition: Parser.php:150
$mFunctionTagHooks
Definition: Parser.php:149
fetchScaryTemplateMaybeFromCache($url)
Definition: Parser.php:3770
const OT_PLAIN
Definition: Defines.php:189
fetchCurrentRevisionOfTitle($title)
Fetch the current revision of a given title.
Definition: Parser.php:3529
$mRevisionTimestamp
Definition: Parser.php:222
$mImageParams
Definition: Parser.php:153
stripAltText($caption, $holders)
Definition: Parser.php:5398
makeLimitReport()
Set the limit report data in the current ParserOutput, and return the limit report HTML comment...
Definition: Parser.php:514
doAllQuotes($text)
Replace single quotes with HTML markup.
Definition: Parser.php:1644
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock()-offset Set to overwrite offset parameter in $wgRequest set to ''to unsetoffset-wrap String Wrap the message in html(usually something like"&lt
static replaceMarkup($search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <...
static normalizeUrlComponent($component, $unsafe)
Definition: Parser.php:2011
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:1256
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way...
Definition: Parser.php:76
const OT_WIKI
Definition: Defines.php:186
Preprocessor $mPreprocessor
Definition: Parser.php:174
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:924
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of normal(non-web) applications--they might conflict with distributors'policies
static getInstance($ts=false)
Get a timestamp instance in GMT.
Definition: MWTimestamp.php:39
const NS_MEDIA
Definition: Defines.php:53
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
static factory($url, array $options=null, $caller=__METHOD__)
Generate a new request object Deprecated:
replaceVariables($text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:2940
const RECOVER_ORIG
wfMatchesDomainList($url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
StripState $mStripState
Definition: Parser.php:186
static setupOOUI($skinName= 'default', $dir= 'ltr')
Helper function to setup the PHP implementation of OOUI to use in this request.
$mDefaultSort
Definition: Parser.php:196
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise...
Definition: Parser.php:912
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
incrementIncludeSize($type, $size)
Increment an include size counter.
Definition: Parser.php:3964
getStripList()
Get a list of strippable XML-like elements.
Definition: Parser.php:1036
const EXT_IMAGE_REGEX
Definition: Parser.php:100
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition: Parser.php:4699
$params
const NS_CATEGORY
Definition: Defines.php:79
static makeHeadline($level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1642
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:2019
$wgFragmentMode
How should section IDs be encoded? This array can contain 1 or 2 elements, each of them can be one of...
static normalizeSectionName($text)
Apply the same normalization as code making links to this section would.
Definition: Parser.php:5875
setHook($tag, callable $callback)
Create an HTML-style tag, e.g.
Definition: Parser.php:4762
and(b) You must cause any modified files to carry prominent notices stating that You changed the files
doTableStuff($text)
parse the wiki syntax used to render tables
Definition: Parser.php:1063
static guessSectionNameFromStrippedText($text)
Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
Definition: Parser.php:5864
wfDeprecated($function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getRevisionSize()
Get the size of the revision.
Definition: Parser.php:5751
$mImageParamsMagicArray
Definition: Parser.php:154
LinkHolderArray $mLinkHolders
Definition: Parser.php:192
static register($parser)
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition: hooks.txt:2019
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition: deferred.txt:4
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to and or sell copies of the and to permit persons to whom the Software is furnished to do so
Definition: LICENSE.txt:10
Some information about database access in MediaWiki By Tim January Database layout For information about the MediaWiki database such as a description of the tables and their please see
Definition: database.txt:2
preSaveTransform($text, Title $title, User $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition: Parser.php:4458
static capturePath(Title $title, IContextSource $context, LinkRenderer $linkRenderer=null)
Just like executePath() but will override global variables and execute the page in "inclusion" mode...
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:884
$buffer
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:941
static hasSubpages($index)
Does the namespace allow subpages?
formatHeadings($text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition: Parser.php:4064
getConverterLanguage()
Get the language object for language conversion.
Definition: Parser.php:902
static tocUnindent($level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1530
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:203
static tocLine($anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1545
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition: design.txt:12
$mInputSize
Definition: Parser.php:226
magicword txt Magic Words are some phrases used in the wikitext They are used for two things
Definition: magicword.txt:4
getUserSig(&$user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any, and normalize to validated, ready-to-insert wikitext...
Definition: Parser.php:4573
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
Definition: Parser.php:82
const NS_FILE
Definition: Defines.php:71
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition: Parser.php:327
static makeImageLink(Parser $parser, Title $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:324
const PTD_FOR_INCLUSION
Definition: Parser.php:107
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped broken
Definition: hooks.txt:2019
armorLinks($text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition: Parser.php:2446
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1799
static splitWhitespace($s)
Return a three-element array: leading whitespace, string contents, trailing whitespace.
Definition: Parser.php:2907
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
setOutputType($ot)
Set the output type.
Definition: Parser.php:804
We use the convention $dbr for read and $dbw for write to help you keep track of whether the database object is a the world will explode Or to be a subsequent write query which succeeded on the master may fail when replicated to the slave due to a unique key collision Replication on the slave will stop and it may take hours to repair the database and get it back online Setting read_only in my cnf on the slave will avoid this but given the dire we prefer to have as many checks as possible We provide a but the wrapper functions like please read the documentation for except in special pages derived from QueryPage It s a common pitfall for new developers to submit code containing SQL queries which examine huge numbers of rows Remember that COUNT * is(N), counting rows in atable is like counting beans in a bucket.------------------------------------------------------------------------Replication------------------------------------------------------------------------The largest installation of MediaWiki, Wikimedia, uses a large set ofslave MySQL servers replicating writes made to a master MySQL server.Itis important to understand the issues associated with this setup if youwant to write code destined for Wikipedia.It's often the case that the best algorithm to use for a given taskdepends on whether or not replication is in use.Due to our unabashedWikipedia-centrism, we often just use the replication-friendly version, but if you like, you can use LoadBalancer::getServerCount() > 1 tocheck to see if replication is in use.===Lag===Lag primarily occurs when large write queries are sent to the master.Writes on the master are executed in parallel, but they are executed inserial when they are replicated to the slaves.The master writes thequery to the binlog when the transaction is committed.The slaves pollthe binlog and start executing the query as soon as it appears.They canservice reads while they are performing a write query, but will not readanything more from the binlog and thus will perform no more writes.Thismeans that if the write query runs for a long time, the slaves will lagbehind the master for the time it takes for the write query to complete.Lag can be exacerbated by high read load.MediaWiki's load balancer willstop sending reads to a slave when it is lagged by more than 30 seconds.If the load ratios are set incorrectly, or if there is too much loadgenerally, this may lead to a slave permanently hovering around 30seconds lag.If all slaves are lagged by more than 30 seconds, MediaWiki will stopwriting to the database.All edits and other write operations will berefused, with an error returned to the user.This gives the slaves achance to catch up.Before we had this mechanism, the slaves wouldregularly lag by several minutes, making review of recent editsdifficult.In addition to this, MediaWiki attempts to ensure that the user seesevents occurring on the wiki in chronological order.A few seconds of lagcan be tolerated, as long as the user sees a consistent picture fromsubsequent requests.This is done by saving the master binlog positionin the session, and then at the start of each request, waiting for theslave to catch up to that position before doing any reads from it.Ifthis wait times out, reads are allowed anyway, but the request isconsidered to be in"lagged slave mode".Lagged slave mode can bechecked by calling LoadBalancer::getLaggedReplicaMode().The onlypractical consequence at present is a warning displayed in the pagefooter.===Lag avoidance===To avoid excessive lag, queries which write large numbers of rows shouldbe split up, generally to write one row at a time.Multi-row INSERT...SELECT queries are the worst offenders should be avoided altogether.Instead do the select first and then the insert.===Working with lag===Despite our best efforts, it's not practical to guarantee a low-lagenvironment.Lag will usually be less than one second, but mayoccasionally be up to 30 seconds.For scalability, it's very importantto keep load on the master low, so simply sending all your queries tothe master is not the answer.So when you have a genuine need forup-to-date data, the following approach is advised:1) Do a quick query to the master for a sequence number or timestamp 2) Run the full query on the slave and check if it matches the data you gotfrom the master 3) If it doesn't, run the full query on the masterTo avoid swamping the master every time the slaves lag, use of thisapproach should be kept to a minimum.In most cases you should just readfrom the slave and let the user deal with the delay.------------------------------------------------------------------------Lock contention------------------------------------------------------------------------Due to the high write rate on Wikipedia(and some other wikis), MediaWiki developers need to be very careful to structure their writesto avoid long-lasting locks.By default, MediaWiki opens a transactionat the first query, and commits it before the output is sent.Locks willbe held from the time when the query is done until the commit.So youcan reduce lock time by doing as much processing as possible before youdo your write queries.Often this approach is not good enough, and it becomes necessary toenclose small groups of queries in their own transaction.Use thefollowing syntax:$dbw=wfGetDB(DB_MASTER
$mTagHooks
Definition: Parser.php:145
Class for handling an array of magic words.
const NS_MEDIAWIKI
Definition: Defines.php:73
static & get($id)
Factory: creates an object representing an ID.
Definition: MagicWord.php:277
enableOOUI()
Set's up the PHP implementation of OOUI for use in this request and instructs OutputPage to enable OO...
Definition: Parser.php:6190
fetchTemplate($title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3591
maybeMakeExternalImage($url)
make an image if it's allowed, either through the global option, through the exception, or through the on-wiki whitelist
Definition: Parser.php:2034
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition: Parser.php:2455
const OT_HTML
Definition: Defines.php:185
static getSubstIDs()
Get an array of parser substitution modifier IDs.
Definition: MagicWord.php:304
static images()
Definition: SiteStats.php:139
$mTransparentTagHooks
Definition: Parser.php:146
$mExpensiveFunctionCount
Definition: Parser.php:198
$mUrlProtocols
Definition: Parser.php:170
$mConf
Definition: Parser.php:170
transformMsg($text, $options, $title=null)
Wrapper for preprocess()
Definition: Parser.php:4718
static newFromId($id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:114
wfUrlProtocols($includeProtocolRelative=true)
Returns a regular expression of url protocols.
static makeExternalLink($url, $text, $escape=true, $linktype= '', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:843
__clone()
Allow extensions to clean up when the parser is cloned.
Definition: Parser.php:304
static getExternalLinkRel($url=false, $title=null)
Get the rel attribute for a particular external link.
Definition: Parser.php:1924
getUser()
Current user.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
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...
this hook is for auditing only $req
Definition: hooks.txt:997
static escapeIdForAttribute($id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:1243
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:790
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc next in line in page history
Definition: hooks.txt:1799
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition: Parser.php:239
const OT_MSG
Definition: Parser.php:114
replaceTransparentTags($text)
Replace transparent tags in $text with the values given by the callbacks.
Definition: Parser.php:5468
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres as and are nearing end of but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition: postgres.txt:22
replaceSection($oldText, $sectionId, $newText)
This function returns $oldtext after the content of the section specified by $section has been replac...
Definition: Parser.php:5660
doDoubleUnderscore($text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores, returns the modified text.
Definition: Parser.php:3991
$mFunctionHooks
Definition: Parser.php:147
static removeHTMLtags($text, $processCallback=null, $args=[], $extratags=[], $removetags=[], $warnCallback=null)
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments. ...
Definition: Sanitizer.php:477
$lines
Definition: router.php:61
testPreprocess($text, Title $title, ParserOptions $options)
Definition: Parser.php:5958
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global then executing the whole list after the page is displayed We don t do anything smart like collating updates to the same table or such because the list is almost always going to have just one item on if so it s not worth the trouble Since there is a job queue in the jobs table
Definition: deferred.txt:11
MagicWordArray $mSubstWords
Definition: Parser.php:168
const TOC_END
Definition: Parser.php:139
static normalizeCharReferences($text)
Ensure that any entities and character references are legal for XML and XHTML specifically.
Definition: Sanitizer.php:1517
setFunctionHook($id, callable $callback, $flags=0)
Create a function, e.g.
Definition: Parser.php:4856
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
callParserFunction($frame, $function, array $args=[])
Call a parser function and return an array with text and flags.
Definition: Parser.php:3392
$wgScriptPath
The path we should point to.
Variant of the Message class.
Definition: RawMessage.php:34
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition: Parser.php:6175
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database etc For and for historical it also represents a few features of articles that don t involve their such as access rights See also title txt Article Encapsulates access to the page table of the database The object represents a an and maintains state such as etc Revision Encapsulates individual page revision data and access to the revision text blobs storage system Higher level code should never touch text storage directly
Definition: design.txt:34
static articles()
Definition: SiteStats.php:103
$mRevisionUser
Definition: Parser.php:223
lock()
Lock the current instance of the parser.
Definition: Parser.php:6126
static pages()
Definition: SiteStats.php:112
$line
Definition: cdb.php:59
const SFH_OBJECT_ARGS
Definition: Parser.php:86
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:56
makeKnownLinkHolder($nt, $text= '', $trail= '', $prefix= '')
Render a forced-blue link inline; protect against double expansion of URLs if we're in a mode that pr...
Definition: Parser.php:2422
static escapeIdForLink($id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
Definition: Sanitizer.php:1270
static statelessFetchTemplate($title, $parser=false)
Static function to get a template Can be overridden via ParserOptions::setTemplateCallback().
Definition: Parser.php:3604
I won t presume to tell you how to I m just describing the methods I chose to use for myself If you do choose to follow these it will probably be easier for you to collaborate with others on the but if you want to contribute without by all means do which work well I also use K &R brace matching style I know that s a religious issue for so if you want to use a style that puts opening braces on the next line
Definition: design.txt:79
static makeMediaLinkFile(Title $title, $file, $html= '')
Create a direct link to a given uploaded file.
Definition: Linker.php:783
$mIncludeCount
Definition: Parser.php:188
usually copyright or history_copyright This message must be in HTML not wikitext if the section is included from a template to be included in the link
Definition: hooks.txt:3038
$mMarkerIndex
Definition: Parser.php:155
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g.with the RejectParserCacheValue hook) because MediaWiki won't do it for you.&$defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition: hooks.txt:2636
getTitle()
Accessor for the Title object.
Definition: Parser.php:785
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition: design.txt:56
extractSections($text, $sectionId, $mode, $newText= '')
Break wikitext input into sections, and either pull or replace some particular section's text...
Definition: Parser.php:5519
ParserOutput $mOutput
Definition: Parser.php:180
getOutput()
Get the ParserOutput object.
Definition: Parser.php:830
doMagicLinks($text)
Replace special strings like "ISBN xxx" and "RFC xxx" with magic external links.
Definition: Parser.php:1448
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation and configuration files Object form shall mean any form resulting from mechanical transformation or translation of a Source including but not limited to compiled object generated and conversions to other media types Work shall mean the work of whether in Source or Object made available under the as indicated by a copyright notice that is included in or attached to the whether in Source or Object that is based or other modifications as a an original work of authorship For the purposes of this Derivative Works shall not include works that remain separable or merely the Work and Derivative Works thereof Contribution shall mean any work of including the original version of the Work and any modifications or additions to that Work or Derivative Works that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner For the purposes of this submitted means any form of or written communication sent to the Licensor or its including but not limited to communication on electronic mailing source code control and issue tracking systems that are managed or on behalf the Licensor for the purpose of discussing and improving the but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work Grant of Copyright License Subject to the terms and conditions of this each Contributor hereby grants to You a non no royalty irrevocable copyright license to prepare Derivative Works publicly display
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
static cleanSigInSig($text)
Strip 3, 4 or 5 tildes out of signatures.
Definition: Parser.php:4673
setDefaultSort($sort)
Mutator for $mDefaultSort.
Definition: Parser.php:5772
fetchFile($title, $options=[])
Fetch a file and its title and register a reference to it.
Definition: Parser.php:3696
static tocIndent()
Add another level to the Table of Contents.
Definition: Linker.php:1519
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:623
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling output() to send it all.It could be easily changed to send incrementally if that becomes useful
static doBlockLevels($text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
$wgServer
URL of the server.
We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going on
Definition: hooks.txt:84
incrementExpensiveFunctionCount()
Increment the expensive function count.
Definition: Parser.php:3978
Status::newGood()`to allow deletion, and then`return false`from the hook function.Ensure you consume the 'ChangeTagAfterDelete'hook to carry out custom deletion actions.$tag:name of the tag $user:user initiating the action &$status:Status object.See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use.&$tags:list of all active tags.Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function.Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change.&$allowedTags:List of all the tags the user is allowed to add.Any tags the user wants to add($addTags) that are not in this array will cause it to fail.You may add or remove tags to this array as required.$addTags:List of tags user intends to add.$user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed.$performer:The User who will perform the change $user:The User whose groups will be changed &$add:The groups that will be added &$remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation.$collationName:Name of the collation in question &$collationObject:Null.Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user's email has been confirmed successfully.$user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object.Called by Content::getParserOutput after parsing has finished.Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML).$content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput.May be used to override the normal model-specific rendering of page content.$content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering.To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state.$generateHtml:boolean, indicating whether full HTML should be generated.If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object.&$output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title.May be used to assign a different model for that title.$title:the Title in question &$model:the model name.Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers.Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core.$modeName:the requested content model name &$handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page.This is especially useful to prevent some content models to be used in some special location.$contentModel:ID of the content model in question $title:the Title in question.&$ok:Output parameter, whether it is OK to use $contentModel on $title.Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run &$pager:Pager object for contributions &$queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions &$data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions &$ret:the HTML line $row:the DB row for this line &$classes:the classes to add to the surrounding< li > &$attribs:associative array of other HTML attributes for the< li > element.Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title &$tools:Array of tool links $specialPage:SpecialPage instance for context and services.Can be either SpecialContributions or DeletedContributionsPage.Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested.Handler functions that modify $result should generally return false to disable further attempts at conversion.$content:The Content object to be converted.$toModel:The ID of the content model to convert to.$lossy:boolean indicating whether lossy conversion is allowed.&$result:Output parameter, in case the handler function wants to provide a converted Content object.Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources.This affects all directives except for the script directive.If you want to add a script source, see ContentSecurityPolicyScriptSource hook.&$defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives.Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs.&$directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources.Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add.&$scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:Cont