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  const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F\p{Zs}]+)
91  \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu';
92 
93  # Regular expression for a non-newline space
94  const SPACE_NOT_NL = '(?:\t|&nbsp;|&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})';
95 
96  # State constants for the definition list colon extraction
97  const COLON_STATE_TEXT = 0;
98  const COLON_STATE_TAG = 1;
105 
106  # Flags for preprocessToDom
107  const PTD_FOR_INCLUSION = 1;
108 
109  # Allowed values for $this->mOutputType
110  # Parameter to startExternalParse().
111  const OT_HTML = 1; # like parse()
112  const OT_WIKI = 2; # like preSaveTransform()
114  const OT_MSG = 3;
115  const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
116 
129  const MARKER_SUFFIX = "-QINU\x7f";
130  const MARKER_PREFIX = "\x7fUNIQ-";
131 
132  # Markers used for wrapping the table of contents
133  const TOC_START = '<mw:toc>';
134  const TOC_END = '</mw:toc>';
135 
136  # Persistent:
137  public $mTagHooks = array();
139  public $mFunctionHooks = array();
140  public $mFunctionSynonyms = array( 0 => array(), 1 => array() );
142  public $mStripList = array();
144  public $mVarCache = array();
145  public $mImageParams = array();
147  public $mMarkerIndex = 0;
148  public $mFirstCall = true;
149 
150  # Initialised by initialiseVariables()
151 
155  public $mVariables;
160  public $mSubstWords;
161  # Initialised in constructor
163 
164  # Initialized in getPreprocessor()
167 
168  # Cleared with clearState():
172  public $mOutput;
174 
178  public $mStripState;
184  public $mLinkHolders;
185 
186  public $mLinkID;
188  public $mDefaultSort;
190  public $mExpensiveFunctionCount; # number of expensive parser function calls
196  public $mUser; # User object; only used when doing pre-save transform
198  # Temporary
199  # These are variables reset at least once per parse regardless of $clearState
204  public $mOptions;
205 
209  public $mTitle; # Title context, used for self-link rendering and similar things
210  public $mOutputType; # Output type, one of the OT_xxx constants
211  public $ot; # Shortcut alias, see setOutputType()
212  public $mRevisionObject; # The revision object of the specified revision ID
213  public $mRevisionId; # ID to display in {{REVISIONID}} tags
214  public $mRevisionTimestamp; # The timestamp of the specified revision ID
215  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
216  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
217  public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
218  public $mInputSize = false; # For {{PAGESIZE}} on current page.
219 
231  public $mLangLinkLanguages;
232 
239  public $currentRevisionCache;
240 
245  public $mInParse = false;
246 
248  protected $mProfiler;
249 
253  public function __construct( $conf = array() ) {
254  $this->mConf = $conf;
255  $this->mUrlProtocols = wfUrlProtocols();
256  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
257  self::EXT_LINK_URL_CLASS . '+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
258  if ( isset( $conf['preprocessorClass'] ) ) {
259  $this->mPreprocessorClass = $conf['preprocessorClass'];
260  } elseif ( defined( 'HPHP_VERSION' ) ) {
261  # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
262  $this->mPreprocessorClass = 'Preprocessor_Hash';
263  } elseif ( extension_loaded( 'domxml' ) ) {
264  # PECL extension that conflicts with the core DOM extension (bug 13770)
265  wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
266  $this->mPreprocessorClass = 'Preprocessor_Hash';
267  } elseif ( extension_loaded( 'dom' ) ) {
268  $this->mPreprocessorClass = 'Preprocessor_DOM';
269  } else {
270  $this->mPreprocessorClass = 'Preprocessor_Hash';
271  }
272  wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
273  }
274 
278  public function __destruct() {
279  if ( isset( $this->mLinkHolders ) ) {
280  unset( $this->mLinkHolders );
281  }
282  foreach ( $this as $name => $value ) {
283  unset( $this->$name );
284  }
285  }
286 
290  public function __clone() {
291  $this->mInParse = false;
292 
293  // Bug 56226: When you create a reference "to" an object field, that
294  // makes the object field itself be a reference too (until the other
295  // reference goes out of scope). When cloning, any field that's a
296  // reference is copied as a reference in the new object. Both of these
297  // are defined PHP5 behaviors, as inconvenient as it is for us when old
298  // hooks from PHP4 days are passing fields by reference.
299  foreach ( array( 'mStripState', 'mVarCache' ) as $k ) {
300  // Make a non-reference copy of the field, then rebind the field to
301  // reference the new copy.
302  $tmp = $this->$k;
303  $this->$k =& $tmp;
304  unset( $tmp );
305  }
306 
307  Hooks::run( 'ParserCloned', array( $this ) );
308  }
309 
313  public function firstCallInit() {
314  if ( !$this->mFirstCall ) {
315  return;
316  }
317  $this->mFirstCall = false;
318 
320  CoreTagHooks::register( $this );
321  $this->initialiseVariables();
322 
323  Hooks::run( 'ParserFirstCallInit', array( &$this ) );
324  }
325 
331  public function clearState() {
332  if ( $this->mFirstCall ) {
333  $this->firstCallInit();
334  }
335  $this->mOutput = new ParserOutput;
336  $this->mOptions->registerWatcher( array( $this->mOutput, 'recordOption' ) );
337  $this->mAutonumber = 0;
338  $this->mLastSection = '';
339  $this->mDTopen = false;
340  $this->mIncludeCount = array();
341  $this->mArgStack = false;
342  $this->mInPre = false;
343  $this->mLinkHolders = new LinkHolderArray( $this );
344  $this->mLinkID = 0;
345  $this->mRevisionObject = $this->mRevisionTimestamp =
346  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
347  $this->mVarCache = array();
348  $this->mUser = null;
349  $this->mLangLinkLanguages = array();
350  $this->currentRevisionCache = null;
351 
352  $this->mStripState = new StripState;
353 
354  # Clear these on every parse, bug 4549
355  $this->mTplRedirCache = $this->mTplDomCache = array();
356 
357  $this->mShowToc = true;
358  $this->mForceTocPosition = false;
359  $this->mIncludeSizes = array(
360  'post-expand' => 0,
361  'arg' => 0,
362  );
363  $this->mPPNodeCount = 0;
364  $this->mGeneratedPPNodeCount = 0;
365  $this->mHighestExpansionDepth = 0;
366  $this->mDefaultSort = false;
367  $this->mHeadings = array();
368  $this->mDoubleUnderscores = array();
369  $this->mExpensiveFunctionCount = 0;
371  # Fix cloning
372  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
373  $this->mPreprocessor = null;
374  }
375 
376  $this->mProfiler = new SectionProfiler();
377 
378  Hooks::run( 'ParserClearState', array( &$this ) );
379  }
380 
393  public function parse( $text, Title $title, ParserOptions $options,
394  $linestart = true, $clearState = true, $revid = null
395  ) {
402 
403  if ( $clearState ) {
404  // We use U+007F DELETE to construct strip markers, so we have to make
405  // sure that this character does not occur in the input text.
406  $text = strtr( $text, "\x7f", "?" );
407  $magicScopeVariable = $this->lock();
408  }
409 
410  $this->startParse( $title, $options, self::OT_HTML, $clearState );
411 
412  $this->currentRevisionCache = null;
413  $this->mInputSize = strlen( $text );
414  if ( $this->mOptions->getEnableLimitReport() ) {
415  $this->mOutput->resetParseStartTime();
416  }
417 
418  $oldRevisionId = $this->mRevisionId;
419  $oldRevisionObject = $this->mRevisionObject;
420  $oldRevisionTimestamp = $this->mRevisionTimestamp;
421  $oldRevisionUser = $this->mRevisionUser;
422  $oldRevisionSize = $this->mRevisionSize;
423  if ( $revid !== null ) {
424  $this->mRevisionId = $revid;
425  $this->mRevisionObject = null;
426  $this->mRevisionTimestamp = null;
427  $this->mRevisionUser = null;
428  $this->mRevisionSize = null;
429  }
430 
431  Hooks::run( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
432  # No more strip!
433  Hooks::run( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
434  $text = $this->internalParse( $text );
435  Hooks::run( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) );
436 
437  $text = $this->internalParseHalfParsed( $text, true, $linestart );
438 
446  if ( !( $options->getDisableTitleConversion()
447  || isset( $this->mDoubleUnderscores['nocontentconvert'] )
448  || isset( $this->mDoubleUnderscores['notitleconvert'] )
449  || $this->mOutput->getDisplayTitle() !== false )
450  ) {
451  $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
452  if ( $convruletitle ) {
453  $this->mOutput->setTitleText( $convruletitle );
454  } else {
455  $titleText = $this->getConverterLanguage()->convertTitle( $title );
456  $this->mOutput->setTitleText( $titleText );
457  }
458  }
459 
460  if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
461  $this->limitationWarn( 'expensive-parserfunction',
462  $this->mExpensiveFunctionCount,
463  $this->mOptions->getExpensiveParserFunctionLimit()
464  );
465  }
466 
467  # Information on include size limits, for the benefit of users who try to skirt them
468  if ( $this->mOptions->getEnableLimitReport() ) {
469  $max = $this->mOptions->getMaxIncludeSize();
470 
471  $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
472  if ( $cpuTime !== null ) {
473  $this->mOutput->setLimitReportData( 'limitreport-cputime',
474  sprintf( "%.3f", $cpuTime )
475  );
476  }
477 
478  $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
479  $this->mOutput->setLimitReportData( 'limitreport-walltime',
480  sprintf( "%.3f", $wallTime )
481  );
482 
483  $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
484  array( $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() )
485  );
486  $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
487  array( $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() )
488  );
489  $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
490  array( $this->mIncludeSizes['post-expand'], $max )
491  );
492  $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
493  array( $this->mIncludeSizes['arg'], $max )
494  );
495  $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
496  array( $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() )
497  );
498  $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
499  array( $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() )
500  );
501  Hooks::run( 'ParserLimitReportPrepare', array( $this, $this->mOutput ) );
502 
503  $limitReport = "NewPP limit report\n";
504  if ( $wgShowHostnames ) {
505  $limitReport .= 'Parsed by ' . wfHostname() . "\n";
506  }
507  foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
508  if ( Hooks::run( 'ParserLimitReportFormat',
509  array( $key, &$value, &$limitReport, false, false )
510  ) ) {
511  $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
512  $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) )
513  ->inLanguage( 'en' )->useDatabase( false );
514  if ( !$valueMsg->exists() ) {
515  $valueMsg = new RawMessage( '$1' );
516  }
517  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
518  $valueMsg->params( $value );
519  $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
520  }
521  }
522  }
523  // Since we're not really outputting HTML, decode the entities and
524  // then re-encode the things that need hiding inside HTML comments.
525  $limitReport = htmlspecialchars_decode( $limitReport );
526  Hooks::run( 'ParserLimitReport', array( $this, &$limitReport ) );
527 
528  // Sanitize for comment. Note '‐' in the replacement is U+2010,
529  // which looks much like the problematic '-'.
530  $limitReport = str_replace( array( '-', '&' ), array( '‐', '&amp;' ), $limitReport );
531  $text .= "\n<!-- \n$limitReport-->\n";
532 
533  // Add on template profiling data
534  $dataByFunc = $this->mProfiler->getFunctionStats();
535  uasort( $dataByFunc, function ( $a, $b ) {
536  return $a['real'] < $b['real']; // descending order
537  } );
538  $profileReport = "Transclusion expansion time report (%,ms,calls,template)\n";
539  foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
540  $profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n",
541  $item['%real'], $item['real'], $item['calls'],
542  htmlspecialchars( $item['name'] ) );
543  }
544  $text .= "\n<!-- \n$profileReport-->\n";
545 
546  if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
547  wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
548  $this->mTitle->getPrefixedDBkey() );
549  }
550  }
551  $this->mOutput->setText( $text );
552 
553  $this->mRevisionId = $oldRevisionId;
554  $this->mRevisionObject = $oldRevisionObject;
555  $this->mRevisionTimestamp = $oldRevisionTimestamp;
556  $this->mRevisionUser = $oldRevisionUser;
557  $this->mRevisionSize = $oldRevisionSize;
558  $this->mInputSize = false;
559  $this->currentRevisionCache = null;
560 
561  return $this->mOutput;
562  }
586  public function recursiveTagParse( $text, $frame = false ) {
587  Hooks::run( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
588  Hooks::run( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
589  $text = $this->internalParse( $text, false, $frame );
590  return $text;
591  }
592 
610  public function recursiveTagParseFully( $text, $frame = false ) {
611  $text = $this->recursiveTagParse( $text, $frame );
612  $text = $this->internalParseHalfParsed( $text, false );
613  return $text;
614  }
615 
627  public function preprocess( $text, Title $title = null,
628  ParserOptions $options, $revid = null, $frame = false
629  ) {
630  $magicScopeVariable = $this->lock();
631  $this->startParse( $title, $options, self::OT_PREPROCESS, true );
632  if ( $revid !== null ) {
633  $this->mRevisionId = $revid;
634  }
635  Hooks::run( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
636  Hooks::run( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
637  $text = $this->replaceVariables( $text, $frame );
638  $text = $this->mStripState->unstripBoth( $text );
639  return $text;
640  }
641 
651  public function recursivePreprocess( $text, $frame = false ) {
652  $text = $this->replaceVariables( $text, $frame );
653  $text = $this->mStripState->unstripBoth( $text );
654  return $text;
655  }
656 
670  public function getPreloadText( $text, Title $title, ParserOptions $options, $params = array() ) {
671  $msg = new RawMessage( $text );
672  $text = $msg->params( $params )->plain();
673 
674  # Parser (re)initialisation
675  $magicScopeVariable = $this->lock();
676  $this->startParse( $title, $options, self::OT_PLAIN, true );
677 
679  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
680  $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
681  $text = $this->mStripState->unstripBoth( $text );
682  return $text;
683  }
684 
691  public static function getRandomString() {
692  wfDeprecated( __METHOD__, '1.26' );
693  return wfRandomString( 16 );
694  }
695 
702  public function setUser( $user ) {
703  $this->mUser = $user;
704  }
705 
712  public function uniqPrefix() {
713  wfDeprecated( __METHOD__, '1.26' );
714  return self::MARKER_PREFIX;
715  }
716 
722  public function setTitle( $t ) {
723  if ( !$t ) {
724  $t = Title::newFromText( 'NO TITLE' );
725  }
726 
727  if ( $t->hasFragment() ) {
728  # Strip the fragment to avoid various odd effects
729  $this->mTitle = clone $t;
730  $this->mTitle->setFragment( '' );
731  } else {
732  $this->mTitle = $t;
733  }
734  }
735 
741  public function getTitle() {
742  return $this->mTitle;
743  }
744 
751  public function Title( $x = null ) {
752  return wfSetVar( $this->mTitle, $x );
753  }
760  public function setOutputType( $ot ) {
761  $this->mOutputType = $ot;
762  # Shortcut alias
763  $this->ot = array(
764  'html' => $ot == self::OT_HTML,
765  'wiki' => $ot == self::OT_WIKI,
766  'pre' => $ot == self::OT_PREPROCESS,
767  'plain' => $ot == self::OT_PLAIN,
768  );
769  }
770 
777  public function OutputType( $x = null ) {
778  return wfSetVar( $this->mOutputType, $x );
779  }
780 
786  public function getOutput() {
787  return $this->mOutput;
788  }
795  public function getOptions() {
797  }
798 
805  public function Options( $x = null ) {
806  return wfSetVar( $this->mOptions, $x );
807  }
808 
812  public function nextLinkID() {
813  return $this->mLinkID++;
814  }
815 
819  public function setLinkID( $id ) {
820  $this->mLinkID = $id;
821  }
822 
827  public function getFunctionLang() {
828  return $this->getTargetLanguage();
829  }
830 
840  public function getTargetLanguage() {
841  $target = $this->mOptions->getTargetLanguage();
842 
843  if ( $target !== null ) {
844  return $target;
845  } elseif ( $this->mOptions->getInterfaceMessage() ) {
846  return $this->mOptions->getUserLangObj();
847  } elseif ( is_null( $this->mTitle ) ) {
848  throw new MWException( __METHOD__ . ': $this->mTitle is null' );
849  }
850 
851  return $this->mTitle->getPageLanguage();
852  }
853 
858  public function getConverterLanguage() {
859  return $this->getTargetLanguage();
860  }
861 
868  public function getUser() {
869  if ( !is_null( $this->mUser ) ) {
870  return $this->mUser;
871  }
872  return $this->mOptions->getUser();
873  }
874 
880  public function getPreprocessor() {
881  if ( !isset( $this->mPreprocessor ) ) {
882  $class = $this->mPreprocessorClass;
883  $this->mPreprocessor = new $class( $this );
884  }
885  return $this->mPreprocessor;
886  }
887 
909  public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) {
910  if ( $uniq_prefix !== null ) {
911  wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' );
912  }
913  static $n = 1;
914  $stripped = '';
915  $matches = array();
916 
917  $taglist = implode( '|', $elements );
918  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
919 
920  while ( $text != '' ) {
921  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
922  $stripped .= $p[0];
923  if ( count( $p ) < 5 ) {
924  break;
925  }
926  if ( count( $p ) > 5 ) {
927  # comment
928  $element = $p[4];
929  $attributes = '';
930  $close = '';
931  $inside = $p[5];
932  } else {
933  # tag
934  $element = $p[1];
935  $attributes = $p[2];
936  $close = $p[3];
937  $inside = $p[4];
938  }
939 
940  $marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
941  $stripped .= $marker;
942 
943  if ( $close === '/>' ) {
944  # Empty element tag, <tag />
945  $content = null;
946  $text = $inside;
947  $tail = null;
948  } else {
949  if ( $element === '!--' ) {
950  $end = '/(-->)/';
951  } else {
952  $end = "/(<\\/$element\\s*>)/i";
953  }
954  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
955  $content = $q[0];
956  if ( count( $q ) < 3 ) {
957  # No end tag -- let it run out to the end of the text.
958  $tail = '';
959  $text = '';
960  } else {
961  $tail = $q[1];
962  $text = $q[2];
963  }
964  }
965 
966  $matches[$marker] = array( $element,
967  $content,
968  Sanitizer::decodeTagAttributes( $attributes ),
969  "<$element$attributes$close$content$tail" );
970  }
971  return $stripped;
972  }
973 
979  public function getStripList() {
980  return $this->mStripList;
981  }
982 
992  public function insertStripItem( $text ) {
993  $marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
994  $this->mMarkerIndex++;
995  $this->mStripState->addGeneral( $marker, $text );
996  return $marker;
997  }
998 
1006  public function doTableStuff( $text ) {
1007 
1008  $lines = StringUtils::explode( "\n", $text );
1009  $out = '';
1010  $td_history = array(); # Is currently a td tag open?
1011  $last_tag_history = array(); # Save history of last lag activated (td, th or caption)
1012  $tr_history = array(); # Is currently a tr tag open?
1013  $tr_attributes = array(); # history of tr attributes
1014  $has_opened_tr = array(); # Did this table open a <tr> element?
1015  $indent_level = 0; # indent level of the table
1016 
1017  foreach ( $lines as $outLine ) {
1018  $line = trim( $outLine );
1019 
1020  if ( $line === '' ) { # empty line, go to next line
1021  $out .= $outLine . "\n";
1022  continue;
1023  }
1024 
1025  $first_character = $line[0];
1026  $matches = array();
1027 
1028  if ( preg_match( '/^(:*)\{\|(.*)$/', $line, $matches ) ) {
1029  # First check if we are starting a new table
1030  $indent_level = strlen( $matches[1] );
1031 
1032  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1033  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1034 
1035  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1036  array_push( $td_history, false );
1037  array_push( $last_tag_history, '' );
1038  array_push( $tr_history, false );
1039  array_push( $tr_attributes, '' );
1040  array_push( $has_opened_tr, false );
1041  } elseif ( count( $td_history ) == 0 ) {
1042  # Don't do any of the following
1043  $out .= $outLine . "\n";
1044  continue;
1045  } elseif ( substr( $line, 0, 2 ) === '|}' ) {
1046  # We are ending a table
1047  $line = '</table>' . substr( $line, 2 );
1048  $last_tag = array_pop( $last_tag_history );
1049 
1050  if ( !array_pop( $has_opened_tr ) ) {
1051  $line = "<tr><td></td></tr>{$line}";
1052  }
1053 
1054  if ( array_pop( $tr_history ) ) {
1055  $line = "</tr>{$line}";
1056  }
1057 
1058  if ( array_pop( $td_history ) ) {
1059  $line = "</{$last_tag}>{$line}";
1060  }
1061  array_pop( $tr_attributes );
1062  $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1063  } elseif ( substr( $line, 0, 2 ) === '|-' ) {
1064  # Now we have a table row
1065  $line = preg_replace( '#^\|-+#', '', $line );
1066 
1067  # Whats after the tag is now only attributes
1068  $attributes = $this->mStripState->unstripBoth( $line );
1069  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1070  array_pop( $tr_attributes );
1071  array_push( $tr_attributes, $attributes );
1072 
1073  $line = '';
1074  $last_tag = array_pop( $last_tag_history );
1075  array_pop( $has_opened_tr );
1076  array_push( $has_opened_tr, true );
1077 
1078  if ( array_pop( $tr_history ) ) {
1079  $line = '</tr>';
1080  }
1081 
1082  if ( array_pop( $td_history ) ) {
1083  $line = "</{$last_tag}>{$line}";
1084  }
1085 
1086  $outLine = $line;
1087  array_push( $tr_history, false );
1088  array_push( $td_history, false );
1089  array_push( $last_tag_history, '' );
1090  } elseif ( $first_character === '|'
1091  || $first_character === '!'
1092  || substr( $line, 0, 2 ) === '|+'
1093  ) {
1094  # This might be cell elements, td, th or captions
1095  if ( substr( $line, 0, 2 ) === '|+' ) {
1096  $first_character = '+';
1097  $line = substr( $line, 1 );
1098  }
1099 
1100  $line = substr( $line, 1 );
1101 
1102  if ( $first_character === '!' ) {
1103  $line = str_replace( '!!', '||', $line );
1104  }
1105 
1106  # Split up multiple cells on the same line.
1107  # FIXME : This can result in improper nesting of tags processed
1108  # by earlier parser steps, but should avoid splitting up eg
1109  # attribute values containing literal "||".
1110  $cells = StringUtils::explodeMarkup( '||', $line );
1111 
1112  $outLine = '';
1113 
1114  # Loop through each table cell
1115  foreach ( $cells as $cell ) {
1116  $previous = '';
1117  if ( $first_character !== '+' ) {
1118  $tr_after = array_pop( $tr_attributes );
1119  if ( !array_pop( $tr_history ) ) {
1120  $previous = "<tr{$tr_after}>\n";
1121  }
1122  array_push( $tr_history, true );
1123  array_push( $tr_attributes, '' );
1124  array_pop( $has_opened_tr );
1125  array_push( $has_opened_tr, true );
1126  }
1127 
1128  $last_tag = array_pop( $last_tag_history );
1129 
1130  if ( array_pop( $td_history ) ) {
1131  $previous = "</{$last_tag}>\n{$previous}";
1132  }
1133 
1134  if ( $first_character === '|' ) {
1135  $last_tag = 'td';
1136  } elseif ( $first_character === '!' ) {
1137  $last_tag = 'th';
1138  } elseif ( $first_character === '+' ) {
1139  $last_tag = 'caption';
1140  } else {
1141  $last_tag = '';
1142  }
1143 
1144  array_push( $last_tag_history, $last_tag );
1145 
1146  # A cell could contain both parameters and data
1147  $cell_data = explode( '|', $cell, 2 );
1148 
1149  # Bug 553: Note that a '|' inside an invalid link should not
1150  # be mistaken as delimiting cell parameters
1151  if ( strpos( $cell_data[0], '[[' ) !== false ) {
1152  $cell = "{$previous}<{$last_tag}>{$cell}";
1153  } elseif ( count( $cell_data ) == 1 ) {
1154  $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1155  } else {
1156  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1157  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1158  $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1159  }
1160 
1161  $outLine .= $cell;
1162  array_push( $td_history, true );
1163  }
1164  }
1165  $out .= $outLine . "\n";
1166  }
1167 
1168  # Closing open td, tr && table
1169  while ( count( $td_history ) > 0 ) {
1170  if ( array_pop( $td_history ) ) {
1171  $out .= "</td>\n";
1172  }
1173  if ( array_pop( $tr_history ) ) {
1174  $out .= "</tr>\n";
1175  }
1176  if ( !array_pop( $has_opened_tr ) ) {
1177  $out .= "<tr><td></td></tr>\n";
1178  }
1179 
1180  $out .= "</table>\n";
1181  }
1182 
1183  # Remove trailing line-ending (b/c)
1184  if ( substr( $out, -1 ) === "\n" ) {
1185  $out = substr( $out, 0, -1 );
1186  }
1187 
1188  # special case: don't return empty table
1189  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1190  $out = '';
1191  }
1192 
1193  return $out;
1194  }
1195 
1208  public function internalParse( $text, $isMain = true, $frame = false ) {
1209 
1210  $origText = $text;
1211 
1212  # Hook to suspend the parser in this state
1213  if ( !Hooks::run( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
1214  return $text;
1215  }
1216 
1217  # if $frame is provided, then use $frame for replacing any variables
1218  if ( $frame ) {
1219  # use frame depth to infer how include/noinclude tags should be handled
1220  # depth=0 means this is the top-level document; otherwise it's an included document
1221  if ( !$frame->depth ) {
1222  $flag = 0;
1223  } else {
1224  $flag = Parser::PTD_FOR_INCLUSION;
1225  }
1226  $dom = $this->preprocessToDom( $text, $flag );
1227  $text = $frame->expand( $dom );
1228  } else {
1229  # if $frame is not provided, then use old-style replaceVariables
1230  $text = $this->replaceVariables( $text );
1231  }
1232 
1233  Hooks::run( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
1234  $text = Sanitizer::removeHTMLtags(
1235  $text,
1236  array( &$this, 'attributeStripCallback' ),
1237  false,
1238  array_keys( $this->mTransparentTagHooks )
1239  );
1240  Hooks::run( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
1241 
1242  # Tables need to come after variable replacement for things to work
1243  # properly; putting them before other transformations should keep
1244  # exciting things like link expansions from showing up in surprising
1245  # places.
1246  $text = $this->doTableStuff( $text );
1247 
1248  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1249 
1250  $text = $this->doDoubleUnderscore( $text );
1251 
1252  $text = $this->doHeadings( $text );
1253  $text = $this->replaceInternalLinks( $text );
1254  $text = $this->doAllQuotes( $text );
1255  $text = $this->replaceExternalLinks( $text );
1256 
1257  # replaceInternalLinks may sometimes leave behind
1258  # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1259  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1260 
1261  $text = $this->doMagicLinks( $text );
1262  $text = $this->formatHeadings( $text, $origText, $isMain );
1263 
1264  return $text;
1265  }
1266 
1276  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1278 
1279  $text = $this->mStripState->unstripGeneral( $text );
1280 
1281  if ( $isMain ) {
1282  Hooks::run( 'ParserAfterUnstrip', array( &$this, &$text ) );
1283  }
1284 
1285  # Clean up special characters, only run once, next-to-last before doBlockLevels
1286  $fixtags = array(
1287  # french spaces, last one Guillemet-left
1288  # only if there is something before the space
1289  '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1290  # french spaces, Guillemet-right
1291  '/(\\302\\253) /' => '\\1&#160;',
1292  '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1293  );
1294  $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1295 
1296  $text = $this->doBlockLevels( $text, $linestart );
1297 
1298  $this->replaceLinkHolders( $text );
1299 
1307  if ( !( $this->mOptions->getDisableContentConversion()
1308  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1309  ) {
1310  if ( !$this->mOptions->getInterfaceMessage() ) {
1311  # The position of the convert() call should not be changed. it
1312  # assumes that the links are all replaced and the only thing left
1313  # is the <nowiki> mark.
1314  $text = $this->getConverterLanguage()->convert( $text );
1315  }
1316  }
1317 
1318  $text = $this->mStripState->unstripNoWiki( $text );
1319 
1320  if ( $isMain ) {
1321  Hooks::run( 'ParserBeforeTidy', array( &$this, &$text ) );
1322  }
1323 
1324  $text = $this->replaceTransparentTags( $text );
1325  $text = $this->mStripState->unstripGeneral( $text );
1326 
1327  $text = Sanitizer::normalizeCharReferences( $text );
1328 
1329  if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
1330  $text = MWTidy::tidy( $text );
1331  } else {
1332  # attempt to sanitize at least some nesting problems
1333  # (bug #2702 and quite a few others)
1334  $tidyregs = array(
1335  # ''Something [http://www.cool.com cool''] -->
1336  # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1337  '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1338  '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1339  # fix up an anchor inside another anchor, only
1340  # at least for a single single nested link (bug 3695)
1341  '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1342  '\\1\\2</a>\\3</a>\\1\\4</a>',
1343  # fix div inside inline elements- doBlockLevels won't wrap a line which
1344  # contains a div, so fix it up here; replace
1345  # div with escaped text
1346  '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1347  '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1348  # remove empty italic or bold tag pairs, some
1349  # introduced by rules above
1350  '/<([bi])><\/\\1>/' => '',
1351  );
1352 
1353  $text = preg_replace(
1354  array_keys( $tidyregs ),
1355  array_values( $tidyregs ),
1356  $text );
1357  }
1358 
1359  if ( $isMain ) {
1360  Hooks::run( 'ParserAfterTidy', array( &$this, &$text ) );
1361  }
1362 
1363  return $text;
1364  }
1365 
1377  public function doMagicLinks( $text ) {
1378  $prots = wfUrlProtocolsWithoutProtRel();
1379  $urlChar = self::EXT_LINK_URL_CLASS;
1380  $space = self::SPACE_NOT_NL; # non-newline space
1381  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1382  $spaces = "$space++"; # possessive match of 1 or more spaces
1383  $text = preg_replace_callback(
1384  '!(?: # Start cases
1385  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1386  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1387  (\b(?i:$prots)$urlChar+) | # m[3]: Free external links
1388  \b(?:RFC|PMID) $spaces # m[4]: RFC or PMID, capture number
1389  ([0-9]+)\b |
1390  \bISBN $spaces ( # m[5]: ISBN, capture number
1391  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1392  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1393  [0-9Xx] # check digit
1394  )\b
1395  )!xu", array( &$this, 'magicLinkCallback' ), $text );
1396  return $text;
1397  }
1398 
1404  public function magicLinkCallback( $m ) {
1405  if ( isset( $m[1] ) && $m[1] !== '' ) {
1406  # Skip anchor
1407  return $m[0];
1408  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1409  # Skip HTML element
1410  return $m[0];
1411  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1412  # Free external link
1413  return $this->makeFreeExternalLink( $m[0] );
1414  } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
1415  # RFC or PMID
1416  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1417  $keyword = 'RFC';
1418  $urlmsg = 'rfcurl';
1419  $cssClass = 'mw-magiclink-rfc';
1420  $id = $m[4];
1421  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1422  $keyword = 'PMID';
1423  $urlmsg = 'pubmedurl';
1424  $cssClass = 'mw-magiclink-pmid';
1425  $id = $m[4];
1426  } else {
1427  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1428  substr( $m[0], 0, 20 ) . '"' );
1429  }
1430  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1431  return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass );
1432  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1433  # ISBN
1434  $isbn = $m[5];
1435  $space = self::SPACE_NOT_NL; # non-newline space
1436  $isbn = preg_replace( "/$space/", ' ', $isbn );
1437  $num = strtr( $isbn, array(
1438  '-' => '',
1439  ' ' => '',
1440  'x' => 'X',
1441  ) );
1442  $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
1443  return '<a href="' .
1444  htmlspecialchars( $titleObj->getLocalURL() ) .
1445  "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1446  } else {
1447  return $m[0];
1448  }
1449  }
1450 
1459  public function makeFreeExternalLink( $url ) {
1460 
1461  $trail = '';
1462 
1463  # The characters '<' and '>' (which were escaped by
1464  # removeHTMLtags()) should not be included in
1465  # URLs, per RFC 2396.
1466  $m2 = array();
1467  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1468  $trail = substr( $url, $m2[0][1] ) . $trail;
1469  $url = substr( $url, 0, $m2[0][1] );
1470  }
1471 
1472  # Move trailing punctuation to $trail
1473  $sep = ',;\.:!?';
1474  # If there is no left bracket, then consider right brackets fair game too
1475  if ( strpos( $url, '(' ) === false ) {
1476  $sep .= ')';
1477  }
1478 
1479  $urlRev = strrev( $url );
1480  $numSepChars = strspn( $urlRev, $sep );
1481  # Don't break a trailing HTML entity by moving the ; into $trail
1482  # This is in hot code, so use substr_compare to avoid having to
1483  # create a new string object for the comparison
1484  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1485  # more optimization: instead of running preg_match with a $
1486  # anchor, which can be slow, do the match on the reversed
1487  # string starting at the desired offset.
1488  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1489  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1490  $numSepChars--;
1491  }
1492  }
1493  if ( $numSepChars ) {
1494  $trail = substr( $url, -$numSepChars ) . $trail;
1495  $url = substr( $url, 0, -$numSepChars );
1496  }
1497 
1498  $url = Sanitizer::cleanUrl( $url );
1499 
1500  # Is this an external image?
1501  $text = $this->maybeMakeExternalImage( $url );
1502  if ( $text === false ) {
1503  # Not an image, make a link
1504  $text = Linker::makeExternalLink( $url,
1505  $this->getConverterLanguage()->markNoConversion( $url, true ),
1506  true, 'free',
1507  $this->getExternalLinkAttribs( $url ) );
1508  # Register it in the output object...
1509  # Replace unnecessary URL escape codes with their equivalent characters
1510  $pasteurized = self::normalizeLinkUrl( $url );
1511  $this->mOutput->addExternalLink( $pasteurized );
1512  }
1513  return $text . $trail;
1514  }
1515 
1525  public function doHeadings( $text ) {
1526  for ( $i = 6; $i >= 1; --$i ) {
1527  $h = str_repeat( '=', $i );
1528  $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1529  }
1530  return $text;
1531  }
1532 
1541  public function doAllQuotes( $text ) {
1542  $outtext = '';
1543  $lines = StringUtils::explode( "\n", $text );
1544  foreach ( $lines as $line ) {
1545  $outtext .= $this->doQuotes( $line ) . "\n";
1546  }
1547  $outtext = substr( $outtext, 0, -1 );
1548  return $outtext;
1549  }
1550 
1558  public function doQuotes( $text ) {
1559  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1560  $countarr = count( $arr );
1561  if ( $countarr == 1 ) {
1562  return $text;
1563  }
1564 
1565  // First, do some preliminary work. This may shift some apostrophes from
1566  // being mark-up to being text. It also counts the number of occurrences
1567  // of bold and italics mark-ups.
1568  $numbold = 0;
1569  $numitalics = 0;
1570  for ( $i = 1; $i < $countarr; $i += 2 ) {
1571  $thislen = strlen( $arr[$i] );
1572  // If there are ever four apostrophes, assume the first is supposed to
1573  // be text, and the remaining three constitute mark-up for bold text.
1574  // (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1575  if ( $thislen == 4 ) {
1576  $arr[$i - 1] .= "'";
1577  $arr[$i] = "'''";
1578  $thislen = 3;
1579  } elseif ( $thislen > 5 ) {
1580  // If there are more than 5 apostrophes in a row, assume they're all
1581  // text except for the last 5.
1582  // (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1583  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1584  $arr[$i] = "'''''";
1585  $thislen = 5;
1586  }
1587  // Count the number of occurrences of bold and italics mark-ups.
1588  if ( $thislen == 2 ) {
1589  $numitalics++;
1590  } elseif ( $thislen == 3 ) {
1591  $numbold++;
1592  } elseif ( $thislen == 5 ) {
1593  $numitalics++;
1594  $numbold++;
1595  }
1596  }
1597 
1598  // If there is an odd number of both bold and italics, it is likely
1599  // that one of the bold ones was meant to be an apostrophe followed
1600  // by italics. Which one we cannot know for certain, but it is more
1601  // likely to be one that has a single-letter word before it.
1602  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1603  $firstsingleletterword = -1;
1604  $firstmultiletterword = -1;
1605  $firstspace = -1;
1606  for ( $i = 1; $i < $countarr; $i += 2 ) {
1607  if ( strlen( $arr[$i] ) == 3 ) {
1608  $x1 = substr( $arr[$i - 1], -1 );
1609  $x2 = substr( $arr[$i - 1], -2, 1 );
1610  if ( $x1 === ' ' ) {
1611  if ( $firstspace == -1 ) {
1612  $firstspace = $i;
1613  }
1614  } elseif ( $x2 === ' ' ) {
1615  if ( $firstsingleletterword == -1 ) {
1616  $firstsingleletterword = $i;
1617  // if $firstsingleletterword is set, we don't
1618  // look at the other options, so we can bail early.
1619  break;
1620  }
1621  } else {
1622  if ( $firstmultiletterword == -1 ) {
1623  $firstmultiletterword = $i;
1624  }
1625  }
1626  }
1627  }
1628 
1629  // If there is a single-letter word, use it!
1630  if ( $firstsingleletterword > -1 ) {
1631  $arr[$firstsingleletterword] = "''";
1632  $arr[$firstsingleletterword - 1] .= "'";
1633  } elseif ( $firstmultiletterword > -1 ) {
1634  // If not, but there's a multi-letter word, use that one.
1635  $arr[$firstmultiletterword] = "''";
1636  $arr[$firstmultiletterword - 1] .= "'";
1637  } elseif ( $firstspace > -1 ) {
1638  // ... otherwise use the first one that has neither.
1639  // (notice that it is possible for all three to be -1 if, for example,
1640  // there is only one pentuple-apostrophe in the line)
1641  $arr[$firstspace] = "''";
1642  $arr[$firstspace - 1] .= "'";
1643  }
1644  }
1645 
1646  // Now let's actually convert our apostrophic mush to HTML!
1647  $output = '';
1648  $buffer = '';
1649  $state = '';
1650  $i = 0;
1651  foreach ( $arr as $r ) {
1652  if ( ( $i % 2 ) == 0 ) {
1653  if ( $state === 'both' ) {
1654  $buffer .= $r;
1655  } else {
1656  $output .= $r;
1657  }
1658  } else {
1659  $thislen = strlen( $r );
1660  if ( $thislen == 2 ) {
1661  if ( $state === 'i' ) {
1662  $output .= '</i>';
1663  $state = '';
1664  } elseif ( $state === 'bi' ) {
1665  $output .= '</i>';
1666  $state = 'b';
1667  } elseif ( $state === 'ib' ) {
1668  $output .= '</b></i><b>';
1669  $state = 'b';
1670  } elseif ( $state === 'both' ) {
1671  $output .= '<b><i>' . $buffer . '</i>';
1672  $state = 'b';
1673  } else { // $state can be 'b' or ''
1674  $output .= '<i>';
1675  $state .= 'i';
1676  }
1677  } elseif ( $thislen == 3 ) {
1678  if ( $state === 'b' ) {
1679  $output .= '</b>';
1680  $state = '';
1681  } elseif ( $state === 'bi' ) {
1682  $output .= '</i></b><i>';
1683  $state = 'i';
1684  } elseif ( $state === 'ib' ) {
1685  $output .= '</b>';
1686  $state = 'i';
1687  } elseif ( $state === 'both' ) {
1688  $output .= '<i><b>' . $buffer . '</b>';
1689  $state = 'i';
1690  } else { // $state can be 'i' or ''
1691  $output .= '<b>';
1692  $state .= 'b';
1693  }
1694  } elseif ( $thislen == 5 ) {
1695  if ( $state === 'b' ) {
1696  $output .= '</b><i>';
1697  $state = 'i';
1698  } elseif ( $state === 'i' ) {
1699  $output .= '</i><b>';
1700  $state = 'b';
1701  } elseif ( $state === 'bi' ) {
1702  $output .= '</i></b>';
1703  $state = '';
1704  } elseif ( $state === 'ib' ) {
1705  $output .= '</b></i>';
1706  $state = '';
1707  } elseif ( $state === 'both' ) {
1708  $output .= '<i><b>' . $buffer . '</b></i>';
1709  $state = '';
1710  } else { // ($state == '')
1711  $buffer = '';
1712  $state = 'both';
1713  }
1714  }
1715  }
1716  $i++;
1717  }
1718  // Now close all remaining tags. Notice that the order is important.
1719  if ( $state === 'b' || $state === 'ib' ) {
1720  $output .= '</b>';
1721  }
1722  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1723  $output .= '</i>';
1724  }
1725  if ( $state === 'bi' ) {
1726  $output .= '</b>';
1727  }
1728  // There might be lonely ''''', so make sure we have a buffer
1729  if ( $state === 'both' && $buffer ) {
1730  $output .= '<b><i>' . $buffer . '</i></b>';
1731  }
1732  return $output;
1733  }
1734 
1748  public function replaceExternalLinks( $text ) {
1749 
1750  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1751  if ( $bits === false ) {
1752  throw new MWException( "PCRE needs to be compiled with "
1753  . "--enable-unicode-properties in order for MediaWiki to function" );
1754  }
1755  $s = array_shift( $bits );
1756 
1757  $i = 0;
1758  while ( $i < count( $bits ) ) {
1759  $url = $bits[$i++];
1760  $i++; // protocol
1761  $text = $bits[$i++];
1762  $trail = $bits[$i++];
1763 
1764  # The characters '<' and '>' (which were escaped by
1765  # removeHTMLtags()) should not be included in
1766  # URLs, per RFC 2396.
1767  $m2 = array();
1768  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1769  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1770  $url = substr( $url, 0, $m2[0][1] );
1771  }
1772 
1773  # If the link text is an image URL, replace it with an <img> tag
1774  # This happened by accident in the original parser, but some people used it extensively
1775  $img = $this->maybeMakeExternalImage( $text );
1776  if ( $img !== false ) {
1777  $text = $img;
1778  }
1779 
1780  $dtrail = '';
1781 
1782  # Set linktype for CSS - if URL==text, link is essentially free
1783  $linktype = ( $text === $url ) ? 'free' : 'text';
1784 
1785  # No link text, e.g. [http://domain.tld/some.link]
1786  if ( $text == '' ) {
1787  # Autonumber
1788  $langObj = $this->getTargetLanguage();
1789  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1790  $linktype = 'autonumber';
1791  } else {
1792  # Have link text, e.g. [http://domain.tld/some.link text]s
1793  # Check for trail
1794  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1795  }
1796 
1797  $text = $this->getConverterLanguage()->markNoConversion( $text );
1798 
1799  $url = Sanitizer::cleanUrl( $url );
1800 
1801  # Use the encoded URL
1802  # This means that users can paste URLs directly into the text
1803  # Funny characters like ö aren't valid in URLs anyway
1804  # This was changed in August 2004
1805  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1806  $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1807 
1808  # Register link in the output object.
1809  # Replace unnecessary URL escape codes with the referenced character
1810  # This prevents spammers from hiding links from the filters
1811  $pasteurized = self::normalizeLinkUrl( $url );
1812  $this->mOutput->addExternalLink( $pasteurized );
1813  }
1814 
1815  return $s;
1816  }
1817 
1827  public static function getExternalLinkRel( $url = false, $title = null ) {
1829  $ns = $title ? $title->getNamespace() : false;
1830  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1831  && !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
1832  ) {
1833  return 'nofollow';
1834  }
1835  return null;
1836  }
1837 
1848  public function getExternalLinkAttribs( $url = false ) {
1849  $attribs = array();
1850  $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle );
1851 
1852  if ( $this->mOptions->getExternalLinkTarget() ) {
1853  $attribs['target'] = $this->mOptions->getExternalLinkTarget();
1854  }
1855  return $attribs;
1856  }
1857 
1865  public static function replaceUnusualEscapes( $url ) {
1866  wfDeprecated( __METHOD__, '1.24' );
1867  return self::normalizeLinkUrl( $url );
1868  }
1869 
1879  public static function normalizeLinkUrl( $url ) {
1880  # First, make sure unsafe characters are encoded
1881  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1882  function ( $m ) {
1883  return rawurlencode( $m[0] );
1884  },
1885  $url
1886  );
1887 
1888  $ret = '';
1889  $end = strlen( $url );
1890 
1891  # Fragment part - 'fragment'
1892  $start = strpos( $url, '#' );
1893  if ( $start !== false && $start < $end ) {
1894  $ret = self::normalizeUrlComponent(
1895  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1896  $end = $start;
1897  }
1898 
1899  # Query part - 'query' minus &=+;
1900  $start = strpos( $url, '?' );
1901  if ( $start !== false && $start < $end ) {
1902  $ret = self::normalizeUrlComponent(
1903  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1904  $end = $start;
1905  }
1906 
1907  # Scheme and path part - 'pchar'
1908  # (we assume no userinfo or encoded colons in the host)
1909  $ret = self::normalizeUrlComponent(
1910  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1911 
1912  return $ret;
1913  }
1914 
1915  private static function normalizeUrlComponent( $component, $unsafe ) {
1916  $callback = function ( $matches ) use ( $unsafe ) {
1917  $char = urldecode( $matches[0] );
1918  $ord = ord( $char );
1919  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1920  # Unescape it
1921  return $char;
1922  } else {
1923  # Leave it escaped, but use uppercase for a-f
1924  return strtoupper( $matches[0] );
1925  }
1926  };
1927  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
1928  }
1929 
1938  private function maybeMakeExternalImage( $url ) {
1939  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1940  $imagesexception = !empty( $imagesfrom );
1941  $text = false;
1942  # $imagesfrom could be either a single string or an array of strings, parse out the latter
1943  if ( $imagesexception && is_array( $imagesfrom ) ) {
1944  $imagematch = false;
1945  foreach ( $imagesfrom as $match ) {
1946  if ( strpos( $url, $match ) === 0 ) {
1947  $imagematch = true;
1948  break;
1949  }
1950  }
1951  } elseif ( $imagesexception ) {
1952  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
1953  } else {
1954  $imagematch = false;
1955  }
1956 
1957  if ( $this->mOptions->getAllowExternalImages()
1958  || ( $imagesexception && $imagematch )
1959  ) {
1960  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1961  # Image found
1962  $text = Linker::makeExternalImage( $url );
1963  }
1964  }
1965  if ( !$text && $this->mOptions->getEnableImageWhitelist()
1966  && preg_match( self::EXT_IMAGE_REGEX, $url )
1967  ) {
1968  $whitelist = explode(
1969  "\n",
1970  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
1971  );
1972 
1973  foreach ( $whitelist as $entry ) {
1974  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
1975  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
1976  continue;
1977  }
1978  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
1979  # Image matches a whitelist entry
1980  $text = Linker::makeExternalImage( $url );
1981  break;
1982  }
1983  }
1984  }
1985  return $text;
1986  }
1997  public function replaceInternalLinks( $s ) {
1998  $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
1999  return $s;
2000  }
2001 
2010  public function replaceInternalLinks2( &$s ) {
2012 
2013  static $tc = false, $e1, $e1_img;
2014  # the % is needed to support urlencoded titles as well
2015  if ( !$tc ) {
2016  $tc = Title::legalChars() . '#%';
2017  # Match a link having the form [[namespace:link|alternate]]trail
2018  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2019  # Match cases where there is no "]]", which might still be images
2020  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2021  }
2022 
2023  $holders = new LinkHolderArray( $this );
2024 
2025  # split the entire text string on occurrences of [[
2026  $a = StringUtils::explode( '[[', ' ' . $s );
2027  # get the first element (all text up to first [[), and remove the space we added
2028  $s = $a->current();
2029  $a->next();
2030  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2031  $s = substr( $s, 1 );
2032 
2033  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2034  $e2 = null;
2035  if ( $useLinkPrefixExtension ) {
2036  # Match the end of a line for a word that's not followed by whitespace,
2037  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2039  $charset = $wgContLang->linkPrefixCharset();
2040  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2041  }
2042 
2043  if ( is_null( $this->mTitle ) ) {
2044  throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2045  }
2046  $nottalk = !$this->mTitle->isTalkPage();
2047 
2048  if ( $useLinkPrefixExtension ) {
2049  $m = array();
2050  if ( preg_match( $e2, $s, $m ) ) {
2051  $first_prefix = $m[2];
2052  } else {
2053  $first_prefix = false;
2054  }
2055  } else {
2056  $prefix = '';
2057  }
2058 
2059  $useSubpages = $this->areSubpagesAllowed();
2060 
2061  // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2062  # Loop for each link
2063  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2064  // @codingStandardsIgnoreStart
2065 
2066  # Check for excessive memory usage
2067  if ( $holders->isBig() ) {
2068  # Too big
2069  # Do the existence check, replace the link holders and clear the array
2070  $holders->replace( $s );
2071  $holders->clear();
2072  }
2073 
2074  if ( $useLinkPrefixExtension ) {
2075  if ( preg_match( $e2, $s, $m ) ) {
2076  $prefix = $m[2];
2077  $s = $m[1];
2078  } else {
2079  $prefix = '';
2080  }
2081  # first link
2082  if ( $first_prefix ) {
2083  $prefix = $first_prefix;
2084  $first_prefix = false;
2085  }
2086  }
2087 
2088  $might_be_img = false;
2089 
2090  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2091  $text = $m[2];
2092  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2093  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2094  # the real problem is with the $e1 regex
2095  # See bug 1300.
2096  #
2097  # Still some problems for cases where the ] is meant to be outside punctuation,
2098  # and no image is in sight. See bug 2095.
2099  #
2100  if ( $text !== ''
2101  && substr( $m[3], 0, 1 ) === ']'
2102  && strpos( $text, '[' ) !== false
2103  ) {
2104  $text .= ']'; # so that replaceExternalLinks($text) works later
2105  $m[3] = substr( $m[3], 1 );
2106  }
2107  # fix up urlencoded title texts
2108  if ( strpos( $m[1], '%' ) !== false ) {
2109  # Should anchors '#' also be rejected?
2110  $m[1] = str_replace( array( '<', '>' ), array( '&lt;', '&gt;' ), rawurldecode( $m[1] ) );
2111  }
2112  $trail = $m[3];
2113  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2114  # Invalid, but might be an image with a link in its caption
2115  $might_be_img = true;
2116  $text = $m[2];
2117  if ( strpos( $m[1], '%' ) !== false ) {
2118  $m[1] = rawurldecode( $m[1] );
2119  }
2120  $trail = "";
2121  } else { # Invalid form; output directly
2122  $s .= $prefix . '[[' . $line;
2123  continue;
2124  }
2125 
2126  $origLink = $m[1];
2127 
2128  # Don't allow internal links to pages containing
2129  # PROTO: where PROTO is a valid URL protocol; these
2130  # should be external links.
2131  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2132  $s .= $prefix . '[[' . $line;
2133  continue;
2134  }
2135 
2136  # Make subpage if necessary
2137  if ( $useSubpages ) {
2138  $link = $this->maybeDoSubpageLink( $origLink, $text );
2139  } else {
2140  $link = $origLink;
2141  }
2142 
2143  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2144  if ( !$noforce ) {
2145  # Strip off leading ':'
2146  $link = substr( $link, 1 );
2147  }
2148 
2149  $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
2150  if ( $nt === null ) {
2151  $s .= $prefix . '[[' . $line;
2152  continue;
2153  }
2154 
2155  $ns = $nt->getNamespace();
2156  $iw = $nt->getInterwiki();
2157 
2158  if ( $might_be_img ) { # if this is actually an invalid link
2159  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2160  $found = false;
2161  while ( true ) {
2162  # look at the next 'line' to see if we can close it there
2163  $a->next();
2164  $next_line = $a->current();
2165  if ( $next_line === false || $next_line === null ) {
2166  break;
2167  }
2168  $m = explode( ']]', $next_line, 3 );
2169  if ( count( $m ) == 3 ) {
2170  # the first ]] closes the inner link, the second the image
2171  $found = true;
2172  $text .= "[[{$m[0]}]]{$m[1]}";
2173  $trail = $m[2];
2174  break;
2175  } elseif ( count( $m ) == 2 ) {
2176  # if there's exactly one ]] that's fine, we'll keep looking
2177  $text .= "[[{$m[0]}]]{$m[1]}";
2178  } else {
2179  # if $next_line is invalid too, we need look no further
2180  $text .= '[[' . $next_line;
2181  break;
2182  }
2183  }
2184  if ( !$found ) {
2185  # we couldn't find the end of this imageLink, so output it raw
2186  # but don't ignore what might be perfectly normal links in the text we've examined
2187  $holders->merge( $this->replaceInternalLinks2( $text ) );
2188  $s .= "{$prefix}[[$link|$text";
2189  # note: no $trail, because without an end, there *is* no trail
2190  continue;
2191  }
2192  } else { # it's not an image, so output it raw
2193  $s .= "{$prefix}[[$link|$text";
2194  # note: no $trail, because without an end, there *is* no trail
2195  continue;
2196  }
2197  }
2198 
2199  $wasblank = ( $text == '' );
2200  if ( $wasblank ) {
2201  $text = $link;
2202  } else {
2203  # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2204  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2205  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2206  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2207  $text = $this->doQuotes( $text );
2208  }
2209 
2210  # Link not escaped by : , create the various objects
2211  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2212  # Interwikis
2213  if (
2214  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2215  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2216  in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2217  )
2218  ) {
2219  # Bug 24502: filter duplicates
2220  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2221  $this->mLangLinkLanguages[$iw] = true;
2222  $this->mOutput->addLanguageLink( $nt->getFullText() );
2223  }
2224 
2225  $s = rtrim( $s . $prefix );
2226  $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2227  continue;
2228  }
2229 
2230  if ( $ns == NS_FILE ) {
2231  if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2232  if ( $wasblank ) {
2233  # if no parameters were passed, $text
2234  # becomes something like "File:Foo.png",
2235  # which we don't want to pass on to the
2236  # image generator
2237  $text = '';
2238  } else {
2239  # recursively parse links inside the image caption
2240  # actually, this will parse them in any other parameters, too,
2241  # but it might be hard to fix that, and it doesn't matter ATM
2242  $text = $this->replaceExternalLinks( $text );
2243  $holders->merge( $this->replaceInternalLinks2( $text ) );
2244  }
2245  # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2246  $s .= $prefix . $this->armorLinks(
2247  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2248  } else {
2249  $s .= $prefix . $trail;
2250  }
2251  continue;
2252  }
2253 
2254  if ( $ns == NS_CATEGORY ) {
2255  $s = rtrim( $s . "\n" ); # bug 87
2256 
2257  if ( $wasblank ) {
2258  $sortkey = $this->getDefaultSort();
2259  } else {
2260  $sortkey = $text;
2261  }
2262  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2263  $sortkey = str_replace( "\n", '', $sortkey );
2264  $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2265  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2266 
2270  $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2271 
2272  continue;
2273  }
2274  }
2275 
2276  # Self-link checking. For some languages, variants of the title are checked in
2277  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2278  # for linking to a different variant.
2279  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2280  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2281  continue;
2282  }
2283 
2284  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2285  # @todo FIXME: Should do batch file existence checks, see comment below
2286  if ( $ns == NS_MEDIA ) {
2287  # Give extensions a chance to select the file revision for us
2288  $options = array();
2289  $descQuery = false;
2290  Hooks::run( 'BeforeParserFetchFileAndTitle',
2291  array( $this, $nt, &$options, &$descQuery ) );
2292  # Fetch and register the file (file title may be different via hooks)
2293  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2294  # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2295  $s .= $prefix . $this->armorLinks(
2296  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2297  continue;
2298  }
2299 
2300  # Some titles, such as valid special pages or files in foreign repos, should
2301  # be shown as bluelinks even though they're not included in the page table
2302  #
2303  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2304  # batch file existence checks for NS_FILE and NS_MEDIA
2305  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2306  $this->mOutput->addLink( $nt );
2307  $s .= $this->makeKnownLinkHolder( $nt, $text, array(), $trail, $prefix );
2308  } else {
2309  # Links will be added to the output link list after checking
2310  $s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix );
2311  }
2312  }
2313  return $holders;
2314  }
2315 
2330  public function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
2331  list( $inside, $trail ) = Linker::splitTrail( $trail );
2333  if ( is_string( $query ) ) {
2334  $query = wfCgiToArray( $query );
2335  }
2336  if ( $text == '' ) {
2337  $text = htmlspecialchars( $nt->getPrefixedText() );
2338  }
2339 
2340  $link = Linker::linkKnown( $nt, "$prefix$text$inside", array(), $query );
2342  return $this->armorLinks( $link ) . $trail;
2343  }
2344 
2355  public function armorLinks( $text ) {
2356  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2357  self::MARKER_PREFIX . "NOPARSE$1", $text );
2358  }
2359 
2364  public function areSubpagesAllowed() {
2365  # Some namespaces don't allow subpages
2366  return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2367  }
2368 
2377  public function maybeDoSubpageLink( $target, &$text ) {
2378  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2379  }
2380 
2387  public function closeParagraph() {
2388  $result = '';
2389  if ( $this->mLastSection != '' ) {
2390  $result = '</' . $this->mLastSection . ">\n";
2391  }
2392  $this->mInPre = false;
2393  $this->mLastSection = '';
2394  return $result;
2395  }
2396 
2407  public function getCommon( $st1, $st2 ) {
2408  $fl = strlen( $st1 );
2409  $shorter = strlen( $st2 );
2410  if ( $fl < $shorter ) {
2411  $shorter = $fl;
2412  }
2413 
2414  for ( $i = 0; $i < $shorter; ++$i ) {
2415  if ( $st1[$i] != $st2[$i] ) {
2416  break;
2417  }
2418  }
2419  return $i;
2420  }
2421 
2431  public function openList( $char ) {
2432  $result = $this->closeParagraph();
2433 
2434  if ( '*' === $char ) {
2435  $result .= "<ul><li>";
2436  } elseif ( '#' === $char ) {
2437  $result .= "<ol><li>";
2438  } elseif ( ':' === $char ) {
2439  $result .= "<dl><dd>";
2440  } elseif ( ';' === $char ) {
2441  $result .= "<dl><dt>";
2442  $this->mDTopen = true;
2443  } else {
2444  $result = '<!-- ERR 1 -->';
2445  }
2446 
2447  return $result;
2448  }
2449 
2457  public function nextItem( $char ) {
2458  if ( '*' === $char || '#' === $char ) {
2459  return "</li>\n<li>";
2460  } elseif ( ':' === $char || ';' === $char ) {
2461  $close = "</dd>\n";
2462  if ( $this->mDTopen ) {
2463  $close = "</dt>\n";
2464  }
2465  if ( ';' === $char ) {
2466  $this->mDTopen = true;
2467  return $close . '<dt>';
2468  } else {
2469  $this->mDTopen = false;
2470  return $close . '<dd>';
2471  }
2472  }
2473  return '<!-- ERR 2 -->';
2474  }
2475 
2483  public function closeList( $char ) {
2484  if ( '*' === $char ) {
2485  $text = "</li></ul>";
2486  } elseif ( '#' === $char ) {
2487  $text = "</li></ol>";
2488  } elseif ( ':' === $char ) {
2489  if ( $this->mDTopen ) {
2490  $this->mDTopen = false;
2491  $text = "</dt></dl>";
2492  } else {
2493  $text = "</dd></dl>";
2494  }
2495  } else {
2496  return '<!-- ERR 3 -->';
2497  }
2498  return $text;
2499  }
2510  public function doBlockLevels( $text, $linestart ) {
2511 
2512  # Parsing through the text line by line. The main thing
2513  # happening here is handling of block-level elements p, pre,
2514  # and making lists from lines starting with * # : etc.
2515  #
2516  $textLines = StringUtils::explode( "\n", $text );
2517 
2518  $lastPrefix = $output = '';
2519  $this->mDTopen = $inBlockElem = false;
2520  $prefixLength = 0;
2521  $paragraphStack = false;
2522  $inBlockquote = false;
2523 
2524  foreach ( $textLines as $oLine ) {
2525  # Fix up $linestart
2526  if ( !$linestart ) {
2527  $output .= $oLine;
2528  $linestart = true;
2529  continue;
2530  }
2531  # * = ul
2532  # # = ol
2533  # ; = dt
2534  # : = dd
2535 
2536  $lastPrefixLength = strlen( $lastPrefix );
2537  $preCloseMatch = preg_match( '/<\\/pre/i', $oLine );
2538  $preOpenMatch = preg_match( '/<pre/i', $oLine );
2539  # If not in a <pre> element, scan for and figure out what prefixes are there.
2540  if ( !$this->mInPre ) {
2541  # Multiple prefixes may abut each other for nested lists.
2542  $prefixLength = strspn( $oLine, '*#:;' );
2543  $prefix = substr( $oLine, 0, $prefixLength );
2544 
2545  # eh?
2546  # ; and : are both from definition-lists, so they're equivalent
2547  # for the purposes of determining whether or not we need to open/close
2548  # elements.
2549  $prefix2 = str_replace( ';', ':', $prefix );
2550  $t = substr( $oLine, $prefixLength );
2551  $this->mInPre = (bool)$preOpenMatch;
2552  } else {
2553  # Don't interpret any other prefixes in preformatted text
2554  $prefixLength = 0;
2555  $prefix = $prefix2 = '';
2556  $t = $oLine;
2557  }
2558 
2559  # List generation
2560  if ( $prefixLength && $lastPrefix === $prefix2 ) {
2561  # Same as the last item, so no need to deal with nesting or opening stuff
2562  $output .= $this->nextItem( substr( $prefix, -1 ) );
2563  $paragraphStack = false;
2564 
2565  if ( substr( $prefix, -1 ) === ';' ) {
2566  # The one nasty exception: definition lists work like this:
2567  # ; title : definition text
2568  # So we check for : in the remainder text to split up the
2569  # title and definition, without b0rking links.
2570  $term = $t2 = '';
2571  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2572  $t = $t2;
2573  $output .= $term . $this->nextItem( ':' );
2574  }
2575  }
2576  } elseif ( $prefixLength || $lastPrefixLength ) {
2577  # We need to open or close prefixes, or both.
2578 
2579  # Either open or close a level...
2580  $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
2581  $paragraphStack = false;
2582 
2583  # Close all the prefixes which aren't shared.
2584  while ( $commonPrefixLength < $lastPrefixLength ) {
2585  $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
2586  --$lastPrefixLength;
2587  }
2588 
2589  # Continue the current prefix if appropriate.
2590  if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
2591  $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
2592  }
2593 
2594  # Open prefixes where appropriate.
2595  if ( $lastPrefix && $prefixLength > $commonPrefixLength ) {
2596  $output .= "\n";
2597  }
2598  while ( $prefixLength > $commonPrefixLength ) {
2599  $char = substr( $prefix, $commonPrefixLength, 1 );
2600  $output .= $this->openList( $char );
2601 
2602  if ( ';' === $char ) {
2603  # @todo FIXME: This is dupe of code above
2604  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2605  $t = $t2;
2606  $output .= $term . $this->nextItem( ':' );
2607  }
2608  }
2609  ++$commonPrefixLength;
2610  }
2611  if ( !$prefixLength && $lastPrefix ) {
2612  $output .= "\n";
2613  }
2614  $lastPrefix = $prefix2;
2615  }
2616 
2617  # If we have no prefixes, go to paragraph mode.
2618  if ( 0 == $prefixLength ) {
2619  # No prefix (not in list)--go to paragraph mode
2620  # XXX: use a stack for nestable elements like span, table and div
2621  $openmatch = preg_match(
2622  '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|'
2623  . '<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
2624  $t
2625  );
2626  $closematch = preg_match(
2627  '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'
2628  . '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|'
2629  . self::MARKER_PREFIX
2630  . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
2631  $t
2632  );
2633 
2634  if ( $openmatch || $closematch ) {
2635  $paragraphStack = false;
2636  # @todo bug 5718: paragraph closed
2637  $output .= $this->closeParagraph();
2638  if ( $preOpenMatch && !$preCloseMatch ) {
2639  $this->mInPre = true;
2640  }
2641  $bqOffset = 0;
2642  while ( preg_match( '/<(\\/?)blockquote[\s>]/i', $t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) {
2643  $inBlockquote = !$bqMatch[1][0]; // is this a close tag?
2644  $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
2645  }
2646  $inBlockElem = !$closematch;
2647  } elseif ( !$inBlockElem && !$this->mInPre ) {
2648  if ( ' ' == substr( $t, 0, 1 )
2649  && ( $this->mLastSection === 'pre' || trim( $t ) != '' )
2650  && !$inBlockquote
2651  ) {
2652  # pre
2653  if ( $this->mLastSection !== 'pre' ) {
2654  $paragraphStack = false;
2655  $output .= $this->closeParagraph() . '<pre>';
2656  $this->mLastSection = 'pre';
2657  }
2658  $t = substr( $t, 1 );
2659  } else {
2660  # paragraph
2661  if ( trim( $t ) === '' ) {
2662  if ( $paragraphStack ) {
2663  $output .= $paragraphStack . '<br />';
2664  $paragraphStack = false;
2665  $this->mLastSection = 'p';
2666  } else {
2667  if ( $this->mLastSection !== 'p' ) {
2668  $output .= $this->closeParagraph();
2669  $this->mLastSection = '';
2670  $paragraphStack = '<p>';
2671  } else {
2672  $paragraphStack = '</p><p>';
2673  }
2674  }
2675  } else {
2676  if ( $paragraphStack ) {
2677  $output .= $paragraphStack;
2678  $paragraphStack = false;
2679  $this->mLastSection = 'p';
2680  } elseif ( $this->mLastSection !== 'p' ) {
2681  $output .= $this->closeParagraph() . '<p>';
2682  $this->mLastSection = 'p';
2683  }
2684  }
2685  }
2686  }
2687  }
2688  # somewhere above we forget to get out of pre block (bug 785)
2689  if ( $preCloseMatch && $this->mInPre ) {
2690  $this->mInPre = false;
2691  }
2692  if ( $paragraphStack === false ) {
2693  $output .= $t;
2694  if ( $prefixLength === 0 ) {
2695  $output .= "\n";
2696  }
2697  }
2698  }
2699  while ( $prefixLength ) {
2700  $output .= $this->closeList( $prefix2[$prefixLength - 1] );
2701  --$prefixLength;
2702  if ( !$prefixLength ) {
2703  $output .= "\n";
2704  }
2705  }
2706  if ( $this->mLastSection != '' ) {
2707  $output .= '</' . $this->mLastSection . '>';
2708  $this->mLastSection = '';
2709  }
2710 
2711  return $output;
2712  }
2713 
2724  public function findColonNoLinks( $str, &$before, &$after ) {
2725 
2726  $pos = strpos( $str, ':' );
2727  if ( $pos === false ) {
2728  # Nothing to find!
2729  return false;
2730  }
2731 
2732  $lt = strpos( $str, '<' );
2733  if ( $lt === false || $lt > $pos ) {
2734  # Easy; no tag nesting to worry about
2735  $before = substr( $str, 0, $pos );
2736  $after = substr( $str, $pos + 1 );
2737  return $pos;
2738  }
2739 
2740  # Ugly state machine to walk through avoiding tags.
2741  $state = self::COLON_STATE_TEXT;
2742  $stack = 0;
2743  $len = strlen( $str );
2744  for ( $i = 0; $i < $len; $i++ ) {
2745  $c = $str[$i];
2746 
2747  switch ( $state ) {
2748  # (Using the number is a performance hack for common cases)
2749  case 0: # self::COLON_STATE_TEXT:
2750  switch ( $c ) {
2751  case "<":
2752  # Could be either a <start> tag or an </end> tag
2753  $state = self::COLON_STATE_TAGSTART;
2754  break;
2755  case ":":
2756  if ( $stack == 0 ) {
2757  # We found it!
2758  $before = substr( $str, 0, $i );
2759  $after = substr( $str, $i + 1 );
2760  return $i;
2761  }
2762  # Embedded in a tag; don't break it.
2763  break;
2764  default:
2765  # Skip ahead looking for something interesting
2766  $colon = strpos( $str, ':', $i );
2767  if ( $colon === false ) {
2768  # Nothing else interesting
2769  return false;
2770  }
2771  $lt = strpos( $str, '<', $i );
2772  if ( $stack === 0 ) {
2773  if ( $lt === false || $colon < $lt ) {
2774  # We found it!
2775  $before = substr( $str, 0, $colon );
2776  $after = substr( $str, $colon + 1 );
2777  return $i;
2778  }
2779  }
2780  if ( $lt === false ) {
2781  # Nothing else interesting to find; abort!
2782  # We're nested, but there's no close tags left. Abort!
2783  break 2;
2784  }
2785  # Skip ahead to next tag start
2786  $i = $lt;
2787  $state = self::COLON_STATE_TAGSTART;
2788  }
2789  break;
2790  case 1: # self::COLON_STATE_TAG:
2791  # In a <tag>
2792  switch ( $c ) {
2793  case ">":
2794  $stack++;
2795  $state = self::COLON_STATE_TEXT;
2796  break;
2797  case "/":
2798  # Slash may be followed by >?
2799  $state = self::COLON_STATE_TAGSLASH;
2800  break;
2801  default:
2802  # ignore
2803  }
2804  break;
2805  case 2: # self::COLON_STATE_TAGSTART:
2806  switch ( $c ) {
2807  case "/":
2808  $state = self::COLON_STATE_CLOSETAG;
2809  break;
2810  case "!":
2811  $state = self::COLON_STATE_COMMENT;
2812  break;
2813  case ">":
2814  # Illegal early close? This shouldn't happen D:
2815  $state = self::COLON_STATE_TEXT;
2816  break;
2817  default:
2818  $state = self::COLON_STATE_TAG;
2819  }
2820  break;
2821  case 3: # self::COLON_STATE_CLOSETAG:
2822  # In a </tag>
2823  if ( $c === ">" ) {
2824  $stack--;
2825  if ( $stack < 0 ) {
2826  wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
2827  return false;
2828  }
2829  $state = self::COLON_STATE_TEXT;
2830  }
2831  break;
2832  case self::COLON_STATE_TAGSLASH:
2833  if ( $c === ">" ) {
2834  # Yes, a self-closed tag <blah/>
2835  $state = self::COLON_STATE_TEXT;
2836  } else {
2837  # Probably we're jumping the gun, and this is an attribute
2838  $state = self::COLON_STATE_TAG;
2839  }
2840  break;
2841  case 5: # self::COLON_STATE_COMMENT:
2842  if ( $c === "-" ) {
2843  $state = self::COLON_STATE_COMMENTDASH;
2844  }
2845  break;
2846  case self::COLON_STATE_COMMENTDASH:
2847  if ( $c === "-" ) {
2848  $state = self::COLON_STATE_COMMENTDASHDASH;
2849  } else {
2850  $state = self::COLON_STATE_COMMENT;
2851  }
2852  break;
2853  case self::COLON_STATE_COMMENTDASHDASH:
2854  if ( $c === ">" ) {
2855  $state = self::COLON_STATE_TEXT;
2856  } else {
2857  $state = self::COLON_STATE_COMMENT;
2858  }
2859  break;
2860  default:
2861  throw new MWException( "State machine error in " . __METHOD__ );
2862  }
2863  }
2864  if ( $stack > 0 ) {
2865  wfDebug( __METHOD__ . ": Invalid input; not enough close tags (stack $stack, state $state)\n" );
2866  return false;
2867  }
2868  return false;
2869  }
2870 
2882  public function getVariableValue( $index, $frame = false ) {
2885 
2886  if ( is_null( $this->mTitle ) ) {
2887  // If no title set, bad things are going to happen
2888  // later. Title should always be set since this
2889  // should only be called in the middle of a parse
2890  // operation (but the unit-tests do funky stuff)
2891  throw new MWException( __METHOD__ . ' Should only be '
2892  . ' called while parsing (no title set)' );
2893  }
2894 
2899  if ( Hooks::run( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
2900  if ( isset( $this->mVarCache[$index] ) ) {
2901  return $this->mVarCache[$index];
2902  }
2903  }
2904 
2905  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2906  Hooks::run( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
2907 
2908  $pageLang = $this->getFunctionLang();
2909 
2910  switch ( $index ) {
2911  case '!':
2912  $value = '|';
2913  break;
2914  case 'currentmonth':
2915  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2916  break;
2917  case 'currentmonth1':
2918  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2919  break;
2920  case 'currentmonthname':
2921  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2922  break;
2923  case 'currentmonthnamegen':
2924  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2925  break;
2926  case 'currentmonthabbrev':
2927  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2928  break;
2929  case 'currentday':
2930  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2931  break;
2932  case 'currentday2':
2933  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2934  break;
2935  case 'localmonth':
2936  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2937  break;
2938  case 'localmonth1':
2939  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2940  break;
2941  case 'localmonthname':
2942  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2943  break;
2944  case 'localmonthnamegen':
2945  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2946  break;
2947  case 'localmonthabbrev':
2948  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2949  break;
2950  case 'localday':
2951  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2952  break;
2953  case 'localday2':
2954  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2955  break;
2956  case 'pagename':
2957  $value = wfEscapeWikiText( $this->mTitle->getText() );
2958  break;
2959  case 'pagenamee':
2960  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2961  break;
2962  case 'fullpagename':
2963  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2964  break;
2965  case 'fullpagenamee':
2966  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2967  break;
2968  case 'subpagename':
2969  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2970  break;
2971  case 'subpagenamee':
2972  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2973  break;
2974  case 'rootpagename':
2975  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2976  break;
2977  case 'rootpagenamee':
2978  $value = wfEscapeWikiText( wfUrlEncode( str_replace(
2979  ' ',
2980  '_',
2981  $this->mTitle->getRootText()
2982  ) ) );
2983  break;
2984  case 'basepagename':
2985  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2986  break;
2987  case 'basepagenamee':
2988  $value = wfEscapeWikiText( wfUrlEncode( str_replace(
2989  ' ',
2990  '_',
2991  $this->mTitle->getBaseText()
2992  ) ) );
2993  break;
2994  case 'talkpagename':
2995  if ( $this->mTitle->canTalk() ) {
2996  $talkPage = $this->mTitle->getTalkPage();
2997  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2998  } else {
2999  $value = '';
3000  }
3001  break;
3002  case 'talkpagenamee':
3003  if ( $this->mTitle->canTalk() ) {
3004  $talkPage = $this->mTitle->getTalkPage();
3005  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
3006  } else {
3007  $value = '';
3008  }
3009  break;
3010  case 'subjectpagename':
3011  $subjPage = $this->mTitle->getSubjectPage();
3012  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
3013  break;
3014  case 'subjectpagenamee':
3015  $subjPage = $this->mTitle->getSubjectPage();
3016  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
3017  break;
3018  case 'pageid': // requested in bug 23427
3019  $pageid = $this->getTitle()->getArticleID();
3020  if ( $pageid == 0 ) {
3021  # 0 means the page doesn't exist in the database,
3022  # which means the user is previewing a new page.
3023  # The vary-revision flag must be set, because the magic word
3024  # will have a different value once the page is saved.
3025  $this->mOutput->setFlag( 'vary-revision' );
3026  wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
3027  }
3028  $value = $pageid ? $pageid : null;
3029  break;
3030  case 'revisionid':
3031  # Let the edit saving system know we should parse the page
3032  # *after* a revision ID has been assigned.
3033  $this->mOutput->setFlag( 'vary-revision' );
3034  wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
3035  $value = $this->mRevisionId;
3036  break;
3037  case 'revisionday':
3038  # Let the edit saving system know we should parse the page
3039  # *after* a revision ID has been assigned. This is for null edits.
3040  $this->mOutput->setFlag( 'vary-revision' );
3041  wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
3042  $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
3043  break;
3044  case 'revisionday2':
3045  # Let the edit saving system know we should parse the page
3046  # *after* a revision ID has been assigned. This is for null edits.
3047  $this->mOutput->setFlag( 'vary-revision' );
3048  wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
3049  $value = substr( $this->getRevisionTimestamp(), 6, 2 );
3050  break;
3051  case 'revisionmonth':
3052  # Let the edit saving system know we should parse the page
3053  # *after* a revision ID has been assigned. This is for null edits.
3054  $this->mOutput->setFlag( 'vary-revision' );
3055  wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
3056  $value = substr( $this->getRevisionTimestamp(), 4, 2 );
3057  break;
3058  case 'revisionmonth1':
3059  # Let the edit saving system know we should parse the page
3060  # *after* a revision ID has been assigned. This is for null edits.
3061  $this->mOutput->setFlag( 'vary-revision' );
3062  wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
3063  $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
3064  break;
3065  case 'revisionyear':
3066  # Let the edit saving system know we should parse the page
3067  # *after* a revision ID has been assigned. This is for null edits.
3068  $this->mOutput->setFlag( 'vary-revision' );
3069  wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
3070  $value = substr( $this->getRevisionTimestamp(), 0, 4 );
3071  break;
3072  case 'revisiontimestamp':
3073  # Let the edit saving system know we should parse the page
3074  # *after* a revision ID has been assigned. This is for null edits.
3075  $this->mOutput->setFlag( 'vary-revision' );
3076  wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
3077  $value = $this->getRevisionTimestamp();
3078  break;
3079  case 'revisionuser':
3080  # Let the edit saving system know we should parse the page
3081  # *after* a revision ID has been assigned. This is for null edits.
3082  $this->mOutput->setFlag( 'vary-revision' );
3083  wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
3084  $value = $this->getRevisionUser();
3085  break;
3086  case 'revisionsize':
3087  # Let the edit saving system know we should parse the page
3088  # *after* a revision ID has been assigned. This is for null edits.
3089  $this->mOutput->setFlag( 'vary-revision' );
3090  wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
3091  $value = $this->getRevisionSize();
3092  break;
3093  case 'namespace':
3094  $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3095  break;
3096  case 'namespacee':
3097  $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3098  break;
3099  case 'namespacenumber':
3100  $value = $this->mTitle->getNamespace();
3101  break;
3102  case 'talkspace':
3103  $value = $this->mTitle->canTalk()
3104  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
3105  : '';
3106  break;
3107  case 'talkspacee':
3108  $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
3109  break;
3110  case 'subjectspace':
3111  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
3112  break;
3113  case 'subjectspacee':
3114  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3115  break;
3116  case 'currentdayname':
3117  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
3118  break;
3119  case 'currentyear':
3120  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
3121  break;
3122  case 'currenttime':
3123  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
3124  break;
3125  case 'currenthour':
3126  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
3127  break;
3128  case 'currentweek':
3129  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3130  # int to remove the padding
3131  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
3132  break;
3133  case 'currentdow':
3134  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
3135  break;
3136  case 'localdayname':
3137  $value = $pageLang->getWeekdayName(
3138  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
3139  );
3140  break;
3141  case 'localyear':
3142  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
3143  break;
3144  case 'localtime':
3145  $value = $pageLang->time(
3146  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
3147  false,
3148  false
3149  );
3150  break;
3151  case 'localhour':
3152  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
3153  break;
3154  case 'localweek':
3155  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3156  # int to remove the padding
3157  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
3158  break;
3159  case 'localdow':
3160  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
3161  break;
3162  case 'numberofarticles':
3163  $value = $pageLang->formatNum( SiteStats::articles() );
3164  break;
3165  case 'numberoffiles':
3166  $value = $pageLang->formatNum( SiteStats::images() );
3167  break;
3168  case 'numberofusers':
3169  $value = $pageLang->formatNum( SiteStats::users() );
3170  break;
3171  case 'numberofactiveusers':
3172  $value = $pageLang->formatNum( SiteStats::activeUsers() );
3173  break;
3174  case 'numberofpages':
3175  $value = $pageLang->formatNum( SiteStats::pages() );
3176  break;
3177  case 'numberofadmins':
3178  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
3179  break;
3180  case 'numberofedits':
3181  $value = $pageLang->formatNum( SiteStats::edits() );
3182  break;
3183  case 'currenttimestamp':
3184  $value = wfTimestamp( TS_MW, $ts );
3185  break;
3186  case 'localtimestamp':
3187  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
3188  break;
3189  case 'currentversion':
3191  break;
3192  case 'articlepath':
3193  return $wgArticlePath;
3194  case 'sitename':
3195  return $wgSitename;
3196  case 'server':
3197  return $wgServer;
3198  case 'servername':
3199  return $wgServerName;
3200  case 'scriptpath':
3201  return $wgScriptPath;
3202  case 'stylepath':
3203  return $wgStylePath;
3204  case 'directionmark':
3205  return $pageLang->getDirMark();
3206  case 'contentlanguage':
3208  return $wgLanguageCode;
3209  case 'cascadingsources':
3211  break;
3212  default:
3213  $ret = null;
3214  Hooks::run(
3215  'ParserGetVariableValueSwitch',
3216  array( &$this, &$this->mVarCache, &$index, &$ret, &$frame )
3217  );
3218 
3219  return $ret;
3220  }
3221 
3222  if ( $index ) {
3223  $this->mVarCache[$index] = $value;
3224  }
3225 
3226  return $value;
3227  }
3228 
3234  public function initialiseVariables() {
3235  $variableIDs = MagicWord::getVariableIDs();
3236  $substIDs = MagicWord::getSubstIDs();
3237 
3238  $this->mVariables = new MagicWordArray( $variableIDs );
3239  $this->mSubstWords = new MagicWordArray( $substIDs );
3240  }
3264  public function preprocessToDom( $text, $flags = 0 ) {
3265  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3266  return $dom;
3267  }
3268 
3276  public static function splitWhitespace( $s ) {
3277  $ltrimmed = ltrim( $s );
3278  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3279  $trimmed = rtrim( $ltrimmed );
3280  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3281  if ( $diff > 0 ) {
3282  $w2 = substr( $ltrimmed, -$diff );
3283  } else {
3284  $w2 = '';
3285  }
3286  return array( $w1, $trimmed, $w2 );
3287  }
3288 
3309  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3310  # Is there any text? Also, Prevent too big inclusions!
3311  if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
3312  return $text;
3313  }
3315  if ( $frame === false ) {
3316  $frame = $this->getPreprocessor()->newFrame();
3317  } elseif ( !( $frame instanceof PPFrame ) ) {
3318  wfDebug( __METHOD__ . " called using plain parameters instead of "
3319  . "a PPFrame instance. Creating custom frame.\n" );
3320  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3321  }
3322 
3323  $dom = $this->preprocessToDom( $text );
3324  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3325  $text = $frame->expand( $dom, $flags );
3326 
3327  return $text;
3328  }
3329 
3337  public static function createAssocArgs( $args ) {
3338  $assocArgs = array();
3339  $index = 1;
3340  foreach ( $args as $arg ) {
3341  $eqpos = strpos( $arg, '=' );
3342  if ( $eqpos === false ) {
3343  $assocArgs[$index++] = $arg;
3344  } else {
3345  $name = trim( substr( $arg, 0, $eqpos ) );
3346  $value = trim( substr( $arg, $eqpos + 1 ) );
3347  if ( $value === false ) {
3348  $value = '';
3349  }
3350  if ( $name !== false ) {
3351  $assocArgs[$name] = $value;
3352  }
3353  }
3354  }
3355 
3356  return $assocArgs;
3357  }
3358 
3383  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3384  # does no harm if $current and $max are present but are unnecessary for the message
3385  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3386  ->inLanguage( $this->mOptions->getUserLangObj() )->text();
3387  $this->mOutput->addWarning( $warning );
3388  $this->addTrackingCategory( "$limitationType-category" );
3389  }
3390 
3403  public function braceSubstitution( $piece, $frame ) {
3404 
3405  // Flags
3406 
3407  // $text has been filled
3408  $found = false;
3409  // wiki markup in $text should be escaped
3410  $nowiki = false;
3411  // $text is HTML, armour it against wikitext transformation
3412  $isHTML = false;
3413  // Force interwiki transclusion to be done in raw mode not rendered
3414  $forceRawInterwiki = false;
3415  // $text is a DOM node needing expansion in a child frame
3416  $isChildObj = false;
3417  // $text is a DOM node needing expansion in the current frame
3418  $isLocalObj = false;
3419 
3420  # Title object, where $text came from
3421  $title = false;
3422 
3423  # $part1 is the bit before the first |, and must contain only title characters.
3424  # Various prefixes will be stripped from it later.
3425  $titleWithSpaces = $frame->expand( $piece['title'] );
3426  $part1 = trim( $titleWithSpaces );
3427  $titleText = false;
3428 
3429  # Original title text preserved for various purposes
3430  $originalTitle = $part1;
3431 
3432  # $args is a list of argument nodes, starting from index 0, not including $part1
3433  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3434  # below won't work b/c this $args isn't an object
3435  $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
3436 
3437  $profileSection = null; // profile templates
3438 
3439  # SUBST
3440  if ( !$found ) {
3441  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3442 
3443  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3444  # Decide whether to expand template or keep wikitext as-is.
3445  if ( $this->ot['wiki'] ) {
3446  if ( $substMatch === false ) {
3447  $literal = true; # literal when in PST with no prefix
3448  } else {
3449  $literal = false; # expand when in PST with subst: or safesubst:
3450  }
3451  } else {
3452  if ( $substMatch == 'subst' ) {
3453  $literal = true; # literal when not in PST with plain subst:
3454  } else {
3455  $literal = false; # expand when not in PST with safesubst: or no prefix
3456  }
3457  }
3458  if ( $literal ) {
3459  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3460  $isLocalObj = true;
3461  $found = true;
3462  }
3463  }
3464 
3465  # Variables
3466  if ( !$found && $args->getLength() == 0 ) {
3467  $id = $this->mVariables->matchStartToEnd( $part1 );
3468  if ( $id !== false ) {
3469  $text = $this->getVariableValue( $id, $frame );
3470  if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3471  $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3472  }
3473  $found = true;
3474  }
3475  }
3476 
3477  # MSG, MSGNW and RAW
3478  if ( !$found ) {
3479  # Check for MSGNW:
3480  $mwMsgnw = MagicWord::get( 'msgnw' );
3481  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3482  $nowiki = true;
3483  } else {
3484  # Remove obsolete MSG:
3485  $mwMsg = MagicWord::get( 'msg' );
3486  $mwMsg->matchStartAndRemove( $part1 );
3487  }
3488 
3489  # Check for RAW:
3490  $mwRaw = MagicWord::get( 'raw' );
3491  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3492  $forceRawInterwiki = true;
3493  }
3494  }
3495 
3496  # Parser functions
3497  if ( !$found ) {
3498  $colonPos = strpos( $part1, ':' );
3499  if ( $colonPos !== false ) {
3500  $func = substr( $part1, 0, $colonPos );
3501  $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
3502  for ( $i = 0; $i < $args->getLength(); $i++ ) {
3503  $funcArgs[] = $args->item( $i );
3504  }
3505  try {
3506  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3507  } catch ( Exception $ex ) {
3508  throw $ex;
3509  }
3510 
3511  # The interface for parser functions allows for extracting
3512  # flags into the local scope. Extract any forwarded flags
3513  # here.
3514  extract( $result );
3515  }
3516  }
3517 
3518  # Finish mangling title and then check for loops.
3519  # Set $title to a Title object and $titleText to the PDBK
3520  if ( !$found ) {
3521  $ns = NS_TEMPLATE;
3522  # Split the title into page and subpage
3523  $subpage = '';
3524  $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3525  if ( $part1 !== $relative ) {
3526  $part1 = $relative;
3527  $ns = $this->mTitle->getNamespace();
3528  }
3529  $title = Title::newFromText( $part1, $ns );
3530  if ( $title ) {
3531  $titleText = $title->getPrefixedText();
3532  # Check for language variants if the template is not found
3533  if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3534  $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3535  }
3536  # Do recursion depth check
3537  $limit = $this->mOptions->getMaxTemplateDepth();
3538  if ( $frame->depth >= $limit ) {
3539  $found = true;
3540  $text = '<span class="error">'
3541  . wfMessage( 'parser-template-recursion-depth-warning' )
3542  ->numParams( $limit )->inContentLanguage()->text()
3543  . '</span>';
3544  }
3545  }
3546  }
3547 
3548  # Load from database
3549  if ( !$found && $title ) {
3550  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3551  if ( !$title->isExternal() ) {
3552  if ( $title->isSpecialPage()
3553  && $this->mOptions->getAllowSpecialInclusion()
3554  && $this->ot['html']
3555  ) {
3556  // Pass the template arguments as URL parameters.
3557  // "uselang" will have no effect since the Language object
3558  // is forced to the one defined in ParserOptions.
3559  $pageArgs = array();
3560  $argsLength = $args->getLength();
3561  for ( $i = 0; $i < $argsLength; $i++ ) {
3562  $bits = $args->item( $i )->splitArg();
3563  if ( strval( $bits['index'] ) === '' ) {
3564  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3565  $value = trim( $frame->expand( $bits['value'] ) );
3566  $pageArgs[$name] = $value;
3567  }
3568  }
3569 
3570  // Create a new context to execute the special page
3571  $context = new RequestContext;
3572  $context->setTitle( $title );
3573  $context->setRequest( new FauxRequest( $pageArgs ) );
3574  $context->setUser( $this->getUser() );
3575  $context->setLanguage( $this->mOptions->getUserLangObj() );
3576  $ret = SpecialPageFactory::capturePath( $title, $context );
3577  if ( $ret ) {
3578  $text = $context->getOutput()->getHTML();
3579  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3580  $found = true;
3581  $isHTML = true;
3582  $this->disableCache();
3583  }
3584  } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3585  $found = false; # access denied
3586  wfDebug( __METHOD__ . ": template inclusion denied for " .
3587  $title->getPrefixedDBkey() . "\n" );
3588  } else {
3589  list( $text, $title ) = $this->getTemplateDom( $title );
3590  if ( $text !== false ) {
3591  $found = true;
3592  $isChildObj = true;
3593  }
3594  }
3595 
3596  # If the title is valid but undisplayable, make a link to it
3597  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3598  $text = "[[:$titleText]]";
3599  $found = true;
3600  }
3601  } elseif ( $title->isTrans() ) {
3602  # Interwiki transclusion
3603  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3604  $text = $this->interwikiTransclude( $title, 'render' );
3605  $isHTML = true;
3606  } else {
3607  $text = $this->interwikiTransclude( $title, 'raw' );
3608  # Preprocess it like a template
3609  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3610  $isChildObj = true;
3611  }
3612  $found = true;
3613  }
3614 
3615  # Do infinite loop check
3616  # This has to be done after redirect resolution to avoid infinite loops via redirects
3617  if ( !$frame->loopCheck( $title ) ) {
3618  $found = true;
3619  $text = '<span class="error">'
3620  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3621  . '</span>';
3622  wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3623  }
3624  }
3625 
3626  # If we haven't found text to substitute by now, we're done
3627  # Recover the source wikitext and return it
3628  if ( !$found ) {
3629  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3630  if ( $profileSection ) {
3631  $this->mProfiler->scopedProfileOut( $profileSection );
3632  }
3633  return array( 'object' => $text );
3634  }
3635 
3636  # Expand DOM-style return values in a child frame
3637  if ( $isChildObj ) {
3638  # Clean up argument array
3639  $newFrame = $frame->newChild( $args, $title );
3640 
3641  if ( $nowiki ) {
3642  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3643  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3644  # Expansion is eligible for the empty-frame cache
3645  $text = $newFrame->cachedExpand( $titleText, $text );
3646  } else {
3647  # Uncached expansion
3648  $text = $newFrame->expand( $text );
3649  }
3650  }
3651  if ( $isLocalObj && $nowiki ) {
3652  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3653  $isLocalObj = false;
3654  }
3655 
3656  if ( $profileSection ) {
3657  $this->mProfiler->scopedProfileOut( $profileSection );
3658  }
3659 
3660  # Replace raw HTML by a placeholder
3661  if ( $isHTML ) {
3662  $text = $this->insertStripItem( $text );
3663  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3664  # Escape nowiki-style return values
3665  $text = wfEscapeWikiText( $text );
3666  } elseif ( is_string( $text )
3667  && !$piece['lineStart']
3668  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3669  ) {
3670  # Bug 529: if the template begins with a table or block-level
3671  # element, it should be treated as beginning a new line.
3672  # This behavior is somewhat controversial.
3673  $text = "\n" . $text;
3674  }
3675 
3676  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3677  # Error, oversize inclusion
3678  if ( $titleText !== false ) {
3679  # Make a working, properly escaped link if possible (bug 23588)
3680  $text = "[[:$titleText]]";
3681  } else {
3682  # This will probably not be a working link, but at least it may
3683  # provide some hint of where the problem is
3684  preg_replace( '/^:/', '', $originalTitle );
3685  $text = "[[:$originalTitle]]";
3686  }
3687  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3688  . 'post-expand include size too large -->' );
3689  $this->limitationWarn( 'post-expand-template-inclusion' );
3690  }
3691 
3692  if ( $isLocalObj ) {
3693  $ret = array( 'object' => $text );
3694  } else {
3695  $ret = array( 'text' => $text );
3696  }
3698  return $ret;
3699  }
3700 
3720  public function callParserFunction( $frame, $function, array $args = array() ) {
3722 
3723 
3724  # Case sensitive functions
3725  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3726  $function = $this->mFunctionSynonyms[1][$function];
3727  } else {
3728  # Case insensitive functions
3729  $function = $wgContLang->lc( $function );
3730  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3731  $function = $this->mFunctionSynonyms[0][$function];
3732  } else {
3733  return array( 'found' => false );
3734  }
3735  }
3736 
3737  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3738 
3739  # Workaround for PHP bug 35229 and similar
3740  if ( !is_callable( $callback ) ) {
3741  throw new MWException( "Tag hook for $function is not callable\n" );
3742  }
3743 
3744  $allArgs = array( &$this );
3745  if ( $flags & self::SFH_OBJECT_ARGS ) {
3746  # Convert arguments to PPNodes and collect for appending to $allArgs
3747  $funcArgs = array();
3748  foreach ( $args as $k => $v ) {
3749  if ( $v instanceof PPNode || $k === 0 ) {
3750  $funcArgs[] = $v;
3751  } else {
3752  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( array( $k => $v ) )->item( 0 );
3753  }
3754  }
3755 
3756  # Add a frame parameter, and pass the arguments as an array
3757  $allArgs[] = $frame;
3758  $allArgs[] = $funcArgs;
3759  } else {
3760  # Convert arguments to plain text and append to $allArgs
3761  foreach ( $args as $k => $v ) {
3762  if ( $v instanceof PPNode ) {
3763  $allArgs[] = trim( $frame->expand( $v ) );
3764  } elseif ( is_int( $k ) && $k >= 0 ) {
3765  $allArgs[] = trim( $v );
3766  } else {
3767  $allArgs[] = trim( "$k=$v" );
3768  }
3769  }
3770  }
3771 
3772  $result = call_user_func_array( $callback, $allArgs );
3773 
3774  # The interface for function hooks allows them to return a wikitext
3775  # string or an array containing the string and any flags. This mungs
3776  # things around to match what this method should return.
3777  if ( !is_array( $result ) ) {
3778  $result = array(
3779  'found' => true,
3780  'text' => $result,
3781  );
3782  } else {
3783  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3784  $result['text'] = $result[0];
3785  }
3786  unset( $result[0] );
3787  $result += array(
3788  'found' => true,
3789  );
3790  }
3791 
3792  $noparse = true;
3793  $preprocessFlags = 0;
3794  if ( isset( $result['noparse'] ) ) {
3795  $noparse = $result['noparse'];
3796  }
3797  if ( isset( $result['preprocessFlags'] ) ) {
3798  $preprocessFlags = $result['preprocessFlags'];
3799  }
3800 
3801  if ( !$noparse ) {
3802  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3803  $result['isChildObj'] = true;
3804  }
3805 
3806  return $result;
3807  }
3808 
3817  public function getTemplateDom( $title ) {
3818  $cacheTitle = $title;
3819  $titleText = $title->getPrefixedDBkey();
3820 
3821  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3822  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3823  $title = Title::makeTitle( $ns, $dbk );
3824  $titleText = $title->getPrefixedDBkey();
3825  }
3826  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3827  return array( $this->mTplDomCache[$titleText], $title );
3828  }
3829 
3830  # Cache miss, go to the database
3831  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3832 
3833  if ( $text === false ) {
3834  $this->mTplDomCache[$titleText] = false;
3835  return array( false, $title );
3836  }
3838  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3839  $this->mTplDomCache[$titleText] = $dom;
3840 
3841  if ( !$title->equals( $cacheTitle ) ) {
3842  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3843  array( $title->getNamespace(), $cdb = $title->getDBkey() );
3844  }
3845 
3846  return array( $dom, $title );
3847  }
3848 
3860  public function fetchCurrentRevisionOfTitle( $title ) {
3861  $cacheKey = $title->getPrefixedDBkey();
3862  if ( !$this->currentRevisionCache ) {
3863  $this->currentRevisionCache = new MapCacheLRU( 100 );
3864  }
3865  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3866  $this->currentRevisionCache->set( $cacheKey,
3867  // Defaults to Parser::statelessFetchRevision()
3868  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3869  );
3870  }
3871  return $this->currentRevisionCache->get( $cacheKey );
3872  }
3873 
3883  public static function statelessFetchRevision( $title, $parser = false ) {
3884  return Revision::newFromTitle( $title );
3885  }
3886 
3892  public function fetchTemplateAndTitle( $title ) {
3893  // Defaults to Parser::statelessFetchTemplate()
3894  $templateCb = $this->mOptions->getTemplateCallback();
3895  $stuff = call_user_func( $templateCb, $title, $this );
3896  // We use U+007F DELETE to distinguish strip markers from regular text.
3897  $text = $stuff['text'];
3898  if ( is_string( $stuff['text'] ) ) {
3899  $text = strtr( $text, "\x7f", "?" );
3900  }
3901  $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3902  if ( isset( $stuff['deps'] ) ) {
3903  foreach ( $stuff['deps'] as $dep ) {
3904  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3905  if ( $dep['title']->equals( $this->getTitle() ) ) {
3906  // If we transclude ourselves, the final result
3907  // will change based on the new version of the page
3908  $this->mOutput->setFlag( 'vary-revision' );
3909  }
3910  }
3911  }
3912  return array( $text, $finalTitle );
3913  }
3914 
3920  public function fetchTemplate( $title ) {
3921  $rv = $this->fetchTemplateAndTitle( $title );
3922  return $rv[0];
3923  }
3924 
3934  public static function statelessFetchTemplate( $title, $parser = false ) {
3935  $text = $skip = false;
3936  $finalTitle = $title;
3937  $deps = array();
3938 
3939  # Loop to fetch the article, with up to 1 redirect
3940  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3941  # Give extensions a chance to select the revision instead
3942  $id = false; # Assume current
3943  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3944  array( $parser, $title, &$skip, &$id ) );
3945 
3946  if ( $skip ) {
3947  $text = false;
3948  $deps[] = array(
3949  'title' => $title,
3950  'page_id' => $title->getArticleID(),
3951  'rev_id' => null
3952  );
3953  break;
3954  }
3955  # Get the revision
3956  if ( $id ) {
3957  $rev = Revision::newFromId( $id );
3958  } elseif ( $parser ) {
3959  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3960  } else {
3961  $rev = Revision::newFromTitle( $title );
3962  }
3963  $rev_id = $rev ? $rev->getId() : 0;
3964  # If there is no current revision, there is no page
3965  if ( $id === false && !$rev ) {
3966  $linkCache = LinkCache::singleton();
3967  $linkCache->addBadLinkObj( $title );
3968  }
3969 
3970  $deps[] = array(
3971  'title' => $title,
3972  'page_id' => $title->getArticleID(),
3973  'rev_id' => $rev_id );
3974  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3975  # We fetched a rev from a different title; register it too...
3976  $deps[] = array(
3977  'title' => $rev->getTitle(),
3978  'page_id' => $rev->getPage(),
3979  'rev_id' => $rev_id );
3980  }
3981 
3982  if ( $rev ) {
3983  $content = $rev->getContent();
3984  $text = $content ? $content->getWikitextForTransclusion() : null;
3985 
3986  if ( $text === false || $text === null ) {
3987  $text = false;
3988  break;
3989  }
3990  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3992  $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3993  if ( !$message->exists() ) {
3994  $text = false;
3995  break;
3996  }
3997  $content = $message->content();
3998  $text = $message->plain();
3999  } else {
4000  break;
4001  }
4002  if ( !$content ) {
4003  break;
4004  }
4005  # Redirect?
4006  $finalTitle = $title;
4007  $title = $content->getRedirectTarget();
4008  }
4009  return array(
4010  'text' => $text,
4011  'finalTitle' => $finalTitle,
4012  'deps' => $deps );
4013  }
4014 
4022  public function fetchFile( $title, $options = array() ) {
4023  $res = $this->fetchFileAndTitle( $title, $options );
4024  return $res[0];
4025  }
4026 
4034  public function fetchFileAndTitle( $title, $options = array() ) {
4035  $file = $this->fetchFileNoRegister( $title, $options );
4037  $time = $file ? $file->getTimestamp() : false;
4038  $sha1 = $file ? $file->getSha1() : false;
4039  # Register the file as a dependency...
4040  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4041  if ( $file && !$title->equals( $file->getTitle() ) ) {
4042  # Update fetched file title
4043  $title = $file->getTitle();
4044  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4045  }
4046  return array( $file, $title );
4047  }
4048 
4059  protected function fetchFileNoRegister( $title, $options = array() ) {
4060  if ( isset( $options['broken'] ) ) {
4061  $file = false; // broken thumbnail forced by hook
4062  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
4063  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
4064  } else { // get by (name,timestamp)
4065  $file = wfFindFile( $title, $options );
4066  }
4067  return $file;
4068  }
4069 
4078  public function interwikiTransclude( $title, $action ) {
4080 
4081  if ( !$wgEnableScaryTranscluding ) {
4082  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
4083  }
4084 
4085  $url = $title->getFullURL( array( 'action' => $action ) );
4086 
4087  if ( strlen( $url ) > 255 ) {
4088  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
4089  }
4090  return $this->fetchScaryTemplateMaybeFromCache( $url );
4091  }
4092 
4097  public function fetchScaryTemplateMaybeFromCache( $url ) {
4099  $dbr = wfGetDB( DB_SLAVE );
4100  $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
4101  $obj = $dbr->selectRow( 'transcache', array( 'tc_time', 'tc_contents' ),
4102  array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
4103  if ( $obj ) {
4104  return $obj->tc_contents;
4105  }
4106 
4107  $req = MWHttpRequest::factory( $url, array(), __METHOD__ );
4108  $status = $req->execute(); // Status object
4109  if ( $status->isOK() ) {
4110  $text = $req->getContent();
4111  } elseif ( $req->getStatus() != 200 ) {
4112  // Though we failed to fetch the content, this status is useless.
4113  return wfMessage( 'scarytranscludefailed-httpstatus' )
4114  ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
4115  } else {
4116  return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
4117  }
4118 
4119  $dbw = wfGetDB( DB_MASTER );
4120  $dbw->replace( 'transcache', array( 'tc_url' ), array(
4121  'tc_url' => $url,
4122  'tc_time' => $dbw->timestamp( time() ),
4123  'tc_contents' => $text
4124  ) );
4125  return $text;
4126  }
4127 
4137  public function argSubstitution( $piece, $frame ) {
4138 
4139  $error = false;
4140  $parts = $piece['parts'];
4141  $nameWithSpaces = $frame->expand( $piece['title'] );
4142  $argName = trim( $nameWithSpaces );
4143  $object = false;
4144  $text = $frame->getArgument( $argName );
4145  if ( $text === false && $parts->getLength() > 0
4146  && ( $this->ot['html']
4147  || $this->ot['pre']
4148  || ( $this->ot['wiki'] && $frame->isTemplate() )
4149  )
4150  ) {
4151  # No match in frame, use the supplied default
4152  $object = $parts->item( 0 )->getChildren();
4153  }
4154  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
4155  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
4156  $this->limitationWarn( 'post-expand-template-argument' );
4157  }
4158 
4159  if ( $text === false && $object === false ) {
4160  # No match anywhere
4161  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
4162  }
4163  if ( $error !== false ) {
4164  $text .= $error;
4165  }
4166  if ( $object !== false ) {
4167  $ret = array( 'object' => $object );
4168  } else {
4169  $ret = array( 'text' => $text );
4170  }
4171 
4172  return $ret;
4173  }
4174 
4190  public function extensionSubstitution( $params, $frame ) {
4191  $name = $frame->expand( $params['name'] );
4192  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
4193  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4194  $marker = self::MARKER_PREFIX . "-$name-"
4195  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4196 
4197  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4198  ( $this->ot['html'] || $this->ot['pre'] );
4199  if ( $isFunctionTag ) {
4200  $markerType = 'none';
4201  } else {
4202  $markerType = 'general';
4203  }
4204  if ( $this->ot['html'] || $isFunctionTag ) {
4205  $name = strtolower( $name );
4206  $attributes = Sanitizer::decodeTagAttributes( $attrText );
4207  if ( isset( $params['attributes'] ) ) {
4208  $attributes = $attributes + $params['attributes'];
4209  }
4210 
4211  if ( isset( $this->mTagHooks[$name] ) ) {
4212  # Workaround for PHP bug 35229 and similar
4213  if ( !is_callable( $this->mTagHooks[$name] ) ) {
4214  throw new MWException( "Tag hook for $name is not callable\n" );
4215  }
4216  $output = call_user_func_array( $this->mTagHooks[$name],
4217  array( $content, $attributes, $this, $frame ) );
4218  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4219  list( $callback, ) = $this->mFunctionTagHooks[$name];
4220  if ( !is_callable( $callback ) ) {
4221  throw new MWException( "Tag hook for $name is not callable\n" );
4222  }
4223 
4224  $output = call_user_func_array( $callback, array( &$this, $frame, $content, $attributes ) );
4225  } else {
4226  $output = '<span class="error">Invalid tag extension name: ' .
4227  htmlspecialchars( $name ) . '</span>';
4228  }
4229 
4230  if ( is_array( $output ) ) {
4231  # Extract flags to local scope (to override $markerType)
4232  $flags = $output;
4233  $output = $flags[0];
4234  unset( $flags[0] );
4235  extract( $flags );
4236  }
4237  } else {
4238  if ( is_null( $attrText ) ) {
4239  $attrText = '';
4240  }
4241  if ( isset( $params['attributes'] ) ) {
4242  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4243  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4244  htmlspecialchars( $attrValue ) . '"';
4245  }
4246  }
4247  if ( $content === null ) {
4248  $output = "<$name$attrText/>";
4249  } else {
4250  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4251  $output = "<$name$attrText>$content$close";
4252  }
4253  }
4254 
4255  if ( $markerType === 'none' ) {
4256  return $output;
4257  } elseif ( $markerType === 'nowiki' ) {
4258  $this->mStripState->addNoWiki( $marker, $output );
4259  } elseif ( $markerType === 'general' ) {
4260  $this->mStripState->addGeneral( $marker, $output );
4261  } else {
4262  throw new MWException( __METHOD__ . ': invalid marker type' );
4263  }
4264  return $marker;
4265  }
4266 
4274  public function incrementIncludeSize( $type, $size ) {
4275  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4276  return false;
4277  } else {
4278  $this->mIncludeSizes[$type] += $size;
4279  return true;
4280  }
4281  }
4282 
4288  public function incrementExpensiveFunctionCount() {
4289  $this->mExpensiveFunctionCount++;
4290  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4291  }
4292 
4301  public function doDoubleUnderscore( $text ) {
4302 
4303  # The position of __TOC__ needs to be recorded
4304  $mw = MagicWord::get( 'toc' );
4305  if ( $mw->match( $text ) ) {
4306  $this->mShowToc = true;
4307  $this->mForceTocPosition = true;
4308 
4309  # Set a placeholder. At the end we'll fill it in with the TOC.
4310  $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
4311 
4312  # Only keep the first one.
4313  $text = $mw->replace( '', $text );
4314  }
4315 
4316  # Now match and remove the rest of them
4318  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4319 
4320  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4321  $this->mOutput->mNoGallery = true;
4322  }
4323  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4324  $this->mShowToc = false;
4325  }
4326  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4327  && $this->mTitle->getNamespace() == NS_CATEGORY
4328  ) {
4329  $this->addTrackingCategory( 'hidden-category-category' );
4330  }
4331  # (bug 8068) Allow control over whether robots index a page.
4332  #
4333  # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
4334  # is not desirable, the last one on the page should win.
4335  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4336  $this->mOutput->setIndexPolicy( 'noindex' );
4337  $this->addTrackingCategory( 'noindex-category' );
4338  }
4339  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4340  $this->mOutput->setIndexPolicy( 'index' );
4341  $this->addTrackingCategory( 'index-category' );
4342  }
4343 
4344  # Cache all double underscores in the database
4345  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4346  $this->mOutput->setProperty( $key, '' );
4347  }
4348 
4349  return $text;
4350  }
4351 
4357  public function addTrackingCategory( $msg ) {
4358  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4359  }
4360 
4377  public function formatHeadings( $text, $origText, $isMain = true ) {
4379 
4380  # Inhibit editsection links if requested in the page
4381  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4382  $maybeShowEditLink = $showEditLink = false;
4383  } else {
4384  $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4385  $showEditLink = $this->mOptions->getEditSection();
4386  }
4387  if ( $showEditLink ) {
4388  $this->mOutput->setEditSectionTokens( true );
4389  }
4390 
4391  # Get all headlines for numbering them and adding funky stuff like [edit]
4392  # links - this is for later, but we need the number of headlines right now
4393  $matches = array();
4394  $numMatches = preg_match_all(
4395  '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4396  $text,
4397  $matches
4398  );
4399 
4400  # if there are fewer than 4 headlines in the article, do not show TOC
4401  # unless it's been explicitly enabled.
4402  $enoughToc = $this->mShowToc &&
4403  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4404 
4405  # Allow user to stipulate that a page should have a "new section"
4406  # link added via __NEWSECTIONLINK__
4407  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4408  $this->mOutput->setNewSection( true );
4409  }
4410 
4411  # Allow user to remove the "new section"
4412  # link via __NONEWSECTIONLINK__
4413  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4414  $this->mOutput->hideNewSection( true );
4415  }
4416 
4417  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4418  # override above conditions and always show TOC above first header
4419  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4420  $this->mShowToc = true;
4421  $enoughToc = true;
4422  }
4423 
4424  # headline counter
4425  $headlineCount = 0;
4426  $numVisible = 0;
4427 
4428  # Ugh .. the TOC should have neat indentation levels which can be
4429  # passed to the skin functions. These are determined here
4430  $toc = '';
4431  $full = '';
4432  $head = array();
4433  $sublevelCount = array();
4434  $levelCount = array();
4435  $level = 0;
4436  $prevlevel = 0;
4437  $toclevel = 0;
4438  $prevtoclevel = 0;
4439  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4440  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4441  $oldType = $this->mOutputType;
4442  $this->setOutputType( self::OT_WIKI );
4443  $frame = $this->getPreprocessor()->newFrame();
4444  $root = $this->preprocessToDom( $origText );
4445  $node = $root->getFirstChild();
4446  $byteOffset = 0;
4447  $tocraw = array();
4448  $refers = array();
4449 
4450  $headlines = $numMatches !== false ? $matches[3] : array();
4451 
4452  foreach ( $headlines as $headline ) {
4453  $isTemplate = false;
4454  $titleText = false;
4455  $sectionIndex = false;
4456  $numbering = '';
4457  $markerMatches = array();
4458  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4459  $serial = $markerMatches[1];
4460  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4461  $isTemplate = ( $titleText != $baseTitleText );
4462  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4463  }
4464 
4465  if ( $toclevel ) {
4466  $prevlevel = $level;
4467  }
4468  $level = $matches[1][$headlineCount];
4469 
4470  if ( $level > $prevlevel ) {
4471  # Increase TOC level
4472  $toclevel++;
4473  $sublevelCount[$toclevel] = 0;
4474  if ( $toclevel < $wgMaxTocLevel ) {
4475  $prevtoclevel = $toclevel;
4476  $toc .= Linker::tocIndent();
4477  $numVisible++;
4478  }
4479  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4480  # Decrease TOC level, find level to jump to
4481 
4482  for ( $i = $toclevel; $i > 0; $i-- ) {
4483  if ( $levelCount[$i] == $level ) {
4484  # Found last matching level
4485  $toclevel = $i;
4486  break;
4487  } elseif ( $levelCount[$i] < $level ) {
4488  # Found first matching level below current level
4489  $toclevel = $i + 1;
4490  break;
4491  }
4492  }
4493  if ( $i == 0 ) {
4494  $toclevel = 1;
4495  }
4496  if ( $toclevel < $wgMaxTocLevel ) {
4497  if ( $prevtoclevel < $wgMaxTocLevel ) {
4498  # Unindent only if the previous toc level was shown :p
4499  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4500  $prevtoclevel = $toclevel;
4501  } else {
4502  $toc .= Linker::tocLineEnd();
4503  }
4504  }
4505  } else {
4506  # No change in level, end TOC line
4507  if ( $toclevel < $wgMaxTocLevel ) {
4508  $toc .= Linker::tocLineEnd();
4509  }
4510  }
4511 
4512  $levelCount[$toclevel] = $level;
4513 
4514  # count number of headlines for each level
4515  $sublevelCount[$toclevel]++;
4516  $dot = 0;
4517  for ( $i = 1; $i <= $toclevel; $i++ ) {
4518  if ( !empty( $sublevelCount[$i] ) ) {
4519  if ( $dot ) {
4520  $numbering .= '.';
4521  }
4522  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4523  $dot = 1;
4524  }
4525  }
4526 
4527  # The safe header is a version of the header text safe to use for links
4528 
4529  # Remove link placeholders by the link text.
4530  # <!--LINK number-->
4531  # turns into
4532  # link text with suffix
4533  # Do this before unstrip since link text can contain strip markers
4534  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4535 
4536  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4537  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4538 
4539  # Strip out HTML (first regex removes any tag not allowed)
4540  # Allowed tags are:
4541  # * <sup> and <sub> (bug 8393)
4542  # * <i> (bug 26375)
4543  # * <b> (r105284)
4544  # * <bdi> (bug 72884)
4545  # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4546  #
4547  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4548  # to allow setting directionality in toc items.
4549  $tocline = preg_replace(
4550  array(
4551  '#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4552  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4553  ),
4554  array( '', '<$1>' ),
4555  $safeHeadline
4556  );
4557  $tocline = trim( $tocline );
4558 
4559  # For the anchor, strip out HTML-y stuff period
4560  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4561  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4562 
4563  # Save headline for section edit hint before it's escaped
4564  $headlineHint = $safeHeadline;
4565 
4566  if ( $wgExperimentalHtmlIds ) {
4567  # For reverse compatibility, provide an id that's
4568  # HTML4-compatible, like we used to.
4569  #
4570  # It may be worth noting, academically, that it's possible for
4571  # the legacy anchor to conflict with a non-legacy headline
4572  # anchor on the page. In this case likely the "correct" thing
4573  # would be to either drop the legacy anchors or make sure
4574  # they're numbered first. However, this would require people
4575  # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4576  # manually, so let's not bother worrying about it.
4577  $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4578  array( 'noninitial', 'legacy' ) );
4579  $safeHeadline = Sanitizer::escapeId( $safeHeadline );
4580 
4581  if ( $legacyHeadline == $safeHeadline ) {
4582  # No reason to have both (in fact, we can't)
4583  $legacyHeadline = false;
4584  }
4585  } else {
4586  $legacyHeadline = false;
4587  $safeHeadline = Sanitizer::escapeId( $safeHeadline,
4588  'noninitial' );
4589  }
4590 
4591  # HTML names must be case-insensitively unique (bug 10721).
4592  # This does not apply to Unicode characters per
4593  # http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4594  # @todo FIXME: We may be changing them depending on the current locale.
4595  $arrayKey = strtolower( $safeHeadline );
4596  if ( $legacyHeadline === false ) {
4597  $legacyArrayKey = false;
4598  } else {
4599  $legacyArrayKey = strtolower( $legacyHeadline );
4600  }
4601 
4602  # Create the anchor for linking from the TOC to the section
4603  $anchor = $safeHeadline;
4604  $legacyAnchor = $legacyHeadline;
4605  if ( isset( $refers[$arrayKey] ) ) {
4606  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4607  $anchor .= "_$i";
4608  $refers["${arrayKey}_$i"] = true;
4609  } else {
4610  $refers[$arrayKey] = true;
4611  }
4612  if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4613  for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4614  $legacyAnchor .= "_$i";
4615  $refers["${legacyArrayKey}_$i"] = true;
4616  } else {
4617  $refers[$legacyArrayKey] = true;
4618  }
4619 
4620  # Don't number the heading if it is the only one (looks silly)
4621  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4622  # the two are different if the line contains a link
4623  $headline = Html::element(
4624  'span',
4625  array( 'class' => 'mw-headline-number' ),
4626  $numbering
4627  ) . ' ' . $headline;
4628  }
4629 
4630  if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4631  $toc .= Linker::tocLine( $anchor, $tocline,
4632  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4633  }
4634 
4635  # Add the section to the section tree
4636  # Find the DOM node for this header
4637  $noOffset = ( $isTemplate || $sectionIndex === false );
4638  while ( $node && !$noOffset ) {
4639  if ( $node->getName() === 'h' ) {
4640  $bits = $node->splitHeading();
4641  if ( $bits['i'] == $sectionIndex ) {
4642  break;
4643  }
4644  }
4645  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4646  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4647  $node = $node->getNextSibling();
4648  }
4649  $tocraw[] = array(
4650  'toclevel' => $toclevel,
4651  'level' => $level,
4652  'line' => $tocline,
4653  'number' => $numbering,
4654  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4655  'fromtitle' => $titleText,
4656  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4657  'anchor' => $anchor,
4658  );
4659 
4660  # give headline the correct <h#> tag
4661  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4662  // Output edit section links as markers with styles that can be customized by skins
4663  if ( $isTemplate ) {
4664  # Put a T flag in the section identifier, to indicate to extractSections()
4665  # that sections inside <includeonly> should be counted.
4666  $editsectionPage = $titleText;
4667  $editsectionSection = "T-$sectionIndex";
4668  $editsectionContent = null;
4669  } else {
4670  $editsectionPage = $this->mTitle->getPrefixedText();
4671  $editsectionSection = $sectionIndex;
4672  $editsectionContent = $headlineHint;
4673  }
4674  // We use a bit of pesudo-xml for editsection markers. The
4675  // language converter is run later on. Using a UNIQ style marker
4676  // leads to the converter screwing up the tokens when it
4677  // converts stuff. And trying to insert strip tags fails too. At
4678  // this point all real inputted tags have already been escaped,
4679  // so we don't have to worry about a user trying to input one of
4680  // these markers directly. We use a page and section attribute
4681  // to stop the language converter from converting these
4682  // important bits of data, but put the headline hint inside a
4683  // content block because the language converter is supposed to
4684  // be able to convert that piece of data.
4685  // Gets replaced with html in ParserOutput::getText
4686  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4687  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4688  if ( $editsectionContent !== null ) {
4689  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4690  } else {
4691  $editlink .= '/>';
4692  }
4693  } else {
4694  $editlink = '';
4695  }
4696  $head[$headlineCount] = Linker::makeHeadline( $level,
4697  $matches['attrib'][$headlineCount], $anchor, $headline,
4698  $editlink, $legacyAnchor );
4699 
4700  $headlineCount++;
4701  }
4702 
4703  $this->setOutputType( $oldType );
4704 
4705  # Never ever show TOC if no headers
4706  if ( $numVisible < 1 ) {
4707  $enoughToc = false;
4708  }
4709 
4710  if ( $enoughToc ) {
4711  if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4712  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4713  }
4714  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4715  $this->mOutput->setTOCHTML( $toc );
4716  $toc = self::TOC_START . $toc . self::TOC_END;
4717  $this->mOutput->addModules( 'mediawiki.toc' );
4718  }
4719 
4720  if ( $isMain ) {
4721  $this->mOutput->setSections( $tocraw );
4722  }
4723 
4724  # split up and insert constructed headlines
4725  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4726  $i = 0;
4727 
4728  // build an array of document sections
4729  $sections = array();
4730  foreach ( $blocks as $block ) {
4731  // $head is zero-based, sections aren't.
4732  if ( empty( $head[$i - 1] ) ) {
4733  $sections[$i] = $block;
4734  } else {
4735  $sections[$i] = $head[$i - 1] . $block;
4736  }
4737 
4748  Hooks::run( 'ParserSectionCreate', array( $this, $i, &$sections[$i], $showEditLink ) );
4749 
4750  $i++;
4751  }
4752 
4753  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4754  // append the TOC at the beginning
4755  // Top anchor now in skin
4756  $sections[0] = $sections[0] . $toc . "\n";
4757  }
4758 
4759  $full .= join( '', $sections );
4760 
4761  if ( $this->mForceTocPosition ) {
4762  return str_replace( '<!--MWTOC-->', $toc, $full );
4763  } else {
4764  return $full;
4765  }
4766  }
4767 
4779  public function preSaveTransform( $text, Title $title, User $user,
4780  ParserOptions $options, $clearState = true
4781  ) {
4782  if ( $clearState ) {
4783  $magicScopeVariable = $this->lock();
4784  }
4785  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4786  $this->setUser( $user );
4787 
4788  $pairs = array(
4789  "\r\n" => "\n",
4790  "\r" => "\n",
4791  );
4792  $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4793  if ( $options->getPreSaveTransform() ) {
4794  $text = $this->pstPass2( $text, $user );
4795  }
4796  $text = $this->mStripState->unstripBoth( $text );
4797 
4798  $this->setUser( null ); #Reset
4799 
4800  return $text;
4801  }
4802 
4811  private function pstPass2( $text, $user ) {
4813 
4814  # Note: This is the timestamp saved as hardcoded wikitext to
4815  # the database, we use $wgContLang here in order to give
4816  # everyone the same signature and use the default one rather
4817  # than the one selected in each user's preferences.
4818  # (see also bug 12815)
4819  $ts = $this->mOptions->getTimestamp();
4821  $ts = $timestamp->format( 'YmdHis' );
4822  $tzMsg = $timestamp->format( 'T' ); # might vary on DST changeover!
4823 
4824  # Allow translation of timezones through wiki. format() can return
4825  # whatever crap the system uses, localised or not, so we cannot
4826  # ship premade translations.
4827  $key = 'timezone-' . strtolower( trim( $tzMsg ) );
4828  $msg = wfMessage( $key )->inContentLanguage();
4829  if ( $msg->exists() ) {
4830  $tzMsg = $msg->text();
4831  }
4832 
4833  $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4834 
4835  # Variable replacement
4836  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4837  $text = $this->replaceVariables( $text );
4838 
4839  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4840  # which may corrupt this parser instance via its wfMessage()->text() call-
4841 
4842  # Signatures
4843  $sigText = $this->getUserSig( $user );
4844  $text = strtr( $text, array(
4845  '~~~~~' => $d,
4846  '~~~~' => "$sigText $d",
4847  '~~~' => $sigText
4848  ) );
4849 
4850  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4851  $tc = '[' . Title::legalChars() . ']';
4852  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4853 
4854  // [[ns:page (context)|]]
4855  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4856  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4857  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4858  // [[ns:page (context), context|]] (using either single or double-width comma)
4859  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4860  // [[|page]] (reverse pipe trick: add context from page title)
4861  $p2 = "/\[\[\\|($tc+)]]/";
4862 
4863  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4864  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4865  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4866  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4867 
4868  $t = $this->mTitle->getText();
4869  $m = array();
4870  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4871  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4872  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4873  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4874  } else {
4875  # if there's no context, don't bother duplicating the title
4876  $text = preg_replace( $p2, '[[\\1]]', $text );
4877  }
4878 
4879  # Trim trailing whitespace
4880  $text = rtrim( $text );
4881 
4882  return $text;
4883  }
4884 
4899  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4901 
4902  $username = $user->getName();
4903 
4904  # If not given, retrieve from the user object.
4905  if ( $nickname === false ) {
4906  $nickname = $user->getOption( 'nickname' );
4907  }
4908 
4909  if ( is_null( $fancySig ) ) {
4910  $fancySig = $user->getBoolOption( 'fancysig' );
4911  }
4912 
4913  $nickname = $nickname == null ? $username : $nickname;
4914 
4915  if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4916  $nickname = $username;
4917  wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4918  } elseif ( $fancySig !== false ) {
4919  # Sig. might contain markup; validate this
4920  if ( $this->validateSig( $nickname ) !== false ) {
4921  # Validated; clean up (if needed) and return it
4922  return $this->cleanSig( $nickname, true );
4923  } else {
4924  # Failed to validate; fall back to the default
4925  $nickname = $username;
4926  wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4927  }
4928  }
4929 
4930  # Make sure nickname doesnt get a sig in a sig
4931  $nickname = self::cleanSigInSig( $nickname );
4932 
4933  # If we're still here, make it a link to the user page
4934  $userText = wfEscapeWikiText( $username );
4935  $nickText = wfEscapeWikiText( $nickname );
4936  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4937 
4938  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4939  ->title( $this->getTitle() )->text();
4940  }
4941 
4948  public function validateSig( $text ) {
4949  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4950  }
4951 
4962  public function cleanSig( $text, $parsing = false ) {
4963  if ( !$parsing ) {
4964  global $wgTitle;
4965  $magicScopeVariable = $this->lock();
4966  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4967  }
4968 
4969  # Option to disable this feature
4970  if ( !$this->mOptions->getCleanSignatures() ) {
4971  return $text;
4972  }
4973 
4974  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4975  # => Move this logic to braceSubstitution()
4976  $substWord = MagicWord::get( 'subst' );
4977  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4978  $substText = '{{' . $substWord->getSynonym( 0 );
4979 
4980  $text = preg_replace( $substRegex, $substText, $text );
4981  $text = self::cleanSigInSig( $text );
4982  $dom = $this->preprocessToDom( $text );
4983  $frame = $this->getPreprocessor()->newFrame();
4984  $text = $frame->expand( $dom );
4985 
4986  if ( !$parsing ) {
4987  $text = $this->mStripState->unstripBoth( $text );
4988  }
4989 
4990  return $text;
4991  }
4992 
4999  public static function cleanSigInSig( $text ) {
5000  $text = preg_replace( '/~{3,5}/', '', $text );
5001  return $text;
5002  }
5003 
5013  public function startExternalParse( Title $title = null, ParserOptions $options,
5014  $outputType, $clearState = true
5015  ) {
5016  $this->startParse( $title, $options, $outputType, $clearState );
5017  }
5018 
5025  private function startParse( Title $title = null, ParserOptions $options,
5026  $outputType, $clearState = true
5027  ) {
5028  $this->setTitle( $title );
5029  $this->mOptions = $options;
5030  $this->setOutputType( $outputType );
5031  if ( $clearState ) {
5032  $this->clearState();
5033  }
5034  }
5035 
5044  public function transformMsg( $text, $options, $title = null ) {
5045  static $executing = false;
5046 
5047  # Guard against infinite recursion
5048  if ( $executing ) {
5049  return $text;
5050  }
5051  $executing = true;
5052 
5053  if ( !$title ) {
5054  global $wgTitle;
5055  $title = $wgTitle;
5056  }
5057 
5058  $text = $this->preprocess( $text, $title, $options );
5059 
5060  $executing = false;
5061  return $text;
5062  }
5063 
5088  public function setHook( $tag, $callback ) {
5089  $tag = strtolower( $tag );
5090  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5091  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5092  }
5093  $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
5094  $this->mTagHooks[$tag] = $callback;
5095  if ( !in_array( $tag, $this->mStripList ) ) {
5096  $this->mStripList[] = $tag;
5097  }
5098 
5099  return $oldVal;
5100  }
5101 
5119  public function setTransparentTagHook( $tag, $callback ) {
5120  $tag = strtolower( $tag );
5121  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5122  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5123  }
5124  $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
5125  $this->mTransparentTagHooks[$tag] = $callback;
5126 
5127  return $oldVal;
5128  }
5129 
5133  public function clearTagHooks() {
5134  $this->mTagHooks = array();
5135  $this->mFunctionTagHooks = array();
5136  $this->mStripList = $this->mDefaultStripList;
5137  }
5138 
5182  public function setFunctionHook( $id, $callback, $flags = 0 ) {
5184 
5185  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5186  $this->mFunctionHooks[$id] = array( $callback, $flags );
5187 
5188  # Add to function cache
5189  $mw = MagicWord::get( $id );
5190  if ( !$mw ) {
5191  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5192  }
5193 
5194  $synonyms = $mw->getSynonyms();
5195  $sensitive = intval( $mw->isCaseSensitive() );
5196 
5197  foreach ( $synonyms as $syn ) {
5198  # Case
5199  if ( !$sensitive ) {
5200  $syn = $wgContLang->lc( $syn );
5201  }
5202  # Add leading hash
5203  if ( !( $flags & self::SFH_NO_HASH ) ) {
5204  $syn = '#' . $syn;
5205  }
5206  # Remove trailing colon
5207  if ( substr( $syn, -1, 1 ) === ':' ) {
5208  $syn = substr( $syn, 0, -1 );
5209  }
5210  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5211  }
5212  return $oldVal;
5213  }
5214 
5220  public function getFunctionHooks() {
5221  return array_keys( $this->mFunctionHooks );
5222  }
5223 
5234  public function setFunctionTagHook( $tag, $callback, $flags ) {
5235  $tag = strtolower( $tag );
5236  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5237  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5238  }
5239  $old = isset( $this->mFunctionTagHooks[$tag] ) ?
5240  $this->mFunctionTagHooks[$tag] : null;
5241  $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
5242 
5243  if ( !in_array( $tag, $this->mStripList ) ) {
5244  $this->mStripList[] = $tag;
5245  }
5247  return $old;
5248  }
5249 
5258  public function replaceLinkHolders( &$text, $options = 0 ) {
5259  $this->mLinkHolders->replace( $text );
5260  }
5261 
5269  public function replaceLinkHoldersText( $text ) {
5270  return $this->mLinkHolders->replaceText( $text );
5271  }
5272 
5286  public function renderImageGallery( $text, $params ) {
5287 
5288  $mode = false;
5289  if ( isset( $params['mode'] ) ) {
5290  $mode = $params['mode'];
5291  }
5292 
5293  try {
5294  $ig = ImageGalleryBase::factory( $mode );
5295  } catch ( Exception $e ) {
5296  // If invalid type set, fallback to default.
5297  $ig = ImageGalleryBase::factory( false );
5298  }
5299 
5300  $ig->setContextTitle( $this->mTitle );
5301  $ig->setShowBytes( false );
5302  $ig->setShowFilename( false );
5303  $ig->setParser( $this );
5304  $ig->setHideBadImages();
5305  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
5306 
5307  if ( isset( $params['showfilename'] ) ) {
5308  $ig->setShowFilename( true );
5309  } else {
5310  $ig->setShowFilename( false );
5311  }
5312  if ( isset( $params['caption'] ) ) {
5313  $caption = $params['caption'];
5314  $caption = htmlspecialchars( $caption );
5315  $caption = $this->replaceInternalLinks( $caption );
5316  $ig->setCaptionHtml( $caption );
5317  }
5318  if ( isset( $params['perrow'] ) ) {
5319  $ig->setPerRow( $params['perrow'] );
5320  }
5321  if ( isset( $params['widths'] ) ) {
5322  $ig->setWidths( $params['widths'] );
5323  }
5324  if ( isset( $params['heights'] ) ) {
5325  $ig->setHeights( $params['heights'] );
5326  }
5327  $ig->setAdditionalOptions( $params );
5328 
5329  Hooks::run( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
5330 
5331  $lines = StringUtils::explode( "\n", $text );
5332  foreach ( $lines as $line ) {
5333  # match lines like these:
5334  # Image:someimage.jpg|This is some image
5335  $matches = array();
5336  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5337  # Skip empty lines
5338  if ( count( $matches ) == 0 ) {
5339  continue;
5340  }
5341 
5342  if ( strpos( $matches[0], '%' ) !== false ) {
5343  $matches[1] = rawurldecode( $matches[1] );
5344  }
5345  $title = Title::newFromText( $matches[1], NS_FILE );
5346  if ( is_null( $title ) ) {
5347  # Bogus title. Ignore these so we don't bomb out later.
5348  continue;
5349  }
5350 
5351  # We need to get what handler the file uses, to figure out parameters.
5352  # Note, a hook can overide the file name, and chose an entirely different
5353  # file (which potentially could be of a different type and have different handler).
5354  $options = array();
5355  $descQuery = false;
5356  Hooks::run( 'BeforeParserFetchFileAndTitle',
5357  array( $this, $title, &$options, &$descQuery ) );
5358  # Don't register it now, as ImageGallery does that later.
5359  $file = $this->fetchFileNoRegister( $title, $options );
5360  $handler = $file ? $file->getHandler() : false;
5361 
5362  $paramMap = array(
5363  'img_alt' => 'gallery-internal-alt',
5364  'img_link' => 'gallery-internal-link',
5365  );
5366  if ( $handler ) {
5367  $paramMap = $paramMap + $handler->getParamMap();
5368  // We don't want people to specify per-image widths.
5369  // Additionally the width parameter would need special casing anyhow.
5370  unset( $paramMap['img_width'] );
5371  }
5372 
5373  $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5374 
5375  $label = '';
5376  $alt = '';
5377  $link = '';
5378  $handlerOptions = array();
5379  if ( isset( $matches[3] ) ) {
5380  // look for an |alt= definition while trying not to break existing
5381  // captions with multiple pipes (|) in it, until a more sensible grammar
5382  // is defined for images in galleries
5383 
5384  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5385  // splitting on '|' is a bit odd, and different from makeImage.
5386  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5387  $parameterMatches = StringUtils::explode( '|', $matches[3] );
5388 
5389  foreach ( $parameterMatches as $parameterMatch ) {
5390  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5391  if ( $magicName ) {
5392  $paramName = $paramMap[$magicName];
5393 
5394  switch ( $paramName ) {
5395  case 'gallery-internal-alt':
5396  $alt = $this->stripAltText( $match, false );
5397  break;
5398  case 'gallery-internal-link':
5399  $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5400  $chars = self::EXT_LINK_URL_CLASS;
5401  $prots = $this->mUrlProtocols;
5402  //check to see if link matches an absolute url, if not then it must be a wiki link.
5403  if ( preg_match( "/^($prots)$chars+$/u", $linkValue ) ) {
5404  $link = $linkValue;
5405  } else {
5406  $localLinkTitle = Title::newFromText( $linkValue );
5407  if ( $localLinkTitle !== null ) {
5408  $link = $localLinkTitle->getLinkURL();
5409  }
5410  }
5411  break;
5412  default:
5413  // Must be a handler specific parameter.
5414  if ( $handler->validateParam( $paramName, $match ) ) {
5415  $handlerOptions[$paramName] = $match;
5416  } else {
5417  // Guess not. Append it to the caption.
5418  wfDebug( "$parameterMatch failed parameter validation\n" );
5419  $label .= '|' . $parameterMatch;
5420  }
5421  }
5422 
5423  } else {
5424  // concatenate all other pipes
5425  $label .= '|' . $parameterMatch;
5426  }
5427  }
5428  // remove the first pipe
5429  $label = substr( $label, 1 );
5430  }
5431 
5432  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5433  }
5434  $html = $ig->toHTML();
5435  Hooks::run( 'AfterParserFetchFileAndTitle', array( $this, $ig, &$html ) );
5436  return $html;
5437  }
5438 
5443  public function getImageParams( $handler ) {
5444  if ( $handler ) {
5445  $handlerClass = get_class( $handler );
5446  } else {
5447  $handlerClass = '';
5448  }
5449  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5450  # Initialise static lists
5451  static $internalParamNames = array(
5452  'horizAlign' => array( 'left', 'right', 'center', 'none' ),
5453  'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5454  'bottom', 'text-bottom' ),
5455  'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
5456  'upright', 'border', 'link', 'alt', 'class' ),
5457  );
5458  static $internalParamMap;
5459  if ( !$internalParamMap ) {
5460  $internalParamMap = array();
5461  foreach ( $internalParamNames as $type => $names ) {
5462  foreach ( $names as $name ) {
5463  $magicName = str_replace( '-', '_', "img_$name" );
5464  $internalParamMap[$magicName] = array( $type, $name );
5465  }
5466  }
5467  }
5469  # Add handler params
5470  $paramMap = $internalParamMap;
5471  if ( $handler ) {
5472  $handlerParamMap = $handler->getParamMap();
5473  foreach ( $handlerParamMap as $magic => $paramName ) {
5474  $paramMap[$magic] = array( 'handler', $paramName );
5475  }
5476  }
5477  $this->mImageParams[$handlerClass] = $paramMap;
5478  $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5479  }
5480  return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
5481  }
5482 
5491  public function makeImage( $title, $options, $holders = false ) {
5492  # Check if the options text is of the form "options|alt text"
5493  # Options are:
5494  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5495  # * left no resizing, just left align. label is used for alt= only
5496  # * right same, but right aligned
5497  # * none same, but not aligned
5498  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5499  # * center center the image
5500  # * frame Keep original image size, no magnify-button.
5501  # * framed Same as "frame"
5502  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5503  # * upright reduce width for upright images, rounded to full __0 px
5504  # * border draw a 1px border around the image
5505  # * alt Text for HTML alt attribute (defaults to empty)
5506  # * class Set a class for img node
5507  # * link Set the target of the image link. Can be external, interwiki, or local
5508  # vertical-align values (no % or length right now):
5509  # * baseline
5510  # * sub
5511  # * super
5512  # * top
5513  # * text-top
5514  # * middle
5515  # * bottom
5516  # * text-bottom
5517 
5518  $parts = StringUtils::explode( "|", $options );
5519 
5520  # Give extensions a chance to select the file revision for us
5521  $options = array();
5522  $descQuery = false;
5523  Hooks::run( 'BeforeParserFetchFileAndTitle',
5524  array( $this, $title, &$options, &$descQuery ) );
5525  # Fetch and register the file (file title may be different via hooks)
5526  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5527 
5528  # Get parameter map
5529  $handler = $file ? $file->getHandler() : false;
5530 
5531  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5532 
5533  if ( !$file ) {
5534  $this->addTrackingCategory( 'broken-file-category' );
5535  }
5536 
5537  # Process the input parameters
5538  $caption = '';
5539  $params = array( 'frame' => array(), 'handler' => array(),
5540  'horizAlign' => array(), 'vertAlign' => array() );
5541  $seenformat = false;
5542  foreach ( $parts as $part ) {
5543  $part = trim( $part );
5544  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5545  $validated = false;
5546  if ( isset( $paramMap[$magicName] ) ) {
5547  list( $type, $paramName ) = $paramMap[$magicName];
5548 
5549  # Special case; width and height come in one variable together
5550  if ( $type === 'handler' && $paramName === 'width' ) {
5551  $parsedWidthParam = $this->parseWidthParam( $value );
5552  if ( isset( $parsedWidthParam['width'] ) ) {
5553  $width = $parsedWidthParam['width'];
5554  if ( $handler->validateParam( 'width', $width ) ) {
5555  $params[$type]['width'] = $width;
5556  $validated = true;
5557  }
5558  }
5559  if ( isset( $parsedWidthParam['height'] ) ) {
5560  $height = $parsedWidthParam['height'];
5561  if ( $handler->validateParam( 'height', $height ) ) {
5562  $params[$type]['height'] = $height;
5563  $validated = true;
5564  }
5565  }
5566  # else no validation -- bug 13436
5567  } else {
5568  if ( $type === 'handler' ) {
5569  # Validate handler parameter
5570  $validated = $handler->validateParam( $paramName, $value );
5571  } else {
5572  # Validate internal parameters
5573  switch ( $paramName ) {
5574  case 'manualthumb':
5575  case 'alt':
5576  case 'class':
5577  # @todo FIXME: Possibly check validity here for
5578  # manualthumb? downstream behavior seems odd with
5579  # missing manual thumbs.
5580  $validated = true;
5581  $value = $this->stripAltText( $value, $holders );
5582  break;
5583  case 'link':
5584  $chars = self::EXT_LINK_URL_CLASS;
5585  $prots = $this->mUrlProtocols;
5586  if ( $value === '' ) {
5587  $paramName = 'no-link';
5588  $value = true;
5589  $validated = true;
5590  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5591  if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) {
5592  $paramName = 'link-url';
5593  $this->mOutput->addExternalLink( $value );
5594  if ( $this->mOptions->getExternalLinkTarget() ) {
5595  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5596  }
5597  $validated = true;
5598  }
5599  } else {
5600  $linkTitle = Title::newFromText( $value );
5601  if ( $linkTitle ) {
5602  $paramName = 'link-title';
5603  $value = $linkTitle;
5604  $this->mOutput->addLink( $linkTitle );
5605  $validated = true;
5606  }
5607  }
5608  break;
5609  case 'frameless':
5610  case 'framed':
5611  case 'thumbnail':
5612  // use first appearing option, discard others.
5613  $validated = ! $seenformat;
5614  $seenformat = true;
5615  break;
5616  default:
5617  # Most other things appear to be empty or numeric...
5618  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5619  }
5620  }
5621 
5622  if ( $validated ) {
5623  $params[$type][$paramName] = $value;
5624  }
5625  }
5626  }
5627  if ( !$validated ) {
5628  $caption = $part;
5629  }
5630  }
5631 
5632  # Process alignment parameters
5633  if ( $params['horizAlign'] ) {
5634  $params['frame']['align'] = key( $params['horizAlign'] );
5635  }
5636  if ( $params['vertAlign'] ) {
5637  $params['frame']['valign'] = key( $params['vertAlign'] );
5638  }
5639 
5640  $params['frame']['caption'] = $caption;
5641 
5642  # Will the image be presented in a frame, with the caption below?
5643  $imageIsFramed = isset( $params['frame']['frame'] )
5644  || isset( $params['frame']['framed'] )
5645  || isset( $params['frame']['thumbnail'] )
5646  || isset( $params['frame']['manualthumb'] );
5647 
5648  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5649  # came to also set the caption, ordinary text after the image -- which
5650  # makes no sense, because that just repeats the text multiple times in
5651  # screen readers. It *also* came to set the title attribute.
5652  #
5653  # Now that we have an alt attribute, we should not set the alt text to
5654  # equal the caption: that's worse than useless, it just repeats the
5655  # text. This is the framed/thumbnail case. If there's no caption, we
5656  # use the unnamed parameter for alt text as well, just for the time be-
5657  # ing, if the unnamed param is set and the alt param is not.
5658  #
5659  # For the future, we need to figure out if we want to tweak this more,
5660  # e.g., introducing a title= parameter for the title; ignoring the un-
5661  # named parameter entirely for images without a caption; adding an ex-
5662  # plicit caption= parameter and preserving the old magic unnamed para-
5663  # meter for BC; ...
5664  if ( $imageIsFramed ) { # Framed image
5665  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5666  # No caption or alt text, add the filename as the alt text so
5667  # that screen readers at least get some description of the image
5668  $params['frame']['alt'] = $title->getText();
5669  }
5670  # Do not set $params['frame']['title'] because tooltips don't make sense
5671  # for framed images
5672  } else { # Inline image
5673  if ( !isset( $params['frame']['alt'] ) ) {
5674  # No alt text, use the "caption" for the alt text
5675  if ( $caption !== '' ) {
5676  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5677  } else {
5678  # No caption, fall back to using the filename for the
5679  # alt text
5680  $params['frame']['alt'] = $title->getText();
5681  }
5682  }
5683  # Use the "caption" for the tooltip text
5684  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5685  }
5686 
5687  Hooks::run( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) );
5688 
5689  # Linker does the rest
5690  $time = isset( $options['time'] ) ? $options['time'] : false;
5691  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5692  $time, $descQuery, $this->mOptions->getThumbSize() );
5693 
5694  # Give the handler a chance to modify the parser object
5695  if ( $handler ) {
5696  $handler->parserTransformHook( $this, $file );
5697  }
5698 
5699  return $ret;
5700  }
5701 
5707  protected function stripAltText( $caption, $holders ) {
5708  # Strip bad stuff out of the title (tooltip). We can't just use
5709  # replaceLinkHoldersText() here, because if this function is called
5710  # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5711  if ( $holders ) {
5712  $tooltip = $holders->replaceText( $caption );
5713  } else {
5714  $tooltip = $this->replaceLinkHoldersText( $caption );
5715  }
5716 
5717  # make sure there are no placeholders in thumbnail attributes
5718  # that are later expanded to html- so expand them now and
5719  # remove the tags
5720  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5721  $tooltip = Sanitizer::stripAllTags( $tooltip );
5722 
5723  return $tooltip;
5724  }
5730  public function disableCache() {
5731  wfDebug( "Parser output marked as uncacheable.\n" );
5732  if ( !$this->mOutput ) {
5733  throw new MWException( __METHOD__ .
5734  " can only be called when actually parsing something" );
5735  }
5736  $this->mOutput->setCacheTime( -1 ); // old style, for compatibility
5737  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5738  }
5739 
5748  public function attributeStripCallback( &$text, $frame = false ) {
5749  $text = $this->replaceVariables( $text, $frame );
5750  $text = $this->mStripState->unstripBoth( $text );
5751  return $text;
5752  }
5753 
5759  public function getTags() {
5760  return array_merge(
5761  array_keys( $this->mTransparentTagHooks ),
5762  array_keys( $this->mTagHooks ),
5763  array_keys( $this->mFunctionTagHooks )
5764  );
5765  }
5766 
5777  public function replaceTransparentTags( $text ) {
5778  $matches = array();
5779  $elements = array_keys( $this->mTransparentTagHooks );
5780  $text = self::extractTagsAndParams( $elements, $text, $matches );
5781  $replacements = array();
5782 
5783  foreach ( $matches as $marker => $data ) {
5784  list( $element, $content, $params, $tag ) = $data;
5785  $tagName = strtolower( $element );
5786  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5787  $output = call_user_func_array(
5788  $this->mTransparentTagHooks[$tagName],
5789  array( $content, $params, $this )
5790  );
5791  } else {
5792  $output = $tag;
5793  }
5794  $replacements[$marker] = $output;
5795  }
5796  return strtr( $text, $replacements );
5797  }
5798 
5828  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5829  global $wgTitle; # not generally used but removes an ugly failure mode
5830 
5831  $magicScopeVariable = $this->lock();
5832  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5833  $outText = '';
5834  $frame = $this->getPreprocessor()->newFrame();
5835 
5836  # Process section extraction flags
5837  $flags = 0;
5838  $sectionParts = explode( '-', $sectionId );
5839  $sectionIndex = array_pop( $sectionParts );
5840  foreach ( $sectionParts as $part ) {
5841  if ( $part === 'T' ) {
5842  $flags |= self::PTD_FOR_INCLUSION;
5843  }
5844  }
5845 
5846  # Check for empty input
5847  if ( strval( $text ) === '' ) {
5848  # Only sections 0 and T-0 exist in an empty document
5849  if ( $sectionIndex == 0 ) {
5850  if ( $mode === 'get' ) {
5851  return '';
5852  } else {
5853  return $newText;
5854  }
5855  } else {
5856  if ( $mode === 'get' ) {
5857  return $newText;
5858  } else {
5859  return $text;
5860  }
5861  }
5862  }
5863 
5864  # Preprocess the text
5865  $root = $this->preprocessToDom( $text, $flags );
5866 
5867  # <h> nodes indicate section breaks
5868  # They can only occur at the top level, so we can find them by iterating the root's children
5869  $node = $root->getFirstChild();
5870 
5871  # Find the target section
5872  if ( $sectionIndex == 0 ) {
5873  # Section zero doesn't nest, level=big
5874  $targetLevel = 1000;
5875  } else {
5876  while ( $node ) {
5877  if ( $node->getName() === 'h' ) {
5878  $bits = $node->splitHeading();
5879  if ( $bits['i'] == $sectionIndex ) {
5880  $targetLevel = $bits['level'];
5881  break;
5882  }
5883  }
5884  if ( $mode === 'replace' ) {
5885  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5886  }
5887  $node = $node->getNextSibling();
5888  }
5889  }
5890 
5891  if ( !$node ) {
5892  # Not found
5893  if ( $mode === 'get' ) {
5894  return $newText;
5895  } else {
5896  return $text;
5897  }
5898  }
5899 
5900  # Find the end of the section, including nested sections
5901  do {
5902  if ( $node->getName() === 'h' ) {
5903  $bits = $node->splitHeading();
5904  $curLevel = $bits['level'];
5905  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5906  break;
5907  }
5908  }
5909  if ( $mode === 'get' ) {
5910  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5911  }
5912  $node = $node->getNextSibling();
5913  } while ( $node );
5914 
5915  # Write out the remainder (in replace mode only)
5916  if ( $mode === 'replace' ) {
5917  # Output the replacement text
5918  # Add two newlines on -- trailing whitespace in $newText is conventionally
5919  # stripped by the editor, so we need both newlines to restore the paragraph gap
5920  # Only add trailing whitespace if there is newText
5921  if ( $newText != "" ) {
5922  $outText .= $newText . "\n\n";
5923  }
5924 
5925  while ( $node ) {
5926  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5927  $node = $node->getNextSibling();
5928  }
5929  }
5931  if ( is_string( $outText ) ) {
5932  # Re-insert stripped tags
5933  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5934  }
5935 
5936  return $outText;
5937  }
5938 
5953  public function getSection( $text, $sectionId, $defaultText = '' ) {
5954  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5955  }
5956 
5969  public function replaceSection( $oldText, $sectionId, $newText ) {
5970  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5971  }
5972 
5978  public function getRevisionId() {
5979  return $this->mRevisionId;
5980  }
5981 
5988  public function getRevisionObject() {
5989  if ( !is_null( $this->mRevisionObject ) ) {
5990  return $this->mRevisionObject;
5991  }
5992  if ( is_null( $this->mRevisionId ) ) {
5993  return null;
5994  }
5995 
5996  $rev = call_user_func(
5997  $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5998  );
5999 
6000  # If the parse is for a new revision, then the callback should have
6001  # already been set to force the object and should match mRevisionId.
6002  # If not, try to fetch by mRevisionId for sanity.
6003  if ( $rev && $rev->getId() != $this->mRevisionId ) {
6004  $rev = Revision::newFromId( $this->mRevisionId );
6005  }
6006 
6007  $this->mRevisionObject = $rev;
6008 
6009  return $this->mRevisionObject;
6010  }
6011 
6017  public function getRevisionTimestamp() {
6018  if ( is_null( $this->mRevisionTimestamp ) ) {
6020 
6021  $revObject = $this->getRevisionObject();
6022  $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
6023 
6024  # The cryptic '' timezone parameter tells to use the site-default
6025  # timezone offset instead of the user settings.
6026  #
6027  # Since this value will be saved into the parser cache, served
6028  # to other users, and potentially even used inside links and such,
6029  # it needs to be consistent for all visitors.
6030  $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
6031 
6032  }
6033  return $this->mRevisionTimestamp;
6034  }
6035 
6041  public function getRevisionUser() {
6042  if ( is_null( $this->mRevisionUser ) ) {
6043  $revObject = $this->getRevisionObject();
6044 
6045  # if this template is subst: the revision id will be blank,
6046  # so just use the current user's name
6047  if ( $revObject ) {
6048  $this->mRevisionUser = $revObject->getUserText();
6049  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6050  $this->mRevisionUser = $this->getUser()->getName();
6051  }
6052  }
6053  return $this->mRevisionUser;
6054  }
6055 
6061  public function getRevisionSize() {
6062  if ( is_null( $this->mRevisionSize ) ) {
6063  $revObject = $this->getRevisionObject();
6064 
6065  # if this variable is subst: the revision id will be blank,
6066  # so just use the parser input size, because the own substituation
6067  # will change the size.
6068  if ( $revObject ) {
6069  $this->mRevisionSize = $revObject->getSize();
6070  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6071  $this->mRevisionSize = $this->mInputSize;
6072  }
6073  }
6074  return $this->mRevisionSize;
6075  }
6076 
6082  public function setDefaultSort( $sort ) {
6083  $this->mDefaultSort = $sort;
6084  $this->mOutput->setProperty( 'defaultsort', $sort );
6085  }
6086 
6097  public function getDefaultSort() {
6098  if ( $this->mDefaultSort !== false ) {
6099  return $this->mDefaultSort;
6100  } else {
6101  return '';
6102  }
6103  }
6104 
6111  public function getCustomDefaultSort() {
6112  return $this->mDefaultSort;
6113  }
6114 
6124  public function guessSectionNameFromWikiText( $text ) {
6125  # Strip out wikitext links(they break the anchor)
6126  $text = $this->stripSectionName( $text );
6128  return '#' . Sanitizer::escapeId( $text, 'noninitial' );
6129  }
6130 
6139  public function guessLegacySectionNameFromWikiText( $text ) {
6140  # Strip out wikitext links(they break the anchor)
6141  $text = $this->stripSectionName( $text );
6143  return '#' . Sanitizer::escapeId( $text, array( 'noninitial', 'legacy' ) );
6144  }
6145 
6160  public function stripSectionName( $text ) {
6161  # Strip internal link markup
6162  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6163  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6164 
6165  # Strip external link markup
6166  # @todo FIXME: Not tolerant to blank link text
6167  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6168  # on how many empty links there are on the page - need to figure that out.
6169  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6170 
6171  # Parse wikitext quotes (italics & bold)
6172  $text = $this->doQuotes( $text );
6173 
6174  # Strip HTML tags
6175  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6176  return $text;
6177  }
6178 
6189  public function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) {
6190  $magicScopeVariable = $this->lock();
6191  $this->startParse( $title, $options, $outputType, true );
6193  $text = $this->replaceVariables( $text );
6194  $text = $this->mStripState->unstripBoth( $text );
6195  $text = Sanitizer::removeHTMLtags( $text );
6196  return $text;
6197  }
6198 
6205  public function testPst( $text, Title $title, ParserOptions $options ) {
6206  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6207  }
6208 
6215  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6216  return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6217  }
6218 
6235  public function markerSkipCallback( $s, $callback ) {
6236  $i = 0;
6237  $out = '';
6238  while ( $i < strlen( $s ) ) {
6239  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6240  if ( $markerStart === false ) {
6241  $out .= call_user_func( $callback, substr( $s, $i ) );
6242  break;
6243  } else {
6244  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6245  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6246  if ( $markerEnd === false ) {
6247  $out .= substr( $s, $markerStart );
6248  break;
6249  } else {
6250  $markerEnd += strlen( self::MARKER_SUFFIX );
6251  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6252  $i = $markerEnd;
6253  }
6254  }
6255  }
6256  return $out;
6257  }
6258 
6265  public function killMarkers( $text ) {
6266  return $this->mStripState->killMarkers( $text );
6267  }
6268 
6285  public function serializeHalfParsedText( $text ) {
6286  $data = array(
6287  'text' => $text,
6288  'version' => self::HALF_PARSED_VERSION,
6289  'stripState' => $this->mStripState->getSubState( $text ),
6290  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6291  );
6292  return $data;
6293  }
6294 
6310  public function unserializeHalfParsedText( $data ) {
6311  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6312  throw new MWException( __METHOD__ . ': invalid version' );
6313  }
6314 
6315  # First, extract the strip state.
6316  $texts = array( $data['text'] );
6317  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6318 
6319  # Now renumber links
6320  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6321 
6322  # Should be good to go.
6323  return $texts[0];
6324  }
6325 
6335  public function isValidHalfParsedText( $data ) {
6336  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6337  }
6338 
6347  public function parseWidthParam( $value ) {
6348  $parsedWidthParam = array();
6349  if ( $value === '' ) {
6350  return $parsedWidthParam;
6351  }
6352  $m = array();
6353  # (bug 13500) In both cases (width/height and width only),
6354  # permit trailing "px" for backward compatibility.
6355  if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6356  $width = intval( $m[1] );
6357  $height = intval( $m[2] );
6358  $parsedWidthParam['width'] = $width;
6359  $parsedWidthParam['height'] = $height;
6360  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6361  $width = intval( $value );
6362  $parsedWidthParam['width'] = $width;
6363  }
6364  return $parsedWidthParam;
6365  }
6366 
6376  protected function lock() {
6377  if ( $this->mInParse ) {
6378  throw new MWException( "Parser state cleared while parsing. "
6379  . "Did you call Parser::parse recursively?" );
6380  }
6381  $this->mInParse = true;
6382 
6383  $that = $this;
6384  $recursiveCheck = new ScopedCallback( function() use ( $that ) {
6385  $that->mInParse = false;
6386  } );
6387 
6388  return $recursiveCheck;
6389  }
6390 
6401  public static function stripOuterParagraph( $html ) {
6402  $m = array();
6403  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6404  if ( strpos( $m[1], '</p>' ) === false ) {
6405  $html = $m[1];
6406  }
6407  }
6408 
6409  return $html;
6410  }
6411 
6422  public function getFreshParser() {
6424  if ( $this->mInParse ) {
6425  return new $wgParserConf['class']( $wgParserConf );
6426  } else {
6427  return $this;
6428  }
6429  }
6430 }
setTitle($t)
Set the context title.
Definition: Parser.php:699
$mAutonumber
Definition: Parser.php:166
$mPPNodeCount
Definition: Parser.php:176
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition: Parser.php:1987
static getVariableIDs()
Get an array of parser variable IDs.
Definition: MagicWord.php:262
const MARKER_PREFIX
Definition: Parser.php:130
external whereas SearchGetNearMatch runs after $term
Definition: hooks.txt:2477
null means default in associative array form
Definition: hooks.txt:1732
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:1732
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1686
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
static decodeTagAttributes($text)
Return an associative array of attribute names and values from a partial tag string.
Definition: Sanitizer.php:1210
$mTplRedirCache
Definition: Parser.php:178
static tocList($toc, $lang=false)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition: Linker.php:1697
static makeExternalLink($url, $text, $escape=true, $linktype= '', $attribs=array(), $title=null)
Make an external link.
Definition: Linker.php:1056
getBoolOption($oname)
Get the user's current setting for a given option, as a boolean value.
Definition: User.php:2654
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList use this to change the tables headers temp or archived zone change it to an object instance and return false override the list derivative used the name of the old file when set the default code will be skipped true if there is text before this autocomment true if there is text after this autocomment add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1269
const OT_PREPROCESS
Definition: Defines.php:227
$mLastSection
Definition: Parser.php:171
$mDoubleUnderscores
Definition: Parser.php:178
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:2258
Group all the pieces relevant to the context of a request into one instance.
or
false for read/write
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:218
getArticleID($flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3140
$wgSitename
Name of the site.
recursivePreprocess($text, $frame=false)
Recursive parser entry point that can be called from an extension tag hook.
Definition: Parser.php:628
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:896
replaceExternalLinks($text)
Replace external links (REL)
Definition: Parser.php:1725
static isNonincludable($index)
It is not possible to use pages from this namespace as template?
nextLinkID()
Definition: Parser.php:789
const SPACE_NOT_NL
Definition: Parser.php:94
static replaceUnusualEscapes($url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition: Parser.php:1842
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:324
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:1502
const OT_PLAIN
Definition: Parser.php:115
static removeHTMLtags($text, $processCallback=null, $args=array(), $extratags=array(), $removetags=array())
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments. ...
Definition: Sanitizer.php:457
static isWellFormedXmlFragment($text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:735
const OT_WIKI
Definition: Parser.php:112
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1862
User $mUser
Definition: Parser.php:183
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers ...
Definition: Parser.php:3211
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:1732
=Architecture==Two class hierarchies are used to provide the functionality associated with the different content models:*Content interface(and AbstractContent base class) define functionality that acts on the concrete content of a page, and *ContentHandler base class provides functionality specific to a content model, but not acting on concrete content.The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content.These Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text.All manipulation and analysis of page content must be done via the appropriate methods of the Content object.For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers.The ContentHandler object for a given content model can be obtained using ContentHandler::getForModelID($id).Also Title, WikiPage and Revision now have getContentHandler() methods for convenience.ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting on the content of some page.ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to create a Content object of the appropriate type.However, it is recommended to instead use WikiPage::getContent() resp.Revision::getContent() to get a page's content as a Content object.These two methods should be the ONLY way in which page content is accessed.Another important function of ContentHandler objects is to define custom action handlers for a content model, see ContentHandler::getActionOverrides().This is similar to what WikiPage::getActionOverrides() was already doing.==Serialization==With the ContentHandler facility, page content no longer has to be text based.Objects implementing the Content interface are used to represent and handle the content internally.For storage and data exchange, each content model supports at least one serialization format via ContentHandler::serializeContent($content).The list of supported formats for a given content model can be accessed using ContentHandler::getSupportedFormats().Content serialization formats are identified using MIME type like strings.The following formats are built in:*text/x-wiki-wikitext *text/javascript-for js pages *text/css-for css pages *text/plain-for future use, e.g.with plain text messages.*text/html-for future use, e.g.with plain html messages.*application/vnd.php.serialized-for future use with the api and for extensions *application/json-for future use with the api, and for use by extensions *application/xml-for future use with the api, and for use by extensions In PHP, use the corresponding CONTENT_FORMAT_XXX constant.Note that when using the API to access page content, especially action=edit, action=parse and action=query &prop=revisions, the model and format of the content should always be handled explicitly.Without that information, interpretation of the provided content is not reliable.The same applies to XML dumps generated via maintenance/dumpBackup.php or Special:Export.Also note that the API will provide encapsulated, serialized content-so if the API was called with format=json, and contentformat is also json(or rather, application/json), the page content is represented as a string containing an escaped json structure.Extensions that use JSON to serialize some types of page content may provide specialized API modules that allow access to that content in a more natural form.==Compatibility==The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least for pages that contain wikitext or other text based content.However, a number of functions and hooks have been deprecated in favor of new versions that are aware of the page's content model, and will now generate warnings when used.Most importantly, the following functions have been deprecated:*Revisions::getText() and Revisions::getRawText() is deprecated in favor Revisions::getContent()*WikiPage::getText() is deprecated in favor WikiPage::getContent() Also, the old Article::getContent()(which returns text) is superceded by Article::getContentObject().However, both methods should be avoided since they do not provide clean access to the page's actual content.For instance, they may return a system message for non-existing pages.Use WikiPage::getContent() instead.Code that relies on a textual representation of the page content should eventually be rewritten.However, ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page.Its behavior is controlled by $wgContentHandlerTextFallback it
Set options of the Parser.
static tidy($text)
Interface with html tidy, used if $wgUseTidy = true.
Definition: MWTidy.php:122
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:968
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:804
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:188
uniqPrefix()
Accessor for mUniqPrefix.
Definition: Parser.php:689
const TOC_START
Definition: Parser.php:133
Title($x=null)
Accessor/mutator for the Title object.
Definition: Parser.php:728
SectionProfiler $mProfiler
Definition: Parser.php:225
$wgEnableScaryTranscluding
Enable interwiki transcluding.
$sort
wfDebug($text, $dest= 'all', array $context=array())
Sends a line to the debug log if enabled or, optionally, to a comment in output.
There are three types of nodes:
has been added to your &Future changes to this page and its associated Talk page will be listed there
$mHeadings
Definition: Parser.php:178
$value
const COLON_STATE_TAGSLASH
Definition: Parser.php:101
static makeSelfLinkObj($nt, $html= '', $query= '', $trail= '', $prefix= '')
Make appropriate markup for a link to the current article.
Definition: Linker.php:401
const NS_SPECIAL
Definition: Defines.php:58
clearState()
Clear Parser state.
Definition: Parser.php:308
$mFirstCall
Definition: Parser.php:148
getPreloadText($text, Title $title, ParserOptions $options, $params=array())
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:647
Options($x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:782
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2467
replaceLinkHolders(&$text, $options=0)
Definition: Parser.php:5235
static activeUsers()
Definition: SiteStats.php:164
$mLinkID
Definition: Parser.php:175
doQuotes($text)
Helper function for doAllQuotes()
Definition: Parser.php:1535
preprocessToDom($text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:3241
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1433
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:3360
static cleanUrl($url)
Definition: Sanitizer.php:1776
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:235
$mGeneratedPPNodeCount
Definition: Parser.php:176
Represents a title within MediaWiki.
Definition: Title.php:33
static getRandomString()
Get a random string.
Definition: Parser.php:668
$mRevisionId
Definition: Parser.php:196
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:1743
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:2487
$wgArticlePath
Definition: img_auth.php:45
OutputType($x=null)
Accessor/mutator for the output type.
Definition: Parser.php:754
const NS_TEMPLATE
Definition: Defines.php:79
const COLON_STATE_COMMENTDASHDASH
Definition: Parser.php:104
recursiveTagParse($text, $frame=false)
Half-parse wikitext to half-parsed HTML.
Definition: Parser.php:563
const NO_ARGS
Allows to change the fields on the form that will be generated just before adding its HTML to parser output $parser
Definition: hooks.txt:324
MagicWordArray $mVariables
Definition: Parser.php:153
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:718
const SFH_NO_HASH
Definition: Parser.php:82
const COLON_STATE_COMMENTDASH
Definition: Parser.php:103
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:180
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:604
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2029
static getCacheTTL($id)
Allow external reads of TTL array.
Definition: MagicWord.php:285
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
Definition: globals.txt:10
const OT_PREPROCESS
Definition: Parser.php:113
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1557
maybeDoSubpageLink($target, &$text)
Handle link to subpage if necessary.
Definition: Parser.php:2354
$mFunctionSynonyms
Definition: Parser.php:140
If you want to remove the page from your watchlist later
setLinkID($id)
Definition: Parser.php:796
$mOutputType
Definition: Parser.php:193
Apache License January http
$mDefaultStripList
Definition: Parser.php:143
$mExtLinkBracketedRegex
Definition: Parser.php:158
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:2500
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:98
static getDoubleUnderscoreArray()
Get a MagicWordArray of double-underscore entities.
Definition: MagicWord.php:298
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:1771
The User object encapsulates all of the user-specific settings (user_id, name, rights, password, email address, options, last login time).
Definition: User.php:39
static decodeCharReferences($text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string...
Definition: Sanitizer.php:1421
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 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
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
pull multiple revisions may often pull multiple times from the same blob.
Definition: deferred.txt:11
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:132
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...
wfCgiToArray($query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
static capturePath(Title $title, IContextSource $context)
Just like executePath() but will override global variables and execute the page in "inclusion" mode...
const NO_TEMPLATES
replaceInternalLinks($s)
Process [[ ]] wikilinks.
Definition: Parser.php:1974
$mVarCache
Definition: Parser.php:144
$wgStylePath
The URL path of the skins directory.
$mRevisionObject
Definition: Parser.php:195
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:1306
internalParse($text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1185
Title $mTitle
Definition: Parser.php:192
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: Parser.php:255
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:223
Some quick notes on the file repository architecture Functionality is
Definition: README:3
isExternal()
Is this Title interwiki?
Definition: Title.php:815
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:882
static register($parser)
$mRevIdForTs
Definition: Parser.php:200
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:1503
$mStripList
Definition: Parser.php:142
$mFunctionTagHooks
Definition: Parser.php:141
const OT_PLAIN
Definition: Defines.php:229
$wgNoFollowLinks
If true, external URL links in wiki text will be given the rel="nofollow" attribute as a hint to sear...
$mRevisionTimestamp
Definition: Parser.php:197
$mImageParams
Definition: Parser.php:145
getDBkey()
Get the main part with underscores.
Definition: Title.php:914
stripAltText($caption, $holders)
Definition: Parser.php:5684
doAllQuotes($text)
Replace single quotes with HTML markup.
Definition: Parser.php:1518
static normalizeUrlComponent($component, $unsafe)
Definition: Parser.php:1892
isAnon()
Get whether the user is anonymous.
Definition: User.php:3187
if($limit) $timestamp
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way...
Definition: Parser.php:73
wfGetDB($db, $groups=array(), $wiki=false)
Get a Database object.
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:968
$mInPre
Definition: Parser.php:171
const OT_WIKI
Definition: Defines.php:226
Preprocessor $mPreprocessor
Definition: Parser.php:161
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:857
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
$res
Definition: database.txt:21
static linkKnown($target, $html=null, $customAttribs=array(), $query=array(), $options=array( 'known', 'noclasses'))
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:262
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:53
replaceVariables($text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:3286
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:169
$mDefaultSort
Definition: Parser.php:177
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise...
Definition: Parser.php:845
static run($event, array $args=array(), $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:137
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
getStripList()
Get a list of strippable XML-like elements.
Definition: Parser.php:956
const EXT_IMAGE_REGEX
Definition: Parser.php:90
startParse(Title $title=null, ParserOptions $options, $outputType, $clearState=true)
Definition: Parser.php:5002
$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:1752
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:886
shown</td >< td > a href
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:983
wfDeprecated($function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$mImageParamsMagicArray
Definition: Parser.php:146
LinkHolderArray $mLinkHolders
Definition: Parser.php:173
$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
const DB_SLAVE
Definition: Defines.php:46
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:817
while(($__line=Maintenance::readconsole())!==false) print n
Definition: eval.php:64
Allows to change the fields on the form that will be generated just before adding its HTML to parser output an object of one of the gallery classes(inheriting from ImageGalleryBase) $html conditions will AND in the final query as a Content object as a Content object $title
Definition: hooks.txt:327
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:4354
getConverterLanguage()
Get the language object for language conversion.
Definition: Parser.php:835
static tocUnindent($level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1655
if(!$wgRequest->checkUrlExtension()) if(!$wgEnableAPI) $wgTitle
Definition: api.php:62
static tocLine($anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1669
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:937
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:1825
__construct($conf=array())
Definition: Parser.php:230
$mInputSize
Definition: Parser.php:201
equals(Title $title)
Compare with another title.
Definition: Title.php:4192
magicword txt Magic Words are some phrases used in the wikitext They are used for two things
Definition: magicword.txt:4
const HALF_PARSED_VERSION
Update this version number when the output of serialiseHalfParsedText() changes in an incompatible wa...
Definition: Parser.php:79
const NS_FILE
Definition: Defines.php:75
firstCallInit()
Do various kinds of initialisation on the first call of the parser.
Definition: Parser.php:290
$wgParserConf
Parser configuration.
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:34
$wgMaxTocLevel
Maximum indent level of toc.
const PTD_FOR_INCLUSION
Definition: Parser.php:107
static escapeId($id, $options=array())
Given a value, escape it so that it can be used in an id attribute and return it. ...
Definition: Sanitizer.php:1126
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped broken
Definition: hooks.txt:1732
armorLinks($text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition: Parser.php:2332
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1531
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set up
Definition: hooks.txt:1931
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
setOutputType($ot)
Set the output type.
Definition: Parser.php:737
$mTagHooks
Definition: Parser.php:137
Class for handling an array of magic words.
Definition: MagicWord.php:699
const NS_MEDIAWIKI
Definition: Defines.php:77
static & get($id)
Factory: creates an object representing an ID.
Definition: MagicWord.php:248
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:240
#define the
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 $content
Definition: hooks.txt:968
maybeMakeExternalImage($url)
make an image if it's allowed, either through the global option, through the exception, or through the on-wiki whitelist
Definition: Parser.php:1915
getOption($oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2595
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition: Parser.php:2341
const MARKER_SUFFIX
Definition: Parser.php:129
wfDebugLog($logGroup, $text, $dest= 'all', array $context=array())
Send a line to a supplementary debug log file, if configured, or main debug log if not...
const OT_HTML
Definition: Defines.php:225
Prior to maintenance scripts were a hodgepodge of code that had no cohesion or formal method of action Beginning in
Definition: maintenance.txt:1
static makeImageLink(Parser $parser, Title $title, $file, $frameParams=array(), $handlerParams=array(), $time=false, $query="", $widthOption=null)
Given parameters derived from [[Image:Foo|options...]], generate the HTML that that syntax inserts in...
Definition: Linker.php:537
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 the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object & $output
Definition: hooks.txt:968
static getSubstIDs()
Get an array of parser substitution modifier IDs.
Definition: MagicWord.php:275
static images()
Definition: SiteStats.php:172
isSpecialPage()
Returns true if this is a special page.
Definition: Title.php:1050
$mTransparentTagHooks
Definition: Parser.php:138
$mExpensiveFunctionCount
Definition: Parser.php:179
$mUrlProtocols
Definition: Parser.php:158
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
static getVersion($flags= '')
Return a string of the MediaWiki version with SVN revision if available.
static newFromTitle($title, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given title.
Definition: Revision.php:104
$mConf
Definition: Parser.php:158
static newFromId($id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:86
if($IP===false)
Definition: WebStart.php:75
wfUrlProtocols($includeProtocolRelative=true)
Returns a regular expression of url protocols.
__clone()
Allow extensions to clean up when the parser is cloned.
Definition: Parser.php:267
static getExternalLinkRel($url=false, $title=null)
Get the rel attribute for a particular external link.
Definition: Parser.php:1804
static & singleton()
Get an instance of this class.
Definition: LinkCache.php:49
string $mUniqPrefix
Deprecated accessor for the strip marker prefix.
Definition: Parser.php:206
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
array $mLangLinkLanguages
Array with the language name of each language link (i.e.
Definition: Parser.php:212
const OT_MSG
Definition: Parser.php:114
replaceTransparentTags($text)
Replace transparent tags in $text with the values given by the callbacks.
Definition: Parser.php:5754
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres as and are nearing end of but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition: postgres.txt:22
getLinkURL($query= '', $query2=false, $proto=PROTO_RELATIVE)
Get a URL that's the simplest URL that will be valid to link, locally, to the current Title...
Definition: Title.php:1783
doDoubleUnderscore($text)
Strip double-underscore items like NOGALLERY and NOTOC Fills $this->mDoubleUnderscores, returns the modified text.
Definition: Parser.php:4278
$mFunctionHooks
Definition: Parser.php:139
$lines
Definition: router.php:66
follow the installation instructions in the PHPUnit Manual at
Definition: README:1
$wgUseTidy
$wgUseTidy: use tidy to make sure HTML output is sane.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global then executing the whole list after the page is displayed We don t do anything smart like collating updates to the same table or such because the list is almost always going to have just one item on if so it s not worth the trouble Since there is a job queue in the jobs table
Definition: deferred.txt:11
MagicWordArray $mSubstWords
Definition: Parser.php:156
const COLON_STATE_TEXT
Definition: Parser.php:97
const TOC_END
Definition: Parser.php:134
static normalizeCharReferences($text)
Ensure that any entities and character references are legal for XML and XHTML specifically.
Definition: Sanitizer.php:1325
the array() calling protocol came about after MediaWiki 1.4rc1.
List of Api Query prop modules.
static element($element, $attribs=array(), $contents= '')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:237
$mArgStack
Definition: Parser.php:171
$wgScriptPath
The path we should point to.
isTrans()
Determine whether the object refers to a page within this project and is transcludable.
Definition: