MediaWiki  master
Parser.php
Go to the documentation of this file.
1 <?php
67 class Parser {
73  const VERSION = '1.6.4';
74 
80 
81  # Flags for Parser::setFunctionHook
82  const SFH_NO_HASH = 1;
83  const SFH_OBJECT_ARGS = 2;
84 
85  # Constants needed for external link processing
86  # Everything except bracket, space, or control characters
87  # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20
88  # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052
89  const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]';
90  # Simplified expression to match an IPv4 or IPv6 address, or
91  # at least one character of a host name (embeds EXT_LINK_URL_CLASS)
92  const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])';
93  # RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR)
94  // @codingStandardsIgnoreStart Generic.Files.LineLength
95  const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
96  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
97  // @codingStandardsIgnoreEnd
98 
99  # Regular expression for a non-newline space
100  const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
101 
102  # State constants for the definition list colon extraction
103  const COLON_STATE_TEXT = 0;
104  const COLON_STATE_TAG = 1;
111 
112  # Flags for preprocessToDom
113  const PTD_FOR_INCLUSION = 1;
114 
115  # Allowed values for $this->mOutputType
116  # Parameter to startExternalParse().
117  const OT_HTML = 1; # like parse()
118  const OT_WIKI = 2; # like preSaveTransform()
120  const OT_MSG = 3;
121  const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
122 
135  const MARKER_SUFFIX = "-QINU\x7f";
136  const MARKER_PREFIX = "\x7fUNIQ-";
137 
138  # Markers used for wrapping the table of contents
139  const TOC_START = '<mw:toc>';
140  const TOC_END = '</mw:toc>';
141 
142  # Persistent:
143  public $mTagHooks = [];
145  public $mFunctionHooks = [];
146  public $mFunctionSynonyms = [ 0 => [], 1 => [] ];
147  public $mFunctionTagHooks = [];
148  public $mStripList = [];
149  public $mDefaultStripList = [];
150  public $mVarCache = [];
151  public $mImageParams = [];
153  public $mMarkerIndex = 0;
154  public $mFirstCall = true;
155 
156  # Initialised by initialiseVariables()
157 
161  public $mVariables;
162 
166  public $mSubstWords;
167  # Initialised in constructor
169 
170  # Initialized in getPreprocessor()
171 
173 
174  # Cleared with clearState():
175 
178  public $mOutput;
180 
184  public $mStripState;
185 
191 
192  public $mLinkID;
196  public $mExpensiveFunctionCount; # number of expensive parser function calls
198 
202  public $mUser; # User object; only used when doing pre-save transform
203 
204  # Temporary
205  # These are variables reset at least once per parse regardless of $clearState
206 
210  public $mOptions;
211 
215  public $mTitle; # Title context, used for self-link rendering and similar things
216  public $mOutputType; # Output type, one of the OT_xxx constants
217  public $ot; # Shortcut alias, see setOutputType()
218  public $mRevisionObject; # The revision object of the specified revision ID
219  public $mRevisionId; # ID to display in {{REVISIONID}} tags
220  public $mRevisionTimestamp; # The timestamp of the specified revision ID
221  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
222  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
223  public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
224  public $mInputSize = false; # For {{PAGESIZE}} on current page.
225 
231 
238 
246 
251  public $mInParse = false;
252 
254  protected $mProfiler;
255 
259  public function __construct( $conf = [] ) {
260  $this->mConf = $conf;
261  $this->mUrlProtocols = wfUrlProtocols();
262  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
263  self::EXT_LINK_ADDR .
264  self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
265  if ( isset( $conf['preprocessorClass'] ) ) {
266  $this->mPreprocessorClass = $conf['preprocessorClass'];
267  } elseif ( defined( 'HPHP_VERSION' ) ) {
268  # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
269  $this->mPreprocessorClass = 'Preprocessor_Hash';
270  } elseif ( extension_loaded( 'domxml' ) ) {
271  # PECL extension that conflicts with the core DOM extension (bug 13770)
272  wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
273  $this->mPreprocessorClass = 'Preprocessor_Hash';
274  } elseif ( extension_loaded( 'dom' ) ) {
275  $this->mPreprocessorClass = 'Preprocessor_DOM';
276  } else {
277  $this->mPreprocessorClass = 'Preprocessor_Hash';
278  }
279  wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
280  }
281 
285  public function __destruct() {
286  if ( isset( $this->mLinkHolders ) ) {
287  unset( $this->mLinkHolders );
288  }
289  foreach ( $this as $name => $value ) {
290  unset( $this->$name );
291  }
292  }
293 
297  public function __clone() {
298  $this->mInParse = false;
299 
300  // Bug 56226: When you create a reference "to" an object field, that
301  // makes the object field itself be a reference too (until the other
302  // reference goes out of scope). When cloning, any field that's a
303  // reference is copied as a reference in the new object. Both of these
304  // are defined PHP5 behaviors, as inconvenient as it is for us when old
305  // hooks from PHP4 days are passing fields by reference.
306  foreach ( [ 'mStripState', 'mVarCache' ] as $k ) {
307  // Make a non-reference copy of the field, then rebind the field to
308  // reference the new copy.
309  $tmp = $this->$k;
310  $this->$k =& $tmp;
311  unset( $tmp );
312  }
313 
314  Hooks::run( 'ParserCloned', [ $this ] );
315  }
316 
320  public function firstCallInit() {
321  if ( !$this->mFirstCall ) {
322  return;
323  }
324  $this->mFirstCall = false;
325 
327  CoreTagHooks::register( $this );
328  $this->initialiseVariables();
329 
330  Hooks::run( 'ParserFirstCallInit', [ &$this ] );
331  }
332 
338  public function clearState() {
339  if ( $this->mFirstCall ) {
340  $this->firstCallInit();
341  }
342  $this->mOutput = new ParserOutput;
343  $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
344  $this->mAutonumber = 0;
345  $this->mLastSection = '';
346  $this->mDTopen = false;
347  $this->mIncludeCount = [];
348  $this->mArgStack = false;
349  $this->mInPre = false;
350  $this->mLinkHolders = new LinkHolderArray( $this );
351  $this->mLinkID = 0;
352  $this->mRevisionObject = $this->mRevisionTimestamp =
353  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
354  $this->mVarCache = [];
355  $this->mUser = null;
356  $this->mLangLinkLanguages = [];
357  $this->currentRevisionCache = null;
358 
359  $this->mStripState = new StripState;
360 
361  # Clear these on every parse, bug 4549
362  $this->mTplRedirCache = $this->mTplDomCache = [];
363 
364  $this->mShowToc = true;
365  $this->mForceTocPosition = false;
366  $this->mIncludeSizes = [
367  'post-expand' => 0,
368  'arg' => 0,
369  ];
370  $this->mPPNodeCount = 0;
371  $this->mGeneratedPPNodeCount = 0;
372  $this->mHighestExpansionDepth = 0;
373  $this->mDefaultSort = false;
374  $this->mHeadings = [];
375  $this->mDoubleUnderscores = [];
376  $this->mExpensiveFunctionCount = 0;
377 
378  # Fix cloning
379  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
380  $this->mPreprocessor = null;
381  }
382 
383  $this->mProfiler = new SectionProfiler();
384 
385  Hooks::run( 'ParserClearState', [ &$this ] );
386  }
387 
400  public function parse( $text, Title $title, ParserOptions $options,
401  $linestart = true, $clearState = true, $revid = null
402  ) {
409 
410  if ( $clearState ) {
411  // We use U+007F DELETE to construct strip markers, so we have to make
412  // sure that this character does not occur in the input text.
413  $text = strtr( $text, "\x7f", "?" );
414  $magicScopeVariable = $this->lock();
415  }
416 
417  $this->startParse( $title, $options, self::OT_HTML, $clearState );
418 
419  $this->currentRevisionCache = null;
420  $this->mInputSize = strlen( $text );
421  if ( $this->mOptions->getEnableLimitReport() ) {
422  $this->mOutput->resetParseStartTime();
423  }
424 
425  $oldRevisionId = $this->mRevisionId;
426  $oldRevisionObject = $this->mRevisionObject;
427  $oldRevisionTimestamp = $this->mRevisionTimestamp;
428  $oldRevisionUser = $this->mRevisionUser;
429  $oldRevisionSize = $this->mRevisionSize;
430  if ( $revid !== null ) {
431  $this->mRevisionId = $revid;
432  $this->mRevisionObject = null;
433  $this->mRevisionTimestamp = null;
434  $this->mRevisionUser = null;
435  $this->mRevisionSize = null;
436  }
437 
438  Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
439  # No more strip!
440  Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
441  $text = $this->internalParse( $text );
442  Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] );
443 
444  $text = $this->internalParseHalfParsed( $text, true, $linestart );
445 
453  if ( !( $options->getDisableTitleConversion()
454  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
455  || isset( $this->mDoubleUnderscores['notitleconvert'] )
456  || $this->mOutput->getDisplayTitle() !== false )
457  ) {
458  $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
459  if ( $convruletitle ) {
460  $this->mOutput->setTitleText( $convruletitle );
461  } else {
462  $titleText = $this->getConverterLanguage()->convertTitle( $title );
463  $this->mOutput->setTitleText( $titleText );
464  }
465  }
466 
467  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
468  $this->limitationWarn( 'expensive-parserfunction',
469  $this->mExpensiveFunctionCount,
470  $this->mOptions->getExpensiveParserFunctionLimit()
471  );
472  }
473 
474  # Information on include size limits, for the benefit of users who try to skirt them
475  if ( $this->mOptions->getEnableLimitReport() ) {
476  $max = $this->mOptions->getMaxIncludeSize();
477 
478  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
479  if ( $cpuTime !== null ) {
480  $this->mOutput->setLimitReportData( 'limitreport-cputime',
481  sprintf( "%.3f", $cpuTime )
482  );
483  }
484 
485  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
486  $this->mOutput->setLimitReportData( 'limitreport-walltime',
487  sprintf( "%.3f", $wallTime )
488  );
489 
490  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
491  [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
492  );
493  $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
494  [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
495  );
496  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
497  [ $this->mIncludeSizes['post-expand'], $max ]
498  );
499  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
500  [ $this->mIncludeSizes['arg'], $max ]
501  );
502  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
503  [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
504  );
505  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
506  [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
507  );
508  Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
509 
510  $limitReport = "NewPP limit report\n";
511  if ( $wgShowHostnames ) {
512  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
513  }
514  $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
515  $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
516  $limitReport .= 'Dynamic content: ' .
517  ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
518  "\n";
519 
520  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
521  if ( Hooks::run( 'ParserLimitReportFormat',
522  [ $key, &$value, &$limitReport, false, false ]
523  ) ) {
524  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
525  $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
526  ->inLanguage( 'en' )->useDatabase( false );
527  if ( !$valueMsg->exists() ) {
528  $valueMsg = new RawMessage( '$1' );
529  }
530  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
531  $valueMsg->params( $value );
532  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
533  }
534  }
535  }
536  // Since we're not really outputting HTML, decode the entities and
537  // then re-encode the things that need hiding inside HTML comments.
538  $limitReport = htmlspecialchars_decode( $limitReport );
539  Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ] );
540 
541  // Sanitize for comment. Note '‐' in the replacement is U+2010,
542  // which looks much like the problematic '-'.
543  $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
544  $text .= "\n<!-- \n$limitReport-->\n";
545 
546  // Add on template profiling data
547  $dataByFunc = $this->mProfiler->getFunctionStats();
548  uasort( $dataByFunc, function ( $a, $b ) {
549  return $a['real'] < $b['real']; // descending order
550  } );
551  $profileReport = "Transclusion expansion time report (%,ms,calls,template)\n";
552  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
553  $profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n",
554  $item['%real'], $item['real'], $item['calls'],
555  htmlspecialchars( $item['name'] ) );
556  }
557  $text .= "\n<!-- \n$profileReport-->\n";
558 
559  if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
560  wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
561  $this->mTitle->getPrefixedDBkey() );
562  }
563  }
564  $this->mOutput->setText( $text );
565 
566  $this->mRevisionId = $oldRevisionId;
567  $this->mRevisionObject = $oldRevisionObject;
568  $this->mRevisionTimestamp = $oldRevisionTimestamp;
569  $this->mRevisionUser = $oldRevisionUser;
570  $this->mRevisionSize = $oldRevisionSize;
571  $this->mInputSize = false;
572  $this->currentRevisionCache = null;
573 
574  return $this->mOutput;
575  }
576 
599  public function recursiveTagParse( $text, $frame = false ) {
600  Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
601  Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
602  $text = $this->internalParse( $text, false, $frame );
603  return $text;
604  }
605 
623  public function recursiveTagParseFully( $text, $frame = false ) {
624  $text = $this->recursiveTagParse( $text, $frame );
625  $text = $this->internalParseHalfParsed( $text, false );
626  return $text;
627  }
628 
640  public function preprocess( $text, Title $title = null,
641  ParserOptions $options, $revid = null, $frame = false
642  ) {
643  $magicScopeVariable = $this->lock();
644  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
645  if ( $revid !== null ) {
646  $this->mRevisionId = $revid;
647  }
648  Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] );
649  Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] );
650  $text = $this->replaceVariables( $text, $frame );
651  $text = $this->mStripState->unstripBoth( $text );
652  return $text;
653  }
654 
664  public function recursivePreprocess( $text, $frame = false ) {
665  $text = $this->replaceVariables( $text, $frame );
666  $text = $this->mStripState->unstripBoth( $text );
667  return $text;
668  }
669 
683  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) {
684  $msg = new RawMessage( $text );
685  $text = $msg->params( $params )->plain();
686 
687  # Parser (re)initialisation
688  $magicScopeVariable = $this->lock();
689  $this->startParse( $title, $options, self::OT_PLAIN, true );
690 
692  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
693  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
694  $text = $this->mStripState->unstripBoth( $text );
695  return $text;
696  }
697 
704  public static function getRandomString() {
705  wfDeprecated( __METHOD__, '1.26' );
706  return wfRandomString( 16 );
707  }
708 
715  public function setUser( $user ) {
716  $this->mUser = $user;
717  }
718 
725  public function uniqPrefix() {
726  wfDeprecated( __METHOD__, '1.26' );
727  return self::MARKER_PREFIX;
728  }
729 
735  public function setTitle( $t ) {
736  if ( !$t ) {
737  $t = Title::newFromText( 'NO TITLE' );
738  }
739 
740  if ( $t->hasFragment() ) {
741  # Strip the fragment to avoid various odd effects
742  $this->mTitle = $t->createFragmentTarget( '' );
743  } else {
744  $this->mTitle = $t;
745  }
746  }
747 
753  public function getTitle() {
754  return $this->mTitle;
755  }
756 
763  public function Title( $x = null ) {
764  return wfSetVar( $this->mTitle, $x );
765  }
766 
772  public function setOutputType( $ot ) {
773  $this->mOutputType = $ot;
774  # Shortcut alias
775  $this->ot = [
776  'html' => $ot == self::OT_HTML,
777  'wiki' => $ot == self::OT_WIKI,
778  'pre' => $ot == self::OT_PREPROCESS,
779  'plain' => $ot == self::OT_PLAIN,
780  ];
781  }
782 
789  public function OutputType( $x = null ) {
790  return wfSetVar( $this->mOutputType, $x );
791  }
792 
798  public function getOutput() {
799  return $this->mOutput;
800  }
801 
807  public function getOptions() {
808  return $this->mOptions;
809  }
810 
817  public function Options( $x = null ) {
818  return wfSetVar( $this->mOptions, $x );
819  }
820 
824  public function nextLinkID() {
825  return $this->mLinkID++;
826  }
827 
831  public function setLinkID( $id ) {
832  $this->mLinkID = $id;
833  }
834 
839  public function getFunctionLang() {
840  return $this->getTargetLanguage();
841  }
842 
852  public function getTargetLanguage() {
853  $target = $this->mOptions->getTargetLanguage();
854 
855  if ( $target !== null ) {
856  return $target;
857  } elseif ( $this->mOptions->getInterfaceMessage() ) {
858  return $this->mOptions->getUserLangObj();
859  } elseif ( is_null( $this->mTitle ) ) {
860  throw new MWException( __METHOD__ . ': $this->mTitle is null' );
861  }
862 
863  return $this->mTitle->getPageLanguage();
864  }
865 
870  public function getConverterLanguage() {
871  return $this->getTargetLanguage();
872  }
873 
880  public function getUser() {
881  if ( !is_null( $this->mUser ) ) {
882  return $this->mUser;
883  }
884  return $this->mOptions->getUser();
885  }
886 
892  public function getPreprocessor() {
893  if ( !isset( $this->mPreprocessor ) ) {
894  $class = $this->mPreprocessorClass;
895  $this->mPreprocessor = new $class( $this );
896  }
897  return $this->mPreprocessor;
898  }
899 
921  public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
922  if ( $uniq_prefix !== null ) {
923  wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
924  }
925  static $n = 1;
926  $stripped = '';
927  $matches = [];
928 
929  $taglist = implode( '|', $elements );
930  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
931 
932  while ( $text != '' ) {
933  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
934  $stripped .= $p[0];
935  if ( count( $p ) < 5 ) {
936  break;
937  }
938  if ( count( $p ) > 5 ) {
939  # comment
940  $element = $p[4];
941  $attributes = '';
942  $close = '';
943  $inside = $p[5];
944  } else {
945  # tag
946  $element = $p[1];
947  $attributes = $p[2];
948  $close = $p[3];
949  $inside = $p[4];
950  }
951 
952  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
953  $stripped .= $marker;
954 
955  if ( $close === '/>' ) {
956  # Empty element tag, <tag />
957  $content = null;
958  $text = $inside;
959  $tail = null;
960  } else {
961  if ( $element === '!--' ) {
962  $end = '/(-->)/';
963  } else {
964  $end = "/(<\\/$element\\s*>)/i";
965  }
966  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
967  $content = $q[0];
968  if ( count( $q ) < 3 ) {
969  # No end tag -- let it run out to the end of the text.
970  $tail = '';
971  $text = '';
972  } else {
973  $tail = $q[1];
974  $text = $q[2];
975  }
976  }
977 
978  $matches[$marker] = [ $element,
979  $content,
980  Sanitizer::decodeTagAttributes( $attributes ),
981  "<$element$attributes$close$content$tail" ];
982  }
983  return $stripped;
984  }
985 
991  public function getStripList() {
992  return $this->mStripList;
993  }
994 
1004  public function insertStripItem( $text ) {
1005  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
1006  $this->mMarkerIndex++;
1007  $this->mStripState->addGeneral( $marker, $text );
1008  return $marker;
1009  }
1010 
1018  public function doTableStuff( $text ) {
1019 
1020  $lines = StringUtils::explode( "\n", $text );
1021  $out = '';
1022  $td_history = []; # Is currently a td tag open?
1023  $last_tag_history = []; # Save history of last lag activated (td, th or caption)
1024  $tr_history = []; # Is currently a tr tag open?
1025  $tr_attributes = []; # history of tr attributes
1026  $has_opened_tr = []; # Did this table open a <tr> element?
1027  $indent_level = 0; # indent level of the table
1028 
1029  foreach ( $lines as $outLine ) {
1030  $line = trim( $outLine );
1031 
1032  if ( $line === '' ) { # empty line, go to next line
1033  $out .= $outLine . "\n";
1034  continue;
1035  }
1036 
1037  $first_character = $line[0];
1038  $first_two = substr( $line, 0, 2 );
1039  $matches = [];
1040 
1041  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1042  # First check if we are starting a new table
1043  $indent_level = strlen( $matches[1] );
1044 
1045  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1046  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1047 
1048  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1049  array_push( $td_history, false );
1050  array_push( $last_tag_history, '' );
1051  array_push( $tr_history, false );
1052  array_push( $tr_attributes, '' );
1053  array_push( $has_opened_tr, false );
1054  } elseif ( count( $td_history ) == 0 ) {
1055  # Don't do any of the following
1056  $out .= $outLine . "\n";
1057  continue;
1058  } elseif ( $first_two === '|}' ) {
1059  # We are ending a table
1060  $line = '</table>' . substr( $line, 2 );
1061  $last_tag = array_pop( $last_tag_history );
1062 
1063  if ( !array_pop( $has_opened_tr ) ) {
1064  $line = "<tr><td></td></tr>{$line}";
1065  }
1066 
1067  if ( array_pop( $tr_history ) ) {
1068  $line = "</tr>{$line}";
1069  }
1070 
1071  if ( array_pop( $td_history ) ) {
1072  $line = "</{$last_tag}>{$line}";
1073  }
1074  array_pop( $tr_attributes );
1075  $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1076  } elseif ( $first_two === '|-' ) {
1077  # Now we have a table row
1078  $line = preg_replace( '#^\|-+#', '', $line );
1079 
1080  # Whats after the tag is now only attributes
1081  $attributes = $this->mStripState->unstripBoth( $line );
1082  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1083  array_pop( $tr_attributes );
1084  array_push( $tr_attributes, $attributes );
1085 
1086  $line = '';
1087  $last_tag = array_pop( $last_tag_history );
1088  array_pop( $has_opened_tr );
1089  array_push( $has_opened_tr, true );
1090 
1091  if ( array_pop( $tr_history ) ) {
1092  $line = '</tr>';
1093  }
1094 
1095  if ( array_pop( $td_history ) ) {
1096  $line = "</{$last_tag}>{$line}";
1097  }
1098 
1099  $outLine = $line;
1100  array_push( $tr_history, false );
1101  array_push( $td_history, false );
1102  array_push( $last_tag_history, '' );
1103  } elseif ( $first_character === '|'
1104  || $first_character === '!'
1105  || $first_two === '|+'
1106  ) {
1107  # This might be cell elements, td, th or captions
1108  if ( $first_two === '|+' ) {
1109  $first_character = '+';
1110  $line = substr( $line, 2 );
1111  } else {
1112  $line = substr( $line, 1 );
1113  }
1114 
1115  // Implies both are valid for table headings.
1116  if ( $first_character === '!' ) {
1117  $line = StringUtils::replaceMarkup( '!!', '||', $line );
1118  }
1119 
1120  # Split up multiple cells on the same line.
1121  # FIXME : This can result in improper nesting of tags processed
1122  # by earlier parser steps.
1123  $cells = explode( '||', $line );
1124 
1125  $outLine = '';
1126 
1127  # Loop through each table cell
1128  foreach ( $cells as $cell ) {
1129  $previous = '';
1130  if ( $first_character !== '+' ) {
1131  $tr_after = array_pop( $tr_attributes );
1132  if ( !array_pop( $tr_history ) ) {
1133  $previous = "<tr{$tr_after}>\n";
1134  }
1135  array_push( $tr_history, true );
1136  array_push( $tr_attributes, '' );
1137  array_pop( $has_opened_tr );
1138  array_push( $has_opened_tr, true );
1139  }
1140 
1141  $last_tag = array_pop( $last_tag_history );
1142 
1143  if ( array_pop( $td_history ) ) {
1144  $previous = "</{$last_tag}>\n{$previous}";
1145  }
1146 
1147  if ( $first_character === '|' ) {
1148  $last_tag = 'td';
1149  } elseif ( $first_character === '!' ) {
1150  $last_tag = 'th';
1151  } elseif ( $first_character === '+' ) {
1152  $last_tag = 'caption';
1153  } else {
1154  $last_tag = '';
1155  }
1156 
1157  array_push( $last_tag_history, $last_tag );
1158 
1159  # A cell could contain both parameters and data
1160  $cell_data = explode( '|', $cell, 2 );
1161 
1162  # Bug 553: Note that a '|' inside an invalid link should not
1163  # be mistaken as delimiting cell parameters
1164  if ( strpos( $cell_data[0], '[[' ) !== false ) {
1165  $cell = "{$previous}<{$last_tag}>{$cell}";
1166  } elseif ( count( $cell_data ) == 1 ) {
1167  $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1168  } else {
1169  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1170  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1171  $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1172  }
1173 
1174  $outLine .= $cell;
1175  array_push( $td_history, true );
1176  }
1177  }
1178  $out .= $outLine . "\n";
1179  }
1180 
1181  # Closing open td, tr && table
1182  while ( count( $td_history ) > 0 ) {
1183  if ( array_pop( $td_history ) ) {
1184  $out .= "</td>\n";
1185  }
1186  if ( array_pop( $tr_history ) ) {
1187  $out .= "</tr>\n";
1188  }
1189  if ( !array_pop( $has_opened_tr ) ) {
1190  $out .= "<tr><td></td></tr>\n";
1191  }
1192 
1193  $out .= "</table>\n";
1194  }
1195 
1196  # Remove trailing line-ending (b/c)
1197  if ( substr( $out, -1 ) === "\n" ) {
1198  $out = substr( $out, 0, -1 );
1199  }
1200 
1201  # special case: don't return empty table
1202  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1203  $out = '';
1204  }
1205 
1206  return $out;
1207  }
1208 
1221  public function internalParse( $text, $isMain = true, $frame = false ) {
1222 
1223  $origText = $text;
1224 
1225  # Hook to suspend the parser in this state
1226  if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) {
1227  return $text;
1228  }
1229 
1230  # if $frame is provided, then use $frame for replacing any variables
1231  if ( $frame ) {
1232  # use frame depth to infer how include/noinclude tags should be handled
1233  # depth=0 means this is the top-level document; otherwise it's an included document
1234  if ( !$frame->depth ) {
1235  $flag = 0;
1236  } else {
1237  $flag = Parser::PTD_FOR_INCLUSION;
1238  }
1239  $dom = $this->preprocessToDom( $text, $flag );
1240  $text = $frame->expand( $dom );
1241  } else {
1242  # if $frame is not provided, then use old-style replaceVariables
1243  $text = $this->replaceVariables( $text );
1244  }
1245 
1246  Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] );
1247  $text = Sanitizer::removeHTMLtags(
1248  $text,
1249  [ &$this, 'attributeStripCallback' ],
1250  false,
1251  array_keys( $this->mTransparentTagHooks )
1252  );
1253  Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] );
1254 
1255  # Tables need to come after variable replacement for things to work
1256  # properly; putting them before other transformations should keep
1257  # exciting things like link expansions from showing up in surprising
1258  # places.
1259  $text = $this->doTableStuff( $text );
1260 
1261  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1262 
1263  $text = $this->doDoubleUnderscore( $text );
1264 
1265  $text = $this->doHeadings( $text );
1266  $text = $this->replaceInternalLinks( $text );
1267  $text = $this->doAllQuotes( $text );
1268  $text = $this->replaceExternalLinks( $text );
1269 
1270  # replaceInternalLinks may sometimes leave behind
1271  # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1272  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1273 
1274  $text = $this->doMagicLinks( $text );
1275  $text = $this->formatHeadings( $text, $origText, $isMain );
1276 
1277  return $text;
1278  }
1279 
1289  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1290  $text = $this->mStripState->unstripGeneral( $text );
1291 
1292  if ( $isMain ) {
1293  Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] );
1294  }
1295 
1296  # Clean up special characters, only run once, next-to-last before doBlockLevels
1297  $fixtags = [
1298  # french spaces, last one Guillemet-left
1299  # only if there is something before the space
1300  '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1301  # french spaces, Guillemet-right
1302  '/(\\302\\253) /' => '\\1&#160;',
1303  '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1304  ];
1305  $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1306 
1307  $text = $this->doBlockLevels( $text, $linestart );
1308 
1309  $this->replaceLinkHolders( $text );
1310 
1318  if ( !( $this->mOptions->getDisableContentConversion()
1319  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1320  ) {
1321  if ( !$this->mOptions->getInterfaceMessage() ) {
1322  # The position of the convert() call should not be changed. it
1323  # assumes that the links are all replaced and the only thing left
1324  # is the <nowiki> mark.
1325  $text = $this->getConverterLanguage()->convert( $text );
1326  }
1327  }
1328 
1329  $text = $this->mStripState->unstripNoWiki( $text );
1330 
1331  if ( $isMain ) {
1332  Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] );
1333  }
1334 
1335  $text = $this->replaceTransparentTags( $text );
1336  $text = $this->mStripState->unstripGeneral( $text );
1337 
1338  $text = Sanitizer::normalizeCharReferences( $text );
1339 
1340  if ( MWTidy::isEnabled() && $this->mOptions->getTidy() ) {
1341  $text = MWTidy::tidy( $text );
1342  $this->mOutput->addModuleStyles( MWTidy::getModuleStyles() );
1343  } else {
1344  # attempt to sanitize at least some nesting problems
1345  # (bug #2702 and quite a few others)
1346  $tidyregs = [
1347  # ''Something [http://www.cool.com cool''] -->
1348  # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1349  '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1350  '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1351  # fix up an anchor inside another anchor, only
1352  # at least for a single single nested link (bug 3695)
1353  '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1354  '\\1\\2</a>\\3</a>\\1\\4</a>',
1355  # fix div inside inline elements- doBlockLevels won't wrap a line which
1356  # contains a div, so fix it up here; replace
1357  # div with escaped text
1358  '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1359  '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1360  # remove empty italic or bold tag pairs, some
1361  # introduced by rules above
1362  '/<([bi])><\/\\1>/' => '',
1363  ];
1364 
1365  $text = preg_replace(
1366  array_keys( $tidyregs ),
1367  array_values( $tidyregs ),
1368  $text );
1369  }
1370 
1371  if ( $isMain ) {
1372  Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] );
1373  }
1374 
1375  return $text;
1376  }
1377 
1389  public function doMagicLinks( $text ) {
1390  $prots = wfUrlProtocolsWithoutProtRel();
1391  $urlChar = self::EXT_LINK_URL_CLASS;
1392  $addr = self::EXT_LINK_ADDR;
1393  $space = self::SPACE_NOT_NL; # non-newline space
1394  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1395  $spaces = "$space++"; # possessive match of 1 or more spaces
1396  $text = preg_replace_callback(
1397  '!(?: # Start cases
1398  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1399  (<.*?>) | # m[2]: Skip stuff inside
1400  # HTML elements' . "
1401  (\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links
1402  # m[4]: Post-protocol path
1403  \b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number
1404  ([0-9]+)\b |
1405  \bISBN $spaces ( # m[6]: ISBN, capture number
1406  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1407  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1408  [0-9Xx] # check digit
1409  )\b
1410  )!xu", [ &$this, 'magicLinkCallback' ], $text );
1411  return $text;
1412  }
1413 
1419  public function magicLinkCallback( $m ) {
1420  if ( isset( $m[1] ) && $m[1] !== '' ) {
1421  # Skip anchor
1422  return $m[0];
1423  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1424  # Skip HTML element
1425  return $m[0];
1426  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1427  # Free external link
1428  return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) );
1429  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1430  # RFC or PMID
1431  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1432  $keyword = 'RFC';
1433  $urlmsg = 'rfcurl';
1434  $cssClass = 'mw-magiclink-rfc';
1435  $id = $m[5];
1436  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1437  $keyword = 'PMID';
1438  $urlmsg = 'pubmedurl';
1439  $cssClass = 'mw-magiclink-pmid';
1440  $id = $m[5];
1441  } else {
1442  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1443  substr( $m[0], 0, 20 ) . '"' );
1444  }
1445  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1446  return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass );
1447  } elseif ( isset( $m[6] ) && $m[6] !== '' ) {
1448  # ISBN
1449  $isbn = $m[6];
1450  $space = self::SPACE_NOT_NL; # non-newline space
1451  $isbn = preg_replace( "/$space/", ' ', $isbn );
1452  $num = strtr( $isbn, [
1453  '-' => '',
1454  ' ' => '',
1455  'x' => 'X',
1456  ] );
1457  $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
1458  return '<a href="' .
1459  htmlspecialchars( $titleObj->getLocalURL() ) .
1460  "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1461  } else {
1462  return $m[0];
1463  }
1464  }
1465 
1475  public function makeFreeExternalLink( $url, $numPostProto ) {
1476  $trail = '';
1477 
1478  # The characters '<' and '>' (which were escaped by
1479  # removeHTMLtags()) should not be included in
1480  # URLs, per RFC 2396.
1481  # Make &nbsp; terminate a URL as well (bug T84937)
1482  $m2 = [];
1483  if ( preg_match(
1484  '/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/',
1485  $url,
1486  $m2,
1487  PREG_OFFSET_CAPTURE
1488  ) ) {
1489  $trail = substr( $url, $m2[0][1] ) . $trail;
1490  $url = substr( $url, 0, $m2[0][1] );
1491  }
1492 
1493  # Move trailing punctuation to $trail
1494  $sep = ',;\.:!?';
1495  # If there is no left bracket, then consider right brackets fair game too
1496  if ( strpos( $url, '(' ) === false ) {
1497  $sep .= ')';
1498  }
1499 
1500  $urlRev = strrev( $url );
1501  $numSepChars = strspn( $urlRev, $sep );
1502  # Don't break a trailing HTML entity by moving the ; into $trail
1503  # This is in hot code, so use substr_compare to avoid having to
1504  # create a new string object for the comparison
1505  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1506  # more optimization: instead of running preg_match with a $
1507  # anchor, which can be slow, do the match on the reversed
1508  # string starting at the desired offset.
1509  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1510  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1511  $numSepChars--;
1512  }
1513  }
1514  if ( $numSepChars ) {
1515  $trail = substr( $url, -$numSepChars ) . $trail;
1516  $url = substr( $url, 0, -$numSepChars );
1517  }
1518 
1519  # Verify that we still have a real URL after trail removal, and
1520  # not just lone protocol
1521  if ( strlen( $trail ) >= $numPostProto ) {
1522  return $url . $trail;
1523  }
1524 
1525  $url = Sanitizer::cleanUrl( $url );
1526 
1527  # Is this an external image?
1528  $text = $this->maybeMakeExternalImage( $url );
1529  if ( $text === false ) {
1530  # Not an image, make a link
1531  $text = Linker::makeExternalLink( $url,
1532  $this->getConverterLanguage()->markNoConversion( $url, true ),
1533  true, 'free',
1534  $this->getExternalLinkAttribs( $url ) );
1535  # Register it in the output object...
1536  # Replace unnecessary URL escape codes with their equivalent characters
1537  $pasteurized = self::normalizeLinkUrl( $url );
1538  $this->mOutput->addExternalLink( $pasteurized );
1539  }
1540  return $text . $trail;
1541  }
1542 
1552  public function doHeadings( $text ) {
1553  for ( $i = 6; $i >= 1; --$i ) {
1554  $h = str_repeat( '=', $i );
1555  $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1556  }
1557  return $text;
1558  }
1559 
1568  public function doAllQuotes( $text ) {
1569  $outtext = '';
1570  $lines = StringUtils::explode( "\n", $text );
1571  foreach ( $lines as $line ) {
1572  $outtext .= $this->doQuotes( $line ) . "\n";
1573  }
1574  $outtext = substr( $outtext, 0, -1 );
1575  return $outtext;
1576  }
1577 
1585  public function doQuotes( $text ) {
1586  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1587  $countarr = count( $arr );
1588  if ( $countarr == 1 ) {
1589  return $text;
1590  }
1591 
1592  // First, do some preliminary work. This may shift some apostrophes from
1593  // being mark-up to being text. It also counts the number of occurrences
1594  // of bold and italics mark-ups.
1595  $numbold = 0;
1596  $numitalics = 0;
1597  for ( $i = 1; $i < $countarr; $i += 2 ) {
1598  $thislen = strlen( $arr[$i] );
1599  // If there are ever four apostrophes, assume the first is supposed to
1600  // be text, and the remaining three constitute mark-up for bold text.
1601  // (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1602  if ( $thislen == 4 ) {
1603  $arr[$i - 1] .= "'";
1604  $arr[$i] = "'''";
1605  $thislen = 3;
1606  } elseif ( $thislen > 5 ) {
1607  // If there are more than 5 apostrophes in a row, assume they're all
1608  // text except for the last 5.
1609  // (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1610  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1611  $arr[$i] = "'''''";
1612  $thislen = 5;
1613  }
1614  // Count the number of occurrences of bold and italics mark-ups.
1615  if ( $thislen == 2 ) {
1616  $numitalics++;
1617  } elseif ( $thislen == 3 ) {
1618  $numbold++;
1619  } elseif ( $thislen == 5 ) {
1620  $numitalics++;
1621  $numbold++;
1622  }
1623  }
1624 
1625  // If there is an odd number of both bold and italics, it is likely
1626  // that one of the bold ones was meant to be an apostrophe followed
1627  // by italics. Which one we cannot know for certain, but it is more
1628  // likely to be one that has a single-letter word before it.
1629  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1630  $firstsingleletterword = -1;
1631  $firstmultiletterword = -1;
1632  $firstspace = -1;
1633  for ( $i = 1; $i < $countarr; $i += 2 ) {
1634  if ( strlen( $arr[$i] ) == 3 ) {
1635  $x1 = substr( $arr[$i - 1], -1 );
1636  $x2 = substr( $arr[$i - 1], -2, 1 );
1637  if ( $x1 === ' ' ) {
1638  if ( $firstspace == -1 ) {
1639  $firstspace = $i;
1640  }
1641  } elseif ( $x2 === ' ' ) {
1642  $firstsingleletterword = $i;
1643  // if $firstsingleletterword is set, we don't
1644  // look at the other options, so we can bail early.
1645  break;
1646  } else {
1647  if ( $firstmultiletterword == -1 ) {
1648  $firstmultiletterword = $i;
1649  }
1650  }
1651  }
1652  }
1653 
1654  // If there is a single-letter word, use it!
1655  if ( $firstsingleletterword > -1 ) {
1656  $arr[$firstsingleletterword] = "''";
1657  $arr[$firstsingleletterword - 1] .= "'";
1658  } elseif ( $firstmultiletterword > -1 ) {
1659  // If not, but there's a multi-letter word, use that one.
1660  $arr[$firstmultiletterword] = "''";
1661  $arr[$firstmultiletterword - 1] .= "'";
1662  } elseif ( $firstspace > -1 ) {
1663  // ... otherwise use the first one that has neither.
1664  // (notice that it is possible for all three to be -1 if, for example,
1665  // there is only one pentuple-apostrophe in the line)
1666  $arr[$firstspace] = "''";
1667  $arr[$firstspace - 1] .= "'";
1668  }
1669  }
1670 
1671  // Now let's actually convert our apostrophic mush to HTML!
1672  $output = '';
1673  $buffer = '';
1674  $state = '';
1675  $i = 0;
1676  foreach ( $arr as $r ) {
1677  if ( ( $i % 2 ) == 0 ) {
1678  if ( $state === 'both' ) {
1679  $buffer .= $r;
1680  } else {
1681  $output .= $r;
1682  }
1683  } else {
1684  $thislen = strlen( $r );
1685  if ( $thislen == 2 ) {
1686  if ( $state === 'i' ) {
1687  $output .= '</i>';
1688  $state = '';
1689  } elseif ( $state === 'bi' ) {
1690  $output .= '</i>';
1691  $state = 'b';
1692  } elseif ( $state === 'ib' ) {
1693  $output .= '</b></i><b>';
1694  $state = 'b';
1695  } elseif ( $state === 'both' ) {
1696  $output .= '<b><i>' . $buffer . '</i>';
1697  $state = 'b';
1698  } else { // $state can be 'b' or ''
1699  $output .= '<i>';
1700  $state .= 'i';
1701  }
1702  } elseif ( $thislen == 3 ) {
1703  if ( $state === 'b' ) {
1704  $output .= '</b>';
1705  $state = '';
1706  } elseif ( $state === 'bi' ) {
1707  $output .= '</i></b><i>';
1708  $state = 'i';
1709  } elseif ( $state === 'ib' ) {
1710  $output .= '</b>';
1711  $state = 'i';
1712  } elseif ( $state === 'both' ) {
1713  $output .= '<i><b>' . $buffer . '</b>';
1714  $state = 'i';
1715  } else { // $state can be 'i' or ''
1716  $output .= '<b>';
1717  $state .= 'b';
1718  }
1719  } elseif ( $thislen == 5 ) {
1720  if ( $state === 'b' ) {
1721  $output .= '</b><i>';
1722  $state = 'i';
1723  } elseif ( $state === 'i' ) {
1724  $output .= '</i><b>';
1725  $state = 'b';
1726  } elseif ( $state === 'bi' ) {
1727  $output .= '</i></b>';
1728  $state = '';
1729  } elseif ( $state === 'ib' ) {
1730  $output .= '</b></i>';
1731  $state = '';
1732  } elseif ( $state === 'both' ) {
1733  $output .= '<i><b>' . $buffer . '</b></i>';
1734  $state = '';
1735  } else { // ($state == '')
1736  $buffer = '';
1737  $state = 'both';
1738  }
1739  }
1740  }
1741  $i++;
1742  }
1743  // Now close all remaining tags. Notice that the order is important.
1744  if ( $state === 'b' || $state === 'ib' ) {
1745  $output .= '</b>';
1746  }
1747  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1748  $output .= '</i>';
1749  }
1750  if ( $state === 'bi' ) {
1751  $output .= '</b>';
1752  }
1753  // There might be lonely ''''', so make sure we have a buffer
1754  if ( $state === 'both' && $buffer ) {
1755  $output .= '<b><i>' . $buffer . '</i></b>';
1756  }
1757  return $output;
1758  }
1759 
1773  public function replaceExternalLinks( $text ) {
1774 
1775  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1776  if ( $bits === false ) {
1777  throw new MWException( "PCRE needs to be compiled with "
1778  . "--enable-unicode-properties in order for MediaWiki to function" );
1779  }
1780  $s = array_shift( $bits );
1781 
1782  $i = 0;
1783  while ( $i < count( $bits ) ) {
1784  $url = $bits[$i++];
1785  $i++; // protocol
1786  $text = $bits[$i++];
1787  $trail = $bits[$i++];
1788 
1789  # The characters '<' and '>' (which were escaped by
1790  # removeHTMLtags()) should not be included in
1791  # URLs, per RFC 2396.
1792  $m2 = [];
1793  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1794  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1795  $url = substr( $url, 0, $m2[0][1] );
1796  }
1797 
1798  # If the link text is an image URL, replace it with an <img> tag
1799  # This happened by accident in the original parser, but some people used it extensively
1800  $img = $this->maybeMakeExternalImage( $text );
1801  if ( $img !== false ) {
1802  $text = $img;
1803  }
1804 
1805  $dtrail = '';
1806 
1807  # Set linktype for CSS - if URL==text, link is essentially free
1808  $linktype = ( $text === $url ) ? 'free' : 'text';
1809 
1810  # No link text, e.g. [http://domain.tld/some.link]
1811  if ( $text == '' ) {
1812  # Autonumber
1813  $langObj = $this->getTargetLanguage();
1814  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1815  $linktype = 'autonumber';
1816  } else {
1817  # Have link text, e.g. [http://domain.tld/some.link text]s
1818  # Check for trail
1819  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1820  }
1821 
1822  $text = $this->getConverterLanguage()->markNoConversion( $text );
1823 
1824  $url = Sanitizer::cleanUrl( $url );
1825 
1826  # Use the encoded URL
1827  # This means that users can paste URLs directly into the text
1828  # Funny characters like ö aren't valid in URLs anyway
1829  # This was changed in August 2004
1830  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1831  $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1832 
1833  # Register link in the output object.
1834  # Replace unnecessary URL escape codes with the referenced character
1835  # This prevents spammers from hiding links from the filters
1836  $pasteurized = self::normalizeLinkUrl( $url );
1837  $this->mOutput->addExternalLink( $pasteurized );
1838  }
1839 
1840  return $s;
1841  }
1842 
1852  public static function getExternalLinkRel( $url = false, $title = null ) {
1854  $ns = $title ? $title->getNamespace() : false;
1855  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1856  && !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
1857  ) {
1858  return 'nofollow';
1859  }
1860  return null;
1861  }
1862 
1873  public function getExternalLinkAttribs( $url = false ) {
1874  $attribs = [];
1875  $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle );
1876 
1877  if ( $this->mOptions->getExternalLinkTarget() ) {
1878  $attribs['target'] = $this->mOptions->getExternalLinkTarget();
1879  }
1880  return $attribs;
1881  }
1882 
1890  public static function replaceUnusualEscapes( $url ) {
1891  wfDeprecated( __METHOD__, '1.24' );
1892  return self::normalizeLinkUrl( $url );
1893  }
1894 
1904  public static function normalizeLinkUrl( $url ) {
1905  # First, make sure unsafe characters are encoded
1906  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1907  function ( $m ) {
1908  return rawurlencode( $m[0] );
1909  },
1910  $url
1911  );
1912 
1913  $ret = '';
1914  $end = strlen( $url );
1915 
1916  # Fragment part - 'fragment'
1917  $start = strpos( $url, '#' );
1918  if ( $start !== false && $start < $end ) {
1919  $ret = self::normalizeUrlComponent(
1920  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1921  $end = $start;
1922  }
1923 
1924  # Query part - 'query' minus &=+;
1925  $start = strpos( $url, '?' );
1926  if ( $start !== false && $start < $end ) {
1927  $ret = self::normalizeUrlComponent(
1928  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1929  $end = $start;
1930  }
1931 
1932  # Scheme and path part - 'pchar'
1933  # (we assume no userinfo or encoded colons in the host)
1934  $ret = self::normalizeUrlComponent(
1935  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1936 
1937  return $ret;
1938  }
1939 
1940  private static function normalizeUrlComponent( $component, $unsafe ) {
1941  $callback = function ( $matches ) use ( $unsafe ) {
1942  $char = urldecode( $matches[0] );
1943  $ord = ord( $char );
1944  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1945  # Unescape it
1946  return $char;
1947  } else {
1948  # Leave it escaped, but use uppercase for a-f
1949  return strtoupper( $matches[0] );
1950  }
1951  };
1952  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
1953  }
1954 
1963  private function maybeMakeExternalImage( $url ) {
1964  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1965  $imagesexception = !empty( $imagesfrom );
1966  $text = false;
1967  # $imagesfrom could be either a single string or an array of strings, parse out the latter
1968  if ( $imagesexception && is_array( $imagesfrom ) ) {
1969  $imagematch = false;
1970  foreach ( $imagesfrom as $match ) {
1971  if ( strpos( $url, $match ) === 0 ) {
1972  $imagematch = true;
1973  break;
1974  }
1975  }
1976  } elseif ( $imagesexception ) {
1977  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
1978  } else {
1979  $imagematch = false;
1980  }
1981 
1982  if ( $this->mOptions->getAllowExternalImages()
1983  || ( $imagesexception && $imagematch )
1984  ) {
1985  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1986  # Image found
1987  $text = Linker::makeExternalImage( $url );
1988  }
1989  }
1990  if ( !$text && $this->mOptions->getEnableImageWhitelist()
1991  && preg_match( self::EXT_IMAGE_REGEX, $url )
1992  ) {
1993  $whitelist = explode(
1994  "\n",
1995  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
1996  );
1997 
1998  foreach ( $whitelist as $entry ) {
1999  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
2000  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
2001  continue;
2002  }
2003  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
2004  # Image matches a whitelist entry
2005  $text = Linker::makeExternalImage( $url );
2006  break;
2007  }
2008  }
2009  }
2010  return $text;
2011  }
2012 
2022  public function replaceInternalLinks( $s ) {
2023  $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2024  return $s;
2025  }
2026 
2035  public function replaceInternalLinks2( &$s ) {
2037 
2038  static $tc = false, $e1, $e1_img;
2039  # the % is needed to support urlencoded titles as well
2040  if ( !$tc ) {
2041  $tc = Title::legalChars() . '#%';
2042  # Match a link having the form [[namespace:link|alternate]]trail
2043  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2044  # Match cases where there is no "]]", which might still be images
2045  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2046  }
2047 
2048  $holders = new LinkHolderArray( $this );
2049 
2050  # split the entire text string on occurrences of [[
2051  $a = StringUtils::explode( '[[', ' ' . $s );
2052  # get the first element (all text up to first [[), and remove the space we added
2053  $s = $a->current();
2054  $a->next();
2055  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2056  $s = substr( $s, 1 );
2057 
2058  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2059  $e2 = null;
2060  if ( $useLinkPrefixExtension ) {
2061  # Match the end of a line for a word that's not followed by whitespace,
2062  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2064  $charset = $wgContLang->linkPrefixCharset();
2065  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2066  }
2067 
2068  if ( is_null( $this->mTitle ) ) {
2069  throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2070  }
2071  $nottalk = !$this->mTitle->isTalkPage();
2072 
2073  if ( $useLinkPrefixExtension ) {
2074  $m = [];
2075  if ( preg_match( $e2, $s, $m ) ) {
2076  $first_prefix = $m[2];
2077  } else {
2078  $first_prefix = false;
2079  }
2080  } else {
2081  $prefix = '';
2082  }
2083 
2084  $useSubpages = $this->areSubpagesAllowed();
2085 
2086  // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2087  # Loop for each link
2088  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2089  // @codingStandardsIgnoreEnd
2090 
2091  # Check for excessive memory usage
2092  if ( $holders->isBig() ) {
2093  # Too big
2094  # Do the existence check, replace the link holders and clear the array
2095  $holders->replace( $s );
2096  $holders->clear();
2097  }
2098 
2099  if ( $useLinkPrefixExtension ) {
2100  if ( preg_match( $e2, $s, $m ) ) {
2101  $prefix = $m[2];
2102  $s = $m[1];
2103  } else {
2104  $prefix = '';
2105  }
2106  # first link
2107  if ( $first_prefix ) {
2108  $prefix = $first_prefix;
2109  $first_prefix = false;
2110  }
2111  }
2112 
2113  $might_be_img = false;
2114 
2115  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2116  $text = $m[2];
2117  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2118  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2119  # the real problem is with the $e1 regex
2120  # See bug 1300.
2121  # Still some problems for cases where the ] is meant to be outside punctuation,
2122  # and no image is in sight. See bug 2095.
2123  if ( $text !== ''
2124  && substr( $m[3], 0, 1 ) === ']'
2125  && strpos( $text, '[' ) !== false
2126  ) {
2127  $text .= ']'; # so that replaceExternalLinks($text) works later
2128  $m[3] = substr( $m[3], 1 );
2129  }
2130  # fix up urlencoded title texts
2131  if ( strpos( $m[1], '%' ) !== false ) {
2132  # Should anchors '#' also be rejected?
2133  $m[1] = str_replace( [ '<', '>' ], [ '&lt;', '&gt;' ], rawurldecode( $m[1] ) );
2134  }
2135  $trail = $m[3];
2136  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2137  # Invalid, but might be an image with a link in its caption
2138  $might_be_img = true;
2139  $text = $m[2];
2140  if ( strpos( $m[1], '%' ) !== false ) {
2141  $m[1] = rawurldecode( $m[1] );
2142  }
2143  $trail = "";
2144  } else { # Invalid form; output directly
2145  $s .= $prefix . '[[' . $line;
2146  continue;
2147  }
2148 
2149  $origLink = $m[1];
2150 
2151  # Don't allow internal links to pages containing
2152  # PROTO: where PROTO is a valid URL protocol; these
2153  # should be external links.
2154  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2155  $s .= $prefix . '[[' . $line;
2156  continue;
2157  }
2158 
2159  # Make subpage if necessary
2160  if ( $useSubpages ) {
2161  $link = $this->maybeDoSubpageLink( $origLink, $text );
2162  } else {
2163  $link = $origLink;
2164  }
2165 
2166  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2167  if ( !$noforce ) {
2168  # Strip off leading ':'
2169  $link = substr( $link, 1 );
2170  }
2171 
2172  $unstrip = $this->mStripState->unstripNoWiki( $link );
2173  $nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2174  if ( $nt === null ) {
2175  $s .= $prefix . '[[' . $line;
2176  continue;
2177  }
2178 
2179  $ns = $nt->getNamespace();
2180  $iw = $nt->getInterwiki();
2181 
2182  if ( $might_be_img ) { # if this is actually an invalid link
2183  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2184  $found = false;
2185  while ( true ) {
2186  # look at the next 'line' to see if we can close it there
2187  $a->next();
2188  $next_line = $a->current();
2189  if ( $next_line === false || $next_line === null ) {
2190  break;
2191  }
2192  $m = explode( ']]', $next_line, 3 );
2193  if ( count( $m ) == 3 ) {
2194  # the first ]] closes the inner link, the second the image
2195  $found = true;
2196  $text .= "[[{$m[0]}]]{$m[1]}";
2197  $trail = $m[2];
2198  break;
2199  } elseif ( count( $m ) == 2 ) {
2200  # if there's exactly one ]] that's fine, we'll keep looking
2201  $text .= "[[{$m[0]}]]{$m[1]}";
2202  } else {
2203  # if $next_line is invalid too, we need look no further
2204  $text .= '[[' . $next_line;
2205  break;
2206  }
2207  }
2208  if ( !$found ) {
2209  # we couldn't find the end of this imageLink, so output it raw
2210  # but don't ignore what might be perfectly normal links in the text we've examined
2211  $holders->merge( $this->replaceInternalLinks2( $text ) );
2212  $s .= "{$prefix}[[$link|$text";
2213  # note: no $trail, because without an end, there *is* no trail
2214  continue;
2215  }
2216  } else { # it's not an image, so output it raw
2217  $s .= "{$prefix}[[$link|$text";
2218  # note: no $trail, because without an end, there *is* no trail
2219  continue;
2220  }
2221  }
2222 
2223  $wasblank = ( $text == '' );
2224  if ( $wasblank ) {
2225  $text = $link;
2226  } else {
2227  # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2228  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2229  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2230  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2231  $text = $this->doQuotes( $text );
2232  }
2233 
2234  # Link not escaped by : , create the various objects
2235  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2236  # Interwikis
2237  if (
2238  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2239  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2240  in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2241  )
2242  ) {
2243  # Bug 24502: filter duplicates
2244  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2245  $this->mLangLinkLanguages[$iw] = true;
2246  $this->mOutput->addLanguageLink( $nt->getFullText() );
2247  }
2248 
2249  $s = rtrim( $s . $prefix );
2250  $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2251  continue;
2252  }
2253 
2254  if ( $ns == NS_FILE ) {
2255  if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2256  if ( $wasblank ) {
2257  # if no parameters were passed, $text
2258  # becomes something like "File:Foo.png",
2259  # which we don't want to pass on to the
2260  # image generator
2261  $text = '';
2262  } else {
2263  # recursively parse links inside the image caption
2264  # actually, this will parse them in any other parameters, too,
2265  # but it might be hard to fix that, and it doesn't matter ATM
2266  $text = $this->replaceExternalLinks( $text );
2267  $holders->merge( $this->replaceInternalLinks2( $text ) );
2268  }
2269  # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2270  $s .= $prefix . $this->armorLinks(
2271  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2272  } else {
2273  $s .= $prefix . $trail;
2274  }
2275  continue;
2276  }
2277 
2278  if ( $ns == NS_CATEGORY ) {
2279  $s = rtrim( $s . "\n" ); # bug 87
2280 
2281  if ( $wasblank ) {
2282  $sortkey = $this->getDefaultSort();
2283  } else {
2284  $sortkey = $text;
2285  }
2286  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2287  $sortkey = str_replace( "\n", '', $sortkey );
2288  $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2289  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2290 
2294  $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2295 
2296  continue;
2297  }
2298  }
2299 
2300  # Self-link checking. For some languages, variants of the title are checked in
2301  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2302  # for linking to a different variant.
2303  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2304  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2305  continue;
2306  }
2307 
2308  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2309  # @todo FIXME: Should do batch file existence checks, see comment below
2310  if ( $ns == NS_MEDIA ) {
2311  # Give extensions a chance to select the file revision for us
2312  $options = [];
2313  $descQuery = false;
2314  Hooks::run( 'BeforeParserFetchFileAndTitle',
2315  [ $this, $nt, &$options, &$descQuery ] );
2316  # Fetch and register the file (file title may be different via hooks)
2317  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2318  # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2319  $s .= $prefix . $this->armorLinks(
2320  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2321  continue;
2322  }
2323 
2324  # Some titles, such as valid special pages or files in foreign repos, should
2325  # be shown as bluelinks even though they're not included in the page table
2326  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2327  # batch file existence checks for NS_FILE and NS_MEDIA
2328  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2329  $this->mOutput->addLink( $nt );
2330  $s .= $this->makeKnownLinkHolder( $nt, $text, [], $trail, $prefix );
2331  } else {
2332  # Links will be added to the output link list after checking
2333  $s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix );
2334  }
2335  }
2336  return $holders;
2337  }
2338 
2353  public function makeKnownLinkHolder( $nt, $text = '', $query = [], $trail = '', $prefix = '' ) {
2354  list( $inside, $trail ) = Linker::splitTrail( $trail );
2355 
2356  if ( is_string( $query ) ) {
2357  $query = wfCgiToArray( $query );
2358  }
2359  if ( $text == '' ) {
2360  $text = htmlspecialchars( $nt->getPrefixedText() );
2361  }
2362 
2363  $link = Linker::linkKnown( $nt, "$prefix$text$inside", [], $query );
2364 
2365  return $this->armorLinks( $link ) . $trail;
2366  }
2367 
2378  public function armorLinks( $text ) {
2379  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2380  self::MARKER_PREFIX . "NOPARSE$1", $text );
2381  }
2382 
2387  public function areSubpagesAllowed() {
2388  # Some namespaces don't allow subpages
2389  return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2390  }
2391 
2400  public function maybeDoSubpageLink( $target, &$text ) {
2401  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2402  }
2403 
2410  public function closeParagraph() {
2411  $result = '';
2412  if ( $this->mLastSection != '' ) {
2413  $result = '</' . $this->mLastSection . ">\n";
2414  }
2415  $this->mInPre = false;
2416  $this->mLastSection = '';
2417  return $result;
2418  }
2419 
2430  public function getCommon( $st1, $st2 ) {
2431  $fl = strlen( $st1 );
2432  $shorter = strlen( $st2 );
2433  if ( $fl < $shorter ) {
2434  $shorter = $fl;
2435  }
2436 
2437  for ( $i = 0; $i < $shorter; ++$i ) {
2438  if ( $st1[$i] != $st2[$i] ) {
2439  break;
2440  }
2441  }
2442  return $i;
2443  }
2444 
2454  public function openList( $char ) {
2455  $result = $this->closeParagraph();
2456 
2457  if ( '*' === $char ) {
2458  $result .= "<ul><li>";
2459  } elseif ( '#' === $char ) {
2460  $result .= "<ol><li>";
2461  } elseif ( ':' === $char ) {
2462  $result .= "<dl><dd>";
2463  } elseif ( ';' === $char ) {
2464  $result .= "<dl><dt>";
2465  $this->mDTopen = true;
2466  } else {
2467  $result = '<!-- ERR 1 -->';
2468  }
2469 
2470  return $result;
2471  }
2472 
2480  public function nextItem( $char ) {
2481  if ( '*' === $char || '#' === $char ) {
2482  return "</li>\n<li>";
2483  } elseif ( ':' === $char || ';' === $char ) {
2484  $close = "</dd>\n";
2485  if ( $this->mDTopen ) {
2486  $close = "</dt>\n";
2487  }
2488  if ( ';' === $char ) {
2489  $this->mDTopen = true;
2490  return $close . '<dt>';
2491  } else {
2492  $this->mDTopen = false;
2493  return $close . '<dd>';
2494  }
2495  }
2496  return '<!-- ERR 2 -->';
2497  }
2498 
2506  public function closeList( $char ) {
2507  if ( '*' === $char ) {
2508  $text = "</li></ul>";
2509  } elseif ( '#' === $char ) {
2510  $text = "</li></ol>";
2511  } elseif ( ':' === $char ) {
2512  if ( $this->mDTopen ) {
2513  $this->mDTopen = false;
2514  $text = "</dt></dl>";
2515  } else {
2516  $text = "</dd></dl>";
2517  }
2518  } else {
2519  return '<!-- ERR 3 -->';
2520  }
2521  return $text;
2522  }
2533  public function doBlockLevels( $text, $linestart ) {
2534 
2535  # Parsing through the text line by line. The main thing
2536  # happening here is handling of block-level elements p, pre,
2537  # and making lists from lines starting with * # : etc.
2538  $textLines = StringUtils::explode( "\n", $text );
2539 
2540  $lastPrefix = $output = '';
2541  $this->mDTopen = $inBlockElem = false;
2542  $prefixLength = 0;
2543  $paragraphStack = false;
2544  $inBlockquote = false;
2545 
2546  foreach ( $textLines as $oLine ) {
2547  # Fix up $linestart
2548  if ( !$linestart ) {
2549  $output .= $oLine;
2550  $linestart = true;
2551  continue;
2552  }
2553  # * = ul
2554  # # = ol
2555  # ; = dt
2556  # : = dd
2557 
2558  $lastPrefixLength = strlen( $lastPrefix );
2559  $preCloseMatch = preg_match( '/<\\/pre/i', $oLine );
2560  $preOpenMatch = preg_match( '/<pre/i', $oLine );
2561  # If not in a <pre> element, scan for and figure out what prefixes are there.
2562  if ( !$this->mInPre ) {
2563  # Multiple prefixes may abut each other for nested lists.
2564  $prefixLength = strspn( $oLine, '*#:;' );
2565  $prefix = substr( $oLine, 0, $prefixLength );
2566 
2567  # eh?
2568  # ; and : are both from definition-lists, so they're equivalent
2569  # for the purposes of determining whether or not we need to open/close
2570  # elements.
2571  $prefix2 = str_replace( ';', ':', $prefix );
2572  $t = substr( $oLine, $prefixLength );
2573  $this->mInPre = (bool)$preOpenMatch;
2574  } else {
2575  # Don't interpret any other prefixes in preformatted text
2576  $prefixLength = 0;
2577  $prefix = $prefix2 = '';
2578  $t = $oLine;
2579  }
2580 
2581  # List generation
2582  if ( $prefixLength && $lastPrefix === $prefix2 ) {
2583  # Same as the last item, so no need to deal with nesting or opening stuff
2584  $output .= $this->nextItem( substr( $prefix, -1 ) );
2585  $paragraphStack = false;
2586 
2587  if ( substr( $prefix, -1 ) === ';' ) {
2588  # The one nasty exception: definition lists work like this:
2589  # ; title : definition text
2590  # So we check for : in the remainder text to split up the
2591  # title and definition, without b0rking links.
2592  $term = $t2 = '';
2593  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2594  $t = $t2;
2595  $output .= $term . $this->nextItem( ':' );
2596  }
2597  }
2598  } elseif ( $prefixLength || $lastPrefixLength ) {
2599  # We need to open or close prefixes, or both.
2600 
2601  # Either open or close a level...
2602  $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
2603  $paragraphStack = false;
2604 
2605  # Close all the prefixes which aren't shared.
2606  while ( $commonPrefixLength < $lastPrefixLength ) {
2607  $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
2608  --$lastPrefixLength;
2609  }
2610 
2611  # Continue the current prefix if appropriate.
2612  if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
2613  $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
2614  }
2615 
2616  # Open prefixes where appropriate.
2617  if ( $lastPrefix && $prefixLength > $commonPrefixLength ) {
2618  $output .= "\n";
2619  }
2620  while ( $prefixLength > $commonPrefixLength ) {
2621  $char = substr( $prefix, $commonPrefixLength, 1 );
2622  $output .= $this->openList( $char );
2623 
2624  if ( ';' === $char ) {
2625  # @todo FIXME: This is dupe of code above
2626  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2627  $t = $t2;
2628  $output .= $term . $this->nextItem( ':' );
2629  }
2630  }
2631  ++$commonPrefixLength;
2632  }
2633  if ( !$prefixLength && $lastPrefix ) {
2634  $output .= "\n";
2635  }
2636  $lastPrefix = $prefix2;
2637  }
2638 
2639  # If we have no prefixes, go to paragraph mode.
2640  if ( 0 == $prefixLength ) {
2641  # No prefix (not in list)--go to paragraph mode
2642  # XXX: use a stack for nestable elements like span, table and div
2643  $openmatch = preg_match(
2644  '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|'
2645  . '<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
2646  $t
2647  );
2648  $closematch = preg_match(
2649  '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'
2650  . '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|'
2651  . self::MARKER_PREFIX
2652  . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
2653  $t
2654  );
2655 
2656  if ( $openmatch || $closematch ) {
2657  $paragraphStack = false;
2658  # @todo bug 5718: paragraph closed
2659  $output .= $this->closeParagraph();
2660  if ( $preOpenMatch && !$preCloseMatch ) {
2661  $this->mInPre = true;
2662  }
2663  $bqOffset = 0;
2664  while ( preg_match( '/<(\\/?)blockquote[\s>]/i', $t,
2665  $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset )
2666  ) {
2667  $inBlockquote = !$bqMatch[1][0]; // is this a close tag?
2668  $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
2669  }
2670  $inBlockElem = !$closematch;
2671  } elseif ( !$inBlockElem && !$this->mInPre ) {
2672  if ( ' ' == substr( $t, 0, 1 )
2673  && ( $this->mLastSection === 'pre' || trim( $t ) != '' )
2674  && !$inBlockquote
2675  ) {
2676  # pre
2677  if ( $this->mLastSection !== 'pre' ) {
2678  $paragraphStack = false;
2679  $output .= $this->closeParagraph() . '<pre>';
2680  $this->mLastSection = 'pre';
2681  }
2682  $t = substr( $t, 1 );
2683  } else {
2684  # paragraph
2685  if ( trim( $t ) === '' ) {
2686  if ( $paragraphStack ) {
2687  $output .= $paragraphStack . '<br />';
2688  $paragraphStack = false;
2689  $this->mLastSection = 'p';
2690  } else {
2691  if ( $this->mLastSection !== 'p' ) {
2692  $output .= $this->closeParagraph();
2693  $this->mLastSection = '';
2694  $paragraphStack = '<p>';
2695  } else {
2696  $paragraphStack = '</p><p>';
2697  }
2698  }
2699  } else {
2700  if ( $paragraphStack ) {
2701  $output .= $paragraphStack;
2702  $paragraphStack = false;
2703  $this->mLastSection = 'p';
2704  } elseif ( $this->mLastSection !== 'p' ) {
2705  $output .= $this->closeParagraph() . '<p>';
2706  $this->mLastSection = 'p';
2707  }
2708  }
2709  }
2710  }
2711  }
2712  # somewhere above we forget to get out of pre block (bug 785)
2713  if ( $preCloseMatch && $this->mInPre ) {
2714  $this->mInPre = false;
2715  }
2716  if ( $paragraphStack === false ) {
2717  $output .= $t;
2718  if ( $prefixLength === 0 ) {
2719  $output .= "\n";
2720  }
2721  }
2722  }
2723  while ( $prefixLength ) {
2724  $output .= $this->closeList( $prefix2[$prefixLength - 1] );
2725  --$prefixLength;
2726  if ( !$prefixLength ) {
2727  $output .= "\n";
2728  }
2729  }
2730  if ( $this->mLastSection != '' ) {
2731  $output .= '</' . $this->mLastSection . '>';
2732  $this->mLastSection = '';
2733  }
2734 
2735  return $output;
2736  }
2737 
2748  public function findColonNoLinks( $str, &$before, &$after ) {
2749 
2750  $pos = strpos( $str, ':' );
2751  if ( $pos === false ) {
2752  # Nothing to find!
2753  return false;
2754  }
2755 
2756  $lt = strpos( $str, '<' );
2757  if ( $lt === false || $lt > $pos ) {
2758  # Easy; no tag nesting to worry about
2759  $before = substr( $str, 0, $pos );
2760  $after = substr( $str, $pos + 1 );
2761  return $pos;
2762  }
2763 
2764  # Ugly state machine to walk through avoiding tags.
2765  $state = self::COLON_STATE_TEXT;
2766  $stack = 0;
2767  $len = strlen( $str );
2768  for ( $i = 0; $i < $len; $i++ ) {
2769  $c = $str[$i];
2770 
2771  switch ( $state ) {
2772  # (Using the number is a performance hack for common cases)
2773  case 0: # self::COLON_STATE_TEXT:
2774  switch ( $c ) {
2775  case "<":
2776  # Could be either a <start> tag or an </end> tag
2777  $state = self::COLON_STATE_TAGSTART;
2778  break;
2779  case ":":
2780  if ( $stack == 0 ) {
2781  # We found it!
2782  $before = substr( $str, 0, $i );
2783  $after = substr( $str, $i + 1 );
2784  return $i;
2785  }
2786  # Embedded in a tag; don't break it.
2787  break;
2788  default:
2789  # Skip ahead looking for something interesting
2790  $colon = strpos( $str, ':', $i );
2791  if ( $colon === false ) {
2792  # Nothing else interesting
2793  return false;
2794  }
2795  $lt = strpos( $str, '<', $i );
2796  if ( $stack === 0 ) {
2797  if ( $lt === false || $colon < $lt ) {
2798  # We found it!
2799  $before = substr( $str, 0, $colon );
2800  $after = substr( $str, $colon + 1 );
2801  return $i;
2802  }
2803  }
2804  if ( $lt === false ) {
2805  # Nothing else interesting to find; abort!
2806  # We're nested, but there's no close tags left. Abort!
2807  break 2;
2808  }
2809  # Skip ahead to next tag start
2810  $i = $lt;
2811  $state = self::COLON_STATE_TAGSTART;
2812  }
2813  break;
2814  case 1: # self::COLON_STATE_TAG:
2815  # In a <tag>
2816  switch ( $c ) {
2817  case ">":
2818  $stack++;
2819  $state = self::COLON_STATE_TEXT;
2820  break;
2821  case "/":
2822  # Slash may be followed by >?
2823  $state = self::COLON_STATE_TAGSLASH;
2824  break;
2825  default:
2826  # ignore
2827  }
2828  break;
2829  case 2: # self::COLON_STATE_TAGSTART:
2830  switch ( $c ) {
2831  case "/":
2832  $state = self::COLON_STATE_CLOSETAG;
2833  break;
2834  case "!":
2835  $state = self::COLON_STATE_COMMENT;
2836  break;
2837  case ">":
2838  # Illegal early close? This shouldn't happen D:
2839  $state = self::COLON_STATE_TEXT;
2840  break;
2841  default:
2842  $state = self::COLON_STATE_TAG;
2843  }
2844  break;
2845  case 3: # self::COLON_STATE_CLOSETAG:
2846  # In a </tag>
2847  if ( $c === ">" ) {
2848  $stack--;
2849  if ( $stack < 0 ) {
2850  wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
2851  return false;
2852  }
2853  $state = self::COLON_STATE_TEXT;
2854  }
2855  break;
2856  case self::COLON_STATE_TAGSLASH:
2857  if ( $c === ">" ) {
2858  # Yes, a self-closed tag <blah/>
2859  $state = self::COLON_STATE_TEXT;
2860  } else {
2861  # Probably we're jumping the gun, and this is an attribute
2862  $state = self::COLON_STATE_TAG;
2863  }
2864  break;
2865  case 5: # self::COLON_STATE_COMMENT:
2866  if ( $c === "-" ) {
2867  $state = self::COLON_STATE_COMMENTDASH;
2868  }
2869  break;
2870  case self::COLON_STATE_COMMENTDASH:
2871  if ( $c === "-" ) {
2872  $state = self::COLON_STATE_COMMENTDASHDASH;
2873  } else {
2874  $state = self::COLON_STATE_COMMENT;
2875  }
2876  break;
2877  case self::COLON_STATE_COMMENTDASHDASH:
2878  if ( $c === ">" ) {
2879  $state = self::COLON_STATE_TEXT;
2880  } else {
2881  $state = self::COLON_STATE_COMMENT;
2882  }
2883  break;
2884  default:
2885  throw new MWException( "State machine error in " . __METHOD__ );
2886  }
2887  }
2888  if ( $stack > 0 ) {
2889  wfDebug( __METHOD__ . ": Invalid input; not enough close tags (stack $stack, state $state)\n" );
2890  return false;
2891  }
2892  return false;
2893  }
2894 
2906  public function getVariableValue( $index, $frame = false ) {
2909 
2910  if ( is_null( $this->mTitle ) ) {
2911  // If no title set, bad things are going to happen
2912  // later. Title should always be set since this
2913  // should only be called in the middle of a parse
2914  // operation (but the unit-tests do funky stuff)
2915  throw new MWException( __METHOD__ . ' Should only be '
2916  . ' called while parsing (no title set)' );
2917  }
2918 
2923  if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) {
2924  if ( isset( $this->mVarCache[$index] ) ) {
2925  return $this->mVarCache[$index];
2926  }
2927  }
2928 
2929  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2930  Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] );
2931 
2932  $pageLang = $this->getFunctionLang();
2933 
2934  switch ( $index ) {
2935  case '!':
2936  $value = '|';
2937  break;
2938  case 'currentmonth':
2939  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2940  break;
2941  case 'currentmonth1':
2942  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2943  break;
2944  case 'currentmonthname':
2945  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2946  break;
2947  case 'currentmonthnamegen':
2948  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2949  break;
2950  case 'currentmonthabbrev':
2951  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2952  break;
2953  case 'currentday':
2954  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2955  break;
2956  case 'currentday2':
2957  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2958  break;
2959  case 'localmonth':
2960  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2961  break;
2962  case 'localmonth1':
2963  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2964  break;
2965  case 'localmonthname':
2966  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2967  break;
2968  case 'localmonthnamegen':
2969  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2970  break;
2971  case 'localmonthabbrev':
2972  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2973  break;
2974  case 'localday':
2975  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2976  break;
2977  case 'localday2':
2978  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2979  break;
2980  case 'pagename':
2981  $value = wfEscapeWikiText( $this->mTitle->getText() );
2982  break;
2983  case 'pagenamee':
2984  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2985  break;
2986  case 'fullpagename':
2987  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2988  break;
2989  case 'fullpagenamee':
2990  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2991  break;
2992  case 'subpagename':
2993  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2994  break;
2995  case 'subpagenamee':
2996  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2997  break;
2998  case 'rootpagename':
2999  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
3000  break;
3001  case 'rootpagenamee':
3002  $value = wfEscapeWikiText( wfUrlencode( str_replace(
3003  ' ',
3004  '_',
3005  $this->mTitle->getRootText()
3006  ) ) );
3007  break;
3008  case 'basepagename':
3009  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
3010  break;
3011  case 'basepagenamee':
3012  $value = wfEscapeWikiText( wfUrlencode( str_replace(
3013  ' ',
3014  '_',
3015  $this->mTitle->getBaseText()
3016  ) ) );
3017  break;
3018  case 'talkpagename':
3019  if ( $this->mTitle->canTalk() ) {
3020  $talkPage = $this->mTitle->getTalkPage();
3021  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
3022  } else {
3023  $value = '';
3024  }
3025  break;
3026  case 'talkpagenamee':
3027  if ( $this->mTitle->canTalk() ) {
3028  $talkPage = $this->mTitle->getTalkPage();
3029  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
3030  } else {
3031  $value = '';
3032  }
3033  break;
3034  case 'subjectpagename':
3035  $subjPage = $this->mTitle->getSubjectPage();
3036  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
3037  break;
3038  case 'subjectpagenamee':
3039  $subjPage = $this->mTitle->getSubjectPage();
3040  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
3041  break;
3042  case 'pageid': // requested in bug 23427
3043  $pageid = $this->getTitle()->getArticleID();
3044  if ( $pageid == 0 ) {
3045  # 0 means the page doesn't exist in the database,
3046  # which means the user is previewing a new page.
3047  # The vary-revision flag must be set, because the magic word
3048  # will have a different value once the page is saved.
3049  $this->mOutput->setFlag( 'vary-revision' );
3050  wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
3051  }
3052  $value = $pageid ? $pageid : null;
3053  break;
3054  case 'revisionid':
3055  # Let the edit saving system know we should parse the page
3056  # *after* a revision ID has been assigned.
3057  $this->mOutput->setFlag( 'vary-revision' );
3058  wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
3059  $value = $this->mRevisionId;
3060  break;
3061  case 'revisionday':
3062  # Let the edit saving system know we should parse the page
3063  # *after* a revision ID has been assigned. This is for null edits.
3064  $this->mOutput->setFlag( 'vary-revision' );
3065  wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
3066  $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
3067  break;
3068  case 'revisionday2':
3069  # Let the edit saving system know we should parse the page
3070  # *after* a revision ID has been assigned. This is for null edits.
3071  $this->mOutput->setFlag( 'vary-revision' );
3072  wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
3073  $value = substr( $this->getRevisionTimestamp(), 6, 2 );
3074  break;
3075  case 'revisionmonth':
3076  # Let the edit saving system know we should parse the page
3077  # *after* a revision ID has been assigned. This is for null edits.
3078  $this->mOutput->setFlag( 'vary-revision' );
3079  wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
3080  $value = substr( $this->getRevisionTimestamp(), 4, 2 );
3081  break;
3082  case 'revisionmonth1':
3083  # Let the edit saving system know we should parse the page
3084  # *after* a revision ID has been assigned. This is for null edits.
3085  $this->mOutput->setFlag( 'vary-revision' );
3086  wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
3087  $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
3088  break;
3089  case 'revisionyear':
3090  # Let the edit saving system know we should parse the page
3091  # *after* a revision ID has been assigned. This is for null edits.
3092  $this->mOutput->setFlag( 'vary-revision' );
3093  wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
3094  $value = substr( $this->getRevisionTimestamp(), 0, 4 );
3095  break;
3096  case 'revisiontimestamp':
3097  # Let the edit saving system know we should parse the page
3098  # *after* a revision ID has been assigned. This is for null edits.
3099  $this->mOutput->setFlag( 'vary-revision' );
3100  wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
3101  $value = $this->getRevisionTimestamp();
3102  break;
3103  case 'revisionuser':
3104  # Let the edit saving system know we should parse the page
3105  # *after* a revision ID has been assigned. This is for null edits.
3106  $this->mOutput->setFlag( 'vary-revision' );
3107  wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
3108  $value = $this->getRevisionUser();
3109  break;
3110  case 'revisionsize':
3111  # Let the edit saving system know we should parse the page
3112  # *after* a revision ID has been assigned. This is for null edits.
3113  $this->mOutput->setFlag( 'vary-revision' );
3114  wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
3115  $value = $this->getRevisionSize();
3116  break;
3117  case 'namespace':
3118  $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3119  break;
3120  case 'namespacee':
3121  $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3122  break;
3123  case 'namespacenumber':
3124  $value = $this->mTitle->getNamespace();
3125  break;
3126  case 'talkspace':
3127  $value = $this->mTitle->canTalk()
3128  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
3129  : '';
3130  break;
3131  case 'talkspacee':
3132  $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
3133  break;
3134  case 'subjectspace':
3135  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
3136  break;
3137  case 'subjectspacee':
3138  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3139  break;
3140  case 'currentdayname':
3141  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
3142  break;
3143  case 'currentyear':
3144  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
3145  break;
3146  case 'currenttime':
3147  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
3148  break;
3149  case 'currenthour':
3150  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
3151  break;
3152  case 'currentweek':
3153  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3154  # int to remove the padding
3155  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
3156  break;
3157  case 'currentdow':
3158  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
3159  break;
3160  case 'localdayname':
3161  $value = $pageLang->getWeekdayName(
3162  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
3163  );
3164  break;
3165  case 'localyear':
3166  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
3167  break;
3168  case 'localtime':
3169  $value = $pageLang->time(
3170  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
3171  false,
3172  false
3173  );
3174  break;
3175  case 'localhour':
3176  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
3177  break;
3178  case 'localweek':
3179  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3180  # int to remove the padding
3181  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
3182  break;
3183  case 'localdow':
3184  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
3185  break;
3186  case 'numberofarticles':
3187  $value = $pageLang->formatNum( SiteStats::articles() );
3188  break;
3189  case 'numberoffiles':
3190  $value = $pageLang->formatNum( SiteStats::images() );
3191  break;
3192  case 'numberofusers':
3193  $value = $pageLang->formatNum( SiteStats::users() );
3194  break;
3195  case 'numberofactiveusers':
3196  $value = $pageLang->formatNum( SiteStats::activeUsers() );
3197  break;
3198  case 'numberofpages':
3199  $value = $pageLang->formatNum( SiteStats::pages() );
3200  break;
3201  case 'numberofadmins':
3202  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
3203  break;
3204  case 'numberofedits':
3205  $value = $pageLang->formatNum( SiteStats::edits() );
3206  break;
3207  case 'currenttimestamp':
3208  $value = wfTimestamp( TS_MW, $ts );
3209  break;
3210  case 'localtimestamp':
3211  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
3212  break;
3213  case 'currentversion':
3215  break;
3216  case 'articlepath':
3217  return $wgArticlePath;
3218  case 'sitename':
3219  return $wgSitename;
3220  case 'server':
3221  return $wgServer;
3222  case 'servername':
3223  return $wgServerName;
3224  case 'scriptpath':
3225  return $wgScriptPath;
3226  case 'stylepath':
3227  return $wgStylePath;
3228  case 'directionmark':
3229  return $pageLang->getDirMark();
3230  case 'contentlanguage':
3232  return $wgLanguageCode;
3233  case 'cascadingsources':
3235  break;
3236  default:
3237  $ret = null;
3238  Hooks::run(
3239  'ParserGetVariableValueSwitch',
3240  [ &$this, &$this->mVarCache, &$index, &$ret, &$frame ]
3241  );
3242 
3243  return $ret;
3244  }
3245 
3246  if ( $index ) {
3247  $this->mVarCache[$index] = $value;
3248  }
3249 
3250  return $value;
3251  }
3252 
3258  public function initialiseVariables() {
3259  $variableIDs = MagicWord::getVariableIDs();
3260  $substIDs = MagicWord::getSubstIDs();
3261 
3262  $this->mVariables = new MagicWordArray( $variableIDs );
3263  $this->mSubstWords = new MagicWordArray( $substIDs );
3264  }
3265 
3288  public function preprocessToDom( $text, $flags = 0 ) {
3289  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3290  return $dom;
3291  }
3292 
3300  public static function splitWhitespace( $s ) {
3301  $ltrimmed = ltrim( $s );
3302  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3303  $trimmed = rtrim( $ltrimmed );
3304  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3305  if ( $diff > 0 ) {
3306  $w2 = substr( $ltrimmed, -$diff );
3307  } else {
3308  $w2 = '';
3309  }
3310  return [ $w1, $trimmed, $w2 ];
3311  }
3312 
3333  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3334  # Is there any text? Also, Prevent too big inclusions!
3335  $textSize = strlen( $text );
3336  if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) {
3337  return $text;
3338  }
3339 
3340  if ( $frame === false ) {
3341  $frame = $this->getPreprocessor()->newFrame();
3342  } elseif ( !( $frame instanceof PPFrame ) ) {
3343  wfDebug( __METHOD__ . " called using plain parameters instead of "
3344  . "a PPFrame instance. Creating custom frame.\n" );
3345  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3346  }
3347 
3348  $dom = $this->preprocessToDom( $text );
3349  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3350  $text = $frame->expand( $dom, $flags );
3351 
3352  return $text;
3353  }
3354 
3362  public static function createAssocArgs( $args ) {
3363  $assocArgs = [];
3364  $index = 1;
3365  foreach ( $args as $arg ) {
3366  $eqpos = strpos( $arg, '=' );
3367  if ( $eqpos === false ) {
3368  $assocArgs[$index++] = $arg;
3369  } else {
3370  $name = trim( substr( $arg, 0, $eqpos ) );
3371  $value = trim( substr( $arg, $eqpos + 1 ) );
3372  if ( $value === false ) {
3373  $value = '';
3374  }
3375  if ( $name !== false ) {
3376  $assocArgs[$name] = $value;
3377  }
3378  }
3379  }
3380 
3381  return $assocArgs;
3382  }
3383 
3410  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3411  # does no harm if $current and $max are present but are unnecessary for the message
3412  # Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown
3413  # only during preview, and that would split the parser cache unnecessarily.
3414  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3415  ->text();
3416  $this->mOutput->addWarning( $warning );
3417  $this->addTrackingCategory( "$limitationType-category" );
3418  }
3419 
3432  public function braceSubstitution( $piece, $frame ) {
3433 
3434  // Flags
3435 
3436  // $text has been filled
3437  $found = false;
3438  // wiki markup in $text should be escaped
3439  $nowiki = false;
3440  // $text is HTML, armour it against wikitext transformation
3441  $isHTML = false;
3442  // Force interwiki transclusion to be done in raw mode not rendered
3443  $forceRawInterwiki = false;
3444  // $text is a DOM node needing expansion in a child frame
3445  $isChildObj = false;
3446  // $text is a DOM node needing expansion in the current frame
3447  $isLocalObj = false;
3448 
3449  # Title object, where $text came from
3450  $title = false;
3451 
3452  # $part1 is the bit before the first |, and must contain only title characters.
3453  # Various prefixes will be stripped from it later.
3454  $titleWithSpaces = $frame->expand( $piece['title'] );
3455  $part1 = trim( $titleWithSpaces );
3456  $titleText = false;
3457 
3458  # Original title text preserved for various purposes
3459  $originalTitle = $part1;
3460 
3461  # $args is a list of argument nodes, starting from index 0, not including $part1
3462  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3463  # below won't work b/c this $args isn't an object
3464  $args = ( null == $piece['parts'] ) ? [] : $piece['parts'];
3465 
3466  $profileSection = null; // profile templates
3467 
3468  # SUBST
3469  if ( !$found ) {
3470  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3471 
3472  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3473  # Decide whether to expand template or keep wikitext as-is.
3474  if ( $this->ot['wiki'] ) {
3475  if ( $substMatch === false ) {
3476  $literal = true; # literal when in PST with no prefix
3477  } else {
3478  $literal = false; # expand when in PST with subst: or safesubst:
3479  }
3480  } else {
3481  if ( $substMatch == 'subst' ) {
3482  $literal = true; # literal when not in PST with plain subst:
3483  } else {
3484  $literal = false; # expand when not in PST with safesubst: or no prefix
3485  }
3486  }
3487  if ( $literal ) {
3488  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3489  $isLocalObj = true;
3490  $found = true;
3491  }
3492  }
3493 
3494  # Variables
3495  if ( !$found && $args->getLength() == 0 ) {
3496  $id = $this->mVariables->matchStartToEnd( $part1 );
3497  if ( $id !== false ) {
3498  $text = $this->getVariableValue( $id, $frame );
3499  if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3500  $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3501  }
3502  $found = true;
3503  }
3504  }
3505 
3506  # MSG, MSGNW and RAW
3507  if ( !$found ) {
3508  # Check for MSGNW:
3509  $mwMsgnw = MagicWord::get( 'msgnw' );
3510  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3511  $nowiki = true;
3512  } else {
3513  # Remove obsolete MSG:
3514  $mwMsg = MagicWord::get( 'msg' );
3515  $mwMsg->matchStartAndRemove( $part1 );
3516  }
3517 
3518  # Check for RAW:
3519  $mwRaw = MagicWord::get( 'raw' );
3520  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3521  $forceRawInterwiki = true;
3522  }
3523  }
3524 
3525  # Parser functions
3526  if ( !$found ) {
3527  $colonPos = strpos( $part1, ':' );
3528  if ( $colonPos !== false ) {
3529  $func = substr( $part1, 0, $colonPos );
3530  $funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ];
3531  $argsLength = $args->getLength();
3532  for ( $i = 0; $i < $argsLength; $i++ ) {
3533  $funcArgs[] = $args->item( $i );
3534  }
3535  try {
3536  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3537  } catch ( Exception $ex ) {
3538  throw $ex;
3539  }
3540 
3541  # The interface for parser functions allows for extracting
3542  # flags into the local scope. Extract any forwarded flags
3543  # here.
3544  extract( $result );
3545  }
3546  }
3547 
3548  # Finish mangling title and then check for loops.
3549  # Set $title to a Title object and $titleText to the PDBK
3550  if ( !$found ) {
3551  $ns = NS_TEMPLATE;
3552  # Split the title into page and subpage
3553  $subpage = '';
3554  $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3555  if ( $part1 !== $relative ) {
3556  $part1 = $relative;
3557  $ns = $this->mTitle->getNamespace();
3558  }
3559  $title = Title::newFromText( $part1, $ns );
3560  if ( $title ) {
3561  $titleText = $title->getPrefixedText();
3562  # Check for language variants if the template is not found
3563  if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3564  $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3565  }
3566  # Do recursion depth check
3567  $limit = $this->mOptions->getMaxTemplateDepth();
3568  if ( $frame->depth >= $limit ) {
3569  $found = true;
3570  $text = '<span class="error">'
3571  . wfMessage( 'parser-template-recursion-depth-warning' )
3572  ->numParams( $limit )->inContentLanguage()->text()
3573  . '</span>';
3574  }
3575  }
3576  }
3577 
3578  # Load from database
3579  if ( !$found && $title ) {
3580  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3581  if ( !$title->isExternal() ) {
3582  if ( $title->isSpecialPage()
3583  && $this->mOptions->getAllowSpecialInclusion()
3584  && $this->ot['html']
3585  ) {
3586  // Pass the template arguments as URL parameters.
3587  // "uselang" will have no effect since the Language object
3588  // is forced to the one defined in ParserOptions.
3589  $pageArgs = [];
3590  $argsLength = $args->getLength();
3591  for ( $i = 0; $i < $argsLength; $i++ ) {
3592  $bits = $args->item( $i )->splitArg();
3593  if ( strval( $bits['index'] ) === '' ) {
3594  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3595  $value = trim( $frame->expand( $bits['value'] ) );
3596  $pageArgs[$name] = $value;
3597  }
3598  }
3599 
3600  // Create a new context to execute the special page
3601  $context = new RequestContext;
3602  $context->setTitle( $title );
3603  $context->setRequest( new FauxRequest( $pageArgs ) );
3604  $context->setUser( $this->getUser() );
3605  $context->setLanguage( $this->mOptions->getUserLangObj() );
3607  if ( $ret ) {
3608  $text = $context->getOutput()->getHTML();
3609  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3610  $found = true;
3611  $isHTML = true;
3612  $this->disableCache();
3613  }
3614  } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3615  $found = false; # access denied
3616  wfDebug( __METHOD__ . ": template inclusion denied for " .
3617  $title->getPrefixedDBkey() . "\n" );
3618  } else {
3619  list( $text, $title ) = $this->getTemplateDom( $title );
3620  if ( $text !== false ) {
3621  $found = true;
3622  $isChildObj = true;
3623  }
3624  }
3625 
3626  # If the title is valid but undisplayable, make a link to it
3627  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3628  $text = "[[:$titleText]]";
3629  $found = true;
3630  }
3631  } elseif ( $title->isTrans() ) {
3632  # Interwiki transclusion
3633  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3634  $text = $this->interwikiTransclude( $title, 'render' );
3635  $isHTML = true;
3636  } else {
3637  $text = $this->interwikiTransclude( $title, 'raw' );
3638  # Preprocess it like a template
3639  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3640  $isChildObj = true;
3641  }
3642  $found = true;
3643  }
3644 
3645  # Do infinite loop check
3646  # This has to be done after redirect resolution to avoid infinite loops via redirects
3647  if ( !$frame->loopCheck( $title ) ) {
3648  $found = true;
3649  $text = '<span class="error">'
3650  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3651  . '</span>';
3652  wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3653  }
3654  }
3655 
3656  # If we haven't found text to substitute by now, we're done
3657  # Recover the source wikitext and return it
3658  if ( !$found ) {
3659  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3660  if ( $profileSection ) {
3661  $this->mProfiler->scopedProfileOut( $profileSection );
3662  }
3663  return [ 'object' => $text ];
3664  }
3665 
3666  # Expand DOM-style return values in a child frame
3667  if ( $isChildObj ) {
3668  # Clean up argument array
3669  $newFrame = $frame->newChild( $args, $title );
3670 
3671  if ( $nowiki ) {
3672  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3673  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3674  # Expansion is eligible for the empty-frame cache
3675  $text = $newFrame->cachedExpand( $titleText, $text );
3676  } else {
3677  # Uncached expansion
3678  $text = $newFrame->expand( $text );
3679  }
3680  }
3681  if ( $isLocalObj && $nowiki ) {
3682  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3683  $isLocalObj = false;
3684  }
3685 
3686  if ( $profileSection ) {
3687  $this->mProfiler->scopedProfileOut( $profileSection );
3688  }
3689 
3690  # Replace raw HTML by a placeholder
3691  if ( $isHTML ) {
3692  $text = $this->insertStripItem( $text );
3693  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3694  # Escape nowiki-style return values
3695  $text = wfEscapeWikiText( $text );
3696  } elseif ( is_string( $text )
3697  && !$piece['lineStart']
3698  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3699  ) {
3700  # Bug 529: if the template begins with a table or block-level
3701  # element, it should be treated as beginning a new line.
3702  # This behavior is somewhat controversial.
3703  $text = "\n" . $text;
3704  }
3705 
3706  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3707  # Error, oversize inclusion
3708  if ( $titleText !== false ) {
3709  # Make a working, properly escaped link if possible (bug 23588)
3710  $text = "[[:$titleText]]";
3711  } else {
3712  # This will probably not be a working link, but at least it may
3713  # provide some hint of where the problem is
3714  preg_replace( '/^:/', '', $originalTitle );
3715  $text = "[[:$originalTitle]]";
3716  }
3717  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3718  . 'post-expand include size too large -->' );
3719  $this->limitationWarn( 'post-expand-template-inclusion' );
3720  }
3721 
3722  if ( $isLocalObj ) {
3723  $ret = [ 'object' => $text ];
3724  } else {
3725  $ret = [ 'text' => $text ];
3726  }
3727 
3728  return $ret;
3729  }
3730 
3750  public function callParserFunction( $frame, $function, array $args = [] ) {
3752 
3753  # Case sensitive functions
3754  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3755  $function = $this->mFunctionSynonyms[1][$function];
3756  } else {
3757  # Case insensitive functions
3758  $function = $wgContLang->lc( $function );
3759  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3760  $function = $this->mFunctionSynonyms[0][$function];
3761  } else {
3762  return [ 'found' => false ];
3763  }
3764  }
3765 
3766  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3767 
3768  # Workaround for PHP bug 35229 and similar
3769  if ( !is_callable( $callback ) ) {
3770  throw new MWException( "Tag hook for $function is not callable\n" );
3771  }
3772 
3773  $allArgs = [ &$this ];
3774  if ( $flags & self::SFH_OBJECT_ARGS ) {
3775  # Convert arguments to PPNodes and collect for appending to $allArgs
3776  $funcArgs = [];
3777  foreach ( $args as $k => $v ) {
3778  if ( $v instanceof PPNode || $k === 0 ) {
3779  $funcArgs[] = $v;
3780  } else {
3781  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 );
3782  }
3783  }
3784 
3785  # Add a frame parameter, and pass the arguments as an array
3786  $allArgs[] = $frame;
3787  $allArgs[] = $funcArgs;
3788  } else {
3789  # Convert arguments to plain text and append to $allArgs
3790  foreach ( $args as $k => $v ) {
3791  if ( $v instanceof PPNode ) {
3792  $allArgs[] = trim( $frame->expand( $v ) );
3793  } elseif ( is_int( $k ) && $k >= 0 ) {
3794  $allArgs[] = trim( $v );
3795  } else {
3796  $allArgs[] = trim( "$k=$v" );
3797  }
3798  }
3799  }
3800 
3801  $result = call_user_func_array( $callback, $allArgs );
3802 
3803  # The interface for function hooks allows them to return a wikitext
3804  # string or an array containing the string and any flags. This mungs
3805  # things around to match what this method should return.
3806  if ( !is_array( $result ) ) {
3807  $result =[
3808  'found' => true,
3809  'text' => $result,
3810  ];
3811  } else {
3812  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3813  $result['text'] = $result[0];
3814  }
3815  unset( $result[0] );
3816  $result += [
3817  'found' => true,
3818  ];
3819  }
3820 
3821  $noparse = true;
3822  $preprocessFlags = 0;
3823  if ( isset( $result['noparse'] ) ) {
3824  $noparse = $result['noparse'];
3825  }
3826  if ( isset( $result['preprocessFlags'] ) ) {
3827  $preprocessFlags = $result['preprocessFlags'];
3828  }
3829 
3830  if ( !$noparse ) {
3831  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3832  $result['isChildObj'] = true;
3833  }
3834 
3835  return $result;
3836  }
3837 
3846  public function getTemplateDom( $title ) {
3847  $cacheTitle = $title;
3848  $titleText = $title->getPrefixedDBkey();
3849 
3850  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3851  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3852  $title = Title::makeTitle( $ns, $dbk );
3853  $titleText = $title->getPrefixedDBkey();
3854  }
3855  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3856  return [ $this->mTplDomCache[$titleText], $title ];
3857  }
3858 
3859  # Cache miss, go to the database
3860  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3861 
3862  if ( $text === false ) {
3863  $this->mTplDomCache[$titleText] = false;
3864  return [ false, $title ];
3865  }
3866 
3867  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3868  $this->mTplDomCache[$titleText] = $dom;
3869 
3870  if ( !$title->equals( $cacheTitle ) ) {
3871  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3872  [ $title->getNamespace(), $cdb = $title->getDBkey() ];
3873  }
3874 
3875  return [ $dom, $title ];
3876  }
3877 
3890  $cacheKey = $title->getPrefixedDBkey();
3891  if ( !$this->currentRevisionCache ) {
3892  $this->currentRevisionCache = new MapCacheLRU( 100 );
3893  }
3894  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3895  $this->currentRevisionCache->set( $cacheKey,
3896  // Defaults to Parser::statelessFetchRevision()
3897  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3898  );
3899  }
3900  return $this->currentRevisionCache->get( $cacheKey );
3901  }
3902 
3912  public static function statelessFetchRevision( $title, $parser = false ) {
3913  return Revision::newFromTitle( $title );
3914  }
3915 
3921  public function fetchTemplateAndTitle( $title ) {
3922  // Defaults to Parser::statelessFetchTemplate()
3923  $templateCb = $this->mOptions->getTemplateCallback();
3924  $stuff = call_user_func( $templateCb, $title, $this );
3925  // We use U+007F DELETE to distinguish strip markers from regular text.
3926  $text = $stuff['text'];
3927  if ( is_string( $stuff['text'] ) ) {
3928  $text = strtr( $text, "\x7f", "?" );
3929  }
3930  $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3931  if ( isset( $stuff['deps'] ) ) {
3932  foreach ( $stuff['deps'] as $dep ) {
3933  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3934  if ( $dep['title']->equals( $this->getTitle() ) ) {
3935  // If we transclude ourselves, the final result
3936  // will change based on the new version of the page
3937  $this->mOutput->setFlag( 'vary-revision' );
3938  }
3939  }
3940  }
3941  return [ $text, $finalTitle ];
3942  }
3943 
3949  public function fetchTemplate( $title ) {
3950  return $this->fetchTemplateAndTitle( $title )[0];
3951  }
3952 
3962  public static function statelessFetchTemplate( $title, $parser = false ) {
3963  $text = $skip = false;
3964  $finalTitle = $title;
3965  $deps = [];
3966 
3967  # Loop to fetch the article, with up to 1 redirect
3968  // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
3969  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3970  // @codingStandardsIgnoreEnd
3971  # Give extensions a chance to select the revision instead
3972  $id = false; # Assume current
3973  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3974  [ $parser, $title, &$skip, &$id ] );
3975 
3976  if ( $skip ) {
3977  $text = false;
3978  $deps[] = [
3979  'title' => $title,
3980  'page_id' => $title->getArticleID(),
3981  'rev_id' => null
3982  ];
3983  break;
3984  }
3985  # Get the revision
3986  if ( $id ) {
3987  $rev = Revision::newFromId( $id );
3988  } elseif ( $parser ) {
3989  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3990  } else {
3992  }
3993  $rev_id = $rev ? $rev->getId() : 0;
3994  # If there is no current revision, there is no page
3995  if ( $id === false && !$rev ) {
3996  $linkCache = LinkCache::singleton();
3997  $linkCache->addBadLinkObj( $title );
3998  }
3999 
4000  $deps[] = [
4001  'title' => $title,
4002  'page_id' => $title->getArticleID(),
4003  'rev_id' => $rev_id ];
4004  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
4005  # We fetched a rev from a different title; register it too...
4006  $deps[] = [
4007  'title' => $rev->getTitle(),
4008  'page_id' => $rev->getPage(),
4009  'rev_id' => $rev_id ];
4010  }
4011 
4012  if ( $rev ) {
4013  $content = $rev->getContent();
4014  $text = $content ? $content->getWikitextForTransclusion() : null;
4015 
4016  if ( $text === false || $text === null ) {
4017  $text = false;
4018  break;
4019  }
4020  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
4022  $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
4023  if ( !$message->exists() ) {
4024  $text = false;
4025  break;
4026  }
4027  $content = $message->content();
4028  $text = $message->plain();
4029  } else {
4030  break;
4031  }
4032  if ( !$content ) {
4033  break;
4034  }
4035  # Redirect?
4036  $finalTitle = $title;
4037  $title = $content->getRedirectTarget();
4038  }
4039  return [
4040  'text' => $text,
4041  'finalTitle' => $finalTitle,
4042  'deps' => $deps ];
4043  }
4044 
4052  public function fetchFile( $title, $options = [] ) {
4053  return $this->fetchFileAndTitle( $title, $options )[0];
4054  }
4055 
4063  public function fetchFileAndTitle( $title, $options = [] ) {
4064  $file = $this->fetchFileNoRegister( $title, $options );
4065 
4066  $time = $file ? $file->getTimestamp() : false;
4067  $sha1 = $file ? $file->getSha1() : false;
4068  # Register the file as a dependency...
4069  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4070  if ( $file && !$title->equals( $file->getTitle() ) ) {
4071  # Update fetched file title
4072  $title = $file->getTitle();
4073  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4074  }
4075  return [ $file, $title ];
4076  }
4077 
4088  protected function fetchFileNoRegister( $title, $options = [] ) {
4089  if ( isset( $options['broken'] ) ) {
4090  $file = false; // broken thumbnail forced by hook
4091  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
4092  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
4093  } else { // get by (name,timestamp)
4094  $file = wfFindFile( $title, $options );
4095  }
4096  return $file;
4097  }
4098 
4107  public function interwikiTransclude( $title, $action ) {
4109 
4110  if ( !$wgEnableScaryTranscluding ) {
4111  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
4112  }
4113 
4114  $url = $title->getFullURL( [ 'action' => $action ] );
4115 
4116  if ( strlen( $url ) > 255 ) {
4117  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
4118  }
4119  return $this->fetchScaryTemplateMaybeFromCache( $url );
4120  }
4121 
4126  public function fetchScaryTemplateMaybeFromCache( $url ) {
4128  $dbr = wfGetDB( DB_SLAVE );
4129  $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
4130  $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
4131  [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
4132  if ( $obj ) {
4133  return $obj->tc_contents;
4134  }
4135 
4136  $req = MWHttpRequest::factory( $url, [], __METHOD__ );
4137  $status = $req->execute(); // Status object
4138  if ( $status->isOK() ) {
4139  $text = $req->getContent();
4140  } elseif ( $req->getStatus() != 200 ) {
4141  // Though we failed to fetch the content, this status is useless.
4142  return wfMessage( 'scarytranscludefailed-httpstatus' )
4143  ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
4144  } else {
4145  return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
4146  }
4147 
4148  $dbw = wfGetDB( DB_MASTER );
4149  $dbw->replace( 'transcache', [ 'tc_url' ], [
4150  'tc_url' => $url,
4151  'tc_time' => $dbw->timestamp( time() ),
4152  'tc_contents' => $text
4153  ] );
4154  return $text;
4155  }
4156 
4166  public function argSubstitution( $piece, $frame ) {
4167 
4168  $error = false;
4169  $parts = $piece['parts'];
4170  $nameWithSpaces = $frame->expand( $piece['title'] );
4171  $argName = trim( $nameWithSpaces );
4172  $object = false;
4173  $text = $frame->getArgument( $argName );
4174  if ( $text === false && $parts->getLength() > 0
4175  && ( $this->ot['html']
4176  || $this->ot['pre']
4177  || ( $this->ot['wiki'] && $frame->isTemplate() )
4178  )
4179  ) {
4180  # No match in frame, use the supplied default
4181  $object = $parts->item( 0 )->getChildren();
4182  }
4183  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
4184  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
4185  $this->limitationWarn( 'post-expand-template-argument' );
4186  }
4187 
4188  if ( $text === false && $object === false ) {
4189  # No match anywhere
4190  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
4191  }
4192  if ( $error !== false ) {
4193  $text .= $error;
4194  }
4195  if ( $object !== false ) {
4196  $ret = [ 'object' => $object ];
4197  } else {
4198  $ret = [ 'text' => $text ];
4199  }
4200 
4201  return $ret;
4202  }
4203 
4219  public function extensionSubstitution( $params, $frame ) {
4220  $name = $frame->expand( $params['name'] );
4221  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
4222  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4223  $marker = self::MARKER_PREFIX . "-$name-"
4224  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4225 
4226  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4227  ( $this->ot['html'] || $this->ot['pre'] );
4228  if ( $isFunctionTag ) {
4229  $markerType = 'none';
4230  } else {
4231  $markerType = 'general';
4232  }
4233  if ( $this->ot['html'] || $isFunctionTag ) {
4234  $name = strtolower( $name );
4235  $attributes = Sanitizer::decodeTagAttributes( $attrText );
4236  if ( isset( $params['attributes'] ) ) {
4237  $attributes = $attributes + $params['attributes'];
4238  }
4239 
4240  if ( isset( $this->mTagHooks[$name] ) ) {
4241  # Workaround for PHP bug 35229 and similar
4242  if ( !is_callable( $this->mTagHooks[$name] ) ) {
4243  throw new MWException( "Tag hook for $name is not callable\n" );
4244  }
4245  $output = call_user_func_array( $this->mTagHooks[$name],
4246  [ $content, $attributes, $this, $frame ] );
4247  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4248  list( $callback, ) = $this->mFunctionTagHooks[$name];
4249  if ( !is_callable( $callback ) ) {
4250  throw new MWException( "Tag hook for $name is not callable\n" );
4251  }
4252 
4253  $output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] );
4254  } else {
4255  $output = '<span class="error">Invalid tag extension name: ' .
4256  htmlspecialchars( $name ) . '</span>';
4257  }
4258 
4259  if ( is_array( $output ) ) {
4260  # Extract flags to local scope (to override $markerType)
4261  $flags = $output;
4262  $output = $flags[0];
4263  unset( $flags[0] );
4264  extract( $flags );
4265  }
4266  } else {
4267  if ( is_null( $attrText ) ) {
4268  $attrText = '';
4269  }
4270  if ( isset( $params['attributes'] ) ) {
4271  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4272  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4273  htmlspecialchars( $attrValue ) . '"';
4274  }
4275  }
4276  if ( $content === null ) {
4277  $output = "<$name$attrText/>";
4278  } else {
4279  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4280  $output = "<$name$attrText>$content$close";
4281  }
4282  }
4283 
4284  if ( $markerType === 'none' ) {
4285  return $output;
4286  } elseif ( $markerType === 'nowiki' ) {
4287  $this->mStripState->addNoWiki( $marker, $output );
4288  } elseif ( $markerType === 'general' ) {
4289  $this->mStripState->addGeneral( $marker, $output );
4290  } else {
4291  throw new MWException( __METHOD__ . ': invalid marker type' );
4292  }
4293  return $marker;
4294  }
4295 
4303  public function incrementIncludeSize( $type, $size ) {
4304  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4305  return false;
4306  } else {
4307  $this->mIncludeSizes[$type] += $size;
4308  return true;
4309  }
4310  }
4311 
4318  $this->mExpensiveFunctionCount++;
4319  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4320  }
4321 
4330  public function doDoubleUnderscore( $text ) {
4331 
4332  # The position of __TOC__ needs to be recorded
4333  $mw = MagicWord::get( 'toc' );
4334  if ( $mw->match( $text ) ) {
4335  $this->mShowToc = true;
4336  $this->mForceTocPosition = true;
4337 
4338  # Set a placeholder. At the end we'll fill it in with the TOC.
4339  $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
4340 
4341  # Only keep the first one.
4342  $text = $mw->replace( '', $text );
4343  }
4344 
4345  # Now match and remove the rest of them
4347  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4348 
4349  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4350  $this->mOutput->mNoGallery = true;
4351  }
4352  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4353  $this->mShowToc = false;
4354  }
4355  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4356  && $this->mTitle->getNamespace() == NS_CATEGORY
4357  ) {
4358  $this->addTrackingCategory( 'hidden-category-category' );
4359  }
4360  # (bug 8068) Allow control over whether robots index a page.
4361  # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
4362  # is not desirable, the last one on the page should win.
4363  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4364  $this->mOutput->setIndexPolicy( 'noindex' );
4365  $this->addTrackingCategory( 'noindex-category' );
4366  }
4367  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4368  $this->mOutput->setIndexPolicy( 'index' );
4369  $this->addTrackingCategory( 'index-category' );
4370  }
4371 
4372  # Cache all double underscores in the database
4373  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4374  $this->mOutput->setProperty( $key, '' );
4375  }
4376 
4377  return $text;
4378  }
4379 
4385  public function addTrackingCategory( $msg ) {
4386  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4387  }
4388 
4405  public function formatHeadings( $text, $origText, $isMain = true ) {
4406  global $wgMaxTocLevel, $wgExperimentalHtmlIds;
4407 
4408  # Inhibit editsection links if requested in the page
4409  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4410  $maybeShowEditLink = $showEditLink = false;
4411  } else {
4412  $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4413  $showEditLink = $this->mOptions->getEditSection();
4414  }
4415  if ( $showEditLink ) {
4416  $this->mOutput->setEditSectionTokens( true );
4417  }
4418 
4419  # Get all headlines for numbering them and adding funky stuff like [edit]
4420  # links - this is for later, but we need the number of headlines right now
4421  $matches = [];
4422  $numMatches = preg_match_all(
4423  '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4424  $text,
4425  $matches
4426  );
4427 
4428  # if there are fewer than 4 headlines in the article, do not show TOC
4429  # unless it's been explicitly enabled.
4430  $enoughToc = $this->mShowToc &&
4431  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4432 
4433  # Allow user to stipulate that a page should have a "new section"
4434  # link added via __NEWSECTIONLINK__
4435  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4436  $this->mOutput->setNewSection( true );
4437  }
4438 
4439  # Allow user to remove the "new section"
4440  # link via __NONEWSECTIONLINK__
4441  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4442  $this->mOutput->hideNewSection( true );
4443  }
4444 
4445  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4446  # override above conditions and always show TOC above first header
4447  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4448  $this->mShowToc = true;
4449  $enoughToc = true;
4450  }
4451 
4452  # headline counter
4453  $headlineCount = 0;
4454  $numVisible = 0;
4455 
4456  # Ugh .. the TOC should have neat indentation levels which can be
4457  # passed to the skin functions. These are determined here
4458  $toc = '';
4459  $full = '';
4460  $head = [];
4461  $sublevelCount = [];
4462  $levelCount = [];
4463  $level = 0;
4464  $prevlevel = 0;
4465  $toclevel = 0;
4466  $prevtoclevel = 0;
4467  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4468  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4469  $oldType = $this->mOutputType;
4470  $this->setOutputType( self::OT_WIKI );
4471  $frame = $this->getPreprocessor()->newFrame();
4472  $root = $this->preprocessToDom( $origText );
4473  $node = $root->getFirstChild();
4474  $byteOffset = 0;
4475  $tocraw = [];
4476  $refers = [];
4477 
4478  $headlines = $numMatches !== false ? $matches[3] : [];
4479 
4480  foreach ( $headlines as $headline ) {
4481  $isTemplate = false;
4482  $titleText = false;
4483  $sectionIndex = false;
4484  $numbering = '';
4485  $markerMatches = [];
4486  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4487  $serial = $markerMatches[1];
4488  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4489  $isTemplate = ( $titleText != $baseTitleText );
4490  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4491  }
4492 
4493  if ( $toclevel ) {
4494  $prevlevel = $level;
4495  }
4496  $level = $matches[1][$headlineCount];
4497 
4498  if ( $level > $prevlevel ) {
4499  # Increase TOC level
4500  $toclevel++;
4501  $sublevelCount[$toclevel] = 0;
4502  if ( $toclevel < $wgMaxTocLevel ) {
4503  $prevtoclevel = $toclevel;
4504  $toc .= Linker::tocIndent();
4505  $numVisible++;
4506  }
4507  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4508  # Decrease TOC level, find level to jump to
4509 
4510  for ( $i = $toclevel; $i > 0; $i-- ) {
4511  if ( $levelCount[$i] == $level ) {
4512  # Found last matching level
4513  $toclevel = $i;
4514  break;
4515  } elseif ( $levelCount[$i] < $level ) {
4516  # Found first matching level below current level
4517  $toclevel = $i + 1;
4518  break;
4519  }
4520  }
4521  if ( $i == 0 ) {
4522  $toclevel = 1;
4523  }
4524  if ( $toclevel < $wgMaxTocLevel ) {
4525  if ( $prevtoclevel < $wgMaxTocLevel ) {
4526  # Unindent only if the previous toc level was shown :p
4527  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4528  $prevtoclevel = $toclevel;
4529  } else {
4530  $toc .= Linker::tocLineEnd();
4531  }
4532  }
4533  } else {
4534  # No change in level, end TOC line
4535  if ( $toclevel < $wgMaxTocLevel ) {
4536  $toc .= Linker::tocLineEnd();
4537  }
4538  }
4539 
4540  $levelCount[$toclevel] = $level;
4541 
4542  # count number of headlines for each level
4543  $sublevelCount[$toclevel]++;
4544  $dot = 0;
4545  for ( $i = 1; $i <= $toclevel; $i++ ) {
4546  if ( !empty( $sublevelCount[$i] ) ) {
4547  if ( $dot ) {
4548  $numbering .= '.';
4549  }
4550  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4551  $dot = 1;
4552  }
4553  }
4554 
4555  # The safe header is a version of the header text safe to use for links
4556 
4557  # Remove link placeholders by the link text.
4558  # <!--LINK number-->
4559  # turns into
4560  # link text with suffix
4561  # Do this before unstrip since link text can contain strip markers
4562  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4563 
4564  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4565  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4566 
4567  # Strip out HTML (first regex removes any tag not allowed)
4568  # Allowed tags are:
4569  # * <sup> and <sub> (bug 8393)
4570  # * <i> (bug 26375)
4571  # * <b> (r105284)
4572  # * <bdi> (bug 72884)
4573  # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4574  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4575  # to allow setting directionality in toc items.
4576  $tocline = preg_replace(
4577  [
4578  '#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4579  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4580  ],
4581  [ '', '<$1>' ],
4582  $safeHeadline
4583  );
4584 
4585  # Strip '<span></span>', which is the result from the above if
4586  # <span id="foo"></span> is used to produce an additional anchor
4587  # for a section.
4588  $tocline = str_replace( '<span></span>', '', $tocline );
4589 
4590  $tocline = trim( $tocline );
4591 
4592  # For the anchor, strip out HTML-y stuff period
4593  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4594  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4595 
4596  # Save headline for section edit hint before it's escaped
4597  $headlineHint = $safeHeadline;
4598 
4599  if ( $wgExperimentalHtmlIds ) {
4600  # For reverse compatibility, provide an id that's
4601  # HTML4-compatible, like we used to.
4602  # It may be worth noting, academically, that it's possible for
4603  # the legacy anchor to conflict with a non-legacy headline
4604  # anchor on the page. In this case likely the "correct" thing
4605  # would be to either drop the legacy anchors or make sure
4606  # they're numbered first. However, this would require people
4607  # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4608  # manually, so let's not bother worrying about it.
4609  $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4610  [ 'noninitial', 'legacy' ] );
4611  $safeHeadline = Sanitizer::escapeId( $safeHeadline );
4612 
4613  if ( $legacyHeadline == $safeHeadline ) {
4614  # No reason to have both (in fact, we can't)
4615  $legacyHeadline = false;
4616  }
4617  } else {
4618  $legacyHeadline = false;
4619  $safeHeadline = Sanitizer::escapeId( $safeHeadline,
4620  'noninitial' );
4621  }
4622 
4623  # HTML names must be case-insensitively unique (bug 10721).
4624  # This does not apply to Unicode characters per
4625  # http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4626  # @todo FIXME: We may be changing them depending on the current locale.
4627  $arrayKey = strtolower( $safeHeadline );
4628  if ( $legacyHeadline === false ) {
4629  $legacyArrayKey = false;
4630  } else {
4631  $legacyArrayKey = strtolower( $legacyHeadline );
4632  }
4633 
4634  # Create the anchor for linking from the TOC to the section
4635  $anchor = $safeHeadline;
4636  $legacyAnchor = $legacyHeadline;
4637  if ( isset( $refers[$arrayKey] ) ) {
4638  // @codingStandardsIgnoreStart
4639  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4640  // @codingStandardsIgnoreEnd
4641  $anchor .= "_$i";
4642  $refers["${arrayKey}_$i"] = true;
4643  } else {
4644  $refers[$arrayKey] = true;
4645  }
4646  if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4647  // @codingStandardsIgnoreStart
4648  for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4649  // @codingStandardsIgnoreEnd
4650  $legacyAnchor .= "_$i";
4651  $refers["${legacyArrayKey}_$i"] = true;
4652  } else {
4653  $refers[$legacyArrayKey] = true;
4654  }
4655 
4656  # Don't number the heading if it is the only one (looks silly)
4657  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4658  # the two are different if the line contains a link
4659  $headline = Html::element(
4660  'span',
4661  [ 'class' => 'mw-headline-number' ],
4662  $numbering
4663  ) . ' ' . $headline;
4664  }
4665 
4666  if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4667  $toc .= Linker::tocLine( $anchor, $tocline,
4668  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4669  }
4670 
4671  # Add the section to the section tree
4672  # Find the DOM node for this header
4673  $noOffset = ( $isTemplate || $sectionIndex === false );
4674  while ( $node && !$noOffset ) {
4675  if ( $node->getName() === 'h' ) {
4676  $bits = $node->splitHeading();
4677  if ( $bits['i'] == $sectionIndex ) {
4678  break;
4679  }
4680  }
4681  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4682  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4683  $node = $node->getNextSibling();
4684  }
4685  $tocraw[] = [
4686  'toclevel' => $toclevel,
4687  'level' => $level,
4688  'line' => $tocline,
4689  'number' => $numbering,
4690  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4691  'fromtitle' => $titleText,
4692  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4693  'anchor' => $anchor,
4694  ];
4695 
4696  # give headline the correct <h#> tag
4697  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4698  // Output edit section links as markers with styles that can be customized by skins
4699  if ( $isTemplate ) {
4700  # Put a T flag in the section identifier, to indicate to extractSections()
4701  # that sections inside <includeonly> should be counted.
4702  $editsectionPage = $titleText;
4703  $editsectionSection = "T-$sectionIndex";
4704  $editsectionContent = null;
4705  } else {
4706  $editsectionPage = $this->mTitle->getPrefixedText();
4707  $editsectionSection = $sectionIndex;
4708  $editsectionContent = $headlineHint;
4709  }
4710  // We use a bit of pesudo-xml for editsection markers. The
4711  // language converter is run later on. Using a UNIQ style marker
4712  // leads to the converter screwing up the tokens when it
4713  // converts stuff. And trying to insert strip tags fails too. At
4714  // this point all real inputted tags have already been escaped,
4715  // so we don't have to worry about a user trying to input one of
4716  // these markers directly. We use a page and section attribute
4717  // to stop the language converter from converting these
4718  // important bits of data, but put the headline hint inside a
4719  // content block because the language converter is supposed to
4720  // be able to convert that piece of data.
4721  // Gets replaced with html in ParserOutput::getText
4722  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4723  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4724  if ( $editsectionContent !== null ) {
4725  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4726  } else {
4727  $editlink .= '/>';
4728  }
4729  } else {
4730  $editlink = '';
4731  }
4732  $head[$headlineCount] = Linker::makeHeadline( $level,
4733  $matches['attrib'][$headlineCount], $anchor, $headline,
4734  $editlink, $legacyAnchor );
4735 
4736  $headlineCount++;
4737  }
4738 
4739  $this->setOutputType( $oldType );
4740 
4741  # Never ever show TOC if no headers
4742  if ( $numVisible < 1 ) {
4743  $enoughToc = false;
4744  }
4745 
4746  if ( $enoughToc ) {
4747  if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4748  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4749  }
4750  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4751  $this->mOutput->setTOCHTML( $toc );
4752  $toc = self::TOC_START . $toc . self::TOC_END;
4753  $this->mOutput->addModules( 'mediawiki.toc' );
4754  }
4755 
4756  if ( $isMain ) {
4757  $this->mOutput->setSections( $tocraw );
4758  }
4759 
4760  # split up and insert constructed headlines
4761  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4762  $i = 0;
4763 
4764  // build an array of document sections
4765  $sections = [];
4766  foreach ( $blocks as $block ) {
4767  // $head is zero-based, sections aren't.
4768  if ( empty( $head[$i - 1] ) ) {
4769  $sections[$i] = $block;
4770  } else {
4771  $sections[$i] = $head[$i - 1] . $block;
4772  }
4773 
4784  Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] );
4785 
4786  $i++;
4787  }
4788 
4789  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4790  // append the TOC at the beginning
4791  // Top anchor now in skin
4792  $sections[0] = $sections[0] . $toc . "\n";
4793  }
4794 
4795  $full .= implode( '', $sections );
4796 
4797  if ( $this->mForceTocPosition ) {
4798  return str_replace( '<!--MWTOC-->', $toc, $full );
4799  } else {
4800  return $full;
4801  }
4802  }
4803 
4815  public function preSaveTransform( $text, Title $title, User $user,
4816  ParserOptions $options, $clearState = true
4817  ) {
4818  if ( $clearState ) {
4819  $magicScopeVariable = $this->lock();
4820  }
4821  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4822  $this->setUser( $user );
4823 
4824  $pairs = [
4825  "\r\n" => "\n",
4826  "\r" => "\n",
4827  ];
4828  $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4829  if ( $options->getPreSaveTransform() ) {
4830  $text = $this->pstPass2( $text, $user );
4831  }
4832  $text = $this->mStripState->unstripBoth( $text );
4833 
4834  $this->setUser( null ); # Reset
4835 
4836  return $text;
4837  }
4838 
4847  private function pstPass2( $text, $user ) {
4849 
4850  # Note: This is the timestamp saved as hardcoded wikitext to
4851  # the database, we use $wgContLang here in order to give
4852  # everyone the same signature and use the default one rather
4853  # than the one selected in each user's preferences.
4854  # (see also bug 12815)
4855  $ts = $this->mOptions->getTimestamp();
4857  $ts = $timestamp->format( 'YmdHis' );
4858  $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
4859 
4860  $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4861 
4862  # Variable replacement
4863  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4864  $text = $this->replaceVariables( $text );
4865 
4866  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4867  # which may corrupt this parser instance via its wfMessage()->text() call-
4868 
4869  # Signatures
4870  $sigText = $this->getUserSig( $user );
4871  $text = strtr( $text, [
4872  '~~~~~' => $d,
4873  '~~~~' => "$sigText $d",
4874  '~~~' => $sigText
4875  ] );
4876 
4877  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4878  $tc = '[' . Title::legalChars() . ']';
4879  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4880 
4881  // [[ns:page (context)|]]
4882  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4883  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4884  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4885  // [[ns:page (context), context|]] (using either single or double-width comma)
4886  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4887  // [[|page]] (reverse pipe trick: add context from page title)
4888  $p2 = "/\[\[\\|($tc+)]]/";
4889 
4890  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4891  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4892  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4893  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4894 
4895  $t = $this->mTitle->getText();
4896  $m = [];
4897  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4898  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4899  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4900  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4901  } else {
4902  # if there's no context, don't bother duplicating the title
4903  $text = preg_replace( $p2, '[[\\1]]', $text );
4904  }
4905 
4906  # Trim trailing whitespace
4907  $text = rtrim( $text );
4908 
4909  return $text;
4910  }
4911 
4926  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4928 
4929  $username = $user->getName();
4930 
4931  # If not given, retrieve from the user object.
4932  if ( $nickname === false ) {
4933  $nickname = $user->getOption( 'nickname' );
4934  }
4935 
4936  if ( is_null( $fancySig ) ) {
4937  $fancySig = $user->getBoolOption( 'fancysig' );
4938  }
4939 
4940  $nickname = $nickname == null ? $username : $nickname;
4941 
4942  if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4943  $nickname = $username;
4944  wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4945  } elseif ( $fancySig !== false ) {
4946  # Sig. might contain markup; validate this
4947  if ( $this->validateSig( $nickname ) !== false ) {
4948  # Validated; clean up (if needed) and return it
4949  return $this->cleanSig( $nickname, true );
4950  } else {
4951  # Failed to validate; fall back to the default
4952  $nickname = $username;
4953  wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4954  }
4955  }
4956 
4957  # Make sure nickname doesnt get a sig in a sig
4958  $nickname = self::cleanSigInSig( $nickname );
4959 
4960  # If we're still here, make it a link to the user page
4961  $userText = wfEscapeWikiText( $username );
4962  $nickText = wfEscapeWikiText( $nickname );
4963  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4964 
4965  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4966  ->title( $this->getTitle() )->text();
4967  }
4968 
4975  public function validateSig( $text ) {
4976  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4977  }
4978 
4989  public function cleanSig( $text, $parsing = false ) {
4990  if ( !$parsing ) {
4991  global $wgTitle;
4992  $magicScopeVariable = $this->lock();
4993  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4994  }
4995 
4996  # Option to disable this feature
4997  if ( !$this->mOptions->getCleanSignatures() ) {
4998  return $text;
4999  }
5000 
5001  # @todo FIXME: Regex doesn't respect extension tags or nowiki
5002  # => Move this logic to braceSubstitution()
5003  $substWord = MagicWord::get( 'subst' );
5004  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
5005  $substText = '{{' . $substWord->getSynonym( 0 );
5006 
5007  $text = preg_replace( $substRegex, $substText, $text );
5008  $text = self::cleanSigInSig( $text );
5009  $dom = $this->preprocessToDom( $text );
5010  $frame = $this->getPreprocessor()->newFrame();
5011  $text = $frame->expand( $dom );
5012 
5013  if ( !$parsing ) {
5014  $text = $this->mStripState->unstripBoth( $text );
5015  }
5016 
5017  return $text;
5018  }
5019 
5026  public static function cleanSigInSig( $text ) {
5027  $text = preg_replace( '/~{3,5}/', '', $text );
5028  return $text;
5029  }
5030 
5041  $outputType, $clearState = true
5042  ) {
5043  $this->startParse( $title, $options, $outputType, $clearState );
5044  }
5045 
5052  private function startParse( Title $title = null, ParserOptions $options,
5053  $outputType, $clearState = true
5054  ) {
5055  $this->setTitle( $title );
5056  $this->mOptions = $options;
5057  $this->setOutputType( $outputType );
5058  if ( $clearState ) {
5059  $this->clearState();
5060  }
5061  }
5062 
5071  public function transformMsg( $text, $options, $title = null ) {
5072  static $executing = false;
5073 
5074  # Guard against infinite recursion
5075  if ( $executing ) {
5076  return $text;
5077  }
5078  $executing = true;
5079 
5080  if ( !$title ) {
5081  global $wgTitle;
5082  $title = $wgTitle;
5083  }
5084 
5085  $text = $this->preprocess( $text, $title, $options );
5086 
5087  $executing = false;
5088  return $text;
5089  }
5090 
5115  public function setHook( $tag, $callback ) {
5116  $tag = strtolower( $tag );
5117  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5118  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5119  }
5120  $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
5121  $this->mTagHooks[$tag] = $callback;
5122  if ( !in_array( $tag, $this->mStripList ) ) {
5123  $this->mStripList[] = $tag;
5124  }
5125 
5126  return $oldVal;
5127  }
5128 
5146  public function setTransparentTagHook( $tag, $callback ) {
5147  $tag = strtolower( $tag );
5148  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5149  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5150  }
5151  $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
5152  $this->mTransparentTagHooks[$tag] = $callback;
5153 
5154  return $oldVal;
5155  }
5156 
5160  public function clearTagHooks() {
5161  $this->mTagHooks = [];
5162  $this->mFunctionTagHooks = [];
5163  $this->mStripList = $this->mDefaultStripList;
5164  }
5165 
5209  public function setFunctionHook( $id, $callback, $flags = 0 ) {
5211 
5212  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5213  $this->mFunctionHooks[$id] = [ $callback, $flags ];
5214 
5215  # Add to function cache
5216  $mw = MagicWord::get( $id );
5217  if ( !$mw ) {
5218  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5219  }
5220 
5221  $synonyms = $mw->getSynonyms();
5222  $sensitive = intval( $mw->isCaseSensitive() );
5223 
5224  foreach ( $synonyms as $syn ) {
5225  # Case
5226  if ( !$sensitive ) {
5227  $syn = $wgContLang->lc( $syn );
5228  }
5229  # Add leading hash
5230  if ( !( $flags & self::SFH_NO_HASH ) ) {
5231  $syn = '#' . $syn;
5232  }
5233  # Remove trailing colon
5234  if ( substr( $syn, -1, 1 ) === ':' ) {
5235  $syn = substr( $syn, 0, -1 );
5236  }
5237  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5238  }
5239  return $oldVal;
5240  }
5241 
5247  public function getFunctionHooks() {
5248  return array_keys( $this->mFunctionHooks );
5249  }
5250 
5261  public function setFunctionTagHook( $tag, $callback, $flags ) {
5262  $tag = strtolower( $tag );
5263  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5264  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5265  }
5266  $old = isset( $this->mFunctionTagHooks[$tag] ) ?
5267  $this->mFunctionTagHooks[$tag] : null;
5268  $this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
5269 
5270  if ( !in_array( $tag, $this->mStripList ) ) {
5271  $this->mStripList[] = $tag;
5272  }
5273 
5274  return $old;
5275  }
5276 
5284  public function replaceLinkHolders( &$text, $options = 0 ) {
5285  $this->mLinkHolders->replace( $text );
5286  }
5287 
5295  public function replaceLinkHoldersText( $text ) {
5296  return $this->mLinkHolders->replaceText( $text );
5297  }
5298 
5312  public function renderImageGallery( $text, $params ) {
5313 
5314  $mode = false;
5315  if ( isset( $params['mode'] ) ) {
5316  $mode = $params['mode'];
5317  }
5318 
5319  try {
5320  $ig = ImageGalleryBase::factory( $mode );
5321  } catch ( Exception $e ) {
5322  // If invalid type set, fallback to default.
5323  $ig = ImageGalleryBase::factory( false );
5324  }
5325 
5326  $ig->setContextTitle( $this->mTitle );
5327  $ig->setShowBytes( false );
5328  $ig->setShowFilename( false );
5329  $ig->setParser( $this );
5330  $ig->setHideBadImages();
5331  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
5332 
5333  if ( isset( $params['showfilename'] ) ) {
5334  $ig->setShowFilename( true );
5335  } else {
5336  $ig->setShowFilename( false );
5337  }
5338  if ( isset( $params['caption'] ) ) {
5339  $caption = $params['caption'];
5340  $caption = htmlspecialchars( $caption );
5341  $caption = $this->replaceInternalLinks( $caption );
5342  $ig->setCaptionHtml( $caption );
5343  }
5344  if ( isset( $params['perrow'] ) ) {
5345  $ig->setPerRow( $params['perrow'] );
5346  }
5347  if ( isset( $params['widths'] ) ) {
5348  $ig->setWidths( $params['widths'] );
5349  }
5350  if ( isset( $params['heights'] ) ) {
5351  $ig->setHeights( $params['heights'] );
5352  }
5353  $ig->setAdditionalOptions( $params );
5354 
5355  Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] );
5356 
5357  $lines = StringUtils::explode( "\n", $text );
5358  foreach ( $lines as $line ) {
5359  # match lines like these:
5360  # Image:someimage.jpg|This is some image
5361  $matches = [];
5362  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5363  # Skip empty lines
5364  if ( count( $matches ) == 0 ) {
5365  continue;
5366  }
5367 
5368  if ( strpos( $matches[0], '%' ) !== false ) {
5369  $matches[1] = rawurldecode( $matches[1] );
5370  }
5372  if ( is_null( $title ) ) {
5373  # Bogus title. Ignore these so we don't bomb out later.
5374  continue;
5375  }
5376 
5377  # We need to get what handler the file uses, to figure out parameters.
5378  # Note, a hook can overide the file name, and chose an entirely different
5379  # file (which potentially could be of a different type and have different handler).
5380  $options = [];
5381  $descQuery = false;
5382  Hooks::run( 'BeforeParserFetchFileAndTitle',
5383  [ $this, $title, &$options, &$descQuery ] );
5384  # Don't register it now, as ImageGallery does that later.
5385  $file = $this->fetchFileNoRegister( $title, $options );
5386  $handler = $file ? $file->getHandler() : false;
5387 
5388  $paramMap = [
5389  'img_alt' => 'gallery-internal-alt',
5390  'img_link' => 'gallery-internal-link',
5391  ];
5392  if ( $handler ) {
5393  $paramMap = $paramMap + $handler->getParamMap();
5394  // We don't want people to specify per-image widths.
5395  // Additionally the width parameter would need special casing anyhow.
5396  unset( $paramMap['img_width'] );
5397  }
5398 
5399  $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5400 
5401  $label = '';
5402  $alt = '';
5403  $link = '';
5404  $handlerOptions = [];
5405  if ( isset( $matches[3] ) ) {
5406  // look for an |alt= definition while trying not to break existing
5407  // captions with multiple pipes (|) in it, until a more sensible grammar
5408  // is defined for images in galleries
5409 
5410  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5411  // splitting on '|' is a bit odd, and different from makeImage.
5412  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5413  $parameterMatches = StringUtils::explode( '|', $matches[3] );
5414 
5415  foreach ( $parameterMatches as $parameterMatch ) {
5416  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5417  if ( $magicName ) {
5418  $paramName = $paramMap[$magicName];
5419 
5420  switch ( $paramName ) {
5421  case 'gallery-internal-alt':
5422  $alt = $this->stripAltText( $match, false );
5423  break;
5424  case 'gallery-internal-link':
5425  $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5426  $chars = self::EXT_LINK_URL_CLASS;
5427  $addr = self::EXT_LINK_ADDR;
5428  $prots = $this->mUrlProtocols;
5429  // check to see if link matches an absolute url, if not then it must be a wiki link.
5430  if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
5431  $link = $linkValue;
5432  } else {
5433  $localLinkTitle = Title::newFromText( $linkValue );
5434  if ( $localLinkTitle !== null ) {
5435  $link = $localLinkTitle->getLinkURL();
5436  }
5437  }
5438  break;
5439  default:
5440  // Must be a handler specific parameter.
5441  if ( $handler->validateParam( $paramName, $match ) ) {
5442  $handlerOptions[$paramName] = $match;
5443  } else {
5444  // Guess not, consider it as caption.
5445  wfDebug( "$parameterMatch failed parameter validation\n" );
5446  $label = '|' . $parameterMatch;
5447  }
5448  }
5449 
5450  } else {
5451  // Last pipe wins.
5452  $label = '|' . $parameterMatch;
5453  }
5454  }
5455  // Remove the pipe.
5456  $label = substr( $label, 1 );
5457  }
5458 
5459  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5460  }
5461  $html = $ig->toHTML();
5462  Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] );
5463  return $html;
5464  }
5465 
5470  public function getImageParams( $handler ) {
5471  if ( $handler ) {
5472  $handlerClass = get_class( $handler );
5473  } else {
5474  $handlerClass = '';
5475  }
5476  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5477  # Initialise static lists
5478  static $internalParamNames = [
5479  'horizAlign' => [ 'left', 'right', 'center', 'none' ],
5480  'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5481  'bottom', 'text-bottom' ],
5482  'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless',
5483  'upright', 'border', 'link', 'alt', 'class' ],
5484  ];
5485  static $internalParamMap;
5486  if ( !$internalParamMap ) {
5487  $internalParamMap = [];
5488  foreach ( $internalParamNames as $type => $names ) {
5489  foreach ( $names as $name ) {
5490  $magicName = str_replace( '-', '_', "img_$name" );
5491  $internalParamMap[$magicName] = [ $type, $name ];
5492  }
5493  }
5494  }
5495 
5496  # Add handler params
5497  $paramMap = $internalParamMap;
5498  if ( $handler ) {
5499  $handlerParamMap = $handler->getParamMap();
5500  foreach ( $handlerParamMap as $magic => $paramName ) {
5501  $paramMap[$magic] = [ 'handler', $paramName ];
5502  }
5503  }
5504  $this->mImageParams[$handlerClass] = $paramMap;
5505  $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5506  }
5507  return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
5508  }
5509 
5518  public function makeImage( $title, $options, $holders = false ) {
5519  # Check if the options text is of the form "options|alt text"
5520  # Options are:
5521  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5522  # * left no resizing, just left align. label is used for alt= only
5523  # * right same, but right aligned
5524  # * none same, but not aligned
5525  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5526  # * center center the image
5527  # * frame Keep original image size, no magnify-button.
5528  # * framed Same as "frame"
5529  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5530  # * upright reduce width for upright images, rounded to full __0 px
5531  # * border draw a 1px border around the image
5532  # * alt Text for HTML alt attribute (defaults to empty)
5533  # * class Set a class for img node
5534  # * link Set the target of the image link. Can be external, interwiki, or local
5535  # vertical-align values (no % or length right now):
5536  # * baseline
5537  # * sub
5538  # * super
5539  # * top
5540  # * text-top
5541  # * middle
5542  # * bottom
5543  # * text-bottom
5544 
5545  $parts = StringUtils::explode( "|", $options );
5546 
5547  # Give extensions a chance to select the file revision for us
5548  $options = [];
5549  $descQuery = false;
5550  Hooks::run( 'BeforeParserFetchFileAndTitle',
5551  [ $this, $title, &$options, &$descQuery ] );
5552  # Fetch and register the file (file title may be different via hooks)
5553  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5554 
5555  # Get parameter map
5556  $handler = $file ? $file->getHandler() : false;
5557 
5558  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5559 
5560  if ( !$file ) {
5561  $this->addTrackingCategory( 'broken-file-category' );
5562  }
5563 
5564  # Process the input parameters
5565  $caption = '';
5566  $params = [ 'frame' => [], 'handler' => [],
5567  'horizAlign' => [], 'vertAlign' => [] ];
5568  $seenformat = false;
5569  foreach ( $parts as $part ) {
5570  $part = trim( $part );
5571  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5572  $validated = false;
5573  if ( isset( $paramMap[$magicName] ) ) {
5574  list( $type, $paramName ) = $paramMap[$magicName];
5575 
5576  # Special case; width and height come in one variable together
5577  if ( $type === 'handler' && $paramName === 'width' ) {
5578  $parsedWidthParam = $this->parseWidthParam( $value );
5579  if ( isset( $parsedWidthParam['width'] ) ) {
5580  $width = $parsedWidthParam['width'];
5581  if ( $handler->validateParam( 'width', $width ) ) {
5582  $params[$type]['width'] = $width;
5583  $validated = true;
5584  }
5585  }
5586  if ( isset( $parsedWidthParam['height'] ) ) {
5587  $height = $parsedWidthParam['height'];
5588  if ( $handler->validateParam( 'height', $height ) ) {
5589  $params[$type]['height'] = $height;
5590  $validated = true;
5591  }
5592  }
5593  # else no validation -- bug 13436
5594  } else {
5595  if ( $type === 'handler' ) {
5596  # Validate handler parameter
5597  $validated = $handler->validateParam( $paramName, $value );
5598  } else {
5599  # Validate internal parameters
5600  switch ( $paramName ) {
5601  case 'manualthumb':
5602  case 'alt':
5603  case 'class':
5604  # @todo FIXME: Possibly check validity here for
5605  # manualthumb? downstream behavior seems odd with
5606  # missing manual thumbs.
5607  $validated = true;
5608  $value = $this->stripAltText( $value, $holders );
5609  break;
5610  case 'link':
5611  $chars = self::EXT_LINK_URL_CLASS;
5612  $addr = self::EXT_LINK_ADDR;
5613  $prots = $this->mUrlProtocols;
5614  if ( $value === '' ) {
5615  $paramName = 'no-link';
5616  $value = true;
5617  $validated = true;
5618  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5619  if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
5620  $paramName = 'link-url';
5621  $this->mOutput->addExternalLink( $value );
5622  if ( $this->mOptions->getExternalLinkTarget() ) {
5623  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5624  }
5625  $validated = true;
5626  }
5627  } else {
5628  $linkTitle = Title::newFromText( $value );
5629  if ( $linkTitle ) {
5630  $paramName = 'link-title';
5631  $value = $linkTitle;
5632  $this->mOutput->addLink( $linkTitle );
5633  $validated = true;
5634  }
5635  }
5636  break;
5637  case 'frameless':
5638  case 'framed':
5639  case 'thumbnail':
5640  // use first appearing option, discard others.
5641  $validated = ! $seenformat;
5642  $seenformat = true;
5643  break;
5644  default:
5645  # Most other things appear to be empty or numeric...
5646  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5647  }
5648  }
5649 
5650  if ( $validated ) {
5651  $params[$type][$paramName] = $value;
5652  }
5653  }
5654  }
5655  if ( !$validated ) {
5656  $caption = $part;
5657  }
5658  }
5659 
5660  # Process alignment parameters
5661  if ( $params['horizAlign'] ) {
5662  $params['frame']['align'] = key( $params['horizAlign'] );
5663  }
5664  if ( $params['vertAlign'] ) {
5665  $params['frame']['valign'] = key( $params['vertAlign'] );
5666  }
5667 
5668  $params['frame']['caption'] = $caption;
5669 
5670  # Will the image be presented in a frame, with the caption below?
5671  $imageIsFramed = isset( $params['frame']['frame'] )
5672  || isset( $params['frame']['framed'] )
5673  || isset( $params['frame']['thumbnail'] )
5674  || isset( $params['frame']['manualthumb'] );
5675 
5676  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5677  # came to also set the caption, ordinary text after the image -- which
5678  # makes no sense, because that just repeats the text multiple times in
5679  # screen readers. It *also* came to set the title attribute.
5680  # Now that we have an alt attribute, we should not set the alt text to
5681  # equal the caption: that's worse than useless, it just repeats the
5682  # text. This is the framed/thumbnail case. If there's no caption, we
5683  # use the unnamed parameter for alt text as well, just for the time be-
5684  # ing, if the unnamed param is set and the alt param is not.
5685  # For the future, we need to figure out if we want to tweak this more,
5686  # e.g., introducing a title= parameter for the title; ignoring the un-
5687  # named parameter entirely for images without a caption; adding an ex-
5688  # plicit caption= parameter and preserving the old magic unnamed para-
5689  # meter for BC; ...
5690  if ( $imageIsFramed ) { # Framed image
5691  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5692  # No caption or alt text, add the filename as the alt text so
5693  # that screen readers at least get some description of the image
5694  $params['frame']['alt'] = $title->getText();
5695  }
5696  # Do not set $params['frame']['title'] because tooltips don't make sense
5697  # for framed images
5698  } else { # Inline image
5699  if ( !isset( $params['frame']['alt'] ) ) {
5700  # No alt text, use the "caption" for the alt text
5701  if ( $caption !== '' ) {
5702  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5703  } else {
5704  # No caption, fall back to using the filename for the
5705  # alt text
5706  $params['frame']['alt'] = $title->getText();
5707  }
5708  }
5709  # Use the "caption" for the tooltip text
5710  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5711  }
5712 
5713  Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
5714 
5715  # Linker does the rest
5716  $time = isset( $options['time'] ) ? $options['time'] : false;
5717  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5718  $time, $descQuery, $this->mOptions->getThumbSize() );
5719 
5720  # Give the handler a chance to modify the parser object
5721  if ( $handler ) {
5722  $handler->parserTransformHook( $this, $file );
5723  }
5724 
5725  return $ret;
5726  }
5727 
5733  protected function stripAltText( $caption, $holders ) {
5734  # Strip bad stuff out of the title (tooltip). We can't just use
5735  # replaceLinkHoldersText() here, because if this function is called
5736  # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5737  if ( $holders ) {
5738  $tooltip = $holders->replaceText( $caption );
5739  } else {
5740  $tooltip = $this->replaceLinkHoldersText( $caption );
5741  }
5742 
5743  # make sure there are no placeholders in thumbnail attributes
5744  # that are later expanded to html- so expand them now and
5745  # remove the tags
5746  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5747  $tooltip = Sanitizer::stripAllTags( $tooltip );
5748 
5749  return $tooltip;
5750  }
5751 
5756  public function disableCache() {
5757  wfDebug( "Parser output marked as uncacheable.\n" );
5758  if ( !$this->mOutput ) {
5759  throw new MWException( __METHOD__ .
5760  " can only be called when actually parsing something" );
5761  }
5762  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5763  }
5764 
5773  public function attributeStripCallback( &$text, $frame = false ) {
5774  $text = $this->replaceVariables( $text, $frame );
5775  $text = $this->mStripState->unstripBoth( $text );
5776  return $text;
5777  }
5778 
5784  public function getTags() {
5785  return array_merge(
5786  array_keys( $this->mTransparentTagHooks ),
5787  array_keys( $this->mTagHooks ),
5788  array_keys( $this->mFunctionTagHooks )
5789  );
5790  }
5791 
5802  public function replaceTransparentTags( $text ) {
5803  $matches = [];
5804  $elements = array_keys( $this->mTransparentTagHooks );
5805  $text = self::extractTagsAndParams( $elements, $text, $matches );
5806  $replacements = [];
5807 
5808  foreach ( $matches as $marker => $data ) {
5809  list( $element, $content, $params, $tag ) = $data;
5810  $tagName = strtolower( $element );
5811  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5812  $output = call_user_func_array(
5813  $this->mTransparentTagHooks[$tagName],
5814  [ $content, $params, $this ]
5815  );
5816  } else {
5817  $output = $tag;
5818  }
5819  $replacements[$marker] = $output;
5820  }
5821  return strtr( $text, $replacements );
5822  }
5823 
5853  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5854  global $wgTitle; # not generally used but removes an ugly failure mode
5855 
5856  $magicScopeVariable = $this->lock();
5857  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5858  $outText = '';
5859  $frame = $this->getPreprocessor()->newFrame();
5860 
5861  # Process section extraction flags
5862  $flags = 0;
5863  $sectionParts = explode( '-', $sectionId );
5864  $sectionIndex = array_pop( $sectionParts );
5865  foreach ( $sectionParts as $part ) {
5866  if ( $part === 'T' ) {
5867  $flags |= self::PTD_FOR_INCLUSION;
5868  }
5869  }
5870 
5871  # Check for empty input
5872  if ( strval( $text ) === '' ) {
5873  # Only sections 0 and T-0 exist in an empty document
5874  if ( $sectionIndex == 0 ) {
5875  if ( $mode === 'get' ) {
5876  return '';
5877  } else {
5878  return $newText;
5879  }
5880  } else {
5881  if ( $mode === 'get' ) {
5882  return $newText;
5883  } else {
5884  return $text;
5885  }
5886  }
5887  }
5888 
5889  # Preprocess the text
5890  $root = $this->preprocessToDom( $text, $flags );
5891 
5892  # <h> nodes indicate section breaks
5893  # They can only occur at the top level, so we can find them by iterating the root's children
5894  $node = $root->getFirstChild();
5895 
5896  # Find the target section
5897  if ( $sectionIndex == 0 ) {
5898  # Section zero doesn't nest, level=big
5899  $targetLevel = 1000;
5900  } else {
5901  while ( $node ) {
5902  if ( $node->getName() === 'h' ) {
5903  $bits = $node->splitHeading();
5904  if ( $bits['i'] == $sectionIndex ) {
5905  $targetLevel = $bits['level'];
5906  break;
5907  }
5908  }
5909  if ( $mode === 'replace' ) {
5910  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5911  }
5912  $node = $node->getNextSibling();
5913  }
5914  }
5915 
5916  if ( !$node ) {
5917  # Not found
5918  if ( $mode === 'get' ) {
5919  return $newText;
5920  } else {
5921  return $text;
5922  }
5923  }
5924 
5925  # Find the end of the section, including nested sections
5926  do {
5927  if ( $node->getName() === 'h' ) {
5928  $bits = $node->splitHeading();
5929  $curLevel = $bits['level'];
5930  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5931  break;
5932  }
5933  }
5934  if ( $mode === 'get' ) {
5935  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5936  }
5937  $node = $node->getNextSibling();
5938  } while ( $node );
5939 
5940  # Write out the remainder (in replace mode only)
5941  if ( $mode === 'replace' ) {
5942  # Output the replacement text
5943  # Add two newlines on -- trailing whitespace in $newText is conventionally
5944  # stripped by the editor, so we need both newlines to restore the paragraph gap
5945  # Only add trailing whitespace if there is newText
5946  if ( $newText != "" ) {
5947  $outText .= $newText . "\n\n";
5948  }
5949 
5950  while ( $node ) {
5951  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5952  $node = $node->getNextSibling();
5953  }
5954  }
5955 
5956  if ( is_string( $outText ) ) {
5957  # Re-insert stripped tags
5958  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5959  }
5960 
5961  return $outText;
5962  }
5963 
5978  public function getSection( $text, $sectionId, $defaultText = '' ) {
5979  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5980  }
5981 
5994  public function replaceSection( $oldText, $sectionId, $newText ) {
5995  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5996  }
5997 
6003  public function getRevisionId() {
6004  return $this->mRevisionId;
6005  }
6006 
6013  public function getRevisionObject() {
6014  if ( !is_null( $this->mRevisionObject ) ) {
6015  return $this->mRevisionObject;
6016  }
6017  if ( is_null( $this->mRevisionId ) ) {
6018  return null;
6019  }
6020 
6021  $rev = call_user_func(
6022  $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
6023  );
6024 
6025  # If the parse is for a new revision, then the callback should have
6026  # already been set to force the object and should match mRevisionId.
6027  # If not, try to fetch by mRevisionId for sanity.
6028  if ( $rev && $rev->getId() != $this->mRevisionId ) {
6029  $rev = Revision::newFromId( $this->mRevisionId );
6030  }
6031 
6032  $this->mRevisionObject = $rev;
6033 
6034  return $this->mRevisionObject;
6035  }
6036 
6042  public function getRevisionTimestamp() {
6043  if ( is_null( $this->mRevisionTimestamp ) ) {
6045 
6046  $revObject = $this->getRevisionObject();
6047  $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
6048 
6049  # The cryptic '' timezone parameter tells to use the site-default
6050  # timezone offset instead of the user settings.
6051  # Since this value will be saved into the parser cache, served
6052  # to other users, and potentially even used inside links and such,
6053  # it needs to be consistent for all visitors.
6054  $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
6055 
6056  }
6057  return $this->mRevisionTimestamp;
6058  }
6059 
6065  public function getRevisionUser() {
6066  if ( is_null( $this->mRevisionUser ) ) {
6067  $revObject = $this->getRevisionObject();
6068 
6069  # if this template is subst: the revision id will be blank,
6070  # so just use the current user's name
6071  if ( $revObject ) {
6072  $this->mRevisionUser = $revObject->getUserText();
6073  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6074  $this->mRevisionUser = $this->getUser()->getName();
6075  }
6076  }
6077  return $this->mRevisionUser;
6078  }
6079 
6085  public function getRevisionSize() {
6086  if ( is_null( $this->mRevisionSize ) ) {
6087  $revObject = $this->getRevisionObject();
6088 
6089  # if this variable is subst: the revision id will be blank,
6090  # so just use the parser input size, because the own substituation
6091  # will change the size.
6092  if ( $revObject ) {
6093  $this->mRevisionSize = $revObject->getSize();
6094  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6095  $this->mRevisionSize = $this->mInputSize;
6096  }
6097  }
6098  return $this->mRevisionSize;
6099  }
6100 
6106  public function setDefaultSort( $sort ) {
6107  $this->mDefaultSort = $sort;
6108  $this->mOutput->setProperty( 'defaultsort', $sort );
6109  }
6110 
6121  public function getDefaultSort() {
6122  if ( $this->mDefaultSort !== false ) {
6123  return $this->mDefaultSort;
6124  } else {
6125  return '';
6126  }
6127  }
6128 
6135  public function getCustomDefaultSort() {
6136  return $this->mDefaultSort;
6137  }
6138 
6148  public function guessSectionNameFromWikiText( $text ) {
6149  # Strip out wikitext links(they break the anchor)
6150  $text = $this->stripSectionName( $text );
6152  return '#' . Sanitizer::escapeId( $text, 'noninitial' );
6153  }
6154 
6163  public function guessLegacySectionNameFromWikiText( $text ) {
6164  # Strip out wikitext links(they break the anchor)
6165  $text = $this->stripSectionName( $text );
6167  return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
6168  }
6169 
6184  public function stripSectionName( $text ) {
6185  # Strip internal link markup
6186  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6187  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6188 
6189  # Strip external link markup
6190  # @todo FIXME: Not tolerant to blank link text
6191  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6192  # on how many empty links there are on the page - need to figure that out.
6193  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6194 
6195  # Parse wikitext quotes (italics & bold)
6196  $text = $this->doQuotes( $text );
6197 
6198  # Strip HTML tags
6199  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6200  return $text;
6201  }
6202 
6213  public function testSrvus( $text, Title $title, ParserOptions $options,
6214  $outputType = self::OT_HTML
6215  ) {
6216  $magicScopeVariable = $this->lock();
6217  $this->startParse( $title, $options, $outputType, true );
6218 
6219  $text = $this->replaceVariables( $text );
6220  $text = $this->mStripState->unstripBoth( $text );
6221  $text = Sanitizer::removeHTMLtags( $text );
6222  return $text;
6223  }
6224 
6231  public function testPst( $text, Title $title, ParserOptions $options ) {
6232  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6233  }
6234 
6241  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6242  return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6243  }
6244 
6261  public function markerSkipCallback( $s, $callback ) {
6262  $i = 0;
6263  $out = '';
6264  while ( $i < strlen( $s ) ) {
6265  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6266  if ( $markerStart === false ) {
6267  $out .= call_user_func( $callback, substr( $s, $i ) );
6268  break;
6269  } else {
6270  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6271  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6272  if ( $markerEnd === false ) {
6273  $out .= substr( $s, $markerStart );
6274  break;
6275  } else {
6276  $markerEnd += strlen( self::MARKER_SUFFIX );
6277  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6278  $i = $markerEnd;
6279  }
6280  }
6281  }
6282  return $out;
6283  }
6284 
6291  public function killMarkers( $text ) {
6292  return $this->mStripState->killMarkers( $text );
6293  }
6294 
6311  public function serializeHalfParsedText( $text ) {
6312  $data = [
6313  'text' => $text,
6314  'version' => self::HALF_PARSED_VERSION,
6315  'stripState' => $this->mStripState->getSubState( $text ),
6316  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6317  ];
6318  return $data;
6319  }
6320 
6336  public function unserializeHalfParsedText( $data ) {
6337  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6338  throw new MWException( __METHOD__ . ': invalid version' );
6339  }
6340 
6341  # First, extract the strip state.
6342  $texts = [ $data['text'] ];
6343  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6344 
6345  # Now renumber links
6346  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6347 
6348  # Should be good to go.
6349  return $texts[0];
6350  }
6351 
6361  public function isValidHalfParsedText( $data ) {
6362  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6363  }
6364 
6373  public function parseWidthParam( $value ) {
6374  $parsedWidthParam = [];
6375  if ( $value === '' ) {
6376  return $parsedWidthParam;
6377  }
6378  $m = [];
6379  # (bug 13500) In both cases (width/height and width only),
6380  # permit trailing "px" for backward compatibility.
6381  if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6382  $width = intval( $m[1] );
6383  $height = intval( $m[2] );
6384  $parsedWidthParam['width'] = $width;
6385  $parsedWidthParam['height'] = $height;
6386  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6387  $width = intval( $value );
6388  $parsedWidthParam['width'] = $width;
6389  }
6390  return $parsedWidthParam;
6391  }
6392 
6402  protected function lock() {
6403  if ( $this->mInParse ) {
6404  throw new MWException( "Parser state cleared while parsing. "
6405  . "Did you call Parser::parse recursively?" );
6406  }
6407  $this->mInParse = true;
6408 
6409  $recursiveCheck = new ScopedCallback( function() {
6410  $this->mInParse = false;
6411  } );
6412 
6413  return $recursiveCheck;
6414  }
6415 
6426  public static function stripOuterParagraph( $html ) {
6427  $m = [];
6428  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6429  if ( strpos( $m[1], '</p>' ) === false ) {
6430  $html = $m[1];
6431  }
6432  }
6433 
6434  return $html;
6435  }
6436 
6447  public function getFreshParser() {
6448  global $wgParserConf;
6449  if ( $this->mInParse ) {
6450  return new $wgParserConf['class']( $wgParserConf );
6451  } else {
6452  return $this;
6453  }
6454  }
6455 
6462  public function enableOOUI() {
6464  $this->mOutput->setEnableOOUI( true );
6465  }
6466 }
getRevisionObject()
Get the revision object for $this->mRevisionId.
Definition: Parser.php:6013
setTitle($t)
Set the context title.
Definition: Parser.php:735
$mAutonumber
Definition: Parser.php:179
markerSkipCallback($s, $callback)
Call a callback function on all regions of the given text that are not inside strip markers...
Definition: Parser.php:6261
#define the
table suitable for use with IDatabase::select()
$mPPNodeCount
Definition: Parser.php:193
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition: Parser.php:2035
static getVariableIDs()
Get an array of parser variable IDs.
Definition: MagicWord.php:271
you don t have to do a grep find to see where the $wgReverseTitle variable is used
Definition: hooks.txt:117
const MARKER_PREFIX
Definition: Parser.php:136
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
external whereas SearchGetNearMatch runs after $term
Definition: hooks.txt:2536
isValidHalfParsedText($data)
Returns true if the given array, presumed to be generated by serializeHalfParsedText(), is compatible with the current version of the parser.
Definition: Parser.php:6361
null means default in associative array form
Definition: hooks.txt:1771
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:1771
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1756
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
getSection($text, $sectionId, $defaultText= '')
This function returns the text of a section, specified by a number ($section).
Definition: Parser.php:5978
static decodeTagAttributes($text)
Return an associative array of attribute names and values from a partial tag string.
Definition: Sanitizer.php:1248
$mTplRedirCache
Definition: Parser.php:195
killMarkers($text)
Remove any strip markers found in the given text.
Definition: Parser.php:6291
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
static tocList($toc, $lang=false)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition: Linker.php:1768
fetchTemplateAndTitle($title)
Fetch the unparsed text of a template and register a reference to it.
Definition: Parser.php:3921
getRevisionUser()
Get the name of the user that edited the last revision.
Definition: Parser.php:6065
setFunctionTagHook($tag, $callback, $flags)
Create a tag function, e.g.
Definition: Parser.php:5261
the array() calling protocol came about after MediaWiki 1.4rc1.
stripSectionName($text)
Strips a text string of wikitext for use in a section anchor.
Definition: Parser.php:6184
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1391
const OT_PREPROCESS
Definition: Defines.php:228
$mLastSection
Definition: Parser.php:186
static linkKnown($target, $html=null, $customAttribs=[], $query=[], $options=[ 'known', 'noclasses'])
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:270
$mDoubleUnderscores
Definition: Parser.php:195
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2300
Group all the pieces relevant to the context of a request into one instance.
getPreloadText($text, Title $title, ParserOptions $options, $params=[])
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:683
$context
Definition: load.php:43
validateSig($text)
Check that the user's signature contains no bad XML.
Definition: Parser.php:4975
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:245
$wgSitename
Name of the site.
renderImageGallery($text, $params)
Renders an image gallery from a text with one line per image.
Definition: Parser.php:5312
recursivePreprocess($text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition: Parser.php:664
replaceExternalLinks($text)
Replace external links (REL)
Definition: Parser.php:1773
static isNonincludable($index)
It is not possible to use pages from this namespace as template?
nextLinkID()
Definition: Parser.php:824
const SPACE_NOT_NL
Definition: Parser.php:100
static replaceUnusualEscapes($url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition: Parser.php:1890
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:312
getImageParams($handler)
Definition: Parser.php:5470
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
doHeadings($text)
Parse headers and return html.
Definition: Parser.php:1552
static getTitleFor($name, $subpage=false, $fragment= '')
Get a localised Title object for a specified special page name.
Definition: SpecialPage.php:73
const OT_PLAIN
Definition: Parser.php:121
getTags()
Accessor.
Definition: Parser.php:5784
findColonNoLinks($str, &$before, &$after)
Split up a string on ':', ignoring any occurrences inside tags to prevent illegal overlapping...
Definition: Parser.php:2748
static isWellFormedXmlFragment($text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:735
const OT_WIKI
Definition: Parser.php:118
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1905
fetchFileAndTitle($title, $options=[])
Fetch a file and its title and register a reference to it.
Definition: Parser.php:4063
User $mUser
Definition: Parser.php:202
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers ...
Definition: Parser.php:3258
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:1771
static isEnabled()
Definition: MWTidy.php:92
Set options of the Parser.
static tidy($text)
Interface with html tidy.
Definition: MWTidy.php:45
getFunctionHooks()
Get all registered function hook identifiers.
Definition: Parser.php:5247
namespace and then decline to actually register it file or subcat img or subcat RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:977
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition: globals.txt:10
wfHostname()
Fetch server name for use in error reporting etc.
getFunctionLang()
Get a language object for use in parser functions such as {{FORMATNUM:}}.
Definition: Parser.php:839
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
argSubstitution($piece, $frame)
Triple brace replacement – used for template arguments.
Definition: Parser.php:4166
testSrvus($text, Title $title, ParserOptions $options, $outputType=self::OT_HTML)
strip/replaceVariables/unstrip for preprocessor regression testing
Definition: Parser.php:6213
uniqPrefix()
Accessor for mUniqPrefix.
Definition: Parser.php:725
const TOC_START
Definition: Parser.php:139
Title($x=null)
Accessor/mutator for the Title object.
Definition: Parser.php:763
SectionProfiler $mProfiler
Definition: Parser.php:254
$wgEnableScaryTranscluding
Enable interwiki transcluding.
$sort
fetchFileNoRegister($title, $options=[])
Helper function for fetchFileAndTitle.
Definition: Parser.php:4088
null for the local wiki Added in
Definition: hooks.txt:1391
There are three types of nodes:
$mHeadings
Definition: Parser.php:195
$value
clearTagHooks()
Remove all tag hooks.
Definition: Parser.php:5160
const COLON_STATE_TAGSLASH
Definition: Parser.php:107
static makeSelfLinkObj($nt, $html= '', $query= '', $trail= '', $prefix= '')
Make appropriate markup for a link to the current article.
Definition: Linker.php:416
const NS_SPECIAL
Definition: Defines.php:58
clearState()
Clear Parser state.
Definition: Parser.php:338
__construct($conf=[])
Definition: Parser.php:259
const EXT_LINK_ADDR
Definition: Parser.php:92
$mFirstCall
Definition: Parser.php:154
interwikiTransclude($title, $action)
Transclude an interwiki link.
Definition: Parser.php:4107
pstPass2($text, $user)
Pre-save transform helper function.
Definition: Parser.php:4847
guessLegacySectionNameFromWikiText($text)
Same as guessSectionNameFromWikiText(), but produces legacy anchors instead.
Definition: Parser.php:6163
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
Options($x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:817
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2526
serializeHalfParsedText($text)
Save the parser state required to convert the given half-parsed text to HTML.
Definition: Parser.php:6311
replaceLinkHolders(&$text, $options=0)
Replace "<!--LINK-->" link placeholders with actual links, in the buffer Placeholders created in Link...
Definition: Parser.php:5284
static activeUsers()
Definition: SiteStats.php:161
$mLinkID
Definition: Parser.php:192
doQuotes($text)
Helper function for doAllQuotes()
Definition: Parser.php:1585
preprocessToDom($text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:3288
limitationWarn($limitationType, $current= '', $max= '')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
Definition: Parser.php:3410
static cleanUrl($url)
Definition: Sanitizer.php:1817
wfUrlencode($s)
We want some things to be included as literal characters in our title URLs for prettiness, which urlencode encodes by default.
static newFromText($text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:277
$mGeneratedPPNodeCount
Definition: Parser.php:193
Represents a title within MediaWiki.
Definition: Title.php:34
static getRandomString()
Get a random string.
Definition: Parser.php:704
$mRevisionId
Definition: Parser.php:219
static stripAllTags($text)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed, encoded as plain text.
Definition: Sanitizer.php:1784
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
doBlockLevels($text, $linestart)
#@-
Definition: Parser.php:2533
$wgArticlePath
Definition: img_auth.php:45
OutputType($x=null)
Accessor/mutator for the output type.
Definition: Parser.php:789
const NS_TEMPLATE
Definition: Defines.php:79
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:117
const COLON_STATE_COMMENTDASHDASH
Definition: Parser.php:110
getVariableValue($index, $frame=false)
Return value of a magic variable (like PAGENAME)
Definition: Parser.php:2906
recursiveTagParse($text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition: Parser.php:599
const NO_ARGS
magic word & $parser
Definition: hooks.txt:2300
MagicWordArray $mVariables
Definition: Parser.php:161
static validateTagAttributes($attribs, $element)
Take an array of attribute names and values and normalize or discard illegal values for the given ele...
Definition: Sanitizer.php:716
const SFH_NO_HASH
Definition: Parser.php:82
const COLON_STATE_COMMENTDASH
Definition: Parser.php:109
globals will be eliminated from MediaWiki replaced by an application object which would be passed to constructors Whether that would be an convenient solution remains to be but certainly PHP makes such object oriented programming models easier than they were in previous versions For the time being MediaWiki programmers will have to work in an environment with some global context At the time of globals were initialised on startup by MediaWiki of these were configuration which are documented in DefaultSettings php There is no comprehensive documentation for the remaining however some of the most important ones are listed below They are typically initialised either in index php or in Setup php For a description of the see design txt $wgTitle Title object created from the request URL $wgOut OutputPage object for HTTP response $wgUser User object for the user associated with the current request $wgLang Language object selected by user preferences $wgContLang Language object associated with the wiki being viewed $wgParser Parser object Parser extensions register their hooks here $wgRequest WebRequest object
Definition: globals.txt:25
wfRandomString($length=32)
Get a random string containing a number of pseudo-random hex characters.
$mForceTocPosition
Definition: Parser.php:197
preprocess($text, Title $title=null, ParserOptions $options, $revid=null, $frame=false)
Expand templates and variables in the text, producing valid, static wikitext.
Definition: Parser.php:640
static getCacheTTL($id)
Allow external reads of TTL array.
Definition: MagicWord.php:294
getRevisionId()
Get the ID of the revision we are parsing.
Definition: Parser.php:6003
const OT_PREPROCESS
Definition: Parser.php:119
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1585
maybeDoSubpageLink($target, &$text)
Handle link to subpage if necessary.
Definition: Parser.php:2400
$mFunctionSynonyms
Definition: Parser.php:146
If you want to remove the page from your watchlist later
replaceLinkHoldersText($text)
Replace "<!--LINK-->" link placeholders with plain text of links (not HTML-formatted).
Definition: Parser.php:5295
setLinkID($id)
Definition: Parser.php:831
$mOutputType
Definition: Parser.php:216
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
$mDefaultStripList
Definition: Parser.php:149
static createAssocArgs($args)
Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
Definition: Parser.php:3362
$mExtLinkBracketedRegex
Definition: Parser.php:168
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 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$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':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:1769
if($line===false) $args
Definition: cdb.php:64
the value to return A Title object or null for latest to be modified or replaced by the hook handler after cache objects are set for highlighting & $link
Definition: hooks.txt:2559
static getLocalInstance($ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
$wgMaxSigChars
Maximum number of Unicode characters in signature.
const COLON_STATE_TAG
Definition: Parser.php:104
static getDoubleUnderscoreArray()
Get a MagicWordArray of double-underscore entities.
Definition: MagicWord.php:307
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:1843
getTemplateDom($title)
Get the semi-parsed DOM representation of a template with a given title, and its redirect destination...
Definition: Parser.php:3846
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:44
static decodeCharReferences($text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string...
Definition: Sanitizer.php:1461
openList($char)
These next three functions open, continue, and close the list element appropriate to the prefix chara...
Definition: Parser.php:2454
cleanSig($text, $parsing=false)
Clean up signature text.
Definition: Parser.php:4989
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
static factory($mode=false, IContextSource $context=null)
Get a new image gallery.
$wgLanguageCode
Site language code.
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static edits()
Definition: SiteStats.php:129
Class for asserting that a callback happens when an dummy object leaves scope.
$wgExtraInterlanguageLinkPrefixes
List of additional interwiki prefixes that should be treated as interlanguage links (i...
startExternalParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Set up some variables which are usually set up in parse() so that an external function can call some ...
Definition: Parser.php:5040
wfCgiToArray($query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
static capturePath(Title $title, IContextSource $context)
Just like executePath() but will override global variables and execute the page in "inclusion" mode...
const NO_TEMPLATES
addTrackingCategory($msg)
Definition: Parser.php:4385
replaceInternalLinks($s)
Process [[ ]] wikilinks.
Definition: Parser.php:2022
$mVarCache
Definition: Parser.php:150
$wgStylePath
The URL path of the skins directory.
disableCache()
Set a flag in the output object indicating that the content is dynamic and shouldn't be cached...
Definition: Parser.php:5756
$mRevisionObject
Definition: Parser.php:218
static normalizeSectionNameWhitespace($section)
Normalizes whitespace in a section name, such as might be returned by Parser::stripSectionName(), for use in the id's that are used for section links.
Definition: Sanitizer.php:1342
internalParse($text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1221
Title $mTitle
Definition: Parser.php:215
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: Parser.php:285
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
bool $mInParse
Recursive call protection.
Definition: Parser.php:251
Some quick notes on the file repository architecture Functionality is
Definition: README:3
getRevisionTimestamp()
Get the timestamp associated with the current revision, adjusted for the default server-local timesta...
Definition: Parser.php:6042
namespace and then decline to actually register it file or subcat img or subcat RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books $tag
Definition: hooks.txt:891
static stripOuterParagraph($html)
Strip outer.
Definition: Parser.php:6426
static register($parser)
$mRevIdForTs
Definition: Parser.php:223
static singleton()
Get an instance of this class.
Definition: LinkCache.php:61
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition: design.txt:25
static normalizeSubpageLink($contextTitle, $target, &$text)
Definition: Linker.php:1562
parseWidthParam($value)
Parsed a width param of imagelink like 300px or 200x300px.
Definition: Parser.php:6373
$mStripList
Definition: Parser.php:148
$mFunctionTagHooks
Definition: Parser.php:147
fetchScaryTemplateMaybeFromCache($url)
Definition: Parser.php:4126
const OT_PLAIN
Definition: Defines.php:230
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
fetchCurrentRevisionOfTitle($title)
Fetch the current revision of a given title.
Definition: Parser.php:3889
$mRevisionTimestamp
Definition: Parser.php:220
$mImageParams
Definition: Parser.php:151
stripAltText($caption, $holders)
Definition: Parser.php:5733
doAllQuotes($text)
Replace single quotes with HTML markup.
Definition: Parser.php:1568
static normalizeUrlComponent($component, $unsafe)
Definition: Parser.php:1940
if($limit) $timestamp
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way...
Definition: Parser.php:73
namespace and then decline to actually register it file or subcat img or subcat RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:977
$mInPre
Definition: Parser.php:186
setHook($tag, $callback)
Create an HTML-style tag, e.g.
Definition: Parser.php:5115
const OT_WIKI
Definition: Defines.php:227
Preprocessor $mPreprocessor
Definition: Parser.php:172
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:892
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of normal(non-web) applications--they might conflict with distributors'policies
static getInstance($ts=false)
Get a timestamp instance in GMT.
const NS_MEDIA
Definition: Defines.php:57
closeList($char)
Definition: Parser.php:2506
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
replaceVariables($text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:3333
const RECOVER_ORIG
wfMatchesDomainList($url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
MediaWiki exception.
Definition: MWException.php:26
StripState $mStripState
Definition: Parser.php:184
$mDefaultSort
Definition: Parser.php:194
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise...
Definition: Parser.php:880
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
incrementIncludeSize($type, $size)
Increment an include size counter.
Definition: Parser.php:4303
getStripList()
Get a list of strippable XML-like elements.
Definition: Parser.php:991
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 after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock()-offset Set to overwrite offset parameter in $wgRequest set to ''to unsetoffset-wrap String Wrap the message in html(usually something like"&lt
const EXT_IMAGE_REGEX
Definition: Parser.php:95
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition: Parser.php:5052
$params
const NS_CATEGORY
Definition: Defines.php:83
static makeHeadline($level, $attribs, $anchor, $html, $link, $legacyAnchor=false)
Create a headline for content.
Definition: Linker.php:1824
static extractTagsAndParams($elements, $text, &$matches, $uniq_prefix=null)
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition: Parser.php:921
and(b) You must cause any modified files to carry prominent notices stating that You changed the files
doTableStuff($text)
parse the wiki syntax used to render tables
Definition: Parser.php:1018
wfDeprecated($function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getRevisionSize()
Get the size of the revision.
Definition: Parser.php:6085
$mImageParamsMagicArray
Definition: Parser.php:152
LinkHolderArray $mLinkHolders
Definition: Parser.php:190
$wgNoFollowDomainExceptions
If this is set to an array of domains, external links to these domain names (or any subdomains) will ...
static register($parser)
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition: deferred.txt:4
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to and or sell copies of the and to permit persons to whom the Software is furnished to do so
Definition: LICENSE.txt:10
$wgTranscludeCacheExpiry
Expiry time for transcluded templates cached in transcache database table.
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
closeParagraph()
#@+ Used by doBlockLevels()
Definition: Parser.php:2410
const DB_SLAVE
Definition: Defines.php:46
preSaveTransform($text, Title $title, User $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition: Parser.php:4815
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:852
$buffer
Allows to change the fields on the form that will be generated are created Can be used to omit specific feeds from being outputted You must not use this hook to add use OutputPage::addFeedLink() instead.&$feedLinks conditions will AND in the final query as a Content object as a Content object $title
Definition: hooks.txt:314
static hasSubpages($index)
Does the namespace allow subpages?
formatHeadings($text, $origText, $isMain=true)
This function accomplishes several tasks: 1) Auto-number headings if that option is enabled 2) Add an...
Definition: Parser.php:4405
getConverterLanguage()
Get the language object for language conversion.
Definition: Parser.php:870
static tocUnindent($level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1723
nextItem($char)
TODO: document.
Definition: Parser.php:2480
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
static tocLine($anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1738
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition: design.txt:12
getExternalLinkAttribs($url=false)
Get an associative array of additional HTML attributes appropriate for a particular external link...
Definition: Parser.php:1873
$mInputSize
Definition: Parser.php:224
magicword txt Magic Words are some phrases used in the wikitext They are used for two things
Definition: magicword.txt:4
getUserSig(&$user, $nickname=false, $fancySig=null)
Fetch the user's signature text, if any