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  $first_two = substr( $line, 0, 2 );
1027  $matches = array();
1028 
1029  if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) {
1030  # First check if we are starting a new table
1031  $indent_level = strlen( $matches[1] );
1032 
1033  $attributes = $this->mStripState->unstripBoth( $matches[2] );
1034  $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' );
1035 
1036  $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>";
1037  array_push( $td_history, false );
1038  array_push( $last_tag_history, '' );
1039  array_push( $tr_history, false );
1040  array_push( $tr_attributes, '' );
1041  array_push( $has_opened_tr, false );
1042  } elseif ( count( $td_history ) == 0 ) {
1043  # Don't do any of the following
1044  $out .= $outLine . "\n";
1045  continue;
1046  } elseif ( $first_two === '|}' ) {
1047  # We are ending a table
1048  $line = '</table>' . substr( $line, 2 );
1049  $last_tag = array_pop( $last_tag_history );
1050 
1051  if ( !array_pop( $has_opened_tr ) ) {
1052  $line = "<tr><td></td></tr>{$line}";
1053  }
1054 
1055  if ( array_pop( $tr_history ) ) {
1056  $line = "</tr>{$line}";
1057  }
1058 
1059  if ( array_pop( $td_history ) ) {
1060  $line = "</{$last_tag}>{$line}";
1061  }
1062  array_pop( $tr_attributes );
1063  $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
1064  } elseif ( $first_two === '|-' ) {
1065  # Now we have a table row
1066  $line = preg_replace( '#^\|-+#', '', $line );
1067 
1068  # Whats after the tag is now only attributes
1069  $attributes = $this->mStripState->unstripBoth( $line );
1070  $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
1071  array_pop( $tr_attributes );
1072  array_push( $tr_attributes, $attributes );
1073 
1074  $line = '';
1075  $last_tag = array_pop( $last_tag_history );
1076  array_pop( $has_opened_tr );
1077  array_push( $has_opened_tr, true );
1078 
1079  if ( array_pop( $tr_history ) ) {
1080  $line = '</tr>';
1081  }
1082 
1083  if ( array_pop( $td_history ) ) {
1084  $line = "</{$last_tag}>{$line}";
1085  }
1086 
1087  $outLine = $line;
1088  array_push( $tr_history, false );
1089  array_push( $td_history, false );
1090  array_push( $last_tag_history, '' );
1091  } elseif ( $first_character === '|'
1092  || $first_character === '!'
1093  || $first_two === '|+'
1094  ) {
1095  # This might be cell elements, td, th or captions
1096  if ( $first_two === '|+' ) {
1097  $first_character = '+';
1098  $line = substr( $line, 2 );
1099  } else {
1100  $line = substr( $line, 1 );
1101  }
1102 
1103  if ( $first_character === '!' ) {
1104  $line = str_replace( '!!', '||', $line );
1105  }
1106 
1107  # Split up multiple cells on the same line.
1108  # FIXME : This can result in improper nesting of tags processed
1109  # by earlier parser steps, but should avoid splitting up eg
1110  # attribute values containing literal "||".
1111  $cells = StringUtils::explodeMarkup( '||', $line );
1112 
1113  $outLine = '';
1114 
1115  # Loop through each table cell
1116  foreach ( $cells as $cell ) {
1117  $previous = '';
1118  if ( $first_character !== '+' ) {
1119  $tr_after = array_pop( $tr_attributes );
1120  if ( !array_pop( $tr_history ) ) {
1121  $previous = "<tr{$tr_after}>\n";
1122  }
1123  array_push( $tr_history, true );
1124  array_push( $tr_attributes, '' );
1125  array_pop( $has_opened_tr );
1126  array_push( $has_opened_tr, true );
1127  }
1128 
1129  $last_tag = array_pop( $last_tag_history );
1130 
1131  if ( array_pop( $td_history ) ) {
1132  $previous = "</{$last_tag}>\n{$previous}";
1133  }
1134 
1135  if ( $first_character === '|' ) {
1136  $last_tag = 'td';
1137  } elseif ( $first_character === '!' ) {
1138  $last_tag = 'th';
1139  } elseif ( $first_character === '+' ) {
1140  $last_tag = 'caption';
1141  } else {
1142  $last_tag = '';
1143  }
1144 
1145  array_push( $last_tag_history, $last_tag );
1146 
1147  # A cell could contain both parameters and data
1148  $cell_data = explode( '|', $cell, 2 );
1149 
1150  # Bug 553: Note that a '|' inside an invalid link should not
1151  # be mistaken as delimiting cell parameters
1152  if ( strpos( $cell_data[0], '[[' ) !== false ) {
1153  $cell = "{$previous}<{$last_tag}>{$cell}";
1154  } elseif ( count( $cell_data ) == 1 ) {
1155  $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
1156  } else {
1157  $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
1158  $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
1159  $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
1160  }
1161 
1162  $outLine .= $cell;
1163  array_push( $td_history, true );
1164  }
1165  }
1166  $out .= $outLine . "\n";
1167  }
1168 
1169  # Closing open td, tr && table
1170  while ( count( $td_history ) > 0 ) {
1171  if ( array_pop( $td_history ) ) {
1172  $out .= "</td>\n";
1173  }
1174  if ( array_pop( $tr_history ) ) {
1175  $out .= "</tr>\n";
1176  }
1177  if ( !array_pop( $has_opened_tr ) ) {
1178  $out .= "<tr><td></td></tr>\n";
1179  }
1180 
1181  $out .= "</table>\n";
1182  }
1183 
1184  # Remove trailing line-ending (b/c)
1185  if ( substr( $out, -1 ) === "\n" ) {
1186  $out = substr( $out, 0, -1 );
1187  }
1188 
1189  # special case: don't return empty table
1190  if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
1191  $out = '';
1192  }
1193 
1194  return $out;
1195  }
1196 
1209  public function internalParse( $text, $isMain = true, $frame = false ) {
1210 
1211  $origText = $text;
1212 
1213  # Hook to suspend the parser in this state
1214  if ( !Hooks::run( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
1215  return $text;
1216  }
1217 
1218  # if $frame is provided, then use $frame for replacing any variables
1219  if ( $frame ) {
1220  # use frame depth to infer how include/noinclude tags should be handled
1221  # depth=0 means this is the top-level document; otherwise it's an included document
1222  if ( !$frame->depth ) {
1223  $flag = 0;
1224  } else {
1225  $flag = Parser::PTD_FOR_INCLUSION;
1226  }
1227  $dom = $this->preprocessToDom( $text, $flag );
1228  $text = $frame->expand( $dom );
1229  } else {
1230  # if $frame is not provided, then use old-style replaceVariables
1231  $text = $this->replaceVariables( $text );
1232  }
1233 
1234  Hooks::run( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
1235  $text = Sanitizer::removeHTMLtags(
1236  $text,
1237  array( &$this, 'attributeStripCallback' ),
1238  false,
1239  array_keys( $this->mTransparentTagHooks )
1240  );
1241  Hooks::run( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
1242 
1243  # Tables need to come after variable replacement for things to work
1244  # properly; putting them before other transformations should keep
1245  # exciting things like link expansions from showing up in surprising
1246  # places.
1247  $text = $this->doTableStuff( $text );
1248 
1249  $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
1250 
1251  $text = $this->doDoubleUnderscore( $text );
1252 
1253  $text = $this->doHeadings( $text );
1254  $text = $this->replaceInternalLinks( $text );
1255  $text = $this->doAllQuotes( $text );
1256  $text = $this->replaceExternalLinks( $text );
1257 
1258  # replaceInternalLinks may sometimes leave behind
1259  # absolute URLs, which have to be masked to hide them from replaceExternalLinks
1260  $text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text );
1261 
1262  $text = $this->doMagicLinks( $text );
1263  $text = $this->formatHeadings( $text, $origText, $isMain );
1264 
1265  return $text;
1266  }
1267 
1277  private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) {
1279 
1280  $text = $this->mStripState->unstripGeneral( $text );
1281 
1282  if ( $isMain ) {
1283  Hooks::run( 'ParserAfterUnstrip', array( &$this, &$text ) );
1284  }
1285 
1286  # Clean up special characters, only run once, next-to-last before doBlockLevels
1287  $fixtags = array(
1288  # french spaces, last one Guillemet-left
1289  # only if there is something before the space
1290  '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;',
1291  # french spaces, Guillemet-right
1292  '/(\\302\\253) /' => '\\1&#160;',
1293  '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
1294  );
1295  $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
1296 
1297  $text = $this->doBlockLevels( $text, $linestart );
1298 
1299  $this->replaceLinkHolders( $text );
1300 
1308  if ( !( $this->mOptions->getDisableContentConversion()
1309  || isset( $this->mDoubleUnderscores['nocontentconvert'] ) )
1310  ) {
1311  if ( !$this->mOptions->getInterfaceMessage() ) {
1312  # The position of the convert() call should not be changed. it
1313  # assumes that the links are all replaced and the only thing left
1314  # is the <nowiki> mark.
1315  $text = $this->getConverterLanguage()->convert( $text );
1316  }
1317  }
1318 
1319  $text = $this->mStripState->unstripNoWiki( $text );
1320 
1321  if ( $isMain ) {
1322  Hooks::run( 'ParserBeforeTidy', array( &$this, &$text ) );
1323  }
1324 
1325  $text = $this->replaceTransparentTags( $text );
1326  $text = $this->mStripState->unstripGeneral( $text );
1327 
1328  $text = Sanitizer::normalizeCharReferences( $text );
1329 
1330  if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
1331  $text = MWTidy::tidy( $text );
1332  } else {
1333  # attempt to sanitize at least some nesting problems
1334  # (bug #2702 and quite a few others)
1335  $tidyregs = array(
1336  # ''Something [http://www.cool.com cool''] -->
1337  # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
1338  '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
1339  '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
1340  # fix up an anchor inside another anchor, only
1341  # at least for a single single nested link (bug 3695)
1342  '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
1343  '\\1\\2</a>\\3</a>\\1\\4</a>',
1344  # fix div inside inline elements- doBlockLevels won't wrap a line which
1345  # contains a div, so fix it up here; replace
1346  # div with escaped text
1347  '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
1348  '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
1349  # remove empty italic or bold tag pairs, some
1350  # introduced by rules above
1351  '/<([bi])><\/\\1>/' => '',
1352  );
1353 
1354  $text = preg_replace(
1355  array_keys( $tidyregs ),
1356  array_values( $tidyregs ),
1357  $text );
1358  }
1359 
1360  if ( $isMain ) {
1361  Hooks::run( 'ParserAfterTidy', array( &$this, &$text ) );
1362  }
1363 
1364  return $text;
1365  }
1366 
1378  public function doMagicLinks( $text ) {
1379  $prots = wfUrlProtocolsWithoutProtRel();
1380  $urlChar = self::EXT_LINK_URL_CLASS;
1381  $space = self::SPACE_NOT_NL; # non-newline space
1382  $spdash = "(?:-|$space)"; # a dash or a non-newline space
1383  $spaces = "$space++"; # possessive match of 1 or more spaces
1384  $text = preg_replace_callback(
1385  '!(?: # Start cases
1386  (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text
1387  (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
1388  (\b(?i:$prots)$urlChar+) | # m[3]: Free external links
1389  \b(?:RFC|PMID) $spaces # m[4]: RFC or PMID, capture number
1390  ([0-9]+)\b |
1391  \bISBN $spaces ( # m[5]: ISBN, capture number
1392  (?: 97[89] $spdash? )? # optional 13-digit ISBN prefix
1393  (?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters
1394  [0-9Xx] # check digit
1395  )\b
1396  )!xu", array( &$this, 'magicLinkCallback' ), $text );
1397  return $text;
1398  }
1399 
1405  public function magicLinkCallback( $m ) {
1406  if ( isset( $m[1] ) && $m[1] !== '' ) {
1407  # Skip anchor
1408  return $m[0];
1409  } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
1410  # Skip HTML element
1411  return $m[0];
1412  } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
1413  # Free external link
1414  return $this->makeFreeExternalLink( $m[0] );
1415  } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
1416  # RFC or PMID
1417  if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
1418  $keyword = 'RFC';
1419  $urlmsg = 'rfcurl';
1420  $cssClass = 'mw-magiclink-rfc';
1421  $id = $m[4];
1422  } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
1423  $keyword = 'PMID';
1424  $urlmsg = 'pubmedurl';
1425  $cssClass = 'mw-magiclink-pmid';
1426  $id = $m[4];
1427  } else {
1428  throw new MWException( __METHOD__ . ': unrecognised match type "' .
1429  substr( $m[0], 0, 20 ) . '"' );
1430  }
1431  $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text();
1432  return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass );
1433  } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
1434  # ISBN
1435  $isbn = $m[5];
1436  $space = self::SPACE_NOT_NL; # non-newline space
1437  $isbn = preg_replace( "/$space/", ' ', $isbn );
1438  $num = strtr( $isbn, array(
1439  '-' => '',
1440  ' ' => '',
1441  'x' => 'X',
1442  ) );
1443  $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
1444  return '<a href="' .
1445  htmlspecialchars( $titleObj->getLocalURL() ) .
1446  "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
1447  } else {
1448  return $m[0];
1449  }
1450  }
1451 
1460  public function makeFreeExternalLink( $url ) {
1461 
1462  $trail = '';
1463 
1464  # The characters '<' and '>' (which were escaped by
1465  # removeHTMLtags()) should not be included in
1466  # URLs, per RFC 2396.
1467  $m2 = array();
1468  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1469  $trail = substr( $url, $m2[0][1] ) . $trail;
1470  $url = substr( $url, 0, $m2[0][1] );
1471  }
1472 
1473  # Move trailing punctuation to $trail
1474  $sep = ',;\.:!?';
1475  # If there is no left bracket, then consider right brackets fair game too
1476  if ( strpos( $url, '(' ) === false ) {
1477  $sep .= ')';
1478  }
1479 
1480  $urlRev = strrev( $url );
1481  $numSepChars = strspn( $urlRev, $sep );
1482  # Don't break a trailing HTML entity by moving the ; into $trail
1483  # This is in hot code, so use substr_compare to avoid having to
1484  # create a new string object for the comparison
1485  if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) {
1486  # more optimization: instead of running preg_match with a $
1487  # anchor, which can be slow, do the match on the reversed
1488  # string starting at the desired offset.
1489  # un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i
1490  if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) {
1491  $numSepChars--;
1492  }
1493  }
1494  if ( $numSepChars ) {
1495  $trail = substr( $url, -$numSepChars ) . $trail;
1496  $url = substr( $url, 0, -$numSepChars );
1497  }
1498 
1499  $url = Sanitizer::cleanUrl( $url );
1500 
1501  # Is this an external image?
1502  $text = $this->maybeMakeExternalImage( $url );
1503  if ( $text === false ) {
1504  # Not an image, make a link
1505  $text = Linker::makeExternalLink( $url,
1506  $this->getConverterLanguage()->markNoConversion( $url, true ),
1507  true, 'free',
1508  $this->getExternalLinkAttribs( $url ) );
1509  # Register it in the output object...
1510  # Replace unnecessary URL escape codes with their equivalent characters
1511  $pasteurized = self::normalizeLinkUrl( $url );
1512  $this->mOutput->addExternalLink( $pasteurized );
1513  }
1514  return $text . $trail;
1515  }
1516 
1526  public function doHeadings( $text ) {
1527  for ( $i = 6; $i >= 1; --$i ) {
1528  $h = str_repeat( '=', $i );
1529  $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
1530  }
1531  return $text;
1532  }
1533 
1542  public function doAllQuotes( $text ) {
1543  $outtext = '';
1544  $lines = StringUtils::explode( "\n", $text );
1545  foreach ( $lines as $line ) {
1546  $outtext .= $this->doQuotes( $line ) . "\n";
1547  }
1548  $outtext = substr( $outtext, 0, -1 );
1549  return $outtext;
1550  }
1551 
1559  public function doQuotes( $text ) {
1560  $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1561  $countarr = count( $arr );
1562  if ( $countarr == 1 ) {
1563  return $text;
1564  }
1565 
1566  // First, do some preliminary work. This may shift some apostrophes from
1567  // being mark-up to being text. It also counts the number of occurrences
1568  // of bold and italics mark-ups.
1569  $numbold = 0;
1570  $numitalics = 0;
1571  for ( $i = 1; $i < $countarr; $i += 2 ) {
1572  $thislen = strlen( $arr[$i] );
1573  // If there are ever four apostrophes, assume the first is supposed to
1574  // be text, and the remaining three constitute mark-up for bold text.
1575  // (bug 13227: ''''foo'''' turns into ' ''' foo ' ''')
1576  if ( $thislen == 4 ) {
1577  $arr[$i - 1] .= "'";
1578  $arr[$i] = "'''";
1579  $thislen = 3;
1580  } elseif ( $thislen > 5 ) {
1581  // If there are more than 5 apostrophes in a row, assume they're all
1582  // text except for the last 5.
1583  // (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''')
1584  $arr[$i - 1] .= str_repeat( "'", $thislen - 5 );
1585  $arr[$i] = "'''''";
1586  $thislen = 5;
1587  }
1588  // Count the number of occurrences of bold and italics mark-ups.
1589  if ( $thislen == 2 ) {
1590  $numitalics++;
1591  } elseif ( $thislen == 3 ) {
1592  $numbold++;
1593  } elseif ( $thislen == 5 ) {
1594  $numitalics++;
1595  $numbold++;
1596  }
1597  }
1598 
1599  // If there is an odd number of both bold and italics, it is likely
1600  // that one of the bold ones was meant to be an apostrophe followed
1601  // by italics. Which one we cannot know for certain, but it is more
1602  // likely to be one that has a single-letter word before it.
1603  if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
1604  $firstsingleletterword = -1;
1605  $firstmultiletterword = -1;
1606  $firstspace = -1;
1607  for ( $i = 1; $i < $countarr; $i += 2 ) {
1608  if ( strlen( $arr[$i] ) == 3 ) {
1609  $x1 = substr( $arr[$i - 1], -1 );
1610  $x2 = substr( $arr[$i - 1], -2, 1 );
1611  if ( $x1 === ' ' ) {
1612  if ( $firstspace == -1 ) {
1613  $firstspace = $i;
1614  }
1615  } elseif ( $x2 === ' ' ) {
1616  if ( $firstsingleletterword == -1 ) {
1617  $firstsingleletterword = $i;
1618  // if $firstsingleletterword is set, we don't
1619  // look at the other options, so we can bail early.
1620  break;
1621  }
1622  } else {
1623  if ( $firstmultiletterword == -1 ) {
1624  $firstmultiletterword = $i;
1625  }
1626  }
1627  }
1628  }
1629 
1630  // If there is a single-letter word, use it!
1631  if ( $firstsingleletterword > -1 ) {
1632  $arr[$firstsingleletterword] = "''";
1633  $arr[$firstsingleletterword - 1] .= "'";
1634  } elseif ( $firstmultiletterword > -1 ) {
1635  // If not, but there's a multi-letter word, use that one.
1636  $arr[$firstmultiletterword] = "''";
1637  $arr[$firstmultiletterword - 1] .= "'";
1638  } elseif ( $firstspace > -1 ) {
1639  // ... otherwise use the first one that has neither.
1640  // (notice that it is possible for all three to be -1 if, for example,
1641  // there is only one pentuple-apostrophe in the line)
1642  $arr[$firstspace] = "''";
1643  $arr[$firstspace - 1] .= "'";
1644  }
1645  }
1646 
1647  // Now let's actually convert our apostrophic mush to HTML!
1648  $output = '';
1649  $buffer = '';
1650  $state = '';
1651  $i = 0;
1652  foreach ( $arr as $r ) {
1653  if ( ( $i % 2 ) == 0 ) {
1654  if ( $state === 'both' ) {
1655  $buffer .= $r;
1656  } else {
1657  $output .= $r;
1658  }
1659  } else {
1660  $thislen = strlen( $r );
1661  if ( $thislen == 2 ) {
1662  if ( $state === 'i' ) {
1663  $output .= '</i>';
1664  $state = '';
1665  } elseif ( $state === 'bi' ) {
1666  $output .= '</i>';
1667  $state = 'b';
1668  } elseif ( $state === 'ib' ) {
1669  $output .= '</b></i><b>';
1670  $state = 'b';
1671  } elseif ( $state === 'both' ) {
1672  $output .= '<b><i>' . $buffer . '</i>';
1673  $state = 'b';
1674  } else { // $state can be 'b' or ''
1675  $output .= '<i>';
1676  $state .= 'i';
1677  }
1678  } elseif ( $thislen == 3 ) {
1679  if ( $state === 'b' ) {
1680  $output .= '</b>';
1681  $state = '';
1682  } elseif ( $state === 'bi' ) {
1683  $output .= '</i></b><i>';
1684  $state = 'i';
1685  } elseif ( $state === 'ib' ) {
1686  $output .= '</b>';
1687  $state = 'i';
1688  } elseif ( $state === 'both' ) {
1689  $output .= '<i><b>' . $buffer . '</b>';
1690  $state = 'i';
1691  } else { // $state can be 'i' or ''
1692  $output .= '<b>';
1693  $state .= 'b';
1694  }
1695  } elseif ( $thislen == 5 ) {
1696  if ( $state === 'b' ) {
1697  $output .= '</b><i>';
1698  $state = 'i';
1699  } elseif ( $state === 'i' ) {
1700  $output .= '</i><b>';
1701  $state = 'b';
1702  } elseif ( $state === 'bi' ) {
1703  $output .= '</i></b>';
1704  $state = '';
1705  } elseif ( $state === 'ib' ) {
1706  $output .= '</b></i>';
1707  $state = '';
1708  } elseif ( $state === 'both' ) {
1709  $output .= '<i><b>' . $buffer . '</b></i>';
1710  $state = '';
1711  } else { // ($state == '')
1712  $buffer = '';
1713  $state = 'both';
1714  }
1715  }
1716  }
1717  $i++;
1718  }
1719  // Now close all remaining tags. Notice that the order is important.
1720  if ( $state === 'b' || $state === 'ib' ) {
1721  $output .= '</b>';
1722  }
1723  if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
1724  $output .= '</i>';
1725  }
1726  if ( $state === 'bi' ) {
1727  $output .= '</b>';
1728  }
1729  // There might be lonely ''''', so make sure we have a buffer
1730  if ( $state === 'both' && $buffer ) {
1731  $output .= '<b><i>' . $buffer . '</i></b>';
1732  }
1733  return $output;
1734  }
1735 
1749  public function replaceExternalLinks( $text ) {
1750 
1751  $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
1752  if ( $bits === false ) {
1753  throw new MWException( "PCRE needs to be compiled with "
1754  . "--enable-unicode-properties in order for MediaWiki to function" );
1755  }
1756  $s = array_shift( $bits );
1757 
1758  $i = 0;
1759  while ( $i < count( $bits ) ) {
1760  $url = $bits[$i++];
1761  $i++; // protocol
1762  $text = $bits[$i++];
1763  $trail = $bits[$i++];
1764 
1765  # The characters '<' and '>' (which were escaped by
1766  # removeHTMLtags()) should not be included in
1767  # URLs, per RFC 2396.
1768  $m2 = array();
1769  if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
1770  $text = substr( $url, $m2[0][1] ) . ' ' . $text;
1771  $url = substr( $url, 0, $m2[0][1] );
1772  }
1773 
1774  # If the link text is an image URL, replace it with an <img> tag
1775  # This happened by accident in the original parser, but some people used it extensively
1776  $img = $this->maybeMakeExternalImage( $text );
1777  if ( $img !== false ) {
1778  $text = $img;
1779  }
1780 
1781  $dtrail = '';
1782 
1783  # Set linktype for CSS - if URL==text, link is essentially free
1784  $linktype = ( $text === $url ) ? 'free' : 'text';
1785 
1786  # No link text, e.g. [http://domain.tld/some.link]
1787  if ( $text == '' ) {
1788  # Autonumber
1789  $langObj = $this->getTargetLanguage();
1790  $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
1791  $linktype = 'autonumber';
1792  } else {
1793  # Have link text, e.g. [http://domain.tld/some.link text]s
1794  # Check for trail
1795  list( $dtrail, $trail ) = Linker::splitTrail( $trail );
1796  }
1797 
1798  $text = $this->getConverterLanguage()->markNoConversion( $text );
1799 
1800  $url = Sanitizer::cleanUrl( $url );
1801 
1802  # Use the encoded URL
1803  # This means that users can paste URLs directly into the text
1804  # Funny characters like ö aren't valid in URLs anyway
1805  # This was changed in August 2004
1806  $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
1807  $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
1808 
1809  # Register link in the output object.
1810  # Replace unnecessary URL escape codes with the referenced character
1811  # This prevents spammers from hiding links from the filters
1812  $pasteurized = self::normalizeLinkUrl( $url );
1813  $this->mOutput->addExternalLink( $pasteurized );
1814  }
1815 
1816  return $s;
1817  }
1818 
1828  public static function getExternalLinkRel( $url = false, $title = null ) {
1830  $ns = $title ? $title->getNamespace() : false;
1831  if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions )
1832  && !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions )
1833  ) {
1834  return 'nofollow';
1835  }
1836  return null;
1837  }
1838 
1849  public function getExternalLinkAttribs( $url = false ) {
1850  $attribs = array();
1851  $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle );
1852 
1853  if ( $this->mOptions->getExternalLinkTarget() ) {
1854  $attribs['target'] = $this->mOptions->getExternalLinkTarget();
1855  }
1856  return $attribs;
1857  }
1858 
1866  public static function replaceUnusualEscapes( $url ) {
1867  wfDeprecated( __METHOD__, '1.24' );
1868  return self::normalizeLinkUrl( $url );
1869  }
1870 
1880  public static function normalizeLinkUrl( $url ) {
1881  # First, make sure unsafe characters are encoded
1882  $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
1883  function ( $m ) {
1884  return rawurlencode( $m[0] );
1885  },
1886  $url
1887  );
1888 
1889  $ret = '';
1890  $end = strlen( $url );
1891 
1892  # Fragment part - 'fragment'
1893  $start = strpos( $url, '#' );
1894  if ( $start !== false && $start < $end ) {
1895  $ret = self::normalizeUrlComponent(
1896  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
1897  $end = $start;
1898  }
1899 
1900  # Query part - 'query' minus &=+;
1901  $start = strpos( $url, '?' );
1902  if ( $start !== false && $start < $end ) {
1903  $ret = self::normalizeUrlComponent(
1904  substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
1905  $end = $start;
1906  }
1907 
1908  # Scheme and path part - 'pchar'
1909  # (we assume no userinfo or encoded colons in the host)
1910  $ret = self::normalizeUrlComponent(
1911  substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
1912 
1913  return $ret;
1914  }
1915 
1916  private static function normalizeUrlComponent( $component, $unsafe ) {
1917  $callback = function ( $matches ) use ( $unsafe ) {
1918  $char = urldecode( $matches[0] );
1919  $ord = ord( $char );
1920  if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
1921  # Unescape it
1922  return $char;
1923  } else {
1924  # Leave it escaped, but use uppercase for a-f
1925  return strtoupper( $matches[0] );
1926  }
1927  };
1928  return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
1929  }
1930 
1939  private function maybeMakeExternalImage( $url ) {
1940  $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
1941  $imagesexception = !empty( $imagesfrom );
1942  $text = false;
1943  # $imagesfrom could be either a single string or an array of strings, parse out the latter
1944  if ( $imagesexception && is_array( $imagesfrom ) ) {
1945  $imagematch = false;
1946  foreach ( $imagesfrom as $match ) {
1947  if ( strpos( $url, $match ) === 0 ) {
1948  $imagematch = true;
1949  break;
1950  }
1951  }
1952  } elseif ( $imagesexception ) {
1953  $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
1954  } else {
1955  $imagematch = false;
1956  }
1957 
1958  if ( $this->mOptions->getAllowExternalImages()
1959  || ( $imagesexception && $imagematch )
1960  ) {
1961  if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
1962  # Image found
1963  $text = Linker::makeExternalImage( $url );
1964  }
1965  }
1966  if ( !$text && $this->mOptions->getEnableImageWhitelist()
1967  && preg_match( self::EXT_IMAGE_REGEX, $url )
1968  ) {
1969  $whitelist = explode(
1970  "\n",
1971  wfMessage( 'external_image_whitelist' )->inContentLanguage()->text()
1972  );
1973 
1974  foreach ( $whitelist as $entry ) {
1975  # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
1976  if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
1977  continue;
1978  }
1979  if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
1980  # Image matches a whitelist entry
1981  $text = Linker::makeExternalImage( $url );
1982  break;
1983  }
1984  }
1985  }
1986  return $text;
1987  }
1998  public function replaceInternalLinks( $s ) {
1999  $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
2000  return $s;
2001  }
2002 
2011  public function replaceInternalLinks2( &$s ) {
2013 
2014  static $tc = false, $e1, $e1_img;
2015  # the % is needed to support urlencoded titles as well
2016  if ( !$tc ) {
2017  $tc = Title::legalChars() . '#%';
2018  # Match a link having the form [[namespace:link|alternate]]trail
2019  $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
2020  # Match cases where there is no "]]", which might still be images
2021  $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
2022  }
2023 
2024  $holders = new LinkHolderArray( $this );
2025 
2026  # split the entire text string on occurrences of [[
2027  $a = StringUtils::explode( '[[', ' ' . $s );
2028  # get the first element (all text up to first [[), and remove the space we added
2029  $s = $a->current();
2030  $a->next();
2031  $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
2032  $s = substr( $s, 1 );
2033 
2034  $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension();
2035  $e2 = null;
2036  if ( $useLinkPrefixExtension ) {
2037  # Match the end of a line for a word that's not followed by whitespace,
2038  # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
2040  $charset = $wgContLang->linkPrefixCharset();
2041  $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
2042  }
2043 
2044  if ( is_null( $this->mTitle ) ) {
2045  throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" );
2046  }
2047  $nottalk = !$this->mTitle->isTalkPage();
2048 
2049  if ( $useLinkPrefixExtension ) {
2050  $m = array();
2051  if ( preg_match( $e2, $s, $m ) ) {
2052  $first_prefix = $m[2];
2053  } else {
2054  $first_prefix = false;
2055  }
2056  } else {
2057  $prefix = '';
2058  }
2059 
2060  $useSubpages = $this->areSubpagesAllowed();
2061 
2062  // @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect
2063  # Loop for each link
2064  for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) {
2065  // @codingStandardsIgnoreStart
2066 
2067  # Check for excessive memory usage
2068  if ( $holders->isBig() ) {
2069  # Too big
2070  # Do the existence check, replace the link holders and clear the array
2071  $holders->replace( $s );
2072  $holders->clear();
2073  }
2074 
2075  if ( $useLinkPrefixExtension ) {
2076  if ( preg_match( $e2, $s, $m ) ) {
2077  $prefix = $m[2];
2078  $s = $m[1];
2079  } else {
2080  $prefix = '';
2081  }
2082  # first link
2083  if ( $first_prefix ) {
2084  $prefix = $first_prefix;
2085  $first_prefix = false;
2086  }
2087  }
2088 
2089  $might_be_img = false;
2090 
2091  if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
2092  $text = $m[2];
2093  # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
2094  # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
2095  # the real problem is with the $e1 regex
2096  # See bug 1300.
2097  #
2098  # Still some problems for cases where the ] is meant to be outside punctuation,
2099  # and no image is in sight. See bug 2095.
2100  #
2101  if ( $text !== ''
2102  && substr( $m[3], 0, 1 ) === ']'
2103  && strpos( $text, '[' ) !== false
2104  ) {
2105  $text .= ']'; # so that replaceExternalLinks($text) works later
2106  $m[3] = substr( $m[3], 1 );
2107  }
2108  # fix up urlencoded title texts
2109  if ( strpos( $m[1], '%' ) !== false ) {
2110  # Should anchors '#' also be rejected?
2111  $m[1] = str_replace( array( '<', '>' ), array( '&lt;', '&gt;' ), rawurldecode( $m[1] ) );
2112  }
2113  $trail = $m[3];
2114  } elseif ( preg_match( $e1_img, $line, $m ) ) {
2115  # Invalid, but might be an image with a link in its caption
2116  $might_be_img = true;
2117  $text = $m[2];
2118  if ( strpos( $m[1], '%' ) !== false ) {
2119  $m[1] = rawurldecode( $m[1] );
2120  }
2121  $trail = "";
2122  } else { # Invalid form; output directly
2123  $s .= $prefix . '[[' . $line;
2124  continue;
2125  }
2126 
2127  $origLink = $m[1];
2128 
2129  # Don't allow internal links to pages containing
2130  # PROTO: where PROTO is a valid URL protocol; these
2131  # should be external links.
2132  if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) {
2133  $s .= $prefix . '[[' . $line;
2134  continue;
2135  }
2136 
2137  # Make subpage if necessary
2138  if ( $useSubpages ) {
2139  $link = $this->maybeDoSubpageLink( $origLink, $text );
2140  } else {
2141  $link = $origLink;
2142  }
2143 
2144  $noforce = ( substr( $origLink, 0, 1 ) !== ':' );
2145  if ( !$noforce ) {
2146  # Strip off leading ':'
2147  $link = substr( $link, 1 );
2148  }
2149 
2150  $unstrip = $this->mStripState->unstripNoWiki( $link );
2151  $nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null;
2152  if ( $nt === null ) {
2153  $s .= $prefix . '[[' . $line;
2154  continue;
2155  }
2156 
2157  $ns = $nt->getNamespace();
2158  $iw = $nt->getInterwiki();
2159 
2160  if ( $might_be_img ) { # if this is actually an invalid link
2161  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2162  $found = false;
2163  while ( true ) {
2164  # look at the next 'line' to see if we can close it there
2165  $a->next();
2166  $next_line = $a->current();
2167  if ( $next_line === false || $next_line === null ) {
2168  break;
2169  }
2170  $m = explode( ']]', $next_line, 3 );
2171  if ( count( $m ) == 3 ) {
2172  # the first ]] closes the inner link, the second the image
2173  $found = true;
2174  $text .= "[[{$m[0]}]]{$m[1]}";
2175  $trail = $m[2];
2176  break;
2177  } elseif ( count( $m ) == 2 ) {
2178  # if there's exactly one ]] that's fine, we'll keep looking
2179  $text .= "[[{$m[0]}]]{$m[1]}";
2180  } else {
2181  # if $next_line is invalid too, we need look no further
2182  $text .= '[[' . $next_line;
2183  break;
2184  }
2185  }
2186  if ( !$found ) {
2187  # we couldn't find the end of this imageLink, so output it raw
2188  # but don't ignore what might be perfectly normal links in the text we've examined
2189  $holders->merge( $this->replaceInternalLinks2( $text ) );
2190  $s .= "{$prefix}[[$link|$text";
2191  # note: no $trail, because without an end, there *is* no trail
2192  continue;
2193  }
2194  } else { # it's not an image, so output it raw
2195  $s .= "{$prefix}[[$link|$text";
2196  # note: no $trail, because without an end, there *is* no trail
2197  continue;
2198  }
2199  }
2200 
2201  $wasblank = ( $text == '' );
2202  if ( $wasblank ) {
2203  $text = $link;
2204  } else {
2205  # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2206  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2207  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2208  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2209  $text = $this->doQuotes( $text );
2210  }
2211 
2212  # Link not escaped by : , create the various objects
2213  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2214  # Interwikis
2215  if (
2216  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2217  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2218  in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2219  )
2220  ) {
2221  # Bug 24502: filter duplicates
2222  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2223  $this->mLangLinkLanguages[$iw] = true;
2224  $this->mOutput->addLanguageLink( $nt->getFullText() );
2225  }
2226 
2227  $s = rtrim( $s . $prefix );
2228  $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2229  continue;
2230  }
2231 
2232  if ( $ns == NS_FILE ) {
2233  if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2234  if ( $wasblank ) {
2235  # if no parameters were passed, $text
2236  # becomes something like "File:Foo.png",
2237  # which we don't want to pass on to the
2238  # image generator
2239  $text = '';
2240  } else {
2241  # recursively parse links inside the image caption
2242  # actually, this will parse them in any other parameters, too,
2243  # but it might be hard to fix that, and it doesn't matter ATM
2244  $text = $this->replaceExternalLinks( $text );
2245  $holders->merge( $this->replaceInternalLinks2( $text ) );
2246  }
2247  # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2248  $s .= $prefix . $this->armorLinks(
2249  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2250  } else {
2251  $s .= $prefix . $trail;
2252  }
2253  continue;
2254  }
2255 
2256  if ( $ns == NS_CATEGORY ) {
2257  $s = rtrim( $s . "\n" ); # bug 87
2258 
2259  if ( $wasblank ) {
2260  $sortkey = $this->getDefaultSort();
2261  } else {
2262  $sortkey = $text;
2263  }
2264  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2265  $sortkey = str_replace( "\n", '', $sortkey );
2266  $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2267  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2268 
2272  $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2273 
2274  continue;
2275  }
2276  }
2277 
2278  # Self-link checking. For some languages, variants of the title are checked in
2279  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2280  # for linking to a different variant.
2281  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2282  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2283  continue;
2284  }
2285 
2286  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2287  # @todo FIXME: Should do batch file existence checks, see comment below
2288  if ( $ns == NS_MEDIA ) {
2289  # Give extensions a chance to select the file revision for us
2290  $options = array();
2291  $descQuery = false;
2292  Hooks::run( 'BeforeParserFetchFileAndTitle',
2293  array( $this, $nt, &$options, &$descQuery ) );
2294  # Fetch and register the file (file title may be different via hooks)
2295  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2296  # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2297  $s .= $prefix . $this->armorLinks(
2298  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2299  continue;
2300  }
2301 
2302  # Some titles, such as valid special pages or files in foreign repos, should
2303  # be shown as bluelinks even though they're not included in the page table
2304  #
2305  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2306  # batch file existence checks for NS_FILE and NS_MEDIA
2307  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2308  $this->mOutput->addLink( $nt );
2309  $s .= $this->makeKnownLinkHolder( $nt, $text, array(), $trail, $prefix );
2310  } else {
2311  # Links will be added to the output link list after checking
2312  $s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix );
2313  }
2314  }
2315  return $holders;
2316  }
2317 
2332  public function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
2333  list( $inside, $trail ) = Linker::splitTrail( $trail );
2335  if ( is_string( $query ) ) {
2336  $query = wfCgiToArray( $query );
2337  }
2338  if ( $text == '' ) {
2339  $text = htmlspecialchars( $nt->getPrefixedText() );
2340  }
2341 
2342  $link = Linker::linkKnown( $nt, "$prefix$text$inside", array(), $query );
2344  return $this->armorLinks( $link ) . $trail;
2345  }
2346 
2357  public function armorLinks( $text ) {
2358  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2359  self::MARKER_PREFIX . "NOPARSE$1", $text );
2360  }
2361 
2366  public function areSubpagesAllowed() {
2367  # Some namespaces don't allow subpages
2368  return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2369  }
2370 
2379  public function maybeDoSubpageLink( $target, &$text ) {
2380  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2381  }
2382 
2389  public function closeParagraph() {
2390  $result = '';
2391  if ( $this->mLastSection != '' ) {
2392  $result = '</' . $this->mLastSection . ">\n";
2393  }
2394  $this->mInPre = false;
2395  $this->mLastSection = '';
2396  return $result;
2397  }
2398 
2409  public function getCommon( $st1, $st2 ) {
2410  $fl = strlen( $st1 );
2411  $shorter = strlen( $st2 );
2412  if ( $fl < $shorter ) {
2413  $shorter = $fl;
2414  }
2415 
2416  for ( $i = 0; $i < $shorter; ++$i ) {
2417  if ( $st1[$i] != $st2[$i] ) {
2418  break;
2419  }
2420  }
2421  return $i;
2422  }
2423 
2433  public function openList( $char ) {
2434  $result = $this->closeParagraph();
2435 
2436  if ( '*' === $char ) {
2437  $result .= "<ul><li>";
2438  } elseif ( '#' === $char ) {
2439  $result .= "<ol><li>";
2440  } elseif ( ':' === $char ) {
2441  $result .= "<dl><dd>";
2442  } elseif ( ';' === $char ) {
2443  $result .= "<dl><dt>";
2444  $this->mDTopen = true;
2445  } else {
2446  $result = '<!-- ERR 1 -->';
2447  }
2448 
2449  return $result;
2450  }
2451 
2459  public function nextItem( $char ) {
2460  if ( '*' === $char || '#' === $char ) {
2461  return "</li>\n<li>";
2462  } elseif ( ':' === $char || ';' === $char ) {
2463  $close = "</dd>\n";
2464  if ( $this->mDTopen ) {
2465  $close = "</dt>\n";
2466  }
2467  if ( ';' === $char ) {
2468  $this->mDTopen = true;
2469  return $close . '<dt>';
2470  } else {
2471  $this->mDTopen = false;
2472  return $close . '<dd>';
2473  }
2474  }
2475  return '<!-- ERR 2 -->';
2476  }
2477 
2485  public function closeList( $char ) {
2486  if ( '*' === $char ) {
2487  $text = "</li></ul>";
2488  } elseif ( '#' === $char ) {
2489  $text = "</li></ol>";
2490  } elseif ( ':' === $char ) {
2491  if ( $this->mDTopen ) {
2492  $this->mDTopen = false;
2493  $text = "</dt></dl>";
2494  } else {
2495  $text = "</dd></dl>";
2496  }
2497  } else {
2498  return '<!-- ERR 3 -->';
2499  }
2500  return $text;
2501  }
2512  public function doBlockLevels( $text, $linestart ) {
2513 
2514  # Parsing through the text line by line. The main thing
2515  # happening here is handling of block-level elements p, pre,
2516  # and making lists from lines starting with * # : etc.
2517  #
2518  $textLines = StringUtils::explode( "\n", $text );
2519 
2520  $lastPrefix = $output = '';
2521  $this->mDTopen = $inBlockElem = false;
2522  $prefixLength = 0;
2523  $paragraphStack = false;
2524  $inBlockquote = false;
2525 
2526  foreach ( $textLines as $oLine ) {
2527  # Fix up $linestart
2528  if ( !$linestart ) {
2529  $output .= $oLine;
2530  $linestart = true;
2531  continue;
2532  }
2533  # * = ul
2534  # # = ol
2535  # ; = dt
2536  # : = dd
2537 
2538  $lastPrefixLength = strlen( $lastPrefix );
2539  $preCloseMatch = preg_match( '/<\\/pre/i', $oLine );
2540  $preOpenMatch = preg_match( '/<pre/i', $oLine );
2541  # If not in a <pre> element, scan for and figure out what prefixes are there.
2542  if ( !$this->mInPre ) {
2543  # Multiple prefixes may abut each other for nested lists.
2544  $prefixLength = strspn( $oLine, '*#:;' );
2545  $prefix = substr( $oLine, 0, $prefixLength );
2546 
2547  # eh?
2548  # ; and : are both from definition-lists, so they're equivalent
2549  # for the purposes of determining whether or not we need to open/close
2550  # elements.
2551  $prefix2 = str_replace( ';', ':', $prefix );
2552  $t = substr( $oLine, $prefixLength );
2553  $this->mInPre = (bool)$preOpenMatch;
2554  } else {
2555  # Don't interpret any other prefixes in preformatted text
2556  $prefixLength = 0;
2557  $prefix = $prefix2 = '';
2558  $t = $oLine;
2559  }
2560 
2561  # List generation
2562  if ( $prefixLength && $lastPrefix === $prefix2 ) {
2563  # Same as the last item, so no need to deal with nesting or opening stuff
2564  $output .= $this->nextItem( substr( $prefix, -1 ) );
2565  $paragraphStack = false;
2566 
2567  if ( substr( $prefix, -1 ) === ';' ) {
2568  # The one nasty exception: definition lists work like this:
2569  # ; title : definition text
2570  # So we check for : in the remainder text to split up the
2571  # title and definition, without b0rking links.
2572  $term = $t2 = '';
2573  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2574  $t = $t2;
2575  $output .= $term . $this->nextItem( ':' );
2576  }
2577  }
2578  } elseif ( $prefixLength || $lastPrefixLength ) {
2579  # We need to open or close prefixes, or both.
2580 
2581  # Either open or close a level...
2582  $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
2583  $paragraphStack = false;
2584 
2585  # Close all the prefixes which aren't shared.
2586  while ( $commonPrefixLength < $lastPrefixLength ) {
2587  $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
2588  --$lastPrefixLength;
2589  }
2590 
2591  # Continue the current prefix if appropriate.
2592  if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
2593  $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
2594  }
2595 
2596  # Open prefixes where appropriate.
2597  if ( $lastPrefix && $prefixLength > $commonPrefixLength ) {
2598  $output .= "\n";
2599  }
2600  while ( $prefixLength > $commonPrefixLength ) {
2601  $char = substr( $prefix, $commonPrefixLength, 1 );
2602  $output .= $this->openList( $char );
2603 
2604  if ( ';' === $char ) {
2605  # @todo FIXME: This is dupe of code above
2606  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2607  $t = $t2;
2608  $output .= $term . $this->nextItem( ':' );
2609  }
2610  }
2611  ++$commonPrefixLength;
2612  }
2613  if ( !$prefixLength && $lastPrefix ) {
2614  $output .= "\n";
2615  }
2616  $lastPrefix = $prefix2;
2617  }
2618 
2619  # If we have no prefixes, go to paragraph mode.
2620  if ( 0 == $prefixLength ) {
2621  # No prefix (not in list)--go to paragraph mode
2622  # XXX: use a stack for nestable elements like span, table and div
2623  $openmatch = preg_match(
2624  '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|'
2625  . '<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
2626  $t
2627  );
2628  $closematch = preg_match(
2629  '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'
2630  . '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|'
2631  . self::MARKER_PREFIX
2632  . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
2633  $t
2634  );
2635 
2636  if ( $openmatch || $closematch ) {
2637  $paragraphStack = false;
2638  # @todo bug 5718: paragraph closed
2639  $output .= $this->closeParagraph();
2640  if ( $preOpenMatch && !$preCloseMatch ) {
2641  $this->mInPre = true;
2642  }
2643  $bqOffset = 0;
2644  while ( preg_match( '/<(\\/?)blockquote[\s>]/i', $t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) {
2645  $inBlockquote = !$bqMatch[1][0]; // is this a close tag?
2646  $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
2647  }
2648  $inBlockElem = !$closematch;
2649  } elseif ( !$inBlockElem && !$this->mInPre ) {
2650  if ( ' ' == substr( $t, 0, 1 )
2651  && ( $this->mLastSection === 'pre' || trim( $t ) != '' )
2652  && !$inBlockquote
2653  ) {
2654  # pre
2655  if ( $this->mLastSection !== 'pre' ) {
2656  $paragraphStack = false;
2657  $output .= $this->closeParagraph() . '<pre>';
2658  $this->mLastSection = 'pre';
2659  }
2660  $t = substr( $t, 1 );
2661  } else {
2662  # paragraph
2663  if ( trim( $t ) === '' ) {
2664  if ( $paragraphStack ) {
2665  $output .= $paragraphStack . '<br />';
2666  $paragraphStack = false;
2667  $this->mLastSection = 'p';
2668  } else {
2669  if ( $this->mLastSection !== 'p' ) {
2670  $output .= $this->closeParagraph();
2671  $this->mLastSection = '';
2672  $paragraphStack = '<p>';
2673  } else {
2674  $paragraphStack = '</p><p>';
2675  }
2676  }
2677  } else {
2678  if ( $paragraphStack ) {
2679  $output .= $paragraphStack;
2680  $paragraphStack = false;
2681  $this->mLastSection = 'p';
2682  } elseif ( $this->mLastSection !== 'p' ) {
2683  $output .= $this->closeParagraph() . '<p>';
2684  $this->mLastSection = 'p';
2685  }
2686  }
2687  }
2688  }
2689  }
2690  # somewhere above we forget to get out of pre block (bug 785)
2691  if ( $preCloseMatch && $this->mInPre ) {
2692  $this->mInPre = false;
2693  }
2694  if ( $paragraphStack === false ) {
2695  $output .= $t;
2696  if ( $prefixLength === 0 ) {
2697  $output .= "\n";
2698  }
2699  }
2700  }
2701  while ( $prefixLength ) {
2702  $output .= $this->closeList( $prefix2[$prefixLength - 1] );
2703  --$prefixLength;
2704  if ( !$prefixLength ) {
2705  $output .= "\n";
2706  }
2707  }
2708  if ( $this->mLastSection != '' ) {
2709  $output .= '</' . $this->mLastSection . '>';
2710  $this->mLastSection = '';
2711  }
2712 
2713  return $output;
2714  }
2715 
2726  public function findColonNoLinks( $str, &$before, &$after ) {
2727 
2728  $pos = strpos( $str, ':' );
2729  if ( $pos === false ) {
2730  # Nothing to find!
2731  return false;
2732  }
2733 
2734  $lt = strpos( $str, '<' );
2735  if ( $lt === false || $lt > $pos ) {
2736  # Easy; no tag nesting to worry about
2737  $before = substr( $str, 0, $pos );
2738  $after = substr( $str, $pos + 1 );
2739  return $pos;
2740  }
2741 
2742  # Ugly state machine to walk through avoiding tags.
2743  $state = self::COLON_STATE_TEXT;
2744  $stack = 0;
2745  $len = strlen( $str );
2746  for ( $i = 0; $i < $len; $i++ ) {
2747  $c = $str[$i];
2748 
2749  switch ( $state ) {
2750  # (Using the number is a performance hack for common cases)
2751  case 0: # self::COLON_STATE_TEXT:
2752  switch ( $c ) {
2753  case "<":
2754  # Could be either a <start> tag or an </end> tag
2755  $state = self::COLON_STATE_TAGSTART;
2756  break;
2757  case ":":
2758  if ( $stack == 0 ) {
2759  # We found it!
2760  $before = substr( $str, 0, $i );
2761  $after = substr( $str, $i + 1 );
2762  return $i;
2763  }
2764  # Embedded in a tag; don't break it.
2765  break;
2766  default:
2767  # Skip ahead looking for something interesting
2768  $colon = strpos( $str, ':', $i );
2769  if ( $colon === false ) {
2770  # Nothing else interesting
2771  return false;
2772  }
2773  $lt = strpos( $str, '<', $i );
2774  if ( $stack === 0 ) {
2775  if ( $lt === false || $colon < $lt ) {
2776  # We found it!
2777  $before = substr( $str, 0, $colon );
2778  $after = substr( $str, $colon + 1 );
2779  return $i;
2780  }
2781  }
2782  if ( $lt === false ) {
2783  # Nothing else interesting to find; abort!
2784  # We're nested, but there's no close tags left. Abort!
2785  break 2;
2786  }
2787  # Skip ahead to next tag start
2788  $i = $lt;
2789  $state = self::COLON_STATE_TAGSTART;
2790  }
2791  break;
2792  case 1: # self::COLON_STATE_TAG:
2793  # In a <tag>
2794  switch ( $c ) {
2795  case ">":
2796  $stack++;
2797  $state = self::COLON_STATE_TEXT;
2798  break;
2799  case "/":
2800  # Slash may be followed by >?
2801  $state = self::COLON_STATE_TAGSLASH;
2802  break;
2803  default:
2804  # ignore
2805  }
2806  break;
2807  case 2: # self::COLON_STATE_TAGSTART:
2808  switch ( $c ) {
2809  case "/":
2810  $state = self::COLON_STATE_CLOSETAG;
2811  break;
2812  case "!":
2813  $state = self::COLON_STATE_COMMENT;
2814  break;
2815  case ">":
2816  # Illegal early close? This shouldn't happen D:
2817  $state = self::COLON_STATE_TEXT;
2818  break;
2819  default:
2820  $state = self::COLON_STATE_TAG;
2821  }
2822  break;
2823  case 3: # self::COLON_STATE_CLOSETAG:
2824  # In a </tag>
2825  if ( $c === ">" ) {
2826  $stack--;
2827  if ( $stack < 0 ) {
2828  wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
2829  return false;
2830  }
2831  $state = self::COLON_STATE_TEXT;
2832  }
2833  break;
2834  case self::COLON_STATE_TAGSLASH:
2835  if ( $c === ">" ) {
2836  # Yes, a self-closed tag <blah/>
2837  $state = self::COLON_STATE_TEXT;
2838  } else {
2839  # Probably we're jumping the gun, and this is an attribute
2840  $state = self::COLON_STATE_TAG;
2841  }
2842  break;
2843  case 5: # self::COLON_STATE_COMMENT:
2844  if ( $c === "-" ) {
2845  $state = self::COLON_STATE_COMMENTDASH;
2846  }
2847  break;
2848  case self::COLON_STATE_COMMENTDASH:
2849  if ( $c === "-" ) {
2850  $state = self::COLON_STATE_COMMENTDASHDASH;
2851  } else {
2852  $state = self::COLON_STATE_COMMENT;
2853  }
2854  break;
2855  case self::COLON_STATE_COMMENTDASHDASH:
2856  if ( $c === ">" ) {
2857  $state = self::COLON_STATE_TEXT;
2858  } else {
2859  $state = self::COLON_STATE_COMMENT;
2860  }
2861  break;
2862  default:
2863  throw new MWException( "State machine error in " . __METHOD__ );
2864  }
2865  }
2866  if ( $stack > 0 ) {
2867  wfDebug( __METHOD__ . ": Invalid input; not enough close tags (stack $stack, state $state)\n" );
2868  return false;
2869  }
2870  return false;
2871  }
2872 
2884  public function getVariableValue( $index, $frame = false ) {
2887 
2888  if ( is_null( $this->mTitle ) ) {
2889  // If no title set, bad things are going to happen
2890  // later. Title should always be set since this
2891  // should only be called in the middle of a parse
2892  // operation (but the unit-tests do funky stuff)
2893  throw new MWException( __METHOD__ . ' Should only be '
2894  . ' called while parsing (no title set)' );
2895  }
2896 
2901  if ( Hooks::run( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
2902  if ( isset( $this->mVarCache[$index] ) ) {
2903  return $this->mVarCache[$index];
2904  }
2905  }
2906 
2907  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2908  Hooks::run( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
2909 
2910  $pageLang = $this->getFunctionLang();
2911 
2912  switch ( $index ) {
2913  case '!':
2914  $value = '|';
2915  break;
2916  case 'currentmonth':
2917  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2918  break;
2919  case 'currentmonth1':
2920  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2921  break;
2922  case 'currentmonthname':
2923  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2924  break;
2925  case 'currentmonthnamegen':
2926  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2927  break;
2928  case 'currentmonthabbrev':
2929  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2930  break;
2931  case 'currentday':
2932  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2933  break;
2934  case 'currentday2':
2935  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2936  break;
2937  case 'localmonth':
2938  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2939  break;
2940  case 'localmonth1':
2941  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2942  break;
2943  case 'localmonthname':
2944  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2945  break;
2946  case 'localmonthnamegen':
2947  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2948  break;
2949  case 'localmonthabbrev':
2950  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2951  break;
2952  case 'localday':
2953  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2954  break;
2955  case 'localday2':
2956  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2957  break;
2958  case 'pagename':
2959  $value = wfEscapeWikiText( $this->mTitle->getText() );
2960  break;
2961  case 'pagenamee':
2962  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2963  break;
2964  case 'fullpagename':
2965  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2966  break;
2967  case 'fullpagenamee':
2968  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2969  break;
2970  case 'subpagename':
2971  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2972  break;
2973  case 'subpagenamee':
2974  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2975  break;
2976  case 'rootpagename':
2977  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2978  break;
2979  case 'rootpagenamee':
2980  $value = wfEscapeWikiText( wfUrlEncode( str_replace(
2981  ' ',
2982  '_',
2983  $this->mTitle->getRootText()
2984  ) ) );
2985  break;
2986  case 'basepagename':
2987  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2988  break;
2989  case 'basepagenamee':
2990  $value = wfEscapeWikiText( wfUrlEncode( str_replace(
2991  ' ',
2992  '_',
2993  $this->mTitle->getBaseText()
2994  ) ) );
2995  break;
2996  case 'talkpagename':
2997  if ( $this->mTitle->canTalk() ) {
2998  $talkPage = $this->mTitle->getTalkPage();
2999  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
3000  } else {
3001  $value = '';
3002  }
3003  break;
3004  case 'talkpagenamee':
3005  if ( $this->mTitle->canTalk() ) {
3006  $talkPage = $this->mTitle->getTalkPage();
3007  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
3008  } else {
3009  $value = '';
3010  }
3011  break;
3012  case 'subjectpagename':
3013  $subjPage = $this->mTitle->getSubjectPage();
3014  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
3015  break;
3016  case 'subjectpagenamee':
3017  $subjPage = $this->mTitle->getSubjectPage();
3018  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
3019  break;
3020  case 'pageid': // requested in bug 23427
3021  $pageid = $this->getTitle()->getArticleID();
3022  if ( $pageid == 0 ) {
3023  # 0 means the page doesn't exist in the database,
3024  # which means the user is previewing a new page.
3025  # The vary-revision flag must be set, because the magic word
3026  # will have a different value once the page is saved.
3027  $this->mOutput->setFlag( 'vary-revision' );
3028  wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
3029  }
3030  $value = $pageid ? $pageid : null;
3031  break;
3032  case 'revisionid':
3033  # Let the edit saving system know we should parse the page
3034  # *after* a revision ID has been assigned.
3035  $this->mOutput->setFlag( 'vary-revision' );
3036  wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
3037  $value = $this->mRevisionId;
3038  break;
3039  case 'revisionday':
3040  # Let the edit saving system know we should parse the page
3041  # *after* a revision ID has been assigned. This is for null edits.
3042  $this->mOutput->setFlag( 'vary-revision' );
3043  wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
3044  $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
3045  break;
3046  case 'revisionday2':
3047  # Let the edit saving system know we should parse the page
3048  # *after* a revision ID has been assigned. This is for null edits.
3049  $this->mOutput->setFlag( 'vary-revision' );
3050  wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
3051  $value = substr( $this->getRevisionTimestamp(), 6, 2 );
3052  break;
3053  case 'revisionmonth':
3054  # Let the edit saving system know we should parse the page
3055  # *after* a revision ID has been assigned. This is for null edits.
3056  $this->mOutput->setFlag( 'vary-revision' );
3057  wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
3058  $value = substr( $this->getRevisionTimestamp(), 4, 2 );
3059  break;
3060  case 'revisionmonth1':
3061  # Let the edit saving system know we should parse the page
3062  # *after* a revision ID has been assigned. This is for null edits.
3063  $this->mOutput->setFlag( 'vary-revision' );
3064  wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
3065  $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
3066  break;
3067  case 'revisionyear':
3068  # Let the edit saving system know we should parse the page
3069  # *after* a revision ID has been assigned. This is for null edits.
3070  $this->mOutput->setFlag( 'vary-revision' );
3071  wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
3072  $value = substr( $this->getRevisionTimestamp(), 0, 4 );
3073  break;
3074  case 'revisiontimestamp':
3075  # Let the edit saving system know we should parse the page
3076  # *after* a revision ID has been assigned. This is for null edits.
3077  $this->mOutput->setFlag( 'vary-revision' );
3078  wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
3079  $value = $this->getRevisionTimestamp();
3080  break;
3081  case 'revisionuser':
3082  # Let the edit saving system know we should parse the page
3083  # *after* a revision ID has been assigned. This is for null edits.
3084  $this->mOutput->setFlag( 'vary-revision' );
3085  wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
3086  $value = $this->getRevisionUser();
3087  break;
3088  case 'revisionsize':
3089  # Let the edit saving system know we should parse the page
3090  # *after* a revision ID has been assigned. This is for null edits.
3091  $this->mOutput->setFlag( 'vary-revision' );
3092  wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
3093  $value = $this->getRevisionSize();
3094  break;
3095  case 'namespace':
3096  $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3097  break;
3098  case 'namespacee':
3099  $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3100  break;
3101  case 'namespacenumber':
3102  $value = $this->mTitle->getNamespace();
3103  break;
3104  case 'talkspace':
3105  $value = $this->mTitle->canTalk()
3106  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
3107  : '';
3108  break;
3109  case 'talkspacee':
3110  $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
3111  break;
3112  case 'subjectspace':
3113  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
3114  break;
3115  case 'subjectspacee':
3116  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3117  break;
3118  case 'currentdayname':
3119  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
3120  break;
3121  case 'currentyear':
3122  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
3123  break;
3124  case 'currenttime':
3125  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
3126  break;
3127  case 'currenthour':
3128  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
3129  break;
3130  case 'currentweek':
3131  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3132  # int to remove the padding
3133  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
3134  break;
3135  case 'currentdow':
3136  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
3137  break;
3138  case 'localdayname':
3139  $value = $pageLang->getWeekdayName(
3140  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
3141  );
3142  break;
3143  case 'localyear':
3144  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
3145  break;
3146  case 'localtime':
3147  $value = $pageLang->time(
3148  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
3149  false,
3150  false
3151  );
3152  break;
3153  case 'localhour':
3154  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
3155  break;
3156  case 'localweek':
3157  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3158  # int to remove the padding
3159  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
3160  break;
3161  case 'localdow':
3162  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
3163  break;
3164  case 'numberofarticles':
3165  $value = $pageLang->formatNum( SiteStats::articles() );
3166  break;
3167  case 'numberoffiles':
3168  $value = $pageLang->formatNum( SiteStats::images() );
3169  break;
3170  case 'numberofusers':
3171  $value = $pageLang->formatNum( SiteStats::users() );
3172  break;
3173  case 'numberofactiveusers':
3174  $value = $pageLang->formatNum( SiteStats::activeUsers() );
3175  break;
3176  case 'numberofpages':
3177  $value = $pageLang->formatNum( SiteStats::pages() );
3178  break;
3179  case 'numberofadmins':
3180  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
3181  break;
3182  case 'numberofedits':
3183  $value = $pageLang->formatNum( SiteStats::edits() );
3184  break;
3185  case 'currenttimestamp':
3186  $value = wfTimestamp( TS_MW, $ts );
3187  break;
3188  case 'localtimestamp':
3189  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
3190  break;
3191  case 'currentversion':
3193  break;
3194  case 'articlepath':
3195  return $wgArticlePath;
3196  case 'sitename':
3197  return $wgSitename;
3198  case 'server':
3199  return $wgServer;
3200  case 'servername':
3201  return $wgServerName;
3202  case 'scriptpath':
3203  return $wgScriptPath;
3204  case 'stylepath':
3205  return $wgStylePath;
3206  case 'directionmark':
3207  return $pageLang->getDirMark();
3208  case 'contentlanguage':
3210  return $wgLanguageCode;
3211  case 'cascadingsources':
3213  break;
3214  default:
3215  $ret = null;
3216  Hooks::run(
3217  'ParserGetVariableValueSwitch',
3218  array( &$this, &$this->mVarCache, &$index, &$ret, &$frame )
3219  );
3220 
3221  return $ret;
3222  }
3223 
3224  if ( $index ) {
3225  $this->mVarCache[$index] = $value;
3226  }
3227 
3228  return $value;
3229  }
3230 
3236  public function initialiseVariables() {
3237  $variableIDs = MagicWord::getVariableIDs();
3238  $substIDs = MagicWord::getSubstIDs();
3239 
3240  $this->mVariables = new MagicWordArray( $variableIDs );
3241  $this->mSubstWords = new MagicWordArray( $substIDs );
3242  }
3266  public function preprocessToDom( $text, $flags = 0 ) {
3267  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3268  return $dom;
3269  }
3270 
3278  public static function splitWhitespace( $s ) {
3279  $ltrimmed = ltrim( $s );
3280  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3281  $trimmed = rtrim( $ltrimmed );
3282  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3283  if ( $diff > 0 ) {
3284  $w2 = substr( $ltrimmed, -$diff );
3285  } else {
3286  $w2 = '';
3287  }
3288  return array( $w1, $trimmed, $w2 );
3289  }
3290 
3311  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3312  # Is there any text? Also, Prevent too big inclusions!
3313  if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
3314  return $text;
3315  }
3317  if ( $frame === false ) {
3318  $frame = $this->getPreprocessor()->newFrame();
3319  } elseif ( !( $frame instanceof PPFrame ) ) {
3320  wfDebug( __METHOD__ . " called using plain parameters instead of "
3321  . "a PPFrame instance. Creating custom frame.\n" );
3322  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3323  }
3324 
3325  $dom = $this->preprocessToDom( $text );
3326  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3327  $text = $frame->expand( $dom, $flags );
3328 
3329  return $text;
3330  }
3331 
3339  public static function createAssocArgs( $args ) {
3340  $assocArgs = array();
3341  $index = 1;
3342  foreach ( $args as $arg ) {
3343  $eqpos = strpos( $arg, '=' );
3344  if ( $eqpos === false ) {
3345  $assocArgs[$index++] = $arg;
3346  } else {
3347  $name = trim( substr( $arg, 0, $eqpos ) );
3348  $value = trim( substr( $arg, $eqpos + 1 ) );
3349  if ( $value === false ) {
3350  $value = '';
3351  }
3352  if ( $name !== false ) {
3353  $assocArgs[$name] = $value;
3354  }
3355  }
3356  }
3357 
3358  return $assocArgs;
3359  }
3360 
3385  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3386  # does no harm if $current and $max are present but are unnecessary for the message
3387  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3388  ->inLanguage( $this->mOptions->getUserLangObj() )->text();
3389  $this->mOutput->addWarning( $warning );
3390  $this->addTrackingCategory( "$limitationType-category" );
3391  }
3392 
3405  public function braceSubstitution( $piece, $frame ) {
3406 
3407  // Flags
3408 
3409  // $text has been filled
3410  $found = false;
3411  // wiki markup in $text should be escaped
3412  $nowiki = false;
3413  // $text is HTML, armour it against wikitext transformation
3414  $isHTML = false;
3415  // Force interwiki transclusion to be done in raw mode not rendered
3416  $forceRawInterwiki = false;
3417  // $text is a DOM node needing expansion in a child frame
3418  $isChildObj = false;
3419  // $text is a DOM node needing expansion in the current frame
3420  $isLocalObj = false;
3421 
3422  # Title object, where $text came from
3423  $title = false;
3424 
3425  # $part1 is the bit before the first |, and must contain only title characters.
3426  # Various prefixes will be stripped from it later.
3427  $titleWithSpaces = $frame->expand( $piece['title'] );
3428  $part1 = trim( $titleWithSpaces );
3429  $titleText = false;
3430 
3431  # Original title text preserved for various purposes
3432  $originalTitle = $part1;
3433 
3434  # $args is a list of argument nodes, starting from index 0, not including $part1
3435  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3436  # below won't work b/c this $args isn't an object
3437  $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
3438 
3439  $profileSection = null; // profile templates
3440 
3441  # SUBST
3442  if ( !$found ) {
3443  $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
3444 
3445  # Possibilities for substMatch: "subst", "safesubst" or FALSE
3446  # Decide whether to expand template or keep wikitext as-is.
3447  if ( $this->ot['wiki'] ) {
3448  if ( $substMatch === false ) {
3449  $literal = true; # literal when in PST with no prefix
3450  } else {
3451  $literal = false; # expand when in PST with subst: or safesubst:
3452  }
3453  } else {
3454  if ( $substMatch == 'subst' ) {
3455  $literal = true; # literal when not in PST with plain subst:
3456  } else {
3457  $literal = false; # expand when not in PST with safesubst: or no prefix
3458  }
3459  }
3460  if ( $literal ) {
3461  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3462  $isLocalObj = true;
3463  $found = true;
3464  }
3465  }
3466 
3467  # Variables
3468  if ( !$found && $args->getLength() == 0 ) {
3469  $id = $this->mVariables->matchStartToEnd( $part1 );
3470  if ( $id !== false ) {
3471  $text = $this->getVariableValue( $id, $frame );
3472  if ( MagicWord::getCacheTTL( $id ) > -1 ) {
3473  $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
3474  }
3475  $found = true;
3476  }
3477  }
3478 
3479  # MSG, MSGNW and RAW
3480  if ( !$found ) {
3481  # Check for MSGNW:
3482  $mwMsgnw = MagicWord::get( 'msgnw' );
3483  if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
3484  $nowiki = true;
3485  } else {
3486  # Remove obsolete MSG:
3487  $mwMsg = MagicWord::get( 'msg' );
3488  $mwMsg->matchStartAndRemove( $part1 );
3489  }
3490 
3491  # Check for RAW:
3492  $mwRaw = MagicWord::get( 'raw' );
3493  if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
3494  $forceRawInterwiki = true;
3495  }
3496  }
3497 
3498  # Parser functions
3499  if ( !$found ) {
3500  $colonPos = strpos( $part1, ':' );
3501  if ( $colonPos !== false ) {
3502  $func = substr( $part1, 0, $colonPos );
3503  $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
3504  for ( $i = 0; $i < $args->getLength(); $i++ ) {
3505  $funcArgs[] = $args->item( $i );
3506  }
3507  try {
3508  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3509  } catch ( Exception $ex ) {
3510  throw $ex;
3511  }
3512 
3513  # The interface for parser functions allows for extracting
3514  # flags into the local scope. Extract any forwarded flags
3515  # here.
3516  extract( $result );
3517  }
3518  }
3519 
3520  # Finish mangling title and then check for loops.
3521  # Set $title to a Title object and $titleText to the PDBK
3522  if ( !$found ) {
3523  $ns = NS_TEMPLATE;
3524  # Split the title into page and subpage
3525  $subpage = '';
3526  $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3527  if ( $part1 !== $relative ) {
3528  $part1 = $relative;
3529  $ns = $this->mTitle->getNamespace();
3530  }
3531  $title = Title::newFromText( $part1, $ns );
3532  if ( $title ) {
3533  $titleText = $title->getPrefixedText();
3534  # Check for language variants if the template is not found
3535  if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3536  $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3537  }
3538  # Do recursion depth check
3539  $limit = $this->mOptions->getMaxTemplateDepth();
3540  if ( $frame->depth >= $limit ) {
3541  $found = true;
3542  $text = '<span class="error">'
3543  . wfMessage( 'parser-template-recursion-depth-warning' )
3544  ->numParams( $limit )->inContentLanguage()->text()
3545  . '</span>';
3546  }
3547  }
3548  }
3549 
3550  # Load from database
3551  if ( !$found && $title ) {
3552  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3553  if ( !$title->isExternal() ) {
3554  if ( $title->isSpecialPage()
3555  && $this->mOptions->getAllowSpecialInclusion()
3556  && $this->ot['html']
3557  ) {
3558  // Pass the template arguments as URL parameters.
3559  // "uselang" will have no effect since the Language object
3560  // is forced to the one defined in ParserOptions.
3561  $pageArgs = array();
3562  $argsLength = $args->getLength();
3563  for ( $i = 0; $i < $argsLength; $i++ ) {
3564  $bits = $args->item( $i )->splitArg();
3565  if ( strval( $bits['index'] ) === '' ) {
3566  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3567  $value = trim( $frame->expand( $bits['value'] ) );
3568  $pageArgs[$name] = $value;
3569  }
3570  }
3571 
3572  // Create a new context to execute the special page
3573  $context = new RequestContext;
3574  $context->setTitle( $title );
3575  $context->setRequest( new FauxRequest( $pageArgs ) );
3576  $context->setUser( $this->getUser() );
3577  $context->setLanguage( $this->mOptions->getUserLangObj() );
3578  $ret = SpecialPageFactory::capturePath( $title, $context );
3579  if ( $ret ) {
3580  $text = $context->getOutput()->getHTML();
3581  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3582  $found = true;
3583  $isHTML = true;
3584  $this->disableCache();
3585  }
3586  } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3587  $found = false; # access denied
3588  wfDebug( __METHOD__ . ": template inclusion denied for " .
3589  $title->getPrefixedDBkey() . "\n" );
3590  } else {
3591  list( $text, $title ) = $this->getTemplateDom( $title );
3592  if ( $text !== false ) {
3593  $found = true;
3594  $isChildObj = true;
3595  }
3596  }
3597 
3598  # If the title is valid but undisplayable, make a link to it
3599  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3600  $text = "[[:$titleText]]";
3601  $found = true;
3602  }
3603  } elseif ( $title->isTrans() ) {
3604  # Interwiki transclusion
3605  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3606  $text = $this->interwikiTransclude( $title, 'render' );
3607  $isHTML = true;
3608  } else {
3609  $text = $this->interwikiTransclude( $title, 'raw' );
3610  # Preprocess it like a template
3611  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3612  $isChildObj = true;
3613  }
3614  $found = true;
3615  }
3616 
3617  # Do infinite loop check
3618  # This has to be done after redirect resolution to avoid infinite loops via redirects
3619  if ( !$frame->loopCheck( $title ) ) {
3620  $found = true;
3621  $text = '<span class="error">'
3622  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3623  . '</span>';
3624  wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3625  }
3626  }
3627 
3628  # If we haven't found text to substitute by now, we're done
3629  # Recover the source wikitext and return it
3630  if ( !$found ) {
3631  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3632  if ( $profileSection ) {
3633  $this->mProfiler->scopedProfileOut( $profileSection );
3634  }
3635  return array( 'object' => $text );
3636  }
3637 
3638  # Expand DOM-style return values in a child frame
3639  if ( $isChildObj ) {
3640  # Clean up argument array
3641  $newFrame = $frame->newChild( $args, $title );
3642 
3643  if ( $nowiki ) {
3644  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3645  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3646  # Expansion is eligible for the empty-frame cache
3647  $text = $newFrame->cachedExpand( $titleText, $text );
3648  } else {
3649  # Uncached expansion
3650  $text = $newFrame->expand( $text );
3651  }
3652  }
3653  if ( $isLocalObj && $nowiki ) {
3654  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3655  $isLocalObj = false;
3656  }
3657 
3658  if ( $profileSection ) {
3659  $this->mProfiler->scopedProfileOut( $profileSection );
3660  }
3661 
3662  # Replace raw HTML by a placeholder
3663  if ( $isHTML ) {
3664  $text = $this->insertStripItem( $text );
3665  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3666  # Escape nowiki-style return values
3667  $text = wfEscapeWikiText( $text );
3668  } elseif ( is_string( $text )
3669  && !$piece['lineStart']
3670  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3671  ) {
3672  # Bug 529: if the template begins with a table or block-level
3673  # element, it should be treated as beginning a new line.
3674  # This behavior is somewhat controversial.
3675  $text = "\n" . $text;
3676  }
3677 
3678  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3679  # Error, oversize inclusion
3680  if ( $titleText !== false ) {
3681  # Make a working, properly escaped link if possible (bug 23588)
3682  $text = "[[:$titleText]]";
3683  } else {
3684  # This will probably not be a working link, but at least it may
3685  # provide some hint of where the problem is
3686  preg_replace( '/^:/', '', $originalTitle );
3687  $text = "[[:$originalTitle]]";
3688  }
3689  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3690  . 'post-expand include size too large -->' );
3691  $this->limitationWarn( 'post-expand-template-inclusion' );
3692  }
3693 
3694  if ( $isLocalObj ) {
3695  $ret = array( 'object' => $text );
3696  } else {
3697  $ret = array( 'text' => $text );
3698  }
3700  return $ret;
3701  }
3702 
3722  public function callParserFunction( $frame, $function, array $args = array() ) {
3724 
3725 
3726  # Case sensitive functions
3727  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3728  $function = $this->mFunctionSynonyms[1][$function];
3729  } else {
3730  # Case insensitive functions
3731  $function = $wgContLang->lc( $function );
3732  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3733  $function = $this->mFunctionSynonyms[0][$function];
3734  } else {
3735  return array( 'found' => false );
3736  }
3737  }
3738 
3739  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3740 
3741  # Workaround for PHP bug 35229 and similar
3742  if ( !is_callable( $callback ) ) {
3743  throw new MWException( "Tag hook for $function is not callable\n" );
3744  }
3745 
3746  $allArgs = array( &$this );
3747  if ( $flags & self::SFH_OBJECT_ARGS ) {
3748  # Convert arguments to PPNodes and collect for appending to $allArgs
3749  $funcArgs = array();
3750  foreach ( $args as $k => $v ) {
3751  if ( $v instanceof PPNode || $k === 0 ) {
3752  $funcArgs[] = $v;
3753  } else {
3754  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( array( $k => $v ) )->item( 0 );
3755  }
3756  }
3757 
3758  # Add a frame parameter, and pass the arguments as an array
3759  $allArgs[] = $frame;
3760  $allArgs[] = $funcArgs;
3761  } else {
3762  # Convert arguments to plain text and append to $allArgs
3763  foreach ( $args as $k => $v ) {
3764  if ( $v instanceof PPNode ) {
3765  $allArgs[] = trim( $frame->expand( $v ) );
3766  } elseif ( is_int( $k ) && $k >= 0 ) {
3767  $allArgs[] = trim( $v );
3768  } else {
3769  $allArgs[] = trim( "$k=$v" );
3770  }
3771  }
3772  }
3773 
3774  $result = call_user_func_array( $callback, $allArgs );
3775 
3776  # The interface for function hooks allows them to return a wikitext
3777  # string or an array containing the string and any flags. This mungs
3778  # things around to match what this method should return.
3779  if ( !is_array( $result ) ) {
3780  $result = array(
3781  'found' => true,
3782  'text' => $result,
3783  );
3784  } else {
3785  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3786  $result['text'] = $result[0];
3787  }
3788  unset( $result[0] );
3789  $result += array(
3790  'found' => true,
3791  );
3792  }
3793 
3794  $noparse = true;
3795  $preprocessFlags = 0;
3796  if ( isset( $result['noparse'] ) ) {
3797  $noparse = $result['noparse'];
3798  }
3799  if ( isset( $result['preprocessFlags'] ) ) {
3800  $preprocessFlags = $result['preprocessFlags'];
3801  }
3802 
3803  if ( !$noparse ) {
3804  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3805  $result['isChildObj'] = true;
3806  }
3807 
3808  return $result;
3809  }
3810 
3819  public function getTemplateDom( $title ) {
3820  $cacheTitle = $title;
3821  $titleText = $title->getPrefixedDBkey();
3822 
3823  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3824  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3825  $title = Title::makeTitle( $ns, $dbk );
3826  $titleText = $title->getPrefixedDBkey();
3827  }
3828  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3829  return array( $this->mTplDomCache[$titleText], $title );
3830  }
3831 
3832  # Cache miss, go to the database
3833  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3834 
3835  if ( $text === false ) {
3836  $this->mTplDomCache[$titleText] = false;
3837  return array( false, $title );
3838  }
3840  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3841  $this->mTplDomCache[$titleText] = $dom;
3842 
3843  if ( !$title->equals( $cacheTitle ) ) {
3844  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3845  array( $title->getNamespace(), $cdb = $title->getDBkey() );
3846  }
3847 
3848  return array( $dom, $title );
3849  }
3850 
3862  public function fetchCurrentRevisionOfTitle( $title ) {
3863  $cacheKey = $title->getPrefixedDBkey();
3864  if ( !$this->currentRevisionCache ) {
3865  $this->currentRevisionCache = new MapCacheLRU( 100 );
3866  }
3867  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3868  $this->currentRevisionCache->set( $cacheKey,
3869  // Defaults to Parser::statelessFetchRevision()
3870  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3871  );
3872  }
3873  return $this->currentRevisionCache->get( $cacheKey );
3874  }
3875 
3885  public static function statelessFetchRevision( $title, $parser = false ) {
3886  return Revision::newFromTitle( $title );
3887  }
3888 
3894  public function fetchTemplateAndTitle( $title ) {
3895  // Defaults to Parser::statelessFetchTemplate()
3896  $templateCb = $this->mOptions->getTemplateCallback();
3897  $stuff = call_user_func( $templateCb, $title, $this );
3898  // We use U+007F DELETE to distinguish strip markers from regular text.
3899  $text = $stuff['text'];
3900  if ( is_string( $stuff['text'] ) ) {
3901  $text = strtr( $text, "\x7f", "?" );
3902  }
3903  $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3904  if ( isset( $stuff['deps'] ) ) {
3905  foreach ( $stuff['deps'] as $dep ) {
3906  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3907  if ( $dep['title']->equals( $this->getTitle() ) ) {
3908  // If we transclude ourselves, the final result
3909  // will change based on the new version of the page
3910  $this->mOutput->setFlag( 'vary-revision' );
3911  }
3912  }
3913  }
3914  return array( $text, $finalTitle );
3915  }
3916 
3922  public function fetchTemplate( $title ) {
3923  $rv = $this->fetchTemplateAndTitle( $title );
3924  return $rv[0];
3925  }
3926 
3936  public static function statelessFetchTemplate( $title, $parser = false ) {
3937  $text = $skip = false;
3938  $finalTitle = $title;
3939  $deps = array();
3940 
3941  # Loop to fetch the article, with up to 1 redirect
3942  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3943  # Give extensions a chance to select the revision instead
3944  $id = false; # Assume current
3945  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3946  array( $parser, $title, &$skip, &$id ) );
3947 
3948  if ( $skip ) {
3949  $text = false;
3950  $deps[] = array(
3951  'title' => $title,
3952  'page_id' => $title->getArticleID(),
3953  'rev_id' => null
3954  );
3955  break;
3956  }
3957  # Get the revision
3958  if ( $id ) {
3959  $rev = Revision::newFromId( $id );
3960  } elseif ( $parser ) {
3961  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3962  } else {
3963  $rev = Revision::newFromTitle( $title );
3964  }
3965  $rev_id = $rev ? $rev->getId() : 0;
3966  # If there is no current revision, there is no page
3967  if ( $id === false && !$rev ) {
3968  $linkCache = LinkCache::singleton();
3969  $linkCache->addBadLinkObj( $title );
3970  }
3971 
3972  $deps[] = array(
3973  'title' => $title,
3974  'page_id' => $title->getArticleID(),
3975  'rev_id' => $rev_id );
3976  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3977  # We fetched a rev from a different title; register it too...
3978  $deps[] = array(
3979  'title' => $rev->getTitle(),
3980  'page_id' => $rev->getPage(),
3981  'rev_id' => $rev_id );
3982  }
3983 
3984  if ( $rev ) {
3985  $content = $rev->getContent();
3986  $text = $content ? $content->getWikitextForTransclusion() : null;
3987 
3988  if ( $text === false || $text === null ) {
3989  $text = false;
3990  break;
3991  }
3992  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3994  $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3995  if ( !$message->exists() ) {
3996  $text = false;
3997  break;
3998  }
3999  $content = $message->content();
4000  $text = $message->plain();
4001  } else {
4002  break;
4003  }
4004  if ( !$content ) {
4005  break;
4006  }
4007  # Redirect?
4008  $finalTitle = $title;
4009  $title = $content->getRedirectTarget();
4010  }
4011  return array(
4012  'text' => $text,
4013  'finalTitle' => $finalTitle,
4014  'deps' => $deps );
4015  }
4016 
4024  public function fetchFile( $title, $options = array() ) {
4025  $res = $this->fetchFileAndTitle( $title, $options );
4026  return $res[0];
4027  }
4028 
4036  public function fetchFileAndTitle( $title, $options = array() ) {
4037  $file = $this->fetchFileNoRegister( $title, $options );
4039  $time = $file ? $file->getTimestamp() : false;
4040  $sha1 = $file ? $file->getSha1() : false;
4041  # Register the file as a dependency...
4042  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4043  if ( $file && !$title->equals( $file->getTitle() ) ) {
4044  # Update fetched file title
4045  $title = $file->getTitle();
4046  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4047  }
4048  return array( $file, $title );
4049  }
4050 
4061  protected function fetchFileNoRegister( $title, $options = array() ) {
4062  if ( isset( $options['broken'] ) ) {
4063  $file = false; // broken thumbnail forced by hook
4064  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
4065  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
4066  } else { // get by (name,timestamp)
4067  $file = wfFindFile( $title, $options );
4068  }
4069  return $file;
4070  }
4071 
4080  public function interwikiTransclude( $title, $action ) {
4082 
4083  if ( !$wgEnableScaryTranscluding ) {
4084  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
4085  }
4086 
4087  $url = $title->getFullURL( array( 'action' => $action ) );
4088 
4089  if ( strlen( $url ) > 255 ) {
4090  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
4091  }
4092  return $this->fetchScaryTemplateMaybeFromCache( $url );
4093  }
4094 
4099  public function fetchScaryTemplateMaybeFromCache( $url ) {
4101  $dbr = wfGetDB( DB_SLAVE );
4102  $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
4103  $obj = $dbr->selectRow( 'transcache', array( 'tc_time', 'tc_contents' ),
4104  array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
4105  if ( $obj ) {
4106  return $obj->tc_contents;
4107  }
4108 
4109  $req = MWHttpRequest::factory( $url, array(), __METHOD__ );
4110  $status = $req->execute(); // Status object
4111  if ( $status->isOK() ) {
4112  $text = $req->getContent();
4113  } elseif ( $req->getStatus() != 200 ) {
4114  // Though we failed to fetch the content, this status is useless.
4115  return wfMessage( 'scarytranscludefailed-httpstatus' )
4116  ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
4117  } else {
4118  return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
4119  }
4120 
4121  $dbw = wfGetDB( DB_MASTER );
4122  $dbw->replace( 'transcache', array( 'tc_url' ), array(
4123  'tc_url' => $url,
4124  'tc_time' => $dbw->timestamp( time() ),
4125  'tc_contents' => $text
4126  ) );
4127  return $text;
4128  }
4129 
4139  public function argSubstitution( $piece, $frame ) {
4140 
4141  $error = false;
4142  $parts = $piece['parts'];
4143  $nameWithSpaces = $frame->expand( $piece['title'] );
4144  $argName = trim( $nameWithSpaces );
4145  $object = false;
4146  $text = $frame->getArgument( $argName );
4147  if ( $text === false && $parts->getLength() > 0
4148  && ( $this->ot['html']
4149  || $this->ot['pre']
4150  || ( $this->ot['wiki'] && $frame->isTemplate() )
4151  )
4152  ) {
4153  # No match in frame, use the supplied default
4154  $object = $parts->item( 0 )->getChildren();
4155  }
4156  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
4157  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
4158  $this->limitationWarn( 'post-expand-template-argument' );
4159  }
4160 
4161  if ( $text === false && $object === false ) {
4162  # No match anywhere
4163  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
4164  }
4165  if ( $error !== false ) {
4166  $text .= $error;
4167  }
4168  if ( $object !== false ) {
4169  $ret = array( 'object' => $object );
4170  } else {
4171  $ret = array( 'text' => $text );
4172  }
4173 
4174  return $ret;
4175  }
4176 
4192  public function extensionSubstitution( $params, $frame ) {
4193  $name = $frame->expand( $params['name'] );
4194  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
4195  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4196  $marker = self::MARKER_PREFIX . "-$name-"
4197  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4198 
4199  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4200  ( $this->ot['html'] || $this->ot['pre'] );
4201  if ( $isFunctionTag ) {
4202  $markerType = 'none';
4203  } else {
4204  $markerType = 'general';
4205  }
4206  if ( $this->ot['html'] || $isFunctionTag ) {
4207  $name = strtolower( $name );
4208  $attributes = Sanitizer::decodeTagAttributes( $attrText );
4209  if ( isset( $params['attributes'] ) ) {
4210  $attributes = $attributes + $params['attributes'];
4211  }
4212 
4213  if ( isset( $this->mTagHooks[$name] ) ) {
4214  # Workaround for PHP bug 35229 and similar
4215  if ( !is_callable( $this->mTagHooks[$name] ) ) {
4216  throw new MWException( "Tag hook for $name is not callable\n" );
4217  }
4218  $output = call_user_func_array( $this->mTagHooks[$name],
4219  array( $content, $attributes, $this, $frame ) );
4220  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4221  list( $callback, ) = $this->mFunctionTagHooks[$name];
4222  if ( !is_callable( $callback ) ) {
4223  throw new MWException( "Tag hook for $name is not callable\n" );
4224  }
4225 
4226  $output = call_user_func_array( $callback, array( &$this, $frame, $content, $attributes ) );
4227  } else {
4228  $output = '<span class="error">Invalid tag extension name: ' .
4229  htmlspecialchars( $name ) . '</span>';
4230  }
4231 
4232  if ( is_array( $output ) ) {
4233  # Extract flags to local scope (to override $markerType)
4234  $flags = $output;
4235  $output = $flags[0];
4236  unset( $flags[0] );
4237  extract( $flags );
4238  }
4239  } else {
4240  if ( is_null( $attrText ) ) {
4241  $attrText = '';
4242  }
4243  if ( isset( $params['attributes'] ) ) {
4244  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4245  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4246  htmlspecialchars( $attrValue ) . '"';
4247  }
4248  }
4249  if ( $content === null ) {
4250  $output = "<$name$attrText/>";
4251  } else {
4252  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4253  $output = "<$name$attrText>$content$close";
4254  }
4255  }
4256 
4257  if ( $markerType === 'none' ) {
4258  return $output;
4259  } elseif ( $markerType === 'nowiki' ) {
4260  $this->mStripState->addNoWiki( $marker, $output );
4261  } elseif ( $markerType === 'general' ) {
4262  $this->mStripState->addGeneral( $marker, $output );
4263  } else {
4264  throw new MWException( __METHOD__ . ': invalid marker type' );
4265  }
4266  return $marker;
4267  }
4268 
4276  public function incrementIncludeSize( $type, $size ) {
4277  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4278  return false;
4279  } else {
4280  $this->mIncludeSizes[$type] += $size;
4281  return true;
4282  }
4283  }
4284 
4290  public function incrementExpensiveFunctionCount() {
4291  $this->mExpensiveFunctionCount++;
4292  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4293  }
4294 
4303  public function doDoubleUnderscore( $text ) {
4304 
4305  # The position of __TOC__ needs to be recorded
4306  $mw = MagicWord::get( 'toc' );
4307  if ( $mw->match( $text ) ) {
4308  $this->mShowToc = true;
4309  $this->mForceTocPosition = true;
4310 
4311  # Set a placeholder. At the end we'll fill it in with the TOC.
4312  $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
4313 
4314  # Only keep the first one.
4315  $text = $mw->replace( '', $text );
4316  }
4317 
4318  # Now match and remove the rest of them
4320  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4321 
4322  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4323  $this->mOutput->mNoGallery = true;
4324  }
4325  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4326  $this->mShowToc = false;
4327  }
4328  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4329  && $this->mTitle->getNamespace() == NS_CATEGORY
4330  ) {
4331  $this->addTrackingCategory( 'hidden-category-category' );
4332  }
4333  # (bug 8068) Allow control over whether robots index a page.
4334  #
4335  # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
4336  # is not desirable, the last one on the page should win.
4337  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4338  $this->mOutput->setIndexPolicy( 'noindex' );
4339  $this->addTrackingCategory( 'noindex-category' );
4340  }
4341  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4342  $this->mOutput->setIndexPolicy( 'index' );
4343  $this->addTrackingCategory( 'index-category' );
4344  }
4345 
4346  # Cache all double underscores in the database
4347  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4348  $this->mOutput->setProperty( $key, '' );
4349  }
4350 
4351  return $text;
4352  }
4353 
4359  public function addTrackingCategory( $msg ) {
4360  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4361  }
4362 
4379  public function formatHeadings( $text, $origText, $isMain = true ) {
4381 
4382  # Inhibit editsection links if requested in the page
4383  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4384  $maybeShowEditLink = $showEditLink = false;
4385  } else {
4386  $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4387  $showEditLink = $this->mOptions->getEditSection();
4388  }
4389  if ( $showEditLink ) {
4390  $this->mOutput->setEditSectionTokens( true );
4391  }
4392 
4393  # Get all headlines for numbering them and adding funky stuff like [edit]
4394  # links - this is for later, but we need the number of headlines right now
4395  $matches = array();
4396  $numMatches = preg_match_all(
4397  '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4398  $text,
4399  $matches
4400  );
4401 
4402  # if there are fewer than 4 headlines in the article, do not show TOC
4403  # unless it's been explicitly enabled.
4404  $enoughToc = $this->mShowToc &&
4405  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4406 
4407  # Allow user to stipulate that a page should have a "new section"
4408  # link added via __NEWSECTIONLINK__
4409  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4410  $this->mOutput->setNewSection( true );
4411  }
4412 
4413  # Allow user to remove the "new section"
4414  # link via __NONEWSECTIONLINK__
4415  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4416  $this->mOutput->hideNewSection( true );
4417  }
4418 
4419  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4420  # override above conditions and always show TOC above first header
4421  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4422  $this->mShowToc = true;
4423  $enoughToc = true;
4424  }
4425 
4426  # headline counter
4427  $headlineCount = 0;
4428  $numVisible = 0;
4429 
4430  # Ugh .. the TOC should have neat indentation levels which can be
4431  # passed to the skin functions. These are determined here
4432  $toc = '';
4433  $full = '';
4434  $head = array();
4435  $sublevelCount = array();
4436  $levelCount = array();
4437  $level = 0;
4438  $prevlevel = 0;
4439  $toclevel = 0;
4440  $prevtoclevel = 0;
4441  $markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX;
4442  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4443  $oldType = $this->mOutputType;
4444  $this->setOutputType( self::OT_WIKI );
4445  $frame = $this->getPreprocessor()->newFrame();
4446  $root = $this->preprocessToDom( $origText );
4447  $node = $root->getFirstChild();
4448  $byteOffset = 0;
4449  $tocraw = array();
4450  $refers = array();
4451 
4452  $headlines = $numMatches !== false ? $matches[3] : array();
4453 
4454  foreach ( $headlines as $headline ) {
4455  $isTemplate = false;
4456  $titleText = false;
4457  $sectionIndex = false;
4458  $numbering = '';
4459  $markerMatches = array();
4460  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4461  $serial = $markerMatches[1];
4462  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4463  $isTemplate = ( $titleText != $baseTitleText );
4464  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4465  }
4466 
4467  if ( $toclevel ) {
4468  $prevlevel = $level;
4469  }
4470  $level = $matches[1][$headlineCount];
4471 
4472  if ( $level > $prevlevel ) {
4473  # Increase TOC level
4474  $toclevel++;
4475  $sublevelCount[$toclevel] = 0;
4476  if ( $toclevel < $wgMaxTocLevel ) {
4477  $prevtoclevel = $toclevel;
4478  $toc .= Linker::tocIndent();
4479  $numVisible++;
4480  }
4481  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4482  # Decrease TOC level, find level to jump to
4483 
4484  for ( $i = $toclevel; $i > 0; $i-- ) {
4485  if ( $levelCount[$i] == $level ) {
4486  # Found last matching level
4487  $toclevel = $i;
4488  break;
4489  } elseif ( $levelCount[$i] < $level ) {
4490  # Found first matching level below current level
4491  $toclevel = $i + 1;
4492  break;
4493  }
4494  }
4495  if ( $i == 0 ) {
4496  $toclevel = 1;
4497  }
4498  if ( $toclevel < $wgMaxTocLevel ) {
4499  if ( $prevtoclevel < $wgMaxTocLevel ) {
4500  # Unindent only if the previous toc level was shown :p
4501  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4502  $prevtoclevel = $toclevel;
4503  } else {
4504  $toc .= Linker::tocLineEnd();
4505  }
4506  }
4507  } else {
4508  # No change in level, end TOC line
4509  if ( $toclevel < $wgMaxTocLevel ) {
4510  $toc .= Linker::tocLineEnd();
4511  }
4512  }
4513 
4514  $levelCount[$toclevel] = $level;
4515 
4516  # count number of headlines for each level
4517  $sublevelCount[$toclevel]++;
4518  $dot = 0;
4519  for ( $i = 1; $i <= $toclevel; $i++ ) {
4520  if ( !empty( $sublevelCount[$i] ) ) {
4521  if ( $dot ) {
4522  $numbering .= '.';
4523  }
4524  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4525  $dot = 1;
4526  }
4527  }
4528 
4529  # The safe header is a version of the header text safe to use for links
4530 
4531  # Remove link placeholders by the link text.
4532  # <!--LINK number-->
4533  # turns into
4534  # link text with suffix
4535  # Do this before unstrip since link text can contain strip markers
4536  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4537 
4538  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4539  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4540 
4541  # Strip out HTML (first regex removes any tag not allowed)
4542  # Allowed tags are:
4543  # * <sup> and <sub> (bug 8393)
4544  # * <i> (bug 26375)
4545  # * <b> (r105284)
4546  # * <bdi> (bug 72884)
4547  # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4548  #
4549  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4550  # to allow setting directionality in toc items.
4551  $tocline = preg_replace(
4552  array(
4553  '#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4554  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4555  ),
4556  array( '', '<$1>' ),
4557  $safeHeadline
4558  );
4559 
4560  # Strip '<span></span>', which is the result from the above if
4561  # <span id="foo"></span> is used to produce an additional anchor
4562  # for a section.
4563  $tocline = str_replace( '<span></span>', '', $tocline );
4564 
4565  $tocline = trim( $tocline );
4566 
4567  # For the anchor, strip out HTML-y stuff period
4568  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4569  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4570 
4571  # Save headline for section edit hint before it's escaped
4572  $headlineHint = $safeHeadline;
4573 
4574  if ( $wgExperimentalHtmlIds ) {
4575  # For reverse compatibility, provide an id that's
4576  # HTML4-compatible, like we used to.
4577  #
4578  # It may be worth noting, academically, that it's possible for
4579  # the legacy anchor to conflict with a non-legacy headline
4580  # anchor on the page. In this case likely the "correct" thing
4581  # would be to either drop the legacy anchors or make sure
4582  # they're numbered first. However, this would require people
4583  # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4584  # manually, so let's not bother worrying about it.
4585  $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4586  array( 'noninitial', 'legacy' ) );
4587  $safeHeadline = Sanitizer::escapeId( $safeHeadline );
4588 
4589  if ( $legacyHeadline == $safeHeadline ) {
4590  # No reason to have both (in fact, we can't)
4591  $legacyHeadline = false;
4592  }
4593  } else {
4594  $legacyHeadline = false;
4595  $safeHeadline = Sanitizer::escapeId( $safeHeadline,
4596  'noninitial' );
4597  }
4598 
4599  # HTML names must be case-insensitively unique (bug 10721).
4600  # This does not apply to Unicode characters per
4601  # http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4602  # @todo FIXME: We may be changing them depending on the current locale.
4603  $arrayKey = strtolower( $safeHeadline );
4604  if ( $legacyHeadline === false ) {
4605  $legacyArrayKey = false;
4606  } else {
4607  $legacyArrayKey = strtolower( $legacyHeadline );
4608  }
4609 
4610  # Create the anchor for linking from the TOC to the section
4611  $anchor = $safeHeadline;
4612  $legacyAnchor = $legacyHeadline;
4613  if ( isset( $refers[$arrayKey] ) ) {
4614  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4615  $anchor .= "_$i";
4616  $refers["${arrayKey}_$i"] = true;
4617  } else {
4618  $refers[$arrayKey] = true;
4619  }
4620  if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4621  for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4622  $legacyAnchor .= "_$i";
4623  $refers["${legacyArrayKey}_$i"] = true;
4624  } else {
4625  $refers[$legacyArrayKey] = true;
4626  }
4627 
4628  # Don't number the heading if it is the only one (looks silly)
4629  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4630  # the two are different if the line contains a link
4631  $headline = Html::element(
4632  'span',
4633  array( 'class' => 'mw-headline-number' ),
4634  $numbering
4635  ) . ' ' . $headline;
4636  }
4637 
4638  if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4639  $toc .= Linker::tocLine( $anchor, $tocline,
4640  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4641  }
4642 
4643  # Add the section to the section tree
4644  # Find the DOM node for this header
4645  $noOffset = ( $isTemplate || $sectionIndex === false );
4646  while ( $node && !$noOffset ) {
4647  if ( $node->getName() === 'h' ) {
4648  $bits = $node->splitHeading();
4649  if ( $bits['i'] == $sectionIndex ) {
4650  break;
4651  }
4652  }
4653  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4654  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4655  $node = $node->getNextSibling();
4656  }
4657  $tocraw[] = array(
4658  'toclevel' => $toclevel,
4659  'level' => $level,
4660  'line' => $tocline,
4661  'number' => $numbering,
4662  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4663  'fromtitle' => $titleText,
4664  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4665  'anchor' => $anchor,
4666  );
4667 
4668  # give headline the correct <h#> tag
4669  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4670  // Output edit section links as markers with styles that can be customized by skins
4671  if ( $isTemplate ) {
4672  # Put a T flag in the section identifier, to indicate to extractSections()
4673  # that sections inside <includeonly> should be counted.
4674  $editsectionPage = $titleText;
4675  $editsectionSection = "T-$sectionIndex";
4676  $editsectionContent = null;
4677  } else {
4678  $editsectionPage = $this->mTitle->getPrefixedText();
4679  $editsectionSection = $sectionIndex;
4680  $editsectionContent = $headlineHint;
4681  }
4682  // We use a bit of pesudo-xml for editsection markers. The
4683  // language converter is run later on. Using a UNIQ style marker
4684  // leads to the converter screwing up the tokens when it
4685  // converts stuff. And trying to insert strip tags fails too. At
4686  // this point all real inputted tags have already been escaped,
4687  // so we don't have to worry about a user trying to input one of
4688  // these markers directly. We use a page and section attribute
4689  // to stop the language converter from converting these
4690  // important bits of data, but put the headline hint inside a
4691  // content block because the language converter is supposed to
4692  // be able to convert that piece of data.
4693  // Gets replaced with html in ParserOutput::getText
4694  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4695  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4696  if ( $editsectionContent !== null ) {
4697  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4698  } else {
4699  $editlink .= '/>';
4700  }
4701  } else {
4702  $editlink = '';
4703  }
4704  $head[$headlineCount] = Linker::makeHeadline( $level,
4705  $matches['attrib'][$headlineCount], $anchor, $headline,
4706  $editlink, $legacyAnchor );
4707 
4708  $headlineCount++;
4709  }
4710 
4711  $this->setOutputType( $oldType );
4712 
4713  # Never ever show TOC if no headers
4714  if ( $numVisible < 1 ) {
4715  $enoughToc = false;
4716  }
4717 
4718  if ( $enoughToc ) {
4719  if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4720  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4721  }
4722  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4723  $this->mOutput->setTOCHTML( $toc );
4724  $toc = self::TOC_START . $toc . self::TOC_END;
4725  $this->mOutput->addModules( 'mediawiki.toc' );
4726  }
4727 
4728  if ( $isMain ) {
4729  $this->mOutput->setSections( $tocraw );
4730  }
4731 
4732  # split up and insert constructed headlines
4733  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4734  $i = 0;
4735 
4736  // build an array of document sections
4737  $sections = array();
4738  foreach ( $blocks as $block ) {
4739  // $head is zero-based, sections aren't.
4740  if ( empty( $head[$i - 1] ) ) {
4741  $sections[$i] = $block;
4742  } else {
4743  $sections[$i] = $head[$i - 1] . $block;
4744  }
4745 
4756  Hooks::run( 'ParserSectionCreate', array( $this, $i, &$sections[$i], $showEditLink ) );
4757 
4758  $i++;
4759  }
4760 
4761  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4762  // append the TOC at the beginning
4763  // Top anchor now in skin
4764  $sections[0] = $sections[0] . $toc . "\n";
4765  }
4766 
4767  $full .= join( '', $sections );
4768 
4769  if ( $this->mForceTocPosition ) {
4770  return str_replace( '<!--MWTOC-->', $toc, $full );
4771  } else {
4772  return $full;
4773  }
4774  }
4775 
4787  public function preSaveTransform( $text, Title $title, User $user,
4788  ParserOptions $options, $clearState = true
4789  ) {
4790  if ( $clearState ) {
4791  $magicScopeVariable = $this->lock();
4792  }
4793  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4794  $this->setUser( $user );
4795 
4796  $pairs = array(
4797  "\r\n" => "\n",
4798  "\r" => "\n",
4799  );
4800  $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4801  if ( $options->getPreSaveTransform() ) {
4802  $text = $this->pstPass2( $text, $user );
4803  }
4804  $text = $this->mStripState->unstripBoth( $text );
4805 
4806  $this->setUser( null ); #Reset
4807 
4808  return $text;
4809  }
4810 
4819  private function pstPass2( $text, $user ) {
4821 
4822  # Note: This is the timestamp saved as hardcoded wikitext to
4823  # the database, we use $wgContLang here in order to give
4824  # everyone the same signature and use the default one rather
4825  # than the one selected in each user's preferences.
4826  # (see also bug 12815)
4827  $ts = $this->mOptions->getTimestamp();
4829  $ts = $timestamp->format( 'YmdHis' );
4830  $tzMsg = $timestamp->format( 'T' ); # might vary on DST changeover!
4831 
4832  # Allow translation of timezones through wiki. format() can return
4833  # whatever crap the system uses, localised or not, so we cannot
4834  # ship premade translations.
4835  $key = 'timezone-' . strtolower( trim( $tzMsg ) );
4836  $msg = wfMessage( $key )->inContentLanguage();
4837  if ( $msg->exists() ) {
4838  $tzMsg = $msg->text();
4839  }
4840 
4841  $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4842 
4843  # Variable replacement
4844  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4845  $text = $this->replaceVariables( $text );
4846 
4847  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4848  # which may corrupt this parser instance via its wfMessage()->text() call-
4849 
4850  # Signatures
4851  $sigText = $this->getUserSig( $user );
4852  $text = strtr( $text, array(
4853  '~~~~~' => $d,
4854  '~~~~' => "$sigText $d",
4855  '~~~' => $sigText
4856  ) );
4857 
4858  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4859  $tc = '[' . Title::legalChars() . ']';
4860  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4861 
4862  // [[ns:page (context)|]]
4863  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4864  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4865  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4866  // [[ns:page (context), context|]] (using either single or double-width comma)
4867  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4868  // [[|page]] (reverse pipe trick: add context from page title)
4869  $p2 = "/\[\[\\|($tc+)]]/";
4870 
4871  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4872  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4873  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4874  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4875 
4876  $t = $this->mTitle->getText();
4877  $m = array();
4878  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4879  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4880  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4881  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4882  } else {
4883  # if there's no context, don't bother duplicating the title
4884  $text = preg_replace( $p2, '[[\\1]]', $text );
4885  }
4886 
4887  # Trim trailing whitespace
4888  $text = rtrim( $text );
4889 
4890  return $text;
4891  }
4892 
4907  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4909 
4910  $username = $user->getName();
4911 
4912  # If not given, retrieve from the user object.
4913  if ( $nickname === false ) {
4914  $nickname = $user->getOption( 'nickname' );
4915  }
4916 
4917  if ( is_null( $fancySig ) ) {
4918  $fancySig = $user->getBoolOption( 'fancysig' );
4919  }
4920 
4921  $nickname = $nickname == null ? $username : $nickname;
4922 
4923  if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4924  $nickname = $username;
4925  wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4926  } elseif ( $fancySig !== false ) {
4927  # Sig. might contain markup; validate this
4928  if ( $this->validateSig( $nickname ) !== false ) {
4929  # Validated; clean up (if needed) and return it
4930  return $this->cleanSig( $nickname, true );
4931  } else {
4932  # Failed to validate; fall back to the default
4933  $nickname = $username;
4934  wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4935  }
4936  }
4937 
4938  # Make sure nickname doesnt get a sig in a sig
4939  $nickname = self::cleanSigInSig( $nickname );
4940 
4941  # If we're still here, make it a link to the user page
4942  $userText = wfEscapeWikiText( $username );
4943  $nickText = wfEscapeWikiText( $nickname );
4944  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4945 
4946  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4947  ->title( $this->getTitle() )->text();
4948  }
4949 
4956  public function validateSig( $text ) {
4957  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4958  }
4959 
4970  public function cleanSig( $text, $parsing = false ) {
4971  if ( !$parsing ) {
4972  global $wgTitle;
4973  $magicScopeVariable = $this->lock();
4974  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4975  }
4976 
4977  # Option to disable this feature
4978  if ( !$this->mOptions->getCleanSignatures() ) {
4979  return $text;
4980  }
4981 
4982  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4983  # => Move this logic to braceSubstitution()
4984  $substWord = MagicWord::get( 'subst' );
4985  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4986  $substText = '{{' . $substWord->getSynonym( 0 );
4987 
4988  $text = preg_replace( $substRegex, $substText, $text );
4989  $text = self::cleanSigInSig( $text );
4990  $dom = $this->preprocessToDom( $text );
4991  $frame = $this->getPreprocessor()->newFrame();
4992  $text = $frame->expand( $dom );
4993 
4994  if ( !$parsing ) {
4995  $text = $this->mStripState->unstripBoth( $text );
4996  }
4997 
4998  return $text;
4999  }
5000 
5007  public static function cleanSigInSig( $text ) {
5008  $text = preg_replace( '/~{3,5}/', '', $text );
5009  return $text;
5010  }
5011 
5021  public function startExternalParse( Title $title = null, ParserOptions $options,
5022  $outputType, $clearState = true
5023  ) {
5024  $this->startParse( $title, $options, $outputType, $clearState );
5025  }
5026 
5033  private function startParse( Title $title = null, ParserOptions $options,
5034  $outputType, $clearState = true
5035  ) {
5036  $this->setTitle( $title );
5037  $this->mOptions = $options;
5038  $this->setOutputType( $outputType );
5039  if ( $clearState ) {
5040  $this->clearState();
5041  }
5042  }
5043 
5052  public function transformMsg( $text, $options, $title = null ) {
5053  static $executing = false;
5054 
5055  # Guard against infinite recursion
5056  if ( $executing ) {
5057  return $text;
5058  }
5059  $executing = true;
5060 
5061  if ( !$title ) {
5062  global $wgTitle;
5063  $title = $wgTitle;
5064  }
5065 
5066  $text = $this->preprocess( $text, $title, $options );
5067 
5068  $executing = false;
5069  return $text;
5070  }
5071 
5096  public function setHook( $tag, $callback ) {
5097  $tag = strtolower( $tag );
5098  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5099  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5100  }
5101  $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
5102  $this->mTagHooks[$tag] = $callback;
5103  if ( !in_array( $tag, $this->mStripList ) ) {
5104  $this->mStripList[] = $tag;
5105  }
5106 
5107  return $oldVal;
5108  }
5109 
5127  public function setTransparentTagHook( $tag, $callback ) {
5128  $tag = strtolower( $tag );
5129  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5130  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5131  }
5132  $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
5133  $this->mTransparentTagHooks[$tag] = $callback;
5134 
5135  return $oldVal;
5136  }
5137 
5141  public function clearTagHooks() {
5142  $this->mTagHooks = array();
5143  $this->mFunctionTagHooks = array();
5144  $this->mStripList = $this->mDefaultStripList;
5145  }
5146 
5190  public function setFunctionHook( $id, $callback, $flags = 0 ) {
5192 
5193  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5194  $this->mFunctionHooks[$id] = array( $callback, $flags );
5195 
5196  # Add to function cache
5197  $mw = MagicWord::get( $id );
5198  if ( !$mw ) {
5199  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5200  }
5201 
5202  $synonyms = $mw->getSynonyms();
5203  $sensitive = intval( $mw->isCaseSensitive() );
5204 
5205  foreach ( $synonyms as $syn ) {
5206  # Case
5207  if ( !$sensitive ) {
5208  $syn = $wgContLang->lc( $syn );
5209  }
5210  # Add leading hash
5211  if ( !( $flags & self::SFH_NO_HASH ) ) {
5212  $syn = '#' . $syn;
5213  }
5214  # Remove trailing colon
5215  if ( substr( $syn, -1, 1 ) === ':' ) {
5216  $syn = substr( $syn, 0, -1 );
5217  }
5218  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5219  }
5220  return $oldVal;
5221  }
5222 
5228  public function getFunctionHooks() {
5229  return array_keys( $this->mFunctionHooks );
5230  }
5231 
5242  public function setFunctionTagHook( $tag, $callback, $flags ) {
5243  $tag = strtolower( $tag );
5244  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5245  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5246  }
5247  $old = isset( $this->mFunctionTagHooks[$tag] ) ?
5248  $this->mFunctionTagHooks[$tag] : null;
5249  $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
5250 
5251  if ( !in_array( $tag, $this->mStripList ) ) {
5252  $this->mStripList[] = $tag;
5253  }
5255  return $old;
5256  }
5257 
5266  public function replaceLinkHolders( &$text, $options = 0 ) {
5267  $this->mLinkHolders->replace( $text );
5268  }
5269 
5277  public function replaceLinkHoldersText( $text ) {
5278  return $this->mLinkHolders->replaceText( $text );
5279  }
5280 
5294  public function renderImageGallery( $text, $params ) {
5295 
5296  $mode = false;
5297  if ( isset( $params['mode'] ) ) {
5298  $mode = $params['mode'];
5299  }
5300 
5301  try {
5302  $ig = ImageGalleryBase::factory( $mode );
5303  } catch ( Exception $e ) {
5304  // If invalid type set, fallback to default.
5305  $ig = ImageGalleryBase::factory( false );
5306  }
5307 
5308  $ig->setContextTitle( $this->mTitle );
5309  $ig->setShowBytes( false );
5310  $ig->setShowFilename( false );
5311  $ig->setParser( $this );
5312  $ig->setHideBadImages();
5313  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
5314 
5315  if ( isset( $params['showfilename'] ) ) {
5316  $ig->setShowFilename( true );
5317  } else {
5318  $ig->setShowFilename( false );
5319  }
5320  if ( isset( $params['caption'] ) ) {
5321  $caption = $params['caption'];
5322  $caption = htmlspecialchars( $caption );
5323  $caption = $this->replaceInternalLinks( $caption );
5324  $ig->setCaptionHtml( $caption );
5325  }
5326  if ( isset( $params['perrow'] ) ) {
5327  $ig->setPerRow( $params['perrow'] );
5328  }
5329  if ( isset( $params['widths'] ) ) {
5330  $ig->setWidths( $params['widths'] );
5331  }
5332  if ( isset( $params['heights'] ) ) {
5333  $ig->setHeights( $params['heights'] );
5334  }
5335  $ig->setAdditionalOptions( $params );
5336 
5337  Hooks::run( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
5338 
5339  $lines = StringUtils::explode( "\n", $text );
5340  foreach ( $lines as $line ) {
5341  # match lines like these:
5342  # Image:someimage.jpg|This is some image
5343  $matches = array();
5344  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5345  # Skip empty lines
5346  if ( count( $matches ) == 0 ) {
5347  continue;
5348  }
5349 
5350  if ( strpos( $matches[0], '%' ) !== false ) {
5351  $matches[1] = rawurldecode( $matches[1] );
5352  }
5353  $title = Title::newFromText( $matches[1], NS_FILE );
5354  if ( is_null( $title ) ) {
5355  # Bogus title. Ignore these so we don't bomb out later.
5356  continue;
5357  }
5358 
5359  # We need to get what handler the file uses, to figure out parameters.
5360  # Note, a hook can overide the file name, and chose an entirely different
5361  # file (which potentially could be of a different type and have different handler).
5362  $options = array();
5363  $descQuery = false;
5364  Hooks::run( 'BeforeParserFetchFileAndTitle',
5365  array( $this, $title, &$options, &$descQuery ) );
5366  # Don't register it now, as ImageGallery does that later.
5367  $file = $this->fetchFileNoRegister( $title, $options );
5368  $handler = $file ? $file->getHandler() : false;
5369 
5370  $paramMap = array(
5371  'img_alt' => 'gallery-internal-alt',
5372  'img_link' => 'gallery-internal-link',
5373  );
5374  if ( $handler ) {
5375  $paramMap = $paramMap + $handler->getParamMap();
5376  // We don't want people to specify per-image widths.
5377  // Additionally the width parameter would need special casing anyhow.
5378  unset( $paramMap['img_width'] );
5379  }
5380 
5381  $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5382 
5383  $label = '';
5384  $alt = '';
5385  $link = '';
5386  $handlerOptions = array();
5387  if ( isset( $matches[3] ) ) {
5388  // look for an |alt= definition while trying not to break existing
5389  // captions with multiple pipes (|) in it, until a more sensible grammar
5390  // is defined for images in galleries
5391 
5392  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5393  // splitting on '|' is a bit odd, and different from makeImage.
5394  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5395  $parameterMatches = StringUtils::explode( '|', $matches[3] );
5396 
5397  foreach ( $parameterMatches as $parameterMatch ) {
5398  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5399  if ( $magicName ) {
5400  $paramName = $paramMap[$magicName];
5401 
5402  switch ( $paramName ) {
5403  case 'gallery-internal-alt':
5404  $alt = $this->stripAltText( $match, false );
5405  break;
5406  case 'gallery-internal-link':
5407  $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5408  $chars = self::EXT_LINK_URL_CLASS;
5409  $prots = $this->mUrlProtocols;
5410  //check to see if link matches an absolute url, if not then it must be a wiki link.
5411  if ( preg_match( "/^($prots)$chars+$/u", $linkValue ) ) {
5412  $link = $linkValue;
5413  } else {
5414  $localLinkTitle = Title::newFromText( $linkValue );
5415  if ( $localLinkTitle !== null ) {
5416  $link = $localLinkTitle->getLinkURL();
5417  }
5418  }
5419  break;
5420  default:
5421  // Must be a handler specific parameter.
5422  if ( $handler->validateParam( $paramName, $match ) ) {
5423  $handlerOptions[$paramName] = $match;
5424  } else {
5425  // Guess not. Append it to the caption.
5426  wfDebug( "$parameterMatch failed parameter validation\n" );
5427  $label .= '|' . $parameterMatch;
5428  }
5429  }
5430 
5431  } else {
5432  // concatenate all other pipes
5433  $label .= '|' . $parameterMatch;
5434  }
5435  }
5436  // remove the first pipe
5437  $label = substr( $label, 1 );
5438  }
5439 
5440  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5441  }
5442  $html = $ig->toHTML();
5443  Hooks::run( 'AfterParserFetchFileAndTitle', array( $this, $ig, &$html ) );
5444  return $html;
5445  }
5446 
5451  public function getImageParams( $handler ) {
5452  if ( $handler ) {
5453  $handlerClass = get_class( $handler );
5454  } else {
5455  $handlerClass = '';
5456  }
5457  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5458  # Initialise static lists
5459  static $internalParamNames = array(
5460  'horizAlign' => array( 'left', 'right', 'center', 'none' ),
5461  'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5462  'bottom', 'text-bottom' ),
5463  'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
5464  'upright', 'border', 'link', 'alt', 'class' ),
5465  );
5466  static $internalParamMap;
5467  if ( !$internalParamMap ) {
5468  $internalParamMap = array();
5469  foreach ( $internalParamNames as $type => $names ) {
5470  foreach ( $names as $name ) {
5471  $magicName = str_replace( '-', '_', "img_$name" );
5472  $internalParamMap[$magicName] = array( $type, $name );
5473  }
5474  }
5475  }
5477  # Add handler params
5478  $paramMap = $internalParamMap;
5479  if ( $handler ) {
5480  $handlerParamMap = $handler->getParamMap();
5481  foreach ( $handlerParamMap as $magic => $paramName ) {
5482  $paramMap[$magic] = array( 'handler', $paramName );
5483  }
5484  }
5485  $this->mImageParams[$handlerClass] = $paramMap;
5486  $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5487  }
5488  return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
5489  }
5490 
5499  public function makeImage( $title, $options, $holders = false ) {
5500  # Check if the options text is of the form "options|alt text"
5501  # Options are:
5502  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5503  # * left no resizing, just left align. label is used for alt= only
5504  # * right same, but right aligned
5505  # * none same, but not aligned
5506  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5507  # * center center the image
5508  # * frame Keep original image size, no magnify-button.
5509  # * framed Same as "frame"
5510  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5511  # * upright reduce width for upright images, rounded to full __0 px
5512  # * border draw a 1px border around the image
5513  # * alt Text for HTML alt attribute (defaults to empty)
5514  # * class Set a class for img node
5515  # * link Set the target of the image link. Can be external, interwiki, or local
5516  # vertical-align values (no % or length right now):
5517  # * baseline
5518  # * sub
5519  # * super
5520  # * top
5521  # * text-top
5522  # * middle
5523  # * bottom
5524  # * text-bottom
5525 
5526  $parts = StringUtils::explode( "|", $options );
5527 
5528  # Give extensions a chance to select the file revision for us
5529  $options = array();
5530  $descQuery = false;
5531  Hooks::run( 'BeforeParserFetchFileAndTitle',
5532  array( $this, $title, &$options, &$descQuery ) );
5533  # Fetch and register the file (file title may be different via hooks)
5534  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5535 
5536  # Get parameter map
5537  $handler = $file ? $file->getHandler() : false;
5538 
5539  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5540 
5541  if ( !$file ) {
5542  $this->addTrackingCategory( 'broken-file-category' );
5543  }
5544 
5545  # Process the input parameters
5546  $caption = '';
5547  $params = array( 'frame' => array(), 'handler' => array(),
5548  'horizAlign' => array(), 'vertAlign' => array() );
5549  $seenformat = false;
5550  foreach ( $parts as $part ) {
5551  $part = trim( $part );
5552  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5553  $validated = false;
5554  if ( isset( $paramMap[$magicName] ) ) {
5555  list( $type, $paramName ) = $paramMap[$magicName];
5556 
5557  # Special case; width and height come in one variable together
5558  if ( $type === 'handler' && $paramName === 'width' ) {
5559  $parsedWidthParam = $this->parseWidthParam( $value );
5560  if ( isset( $parsedWidthParam['width'] ) ) {
5561  $width = $parsedWidthParam['width'];
5562  if ( $handler->validateParam( 'width', $width ) ) {
5563  $params[$type]['width'] = $width;
5564  $validated = true;
5565  }
5566  }
5567  if ( isset( $parsedWidthParam['height'] ) ) {
5568  $height = $parsedWidthParam['height'];
5569  if ( $handler->validateParam( 'height', $height ) ) {
5570  $params[$type]['height'] = $height;
5571  $validated = true;
5572  }
5573  }
5574  # else no validation -- bug 13436
5575  } else {
5576  if ( $type === 'handler' ) {
5577  # Validate handler parameter
5578  $validated = $handler->validateParam( $paramName, $value );
5579  } else {
5580  # Validate internal parameters
5581  switch ( $paramName ) {
5582  case 'manualthumb':
5583  case 'alt':
5584  case 'class':
5585  # @todo FIXME: Possibly check validity here for
5586  # manualthumb? downstream behavior seems odd with
5587  # missing manual thumbs.
5588  $validated = true;
5589  $value = $this->stripAltText( $value, $holders );
5590  break;
5591  case 'link':
5592  $chars = self::EXT_LINK_URL_CLASS;
5593  $prots = $this->mUrlProtocols;
5594  if ( $value === '' ) {
5595  $paramName = 'no-link';
5596  $value = true;
5597  $validated = true;
5598  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5599  if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) {
5600  $paramName = 'link-url';
5601  $this->mOutput->addExternalLink( $value );
5602  if ( $this->mOptions->getExternalLinkTarget() ) {
5603  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5604  }
5605  $validated = true;
5606  }
5607  } else {
5608  $linkTitle = Title::newFromText( $value );
5609  if ( $linkTitle ) {
5610  $paramName = 'link-title';
5611  $value = $linkTitle;
5612  $this->mOutput->addLink( $linkTitle );
5613  $validated = true;
5614  }
5615  }
5616  break;
5617  case 'frameless':
5618  case 'framed':
5619  case 'thumbnail':
5620  // use first appearing option, discard others.
5621  $validated = ! $seenformat;
5622  $seenformat = true;
5623  break;
5624  default:
5625  # Most other things appear to be empty or numeric...
5626  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5627  }
5628  }
5629 
5630  if ( $validated ) {
5631  $params[$type][$paramName] = $value;
5632  }
5633  }
5634  }
5635  if ( !$validated ) {
5636  $caption = $part;
5637  }
5638  }
5639 
5640  # Process alignment parameters
5641  if ( $params['horizAlign'] ) {
5642  $params['frame']['align'] = key( $params['horizAlign'] );
5643  }
5644  if ( $params['vertAlign'] ) {
5645  $params['frame']['valign'] = key( $params['vertAlign'] );
5646  }
5647 
5648  $params['frame']['caption'] = $caption;
5649 
5650  # Will the image be presented in a frame, with the caption below?
5651  $imageIsFramed = isset( $params['frame']['frame'] )
5652  || isset( $params['frame']['framed'] )
5653  || isset( $params['frame']['thumbnail'] )
5654  || isset( $params['frame']['manualthumb'] );
5655 
5656  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5657  # came to also set the caption, ordinary text after the image -- which
5658  # makes no sense, because that just repeats the text multiple times in
5659  # screen readers. It *also* came to set the title attribute.
5660  #
5661  # Now that we have an alt attribute, we should not set the alt text to
5662  # equal the caption: that's worse than useless, it just repeats the
5663  # text. This is the framed/thumbnail case. If there's no caption, we
5664  # use the unnamed parameter for alt text as well, just for the time be-
5665  # ing, if the unnamed param is set and the alt param is not.
5666  #
5667  # For the future, we need to figure out if we want to tweak this more,
5668  # e.g., introducing a title= parameter for the title; ignoring the un-
5669  # named parameter entirely for images without a caption; adding an ex-
5670  # plicit caption= parameter and preserving the old magic unnamed para-
5671  # meter for BC; ...
5672  if ( $imageIsFramed ) { # Framed image
5673  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5674  # No caption or alt text, add the filename as the alt text so
5675  # that screen readers at least get some description of the image
5676  $params['frame']['alt'] = $title->getText();
5677  }
5678  # Do not set $params['frame']['title'] because tooltips don't make sense
5679  # for framed images
5680  } else { # Inline image
5681  if ( !isset( $params['frame']['alt'] ) ) {
5682  # No alt text, use the "caption" for the alt text
5683  if ( $caption !== '' ) {
5684  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5685  } else {
5686  # No caption, fall back to using the filename for the
5687  # alt text
5688  $params['frame']['alt'] = $title->getText();
5689  }
5690  }
5691  # Use the "caption" for the tooltip text
5692  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5693  }
5694 
5695  Hooks::run( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) );
5696 
5697  # Linker does the rest
5698  $time = isset( $options['time'] ) ? $options['time'] : false;
5699  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5700  $time, $descQuery, $this->mOptions->getThumbSize() );
5701 
5702  # Give the handler a chance to modify the parser object
5703  if ( $handler ) {
5704  $handler->parserTransformHook( $this, $file );
5705  }
5706 
5707  return $ret;
5708  }
5709 
5715  protected function stripAltText( $caption, $holders ) {
5716  # Strip bad stuff out of the title (tooltip). We can't just use
5717  # replaceLinkHoldersText() here, because if this function is called
5718  # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5719  if ( $holders ) {
5720  $tooltip = $holders->replaceText( $caption );
5721  } else {
5722  $tooltip = $this->replaceLinkHoldersText( $caption );
5723  }
5724 
5725  # make sure there are no placeholders in thumbnail attributes
5726  # that are later expanded to html- so expand them now and
5727  # remove the tags
5728  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5729  $tooltip = Sanitizer::stripAllTags( $tooltip );
5730 
5731  return $tooltip;
5732  }
5738  public function disableCache() {
5739  wfDebug( "Parser output marked as uncacheable.\n" );
5740  if ( !$this->mOutput ) {
5741  throw new MWException( __METHOD__ .
5742  " can only be called when actually parsing something" );
5743  }
5744  $this->mOutput->setCacheTime( -1 ); // old style, for compatibility
5745  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5746  }
5747 
5756  public function attributeStripCallback( &$text, $frame = false ) {
5757  $text = $this->replaceVariables( $text, $frame );
5758  $text = $this->mStripState->unstripBoth( $text );
5759  return $text;
5760  }
5761 
5767  public function getTags() {
5768  return array_merge(
5769  array_keys( $this->mTransparentTagHooks ),
5770  array_keys( $this->mTagHooks ),
5771  array_keys( $this->mFunctionTagHooks )
5772  );
5773  }
5774 
5785  public function replaceTransparentTags( $text ) {
5786  $matches = array();
5787  $elements = array_keys( $this->mTransparentTagHooks );
5788  $text = self::extractTagsAndParams( $elements, $text, $matches );
5789  $replacements = array();
5790 
5791  foreach ( $matches as $marker => $data ) {
5792  list( $element, $content, $params, $tag ) = $data;
5793  $tagName = strtolower( $element );
5794  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5795  $output = call_user_func_array(
5796  $this->mTransparentTagHooks[$tagName],
5797  array( $content, $params, $this )
5798  );
5799  } else {
5800  $output = $tag;
5801  }
5802  $replacements[$marker] = $output;
5803  }
5804  return strtr( $text, $replacements );
5805  }
5806 
5836  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5837  global $wgTitle; # not generally used but removes an ugly failure mode
5838 
5839  $magicScopeVariable = $this->lock();
5840  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5841  $outText = '';
5842  $frame = $this->getPreprocessor()->newFrame();
5843 
5844  # Process section extraction flags
5845  $flags = 0;
5846  $sectionParts = explode( '-', $sectionId );
5847  $sectionIndex = array_pop( $sectionParts );
5848  foreach ( $sectionParts as $part ) {
5849  if ( $part === 'T' ) {
5850  $flags |= self::PTD_FOR_INCLUSION;
5851  }
5852  }
5853 
5854  # Check for empty input
5855  if ( strval( $text ) === '' ) {
5856  # Only sections 0 and T-0 exist in an empty document
5857  if ( $sectionIndex == 0 ) {
5858  if ( $mode === 'get' ) {
5859  return '';
5860  } else {
5861  return $newText;
5862  }
5863  } else {
5864  if ( $mode === 'get' ) {
5865  return $newText;
5866  } else {
5867  return $text;
5868  }
5869  }
5870  }
5871 
5872  # Preprocess the text
5873  $root = $this->preprocessToDom( $text, $flags );
5874 
5875  # <h> nodes indicate section breaks
5876  # They can only occur at the top level, so we can find them by iterating the root's children
5877  $node = $root->getFirstChild();
5878 
5879  # Find the target section
5880  if ( $sectionIndex == 0 ) {
5881  # Section zero doesn't nest, level=big
5882  $targetLevel = 1000;
5883  } else {
5884  while ( $node ) {
5885  if ( $node->getName() === 'h' ) {
5886  $bits = $node->splitHeading();
5887  if ( $bits['i'] == $sectionIndex ) {
5888  $targetLevel = $bits['level'];
5889  break;
5890  }
5891  }
5892  if ( $mode === 'replace' ) {
5893  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5894  }
5895  $node = $node->getNextSibling();
5896  }
5897  }
5898 
5899  if ( !$node ) {
5900  # Not found
5901  if ( $mode === 'get' ) {
5902  return $newText;
5903  } else {
5904  return $text;
5905  }
5906  }
5907 
5908  # Find the end of the section, including nested sections
5909  do {
5910  if ( $node->getName() === 'h' ) {
5911  $bits = $node->splitHeading();
5912  $curLevel = $bits['level'];
5913  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5914  break;
5915  }
5916  }
5917  if ( $mode === 'get' ) {
5918  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5919  }
5920  $node = $node->getNextSibling();
5921  } while ( $node );
5922 
5923  # Write out the remainder (in replace mode only)
5924  if ( $mode === 'replace' ) {
5925  # Output the replacement text
5926  # Add two newlines on -- trailing whitespace in $newText is conventionally
5927  # stripped by the editor, so we need both newlines to restore the paragraph gap
5928  # Only add trailing whitespace if there is newText
5929  if ( $newText != "" ) {
5930  $outText .= $newText . "\n\n";
5931  }
5932 
5933  while ( $node ) {
5934  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5935  $node = $node->getNextSibling();
5936  }
5937  }
5939  if ( is_string( $outText ) ) {
5940  # Re-insert stripped tags
5941  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5942  }
5943 
5944  return $outText;
5945  }
5946 
5961  public function getSection( $text, $sectionId, $defaultText = '' ) {
5962  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5963  }
5964 
5977  public function replaceSection( $oldText, $sectionId, $newText ) {
5978  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5979  }
5980 
5986  public function getRevisionId() {
5987  return $this->mRevisionId;
5988  }
5989 
5996  public function getRevisionObject() {
5997  if ( !is_null( $this->mRevisionObject ) ) {
5998  return $this->mRevisionObject;
5999  }
6000  if ( is_null( $this->mRevisionId ) ) {
6001  return null;
6002  }
6003 
6004  $rev = call_user_func(
6005  $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
6006  );
6007 
6008  # If the parse is for a new revision, then the callback should have
6009  # already been set to force the object and should match mRevisionId.
6010  # If not, try to fetch by mRevisionId for sanity.
6011  if ( $rev && $rev->getId() != $this->mRevisionId ) {
6012  $rev = Revision::newFromId( $this->mRevisionId );
6013  }
6014 
6015  $this->mRevisionObject = $rev;
6016 
6017  return $this->mRevisionObject;
6018  }
6019 
6025  public function getRevisionTimestamp() {
6026  if ( is_null( $this->mRevisionTimestamp ) ) {
6028 
6029  $revObject = $this->getRevisionObject();
6030  $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
6031 
6032  # The cryptic '' timezone parameter tells to use the site-default
6033  # timezone offset instead of the user settings.
6034  #
6035  # Since this value will be saved into the parser cache, served
6036  # to other users, and potentially even used inside links and such,
6037  # it needs to be consistent for all visitors.
6038  $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
6039 
6040  }
6041  return $this->mRevisionTimestamp;
6042  }
6043 
6049  public function getRevisionUser() {
6050  if ( is_null( $this->mRevisionUser ) ) {
6051  $revObject = $this->getRevisionObject();
6052 
6053  # if this template is subst: the revision id will be blank,
6054  # so just use the current user's name
6055  if ( $revObject ) {
6056  $this->mRevisionUser = $revObject->getUserText();
6057  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6058  $this->mRevisionUser = $this->getUser()->getName();
6059  }
6060  }
6061  return $this->mRevisionUser;
6062  }
6063 
6069  public function getRevisionSize() {
6070  if ( is_null( $this->mRevisionSize ) ) {
6071  $revObject = $this->getRevisionObject();
6072 
6073  # if this variable is subst: the revision id will be blank,
6074  # so just use the parser input size, because the own substituation
6075  # will change the size.
6076  if ( $revObject ) {
6077  $this->mRevisionSize = $revObject->getSize();
6078  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6079  $this->mRevisionSize = $this->mInputSize;
6080  }
6081  }
6082  return $this->mRevisionSize;
6083  }
6084 
6090  public function setDefaultSort( $sort ) {
6091  $this->mDefaultSort = $sort;
6092  $this->mOutput->setProperty( 'defaultsort', $sort );
6093  }
6094 
6105  public function getDefaultSort() {
6106  if ( $this->mDefaultSort !== false ) {
6107  return $this->mDefaultSort;
6108  } else {
6109  return '';
6110  }
6111  }
6112 
6119  public function getCustomDefaultSort() {
6120  return $this->mDefaultSort;
6121  }
6122 
6132  public function guessSectionNameFromWikiText( $text ) {
6133  # Strip out wikitext links(they break the anchor)
6134  $text = $this->stripSectionName( $text );
6136  return '#' . Sanitizer::escapeId( $text, 'noninitial' );
6137  }
6138 
6147  public function guessLegacySectionNameFromWikiText( $text ) {
6148  # Strip out wikitext links(they break the anchor)
6149  $text = $this->stripSectionName( $text );
6151  return '#' . Sanitizer::escapeId( $text, array( 'noninitial', 'legacy' ) );
6152  }
6153 
6168  public function stripSectionName( $text ) {
6169  # Strip internal link markup
6170  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6171  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6172 
6173  # Strip external link markup
6174  # @todo FIXME: Not tolerant to blank link text
6175  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6176  # on how many empty links there are on the page - need to figure that out.
6177  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6178 
6179  # Parse wikitext quotes (italics & bold)
6180  $text = $this->doQuotes( $text );
6181 
6182  # Strip HTML tags
6183  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6184  return $text;
6185  }
6186 
6197  public function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) {
6198  $magicScopeVariable = $this->lock();
6199  $this->startParse( $title, $options, $outputType, true );
6201  $text = $this->replaceVariables( $text );
6202  $text = $this->mStripState->unstripBoth( $text );
6203  $text = Sanitizer::removeHTMLtags( $text );
6204  return $text;
6205  }
6206 
6213  public function testPst( $text, Title $title, ParserOptions $options ) {
6214  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6215  }
6216 
6223  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6224  return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6225  }
6226 
6243  public function markerSkipCallback( $s, $callback ) {
6244  $i = 0;
6245  $out = '';
6246  while ( $i < strlen( $s ) ) {
6247  $markerStart = strpos( $s, self::MARKER_PREFIX, $i );
6248  if ( $markerStart === false ) {
6249  $out .= call_user_func( $callback, substr( $s, $i ) );
6250  break;
6251  } else {
6252  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6253  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6254  if ( $markerEnd === false ) {
6255  $out .= substr( $s, $markerStart );
6256  break;
6257  } else {
6258  $markerEnd += strlen( self::MARKER_SUFFIX );
6259  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6260  $i = $markerEnd;
6261  }
6262  }
6263  }
6264  return $out;
6265  }
6266 
6273  public function killMarkers( $text ) {
6274  return $this->mStripState->killMarkers( $text );
6275  }
6276 
6293  public function serializeHalfParsedText( $text ) {
6294  $data = array(
6295  'text' => $text,
6296  'version' => self::HALF_PARSED_VERSION,
6297  'stripState' => $this->mStripState->getSubState( $text ),
6298  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6299  );
6300  return $data;
6301  }
6302 
6318  public function unserializeHalfParsedText( $data ) {
6319  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6320  throw new MWException( __METHOD__ . ': invalid version' );
6321  }
6322 
6323  # First, extract the strip state.
6324  $texts = array( $data['text'] );
6325  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6326 
6327  # Now renumber links
6328  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6329 
6330  # Should be good to go.
6331  return $texts[0];
6332  }
6333 
6343  public function isValidHalfParsedText( $data ) {
6344  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6345  }
6346 
6355  public function parseWidthParam( $value ) {
6356  $parsedWidthParam = array();
6357  if ( $value === '' ) {
6358  return $parsedWidthParam;
6359  }
6360  $m = array();
6361  # (bug 13500) In both cases (width/height and width only),
6362  # permit trailing "px" for backward compatibility.
6363  if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6364  $width = intval( $m[1] );
6365  $height = intval( $m[2] );
6366  $parsedWidthParam['width'] = $width;
6367  $parsedWidthParam['height'] = $height;
6368  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6369  $width = intval( $value );
6370  $parsedWidthParam['width'] = $width;
6371  }
6372  return $parsedWidthParam;
6373  }
6374 
6384  protected function lock() {
6385  if ( $this->mInParse ) {
6386  throw new MWException( "Parser state cleared while parsing. "
6387  . "Did you call Parser::parse recursively?" );
6388  }
6389  $this->mInParse = true;
6390 
6391  $that = $this;
6392  $recursiveCheck = new ScopedCallback( function() use ( $that ) {
6393  $that->mInParse = false;
6394  } );
6395 
6396  return $recursiveCheck;
6397  }
6398 
6409  public static function stripOuterParagraph( $html ) {
6410  $m = array();
6411  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6412  if ( strpos( $m[1], '</p>' ) === false ) {
6413  $html = $m[1];
6414  }
6415  }
6416 
6417  return $html;
6418  }
6419 
6430  public function getFreshParser() {
6432  if ( $this->mInParse ) {
6433  return new $wgParserConf['class']( $wgParserConf );
6434  } else {
6435  return $this;
6436  }
6437  }
6438 }
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:1988
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:2487
null means default in associative array form
Definition: hooks.txt:1740
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:1740
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1685
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:1696
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:2628
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 Hook subscribers can return false to omit this line from recentchanges 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:2266
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:1726
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:1843
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:1503
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:1870
User $mUser
Definition: Parser.php:183
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers ...
Definition: Parser.php:3213
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:1740
=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:2477
replaceLinkHolders(&$text, $options=0)
Definition: Parser.php:5243
static activeUsers()
Definition: SiteStats.php:164
$mLinkID
Definition: Parser.php:175
doQuotes($text)
Helper function for doAllQuotes()
Definition: Parser.php:1536
preprocessToDom($text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:3243
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:3362
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:2489
$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:2002
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:1565
maybeDoSubpageLink($target, &$text)
Handle link to subpage if necessary.
Definition: Parser.php:2356
$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:2510
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:1770
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:1975
$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:1186
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:1502
$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:5692
doAllQuotes($text)
Replace single quotes with HTML markup.
Definition: Parser.php:1519
static normalizeUrlComponent($component, $unsafe)
Definition: Parser.php:1893
isAnon()
Get whether the user is anonymous.
Definition: User.php:3161
if($limit) $timestamp
you don t have to do a grep find to see where the $wgReverseTitle variable is used
Definition: hooks.txt:117
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:3288
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:5010
$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:1751
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:4356
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:1654
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:1668
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:1826
__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:1740
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:2334
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:1539
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:1939
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:1916
getOption($oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2569
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition: Parser.php:2343
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.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2283
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:1805
static & singleton()
Get an instance of this class.
Definition: LinkCache.php:61
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:5762
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:4280
$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: