MediaWiki  1.33.0
Parser.php
Go to the documentation of this file.
1 <?php
27 use Wikimedia\ScopedCallback;
28 
69 class Parser {
75  const VERSION = '1.6.4';
76 
81  const HALF_PARSED_VERSION = 2;
82 
83  # Flags for Parser::setFunctionHook
84  const SFH_NO_HASH = 1;
85  const SFH_OBJECT_ARGS = 2;
86 
87  # Constants needed for external link processing
88  # Everything except bracket, space, or control characters
89  # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
90  # as well as U+3000 is IDEOGRAPHIC SPACE for T21052
91  # \x{FFFD} is the Unicode replacement character, which Preprocessor_DOM
92  # uses to replace invalid HTML characters.
93  const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]';
94  # Simplified expression to match an IPv4 or IPv6 address, or
95  # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
96  const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}])';
97  # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
98  // phpcs:ignore Generic.Files.LineLength
99  const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}\x{FFFD}]+)
100  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
101 
102  # Regular expression for a non-newline space
103  const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
104 
105  # Flags for preprocessToDom
106  const PTD_FOR_INCLUSION = 1;
107 
108  # Allowed values for $this->mOutputType
109  # Parameter to startExternalParse().
110  const OT_HTML = 1; # like parse()
111  const OT_WIKI = 2; # like preSaveTransform()
112  const OT_PREPROCESS = 3; # like preprocess()
113  const OT_MSG = 3;
114  const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
115 
133  const MARKER_SUFFIX = "-QINU`\"'\x7f";
134  const MARKER_PREFIX = "\x7f'\"`UNIQ-";
135 
136  # Markers used for wrapping the table of contents
137  const TOC_START = '<mw:toc>';
138  const TOC_END = '</mw:toc>';
139 
141  const MAX_TTS = 900;
142 
143  # Persistent:
144  public $mTagHooks = [];
145  public $mTransparentTagHooks = [];
146  public $mFunctionHooks = [];
147  public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
148  public $mFunctionTagHooks = [];
149  public $mStripList = [];
150  public $mDefaultStripList = [];
151  public $mVarCache = [];
152  public $mImageParams = [];
153  public $mImageParamsMagicArray = [];
154  public $mMarkerIndex = 0;
158  public $mFirstCall = true;
159 
160  # Initialised by initialiseVariables()
161 
165  public $mVariables;
166 
170  public $mSubstWords;
171  # Initialised in constructor
172  public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols;
173 
174  # Initialized in getPreprocessor()
175 
176  public $mPreprocessor;
177 
178  # Cleared with clearState():
179 
182  public $mOutput;
183  public $mAutonumber;
184 
188  public $mStripState;
189 
190  public $mIncludeCount;
194  public $mLinkHolders;
195 
196  public $mLinkID;
197  public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
198  public $mDefaultSort;
199  public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
200  public $mExpensiveFunctionCount; # number of expensive parser function calls
201  public $mShowToc, $mForceTocPosition;
202 
206  public $mUser; # User object; only used when doing pre-save transform
207 
208  # Temporary
209  # These are variables reset at least once per parse regardless of $clearState
210 
214  public $mOptions;
215 
219  public $mTitle; # Title context, used for self-link rendering and similar things
220  public $mOutputType; # Output type, one of the OT_xxx constants
221  public $ot; # Shortcut alias, see setOutputType()
222  public $mRevisionObject; # The revision object of the specified revision ID
223  public $mRevisionId; # ID to display in {{REVISIONID}} tags
224  public $mRevisionTimestamp; # The timestamp of the specified revision ID
225  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
226  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
227  public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
228  public $mInputSize = false; # For {{PAGESIZE}} on current page.
229 
234  public $mUniqPrefix = self::MARKER_PREFIX;
235 
241  public $mLangLinkLanguages;
242 
249  public $currentRevisionCache;
250 
255  public $mInParse = false;
256 
258  protected $mProfiler;
259 
263  protected $mLinkRenderer;
264 
266  private $magicWordFactory;
267 
269  private $contLang;
270 
272  private $factory;
273 
275  private $specialPageFactory;
276 
278  private $siteConfig;
279 
281  private $linkRendererFactory;
282 
293  public function __construct(
294  array $parserConf = [], MagicWordFactory $magicWordFactory = null,
295  Language $contLang = null, ParserFactory $factory = null, $urlProtocols = null,
296  SpecialPageFactory $spFactory = null, Config $siteConfig = null,
297  LinkRendererFactory $linkRendererFactory = null
298  ) {
299  $this->mConf = $parserConf;
300  $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
301  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
302  self::EXT_LINK_ADDR .
303  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
304  if ( isset( $parserConf['preprocessorClass'] ) ) {
305  $this->mPreprocessorClass = $parserConf['preprocessorClass'];
306  } elseif ( wfIsHHVM() ) {
307  # Under HHVM Preprocessor_Hash is much faster than Preprocessor_DOM
308  $this->mPreprocessorClass = Preprocessor_Hash::class;
309  } elseif ( extension_loaded( 'domxml' ) ) {
310  # PECL extension that conflicts with the core DOM extension (T15770)
311  wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
312  $this->mPreprocessorClass = Preprocessor_Hash::class;
313  } elseif ( extension_loaded( 'dom' ) ) {
314  $this->mPreprocessorClass = Preprocessor_DOM::class;
315  } else {
316  $this->mPreprocessorClass = Preprocessor_Hash::class;
317  }
318  wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
319 
320  $services = MediaWikiServices::getInstance();
321  $this->magicWordFactory = $magicWordFactory ??
322  $services->getMagicWordFactory();
323 
324  $this->contLang = $contLang ?? $services->getContentLanguage();
325 
326  $this->factory = $factory ?? $services->getParserFactory();
327  $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
328  $this->siteConfig = $siteConfig ?? MediaWikiServices::getInstance()->getMainConfig();
329 
330  $this->linkRendererFactory =
331  $linkRendererFactory ?? MediaWikiServices::getInstance()->getLinkRendererFactory();
332  }
333 
337  public function __destruct() {
338  if ( isset( $this->mLinkHolders ) ) {
339  unset( $this->mLinkHolders );
340  }
341  foreach ( $this as $name => $value ) {
342  unset( $this->$name );
343  }
344  }
345 
349  public function __clone() {
350  $this->mInParse = false;
351 
352  // T58226: When you create a reference "to" an object field, that
353  // makes the object field itself be a reference too (until the other
354  // reference goes out of scope). When cloning, any field that's a
355  // reference is copied as a reference in the new object. Both of these
356  // are defined PHP5 behaviors, as inconvenient as it is for us when old
357  // hooks from PHP4 days are passing fields by reference.
358  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
359  // Make a non-reference copy of the field, then rebind the field to
360  // reference the new copy.
361  $tmp = $this->$k;
362  $this->$k =& $tmp;
363  unset( $tmp );
364  }
365 
366  Hooks::run( 'ParserCloned', [ $this ] );
367  }
368 
372  public function firstCallInit() {
373  if ( !$this->mFirstCall ) {
374  return;
375  }
376  $this->mFirstCall = false;
377 
379  CoreTagHooks::register( $this );
380  $this->initialiseVariables();
381 
382  // Avoid PHP 7.1 warning from passing $this by reference
383  $parser = $this;
384  Hooks::run( 'ParserFirstCallInit', [ &$parser ] );
385  }
386 
392  public function clearState() {
393  $this->firstCallInit();
394  $this->mOutput = new ParserOutput;
395  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
396  $this->mAutonumber = 0;
397  $this->mIncludeCount = [];
398  $this->mLinkHolders = new LinkHolderArray( $this );
399  $this->mLinkID = 0;
400  $this->mRevisionObject = $this->mRevisionTimestamp =
401  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
402  $this->mVarCache = [];
403  $this->mUser = null;
404  $this->mLangLinkLanguages = [];
405  $this->currentRevisionCache = null;
406 
407  $this->mStripState = new StripState( $this );
408 
409  # Clear these on every parse, T6549
410  $this->mTplRedirCache = $this->mTplDomCache = [];
411 
412  $this->mShowToc = true;
413  $this->mForceTocPosition = false;
414  $this->mIncludeSizes = [
415  'post-expand' => 0,
416  'arg' => 0,
417  ];
418  $this->mPPNodeCount = 0;
419  $this->mGeneratedPPNodeCount = 0;
420  $this->mHighestExpansionDepth = 0;
421  $this->mDefaultSort = false;
422  $this->mHeadings = [];
423  $this->mDoubleUnderscores = [];
424  $this->mExpensiveFunctionCount = 0;
425 
426  # Fix cloning
427  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
428  $this->mPreprocessor = null;
429  }
430 
431  $this->mProfiler = new SectionProfiler();
432 
433  // Avoid PHP 7.1 warning from passing $this by reference
434  $parser = $this;
435  Hooks::run( 'ParserClearState', [ &$parser ] );
436  }
437 
452  public function parse(
454  $linestart = true, $clearState = true, $revid = null
455  ) {
456  if ( $clearState ) {
457  // We use U+007F DELETE to construct strip markers, so we have to make
458  // sure that this character does not occur in the input text.
459  $text = strtr( $text, "\x7f", "?" );
460  $magicScopeVariable = $this->lock();
461  }
462  // Strip U+0000 NULL (T159174)
463  $text = str_replace( "\000", '', $text );
464 
465  $this->startParse( $title, $options, self::OT_HTML, $clearState );
466 
467  $this->currentRevisionCache = null;
468  $this->mInputSize = strlen( $text );
469  if ( $this->mOptions->getEnableLimitReport() ) {
470  $this->mOutput->resetParseStartTime();
471  }
472 
473  $oldRevisionId = $this->mRevisionId;
474  $oldRevisionObject = $this->mRevisionObject;
475  $oldRevisionTimestamp = $this->mRevisionTimestamp;
476  $oldRevisionUser = $this->mRevisionUser;
477  $oldRevisionSize = $this->mRevisionSize;
478  if ( $revid !== null ) {
479  $this->mRevisionId = $revid;
480  $this->mRevisionObject = null;
481  $this->mRevisionTimestamp = null;
482  $this->mRevisionUser = null;
483  $this->mRevisionSize = null;
484  }
485 
486  // Avoid PHP 7.1 warning from passing $this by reference
487  $parser = $this;
488  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
489  # No more strip!
490  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
491  $text = $this->internalParse( $text );
492  Hooks::run( 'ParserAfterParse', [ &$parser, &$text, &$this->mStripState ] );
493 
494  $text = $this->internalParseHalfParsed( $text, true, $linestart );
495 
503  if ( !( $options->getDisableTitleConversion()
504  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
505  || isset( $this->mDoubleUnderscores['notitleconvert'] )
506  || $this->mOutput->getDisplayTitle() !== false )
507  ) {
508  $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
509  if ( $convruletitle ) {
510  $this->mOutput->setTitleText( $convruletitle );
511  } else {
512  $titleText = $this->getTargetLanguage()->convertTitle( $title );
513  $this->mOutput->setTitleText( $titleText );
514  }
515  }
516 
517  # Compute runtime adaptive expiry if set
518  $this->mOutput->finalizeAdaptiveCacheExpiry();
519 
520  # Warn if too many heavyweight parser functions were used
521  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
522  $this->limitationWarn( 'expensive-parserfunction',
523  $this->mExpensiveFunctionCount,
524  $this->mOptions->getExpensiveParserFunctionLimit()
525  );
526  }
527 
528  # Information on limits, for the benefit of users who try to skirt them
529  if ( $this->mOptions->getEnableLimitReport() ) {
530  $text .= $this->makeLimitReport();
531  }
532 
533  # Wrap non-interface parser output in a <div> so it can be targeted
534  # with CSS (T37247)
535  $class = $this->mOptions->getWrapOutputClass();
536  if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
537  $this->mOutput->addWrapperDivClass( $class );
538  }
539 
540  $this->mOutput->setText( $text );
541 
542  $this->mRevisionId = $oldRevisionId;
543  $this->mRevisionObject = $oldRevisionObject;
544  $this->mRevisionTimestamp = $oldRevisionTimestamp;
545  $this->mRevisionUser = $oldRevisionUser;
546  $this->mRevisionSize = $oldRevisionSize;
547  $this->mInputSize = false;
548  $this->currentRevisionCache = null;
549 
550  return $this->mOutput;
551  }
552 
559  protected function makeLimitReport() {
560  $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
561 
562  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
563  if ( $cpuTime !== null ) {
564  $this->mOutput->setLimitReportData( 'limitreport-cputime',
565  sprintf( "%.3f", $cpuTime )
566  );
567  }
568 
569  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
570  $this->mOutput->setLimitReportData( 'limitreport-walltime',
571  sprintf( "%.3f", $wallTime )
572  );
573 
574  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
575  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
576  );
577  $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
578  [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
579  );
580  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
581  [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
582  );
583  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
584  [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
585  );
586  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
587  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
588  );
589  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
590  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
591  );
592 
593  foreach ( $this->mStripState->getLimitReport() as list( $key, $value ) ) {
594  $this->mOutput->setLimitReportData( $key, $value );
595  }
596 
597  Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
598 
599  $limitReport = "NewPP limit report\n";
600  if ( $this->siteConfig->get( 'ShowHostnames' ) ) {
601  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
602  }
603  $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
604  $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
605  $limitReport .= 'Dynamic content: ' .
606  ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
607  "\n";
608 
609  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
610  if ( Hooks::run( 'ParserLimitReportFormat',
611  [ $key, &$value, &$limitReport, false, false ]
612  ) ) {
613  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
614  $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
615  ->inLanguage( 'en' )->useDatabase( false );
616  if ( !$valueMsg->exists() ) {
617  $valueMsg = new RawMessage( '$1' );
618  }
619  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
620  $valueMsg->params( $value );
621  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
622  }
623  }
624  }
625  // Since we're not really outputting HTML, decode the entities and
626  // then re-encode the things that need hiding inside HTML comments.
627  $limitReport = htmlspecialchars_decode( $limitReport );
628 
629  // Sanitize for comment. Note '‐' in the replacement is U+2010,
630  // which looks much like the problematic '-'.
631  $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
632  $text = "\n<!-- \n$limitReport-->\n";
633 
634  // Add on template profiling data in human/machine readable way
635  $dataByFunc = $this->mProfiler->getFunctionStats();
636  uasort( $dataByFunc, function ( $a, $b ) {
637  return $b['real'] <=> $a['real']; // descending order
638  } );
639  $profileReport = [];
640  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
641  $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
642  $item['%real'], $item['real'], $item['calls'],
643  htmlspecialchars( $item['name'] ) );
644  }
645  $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
646  $text .= implode( "\n", $profileReport ) . "\n-->\n";
647 
648  $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
649 
650  // Add other cache related metadata
651  if ( $this->siteConfig->get( 'ShowHostnames' ) ) {
652  $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
653  }
654  $this->mOutput->setLimitReportData( 'cachereport-timestamp',
655  $this->mOutput->getCacheTime() );
656  $this->mOutput->setLimitReportData( 'cachereport-ttl',
657  $this->mOutput->getCacheExpiry() );
658  $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
659  $this->mOutput->hasDynamicContent() );
660 
661  if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
662  wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
663  $this->mTitle->getPrefixedDBkey() );
664  }
665  return $text;
666  }
667 
692  public function recursiveTagParse( $text, $frame = false ) {
693  // Avoid PHP 7.1 warning from passing $this by reference
694  $parser = $this;
695  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
696  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
697  $text = $this->internalParse( $text, false, $frame );
698  return $text;
699  }
700 
720  public function recursiveTagParseFully( $text, $frame = false ) {
721  $text = $this->recursiveTagParse( $text, $frame );
722  $text = $this->internalParseHalfParsed( $text, false );
723  return $text;
724  }
725 
737  public function preprocess( $text, Title $title = null,
738  ParserOptions $options, $revid = null, $frame = false
739  ) {
740  $magicScopeVariable = $this->lock();
741  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
742  if ( $revid !== null ) {
743  $this->mRevisionId = $revid;
744  }
745  // Avoid PHP 7.1 warning from passing $this by reference
746  $parser = $this;
747  Hooks::run( 'ParserBeforeStrip', [ &$parser, &$text, &$this->mStripState ] );
748  Hooks::run( 'ParserAfterStrip', [ &$parser, &$text, &$this->mStripState ] );
749  $text = $this->replaceVariables( $text, $frame );
750  $text = $this->mStripState->unstripBoth( $text );
751  return $text;
752  }
753 
763  public function recursivePreprocess( $text, $frame = false ) {
764  $text = $this->replaceVariables( $text, $frame );
765  $text = $this->mStripState->unstripBoth( $text );
766  return $text;
767  }
768 
782  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
783  $msg = new RawMessage( $text );
784  $text = $msg->params( $params )->plain();
785 
786  # Parser (re)initialisation
787  $magicScopeVariable = $this->lock();
788  $this->startParse( $title, $options, self::OT_PLAIN, true );
789 
791  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
792  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
793  $text = $this->mStripState->unstripBoth( $text );
794  return $text;
795  }
796 
803  public function setUser( $user ) {
804  $this->mUser = $user;
805  }
806 
812  public function setTitle( $t ) {
813  if ( !$t ) {
814  $t = Title::newFromText( 'NO TITLE' );
815  }
816 
817  if ( $t->hasFragment() ) {
818  # Strip the fragment to avoid various odd effects
819  $this->mTitle = $t->createFragmentTarget( '' );
820  } else {
821  $this->mTitle = $t;
822  }
823  }
824 
830  public function getTitle() {
831  return $this->mTitle;
832  }
833 
840  public function Title( $x = null ) {
841  return wfSetVar( $this->mTitle, $x );
842  }
843 
849  public function setOutputType( $ot ) {
850  $this->mOutputType = $ot;
851  # Shortcut alias
852  $this->ot = [
853  'html' => $ot == self::OT_HTML,
854  'wiki' => $ot == self::OT_WIKI,
855  'pre' => $ot == self::OT_PREPROCESS,
856  'plain' => $ot == self::OT_PLAIN,
857  ];
858  }
859 
866  public function OutputType( $x = null ) {
867  return wfSetVar( $this->mOutputType, $x );
868  }
869 
875  public function getOutput() {
876  return $this->mOutput;
877  }
878 
884  public function getOptions() {
885  return $this->mOptions;
886  }
887 
894  public function Options( $x = null ) {
895  return wfSetVar( $this->mOptions, $x );
896  }
897 
901  public function nextLinkID() {
902  return $this->mLinkID++;
903  }
904 
908  public function setLinkID( $id ) {
909  $this->mLinkID = $id;
910  }
911 
916  public function getFunctionLang() {
917  return $this->getTargetLanguage();
918  }
919 
929  public function getTargetLanguage() {
930  $target = $this->mOptions->getTargetLanguage();
931 
932  if ( $target !== null ) {
933  return $target;
934  } elseif ( $this->mOptions->getInterfaceMessage() ) {
935  return $this->mOptions->getUserLangObj();
936  } elseif ( is_null( $this->mTitle ) ) {
937  throw new MWException( __METHOD__ . ': $this->mTitle is null' );
938  }
939 
940  return $this->mTitle->getPageLanguage();
941  }
942 
948  public function getConverterLanguage() {
949  return $this->getTargetLanguage();
950  }
951 
958  public function getUser() {
959  if ( !is_null( $this->mUser ) ) {
960  return $this->mUser;
961  }
962  return $this->mOptions->getUser();
963  }
964 
970  public function getPreprocessor() {
971  if ( !isset( $this->mPreprocessor ) ) {
972  $class = $this->mPreprocessorClass;
973  $this->mPreprocessor = new $class( $this );
974  }
975  return $this->mPreprocessor;
976  }
977 
984  public function getLinkRenderer() {
985  // XXX We make the LinkRenderer with current options and then cache it forever
986  if ( !$this->mLinkRenderer ) {
987  $this->mLinkRenderer = $this->linkRendererFactory->create();
988  $this->mLinkRenderer->setStubThreshold(
989  $this->getOptions()->getStubThreshold()
990  );
991  }
992 
993  return $this->mLinkRenderer;
994  }
995 
1002  public function getMagicWordFactory() {
1003  return $this->magicWordFactory;
1004  }
1005 
1012  public function getContentLanguage() {
1013  return $this->contLang;
1014  }
1015 
1035  public static function extractTagsAndParams( $elements, $text, &$matches ) {
1036  static $n = 1;
1037  $stripped = '';
1038  $matches = [];
1039 
1040  $taglist = implode( '|', $elements );
1041  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
1042 
1043  while ( $text != '' ) {
1044  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
1045  $stripped .= $p[0];
1046  if ( count( $p ) < 5 ) {
1047  break;
1048  }
1049  if ( count( $p ) > 5 ) {
1050  # comment
1051  $element = $p[4];
1052  $attributes = '';
1053  $close = '';
1054  $inside = $p[5];
1055  } else {
1056  # tag
1057  list( , $element, $attributes, $close, $inside ) = $p;
1058  }
1059 
1060  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
1061  $stripped .= $marker;
1062 
1063  if ( $close === '/>' ) {
1064  # Empty element tag, <tag />
1065  $content = null;
1066  $text = $inside;
1067  $tail = null;
1068  } else {
1069  if ( $element === '!--' ) {
1070  $end = '/(-->)/';
1071  } else {
1072  $end = "/(<\\/$element\\s*>)/i";
1073  }
1074  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
1075  $content = $q[0];
1076  if ( count( $q ) < 3 ) {
1077  # No end tag -- let it run out to the end of the text.
1078  $tail = '';
1079  $text = '';
1080  } else {
1081  list( , $tail, $text ) = $q;
1082  }
1083  }
1084 
1085  $matches[$marker] = [ $element,
1086  $content,
1087  Sanitizer::decodeTagAttributes( $attributes ),
1088  "<$element$attributes$close$content$tail" ];
1089  }
1090  return $stripped;
1091  }
1092 
1098  public function getStripList() {
1099  return $this->mStripList;
1100  }
1101 
1111  public function insertStripItem( $text ) {
1112  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1113  $this->mMarkerIndex++;
1114  $this->mStripState->addGeneral( $marker, $text );
1115  return $marker;
1116  }
1117 
1125  public function doTableStuff( $text ) {
1126  $lines = StringUtils::explode( "\n", $text );
1127  $out = '';
1128  $td_history = []; # Is currently a td tag open?
1129  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1130  $tr_history = []; # Is currently a tr tag open?
1131  $tr_attributes = []; # history of tr attributes
1132  $has_opened_tr = []; # Did this table open a <tr> element?
1133  $indent_level = 0; # indent level of the table
1134 
1135  foreach ( $lines as $outLine ) {
1136  $line = trim( $outLine );
1137 
1138  if ( $line === '' ) { # empty line, go to next line
1139  $out .= $outLine . "\n";
1140  continue;
1141  }
1142 
1143  $first_character = $line[0];
1144  $first_two = substr( $line, 0, 2 );
1145  $matches = [];
1146 
1147  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1148  # First check if we are starting a new table
1149  $indent_level = strlen( $matches[1] );
1150 
1151  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1152  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1153 
1154  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1155  array_push( $td_history, false );
1156  array_push( $last_tag_history, '' );
1157  array_push( $tr_history, false );
1158  array_push( $tr_attributes, '' );
1159  array_push( $has_opened_tr, false );
1160  } elseif ( count( $td_history ) == 0 ) {
1161  # Don't do any of the following
1162  $out .= $outLine . "\n";
1163  continue;
1164  } elseif ( $first_two === '|}' ) {
1165  # We are ending a table
1166  $line = '</table>' . substr( $line, 2 );
1167  $last_tag = array_pop( $last_tag_history );
1168 
1169  if ( !array_pop( $has_opened_tr ) ) {
1170  $line = "<tr><td></td></tr>{$line}";
1171  }
1172 
1173  if ( array_pop( $tr_history ) ) {
1174  $line = "</tr>{$line}";
1175  }
1176 
1177  if ( array_pop( $td_history ) ) {
1178  $line = "</{$last_tag}>{$line}";
1179  }
1180  array_pop( $tr_attributes );
1181  if ( $indent_level > 0 ) {
1182  $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
1183  } else {
1184  $outLine = $line;
1185  }
1186  } elseif ( $first_two === '|-' ) {
1187  # Now we have a table row
1188  $line = preg_replace( '#^\|-+#', '', $line );
1189 
1190  # Whats after the tag is now only attributes
1191  $attributes = $this->mStripState->unstripBoth( $line );
1192  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1193  array_pop( $tr_attributes );
1194  array_push( $tr_attributes, $attributes );
1195 
1196  $line = '';
1197  $last_tag = array_pop( $last_tag_history );
1198  array_pop( $has_opened_tr );
1199  array_push( $has_opened_tr, true );
1200 
1201  if ( array_pop( $tr_history ) ) {
1202  $line = '</tr>';
1203  }
1204 
1205  if ( array_pop( $td_history ) ) {
1206  $line = "</{$last_tag}>{$line}";
1207  }
1208 
1209  $outLine = $line;
1210  array_push( $tr_history, false );
1211  array_push( $td_history, false );
1212  array_push( $last_tag_history, '' );
1213  } elseif ( $first_character === '|'
1214  || $first_character === '!'
1215  || $first_two === '|+'
1216  ) {
1217  # This might be cell elements, td, th or captions
1218  if ( $first_two === '|+' ) {
1219  $first_character = '+';
1220  $line = substr( $line, 2 );
1221  } else {
1222  $line = substr( $line, 1 );
1223  }
1224 
1225  // Implies both are valid for table headings.
1226  if ( $first_character === '!' ) {
1227  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1228  }
1229 
1230  # Split up multiple cells on the same line.
1231  # FIXME : This can result in improper nesting of tags processed
1232  # by earlier parser steps.
1233  $cells = explode( '||', $line );
1234 
1235  $outLine = '';
1236 
1237  # Loop through each table cell
1238  foreach ( $cells as $cell ) {
1239  $previous = '';
1240  if ( $first_character !== '+' ) {
1241  $tr_after = array_pop( $tr_attributes );
1242  if ( !array_pop( $tr_history ) ) {
1243  $previous = "<tr{$tr_after}>\n";
1244  }
1245  array_push( $tr_history, true );
1246  array_push( $tr_attributes, '' );
1247  array_pop( $has_opened_tr );
1248  array_push( $has_opened_tr, true );
1249  }
1250 
1251  $last_tag = array_pop( $last_tag_history );
1252 
1253  if ( array_pop( $td_history ) ) {
1254  $previous = "</{$last_tag}>\n{$previous}";
1255  }
1256 
1257  if ( $first_character === '|' ) {
1258  $last_tag = 'td';
1259  } elseif ( $first_character === '!' ) {
1260  $last_tag = 'th';
1261  } elseif ( $first_character === '+' ) {
1262  $last_tag = 'caption';
1263  } else {
1264  $last_tag = '';
1265  }
1266 
1267  array_push( $last_tag_history, $last_tag );
1268 
1269  # A cell could contain both parameters and data
1270  $cell_data = explode( '|', $cell, 2 );
1271 
1272  # T2553: Note that a '|' inside an invalid link should not
1273  # be mistaken as delimiting cell parameters
1274  # Bug T153140: Neither should language converter markup.
1275  if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
1276  $cell = "{$previous}<{$last_tag}>" . trim( $cell );
1277  } elseif ( count( $cell_data ) == 1 ) {
1278  // Whitespace in cells is trimmed
1279  $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
1280  } else {
1281  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1282  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1283  // Whitespace in cells is trimmed
1284  $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
1285  }
1286 
1287  $outLine .= $cell;
1288  array_push( $td_history, true );
1289  }
1290  }
1291  $out .= $outLine . "\n";
1292  }
1293 
1294  # Closing open td, tr && table
1295  while ( count( $td_history ) > 0 ) {
1296  if ( array_pop( $td_history ) ) {
1297  $out .= "</td>\n";
1298  }
1299  if ( array_pop( $tr_history ) ) {
1300  $out .= "</tr>\n";
1301  }
1302  if ( !array_pop( $has_opened_tr ) ) {
1303  $out .= "<tr><td></td></tr>\n";
1304  }
1305 
1306  $out .= "</table>\n";
1307  }
1308 
1309  # Remove trailing line-ending (b/c)
1310  if ( substr( $out, -1 ) === "\n" ) {
1311  $out = substr( $out, 0, -1 );
1312  }
1313 
1314  # special case: don't return empty table
1315  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1316  $out = '';
1317  }
1318 
1319  return $out;
1320  }
1321 
1335  public function internalParse( $text, $isMain = true, $frame = false ) {
1336  $origText = $text;
1337 
1338  // Avoid PHP 7.1 warning from passing $this by reference
1339  $parser = $this;
1340 
1341  # Hook to suspend the parser in this state
1342  if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$parser, &$text, &$this->mStripState ] ) ) {
1343  return $text;
1344  }
1345 
1346  # if $frame is provided, then use $frame for replacing any variables
1347  if ( $frame ) {
1348  # use frame depth to infer how include/noinclude tags should be handled
1349  # depth=0 means this is the top-level document; otherwise it's an included document
1350  if ( !$frame->depth ) {
1351  $flag = 0;
1352  } else {
1353  $flag = self::PTD_FOR_INCLUSION;
1354  }
1355  $dom = $this->preprocessToDom( $text, $flag );
1356  $text = $frame->expand( $dom );
1357  } else {
1358  # if $frame is not provided, then use old-style replaceVariables
1359  $text = $this->replaceVariables( $text );
1360  }
1361 
1362  Hooks::run( 'InternalParseBeforeSanitize', [ &$parser, &$text, &$this->mStripState ] );
1363  $text = Sanitizer::removeHTMLtags(
1364  $text,
1365  [ $this, 'attributeStripCallback' ],
1366  false,
1367  array_keys( $this->mTransparentTagHooks ),
1368  [],
1369  [ $this, 'addTrackingCategory' ]
1370  );
1371  Hooks::run( 'InternalParseBeforeLinks', [ &$parser, &$text, &$this->mStripState ] );
1372 
1373  # Tables need to come after variable replacement for things to work
1374  # properly; putting them before other transformations should keep
1375  # exciting things like link expansions from showing up in surprising
1376  # places.
1377  $text = $this->doTableStuff( $text );
1378 
1379  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1380 
1381  $text = $this->doDoubleUnderscore( $text );
1382 
1383  $text = $this->doHeadings( $text );
1384  $text = $this->replaceInternalLinks( $text );
1385  $text = $this->doAllQuotes( $text );
1386  $text = $this->replaceExternalLinks( $text );
1387 
1388  # replaceInternalLinks may sometimes leave behind
1389  # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1390  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1391 
1392  $text = $this->doMagicLinks( $text );
1393  $text = $this->formatHeadings( $text, $origText, $isMain );
1394 
1395  return $text;
1396  }
1397 
1407  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1408  $text = $this->mStripState->unstripGeneral( $text );
1409 
1410  // Avoid PHP 7.1 warning from passing $this by reference
1411  $parser = $this;
1412 
1413  if ( $isMain ) {
1414  Hooks::run( 'ParserAfterUnstrip', [ &$parser, &$text ] );
1415  }
1416 
1417  # Clean up special characters, only run once, next-to-last before doBlockLevels
1418  $text = Sanitizer::armorFrenchSpaces( $text );
1419 
1420  $text = $this->doBlockLevels( $text, $linestart );
1421 
1422  $this->replaceLinkHolders( $text );
1423 
1431  if ( !( $this->mOptions->getDisableContentConversion()
1432  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1433  && !$this->mOptions->getInterfaceMessage()
1434  ) {
1435  # The position of the convert() call should not be changed. it
1436  # assumes that the links are all replaced and the only thing left
1437  # is the <nowiki> mark.
1438  $text = $this->getTargetLanguage()->convert( $text );
1439  }
1440 
1441  $text = $this->mStripState->unstripNoWiki( $text );
1442 
1443  if ( $isMain ) {
1444  Hooks::run( 'ParserBeforeTidy', [ &$parser, &$text ] );
1445  }
1446 
1447  $text = $this->replaceTransparentTags( $text );
1448  $text = $this->mStripState->unstripGeneral( $text );
1449 
1450  $text = Sanitizer::normalizeCharReferences( $text );
1451 
1452  if ( MWTidy::isEnabled() ) {
1453  if ( $this->mOptions->getTidy() ) {
1454  $text = MWTidy::tidy( $text );
1455  }
1456  } else {
1457  # attempt to sanitize at least some nesting problems
1458  # (T4702 and quite a few others)
1459  # This code path is buggy and deprecated!
1460  wfDeprecated( 'disabling tidy', '1.33' );
1461  $tidyregs = [
1462  # ''Something [http://www.cool.com cool''] -->
1463  # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1464  '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1465  '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1466  # fix up an anchor inside another anchor, only
1467  # at least for a single single nested link (T5695)
1468  '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1469  '\\1\\2</a>\\3</a>\\1\\4</a>',
1470  # fix div inside inline elements- doBlockLevels won't wrap a line which
1471  # contains a div, so fix it up here; replace
1472  # div with escaped text
1473  '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1474  '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1475  # remove empty italic or bold tag pairs, some
1476  # introduced by rules above
1477  '/<([bi])><\/\\1>/' => '',
1478  ];
1479 
1480  $text = preg_replace(
1481  array_keys( $tidyregs ),
1482  array_values( $tidyregs ),
1483  $text );
1484  }
1485 
1486  if ( $isMain ) {
1487  Hooks::run( 'ParserAfterTidy', [ &$parser, &$text ] );
1488  }
1489 
1490  return $text;
1491  }
1492 
1504  public function doMagicLinks( $text ) {
1505  $prots = wfUrlProtocolsWithoutProtRel();
1506  $urlChar = self::EXT_LINK_URL_CLASS;
1507  $addr = self::EXT_LINK_ADDR;
1508  $space = self::SPACE_NOT_NL; # non-newline space
1509  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1510  $spaces = "$space++"; # possessive match of 1 or more spaces
1511  $text = preg_replace_callback(
1512  '!(?: # Start cases
1513  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1514  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1515  (\b # m[3]: Free external links
1516  (?i:$prots)
1517  ($addr$urlChar*) # m[4]: Post-protocol path
1518  ) |
1519  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1520  ([0-9]+)\b |
1521  \bISBN $spaces ( # m[6]: ISBN, capture number
1522  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1523  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1524  [0-9Xx] # check digit
1525  )\b
1526  )!xu", [ $this, 'magicLinkCallback' ], $text );
1527  return $text;
1528  }
1529 
1535  public function magicLinkCallback( $m ) {
1536  if ( isset( $m[1] ) && $m[1] !== '' ) {
1537  # Skip anchor
1538  return $m[0];
1539  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1540  # Skip HTML element
1541  return $m[0];
1542  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1543  # Free external link
1544  return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1545  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1546  # RFC or PMID
1547  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1548  if ( !$this->mOptions->getMagicRFCLinks() ) {
1549  return $m[0];
1550  }
1551  $keyword = 'RFC';
1552  $urlmsg = 'rfcurl';
1553  $cssClass = 'mw-magiclink-rfc';
1554  $trackingCat = 'magiclink-tracking-rfc';
1555  $id = $m[5];
1556  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1557  if ( !$this->mOptions->getMagicPMIDLinks() ) {
1558  return $m[0];
1559  }
1560  $keyword = 'PMID';
1561  $urlmsg = 'pubmedurl';
1562  $cssClass = 'mw-magiclink-pmid';
1563  $trackingCat = 'magiclink-tracking-pmid';
1564  $id = $m[5];
1565  } else {
1566  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1567  substr( $m[0], 0, 20 ) . '"' );
1568  }
1569  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1570  $this->addTrackingCategory( $trackingCat );
1571  return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle );
1572  } elseif ( isset( $m[6] ) && $m[6] !== ''
1573  && $this->mOptions->getMagicISBNLinks()
1574  ) {
1575  # ISBN
1576  $isbn = $m[6];
1577  $space = self::SPACE_NOT_NL; # non-newline space
1578  $isbn = preg_replace( "/$space/", ' ', $isbn );
1579  $num = strtr( $isbn, [
1580  '-' => '',
1581  ' ' => '',
1582  'x' => 'X',
1583  ] );
1584  $this->addTrackingCategory( 'magiclink-tracking-isbn' );
1585  return $this->getLinkRenderer()->makeKnownLink(
1586  SpecialPage::getTitleFor( 'Booksources', $num ),
1587  "ISBN $isbn",
1588  [
1589  'class' => 'internal mw-magiclink-isbn',
1590  'title' => false // suppress title attribute
1591  ]
1592  );
1593  } else {
1594  return $m[0];
1595  }
1596  }
1597 
1607  public function makeFreeExternalLink( $url, $numPostProto ) {
1608  $trail = '';
1609 
1610  # The characters '<' and '>' (which were escaped by
1611  # removeHTMLtags()) should not be included in
1612  # URLs, per RFC 2396.
1613  # Make &nbsp; terminate a URL as well (bug T84937)
1614  $m2 = [];
1615  if ( preg_match(
1616  '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1617  $url,
1618  $m2,
1619  PREG_OFFSET_CAPTURE
1620  ) ) {
1621  $trail = substr( $url, $m2[0][1] ) . $trail;
1622  $url = substr( $url, 0, $m2[0][1] );
1623  }
1624 
1625  # Move trailing punctuation to $trail
1626  $sep = ',;\.:!?';
1627  # If there is no left bracket, then consider right brackets fair game too
1628  if ( strpos( $url, '(' ) === false ) {
1629  $sep .= ')';
1630  }
1631 
1632  $urlRev = strrev( $url );
1633  $numSepChars = strspn( $urlRev, $sep );
1634  # Don't break a trailing HTML entity by moving the ; into $trail
1635  # This is in hot code, so use substr_compare to avoid having to
1636  # create a new string object for the comparison
1637  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1638  # more optimization: instead of running preg_match with a $
1639  # anchor, which can be slow, do the match on the reversed
1640  # string starting at the desired offset.
1641  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1642  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1643  $numSepChars--;
1644  }
1645  }
1646  if ( $numSepChars ) {
1647  $trail = substr( $url, -$numSepChars ) . $trail;
1648  $url = substr( $url, 0, -$numSepChars );
1649  }
1650 
1651  # Verify that we still have a real URL after trail removal, and
1652  # not just lone protocol
1653  if ( strlen( $trail ) >= $numPostProto ) {
1654  return $url . $trail;
1655  }
1656 
1657  $url = Sanitizer::cleanUrl( $url );
1658 
1659  # Is this an external image?
1660  $text = $this->maybeMakeExternalImage( $url );
1661  if ( $text === false ) {
1662  # Not an image, make a link
1663  $text = Linker::makeExternalLink( $url,
1664  $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
1665  true, 'free',
1666  $this->getExternalLinkAttribs( $url ), $this->mTitle );
1667  # Register it in the output object...
1668  $this->mOutput->addExternalLink( $url );
1669  }
1670  return $text . $trail;
1671  }
1672 
1682  public function doHeadings( $text ) {
1683  for ( $i = 6; $i >= 1; --$i ) {
1684  $h = str_repeat( '=', $i );
1685  // Trim non-newline whitespace from headings
1686  // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
1687  $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
1688  }
1689  return $text;
1690  }
1691 
1700  public function doAllQuotes( $text ) {
1701  $outtext = '';
1702  $lines = StringUtils::explode( "\n", $text );
1703  foreach ( $lines as $line ) {
1704  $outtext .= $this->doQuotes( $line ) . "\n";
1705  }
1706  $outtext = substr( $outtext, 0, -1 );
1707  return $outtext;
1708  }
1709 
1717  public function doQuotes( $text ) {
1718  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1719  $countarr = count( $arr );
1720  if ( $countarr == 1 ) {
1721  return $text;
1722  }
1723 
1724  // First, do some preliminary work. This may shift some apostrophes from
1725  // being mark-up to being text. It also counts the number of occurrences
1726  // of bold and italics mark-ups.
1727  $numbold = 0;
1728  $numitalics = 0;
1729  for ( $i = 1; $i < $countarr; $i += 2 ) {
1730  $thislen = strlen( $arr[$i] );
1731  // If there are ever four apostrophes, assume the first is supposed to
1732  // be text, and the remaining three constitute mark-up for bold text.
1733  // (T15227: ''''foo'''' turns into ' ''' foo ' ''')
1734  if ( $thislen == 4 ) {
1735  $arr[$i - 1] .= "'";
1736  $arr[$i] = "'''";
1737  $thislen = 3;
1738  } elseif ( $thislen > 5 ) {
1739  // If there are more than 5 apostrophes in a row, assume they're all
1740  // text except for the last 5.
1741  // (T15227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1742  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1743  $arr[$i] = "'''''";
1744  $thislen = 5;
1745  }
1746  // Count the number of occurrences of bold and italics mark-ups.
1747  if ( $thislen == 2 ) {
1748  $numitalics++;
1749  } elseif ( $thislen == 3 ) {
1750  $numbold++;
1751  } elseif ( $thislen == 5 ) {
1752  $numitalics++;
1753  $numbold++;
1754  }
1755  }
1756 
1757  // If there is an odd number of both bold and italics, it is likely
1758  // that one of the bold ones was meant to be an apostrophe followed
1759  // by italics. Which one we cannot know for certain, but it is more
1760  // likely to be one that has a single-letter word before it.
1761  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1762  $firstsingleletterword = -1;
1763  $firstmultiletterword = -1;
1764  $firstspace = -1;
1765  for ( $i = 1; $i < $countarr; $i += 2 ) {
1766  if ( strlen( $arr[$i] ) == 3 ) {
1767  $x1 = substr( $arr[$i - 1], -1 );
1768  $x2 = substr( $arr[$i - 1], -2, 1 );
1769  if ( $x1 === ' ' ) {
1770  if ( $firstspace == -1 ) {
1771  $firstspace = $i;
1772  }
1773  } elseif ( $x2 === ' ' ) {
1774  $firstsingleletterword = $i;
1775  // if $firstsingleletterword is set, we don't
1776  // look at the other options, so we can bail early.
1777  break;
1778  } elseif ( $firstmultiletterword == -1 ) {
1779  $firstmultiletterword = $i;
1780  }
1781  }
1782  }
1783 
1784  // If there is a single-letter word, use it!
1785  if ( $firstsingleletterword > -1 ) {
1786  $arr[$firstsingleletterword] = "''";
1787  $arr[$firstsingleletterword - 1] .= "'";
1788  } elseif ( $firstmultiletterword > -1 ) {
1789  // If not, but there's a multi-letter word, use that one.
1790  $arr[$firstmultiletterword] = "''";
1791  $arr[$firstmultiletterword - 1] .= "'";
1792  } elseif ( $firstspace > -1 ) {
1793  // ... otherwise use the first one that has neither.
1794  // (notice that it is possible for all three to be -1 if, for example,
1795  // there is only one pentuple-apostrophe in the line)
1796  $arr[$firstspace] = "''";
1797  $arr[$firstspace - 1] .= "'";
1798  }
1799  }
1800 
1801  // Now let's actually convert our apostrophic mush to HTML!
1802  $output = '';
1803  $buffer = '';
1804  $state = '';
1805  $i = 0;
1806  foreach ( $arr as $r ) {
1807  if ( ( $i % 2 ) == 0 ) {
1808  if ( $state === 'both' ) {
1809  $buffer .= $r;
1810  } else {
1811  $output .= $r;
1812  }
1813  } else {
1814  $thislen = strlen( $r );
1815  if ( $thislen == 2 ) {
1816  if ( $state === 'i' ) {
1817  $output .= '</i>';
1818  $state = '';
1819  } elseif ( $state === 'bi' ) {
1820  $output .= '</i>';
1821  $state = 'b';
1822  } elseif ( $state === 'ib' ) {
1823  $output .= '</b></i><b>';
1824  $state = 'b';
1825  } elseif ( $state === 'both' ) {
1826  $output .= '<b><i>' . $buffer . '</i>';
1827  $state = 'b';
1828  } else { // $state can be 'b' or ''
1829  $output .= '<i>';
1830  $state .= 'i';
1831  }
1832  } elseif ( $thislen == 3 ) {
1833  if ( $state === 'b' ) {
1834  $output .= '</b>';
1835  $state = '';
1836  } elseif ( $state === 'bi' ) {
1837  $output .= '</i></b><i>';
1838  $state = 'i';
1839  } elseif ( $state === 'ib' ) {
1840  $output .= '</b>';
1841  $state = 'i';
1842  } elseif ( $state === 'both' ) {
1843  $output .= '<i><b>' . $buffer . '</b>';
1844  $state = 'i';
1845  } else { // $state can be 'i' or ''
1846  $output .= '<b>';
1847  $state .= 'b';
1848  }
1849  } elseif ( $thislen == 5 ) {
1850  if ( $state === 'b' ) {
1851  $output .= '</b><i>';
1852  $state = 'i';
1853  } elseif ( $state === 'i' ) {
1854  $output .= '</i><b>';
1855  $state = 'b';
1856  } elseif ( $state === 'bi' ) {
1857  $output .= '</i></b>';
1858  $state = '';
1859  } elseif ( $state === 'ib' ) {
1860  $output .= '</b></i>';
1861  $state = '';
1862  } elseif ( $state === 'both' ) {
1863  $output .= '<i><b>' . $buffer . '</b></i>';
1864  $state = '';
1865  } else { // ($state == '')
1866  $buffer = '';
1867  $state = 'both';
1868  }
1869  }
1870  }
1871  $i++;
1872  }
1873  // Now close all remaining tags. Notice that the order is important.
1874  if ( $state === 'b' || $state === 'ib' ) {
1875  $output .= '</b>';
1876  }
1877  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1878  $output .= '</i>';
1879  }
1880  if ( $state === 'bi' ) {
1881  $output .= '</b>';
1882  }
1883  // There might be lonely ''''', so make sure we have a buffer
1884  if ( $state === 'both' && $buffer ) {
1885  $output .= '<b><i>' . $buffer . '</i></b>';
1886  }
1887  return $output;
1888  }
1889 
1903  public function replaceExternalLinks( $text ) {
1904  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1905  if ( $bits === false ) {
1906  throw new MWException( "PCRE needs to be compiled with "
1907  . "--enable-unicode-properties in order for MediaWiki to function" );
1908  }
1909  $s = array_shift( $bits );
1910 
1911  $i = 0;
1912  while ( $i < count( $bits ) ) {
1913  $url = $bits[$i++];
1914  $i++; // protocol
1915  $text = $bits[$i++];
1916  $trail = $bits[$i++];
1917 
1918  # The characters '<' and '>' (which were escaped by
1919  # removeHTMLtags()) should not be included in
1920  # URLs, per RFC 2396.
1921  $m2 = [];
1922  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1923  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1924  $url = substr( $url, 0, $m2[0][1] );
1925  }
1926 
1927  # If the link text is an image URL, replace it with an <img> tag
1928  # This happened by accident in the original parser, but some people used it extensively
1929  $img = $this->maybeMakeExternalImage( $text );
1930  if ( $img !== false ) {
1931  $text = $img;
1932  }
1933 
1934  $dtrail = '';
1935 
1936  # Set linktype for CSS
1937  $linktype = 'text';
1938 
1939  # No link text, e.g. [http://domain.tld/some.link]
1940  if ( $text == '' ) {
1941  # Autonumber
1942  $langObj = $this->getTargetLanguage();
1943  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1944  $linktype = 'autonumber';
1945  } else {
1946  # Have link text, e.g. [http://domain.tld/some.link text]s
1947  # Check for trail
1948  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1949  }
1950 
1951  // Excluding protocol-relative URLs may avoid many false positives.
1952  if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
1953  $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
1954  }
1955 
1956  $url = Sanitizer::cleanUrl( $url );
1957 
1958  # Use the encoded URL
1959  # This means that users can paste URLs directly into the text
1960  # Funny characters like ö aren't valid in URLs anyway
1961  # This was changed in August 2004
1962  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1963  $this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail;
1964 
1965  # Register link in the output object.
1966  $this->mOutput->addExternalLink( $url );
1967  }
1968 
1969  return $s;
1970  }
1971 
1981  public static function getExternalLinkRel( $url = false, $title = null ) {
1983  $ns = $title ? $title->getNamespace() : false;
1984  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1986  ) {
1987  return 'nofollow';
1988  }
1989  return null;
1990  }
1991 
2002  public function getExternalLinkAttribs( $url ) {
2003  $attribs = [];
2004  $rel = self::getExternalLinkRel( $url, $this->mTitle );
2005 
2006  $target = $this->mOptions->getExternalLinkTarget();
2007  if ( $target ) {
2008  $attribs['target'] = $target;
2009  if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) {
2010  // T133507. New windows can navigate parent cross-origin.
2011  // Including noreferrer due to lacking browser
2012  // support of noopener. Eventually noreferrer should be removed.
2013  if ( $rel !== '' ) {
2014  $rel .= ' ';
2015  }
2016  $rel .= 'noreferrer noopener';
2017  }
2018  }
2019  $attribs['rel'] = $rel;
2020  return $attribs;
2021  }
2022 
2032  public static function normalizeLinkUrl( $url ) {
2033  # Test for RFC 3986 IPv6 syntax
2034  $scheme = '[a-z][a-z0-9+.-]*:';
2035  $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
2036  $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
2037  if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
2038  IP::isValid( rawurldecode( $m[1] ) )
2039  ) {
2040  $isIPv6 = rawurldecode( $m[1] );
2041  } else {
2042  $isIPv6 = false;
2043  }
2044 
2045  # Make sure unsafe characters are encoded
2046  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
2047  function ( $m ) {
2048  return rawurlencode( $m[0] );
2049  },
2050  $url
2051  );
2052 
2053  $ret = '';
2054  $end = strlen( $url );
2055 
2056  # Fragment part - 'fragment'
2057  $start = strpos( $url, '#' );
2058  if ( $start !== false && $start < $end ) {
2059  $ret = self::normalizeUrlComponent(
2060  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
2061  $end = $start;
2062  }
2063 
2064  # Query part - 'query' minus &=+;
2065  $start = strpos( $url, '?' );
2066  if ( $start !== false && $start < $end ) {
2067  $ret = self::normalizeUrlComponent(
2068  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
2069  $end = $start;
2070  }
2071 
2072  # Scheme and path part - 'pchar'
2073  # (we assume no userinfo or encoded colons in the host)
2074  $ret = self::normalizeUrlComponent(
2075  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
2076 
2077  # Fix IPv6 syntax
2078  if ( $isIPv6 !== false ) {
2079  $ipv6Host = "%5B({$isIPv6})%5D";
2080  $ret = preg_replace(
2081  "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
2082  "$1[$2]",
2083  $ret
2084  );
2085  }
2086 
2087  return $ret;
2088  }
2089 
2090  private static function normalizeUrlComponent( $component, $unsafe ) {
2091  $callback = function ( $matches ) use ( $unsafe ) {
2092  $char = urldecode( $matches[0] );
2093  $ord = ord( $char );
2094  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
2095  # Unescape it
2096  return $char;
2097  } else {
2098  # Leave it escaped, but use uppercase for a-f
2099  return strtoupper( $matches[0] );
2100  }
2101  };
2102  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
2103  }
2104 
2113  private function maybeMakeExternalImage( $url ) {
2114  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
2115  $imagesexception = !empty( $imagesfrom );
2116  $text = false;
2117  # $imagesfrom could be either a single string or an array of strings, parse out the latter
2118  if ( $imagesexception && is_array( $imagesfrom ) ) {
2119  $imagematch = false;
2120  foreach ( $imagesfrom as $match ) {
2121  if ( strpos( $url, $match ) === 0 ) {
2122  $imagematch = true;
2123  break;
2124  }
2125  }
2126  } elseif ( $imagesexception ) {
2127  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
2128  } else {
2129  $imagematch = false;
2130  }
2131 
2132  if ( $this->mOptions->getAllowExternalImages()
2133  || ( $imagesexception && $imagematch )
2134  ) {
2135  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
2136  # Image found
2137  $text = Linker::makeExternalImage( $url );
2138  }
2139  }
2140  if ( !$text && $this->mOptions->getEnableImageWhitelist()
2141  && preg_match( self::EXT_IMAGE_REGEX, $url )
2142  ) {
2143  $whitelist = explode(
2144  "\n",
2145  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
2146  );
2147 
2148  foreach ( $whitelist as $entry ) {
2149  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2150  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2151  continue;
2152  }
2153  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2154  # Image matches a whitelist entry
2155  $text = Linker::makeExternalImage( $url );
2156  break;
2157  }
2158  }
2159  }
2160  return $text;
2161  }
2162 
2172  public function replaceInternalLinks( $s ) {
2173  $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2174  return $s;
2175  }
2176 
2185  public function replaceInternalLinks2( &$s ) {
2186  static $tc = false, $e1, $e1_img;
2187  # the % is needed to support urlencoded titles as well
2188  if ( !$tc ) {
2189  $tc = Title::legalChars() . '#%';
2190  # Match a link having the form [[namespace:link|alternate]]trail
2191  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2192  # Match cases where there is no "]]", which might still be images
2193  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2194  }
2195 
2196  $holders = new LinkHolderArray( $this );
2197 
2198  # split the entire text string on occurrences of [[
2199  $a = StringUtils::explode( '[[', ' ' . $s );
2200  # get the first element (all text up to first [[), and remove the space we added
2201  $s = $a->current();
2202  $a->next();
2203  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2204  $s = substr( $s, 1 );
2205 
2206  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2207  $e2 = null;
2208  if ( $useLinkPrefixExtension ) {
2209  # Match the end of a line for a word that's not followed by whitespace,
2210  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2211  $charset = $this->contLang->linkPrefixCharset();
2212  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2213  }
2214 
2215  if ( is_null( $this->mTitle ) ) {
2216  throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2217  }
2218  $nottalk = !$this->mTitle->isTalkPage();
2219 
2220  if ( $useLinkPrefixExtension ) {
2221  $m = [];
2222  if ( preg_match( $e2, $s, $m ) ) {
2223  $first_prefix = $m[2];
2224  } else {
2225  $first_prefix = false;
2226  }
2227  } else {
2228  $prefix = '';
2229  }
2230 
2231  $useSubpages = $this->areSubpagesAllowed();
2232 
2233  # Loop for each link
2234  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2235  # Check for excessive memory usage
2236  if ( $holders->isBig() ) {
2237  # Too big
2238  # Do the existence check, replace the link holders and clear the array
2239  $holders->replace( $s );
2240  $holders->clear();
2241  }
2242 
2243  if ( $useLinkPrefixExtension ) {
2244  if ( preg_match( $e2, $s, $m ) ) {
2245  list( , $s, $prefix ) = $m;
2246  } else {
2247  $prefix = '';
2248  }
2249  # first link
2250  if ( $first_prefix ) {
2251  $prefix = $first_prefix;
2252  $first_prefix = false;
2253  }
2254  }
2255 
2256  $might_be_img = false;
2257 
2258  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2259  $text = $m[2];
2260  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2261  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2262  # the real problem is with the $e1 regex
2263  # See T1500.
2264  # Still some problems for cases where the ] is meant to be outside punctuation,
2265  # and no image is in sight. See T4095.
2266  if ( $text !== ''
2267  && substr( $m[3], 0, 1 ) === ']'
2268  && strpos( $text, '[' ) !== false
2269  ) {
2270  $text .= ']'; # so that replaceExternalLinks($text) works later
2271  $m[3] = substr( $m[3], 1 );
2272  }
2273  # fix up urlencoded title texts
2274  if ( strpos( $m[1], '%' ) !== false ) {
2275  # Should anchors '#' also be rejected?
2276  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2277  }
2278  $trail = $m[3];
2279  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2280  # Invalid, but might be an image with a link in its caption
2281  $might_be_img = true;
2282  $text = $m[2];
2283  if ( strpos( $m[1], '%' ) !== false ) {
2284  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2285  }
2286  $trail = "";
2287  } else { # Invalid form; output directly
2288  $s .= $prefix . '[[' . $line;
2289  continue;
2290  }
2291 
2292  $origLink = ltrim( $m[1], ' ' );
2293 
2294  # Don't allow internal links to pages containing
2295  # PROTO: where PROTO is a valid URL protocol; these
2296  # should be external links.
2297  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2298  $s .= $prefix . '[[' . $line;
2299  continue;
2300  }
2301 
2302  # Make subpage if necessary
2303  if ( $useSubpages ) {
2304  $link = $this->maybeDoSubpageLink( $origLink, $text );
2305  } else {
2306  $link = $origLink;
2307  }
2308 
2309  // \x7f isn't a default legal title char, so most likely strip
2310  // markers will force us into the "invalid form" path above. But,
2311  // just in case, let's assert that xmlish tags aren't valid in
2312  // the title position.
2313  $unstrip = $this->mStripState->killMarkers( $link );
2314  $noMarkers = ( $unstrip === $link );
2315 
2316  $nt = $noMarkers ? Title::newFromText( $link ) : null;
2317  if ( $nt === null ) {
2318  $s .= $prefix . '[[' . $line;
2319  continue;
2320  }
2321 
2322  $ns = $nt->getNamespace();
2323  $iw = $nt->getInterwiki();
2324 
2325  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2326 
2327  if ( $might_be_img ) { # if this is actually an invalid link
2328  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2329  $found = false;
2330  while ( true ) {
2331  # look at the next 'line' to see if we can close it there
2332  $a->next();
2333  $next_line = $a->current();
2334  if ( $next_line === false || $next_line === null ) {
2335  break;
2336  }
2337  $m = explode( ']]', $next_line, 3 );
2338  if ( count( $m ) == 3 ) {
2339  # the first ]] closes the inner link, the second the image
2340  $found = true;
2341  $text .= "[[{$m[0]}]]{$m[1]}";
2342  $trail = $m[2];
2343  break;
2344  } elseif ( count( $m ) == 2 ) {
2345  # if there's exactly one ]] that's fine, we'll keep looking
2346  $text .= "[[{$m[0]}]]{$m[1]}";
2347  } else {
2348  # if $next_line is invalid too, we need look no further
2349  $text .= '[[' . $next_line;
2350  break;
2351  }
2352  }
2353  if ( !$found ) {
2354  # we couldn't find the end of this imageLink, so output it raw
2355  # but don't ignore what might be perfectly normal links in the text we've examined
2356  $holders->merge( $this->replaceInternalLinks2( $text ) );
2357  $s .= "{$prefix}[[$link|$text";
2358  # note: no $trail, because without an end, there *is* no trail
2359  continue;
2360  }
2361  } else { # it's not an image, so output it raw
2362  $s .= "{$prefix}[[$link|$text";
2363  # note: no $trail, because without an end, there *is* no trail
2364  continue;
2365  }
2366  }
2367 
2368  $wasblank = ( $text == '' );
2369  if ( $wasblank ) {
2370  $text = $link;
2371  if ( !$noforce ) {
2372  # Strip off leading ':'
2373  $text = substr( $text, 1 );
2374  }
2375  } else {
2376  # T6598 madness. Handle the quotes only if they come from the alternate part
2377  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2378  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2379  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2380  $text = $this->doQuotes( $text );
2381  }
2382 
2383  # Link not escaped by : , create the various objects
2384  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2385  # Interwikis
2386  if (
2387  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2388  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2389  in_array( $iw, $this->siteConfig->get( 'ExtraInterlanguageLinkPrefixes' ) )
2390  )
2391  ) {
2392  # T26502: filter duplicates
2393  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2394  $this->mLangLinkLanguages[$iw] = true;
2395  $this->mOutput->addLanguageLink( $nt->getFullText() );
2396  }
2397 
2401  $s = rtrim( $s . $prefix ) . $trail; # T175416
2402  continue;
2403  }
2404 
2405  if ( $ns == NS_FILE ) {
2406  if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2407  if ( $wasblank ) {
2408  # if no parameters were passed, $text
2409  # becomes something like "File:Foo.png",
2410  # which we don't want to pass on to the
2411  # image generator
2412  $text = '';
2413  } else {
2414  # recursively parse links inside the image caption
2415  # actually, this will parse them in any other parameters, too,
2416  # but it might be hard to fix that, and it doesn't matter ATM
2417  $text = $this->replaceExternalLinks( $text );
2418  $holders->merge( $this->replaceInternalLinks2( $text ) );
2419  }
2420  # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2421  $s .= $prefix . $this->armorLinks(
2422  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2423  continue;
2424  }
2425  } elseif ( $ns == NS_CATEGORY ) {
2429  $s = rtrim( $s . $prefix ) . $trail; # T2087, T87753
2430 
2431  if ( $wasblank ) {
2432  $sortkey = $this->getDefaultSort();
2433  } else {
2434  $sortkey = $text;
2435  }
2436  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2437  $sortkey = str_replace( "\n", '', $sortkey );
2438  $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
2439  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2440 
2441  continue;
2442  }
2443  }
2444 
2445  # Self-link checking. For some languages, variants of the title are checked in
2446  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2447  # for linking to a different variant.
2448  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2449  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2450  continue;
2451  }
2452 
2453  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2454  # @todo FIXME: Should do batch file existence checks, see comment below
2455  if ( $ns == NS_MEDIA ) {
2456  # Give extensions a chance to select the file revision for us
2457  $options = [];
2458  $descQuery = false;
2459  Hooks::run( 'BeforeParserFetchFileAndTitle',
2460  [ $this, $nt, &$options, &$descQuery ] );
2461  # Fetch and register the file (file title may be different via hooks)
2462  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2463  # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2464  $s .= $prefix . $this->armorLinks(
2465  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2466  continue;
2467  }
2468 
2469  # Some titles, such as valid special pages or files in foreign repos, should
2470  # be shown as bluelinks even though they're not included in the page table
2471  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2472  # batch file existence checks for NS_FILE and NS_MEDIA
2473  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2474  $this->mOutput->addLink( $nt );
2475  $s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix );
2476  } else {
2477  # Links will be added to the output link list after checking
2478  $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2479  }
2480  }
2481  return $holders;
2482  }
2483 
2497  protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) {
2498  list( $inside, $trail ) = Linker::splitTrail( $trail );
2499 
2500  if ( $text == '' ) {
2501  $text = htmlspecialchars( $nt->getPrefixedText() );
2502  }
2503 
2504  $link = $this->getLinkRenderer()->makeKnownLink(
2505  $nt, new HtmlArmor( "$prefix$text$inside" )
2506  );
2507 
2508  return $this->armorLinks( $link ) . $trail;
2509  }
2510 
2521  public function armorLinks( $text ) {
2522  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2523  self::MARKER_PREFIX . "NOPARSE$1", $text );
2524  }
2525 
2530  public function areSubpagesAllowed() {
2531  # Some namespaces don't allow subpages
2532  return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2533  }
2534 
2543  public function maybeDoSubpageLink( $target, &$text ) {
2544  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2545  }
2546 
2555  public function doBlockLevels( $text, $linestart ) {
2556  return BlockLevelPass::doBlockLevels( $text, $linestart );
2557  }
2558 
2570  public function getVariableValue( $index, $frame = false ) {
2571  if ( is_null( $this->mTitle ) ) {
2572  // If no title set, bad things are going to happen
2573  // later. Title should always be set since this
2574  // should only be called in the middle of a parse
2575  // operation (but the unit-tests do funky stuff)
2576  throw new MWException( __METHOD__ . ' Should only be '
2577  . ' called while parsing (no title set)' );
2578  }
2579 
2580  // Avoid PHP 7.1 warning from passing $this by reference
2581  $parser = $this;
2582 
2587  if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] )
2588  && isset( $this->mVarCache[$index] )
2589  ) {
2590  return $this->mVarCache[$index];
2591  }
2592 
2593  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2594  Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
2595 
2596  // In miser mode, disable words that always cause double-parses on page save (T137900)
2597  static $slowRevWords = [ 'revisionid' => true ]; // @TODO: 'revisiontimestamp'
2598  if (
2599  isset( $slowRevWords[$index] ) &&
2600  $this->siteConfig->get( 'MiserMode' ) &&
2601  !$this->mOptions->getInterfaceMessage() &&
2602  // @TODO: disallow this word on all namespaces
2603  MWNamespace::isContent( $this->mTitle->getNamespace() )
2604  ) {
2605  return $this->mRevisionId ? '-' : '';
2606  };
2607 
2608  $pageLang = $this->getFunctionLang();
2609 
2610  switch ( $index ) {
2611  case '!':
2612  $value = '|';
2613  break;
2614  case 'currentmonth':
2615  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
2616  break;
2617  case 'currentmonth1':
2618  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
2619  break;
2620  case 'currentmonthname':
2621  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2622  break;
2623  case 'currentmonthnamegen':
2624  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2625  break;
2626  case 'currentmonthabbrev':
2627  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2628  break;
2629  case 'currentday':
2630  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
2631  break;
2632  case 'currentday2':
2633  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
2634  break;
2635  case 'localmonth':
2636  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
2637  break;
2638  case 'localmonth1':
2639  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
2640  break;
2641  case 'localmonthname':
2642  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2643  break;
2644  case 'localmonthnamegen':
2645  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2646  break;
2647  case 'localmonthabbrev':
2648  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2649  break;
2650  case 'localday':
2651  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
2652  break;
2653  case 'localday2':
2654  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
2655  break;
2656  case 'pagename':
2657  $value = wfEscapeWikiText( $this->mTitle->getText() );
2658  break;
2659  case 'pagenamee':
2660  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2661  break;
2662  case 'fullpagename':
2663  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2664  break;
2665  case 'fullpagenamee':
2666  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2667  break;
2668  case 'subpagename':
2669  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2670  break;
2671  case 'subpagenamee':
2672  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2673  break;
2674  case 'rootpagename':
2675  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2676  break;
2677  case 'rootpagenamee':
2678  $value = wfEscapeWikiText( wfUrlencode( str_replace(
2679  ' ',
2680  '_',
2681  $this->mTitle->getRootText()
2682  ) ) );
2683  break;
2684  case 'basepagename':
2685  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2686  break;
2687  case 'basepagenamee':
2688  $value = wfEscapeWikiText( wfUrlencode( str_replace(
2689  ' ',
2690  '_',
2691  $this->mTitle->getBaseText()
2692  ) ) );
2693  break;
2694  case 'talkpagename':
2695  if ( $this->mTitle->canHaveTalkPage() ) {
2696  $talkPage = $this->mTitle->getTalkPage();
2697  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2698  } else {
2699  $value = '';
2700  }
2701  break;
2702  case 'talkpagenamee':
2703  if ( $this->mTitle->canHaveTalkPage() ) {
2704  $talkPage = $this->mTitle->getTalkPage();
2705  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
2706  } else {
2707  $value = '';
2708  }
2709  break;
2710  case 'subjectpagename':
2711  $subjPage = $this->mTitle->getSubjectPage();
2712  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
2713  break;
2714  case 'subjectpagenamee':
2715  $subjPage = $this->mTitle->getSubjectPage();
2716  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
2717  break;
2718  case 'pageid': // requested in T25427
2719  $pageid = $this->getTitle()->getArticleID();
2720  if ( $pageid == 0 ) {
2721  # 0 means the page doesn't exist in the database,
2722  # which means the user is previewing a new page.
2723  # The vary-revision flag must be set, because the magic word
2724  # will have a different value once the page is saved.
2725  $this->mOutput->setFlag( 'vary-revision' );
2726  wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
2727  }
2728  $value = $pageid ?: null;
2729  break;
2730  case 'revisionid':
2731  # Let the edit saving system know we should parse the page
2732  # *after* a revision ID has been assigned.
2733  $this->mOutput->setFlag( 'vary-revision-id' );
2734  wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
2735  $value = $this->mRevisionId;
2736 
2737  if ( !$value ) {
2738  $rev = $this->getRevisionObject();
2739  if ( $rev ) {
2740  $value = $rev->getId();
2741  }
2742  }
2743 
2744  if ( !$value ) {
2745  $value = $this->mOptions->getSpeculativeRevId();
2746  if ( $value ) {
2747  $this->mOutput->setSpeculativeRevIdUsed( $value );
2748  }
2749  }
2750  break;
2751  case 'revisionday':
2752  $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2753  break;
2754  case 'revisionday2':
2755  $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
2756  break;
2757  case 'revisionmonth':
2758  $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2759  break;
2760  case 'revisionmonth1':
2761  $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
2762  break;
2763  case 'revisionyear':
2764  $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
2765  break;
2766  case 'revisiontimestamp':
2767  # Let the edit saving system know we should parse the page
2768  # *after* a revision ID has been assigned. This is for null edits.
2769  $this->mOutput->setFlag( 'vary-revision' );
2770  wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
2771  $value = $this->getRevisionTimestamp();
2772  break;
2773  case 'revisionuser':
2774  # Let the edit saving system know we should parse the page
2775  # *after* a revision ID has been assigned for null edits.
2776  $this->mOutput->setFlag( 'vary-user' );
2777  wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" );
2778  $value = $this->getRevisionUser();
2779  break;
2780  case 'revisionsize':
2781  $value = $this->getRevisionSize();
2782  break;
2783  case 'namespace':
2784  $value = str_replace( '_', ' ',
2785  $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
2786  break;
2787  case 'namespacee':
2788  $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
2789  break;
2790  case 'namespacenumber':
2791  $value = $this->mTitle->getNamespace();
2792  break;
2793  case 'talkspace':
2794  $value = $this->mTitle->canHaveTalkPage()
2795  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
2796  : '';
2797  break;
2798  case 'talkspacee':
2799  $value = $this->mTitle->canHaveTalkPage() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
2800  break;
2801  case 'subjectspace':
2802  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
2803  break;
2804  case 'subjectspacee':
2805  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
2806  break;
2807  case 'currentdayname':
2808  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
2809  break;
2810  case 'currentyear':
2811  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
2812  break;
2813  case 'currenttime':
2814  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
2815  break;
2816  case 'currenthour':
2817  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
2818  break;
2819  case 'currentweek':
2820  # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2821  # int to remove the padding
2822  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
2823  break;
2824  case 'currentdow':
2825  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
2826  break;
2827  case 'localdayname':
2828  $value = $pageLang->getWeekdayName(
2829  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
2830  );
2831  break;
2832  case 'localyear':
2833  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
2834  break;
2835  case 'localtime':
2836  $value = $pageLang->time(
2837  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
2838  false,
2839  false
2840  );
2841  break;
2842  case 'localhour':
2843  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
2844  break;
2845  case 'localweek':
2846  # @bug T6594 PHP5 has it zero padded, PHP4 does not, cast to
2847  # int to remove the padding
2848  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
2849  break;
2850  case 'localdow':
2851  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
2852  break;
2853  case 'numberofarticles':
2854  $value = $pageLang->formatNum( SiteStats::articles() );
2855  break;
2856  case 'numberoffiles':
2857  $value = $pageLang->formatNum( SiteStats::images() );
2858  break;
2859  case 'numberofusers':
2860  $value = $pageLang->formatNum( SiteStats::users() );
2861  break;
2862  case 'numberofactiveusers':
2863  $value = $pageLang->formatNum( SiteStats::activeUsers() );
2864  break;
2865  case 'numberofpages':
2866  $value = $pageLang->formatNum( SiteStats::pages() );
2867  break;
2868  case 'numberofadmins':
2869  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
2870  break;
2871  case 'numberofedits':
2872  $value = $pageLang->formatNum( SiteStats::edits() );
2873  break;
2874  case 'currenttimestamp':
2875  $value = wfTimestamp( TS_MW, $ts );
2876  break;
2877  case 'localtimestamp':
2878  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
2879  break;
2880  case 'currentversion':
2882  break;
2883  case 'articlepath':
2884  return $this->siteConfig->get( 'ArticlePath' );
2885  case 'sitename':
2886  return $this->siteConfig->get( 'Sitename' );
2887  case 'server':
2888  return $this->siteConfig->get( 'Server' );
2889  case 'servername':
2890  return $this->siteConfig->get( 'ServerName' );
2891  case 'scriptpath':
2892  return $this->siteConfig->get( 'ScriptPath' );
2893  case 'stylepath':
2894  return $this->siteConfig->get( 'StylePath' );
2895  case 'directionmark':
2896  return $pageLang->getDirMark();
2897  case 'contentlanguage':
2898  return $this->siteConfig->get( 'LanguageCode' );
2899  case 'pagelanguage':
2900  $value = $pageLang->getCode();
2901  break;
2902  case 'cascadingsources':
2904  break;
2905  default:
2906  $ret = null;
2907  Hooks::run(
2908  'ParserGetVariableValueSwitch',
2909  [ &$parser, &$this->mVarCache, &$index, &$ret, &$frame ]
2910  );
2911 
2912  return $ret;
2913  }
2914 
2915  if ( $index ) {
2916  $this->mVarCache[$index] = $value;
2917  }
2918 
2919  return $value;
2920  }
2921 
2929  private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
2930  # Get the timezone-adjusted timestamp to be used for this revision
2931  $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
2932  # Possibly set vary-revision if there is not yet an associated revision
2933  if ( !$this->getRevisionObject() ) {
2934  # Get the timezone-adjusted timestamp $mtts seconds in the future
2935  $resThen = substr(
2936  $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
2937  $start,
2938  $len
2939  );
2940 
2941  if ( $resNow !== $resThen ) {
2942  # Let the edit saving system know we should parse the page
2943  # *after* a revision ID has been assigned. This is for null edits.
2944  $this->mOutput->setFlag( 'vary-revision' );
2945  wfDebug( __METHOD__ . ": $variable used, setting vary-revision...\n" );
2946  }
2947  }
2948 
2949  return $resNow;
2950  }
2951 
2957  public function initialiseVariables() {
2958  $variableIDs = $this->magicWordFactory->getVariableIDs();
2959  $substIDs = $this->magicWordFactory->getSubstIDs();
2960 
2961  $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
2962  $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
2963  }
2964 
2987  public function preprocessToDom( $text, $flags = 0 ) {
2988  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
2989  return $dom;
2990  }
2991 
2999  public static function splitWhitespace( $s ) {
3000  $ltrimmed = ltrim( $s );
3001  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3002  $trimmed = rtrim( $ltrimmed );
3003  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3004  if ( $diff > 0 ) {
3005  $w2 = substr( $ltrimmed, -$diff );
3006  } else {
3007  $w2 = '';
3008  }
3009  return [ $w1, $trimmed, $w2 ];
3010  }
3011 
3032  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3033  # Is there any text? Also, Prevent too big inclusions!
3034  $textSize = strlen( $text );
3035  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3036  return $text;
3037  }
3038 
3039  if ( $frame === false ) {
3040  $frame = $this->getPreprocessor()->newFrame();
3041  } elseif ( !( $frame instanceof PPFrame ) ) {
3042  wfDebug( __METHOD__ . " called using plain parameters instead of "
3043  . "a PPFrame instance. Creating custom frame.\n" );
3044  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3045  }
3046 
3047  $dom = $this->preprocessToDom( $text );
3048  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3049  $text = $frame->expand( $dom, $flags );
3050 
3051  return $text;
3052  }
3053 
3061  public static function createAssocArgs( $args ) {
3062  $assocArgs = [];
3063  $index = 1;
3064  foreach ( $args as $arg ) {
3065  $eqpos = strpos( $arg, '=' );
3066  if ( $eqpos === false ) {
3067  $assocArgs[$index++] = $arg;
3068  } else {
3069  $name = trim( substr( $arg, 0, $eqpos ) );
3070  $value = trim( substr( $arg, $eqpos + 1 ) );
3071  if ( $value === false ) {
3072  $value = '';
3073  }
3074  if ( $name !== false ) {
3075  $assocArgs[$name] = $value;
3076  }
3077  }
3078  }
3079 
3080  return $assocArgs;
3081  }
3082 
3109  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3110  # does no harm if $current and $max are present but are unnecessary for the message
3111  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3112  # only during preview, and that would split the parser cache unnecessarily.
3113  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3114  ->text();
3115  $this->mOutput->addWarning( $warning );
3116  $this->addTrackingCategory( "$limitationType-category" );
3117  }
3118 
3131  public function braceSubstitution( $piece, $frame ) {
3132  // Flags
3133 
3134  // $text has been filled
3135  $found = false;
3136  // wiki markup in $text should be escaped
3137  $nowiki = false;
3138  // $text is HTML, armour it against wikitext transformation
3139  $isHTML = false;
3140  // Force interwiki transclusion to be done in raw mode not rendered
3141  $forceRawInterwiki = false;
3142  // $text is a DOM node needing expansion in a child frame
3143  $isChildObj = false;
3144  // $text is a DOM node needing expansion in the current frame
3145  $isLocalObj = false;
3146 
3147  # Title object, where $text came from
3148  $title = false;
3149 
3150  # $part1 is the bit before the first |, and must contain only title characters.
3151  # Various prefixes will be stripped from it later.
3152  $titleWithSpaces = $frame->expand( $piece['title'] );
3153  $part1 = trim( $titleWithSpaces );
3154  $titleText = false;
3155 
3156  # Original title text preserved for various purposes
3157  $originalTitle = $part1;
3158 
3159  # $args is a list of argument nodes, starting from index 0, not including $part1
3160  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3161  # below won't work b/c this $args isn't an object
3162  $args = ( $piece['parts'] == null ) ? [] : $piece['parts'];
3163 
3164  $profileSection = null; // profile templates
3165 
3166  # SUBST
3167  if ( !$found ) {
3168  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3169 
3170  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3171  # Decide whether to expand template or keep wikitext as-is.
3172  if ( $this->ot['wiki'] ) {
3173  if ( $substMatch === false ) {
3174  $literal = true; # literal when in PST with no prefix
3175  } else {
3176  $literal = false; # expand when in PST with subst: or safesubst:
3177  }
3178  } else {
3179  if ( $substMatch == 'subst' ) {
3180  $literal = true; # literal when not in PST with plain subst:
3181  } else {
3182  $literal = false; # expand when not in PST with safesubst: or no prefix
3183  }
3184  }
3185  if ( $literal ) {
3186  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3187  $isLocalObj = true;
3188  $found = true;
3189  }
3190  }
3191 
3192  # Variables
3193  if ( !$found && $args->getLength() == 0 ) {
3194  $id = $this->mVariables->matchStartToEnd( $part1 );
3195  if ( $id !== false ) {
3196  $text = $this->getVariableValue( $id, $frame );
3197  if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
3198  $this->mOutput->updateCacheExpiry(
3199  $this->magicWordFactory->getCacheTTL( $id ) );
3200  }
3201  $found = true;
3202  }
3203  }
3204 
3205  # MSG, MSGNW and RAW
3206  if ( !$found ) {
3207  # Check for MSGNW:
3208  $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
3209  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3210  $nowiki = true;
3211  } else {
3212  # Remove obsolete MSG:
3213  $mwMsg = $this->magicWordFactory->get( 'msg' );
3214  $mwMsg->matchStartAndRemove( $part1 );
3215  }
3216 
3217  # Check for RAW:
3218  $mwRaw = $this->magicWordFactory->get( 'raw' );
3219  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3220  $forceRawInterwiki = true;
3221  }
3222  }
3223 
3224  # Parser functions
3225  if ( !$found ) {
3226  $colonPos = strpos( $part1, ':' );
3227  if ( $colonPos !== false ) {
3228  $func = substr( $part1, 0, $colonPos );
3229  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3230  $argsLength = $args->getLength();
3231  for ( $i = 0; $i < $argsLength; $i++ ) {
3232  $funcArgs[] = $args->item( $i );
3233  }
3234 
3235  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3236 
3237  // Extract any forwarded flags
3238  if ( isset( $result['title'] ) ) {
3239  $title = $result['title'];
3240  }
3241  if ( isset( $result['found'] ) ) {
3242  $found = $result['found'];
3243  }
3244  if ( array_key_exists( 'text', $result ) ) {
3245  // a string or null
3246  $text = $result['text'];
3247  }
3248  if ( isset( $result['nowiki'] ) ) {
3249  $nowiki = $result['nowiki'];
3250  }
3251  if ( isset( $result['isHTML'] ) ) {
3252  $isHTML = $result['isHTML'];
3253  }
3254  if ( isset( $result['forceRawInterwiki'] ) ) {
3255  $forceRawInterwiki = $result['forceRawInterwiki'];
3256  }
3257  if ( isset( $result['isChildObj'] ) ) {
3258  $isChildObj = $result['isChildObj'];
3259  }
3260  if ( isset( $result['isLocalObj'] ) ) {
3261  $isLocalObj = $result['isLocalObj'];
3262  }
3263  }
3264  }
3265 
3266  # Finish mangling title and then check for loops.
3267  # Set $title to a Title object and $titleText to the PDBK
3268  if ( !$found ) {
3269  $ns = NS_TEMPLATE;
3270  # Split the title into page and subpage
3271  $subpage = '';
3272  $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3273  if ( $part1 !== $relative ) {
3274  $part1 = $relative;
3275  $ns = $this->mTitle->getNamespace();
3276  }
3277  $title = Title::newFromText( $part1, $ns );
3278  if ( $title ) {
3279  $titleText = $title->getPrefixedText();
3280  # Check for language variants if the template is not found
3281  if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3282  $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
3283  }
3284  # Do recursion depth check
3285  $limit = $this->mOptions->getMaxTemplateDepth();
3286  if ( $frame->depth >= $limit ) {
3287  $found = true;
3288  $text = '<span class="error">'
3289  . wfMessage( 'parser-template-recursion-depth-warning' )
3290  ->numParams( $limit )->inContentLanguage()->text()
3291  . '</span>';
3292  }
3293  }
3294  }
3295 
3296  # Load from database
3297  if ( !$found && $title ) {
3298  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3299  if ( !$title->isExternal() ) {
3300  if ( $title->isSpecialPage()
3301  && $this->mOptions->getAllowSpecialInclusion()
3302  && $this->ot['html']
3303  ) {
3304  $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
3305  // Pass the template arguments as URL parameters.
3306  // "uselang" will have no effect since the Language object
3307  // is forced to the one defined in ParserOptions.
3308  $pageArgs = [];
3309  $argsLength = $args->getLength();
3310  for ( $i = 0; $i < $argsLength; $i++ ) {
3311  $bits = $args->item( $i )->splitArg();
3312  if ( strval( $bits['index'] ) === '' ) {
3313  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3314  $value = trim( $frame->expand( $bits['value'] ) );
3315  $pageArgs[$name] = $value;
3316  }
3317  }
3318 
3319  // Create a new context to execute the special page
3320  $context = new RequestContext;
3321  $context->setTitle( $title );
3322  $context->setRequest( new FauxRequest( $pageArgs ) );
3323  if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) {
3324  $context->setUser( $this->getUser() );
3325  } else {
3326  // If this page is cached, then we better not be per user.
3327  $context->setUser( User::newFromName( '127.0.0.1', false ) );
3328  }
3329  $context->setLanguage( $this->mOptions->getUserLangObj() );
3330  $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
3331  if ( $ret ) {
3332  $text = $context->getOutput()->getHTML();
3333  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3334  $found = true;
3335  $isHTML = true;
3336  if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
3337  $this->mOutput->updateRuntimeAdaptiveExpiry(
3338  $specialPage->maxIncludeCacheTime()
3339  );
3340  }
3341  }
3342  } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3343  $found = false; # access denied
3344  wfDebug( __METHOD__ . ": template inclusion denied for " .
3345  $title->getPrefixedDBkey() . "\n" );
3346  } else {
3347  list( $text, $title ) = $this->getTemplateDom( $title );
3348  if ( $text !== false ) {
3349  $found = true;
3350  $isChildObj = true;
3351  }
3352  }
3353 
3354  # If the title is valid but undisplayable, make a link to it
3355  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3356  $text = "[[:$titleText]]";
3357  $found = true;
3358  }
3359  } elseif ( $title->isTrans() ) {
3360  # Interwiki transclusion
3361  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3362  $text = $this->interwikiTransclude( $title, 'render' );
3363  $isHTML = true;
3364  } else {
3365  $text = $this->interwikiTransclude( $title, 'raw' );
3366  # Preprocess it like a template
3367  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3368  $isChildObj = true;
3369  }
3370  $found = true;
3371  }
3372 
3373  # Do infinite loop check
3374  # This has to be done after redirect resolution to avoid infinite loops via redirects
3375  if ( !$frame->loopCheck( $title ) ) {
3376  $found = true;
3377  $text = '<span class="error">'
3378  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3379  . '</span>';
3380  $this->addTrackingCategory( 'template-loop-category' );
3381  $this->mOutput->addWarning( wfMessage( 'template-loop-warning',
3382  wfEscapeWikiText( $titleText ) )->text() );
3383  wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3384  }
3385  }
3386 
3387  # If we haven't found text to substitute by now, we're done
3388  # Recover the source wikitext and return it
3389  if ( !$found ) {
3390  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3391  if ( $profileSection ) {
3392  $this->mProfiler->scopedProfileOut( $profileSection );
3393  }
3394  return [ 'object' => $text ];
3395  }
3396 
3397  # Expand DOM-style return values in a child frame
3398  if ( $isChildObj ) {
3399  # Clean up argument array
3400  $newFrame = $frame->newChild( $args, $title );
3401 
3402  if ( $nowiki ) {
3403  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3404  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3405  # Expansion is eligible for the empty-frame cache
3406  $text = $newFrame->cachedExpand( $titleText, $text );
3407  } else {
3408  # Uncached expansion
3409  $text = $newFrame->expand( $text );
3410  }
3411  }
3412  if ( $isLocalObj && $nowiki ) {
3413  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3414  $isLocalObj = false;
3415  }
3416 
3417  if ( $profileSection ) {
3418  $this->mProfiler->scopedProfileOut( $profileSection );
3419  }
3420 
3421  # Replace raw HTML by a placeholder
3422  if ( $isHTML ) {
3423  $text = $this->insertStripItem( $text );
3424  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3425  # Escape nowiki-style return values
3426  $text = wfEscapeWikiText( $text );
3427  } elseif ( is_string( $text )
3428  && !$piece['lineStart']
3429  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3430  ) {
3431  # T2529: if the template begins with a table or block-level
3432  # element, it should be treated as beginning a new line.
3433  # This behavior is somewhat controversial.
3434  $text = "\n" . $text;
3435  }
3436 
3437  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3438  # Error, oversize inclusion
3439  if ( $titleText !== false ) {
3440  # Make a working, properly escaped link if possible (T25588)
3441  $text = "[[:$titleText]]";
3442  } else {
3443  # This will probably not be a working link, but at least it may
3444  # provide some hint of where the problem is
3445  preg_replace( '/^:/', '', $originalTitle );
3446  $text = "[[:$originalTitle]]";
3447  }
3448  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3449  . 'post-expand include size too large -->' );
3450  $this->limitationWarn( 'post-expand-template-inclusion' );
3451  }
3452 
3453  if ( $isLocalObj ) {
3454  $ret = [ 'object' => $text ];
3455  } else {
3456  $ret = [ 'text' => $text ];
3457  }
3458 
3459  return $ret;
3460  }
3461 
3481  public function callParserFunction( $frame, $function, array $args = [] ) {
3482  # Case sensitive functions
3483  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3484  $function = $this->mFunctionSynonyms[1][$function];
3485  } else {
3486  # Case insensitive functions
3487  $function = $this->contLang->lc( $function );
3488  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3489  $function = $this->mFunctionSynonyms[0][$function];
3490  } else {
3491  return [ 'found' => false ];
3492  }
3493  }
3494 
3495  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3496 
3497  // Avoid PHP 7.1 warning from passing $this by reference
3498  $parser = $this;
3499 
3500  $allArgs = [ &$parser ];
3501  if ( $flags & self::SFH_OBJECT_ARGS ) {
3502  # Convert arguments to PPNodes and collect for appending to $allArgs
3503  $funcArgs = [];
3504  foreach ( $args as $k => $v ) {
3505  if ( $v instanceof PPNode || $k === 0 ) {
3506  $funcArgs[] = $v;
3507  } else {
3508  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3509  }
3510  }
3511 
3512  # Add a frame parameter, and pass the arguments as an array
3513  $allArgs[] = $frame;
3514  $allArgs[] = $funcArgs;
3515  } else {
3516  # Convert arguments to plain text and append to $allArgs
3517  foreach ( $args as $k => $v ) {
3518  if ( $v instanceof PPNode ) {
3519  $allArgs[] = trim( $frame->expand( $v ) );
3520  } elseif ( is_int( $k ) && $k >= 0 ) {
3521  $allArgs[] = trim( $v );
3522  } else {
3523  $allArgs[] = trim( "$k=$v" );
3524  }
3525  }
3526  }
3527 
3528  $result = $callback( ...$allArgs );
3529 
3530  # The interface for function hooks allows them to return a wikitext
3531  # string or an array containing the string and any flags. This mungs
3532  # things around to match what this method should return.
3533  if ( !is_array( $result ) ) {
3534  $result = [
3535  'found' => true,
3536  'text' => $result,
3537  ];
3538  } else {
3539  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3540  $result['text'] = $result[0];
3541  }
3542  unset( $result[0] );
3543  $result += [
3544  'found' => true,
3545  ];
3546  }
3547 
3548  $noparse = true;
3549  $preprocessFlags = 0;
3550  if ( isset( $result['noparse'] ) ) {
3551  $noparse = $result['noparse'];
3552  }
3553  if ( isset( $result['preprocessFlags'] ) ) {
3554  $preprocessFlags = $result['preprocessFlags'];
3555  }
3556 
3557  if ( !$noparse ) {
3558  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3559  $result['isChildObj'] = true;
3560  }
3561 
3562  return $result;
3563  }
3564 
3573  public function getTemplateDom( $title ) {
3574  $cacheTitle = $title;
3575  $titleText = $title->getPrefixedDBkey();
3576 
3577  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3578  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3579  $title = Title::makeTitle( $ns, $dbk );
3580  $titleText = $title->getPrefixedDBkey();
3581  }
3582  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3583  return [ $this->mTplDomCache[$titleText], $title ];
3584  }
3585 
3586  # Cache miss, go to the database
3587  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3588 
3589  if ( $text === false ) {
3590  $this->mTplDomCache[$titleText] = false;
3591  return [ false, $title ];
3592  }
3593 
3594  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3595  $this->mTplDomCache[$titleText] = $dom;
3596 
3597  if ( !$title->equals( $cacheTitle ) ) {
3598  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3599  [ $title->getNamespace(), $title->getDBkey() ];
3600  }
3601 
3602  return [ $dom, $title ];
3603  }
3604 
3616  public function fetchCurrentRevisionOfTitle( $title ) {
3617  $cacheKey = $title->getPrefixedDBkey();
3618  if ( !$this->currentRevisionCache ) {
3619  $this->currentRevisionCache = new MapCacheLRU( 100 );
3620  }
3621  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3622  $this->currentRevisionCache->set( $cacheKey,
3623  // Defaults to Parser::statelessFetchRevision()
3624  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3625  );
3626  }
3627  return $this->currentRevisionCache->get( $cacheKey );
3628  }
3629 
3639  public static function statelessFetchRevision( Title $title, $parser = false ) {
3641 
3642  return $rev;
3643  }
3644 
3650  public function fetchTemplateAndTitle( $title ) {
3651  // Defaults to Parser::statelessFetchTemplate()
3652  $templateCb = $this->mOptions->getTemplateCallback();
3653  $stuff = call_user_func( $templateCb, $title, $this );
3654  // We use U+007F DELETE to distinguish strip markers from regular text.
3655  $text = $stuff['text'];
3656  if ( is_string( $stuff['text'] ) ) {
3657  $text = strtr( $text, "\x7f", "?" );
3658  }
3659  $finalTitle = $stuff['finalTitle'] ?? $title;
3660  if ( isset( $stuff['deps'] ) ) {
3661  foreach ( $stuff['deps'] as $dep ) {
3662  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3663  if ( $dep['title']->equals( $this->getTitle() ) ) {
3664  // If we transclude ourselves, the final result
3665  // will change based on the new version of the page
3666  $this->mOutput->setFlag( 'vary-revision' );
3667  }
3668  }
3669  }
3670  return [ $text, $finalTitle ];
3671  }
3672 
3678  public function fetchTemplate( $title ) {
3679  return $this->fetchTemplateAndTitle( $title )[0];
3680  }
3681 
3691  public static function statelessFetchTemplate( $title, $parser = false ) {
3692  $text = $skip = false;
3693  $finalTitle = $title;
3694  $deps = [];
3695 
3696  # Loop to fetch the article, with up to 1 redirect
3697  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3698  # Give extensions a chance to select the revision instead
3699  $id = false; # Assume current
3700  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3701  [ $parser, $title, &$skip, &$id ] );
3702 
3703  if ( $skip ) {
3704  $text = false;
3705  $deps[] = [
3706  'title' => $title,
3707  'page_id' => $title->getArticleID(),
3708  'rev_id' => null
3709  ];
3710  break;
3711  }
3712  # Get the revision
3713  if ( $id ) {
3714  $rev = Revision::newFromId( $id );
3715  } elseif ( $parser ) {
3716  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3717  } else {
3719  }
3720  $rev_id = $rev ? $rev->getId() : 0;
3721  # If there is no current revision, there is no page
3722  if ( $id === false && !$rev ) {
3723  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3724  $linkCache->addBadLinkObj( $title );
3725  }
3726 
3727  $deps[] = [
3728  'title' => $title,
3729  'page_id' => $title->getArticleID(),
3730  'rev_id' => $rev_id ];
3731  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3732  # We fetched a rev from a different title; register it too...
3733  $deps[] = [
3734  'title' => $rev->getTitle(),
3735  'page_id' => $rev->getPage(),
3736  'rev_id' => $rev_id ];
3737  }
3738 
3739  if ( $rev ) {
3740  $content = $rev->getContent();
3741  $text = $content ? $content->getWikitextForTransclusion() : null;
3742 
3743  Hooks::run( 'ParserFetchTemplate',
3744  [ $parser, $title, $rev, &$text, &$deps ] );
3745 
3746  if ( $text === false || $text === null ) {
3747  $text = false;
3748  break;
3749  }
3750  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3751  $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
3752  lcfirst( $title->getText() ) )->inContentLanguage();
3753  if ( !$message->exists() ) {
3754  $text = false;
3755  break;
3756  }
3757  $content = $message->content();
3758  $text = $message->plain();
3759  } else {
3760  break;
3761  }
3762  if ( !$content ) {
3763  break;
3764  }
3765  # Redirect?
3766  $finalTitle = $title;
3767  $title = $content->getRedirectTarget();
3768  }
3769  return [
3770  'text' => $text,
3771  'finalTitle' => $finalTitle,
3772  'deps' => $deps ];
3773  }
3774 
3783  public function fetchFile( $title, $options = [] ) {
3784  wfDeprecated( __METHOD__, '1.32' );
3785  return $this->fetchFileAndTitle( $title, $options )[0];
3786  }
3787 
3795  public function fetchFileAndTitle( $title, $options = [] ) {
3796  $file = $this->fetchFileNoRegister( $title, $options );
3797 
3798  $time = $file ? $file->getTimestamp() : false;
3799  $sha1 = $file ? $file->getSha1() : false;
3800  # Register the file as a dependency...
3801  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3802  if ( $file && !$title->equals( $file->getTitle() ) ) {
3803  # Update fetched file title
3804  $title = $file->getTitle();
3805  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
3806  }
3807  return [ $file, $title ];
3808  }
3809 
3820  protected function fetchFileNoRegister( $title, $options = [] ) {
3821  if ( isset( $options['broken'] ) ) {
3822  $file = false; // broken thumbnail forced by hook
3823  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
3824  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
3825  } else { // get by (name,timestamp)
3827  }
3828  return $file;
3829  }
3830 
3839  public function interwikiTransclude( $title, $action ) {
3840  if ( !$this->siteConfig->get( 'EnableScaryTranscluding' ) ) {
3841  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
3842  }
3843 
3844  $url = $title->getFullURL( [ 'action' => $action ] );
3845  if ( strlen( $url ) > 1024 ) {
3846  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
3847  }
3848 
3849  $wikiId = $title->getTransWikiID(); // remote wiki ID or false
3850 
3851  $fname = __METHOD__;
3852  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
3853 
3854  $data = $cache->getWithSetCallback(
3855  $cache->makeGlobalKey(
3856  'interwiki-transclude',
3857  ( $wikiId !== false ) ? $wikiId : 'external',
3858  sha1( $url )
3859  ),
3860  $this->siteConfig->get( 'TranscludeCacheExpiry' ),
3861  function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
3862  $req = MWHttpRequest::factory( $url, [], $fname );
3863 
3864  $status = $req->execute(); // Status object
3865  if ( !$status->isOK() ) {
3866  $ttl = $cache::TTL_UNCACHEABLE;
3867  } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
3868  $ttl = min( $cache::TTL_LAGGED, $ttl );
3869  }
3870 
3871  return [
3872  'text' => $status->isOK() ? $req->getContent() : null,
3873  'code' => $req->getStatus()
3874  ];
3875  },
3876  [
3877  'checkKeys' => ( $wikiId !== false )
3878  ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
3879  : [],
3880  'pcGroup' => 'interwiki-transclude:5',
3881  'pcTTL' => $cache::TTL_PROC_LONG
3882  ]
3883  );
3884 
3885  if ( is_string( $data['text'] ) ) {
3886  $text = $data['text'];
3887  } elseif ( $data['code'] != 200 ) {
3888  // Though we failed to fetch the content, this status is useless.
3889  $text = wfMessage( 'scarytranscludefailed-httpstatus' )
3890  ->params( $url, $data['code'] )->inContentLanguage()->text();
3891  } else {
3892  $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
3893  }
3894 
3895  return $text;
3896  }
3897 
3907  public function argSubstitution( $piece, $frame ) {
3908  $error = false;
3909  $parts = $piece['parts'];
3910  $nameWithSpaces = $frame->expand( $piece['title'] );
3911  $argName = trim( $nameWithSpaces );
3912  $object = false;
3913  $text = $frame->getArgument( $argName );
3914  if ( $text === false && $parts->getLength() > 0
3915  && ( $this->ot['html']
3916  || $this->ot['pre']
3917  || ( $this->ot['wiki'] && $frame->isTemplate() )
3918  )
3919  ) {
3920  # No match in frame, use the supplied default
3921  $object = $parts->item( 0 )->getChildren();
3922  }
3923  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
3924  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
3925  $this->limitationWarn( 'post-expand-template-argument' );
3926  }
3927 
3928  if ( $text === false && $object === false ) {
3929  # No match anywhere
3930  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
3931  }
3932  if ( $error !== false ) {
3933  $text .= $error;
3934  }
3935  if ( $object !== false ) {
3936  $ret = [ 'object' => $object ];
3937  } else {
3938  $ret = [ 'text' => $text ];
3939  }
3940 
3941  return $ret;
3942  }
3943 
3959  public function extensionSubstitution( $params, $frame ) {
3960  static $errorStr = '<span class="error">';
3961  static $errorLen = 20;
3962 
3963  $name = $frame->expand( $params['name'] );
3964  if ( substr( $name, 0, $errorLen ) === $errorStr ) {
3965  // Probably expansion depth or node count exceeded. Just punt the
3966  // error up.
3967  return $name;
3968  }
3969 
3970  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
3971  if ( substr( $attrText, 0, $errorLen ) === $errorStr ) {
3972  // See above
3973  return $attrText;
3974  }
3975 
3976  // We can't safely check if the expansion for $content resulted in an
3977  // error, because the content could happen to be the error string
3978  // (T149622).
3979  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
3980 
3981  $marker = self::MARKER_PREFIX . "-$name-"
3982  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
3983 
3984  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
3985  ( $this->ot['html'] || $this->ot['pre'] );
3986  if ( $isFunctionTag ) {
3987  $markerType = 'none';
3988  } else {
3989  $markerType = 'general';
3990  }
3991  if ( $this->ot['html'] || $isFunctionTag ) {
3992  $name = strtolower( $name );
3993  $attributes = Sanitizer::decodeTagAttributes( $attrText );
3994  if ( isset( $params['attributes'] ) ) {
3995  $attributes += $params['attributes'];
3996  }
3997 
3998  if ( isset( $this->mTagHooks[$name] ) ) {
3999  $output = call_user_func_array( $this->mTagHooks[$name],
4000  [ $content, $attributes, $this, $frame ] );
4001  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4002  list( $callback, ) = $this->mFunctionTagHooks[$name];
4003 
4004  // Avoid PHP 7.1 warning from passing $this by reference
4005  $parser = $this;
4006  $output = call_user_func_array( $callback, [ &$parser, $frame, $content, $attributes ] );
4007  } else {
4008  $output = '<span class="error">Invalid tag extension name: ' .
4009  htmlspecialchars( $name ) . '</span>';
4010  }
4011 
4012  if ( is_array( $output ) ) {
4013  // Extract flags
4014  $flags = $output;
4015  $output = $flags[0];
4016  if ( isset( $flags['markerType'] ) ) {
4017  $markerType = $flags['markerType'];
4018  }
4019  }
4020  } else {
4021  if ( is_null( $attrText ) ) {
4022  $attrText = '';
4023  }
4024  if ( isset( $params['attributes'] ) ) {
4025  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4026  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4027  htmlspecialchars( $attrValue ) . '"';
4028  }
4029  }
4030  if ( $content === null ) {
4031  $output = "<$name$attrText/>";
4032  } else {
4033  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4034  if ( substr( $close, 0, $errorLen ) === $errorStr ) {
4035  // See above
4036  return $close;
4037  }
4038  $output = "<$name$attrText>$content$close";
4039  }
4040  }
4041 
4042  if ( $markerType === 'none' ) {
4043  return $output;
4044  } elseif ( $markerType === 'nowiki' ) {
4045  $this->mStripState->addNoWiki( $marker, $output );
4046  } elseif ( $markerType === 'general' ) {
4047  $this->mStripState->addGeneral( $marker, $output );
4048  } else {
4049  throw new MWException( __METHOD__ . ': invalid marker type' );
4050  }
4051  return $marker;
4052  }
4053 
4061  public function incrementIncludeSize( $type, $size ) {
4062  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4063  return false;
4064  } else {
4065  $this->mIncludeSizes[$type] += $size;
4066  return true;
4067  }
4068  }
4069 
4075  public function incrementExpensiveFunctionCount() {
4076  $this->mExpensiveFunctionCount++;
4077  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4078  }
4079 
4088  public function doDoubleUnderscore( $text ) {
4089  # The position of __TOC__ needs to be recorded
4090  $mw = $this->magicWordFactory->get( 'toc' );
4091  if ( $mw->match( $text ) ) {
4092  $this->mShowToc = true;
4093  $this->mForceTocPosition = true;
4094 
4095  # Set a placeholder. At the end we'll fill it in with the TOC.
4096  $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
4097 
4098  # Only keep the first one.
4099  $text = $mw->replace( '', $text );
4100  }
4101 
4102  # Now match and remove the rest of them
4103  $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
4104  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4105 
4106  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4107  $this->mOutput->mNoGallery = true;
4108  }
4109  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4110  $this->mShowToc = false;
4111  }
4112  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4113  && $this->mTitle->getNamespace() == NS_CATEGORY
4114  ) {
4115  $this->addTrackingCategory( 'hidden-category-category' );
4116  }
4117  # (T10068) Allow control over whether robots index a page.
4118  # __INDEX__ always overrides __NOINDEX__, see T16899
4119  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4120  $this->mOutput->setIndexPolicy( 'noindex' );
4121  $this->addTrackingCategory( 'noindex-category' );
4122  }
4123  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4124  $this->mOutput->setIndexPolicy( 'index' );
4125  $this->addTrackingCategory( 'index-category' );
4126  }
4127 
4128  # Cache all double underscores in the database
4129  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4130  $this->mOutput->setProperty( $key, '' );
4131  }
4132 
4133  return $text;
4134  }
4135 
4141  public function addTrackingCategory( $msg ) {
4142  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4143  }
4144 
4161  public function formatHeadings( $text, $origText, $isMain = true ) {
4162  # Inhibit editsection links if requested in the page
4163  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4164  $maybeShowEditLink = false;
4165  } else {
4166  $maybeShowEditLink = true; /* Actual presence will depend on post-cache transforms */
4167  }
4168 
4169  # Get all headlines for numbering them and adding funky stuff like [edit]
4170  # links - this is for later, but we need the number of headlines right now
4171  # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
4172  # be trimmed here since whitespace in HTML headings is significant.
4173  $matches = [];
4174  $numMatches = preg_match_all(
4175  '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
4176  $text,
4177  $matches
4178  );
4179 
4180  # if there are fewer than 4 headlines in the article, do not show TOC
4181  # unless it's been explicitly enabled.
4182  $enoughToc = $this->mShowToc &&
4183  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4184 
4185  # Allow user to stipulate that a page should have a "new section"
4186  # link added via __NEWSECTIONLINK__
4187  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4188  $this->mOutput->setNewSection( true );
4189  }
4190 
4191  # Allow user to remove the "new section"
4192  # link via __NONEWSECTIONLINK__
4193  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4194  $this->mOutput->hideNewSection( true );
4195  }
4196 
4197  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4198  # override above conditions and always show TOC above first header
4199  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4200  $this->mShowToc = true;
4201  $enoughToc = true;
4202  }
4203 
4204  # headline counter
4205  $headlineCount = 0;
4206  $numVisible = 0;
4207 
4208  # Ugh .. the TOC should have neat indentation levels which can be
4209  # passed to the skin functions. These are determined here
4210  $toc = '';
4211  $full = '';
4212  $head = [];
4213  $sublevelCount = [];
4214  $levelCount = [];
4215  $level = 0;
4216  $prevlevel = 0;
4217  $toclevel = 0;
4218  $prevtoclevel = 0;
4219  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4220  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4221  $oldType = $this->mOutputType;
4222  $this->setOutputType( self::OT_WIKI );
4223  $frame = $this->getPreprocessor()->newFrame();
4224  $root = $this->preprocessToDom( $origText );
4225  $node = $root->getFirstChild();
4226  $byteOffset = 0;
4227  $tocraw = [];
4228  $refers = [];
4229 
4230  $headlines = $numMatches !== false ? $matches[3] : [];
4231 
4232  $maxTocLevel = $this->siteConfig->get( 'MaxTocLevel' );
4233  foreach ( $headlines as $headline ) {
4234  $isTemplate = false;
4235  $titleText = false;
4236  $sectionIndex = false;
4237  $numbering = '';
4238  $markerMatches = [];
4239  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4240  $serial = $markerMatches[1];
4241  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4242  $isTemplate = ( $titleText != $baseTitleText );
4243  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4244  }
4245 
4246  if ( $toclevel ) {
4247  $prevlevel = $level;
4248  }
4249  $level = $matches[1][$headlineCount];
4250 
4251  if ( $level > $prevlevel ) {
4252  # Increase TOC level
4253  $toclevel++;
4254  $sublevelCount[$toclevel] = 0;
4255  if ( $toclevel < $maxTocLevel ) {
4256  $prevtoclevel = $toclevel;
4257  $toc .= Linker::tocIndent();
4258  $numVisible++;
4259  }
4260  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4261  # Decrease TOC level, find level to jump to
4262 
4263  for ( $i = $toclevel; $i > 0; $i-- ) {
4264  if ( $levelCount[$i] == $level ) {
4265  # Found last matching level
4266  $toclevel = $i;
4267  break;
4268  } elseif ( $levelCount[$i] < $level ) {
4269  # Found first matching level below current level
4270  $toclevel = $i + 1;
4271  break;
4272  }
4273  }
4274  if ( $i == 0 ) {
4275  $toclevel = 1;
4276  }
4277  if ( $toclevel < $maxTocLevel ) {
4278  if ( $prevtoclevel < $maxTocLevel ) {
4279  # Unindent only if the previous toc level was shown :p
4280  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4281  $prevtoclevel = $toclevel;
4282  } else {
4283  $toc .= Linker::tocLineEnd();
4284  }
4285  }
4286  } else {
4287  # No change in level, end TOC line
4288  if ( $toclevel < $maxTocLevel ) {
4289  $toc .= Linker::tocLineEnd();
4290  }
4291  }
4292 
4293  $levelCount[$toclevel] = $level;
4294 
4295  # count number of headlines for each level
4296  $sublevelCount[$toclevel]++;
4297  $dot = 0;
4298  for ( $i = 1; $i <= $toclevel; $i++ ) {
4299  if ( !empty( $sublevelCount[$i] ) ) {
4300  if ( $dot ) {
4301  $numbering .= '.';
4302  }
4303  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4304  $dot = 1;
4305  }
4306  }
4307 
4308  # The safe header is a version of the header text safe to use for links
4309 
4310  # Remove link placeholders by the link text.
4311  # <!--LINK number-->
4312  # turns into
4313  # link text with suffix
4314  # Do this before unstrip since link text can contain strip markers
4315  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4316 
4317  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4318  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4319 
4320  # Remove any <style> or <script> tags (T198618)
4321  $safeHeadline = preg_replace(
4322  '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
4323  '',
4324  $safeHeadline
4325  );
4326 
4327  # Strip out HTML (first regex removes any tag not allowed)
4328  # Allowed tags are:
4329  # * <sup> and <sub> (T10393)
4330  # * <i> (T28375)
4331  # * <b> (r105284)
4332  # * <bdi> (T74884)
4333  # * <span dir="rtl"> and <span dir="ltr"> (T37167)
4334  # * <s> and <strike> (T35715)
4335  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4336  # to allow setting directionality in toc items.
4337  $tocline = preg_replace(
4338  [
4339  '#<(?!/?(span|sup|sub|bdi|i|b|s|strike)(?: [^>]*)?>).*?>#',
4340  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b|s|strike))(?: .*?)?>#'
4341  ],
4342  [ '', '<$1>' ],
4343  $safeHeadline
4344  );
4345 
4346  # Strip '<span></span>', which is the result from the above if
4347  # <span id="foo"></span> is used to produce an additional anchor
4348  # for a section.
4349  $tocline = str_replace( '<span></span>', '', $tocline );
4350 
4351  $tocline = trim( $tocline );
4352 
4353  # For the anchor, strip out HTML-y stuff period
4354  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4355  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4356 
4357  # Save headline for section edit hint before it's escaped
4358  $headlineHint = $safeHeadline;
4359 
4360  # Decode HTML entities
4361  $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
4362 
4363  $safeHeadline = self::normalizeSectionName( $safeHeadline );
4364 
4365  $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
4366  $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
4367  $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
4368  if ( $fallbackHeadline === $safeHeadline ) {
4369  # No reason to have both (in fact, we can't)
4370  $fallbackHeadline = false;
4371  }
4372 
4373  # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
4374  # @todo FIXME: We may be changing them depending on the current locale.
4375  $arrayKey = strtolower( $safeHeadline );
4376  if ( $fallbackHeadline === false ) {
4377  $fallbackArrayKey = false;
4378  } else {
4379  $fallbackArrayKey = strtolower( $fallbackHeadline );
4380  }
4381 
4382  # Create the anchor for linking from the TOC to the section
4383  $anchor = $safeHeadline;
4384  $fallbackAnchor = $fallbackHeadline;
4385  if ( isset( $refers[$arrayKey] ) ) {
4386  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4387  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4388  $anchor .= "_$i";
4389  $linkAnchor .= "_$i";
4390  $refers["${arrayKey}_$i"] = true;
4391  } else {
4392  $refers[$arrayKey] = true;
4393  }
4394  if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
4395  // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
4396  for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
4397  $fallbackAnchor .= "_$i";
4398  $refers["${fallbackArrayKey}_$i"] = true;
4399  } else {
4400  $refers[$fallbackArrayKey] = true;
4401  }
4402 
4403  # Don't number the heading if it is the only one (looks silly)
4404  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4405  # the two are different if the line contains a link
4406  $headline = Html::element(
4407  'span',
4408  [ 'class' => 'mw-headline-number' ],
4409  $numbering
4410  ) . ' ' . $headline;
4411  }
4412 
4413  if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
4414  $toc .= Linker::tocLine( $linkAnchor, $tocline,
4415  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4416  }
4417 
4418  # Add the section to the section tree
4419  # Find the DOM node for this header
4420  $noOffset = ( $isTemplate || $sectionIndex === false );
4421  while ( $node && !$noOffset ) {
4422  if ( $node->getName() === 'h' ) {
4423  $bits = $node->splitHeading();
4424  if ( $bits['i'] == $sectionIndex ) {
4425  break;
4426  }
4427  }
4428  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4429  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4430  $node = $node->getNextSibling();
4431  }
4432  $tocraw[] = [
4433  'toclevel' => $toclevel,
4434  'level' => $level,
4435  'line' => $tocline,
4436  'number' => $numbering,
4437  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4438  'fromtitle' => $titleText,
4439  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4440  'anchor' => $anchor,
4441  ];
4442 
4443  # give headline the correct <h#> tag
4444  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4445  // Output edit section links as markers with styles that can be customized by skins
4446  if ( $isTemplate ) {
4447  # Put a T flag in the section identifier, to indicate to extractSections()
4448  # that sections inside <includeonly> should be counted.
4449  $editsectionPage = $titleText;
4450  $editsectionSection = "T-$sectionIndex";
4451  $editsectionContent = null;
4452  } else {
4453  $editsectionPage = $this->mTitle->getPrefixedText();
4454  $editsectionSection = $sectionIndex;
4455  $editsectionContent = $headlineHint;
4456  }
4457  // We use a bit of pesudo-xml for editsection markers. The
4458  // language converter is run later on. Using a UNIQ style marker
4459  // leads to the converter screwing up the tokens when it
4460  // converts stuff. And trying to insert strip tags fails too. At
4461  // this point all real inputted tags have already been escaped,
4462  // so we don't have to worry about a user trying to input one of
4463  // these markers directly. We use a page and section attribute
4464  // to stop the language converter from converting these
4465  // important bits of data, but put the headline hint inside a
4466  // content block because the language converter is supposed to
4467  // be able to convert that piece of data.
4468  // Gets replaced with html in ParserOutput::getText
4469  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4470  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4471  if ( $editsectionContent !== null ) {
4472  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4473  } else {
4474  $editlink .= '/>';
4475  }
4476  } else {
4477  $editlink = '';
4478  }
4479  $head[$headlineCount] = Linker::makeHeadline( $level,
4480  $matches['attrib'][$headlineCount], $anchor, $headline,
4481  $editlink, $fallbackAnchor );
4482 
4483  $headlineCount++;
4484  }
4485 
4486  $this->setOutputType( $oldType );
4487 
4488  # Never ever show TOC if no headers
4489  if ( $numVisible < 1 ) {
4490  $enoughToc = false;
4491  }
4492 
4493  if ( $enoughToc ) {
4494  if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
4495  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4496  }
4497  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4498  $this->mOutput->setTOCHTML( $toc );
4499  $toc = self::TOC_START . $toc . self::TOC_END;
4500  }
4501 
4502  if ( $isMain ) {
4503  $this->mOutput->setSections( $tocraw );
4504  }
4505 
4506  # split up and insert constructed headlines
4507  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4508  $i = 0;
4509 
4510  // build an array of document sections
4511  $sections = [];
4512  foreach ( $blocks as $block ) {
4513  // $head is zero-based, sections aren't.
4514  if ( empty( $head[$i - 1] ) ) {
4515  $sections[$i] = $block;
4516  } else {
4517  $sections[$i] = $head[$i - 1] . $block;
4518  }
4519 
4530  Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $maybeShowEditLink ] );
4531 
4532  $i++;
4533  }
4534 
4535  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4536  // append the TOC at the beginning
4537  // Top anchor now in skin
4538  $sections[0] .= $toc . "\n";
4539  }
4540 
4541  $full .= implode( '', $sections );
4542 
4543  if ( $this->mForceTocPosition ) {
4544  return str_replace( '<!--MWTOC\'"-->', $toc, $full );
4545  } else {
4546  return $full;
4547  }
4548  }
4549 
4561  public function preSaveTransform( $text, Title $title, User $user,
4562  ParserOptions $options, $clearState = true
4563  ) {
4564  if ( $clearState ) {
4565  $magicScopeVariable = $this->lock();
4566  }
4567  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4568  $this->setUser( $user );
4569 
4570  // Strip U+0000 NULL (T159174)
4571  $text = str_replace( "\000", '', $text );
4572 
4573  // We still normalize line endings for backwards-compatibility
4574  // with other code that just calls PST, but this should already
4575  // be handled in TextContent subclasses
4576  $text = TextContent::normalizeLineEndings( $text );
4577 
4578  if ( $options->getPreSaveTransform() ) {
4579  $text = $this->pstPass2( $text, $user );
4580  }
4581  $text = $this->mStripState->unstripBoth( $text );
4582 
4583  $this->setUser( null ); # Reset
4584 
4585  return $text;
4586  }
4587 
4596  private function pstPass2( $text, $user ) {
4597  # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
4598  # $this->contLang here in order to give everyone the same signature and use the default one
4599  # rather than the one selected in each user's preferences. (see also T14815)
4600  $ts = $this->mOptions->getTimestamp();
4601  $timestamp = MWTimestamp::getLocalInstance( $ts );
4602  $ts = $timestamp->format( 'YmdHis' );
4603  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4604 
4605  $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4606 
4607  # Variable replacement
4608  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4609  $text = $this->replaceVariables( $text );
4610 
4611  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4612  # which may corrupt this parser instance via its wfMessage()->text() call-
4613 
4614  # Signatures
4615  if ( strpos( $text, '~~~' ) !== false ) {
4616  $sigText = $this->getUserSig( $user );
4617  $text = strtr( $text, [
4618  '~~~~~' => $d,
4619  '~~~~' => "$sigText $d",
4620  '~~~' => $sigText
4621  ] );
4622  # The main two signature forms used above are time-sensitive
4623  $this->mOutput->setFlag( 'user-signature' );
4624  }
4625 
4626  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4627  $tc = '[' . Title::legalChars() . ']';
4628  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4629 
4630  // [[ns:page (context)|]]
4631  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4632  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4633  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4634  // [[ns:page (context), context|]] (using either single or double-width comma)
4635  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4636  // [[|page]] (reverse pipe trick: add context from page title)
4637  $p2 = "/\[\[\\|($tc+)]]/";
4638 
4639  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4640  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4641  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4642  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4643 
4644  $t = $this->mTitle->getText();
4645  $m = [];
4646  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4647  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4648  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4649  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4650  } else {
4651  # if there's no context, don't bother duplicating the title
4652  $text = preg_replace( $p2, '[[\\1]]', $text );
4653  }
4654 
4655  return $text;
4656  }
4657 
4672  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4673  $username = $user->getName();
4674 
4675  # If not given, retrieve from the user object.
4676  if ( $nickname === false ) {
4677  $nickname = $user->getOption( 'nickname' );
4678  }
4679 
4680  if ( is_null( $fancySig ) ) {
4681  $fancySig = $user->getBoolOption( 'fancysig' );
4682  }
4683 
4684  $nickname = $nickname == null ? $username : $nickname;
4685 
4686  if ( mb_strlen( $nickname ) > $this->siteConfig->get( 'MaxSigChars' ) ) {
4687  $nickname = $username;
4688  wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4689  } elseif ( $fancySig !== false ) {
4690  # Sig. might contain markup; validate this
4691  if ( $this->validateSig( $nickname ) !== false ) {
4692  # Validated; clean up (if needed) and return it
4693  return $this->cleanSig( $nickname, true );
4694  } else {
4695  # Failed to validate; fall back to the default
4696  $nickname = $username;
4697  wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4698  }
4699  }
4700 
4701  # Make sure nickname doesnt get a sig in a sig
4702  $nickname = self::cleanSigInSig( $nickname );
4703 
4704  # If we're still here, make it a link to the user page
4705  $userText = wfEscapeWikiText( $username );
4706  $nickText = wfEscapeWikiText( $nickname );
4707  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4708 
4709  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4710  ->title( $this->getTitle() )->text();
4711  }
4712 
4719  public function validateSig( $text ) {
4720  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4721  }
4722 
4733  public function cleanSig( $text, $parsing = false ) {
4734  if ( !$parsing ) {
4735  global $wgTitle;
4736  $magicScopeVariable = $this->lock();
4737  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4738  }
4739 
4740  # Option to disable this feature
4741  if ( !$this->mOptions->getCleanSignatures() ) {
4742  return $text;
4743  }
4744 
4745  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4746  # => Move this logic to braceSubstitution()
4747  $substWord = $this->magicWordFactory->get( 'subst' );
4748  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4749  $substText = '{{' . $substWord->getSynonym( 0 );
4750 
4751  $text = preg_replace( $substRegex, $substText, $text );
4752  $text = self::cleanSigInSig( $text );
4753  $dom = $this->preprocessToDom( $text );
4754  $frame = $this->getPreprocessor()->newFrame();
4755  $text = $frame->expand( $dom );
4756 
4757  if ( !$parsing ) {
4758  $text = $this->mStripState->unstripBoth( $text );
4759  }
4760 
4761  return $text;
4762  }
4763 
4770  public static function cleanSigInSig( $text ) {
4771  $text = preg_replace( '/~{3,5}/', '', $text );
4772  return $text;
4773  }
4774 
4784  public function startExternalParse( Title $title = null, ParserOptions $options,
4785  $outputType, $clearState = true
4786  ) {
4787  $this->startParse( $title, $options, $outputType, $clearState );
4788  }
4789 
4796  private function startParse( Title $title = null, ParserOptions $options,
4797  $outputType, $clearState = true
4798  ) {
4799  $this->setTitle( $title );
4800  $this->mOptions = $options;
4801  $this->setOutputType( $outputType );
4802  if ( $clearState ) {
4803  $this->clearState();
4804  }
4805  }
4806 
4815  public function transformMsg( $text, $options, $title = null ) {
4816  static $executing = false;
4817 
4818  # Guard against infinite recursion
4819  if ( $executing ) {
4820  return $text;
4821  }
4822  $executing = true;
4823 
4824  if ( !$title ) {
4825  global $wgTitle;
4826  $title = $wgTitle;
4827  }
4828 
4829  $text = $this->preprocess( $text, $title, $options );
4830 
4831  $executing = false;
4832  return $text;
4833  }
4834 
4859  public function setHook( $tag, callable $callback ) {
4860  $tag = strtolower( $tag );
4861  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4862  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
4863  }
4864  $oldVal = $this->mTagHooks[$tag] ?? null;
4865  $this->mTagHooks[$tag] = $callback;
4866  if ( !in_array( $tag, $this->mStripList ) ) {
4867  $this->mStripList[] = $tag;
4868  }
4869 
4870  return $oldVal;
4871  }
4872 
4890  public function setTransparentTagHook( $tag, callable $callback ) {
4891  $tag = strtolower( $tag );
4892  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
4893  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
4894  }
4895  $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
4896  $this->mTransparentTagHooks[$tag] = $callback;
4897 
4898  return $oldVal;
4899  }
4900 
4904  public function clearTagHooks() {
4905  $this->mTagHooks = [];
4906  $this->mFunctionTagHooks = [];
4907  $this->mStripList = $this->mDefaultStripList;
4908  }
4909 
4953  public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
4954  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
4955  $this->mFunctionHooks[$id] = [ $callback, $flags ];
4956 
4957  # Add to function cache
4958  $mw = $this->magicWordFactory->get( $id );
4959  if ( !$mw ) {
4960  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
4961  }
4962 
4963  $synonyms = $mw->getSynonyms();
4964  $sensitive = intval( $mw->isCaseSensitive() );
4965 
4966  foreach ( $synonyms as $syn ) {
4967  # Case
4968  if ( !$sensitive ) {
4969  $syn = $this->contLang->lc( $syn );
4970  }
4971  # Add leading hash
4972  if ( !( $flags & self::SFH_NO_HASH ) ) {
4973  $syn = '#' . $syn;
4974  }
4975  # Remove trailing colon
4976  if ( substr( $syn, -1, 1 ) === ':' ) {
4977  $syn = substr( $syn, 0, -1 );
4978  }
4979  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
4980  }
4981  return $oldVal;
4982  }
4983 
4989  public function getFunctionHooks() {
4990  $this->firstCallInit();
4991  return array_keys( $this->mFunctionHooks );
4992  }
4993 
5004  public function setFunctionTagHook( $tag, callable $callback, $flags ) {
5005  $tag = strtolower( $tag );
5006  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5007  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5008  }
5009  $old = $this->mFunctionTagHooks[$tag] ?? null;
5010  $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5011 
5012  if ( !in_array( $tag, $this->mStripList ) ) {
5013  $this->mStripList[] = $tag;
5014  }
5015 
5016  return $old;
5017  }
5018 
5026  public function replaceLinkHolders( &$text, $options = 0 ) {
5027  $this->mLinkHolders->replace( $text );
5028  }
5029 
5037  public function replaceLinkHoldersText( $text ) {
5038  return $this->mLinkHolders->replaceText( $text );
5039  }
5040 
5054  public function renderImageGallery( $text, $params ) {
5055  $mode = false;
5056  if ( isset( $params['mode'] ) ) {
5057  $mode = $params['mode'];
5058  }
5059 
5060  try {
5061  $ig = ImageGalleryBase::factory( $mode );
5062  } catch ( Exception $e ) {
5063  // If invalid type set, fallback to default.
5064  $ig = ImageGalleryBase::factory( false );
5065  }
5066 
5067  $ig->setContextTitle( $this->mTitle );
5068  $ig->setShowBytes( false );
5069  $ig->setShowDimensions( false );
5070  $ig->setShowFilename( false );
5071  $ig->setParser( $this );
5072  $ig->setHideBadImages();
5073  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'ul' ) );
5074 
5075  if ( isset( $params['showfilename'] ) ) {
5076  $ig->setShowFilename( true );
5077  } else {
5078  $ig->setShowFilename( false );
5079  }
5080  if ( isset( $params['caption'] ) ) {
5081  // NOTE: We aren't passing a frame here or below. Frame info
5082  // is currently opaque to Parsoid, which acts on OT_PREPROCESS.
5083  // See T107332#4030581
5084  $caption = $this->recursiveTagParse( $params['caption'] );
5085  $ig->setCaptionHtml( $caption );
5086  }
5087  if ( isset( $params['perrow'] ) ) {
5088  $ig->setPerRow( $params['perrow'] );
5089  }
5090  if ( isset( $params['widths'] ) ) {
5091  $ig->setWidths( $params['widths'] );
5092  }
5093  if ( isset( $params['heights'] ) ) {
5094  $ig->setHeights( $params['heights'] );
5095  }
5096  $ig->setAdditionalOptions( $params );
5097 
5098  // Avoid PHP 7.1 warning from passing $this by reference
5099  $parser = $this;
5100  Hooks::run( 'BeforeParserrenderImageGallery', [ &$parser, &$ig ] );
5101 
5102  $lines = StringUtils::explode( "\n", $text );
5103  foreach ( $lines as $line ) {
5104  # match lines like these:
5105  # Image:someimage.jpg|This is some image
5106  $matches = [];
5107  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5108  # Skip empty lines
5109  if ( count( $matches ) == 0 ) {
5110  continue;
5111  }
5112 
5113  if ( strpos( $matches[0], '%' ) !== false ) {
5114  $matches[1] = rawurldecode( $matches[1] );
5115  }
5117  if ( is_null( $title ) ) {
5118  # Bogus title. Ignore these so we don't bomb out later.
5119  continue;
5120  }
5121 
5122  # We need to get what handler the file uses, to figure out parameters.
5123  # Note, a hook can overide the file name, and chose an entirely different
5124  # file (which potentially could be of a different type and have different handler).
5125  $options = [];
5126  $descQuery = false;
5127  Hooks::run( 'BeforeParserFetchFileAndTitle',
5128  [ $this, $title, &$options, &$descQuery ] );
5129  # Don't register it now, as TraditionalImageGallery does that later.
5130  $file = $this->fetchFileNoRegister( $title, $options );
5131  $handler = $file ? $file->getHandler() : false;
5132 
5133  $paramMap = [
5134  'img_alt' => 'gallery-internal-alt',
5135  'img_link' => 'gallery-internal-link',
5136  ];
5137  if ( $handler ) {
5138  $paramMap += $handler->getParamMap();
5139  // We don't want people to specify per-image widths.
5140  // Additionally the width parameter would need special casing anyhow.
5141  unset( $paramMap['img_width'] );
5142  }
5143 
5144  $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5145 
5146  $label = '';
5147  $alt = '';
5148  $link = '';
5149  $handlerOptions = [];
5150  if ( isset( $matches[3] ) ) {
5151  // look for an |alt= definition while trying not to break existing
5152  // captions with multiple pipes (|) in it, until a more sensible grammar
5153  // is defined for images in galleries
5154 
5155  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5156  // splitting on '|' is a bit odd, and different from makeImage.
5157  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5158  // Protect LanguageConverter markup
5159  $parameterMatches = StringUtils::delimiterExplode(
5160  '-{', '}-', '|', $matches[3], true /* nested */
5161  );
5162 
5163  foreach ( $parameterMatches as $parameterMatch ) {
5164  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5165  if ( $magicName ) {
5166  $paramName = $paramMap[$magicName];
5167 
5168  switch ( $paramName ) {
5169  case 'gallery-internal-alt':
5170  $alt = $this->stripAltText( $match, false );
5171  break;
5172  case 'gallery-internal-link':
5173  $linkValue = $this->stripAltText( $match, false );
5174  if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
5175  // Result of LanguageConverter::markNoConversion
5176  // invoked on an external link.
5177  $linkValue = substr( $linkValue, 4, -2 );
5178  }
5179  list( $type, $target ) = $this->parseLinkParameter( $linkValue );
5180  if ( $type === 'link-url' ) {
5181  $link = $target;
5182  $this->mOutput->addExternalLink( $target );
5183  } elseif ( $type === 'link-title' ) {
5184  $link = $target->getLinkURL();
5185  $this->mOutput->addLink( $target );
5186  }
5187  break;
5188  default:
5189  // Must be a handler specific parameter.
5190  if ( $handler->validateParam( $paramName, $match ) ) {
5191  $handlerOptions[$paramName] = $match;
5192  } else {
5193  // Guess not, consider it as caption.
5194  wfDebug( "$parameterMatch failed parameter validation\n" );
5195  $label = $parameterMatch;
5196  }
5197  }
5198 
5199  } else {
5200  // Last pipe wins.
5201  $label = $parameterMatch;
5202  }
5203  }
5204  }
5205 
5206  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5207  }
5208  $html = $ig->toHTML();
5209  Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5210  return $html;
5211  }
5212 
5217  public function getImageParams( $handler ) {
5218  if ( $handler ) {
5219  $handlerClass = get_class( $handler );
5220  } else {
5221  $handlerClass = '';
5222  }
5223  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5224  # Initialise static lists
5225  static $internalParamNames = [
5226  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5227  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5228  'bottom', 'text-bottom' ],
5229  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5230  'upright', 'border', 'link', 'alt', 'class' ],
5231  ];
5232  static $internalParamMap;
5233  if ( !$internalParamMap ) {
5234  $internalParamMap = [];
5235  foreach ( $internalParamNames as $type => $names ) {
5236  foreach ( $names as $name ) {
5237  // For grep: img_left, img_right, img_center, img_none,
5238  // img_baseline, img_sub, img_super, img_top, img_text_top, img_middle,
5239  // img_bottom, img_text_bottom,
5240  // img_thumbnail, img_manualthumb, img_framed, img_frameless, img_upright,
5241  // img_border, img_link, img_alt, img_class
5242  $magicName = str_replace( '-', '_', "img_$name" );
5243  $internalParamMap[$magicName] = [ $type, $name ];
5244  }
5245  }
5246  }
5247 
5248  # Add handler params
5249  $paramMap = $internalParamMap;
5250  if ( $handler ) {
5251  $handlerParamMap = $handler->getParamMap();
5252  foreach ( $handlerParamMap as $magic => $paramName ) {
5253  $paramMap[$magic] = [ 'handler', $paramName ];
5254  }
5255  }
5256  $this->mImageParams[$handlerClass] = $paramMap;
5257  $this->mImageParamsMagicArray[$handlerClass] =
5258  $this->magicWordFactory->newArray( array_keys( $paramMap ) );
5259  }
5260  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5261  }
5262 
5271  public function makeImage( $title, $options, $holders = false ) {
5272  # Check if the options text is of the form "options|alt text"
5273  # Options are:
5274  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5275  # * left no resizing, just left align. label is used for alt= only
5276  # * right same, but right aligned
5277  # * none same, but not aligned
5278  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5279  # * center center the image
5280  # * frame Keep original image size, no magnify-button.
5281  # * framed Same as "frame"
5282  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5283  # * upright reduce width for upright images, rounded to full __0 px
5284  # * border draw a 1px border around the image
5285  # * alt Text for HTML alt attribute (defaults to empty)
5286  # * class Set a class for img node
5287  # * link Set the target of the image link. Can be external, interwiki, or local
5288  # vertical-align values (no % or length right now):
5289  # * baseline
5290  # * sub
5291  # * super
5292  # * top
5293  # * text-top
5294  # * middle
5295  # * bottom
5296  # * text-bottom
5297 
5298  # Protect LanguageConverter markup when splitting into parts
5300  '-{', '}-', '|', $options, true /* allow nesting */
5301  );
5302 
5303  # Give extensions a chance to select the file revision for us
5304  $options = [];
5305  $descQuery = false;
5306  Hooks::run( 'BeforeParserFetchFileAndTitle',
5307  [ $this, $title, &$options, &$descQuery ] );
5308  # Fetch and register the file (file title may be different via hooks)
5309  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5310 
5311  # Get parameter map
5312  $handler = $file ? $file->getHandler() : false;
5313 
5314  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5315 
5316  if ( !$file ) {
5317  $this->addTrackingCategory( 'broken-file-category' );
5318  }
5319 
5320  # Process the input parameters
5321  $caption = '';
5322  $params = [ 'frame' => [], 'handler' => [],
5323  'horizAlign' => [], 'vertAlign' => [] ];
5324  $seenformat = false;
5325  foreach ( $parts as $part ) {
5326  $part = trim( $part );
5327  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5328  $validated = false;
5329  if ( isset( $paramMap[$magicName] ) ) {
5330  list( $type, $paramName ) = $paramMap[$magicName];
5331 
5332  # Special case; width and height come in one variable together
5333  if ( $type === 'handler' && $paramName === 'width' ) {
5334  $parsedWidthParam = self::parseWidthParam( $value );
5335  if ( isset( $parsedWidthParam['width'] ) ) {
5336  $width = $parsedWidthParam['width'];
5337  if ( $handler->validateParam( 'width', $width ) ) {
5338  $params[$type]['width'] = $width;
5339  $validated = true;
5340  }
5341  }
5342  if ( isset( $parsedWidthParam['height'] ) ) {
5343  $height = $parsedWidthParam['height'];
5344  if ( $handler->validateParam( 'height', $height ) ) {
5345  $params[$type]['height'] = $height;
5346  $validated = true;
5347  }
5348  }
5349  # else no validation -- T15436
5350  } else {
5351  if ( $type === 'handler' ) {
5352  # Validate handler parameter
5353  $validated = $handler->validateParam( $paramName, $value );
5354  } else {
5355  # Validate internal parameters
5356  switch ( $paramName ) {
5357  case 'manualthumb':
5358  case 'alt':
5359  case 'class':
5360  # @todo FIXME: Possibly check validity here for
5361  # manualthumb? downstream behavior seems odd with
5362  # missing manual thumbs.
5363  $validated = true;
5364  $value = $this->stripAltText( $value, $holders );
5365  break;
5366  case 'link':
5367  list( $paramName, $value ) =
5368  $this->parseLinkParameter(
5369  $this->stripAltText( $value, $holders )
5370  );
5371  if ( $paramName ) {
5372  $validated = true;
5373  if ( $paramName === 'no-link' ) {
5374  $value = true;
5375  }
5376  if ( ( $paramName === 'link-url' ) && $this->mOptions->getExternalLinkTarget() ) {
5377  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5378  }
5379  }
5380  break;
5381  case 'frameless':
5382  case 'framed':
5383  case 'thumbnail':
5384  // use first appearing option, discard others.
5385  $validated = !$seenformat;
5386  $seenformat = true;
5387  break;
5388  default:
5389  # Most other things appear to be empty or numeric...
5390  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5391  }
5392  }
5393 
5394  if ( $validated ) {
5395  $params[$type][$paramName] = $value;
5396  }
5397  }
5398  }
5399  if ( !$validated ) {
5400  $caption = $part;
5401  }
5402  }
5403 
5404  # Process alignment parameters
5405  if ( $params['horizAlign'] ) {
5406  $params['frame']['align'] = key( $params['horizAlign'] );
5407  }
5408  if ( $params['vertAlign'] ) {
5409  $params['frame']['valign'] = key( $params['vertAlign'] );
5410  }
5411 
5412  $params['frame']['caption'] = $caption;
5413 
5414  # Will the image be presented in a frame, with the caption below?
5415  $imageIsFramed = isset( $params['frame']['frame'] )
5416  || isset( $params['frame']['framed'] )
5417  || isset( $params['frame']['thumbnail'] )
5418  || isset( $params['frame']['manualthumb'] );
5419 
5420  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5421  # came to also set the caption, ordinary text after the image -- which
5422  # makes no sense, because that just repeats the text multiple times in
5423  # screen readers. It *also* came to set the title attribute.
5424  # Now that we have an alt attribute, we should not set the alt text to
5425  # equal the caption: that's worse than useless, it just repeats the
5426  # text. This is the framed/thumbnail case. If there's no caption, we
5427  # use the unnamed parameter for alt text as well, just for the time be-
5428  # ing, if the unnamed param is set and the alt param is not.
5429  # For the future, we need to figure out if we want to tweak this more,
5430  # e.g., introducing a title= parameter for the title; ignoring the un-
5431  # named parameter entirely for images without a caption; adding an ex-
5432  # plicit caption= parameter and preserving the old magic unnamed para-
5433  # meter for BC; ...
5434  if ( $imageIsFramed ) { # Framed image
5435  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5436  # No caption or alt text, add the filename as the alt text so
5437  # that screen readers at least get some description of the image
5438  $params['frame']['alt'] = $title->getText();
5439  }
5440  # Do not set $params['frame']['title'] because tooltips don't make sense
5441  # for framed images
5442  } else { # Inline image
5443  if ( !isset( $params['frame']['alt'] ) ) {
5444  # No alt text, use the "caption" for the alt text
5445  if ( $caption !== '' ) {
5446  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5447  } else {
5448  # No caption, fall back to using the filename for the
5449  # alt text
5450  $params['frame']['alt'] = $title->getText();
5451  }
5452  }
5453  # Use the "caption" for the tooltip text
5454  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5455  }
5456  $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
5457 
5458  Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5459 
5460  # Linker does the rest
5461  $time = $options['time'] ?? false;
5462  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5463  $time, $descQuery, $this->mOptions->getThumbSize() );
5464 
5465  # Give the handler a chance to modify the parser object
5466  if ( $handler ) {
5467  $handler->parserTransformHook( $this, $file );
5468  }
5469 
5470  return $ret;
5471  }
5472 
5491  public function parseLinkParameter( $value ) {
5492  $chars = self::EXT_LINK_URL_CLASS;
5493  $addr = self::EXT_LINK_ADDR;
5494  $prots = $this->mUrlProtocols;
5495  $type = null;
5496  $target = false;
5497  if ( $value === '' ) {
5498  $type = 'no-link';
5499  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5500  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5501  $this->mOutput->addExternalLink( $value );
5502  $type = 'link-url';
5503  $target = $value;
5504  }
5505  } else {
5506  $linkTitle = Title::newFromText( $value );
5507  if ( $linkTitle ) {
5508  $this->mOutput->addLink( $linkTitle );
5509  $type = 'link-title';
5510  $target = $linkTitle;
5511  }
5512  }
5513  return [ $type, $target ];
5514  }
5515 
5521  protected function stripAltText( $caption, $holders ) {
5522  # Strip bad stuff out of the title (tooltip). We can't just use
5523  # replaceLinkHoldersText() here, because if this function is called
5524  # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5525  if ( $holders ) {
5526  $tooltip = $holders->replaceText( $caption );
5527  } else {
5528  $tooltip = $this->replaceLinkHoldersText( $caption );
5529  }
5530 
5531  # make sure there are no placeholders in thumbnail attributes
5532  # that are later expanded to html- so expand them now and
5533  # remove the tags
5534  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5535  # Compatibility hack! In HTML certain entity references not terminated
5536  # by a semicolon are decoded (but not if we're in an attribute; that's
5537  # how link URLs get away without properly escaping & in queries).
5538  # But wikitext has always required semicolon-termination of entities,
5539  # so encode & where needed to avoid decode of semicolon-less entities.
5540  # See T209236 and
5541  # https://www.w3.org/TR/html5/syntax.html#named-character-references
5542  # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
5543  $tooltip = preg_replace( "/
5544  & # 1. entity prefix
5545  (?= # 2. followed by:
5546  (?: # a. one of the legacy semicolon-less named entities
5547  A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
5548  C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
5549  GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
5550  O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
5551  U(?:acute|circ|grave|uml)|Yacute|
5552  a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
5553  c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
5554  divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
5555  frac(?:1(?:2|4)|34)|
5556  gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
5557  i(?:acute|circ|excl|grave|quest|uml)|laquo|
5558  lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
5559  m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
5560  not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
5561  o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
5562  p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
5563  s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
5564  u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
5565  )
5566  (?:[^;]|$)) # b. and not followed by a semicolon
5567  # S = study, for efficiency
5568  /Sx", '&amp;', $tooltip );
5569  $tooltip = Sanitizer::stripAllTags( $tooltip );
5570 
5571  return $tooltip;
5572  }
5573 
5579  public function disableCache() {
5580  wfDebug( "Parser output marked as uncacheable.\n" );
5581  if ( !$this->mOutput ) {
5582  throw new MWException( __METHOD__ .
5583  " can only be called when actually parsing something" );
5584  }
5585  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5586  }
5587 
5596  public function attributeStripCallback( &$text, $frame = false ) {
5597  $text = $this->replaceVariables( $text, $frame );
5598  $text = $this->mStripState->unstripBoth( $text );
5599  return $text;
5600  }
5601 
5607  public function getTags() {
5608  $this->firstCallInit();
5609  return array_merge(
5610  array_keys( $this->mTransparentTagHooks ),
5611  array_keys( $this->mTagHooks ),
5612  array_keys( $this->mFunctionTagHooks )
5613  );
5614  }
5615 
5620  public function getFunctionSynonyms() {
5621  $this->firstCallInit();
5622  return $this->mFunctionSynonyms;
5623  }
5624 
5629  public function getUrlProtocols() {
5630  return $this->mUrlProtocols;
5631  }
5632 
5643  public function replaceTransparentTags( $text ) {
5644  $matches = [];
5645  $elements = array_keys( $this->mTransparentTagHooks );
5646  $text = self::extractTagsAndParams( $elements, $text, $matches );
5647  $replacements = [];
5648 
5649  foreach ( $matches as $marker => $data ) {
5650  list( $element, $content, $params, $tag ) = $data;
5651  $tagName = strtolower( $element );
5652  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5653  $output = call_user_func_array(
5654  $this->mTransparentTagHooks[$tagName],
5655  [ $content, $params, $this ]
5656  );
5657  } else {
5658  $output = $tag;
5659  }
5660  $replacements[$marker] = $output;
5661  }
5662  return strtr( $text, $replacements );
5663  }
5664 
5694  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5695  global $wgTitle; # not generally used but removes an ugly failure mode
5696 
5697  $magicScopeVariable = $this->lock();
5698  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5699  $outText = '';
5700  $frame = $this->getPreprocessor()->newFrame();
5701 
5702  # Process section extraction flags
5703  $flags = 0;
5704  $sectionParts = explode( '-', $sectionId );
5705  $sectionIndex = array_pop( $sectionParts );
5706  foreach ( $sectionParts as $part ) {
5707  if ( $part === 'T' ) {
5708  $flags |= self::PTD_FOR_INCLUSION;
5709  }
5710  }
5711 
5712  # Check for empty input
5713  if ( strval( $text ) === '' ) {
5714  # Only sections 0 and T-0 exist in an empty document
5715  if ( $sectionIndex == 0 ) {
5716  if ( $mode === 'get' ) {
5717  return '';
5718  }
5719 
5720  return $newText;
5721  } else {
5722  if ( $mode === 'get' ) {
5723  return $newText;
5724  }
5725 
5726  return $text;
5727  }
5728  }
5729 
5730  # Preprocess the text
5731  $root = $this->preprocessToDom( $text, $flags );
5732 
5733  # <h> nodes indicate section breaks
5734  # They can only occur at the top level, so we can find them by iterating the root's children
5735  $node = $root->getFirstChild();
5736 
5737  # Find the target section
5738  if ( $sectionIndex == 0 ) {
5739  # Section zero doesn't nest, level=big
5740  $targetLevel = 1000;
5741  } else {
5742  while ( $node ) {
5743  if ( $node->getName() === 'h' ) {
5744  $bits = $node->splitHeading();
5745  if ( $bits['i'] == $sectionIndex ) {
5746  $targetLevel = $bits['level'];
5747  break;
5748  }
5749  }
5750  if ( $mode === 'replace' ) {
5751  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5752  }
5753  $node = $node->getNextSibling();
5754  }
5755  }
5756 
5757  if ( !$node ) {
5758  # Not found
5759  if ( $mode === 'get' ) {
5760  return $newText;
5761  } else {
5762  return $text;
5763  }
5764  }
5765 
5766  # Find the end of the section, including nested sections
5767  do {
5768  if ( $node->getName() === 'h' ) {
5769  $bits = $node->splitHeading();
5770  $curLevel = $bits['level'];
5771  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5772  break;
5773  }
5774  }
5775  if ( $mode === 'get' ) {
5776  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5777  }
5778  $node = $node->getNextSibling();
5779  } while ( $node );
5780 
5781  # Write out the remainder (in replace mode only)
5782  if ( $mode === 'replace' ) {
5783  # Output the replacement text
5784  # Add two newlines on -- trailing whitespace in $newText is conventionally
5785  # stripped by the editor, so we need both newlines to restore the paragraph gap
5786  # Only add trailing whitespace if there is newText
5787  if ( $newText != "" ) {
5788  $outText .= $newText . "\n\n";
5789  }
5790 
5791  while ( $node ) {
5792  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5793  $node = $node->getNextSibling();
5794  }
5795  }
5796 
5797  if ( is_string( $outText ) ) {
5798  # Re-insert stripped tags
5799  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5800  }
5801 
5802  return $outText;
5803  }
5804 
5819  public function getSection( $text, $sectionId, $defaultText = '' ) {
5820  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5821  }
5822 
5835  public function replaceSection( $oldText, $sectionId, $newText ) {
5836  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5837  }
5838 
5844  public function getRevisionId() {
5845  return $this->mRevisionId;
5846  }
5847 
5854  public function getRevisionObject() {
5855  if ( !is_null( $this->mRevisionObject ) ) {
5856  return $this->mRevisionObject;
5857  }
5858 
5859  // NOTE: try to get the RevisionObject even if mRevisionId is null.
5860  // This is useful when parsing revision that has not yet been saved.
5861  // However, if we get back a saved revision even though we are in
5862  // preview mode, we'll have to ignore it, see below.
5863  // NOTE: This callback may be used to inject an OLD revision that was
5864  // already loaded, so "current" is a bit of a misnomer. We can't just
5865  // skip it if mRevisionId is set.
5866  $rev = call_user_func(
5867  $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5868  );
5869 
5870  if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
5871  // We are in preview mode (mRevisionId is null), and the current revision callback
5872  // returned an existing revision. Ignore it and return null, it's probably the page's
5873  // current revision, which is not what we want here. Note that we do want to call the
5874  // callback to allow the unsaved revision to be injected here, e.g. for
5875  // self-transclusion previews.
5876  return null;
5877  }
5878 
5879  // If the parse is for a new revision, then the callback should have
5880  // already been set to force the object and should match mRevisionId.
5881  // If not, try to fetch by mRevisionId for sanity.
5882  if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
5883  $rev = Revision::newFromId( $this->mRevisionId );
5884  }
5885 
5886  $this->mRevisionObject = $rev;
5887 
5888  return $this->mRevisionObject;
5889  }
5890 
5896  public function getRevisionTimestamp() {
5897  if ( is_null( $this->mRevisionTimestamp ) ) {
5898  $revObject = $this->getRevisionObject();
5899  $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
5900 
5901  # The cryptic '' timezone parameter tells to use the site-default
5902  # timezone offset instead of the user settings.
5903  # Since this value will be saved into the parser cache, served
5904  # to other users, and potentially even used inside links and such,
5905  # it needs to be consistent for all visitors.
5906  $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
5907  }
5908  return $this->mRevisionTimestamp;
5909  }
5910 
5916  public function getRevisionUser() {
5917  if ( is_null( $this->mRevisionUser ) ) {
5918  $revObject = $this->getRevisionObject();
5919 
5920  # if this template is subst: the revision id will be blank,
5921  # so just use the current user's name
5922  if ( $revObject ) {
5923  $this->mRevisionUser = $revObject->getUserText();
5924  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
5925  $this->mRevisionUser = $this->getUser()->getName();
5926  }
5927  }
5928  return $this->mRevisionUser;
5929  }
5930 
5936  public function getRevisionSize() {
5937  if ( is_null( $this->mRevisionSize ) ) {
5938  $revObject = $this->getRevisionObject();
5939 
5940  # if this variable is subst: the revision id will be blank,
5941  # so just use the parser input size, because the own substituation
5942  # will change the size.
5943  if ( $revObject ) {
5944  $this->mRevisionSize = $revObject->getSize();
5945  } else {
5946  $this->mRevisionSize = $this->mInputSize;
5947  }
5948  }
5949  return $this->mRevisionSize;
5950  }
5951 
5957  public function setDefaultSort( $sort ) {
5958  $this->mDefaultSort = $sort;
5959  $this->mOutput->setProperty( 'defaultsort', $sort );
5960  }
5961 
5972  public function getDefaultSort() {
5973  if ( $this->mDefaultSort !== false ) {
5974  return $this->mDefaultSort;
5975  } else {
5976  return '';
5977  }
5978  }
5979 
5986  public function getCustomDefaultSort() {
5987  return $this->mDefaultSort;
5988  }
5989 
5990  private static function getSectionNameFromStrippedText( $text ) {
5991  $text = Sanitizer::normalizeSectionNameWhitespace( $text );
5992  $text = Sanitizer::decodeCharReferences( $text );
5993  $text = self::normalizeSectionName( $text );
5994  return $text;
5995  }
5996 
5997  private static function makeAnchor( $sectionName ) {
5998  return '#' . Sanitizer::escapeIdForLink( $sectionName );
5999  }
6000 
6001  private function makeLegacyAnchor( $sectionName ) {
6002  $fragmentMode = $this->siteConfig->get( 'FragmentMode' );
6003  if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
6004  // ForAttribute() and ForLink() are the same for legacy encoding
6005  $id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
6006  } else {
6007  $id = Sanitizer::escapeIdForLink( $sectionName );
6008  }
6009 
6010  return "#$id";
6011  }
6012 
6021  public function guessSectionNameFromWikiText( $text ) {
6022  # Strip out wikitext links(they break the anchor)
6023  $text = $this->stripSectionName( $text );
6024  $sectionName = self::getSectionNameFromStrippedText( $text );
6025  return self::makeAnchor( $sectionName );
6026  }
6027 
6037  public function guessLegacySectionNameFromWikiText( $text ) {
6038  # Strip out wikitext links(they break the anchor)
6039  $text = $this->stripSectionName( $text );
6040  $sectionName = self::getSectionNameFromStrippedText( $text );
6041  return $this->makeLegacyAnchor( $sectionName );
6042  }
6043 
6049  public static function guessSectionNameFromStrippedText( $text ) {
6050  $sectionName = self::getSectionNameFromStrippedText( $text );
6051  return self::makeAnchor( $sectionName );
6052  }
6053 
6060  private static function normalizeSectionName( $text ) {
6061  # T90902: ensure the same normalization is applied for IDs as to links
6062  $titleParser = MediaWikiServices::getInstance()->getTitleParser();
6063  try {
6064 
6065  $parts = $titleParser->splitTitleString( "#$text" );
6066  } catch ( MalformedTitleException $ex ) {
6067  return $text;
6068  }
6069  return $parts['fragment'];
6070  }
6071 
6086  public function stripSectionName( $text ) {
6087  # Strip internal link markup
6088  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6089  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6090 
6091  # Strip external link markup
6092  # @todo FIXME: Not tolerant to blank link text
6093  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6094  # on how many empty links there are on the page - need to figure that out.
6095  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6096 
6097  # Parse wikitext quotes (italics & bold)
6098  $text = $this->doQuotes( $text );
6099 
6100  # Strip HTML tags
6101  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6102  return $text;
6103  }
6104 
6115  public function testSrvus( $text, Title $title, ParserOptions $options,
6116  $outputType = self::OT_HTML
6117  ) {
6118  $magicScopeVariable = $this->lock();
6119  $this->startParse( $title, $options, $outputType, true );
6120 
6121  $text = $this->replaceVariables( $text );
6122  $text = $this->mStripState->unstripBoth( $text );
6123  $text = Sanitizer::removeHTMLtags( $text );
6124  return $text;
6125  }
6126 
6133  public function testPst( $text, Title $title, ParserOptions $options ) {
6134  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6135  }
6136 
6143  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6144  return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6145  }
6146 
6163  public function markerSkipCallback( $s, $callback ) {
6164  $i = 0;
6165  $out = '';
6166  while ( $i < strlen( $s ) ) {
6167  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6168  if ( $markerStart === false ) {
6169  $out .= call_user_func( $callback, substr( $s, $i ) );
6170  break;
6171  } else {
6172  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6173  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6174  if ( $markerEnd === false ) {
6175  $out .= substr( $s, $markerStart );
6176  break;
6177  } else {
6178  $markerEnd += strlen( self::MARKER_SUFFIX );
6179  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6180  $i = $markerEnd;
6181  }
6182  }
6183  }
6184  return $out;
6185  }
6186 
6193  public function killMarkers( $text ) {
6194  return $this->mStripState->killMarkers( $text );
6195  }
6196 
6214  public function serializeHalfParsedText( $text ) {
6215  wfDeprecated( __METHOD__, '1.31' );
6216  $data = [
6217  'text' => $text,
6218  'version' => self::HALF_PARSED_VERSION,
6219  'stripState' => $this->mStripState->getSubState( $text ),
6220  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6221  ];
6222  return $data;
6223  }
6224 
6241  public function unserializeHalfParsedText( $data ) {
6242  wfDeprecated( __METHOD__, '1.31' );
6243  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6244  throw new MWException( __METHOD__ . ': invalid version' );
6245  }
6246 
6247  # First, extract the strip state.
6248  $texts = [ $data['text'] ];
6249  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6250 
6251  # Now renumber links
6252  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6253 
6254  # Should be good to go.
6255  return $texts[0];
6256  }
6257 
6268  public function isValidHalfParsedText( $data ) {
6269  wfDeprecated( __METHOD__, '1.31' );
6270  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6271  }
6272 
6282  public static function parseWidthParam( $value, $parseHeight = true ) {
6283  $parsedWidthParam = [];
6284  if ( $value === '' ) {
6285  return $parsedWidthParam;
6286  }
6287  $m = [];
6288  # (T15500) In both cases (width/height and width only),
6289  # permit trailing "px" for backward compatibility.
6290  if ( $parseHeight && preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6291  $width = intval( $m[1] );
6292  $height = intval( $m[2] );
6293  $parsedWidthParam['width'] = $width;
6294  $parsedWidthParam['height'] = $height;
6295  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6296  $width = intval( $value );
6297  $parsedWidthParam['width'] = $width;
6298  }
6299  return $parsedWidthParam;
6300  }
6301 
6311  protected function lock() {
6312  if ( $this->mInParse ) {
6313  throw new MWException( "Parser state cleared while parsing. "
6314  . "Did you call Parser::parse recursively? Lock is held by: " . $this->mInParse );
6315  }
6316 
6317  // Save the backtrace when locking, so that if some code tries locking again,
6318  // we can print the lock owner's backtrace for easier debugging
6319  $e = new Exception;
6320  $this->mInParse = $e->getTraceAsString();
6321 
6322  $recursiveCheck = new ScopedCallback( function () {
6323  $this->mInParse = false;
6324  } );
6325 
6326  return $recursiveCheck;
6327  }
6328 
6339  public static function stripOuterParagraph( $html ) {
6340  $m = [];
6341  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) && strpos( $m[1], '</p>' ) === false ) {
6342  $html = $m[1];
6343  }
6344 
6345  return $html;
6346  }
6347 
6358  public function getFreshParser() {
6359  if ( $this->mInParse ) {
6360  return $this->factory->create();
6361  } else {
6362  return $this;
6363  }
6364  }
6365 
6372  public function enableOOUI() {
6373  OutputPage::setupOOUI();
6374  $this->mOutput->setEnableOOUI( true );
6375  }
6376 }
OT_MSG
const OT_MSG
Definition: Defines.php:187
SiteStats\articles
static articles()
Definition: SiteStats.php:103
$status
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:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $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). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1266
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:42
save
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
MagicWordArray
Class for handling an array of magic words.
Definition: MagicWordArray.php:32
$user
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1476
broken
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:1985
if
if($IP===false)
Definition: cleanupArchiveUserText.php:4
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
Revision\newKnownCurrent
static newKnownCurrent(IDatabase $db, $pageIdOrTitle, $revId=0)
Load a revision based on a known page ID and current revision ID from the DB.
Definition: Revision.php:1327
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:306
PPFrame\STRIP_COMMENTS
const STRIP_COMMENTS
Definition: Preprocessor.php:169
MWNamespace\isNonincludable
static isNonincludable( $index)
It is not possible to use pages from this namespace as template?
Definition: MWNamespace.php:458
HtmlArmor
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:28
RepoGroup\singleton
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:61
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
ParserOutput
Definition: ParserOutput.php:25
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:118
SiteStats\users
static users()
Definition: SiteStats.php:121
MagicWordFactory
A factory that stores information about MagicWords, and creates them on demand with caching.
Definition: MagicWordFactory.php:34
$context
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
SiteStats\activeUsers
static activeUsers()
Definition: SiteStats.php:130
Linker\makeSelfLinkObj
static makeSelfLinkObj( $nt, $html='', $query='', $trail='', $prefix='')
Make appropriate markup for a link to the current article.
Definition: Linker.php:168
PPFrame\NO_ARGS
const NO_ARGS
Definition: Preprocessor.php:167
wfSetVar
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
Definition: GlobalFunctions.php:1633
captcha-old.count
count
Definition: captcha-old.py:249
Linker\tocIndent
static tocIndent()
Add another level to the Table of Contents.
Definition: Linker.php:1550
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:41
$result
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. '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 '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 since 1.28! 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:1983
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1912
SiteStats\pages
static pages()
Definition: SiteStats.php:112
$wgNoFollowDomainExceptions
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
Definition: DefaultSettings.php:4290
$out
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:780
wfUrlencode
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
Definition: GlobalFunctions.php:333
SiteStats\numberingroup
static numberingroup( $group)
Find the number of users in a given user group.
Definition: SiteStats.php:150
$req
this hook is for auditing only $req
Definition: hooks.txt:979
it
=Architecture==Two class hierarchies are used to provide the functionality associated with the different content models:*Content interface(and AbstractContent base class) define functionality that acts on the concrete content of a page, and *ContentHandler base class provides functionality specific to a content model, but not acting on concrete content. The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content. These Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text. All manipulation and analysis of page content must be done via the appropriate methods of the Content object. For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers. The ContentHandler object for a given content model can be obtained using ContentHandler::getForModelID($id). Also Title, WikiPage and Revision now have getContentHandler() methods for convenience. ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting on the content of some page. ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to create a Content object of the appropriate type. However, it is recommended to instead use WikiPage::getContent() resp. Revision::getContent() to get a page 's content as a Content object. These two methods should be the ONLY way in which page content is accessed. Another important function of ContentHandler objects is to define custom action handlers for a content model, see ContentHandler::getActionOverrides(). This is similar to what WikiPage::getActionOverrides() was already doing.==Serialization==With the ContentHandler facility, page content no longer has to be text based. Objects implementing the Content interface are used to represent and handle the content internally. For storage and data exchange, each content model supports at least one serialization format via ContentHandler::serializeContent($content). The list of supported formats for a given content model can be accessed using ContentHandler::getSupportedFormats(). Content serialization formats are identified using MIME type like strings. The following formats are built in:*text/x-wiki - wikitext *text/javascript - for js pages *text/css - for css pages *text/plain - for future use, e.g. with plain text messages. *text/html - for future use, e.g. with plain html messages. *application/vnd.php.serialized - for future use with the api and for extensions *application/json - for future use with the api, and for use by extensions *application/xml - for future use with the api, and for use by extensions In PHP, use the corresponding CONTENT_FORMAT_XXX constant. Note that when using the API to access page content, especially action=edit, action=parse and action=query &prop=revisions, the model and format of the content should always be handled explicitly. Without that information, interpretation of the provided content is not reliable. The same applies to XML dumps generated via maintenance/dumpBackup.php or Special:Export. Also note that the API will provide encapsulated, serialized content - so if the API was called with format=json, and contentformat is also json(or rather, application/json), the page content is represented as a string containing an escaped json structure. Extensions that use JSON to serialize some types of page content may provide specialized API modules that allow access to that content in a more natural form.==Compatibility==The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least for pages that contain wikitext or other text based content. However, a number of functions and hooks have been deprecated in favor of new versions that are aware of the page 's content model, and will now generate warnings when used. Most importantly, the following functions have been deprecated:*Revisions::getText() is deprecated in favor Revisions::getContent() *WikiPage::getText() is deprecated in favor WikiPage::getContent() Also, the old Article::getContent()(which returns text) is superceded by Article::getContentObject(). However, both methods should be avoided since they do not provide clean access to the page 's actual content. For instance, they may return a system message for non-existing pages. Use WikiPage::getContent() instead. Code that relies on a textual representation of the page content should eventually be rewritten. However, ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page. Its behavior is controlled by $wgContentHandlerTextFallback it
Definition: contenthandler.txt:104
normal
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
SFH_OBJECT_ARGS
const SFH_OBJECT_ARGS
Definition: Defines.php:198
OT_PREPROCESS
const OT_PREPROCESS
Definition: Defines.php:186
NS_FILE
const NS_FILE
Definition: Defines.php:70
OT_PLAIN
const OT_PLAIN
Definition: Defines.php:188
$params
$params
Definition: styleTest.css.php:44
wfHostname
wfHostname()
Fetch server name for use in error reporting etc.
Definition: GlobalFunctions.php:1352
NS_TEMPLATE
const NS_TEMPLATE
Definition: Defines.php:74
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:585
link
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:3053
MediaWiki\Linker\LinkRendererFactory
Factory to create LinkRender objects.
Definition: LinkRendererFactory.php:31
$s
$s
Definition: mergeMessageFileList.php:186
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:82
a
</source > ! result< div class="mw-highlight mw-content-ltr" dir="ltr">< pre >< span ></span >< span class="kd"> var</span >< span class="nx"> a</span >< span class="p"></span ></pre ></div > ! end ! test Multiline< source/> in lists !input *< source > a b</source > *foo< source > a b</source > ! html< ul >< li >< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > a b</pre ></div ></li ></ul >< ul >< li > foo< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > a b</pre ></div ></li ></ul > ! html tidy< ul >< li >< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > a b</pre ></div ></li ></ul >< ul >< li > foo< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > a b</pre ></div ></li ></ul > ! end ! test Custom attributes !input< source lang="javascript" id="foo" class="bar" dir="rtl" style="font-size: larger;"> var a
Definition: parserTests.txt:85
page
target page
Definition: All_system_messages.txt:1267
$wgTitle
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition: api.php:57
MWTidy\isEnabled
static isEnabled()
Definition: MWTidy.php:54
so
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is and will automatically terminate your rights under this License parties who have received or from you under this License will not have their licenses terminated so long as such parties remain in full compliance You are not required to accept this since you have not signed it nothing else grants you permission to modify or distribute the Program or its derivative works These actions are prohibited by law if you do not accept this License by modifying or distributing the you indicate your acceptance of this License to do so
Definition: COPYING.txt:185
StripState
Definition: StripState.php:28
Makefile.open
open
Definition: Makefile.py:18
is
This document provides an overview of the usage of PageUpdater and that is
Definition: pageupdater.txt:3
Linker\tocLine
static tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1576
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:1043
php
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
PPFrame\NO_TEMPLATES
const NO_TEMPLATES
Definition: Preprocessor.php:168
specified
! hooks source ! endhooks ! test Non existent language !input< source lang="doesnotexist"> foobar</source > ! result< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > foobar</pre ></div > ! end ! test No language specified ! wikitext< source > foo</source > ! html< div class="mw-highlight mw-content-ltr" dir="ltr">< pre > foo</pre ></div > ! end ! test No language specified(no wellformed xml) !! config !! wikitext< source > bar</source > !! html< div class
Preprocessor
Definition: Preprocessor.php:29
later
If you want to remove the page from your watchlist later
Definition: All_system_messages.txt:361
SiteStats\images
static images()
Definition: SiteStats.php:139
StringUtils\replaceMarkup
static replaceMarkup( $search, $replace, $text)
More or less "markup-safe" str_replace() Ignores any instances of the separator inside <....
Definition: StringUtils.php:294
Revision\newFromTitle
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: Revision.php:137
Config
Interface for configuration instances.
Definition: Config.php:28
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:53
$data
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
Definition: generatePhpCharToUpperMappings.php:13
Linker\tocList
static tocList( $toc, $lang=null)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition: Linker.php:1613
$html
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:1985
MWException
MediaWiki exception.
Definition: MWException.php:26
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
Definition: GlobalFunctions.php:1078
table
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
BlockLevelPass\doBlockLevels
static doBlockLevels( $text, $lineStart)
Make lists from lines starting with ':', '*', '#', etc.
Definition: BlockLevelPass.php:50
$handler
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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 modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:780
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2636
wfUrlProtocolsWithoutProtRel
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
Definition: GlobalFunctions.php:788
$matches
$matches
Definition: NoLocalSettings.php:24
in
null for the wiki Added in
Definition: hooks.txt:1588
CoreTagHooks\register
static register( $parser)
Definition: CoreTagHooks.php:33
mode
if write to the Free Software Franklin Fifth MA USA Also add information on how to contact you by electronic and paper mail If the program is make it output a short notice like this when it starts in an interactive mode
Definition: COPYING.txt:307
StringUtils\explode
static explode( $separator, $subject)
Workalike for explode() with limited memory usage.
Definition: StringUtils.php:336
PPNode
There are three types of nodes:
Definition: Preprocessor.php:360
LinkHolderArray
Definition: LinkHolderArray.php:29
PPFrame\RECOVER_ORIG
const RECOVER_ORIG
Definition: Preprocessor.php:174
$attribs
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:1985
Linker\makeHeadline
static makeHeadline( $level, $attribs, $anchor, $html, $link, $fallbackAnchor=false)
Create a headline for content.
Definition: Linker.php:1693
not
if not
Definition: COPYING.txt:307
MWNamespace\isContent
static isContent( $index)
Does this namespace contain content, for the purposes of calculating statistics, etc?
Definition: MWNamespace.php:333
Linker\tocLineEnd
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1600
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:37
use
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 use
Definition: MIT-LICENSE.txt:10
MWNamespace\hasSubpages
static hasSubpages( $index)
Does the namespace allow subpages?
Definition: MWNamespace.php:366
form
null means default in associative array form
Definition: hooks.txt:1985
$lines
$lines
Definition: router.php:61
$parser
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition: hooks.txt:1802
MWTimestamp\getInstance
static getInstance( $ts=false)
Get a timestamp instance in GMT.
Definition: MWTimestamp.php:39
Linker\makeExternalLink
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:842
OT_WIKI
const OT_WIKI
Definition: Defines.php:185
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:576
$output
$output
Definition: SyntaxHighlight.php:334
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
SectionProfiler
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
Definition: SectionProfiler.php:30
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1941
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
RequestContext
Group all the pieces relevant to the context of a request into one instance.
Definition: RequestContext.php:32
array
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:949
list
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
$sort
$sort
Definition: profileinfo.php:328
Linker\splitTrail
static splitTrail( $trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition: Linker.php:1714
or
or
Definition: COPYING.txt:140
null
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
$fname
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition: Setup.php:123
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
key
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 use $formDescriptor instead 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 key
Definition: hooks.txt:2154
MediaWiki\Special\SpecialPageFactory
Factory for handling the special page list and generating SpecialPage objects.
Definition: SpecialPageFactory.php:63
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:743
SpecialVersion\getVersion
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
Definition: SpecialVersion.php:271
$line
$line
Definition: cdb.php:59
ParserFactory
Definition: ParserFactory.php:28
CoreParserFunctions\register
static register( $parser)
Definition: CoreParserFunctions.php:34
see
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
$e
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2162
$value
$value
Definition: styleTest.css.php:49
$wgNoFollowNsExceptions
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
Definition: DefaultSettings.php:4275
$wgNoFollowLinks
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
Definition: DefaultSettings.php:4269
NS_MEDIA
const NS_MEDIA
Definition: Defines.php:52
PPFrame
Definition: Preprocessor.php:166
on
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function 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:77
StringUtils\delimiterExplode
static delimiterExplode( $startDelim, $endDelim, $separator, $subject, $nested=false)
Explode a string, but ignore any instances of the separator inside the given start and end delimiters...
Definition: StringUtils.php:56
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1577
$ret
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:1985
display
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
Definition: APACHE-LICENSE-2.0.txt:49
Linker\makeImageLink
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:307
IP\isValid
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
format
if the prop value should be in the metadata multi language array format
Definition: hooks.txt:1644
tag
</code > tag
Definition: citeParserTests.txt:225
plain
either a plain
Definition: hooks.txt:2046
wfFindFile
wfFindFile( $title, $options=[])
Find a file.
Definition: GlobalFunctions.php:2677
SFH_NO_HASH
const SFH_NO_HASH
Definition: Defines.php:197
text
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
Definition: All_system_messages.txt:1267
$args
if( $line===false) $args
Definition: cdb.php:64
OT_HTML
const OT_HTML
Definition: Defines.php:184
Title
Represents a title within MediaWiki.
Definition: Title.php:40
CoreParserFunctions\cascadingsources
static cascadingsources( $parser, $title='')
Returns the sources of any cascading protection acting on a specified page.
Definition: CoreParserFunctions.php:1339
and
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
like
For a write use something like
Definition: database.txt:26
wfMatchesDomainList
wfMatchesDomainList( $url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
Definition: GlobalFunctions.php:915
$cache
$cache
Definition: mcc.php:33
MalformedTitleException
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
Definition: MalformedTitleException.php:25
$options
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:1985
Xml\isWellFormedXmlFragment
static isWellFormedXmlFragment( $text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:730
used
you don t have to do a grep find to see where the $wgReverseTitle variable is used
Definition: hooks.txt:115
history
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:1769
things
magicword txt Magic Words are some phrases used in the wikitext They are used for two things
Definition: magicword.txt:4
$rev
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:1769
as
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
Linker\tocUnindent
static tocUnindent( $level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1561
public
we sometimes make exceptions for this Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally NO WARRANTY BECAUSE THE PROGRAM IS LICENSED FREE OF THERE IS NO WARRANTY FOR THE TO THE EXTENT PERMITTED BY APPLICABLE LAW EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND OR OTHER PARTIES PROVIDE THE PROGRAM AS IS WITHOUT WARRANTY OF ANY EITHER EXPRESSED OR BUT NOT LIMITED THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU SHOULD THE PROGRAM PROVE YOU ASSUME THE COST OF ALL NECESSARY REPAIR OR CORRECTION IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT OR ANY OTHER PARTY WHO MAY MODIFY AND OR REDISTRIBUTE THE PROGRAM AS PERMITTED BE LIABLE TO YOU FOR INCLUDING ANY INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new and you want it to be of the greatest possible use to the public
Definition: COPYING.txt:284
Linker\makeMediaLinkFile
static makeMediaLinkFile(Title $title, $file, $html='')
Create a direct link to a given uploaded file.
Definition: Linker.php:775
StringUtils\delimiterReplace
static delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags='')
Perform an operation equivalent to preg_replace() with flags.
Definition: StringUtils.php:245
$link
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3053
revision
In both all secondary updates will be triggered handle like object that caches derived data representing a revision
Definition: pageupdater.txt:78
true
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 true
Definition: hooks.txt:1985
TextContent\normalizeLineEndings
static normalizeLineEndings( $text)
Do a "\\r\\n" -> "\\n" and "\\r" -> "\\n" transformation as well as trim trailing whitespace.
Definition: TextContent.php:180
$content
$content
Definition: pageupdater.txt:72
captcha-old.parser
parser
Definition: captcha-old.py:210
Linker\normalizeSubpageLink
static normalizeSubpageLink( $contextTitle, $target, &$text)
Definition: Linker.php:1394
of
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
Definition: globals.txt:10
ImageGalleryBase\factory
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
Definition: ImageGalleryBase.php:103
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
that
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
class
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
$time
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1802
$t
$t
Definition: testCompression.php:69
Title\legalChars
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:669
object
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 $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
$services
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition: hooks.txt:2220
captcha-old.output
output
Definition: captcha-old.py:240
MediaWikiServices
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
RawMessage
Variant of the Message class.
Definition: RawMessage.php:34
wfMessage
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 use $formDescriptor instead 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 unset offset - wrap String Wrap the message in html(usually something like "&lt
wfIsHHVM
wfIsHHVM()
Check if we are running under HHVM.
Definition: GlobalFunctions.php:1964
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:48
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
type
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
$username
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:780
MWTimestamp\getLocalInstance
static getLocalInstance( $ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
Definition: MWTimestamp.php:204
Linker\makeExternalImage
static makeExternalImage( $url, $alt='')
Return the code for images which were added via external links, via Parser::maybeMakeExternalImage().
Definition: Linker.php:251
SiteStats\edits
static edits()
Definition: SiteStats.php:94
Language
Internationalisation code.
Definition: Language.php:36
$buffer
$buffer
Definition: mwdoc-filter.php:49
MWHttpRequest\factory
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object Deprecated:
Definition: MWHttpRequest.php:183
$type
$type
Definition: testCompression.php:48
MWTidy\tidy
static tidy( $text)
Interface with Remex tidy.
Definition: MWTidy.php:42