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 
117  # Marker Suffix needs to be accessible staticly.
118  const MARKER_SUFFIX = "-QINU\x7f";
119 
120  # Markers used for wrapping the table of contents
121  const TOC_START = '<mw:toc>';
122  const TOC_END = '</mw:toc>';
123 
124  # Persistent:
125  public $mTagHooks = array();
127  public $mFunctionHooks = array();
128  public $mFunctionSynonyms = array( 0 => array(), 1 => array() );
130  public $mStripList = array();
132  public $mVarCache = array();
133  public $mImageParams = array();
135  public $mMarkerIndex = 0;
136  public $mFirstCall = true;
137 
138  # Initialised by initialiseVariables()
139 
143  public $mVariables;
148  public $mSubstWords;
149  # Initialised in constructor
151 
152  # Initialized in getPreprocessor()
155 
156  # Cleared with clearState():
160  public $mOutput;
162 
166  public $mStripState;
172  public $mLinkHolders;
173 
174  public $mLinkID;
176  public $mDefaultSort;
178  public $mExpensiveFunctionCount; # number of expensive parser function calls
184  public $mUser; # User object; only used when doing pre-save transform
186  # Temporary
187  # These are variables reset at least once per parse regardless of $clearState
192  public $mOptions;
193 
197  public $mTitle; # Title context, used for self-link rendering and similar things
198  public $mOutputType; # Output type, one of the OT_xxx constants
199  public $ot; # Shortcut alias, see setOutputType()
200  public $mRevisionObject; # The revision object of the specified revision ID
201  public $mRevisionId; # ID to display in {{REVISIONID}} tags
202  public $mRevisionTimestamp; # The timestamp of the specified revision ID
203  public $mRevisionUser; # User to display in {{REVISIONUSER}} tag
204  public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable
205  public $mRevIdForTs; # The revision ID which was used to fetch the timestamp
206  public $mInputSize = false; # For {{PAGESIZE}} on current page.
207 
211  public $mUniqPrefix;
212 
218  public $mLangLinkLanguages;
219 
226  public $currentRevisionCache;
227 
232  public $mInParse = false;
233 
235  protected $mProfiler;
236 
240  public function __construct( $conf = array() ) {
241  $this->mConf = $conf;
242  $this->mUrlProtocols = wfUrlProtocols();
243  $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
244  self::EXT_LINK_URL_CLASS . '+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su';
245  if ( isset( $conf['preprocessorClass'] ) ) {
246  $this->mPreprocessorClass = $conf['preprocessorClass'];
247  } elseif ( defined( 'HPHP_VERSION' ) ) {
248  # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
249  $this->mPreprocessorClass = 'Preprocessor_Hash';
250  } elseif ( extension_loaded( 'domxml' ) ) {
251  # PECL extension that conflicts with the core DOM extension (bug 13770)
252  wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
253  $this->mPreprocessorClass = 'Preprocessor_Hash';
254  } elseif ( extension_loaded( 'dom' ) ) {
255  $this->mPreprocessorClass = 'Preprocessor_DOM';
256  } else {
257  $this->mPreprocessorClass = 'Preprocessor_Hash';
258  }
259  wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
260  }
261 
265  public function __destruct() {
266  if ( isset( $this->mLinkHolders ) ) {
267  unset( $this->mLinkHolders );
268  }
269  foreach ( $this as $name => $value ) {
270  unset( $this->$name );
271  }
272  }
273 
277  public function __clone() {
278  $this->mInParse = false;
279 
280  // Bug 56226: When you create a reference "to" an object field, that
281  // makes the object field itself be a reference too (until the other
282  // reference goes out of scope). When cloning, any field that's a
283  // reference is copied as a reference in the new object. Both of these
284  // are defined PHP5 behaviors, as inconvenient as it is for us when old
285  // hooks from PHP4 days are passing fields by reference.
286  foreach ( array( 'mStripState', 'mVarCache' ) as $k ) {
287  // Make a non-reference copy of the field, then rebind the field to
288  // reference the new copy.
289  $tmp = $this->$k;
290  $this->$k =& $tmp;
291  unset( $tmp );
292  }
293 
294  Hooks::run( 'ParserCloned', array( $this ) );
295  }
296 
300  public function firstCallInit() {
301  if ( !$this->mFirstCall ) {
302  return;
303  }
304  $this->mFirstCall = false;
305 
307  CoreTagHooks::register( $this );
308  $this->initialiseVariables();
309 
310  Hooks::run( 'ParserFirstCallInit', array( &$this ) );
311  }
312 
318  public function clearState() {
319  if ( $this->mFirstCall ) {
320  $this->firstCallInit();
321  }
322  $this->mOutput = new ParserOutput;
323  $this->mOptions->registerWatcher( array( $this->mOutput, 'recordOption' ) );
324  $this->mAutonumber = 0;
325  $this->mLastSection = '';
326  $this->mDTopen = false;
327  $this->mIncludeCount = array();
328  $this->mArgStack = false;
329  $this->mInPre = false;
330  $this->mLinkHolders = new LinkHolderArray( $this );
331  $this->mLinkID = 0;
332  $this->mRevisionObject = $this->mRevisionTimestamp =
333  $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null;
334  $this->mVarCache = array();
335  $this->mUser = null;
336  $this->mLangLinkLanguages = array();
337  $this->currentRevisionCache = null;
338 
349  $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
350  $this->mStripState = new StripState( $this->mUniqPrefix );
351 
352  # Clear these on every parse, bug 4549
353  $this->mTplRedirCache = $this->mTplDomCache = array();
354 
355  $this->mShowToc = true;
356  $this->mForceTocPosition = false;
357  $this->mIncludeSizes = array(
358  'post-expand' => 0,
359  'arg' => 0,
360  );
361  $this->mPPNodeCount = 0;
362  $this->mGeneratedPPNodeCount = 0;
363  $this->mHighestExpansionDepth = 0;
364  $this->mDefaultSort = false;
365  $this->mHeadings = array();
366  $this->mDoubleUnderscores = array();
367  $this->mExpensiveFunctionCount = 0;
368 
369  # Fix cloning
370  if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
371  $this->mPreprocessor = null;
372  }
373 
374  $this->mProfiler = new SectionProfiler();
375 
376  Hooks::run( 'ParserClearState', array( &$this ) );
377  }
378 
391  public function parse( $text, Title $title, ParserOptions $options,
392  $linestart = true, $clearState = true, $revid = null
393  ) {
400 
401  if ( $clearState ) {
402  $magicScopeVariable = $this->lock();
403  }
404 
405  $this->startParse( $title, $options, self::OT_HTML, $clearState );
406 
407  $this->currentRevisionCache = null;
408  $this->mInputSize = strlen( $text );
409  if ( $this->mOptions->getEnableLimitReport() ) {
410  $this->mOutput->resetParseStartTime();
411  }
412 
413  # Remove the strip marker tag prefix from the input, if present.
414  if ( $clearState ) {
415  $text = str_replace( $this->mUniqPrefix, '', $text );
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  }
563 
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 
690  public static function getRandomString() {
691  return wfRandomString( 16 );
692  }
693 
700  public function setUser( $user ) {
701  $this->mUser = $user;
702  }
709  public function uniqPrefix() {
710  if ( !isset( $this->mUniqPrefix ) ) {
711  # @todo FIXME: This is probably *horribly wrong*
712  # LanguageConverter seems to want $wgParser's uniqPrefix, however
713  # if this is called for a parser cache hit, the parser may not
714  # have ever been initialized in the first place.
715  # Not really sure what the heck is supposed to be going on here.
716  return '';
717  # throw new MWException( "Accessing uninitialized mUniqPrefix" );
718  }
719  return $this->mUniqPrefix;
720  }
721 
727  public function setTitle( $t ) {
728  if ( !$t ) {
729  $t = Title::newFromText( 'NO TITLE' );
730  }
731 
732  if ( $t->hasFragment() ) {
733  # Strip the fragment to avoid various odd effects
734  $this->mTitle = clone $t;
735  $this->mTitle->setFragment( '' );
736  } else {
737  $this->mTitle = $t;
738  }
739  }
740 
746  public function getTitle() {
747  return $this->mTitle;
748  }
749 
756  public function Title( $x = null ) {
757  return wfSetVar( $this->mTitle, $x );
758  }
759 
765  public function setOutputType( $ot ) {
766  $this->mOutputType = $ot;
767  # Shortcut alias
768  $this->ot = array(
769  'html' => $ot == self::OT_HTML,
770  'wiki' => $ot == self::OT_WIKI,
771  'pre' => $ot == self::OT_PREPROCESS,
772  'plain' => $ot == self::OT_PLAIN,
773  );
774  }
775 
782  public function OutputType( $x = null ) {
783  return wfSetVar( $this->mOutputType, $x );
784  }
785 
791  public function getOutput() {
792  return $this->mOutput;
793  }
794 
800  public function getOptions() {
801  return $this->mOptions;
802  }
803 
810  public function Options( $x = null ) {
811  return wfSetVar( $this->mOptions, $x );
812  }
813 
817  public function nextLinkID() {
818  return $this->mLinkID++;
819  }
820 
824  public function setLinkID( $id ) {
825  $this->mLinkID = $id;
826  }
827 
832  public function getFunctionLang() {
833  return $this->getTargetLanguage();
834  }
835 
845  public function getTargetLanguage() {
846  $target = $this->mOptions->getTargetLanguage();
847 
848  if ( $target !== null ) {
849  return $target;
850  } elseif ( $this->mOptions->getInterfaceMessage() ) {
851  return $this->mOptions->getUserLangObj();
852  } elseif ( is_null( $this->mTitle ) ) {
853  throw new MWException( __METHOD__ . ': $this->mTitle is null' );
854  }
855 
856  return $this->mTitle->getPageLanguage();
857  }
858 
863  public function getConverterLanguage() {
864  return $this->getTargetLanguage();
865  }
866 
873  public function getUser() {
874  if ( !is_null( $this->mUser ) ) {
875  return $this->mUser;
876  }
877  return $this->mOptions->getUser();
878  }
879 
885  public function getPreprocessor() {
886  if ( !isset( $this->mPreprocessor ) ) {
887  $class = $this->mPreprocessorClass;
888  $this->mPreprocessor = new $class( $this );
889  }
890  return $this->mPreprocessor;
891  }
892 
913  public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = '' ) {
914  static $n = 1;
915  $stripped = '';
916  $matches = array();
917 
918  $taglist = implode( '|', $elements );
919  $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
920 
921  while ( $text != '' ) {
922  $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
923  $stripped .= $p[0];
924  if ( count( $p ) < 5 ) {
925  break;
926  }
927  if ( count( $p ) > 5 ) {
928  # comment
929  $element = $p[4];
930  $attributes = '';
931  $close = '';
932  $inside = $p[5];
933  } else {
934  # tag
935  $element = $p[1];
936  $attributes = $p[2];
937  $close = $p[3];
938  $inside = $p[4];
939  }
940 
941  $marker = "$uniq_prefix-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
942  $stripped .= $marker;
943 
944  if ( $close === '/>' ) {
945  # Empty element tag, <tag />
946  $content = null;
947  $text = $inside;
948  $tail = null;
949  } else {
950  if ( $element === '!--' ) {
951  $end = '/(-->)/';
952  } else {
953  $end = "/(<\\/$element\\s*>)/i";
954  }
955  $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
956  $content = $q[0];
957  if ( count( $q ) < 3 ) {
958  # No end tag -- let it run out to the end of the text.
959  $tail = '';
960  $text = '';
961  } else {
962  $tail = $q[1];
963  $text = $q[2];
964  }
965  }
966 
967  $matches[$marker] = array( $element,
968  $content,
970  "<$element$attributes$close$content$tail" );
971  }
972  return $stripped;
973  }
974 
980  public function getStripList() {
981  return $this->mStripList;
982  }
993  public function insertStripItem( $text ) {
994  $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
995  $this->mMarkerIndex++;
996  $this->mStripState->addGeneral( $rnd, $text );
997  return $rnd;
998  }
999 
1007  public function doTableStuff( $text ) {
1008 
1009  $lines = StringUtils::explode( "\n", $text );
1010  $out = '';
1011  $td_history = array(); # Is currently a td tag open?
1012  $last_tag_history = array(); # Save history of last lag activated (td, th or caption)
1013  $tr_history = array(); # Is currently a tr tag open?
1014  $tr_attributes = array(); # history of tr attributes
1015  $has_opened_tr = array(); # Did this table open a <tr> element?
1016  $indent_level = 0; # indent level of the table
1017 
1018  foreach ( $lines as $outLine ) {
1019  $line = trim( $outLine );
1020 
1021  if ( $line === '' ) { # empty line, go to next line
1022  $out .= $outLine . "\n";
1023  continue;
1024  }
1025 
1026  $first_character = $line[0];
1027  $matches = array();
1028 
1029  if ( preg_match( '/^(:*)\{\|(.*)$/', $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 ( substr( $line, 0, 2 ) === '|}' ) {
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 ( substr( $line, 0, 2 ) === '|-' ) {
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  || substr( $line, 0, 2 ) === '|+'
1094  ) {
1095  # This might be cell elements, td, th or captions
1096  if ( substr( $line, 0, 2 ) === '|+' ) {
1097  $first_character = '+';
1098  $line = substr( $line, 1 );
1099  }
1100 
1101  $line = substr( $line, 1 );
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( $this->mUniqPrefix . '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  }
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  }
1988 
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  $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
2151  if ( $nt === null ) {
2152  $s .= $prefix . '[[' . $line;
2153  continue;
2154  }
2155 
2156  $ns = $nt->getNamespace();
2157  $iw = $nt->getInterwiki();
2158 
2159  if ( $might_be_img ) { # if this is actually an invalid link
2160  if ( $ns == NS_FILE && $noforce ) { # but might be an image
2161  $found = false;
2162  while ( true ) {
2163  # look at the next 'line' to see if we can close it there
2164  $a->next();
2165  $next_line = $a->current();
2166  if ( $next_line === false || $next_line === null ) {
2167  break;
2168  }
2169  $m = explode( ']]', $next_line, 3 );
2170  if ( count( $m ) == 3 ) {
2171  # the first ]] closes the inner link, the second the image
2172  $found = true;
2173  $text .= "[[{$m[0]}]]{$m[1]}";
2174  $trail = $m[2];
2175  break;
2176  } elseif ( count( $m ) == 2 ) {
2177  # if there's exactly one ]] that's fine, we'll keep looking
2178  $text .= "[[{$m[0]}]]{$m[1]}";
2179  } else {
2180  # if $next_line is invalid too, we need look no further
2181  $text .= '[[' . $next_line;
2182  break;
2183  }
2184  }
2185  if ( !$found ) {
2186  # we couldn't find the end of this imageLink, so output it raw
2187  # but don't ignore what might be perfectly normal links in the text we've examined
2188  $holders->merge( $this->replaceInternalLinks2( $text ) );
2189  $s .= "{$prefix}[[$link|$text";
2190  # note: no $trail, because without an end, there *is* no trail
2191  continue;
2192  }
2193  } else { # it's not an image, so output it raw
2194  $s .= "{$prefix}[[$link|$text";
2195  # note: no $trail, because without an end, there *is* no trail
2196  continue;
2197  }
2198  }
2199 
2200  $wasblank = ( $text == '' );
2201  if ( $wasblank ) {
2202  $text = $link;
2203  } else {
2204  # Bug 4598 madness. Handle the quotes only if they come from the alternate part
2205  # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a>
2206  # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']]
2207  # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
2208  $text = $this->doQuotes( $text );
2209  }
2210 
2211  # Link not escaped by : , create the various objects
2212  if ( $noforce && !$nt->wasLocalInterwiki() ) {
2213  # Interwikis
2214  if (
2215  $iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
2216  Language::fetchLanguageName( $iw, null, 'mw' ) ||
2217  in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
2218  )
2219  ) {
2220  # Bug 24502: filter duplicates
2221  if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
2222  $this->mLangLinkLanguages[$iw] = true;
2223  $this->mOutput->addLanguageLink( $nt->getFullText() );
2224  }
2225 
2226  $s = rtrim( $s . $prefix );
2227  $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
2228  continue;
2229  }
2230 
2231  if ( $ns == NS_FILE ) {
2232  if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
2233  if ( $wasblank ) {
2234  # if no parameters were passed, $text
2235  # becomes something like "File:Foo.png",
2236  # which we don't want to pass on to the
2237  # image generator
2238  $text = '';
2239  } else {
2240  # recursively parse links inside the image caption
2241  # actually, this will parse them in any other parameters, too,
2242  # but it might be hard to fix that, and it doesn't matter ATM
2243  $text = $this->replaceExternalLinks( $text );
2244  $holders->merge( $this->replaceInternalLinks2( $text ) );
2245  }
2246  # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
2247  $s .= $prefix . $this->armorLinks(
2248  $this->makeImage( $nt, $text, $holders ) ) . $trail;
2249  } else {
2250  $s .= $prefix . $trail;
2251  }
2252  continue;
2253  }
2254 
2255  if ( $ns == NS_CATEGORY ) {
2256  $s = rtrim( $s . "\n" ); # bug 87
2257 
2258  if ( $wasblank ) {
2259  $sortkey = $this->getDefaultSort();
2260  } else {
2261  $sortkey = $text;
2262  }
2263  $sortkey = Sanitizer::decodeCharReferences( $sortkey );
2264  $sortkey = str_replace( "\n", '', $sortkey );
2265  $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
2266  $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
2267 
2271  $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail;
2272 
2273  continue;
2274  }
2275  }
2276 
2277  # Self-link checking. For some languages, variants of the title are checked in
2278  # LinkHolderArray::doVariants() to allow batching the existence checks necessary
2279  # for linking to a different variant.
2280  if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) {
2281  $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
2282  continue;
2283  }
2284 
2285  # NS_MEDIA is a pseudo-namespace for linking directly to a file
2286  # @todo FIXME: Should do batch file existence checks, see comment below
2287  if ( $ns == NS_MEDIA ) {
2288  # Give extensions a chance to select the file revision for us
2289  $options = array();
2290  $descQuery = false;
2291  Hooks::run( 'BeforeParserFetchFileAndTitle',
2292  array( $this, $nt, &$options, &$descQuery ) );
2293  # Fetch and register the file (file title may be different via hooks)
2294  list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options );
2295  # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
2296  $s .= $prefix . $this->armorLinks(
2297  Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
2298  continue;
2299  }
2300 
2301  # Some titles, such as valid special pages or files in foreign repos, should
2302  # be shown as bluelinks even though they're not included in the page table
2303  #
2304  # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do
2305  # batch file existence checks for NS_FILE and NS_MEDIA
2306  if ( $iw == '' && $nt->isAlwaysKnown() ) {
2307  $this->mOutput->addLink( $nt );
2308  $s .= $this->makeKnownLinkHolder( $nt, $text, array(), $trail, $prefix );
2309  } else {
2310  # Links will be added to the output link list after checking
2311  $s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix );
2312  }
2313  }
2314  return $holders;
2315  }
2316 
2331  public function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
2332  list( $inside, $trail ) = Linker::splitTrail( $trail );
2333 
2334  if ( is_string( $query ) ) {
2335  $query = wfCgiToArray( $query );
2336  }
2337  if ( $text == '' ) {
2338  $text = htmlspecialchars( $nt->getPrefixedText() );
2339  }
2340 
2341  $link = Linker::linkKnown( $nt, "$prefix$text$inside", array(), $query );
2342 
2343  return $this->armorLinks( $link ) . $trail;
2344  }
2345 
2356  public function armorLinks( $text ) {
2357  return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/',
2358  "{$this->mUniqPrefix}NOPARSE$1", $text );
2359  }
2360 
2365  public function areSubpagesAllowed() {
2366  # Some namespaces don't allow subpages
2367  return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
2368  }
2369 
2378  public function maybeDoSubpageLink( $target, &$text ) {
2379  return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
2380  }
2381 
2388  public function closeParagraph() {
2389  $result = '';
2390  if ( $this->mLastSection != '' ) {
2391  $result = '</' . $this->mLastSection . ">\n";
2392  }
2393  $this->mInPre = false;
2394  $this->mLastSection = '';
2395  return $result;
2396  }
2397 
2408  public function getCommon( $st1, $st2 ) {
2409  $fl = strlen( $st1 );
2410  $shorter = strlen( $st2 );
2411  if ( $fl < $shorter ) {
2412  $shorter = $fl;
2413  }
2414 
2415  for ( $i = 0; $i < $shorter; ++$i ) {
2416  if ( $st1[$i] != $st2[$i] ) {
2417  break;
2418  }
2419  }
2420  return $i;
2421  }
2422 
2432  public function openList( $char ) {
2433  $result = $this->closeParagraph();
2435  if ( '*' === $char ) {
2436  $result .= "<ul><li>";
2437  } elseif ( '#' === $char ) {
2438  $result .= "<ol><li>";
2439  } elseif ( ':' === $char ) {
2440  $result .= "<dl><dd>";
2441  } elseif ( ';' === $char ) {
2442  $result .= "<dl><dt>";
2443  $this->mDTopen = true;
2444  } else {
2445  $result = '<!-- ERR 1 -->';
2446  }
2447 
2448  return $result;
2449  }
2450 
2458  public function nextItem( $char ) {
2459  if ( '*' === $char || '#' === $char ) {
2460  return "</li>\n<li>";
2461  } elseif ( ':' === $char || ';' === $char ) {
2462  $close = "</dd>\n";
2463  if ( $this->mDTopen ) {
2464  $close = "</dt>\n";
2465  }
2466  if ( ';' === $char ) {
2467  $this->mDTopen = true;
2468  return $close . '<dt>';
2469  } else {
2470  $this->mDTopen = false;
2471  return $close . '<dd>';
2472  }
2473  }
2474  return '<!-- ERR 2 -->';
2475  }
2476 
2484  public function closeList( $char ) {
2485  if ( '*' === $char ) {
2486  $text = "</li></ul>";
2487  } elseif ( '#' === $char ) {
2488  $text = "</li></ol>";
2489  } elseif ( ':' === $char ) {
2490  if ( $this->mDTopen ) {
2491  $this->mDTopen = false;
2492  $text = "</dt></dl>";
2493  } else {
2494  $text = "</dd></dl>";
2495  }
2496  } else {
2497  return '<!-- ERR 3 -->';
2498  }
2499  return $text;
2500  }
2511  public function doBlockLevels( $text, $linestart ) {
2512 
2513  # Parsing through the text line by line. The main thing
2514  # happening here is handling of block-level elements p, pre,
2515  # and making lists from lines starting with * # : etc.
2516  #
2517  $textLines = StringUtils::explode( "\n", $text );
2518 
2519  $lastPrefix = $output = '';
2520  $this->mDTopen = $inBlockElem = false;
2521  $prefixLength = 0;
2522  $paragraphStack = false;
2523  $inBlockquote = false;
2524 
2525  foreach ( $textLines as $oLine ) {
2526  # Fix up $linestart
2527  if ( !$linestart ) {
2528  $output .= $oLine;
2529  $linestart = true;
2530  continue;
2531  }
2532  # * = ul
2533  # # = ol
2534  # ; = dt
2535  # : = dd
2536 
2537  $lastPrefixLength = strlen( $lastPrefix );
2538  $preCloseMatch = preg_match( '/<\\/pre/i', $oLine );
2539  $preOpenMatch = preg_match( '/<pre/i', $oLine );
2540  # If not in a <pre> element, scan for and figure out what prefixes are there.
2541  if ( !$this->mInPre ) {
2542  # Multiple prefixes may abut each other for nested lists.
2543  $prefixLength = strspn( $oLine, '*#:;' );
2544  $prefix = substr( $oLine, 0, $prefixLength );
2545 
2546  # eh?
2547  # ; and : are both from definition-lists, so they're equivalent
2548  # for the purposes of determining whether or not we need to open/close
2549  # elements.
2550  $prefix2 = str_replace( ';', ':', $prefix );
2551  $t = substr( $oLine, $prefixLength );
2552  $this->mInPre = (bool)$preOpenMatch;
2553  } else {
2554  # Don't interpret any other prefixes in preformatted text
2555  $prefixLength = 0;
2556  $prefix = $prefix2 = '';
2557  $t = $oLine;
2558  }
2559 
2560  # List generation
2561  if ( $prefixLength && $lastPrefix === $prefix2 ) {
2562  # Same as the last item, so no need to deal with nesting or opening stuff
2563  $output .= $this->nextItem( substr( $prefix, -1 ) );
2564  $paragraphStack = false;
2565 
2566  if ( substr( $prefix, -1 ) === ';' ) {
2567  # The one nasty exception: definition lists work like this:
2568  # ; title : definition text
2569  # So we check for : in the remainder text to split up the
2570  # title and definition, without b0rking links.
2571  $term = $t2 = '';
2572  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2573  $t = $t2;
2574  $output .= $term . $this->nextItem( ':' );
2575  }
2576  }
2577  } elseif ( $prefixLength || $lastPrefixLength ) {
2578  # We need to open or close prefixes, or both.
2579 
2580  # Either open or close a level...
2581  $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
2582  $paragraphStack = false;
2583 
2584  # Close all the prefixes which aren't shared.
2585  while ( $commonPrefixLength < $lastPrefixLength ) {
2586  $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] );
2587  --$lastPrefixLength;
2588  }
2589 
2590  # Continue the current prefix if appropriate.
2591  if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
2592  $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] );
2593  }
2594 
2595  # Open prefixes where appropriate.
2596  if ( $lastPrefix && $prefixLength > $commonPrefixLength ) {
2597  $output .= "\n";
2598  }
2599  while ( $prefixLength > $commonPrefixLength ) {
2600  $char = substr( $prefix, $commonPrefixLength, 1 );
2601  $output .= $this->openList( $char );
2602 
2603  if ( ';' === $char ) {
2604  # @todo FIXME: This is dupe of code above
2605  if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
2606  $t = $t2;
2607  $output .= $term . $this->nextItem( ':' );
2608  }
2609  }
2610  ++$commonPrefixLength;
2611  }
2612  if ( !$prefixLength && $lastPrefix ) {
2613  $output .= "\n";
2614  }
2615  $lastPrefix = $prefix2;
2616  }
2617 
2618  # If we have no prefixes, go to paragraph mode.
2619  if ( 0 == $prefixLength ) {
2620  # No prefix (not in list)--go to paragraph mode
2621  # XXX: use a stack for nestable elements like span, table and div
2622  $openmatch = preg_match(
2623  '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|'
2624  . '<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS',
2625  $t
2626  );
2627  $closematch = preg_match(
2628  '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'
2629  . '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|'
2630  . $this->mUniqPrefix
2631  . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS',
2632  $t
2633  );
2634 
2635  if ( $openmatch || $closematch ) {
2636  $paragraphStack = false;
2637  # @todo bug 5718: paragraph closed
2638  $output .= $this->closeParagraph();
2639  if ( $preOpenMatch && !$preCloseMatch ) {
2640  $this->mInPre = true;
2641  }
2642  $bqOffset = 0;
2643  while ( preg_match( '/<(\\/?)blockquote[\s>]/i', $t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) {
2644  $inBlockquote = !$bqMatch[1][0]; // is this a close tag?
2645  $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] );
2646  }
2647  $inBlockElem = !$closematch;
2648  } elseif ( !$inBlockElem && !$this->mInPre ) {
2649  if ( ' ' == substr( $t, 0, 1 )
2650  && ( $this->mLastSection === 'pre' || trim( $t ) != '' )
2651  && !$inBlockquote
2652  ) {
2653  # pre
2654  if ( $this->mLastSection !== 'pre' ) {
2655  $paragraphStack = false;
2656  $output .= $this->closeParagraph() . '<pre>';
2657  $this->mLastSection = 'pre';
2658  }
2659  $t = substr( $t, 1 );
2660  } else {
2661  # paragraph
2662  if ( trim( $t ) === '' ) {
2663  if ( $paragraphStack ) {
2664  $output .= $paragraphStack . '<br />';
2665  $paragraphStack = false;
2666  $this->mLastSection = 'p';
2667  } else {
2668  if ( $this->mLastSection !== 'p' ) {
2669  $output .= $this->closeParagraph();
2670  $this->mLastSection = '';
2671  $paragraphStack = '<p>';
2672  } else {
2673  $paragraphStack = '</p><p>';
2674  }
2675  }
2676  } else {
2677  if ( $paragraphStack ) {
2678  $output .= $paragraphStack;
2679  $paragraphStack = false;
2680  $this->mLastSection = 'p';
2681  } elseif ( $this->mLastSection !== 'p' ) {
2682  $output .= $this->closeParagraph() . '<p>';
2683  $this->mLastSection = 'p';
2684  }
2685  }
2686  }
2687  }
2688  }
2689  # somewhere above we forget to get out of pre block (bug 785)
2690  if ( $preCloseMatch && $this->mInPre ) {
2691  $this->mInPre = false;
2692  }
2693  if ( $paragraphStack === false ) {
2694  $output .= $t;
2695  if ( $prefixLength === 0 ) {
2696  $output .= "\n";
2697  }
2698  }
2699  }
2700  while ( $prefixLength ) {
2701  $output .= $this->closeList( $prefix2[$prefixLength - 1] );
2702  --$prefixLength;
2703  if ( !$prefixLength ) {
2704  $output .= "\n";
2705  }
2706  }
2707  if ( $this->mLastSection != '' ) {
2708  $output .= '</' . $this->mLastSection . '>';
2709  $this->mLastSection = '';
2710  }
2711 
2712  return $output;
2713  }
2714 
2725  public function findColonNoLinks( $str, &$before, &$after ) {
2726 
2727  $pos = strpos( $str, ':' );
2728  if ( $pos === false ) {
2729  # Nothing to find!
2730  return false;
2731  }
2732 
2733  $lt = strpos( $str, '<' );
2734  if ( $lt === false || $lt > $pos ) {
2735  # Easy; no tag nesting to worry about
2736  $before = substr( $str, 0, $pos );
2737  $after = substr( $str, $pos + 1 );
2738  return $pos;
2739  }
2740 
2741  # Ugly state machine to walk through avoiding tags.
2742  $state = self::COLON_STATE_TEXT;
2743  $stack = 0;
2744  $len = strlen( $str );
2745  for ( $i = 0; $i < $len; $i++ ) {
2746  $c = $str[$i];
2747 
2748  switch ( $state ) {
2749  # (Using the number is a performance hack for common cases)
2750  case 0: # self::COLON_STATE_TEXT:
2751  switch ( $c ) {
2752  case "<":
2753  # Could be either a <start> tag or an </end> tag
2754  $state = self::COLON_STATE_TAGSTART;
2755  break;
2756  case ":":
2757  if ( $stack == 0 ) {
2758  # We found it!
2759  $before = substr( $str, 0, $i );
2760  $after = substr( $str, $i + 1 );
2761  return $i;
2762  }
2763  # Embedded in a tag; don't break it.
2764  break;
2765  default:
2766  # Skip ahead looking for something interesting
2767  $colon = strpos( $str, ':', $i );
2768  if ( $colon === false ) {
2769  # Nothing else interesting
2770  return false;
2771  }
2772  $lt = strpos( $str, '<', $i );
2773  if ( $stack === 0 ) {
2774  if ( $lt === false || $colon < $lt ) {
2775  # We found it!
2776  $before = substr( $str, 0, $colon );
2777  $after = substr( $str, $colon + 1 );
2778  return $i;
2779  }
2780  }
2781  if ( $lt === false ) {
2782  # Nothing else interesting to find; abort!
2783  # We're nested, but there's no close tags left. Abort!
2784  break 2;
2785  }
2786  # Skip ahead to next tag start
2787  $i = $lt;
2788  $state = self::COLON_STATE_TAGSTART;
2789  }
2790  break;
2791  case 1: # self::COLON_STATE_TAG:
2792  # In a <tag>
2793  switch ( $c ) {
2794  case ">":
2795  $stack++;
2796  $state = self::COLON_STATE_TEXT;
2797  break;
2798  case "/":
2799  # Slash may be followed by >?
2800  $state = self::COLON_STATE_TAGSLASH;
2801  break;
2802  default:
2803  # ignore
2804  }
2805  break;
2806  case 2: # self::COLON_STATE_TAGSTART:
2807  switch ( $c ) {
2808  case "/":
2809  $state = self::COLON_STATE_CLOSETAG;
2810  break;
2811  case "!":
2812  $state = self::COLON_STATE_COMMENT;
2813  break;
2814  case ">":
2815  # Illegal early close? This shouldn't happen D:
2816  $state = self::COLON_STATE_TEXT;
2817  break;
2818  default:
2819  $state = self::COLON_STATE_TAG;
2820  }
2821  break;
2822  case 3: # self::COLON_STATE_CLOSETAG:
2823  # In a </tag>
2824  if ( $c === ">" ) {
2825  $stack--;
2826  if ( $stack < 0 ) {
2827  wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" );
2828  return false;
2829  }
2830  $state = self::COLON_STATE_TEXT;
2831  }
2832  break;
2833  case self::COLON_STATE_TAGSLASH:
2834  if ( $c === ">" ) {
2835  # Yes, a self-closed tag <blah/>
2836  $state = self::COLON_STATE_TEXT;
2837  } else {
2838  # Probably we're jumping the gun, and this is an attribute
2839  $state = self::COLON_STATE_TAG;
2840  }
2841  break;
2842  case 5: # self::COLON_STATE_COMMENT:
2843  if ( $c === "-" ) {
2844  $state = self::COLON_STATE_COMMENTDASH;
2845  }
2846  break;
2847  case self::COLON_STATE_COMMENTDASH:
2848  if ( $c === "-" ) {
2849  $state = self::COLON_STATE_COMMENTDASHDASH;
2850  } else {
2851  $state = self::COLON_STATE_COMMENT;
2852  }
2853  break;
2854  case self::COLON_STATE_COMMENTDASHDASH:
2855  if ( $c === ">" ) {
2856  $state = self::COLON_STATE_TEXT;
2857  } else {
2858  $state = self::COLON_STATE_COMMENT;
2859  }
2860  break;
2861  default:
2862  throw new MWException( "State machine error in " . __METHOD__ );
2863  }
2864  }
2865  if ( $stack > 0 ) {
2866  wfDebug( __METHOD__ . ": Invalid input; not enough close tags (stack $stack, state $state)\n" );
2867  return false;
2868  }
2869  return false;
2870  }
2871 
2883  public function getVariableValue( $index, $frame = false ) {
2886 
2887  if ( is_null( $this->mTitle ) ) {
2888  // If no title set, bad things are going to happen
2889  // later. Title should always be set since this
2890  // should only be called in the middle of a parse
2891  // operation (but the unit-tests do funky stuff)
2892  throw new MWException( __METHOD__ . ' Should only be '
2893  . ' called while parsing (no title set)' );
2894  }
2895 
2900  if ( Hooks::run( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
2901  if ( isset( $this->mVarCache[$index] ) ) {
2902  return $this->mVarCache[$index];
2903  }
2904  }
2905 
2906  $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
2907  Hooks::run( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
2908 
2909  $pageLang = $this->getFunctionLang();
2910 
2911  switch ( $index ) {
2912  case '!':
2913  $value = '|';
2914  break;
2915  case 'currentmonth':
2916  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
2917  break;
2918  case 'currentmonth1':
2919  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2920  break;
2921  case 'currentmonthname':
2922  $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2923  break;
2924  case 'currentmonthnamegen':
2925  $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2926  break;
2927  case 'currentmonthabbrev':
2928  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
2929  break;
2930  case 'currentday':
2931  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
2932  break;
2933  case 'currentday2':
2934  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
2935  break;
2936  case 'localmonth':
2937  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
2938  break;
2939  case 'localmonth1':
2940  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2941  break;
2942  case 'localmonthname':
2943  $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2944  break;
2945  case 'localmonthnamegen':
2946  $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2947  break;
2948  case 'localmonthabbrev':
2949  $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
2950  break;
2951  case 'localday':
2952  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
2953  break;
2954  case 'localday2':
2955  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
2956  break;
2957  case 'pagename':
2958  $value = wfEscapeWikiText( $this->mTitle->getText() );
2959  break;
2960  case 'pagenamee':
2961  $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
2962  break;
2963  case 'fullpagename':
2964  $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
2965  break;
2966  case 'fullpagenamee':
2967  $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
2968  break;
2969  case 'subpagename':
2970  $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
2971  break;
2972  case 'subpagenamee':
2973  $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
2974  break;
2975  case 'rootpagename':
2976  $value = wfEscapeWikiText( $this->mTitle->getRootText() );
2977  break;
2978  case 'rootpagenamee':
2979  $value = wfEscapeWikiText( wfUrlEncode( str_replace(
2980  ' ',
2981  '_',
2982  $this->mTitle->getRootText()
2983  ) ) );
2984  break;
2985  case 'basepagename':
2986  $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
2987  break;
2988  case 'basepagenamee':
2989  $value = wfEscapeWikiText( wfUrlEncode( str_replace(
2990  ' ',
2991  '_',
2992  $this->mTitle->getBaseText()
2993  ) ) );
2994  break;
2995  case 'talkpagename':
2996  if ( $this->mTitle->canTalk() ) {
2997  $talkPage = $this->mTitle->getTalkPage();
2998  $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
2999  } else {
3000  $value = '';
3001  }
3002  break;
3003  case 'talkpagenamee':
3004  if ( $this->mTitle->canTalk() ) {
3005  $talkPage = $this->mTitle->getTalkPage();
3006  $value = wfEscapeWikiText( $talkPage->getPrefixedURL() );
3007  } else {
3008  $value = '';
3009  }
3010  break;
3011  case 'subjectpagename':
3012  $subjPage = $this->mTitle->getSubjectPage();
3013  $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
3014  break;
3015  case 'subjectpagenamee':
3016  $subjPage = $this->mTitle->getSubjectPage();
3017  $value = wfEscapeWikiText( $subjPage->getPrefixedURL() );
3018  break;
3019  case 'pageid': // requested in bug 23427
3020  $pageid = $this->getTitle()->getArticleID();
3021  if ( $pageid == 0 ) {
3022  # 0 means the page doesn't exist in the database,
3023  # which means the user is previewing a new page.
3024  # The vary-revision flag must be set, because the magic word
3025  # will have a different value once the page is saved.
3026  $this->mOutput->setFlag( 'vary-revision' );
3027  wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
3028  }
3029  $value = $pageid ? $pageid : null;
3030  break;
3031  case 'revisionid':
3032  # Let the edit saving system know we should parse the page
3033  # *after* a revision ID has been assigned.
3034  $this->mOutput->setFlag( 'vary-revision' );
3035  wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
3036  $value = $this->mRevisionId;
3037  break;
3038  case 'revisionday':
3039  # Let the edit saving system know we should parse the page
3040  # *after* a revision ID has been assigned. This is for null edits.
3041  $this->mOutput->setFlag( 'vary-revision' );
3042  wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
3043  $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
3044  break;
3045  case 'revisionday2':
3046  # Let the edit saving system know we should parse the page
3047  # *after* a revision ID has been assigned. This is for null edits.
3048  $this->mOutput->setFlag( 'vary-revision' );
3049  wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
3050  $value = substr( $this->getRevisionTimestamp(), 6, 2 );
3051  break;
3052  case 'revisionmonth':
3053  # Let the edit saving system know we should parse the page
3054  # *after* a revision ID has been assigned. This is for null edits.
3055  $this->mOutput->setFlag( 'vary-revision' );
3056  wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
3057  $value = substr( $this->getRevisionTimestamp(), 4, 2 );
3058  break;
3059  case 'revisionmonth1':
3060  # Let the edit saving system know we should parse the page
3061  # *after* a revision ID has been assigned. This is for null edits.
3062  $this->mOutput->setFlag( 'vary-revision' );
3063  wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
3064  $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
3065  break;
3066  case 'revisionyear':
3067  # Let the edit saving system know we should parse the page
3068  # *after* a revision ID has been assigned. This is for null edits.
3069  $this->mOutput->setFlag( 'vary-revision' );
3070  wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
3071  $value = substr( $this->getRevisionTimestamp(), 0, 4 );
3072  break;
3073  case 'revisiontimestamp':
3074  # Let the edit saving system know we should parse the page
3075  # *after* a revision ID has been assigned. This is for null edits.
3076  $this->mOutput->setFlag( 'vary-revision' );
3077  wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
3078  $value = $this->getRevisionTimestamp();
3079  break;
3080  case 'revisionuser':
3081  # Let the edit saving system know we should parse the page
3082  # *after* a revision ID has been assigned. This is for null edits.
3083  $this->mOutput->setFlag( 'vary-revision' );
3084  wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
3085  $value = $this->getRevisionUser();
3086  break;
3087  case 'revisionsize':
3088  # Let the edit saving system know we should parse the page
3089  # *after* a revision ID has been assigned. This is for null edits.
3090  $this->mOutput->setFlag( 'vary-revision' );
3091  wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" );
3092  $value = $this->getRevisionSize();
3093  break;
3094  case 'namespace':
3095  $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3096  break;
3097  case 'namespacee':
3098  $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
3099  break;
3100  case 'namespacenumber':
3101  $value = $this->mTitle->getNamespace();
3102  break;
3103  case 'talkspace':
3104  $value = $this->mTitle->canTalk()
3105  ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() )
3106  : '';
3107  break;
3108  case 'talkspacee':
3109  $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
3110  break;
3111  case 'subjectspace':
3112  $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() );
3113  break;
3114  case 'subjectspacee':
3115  $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
3116  break;
3117  case 'currentdayname':
3118  $value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 );
3119  break;
3120  case 'currentyear':
3121  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true );
3122  break;
3123  case 'currenttime':
3124  $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false );
3125  break;
3126  case 'currenthour':
3127  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true );
3128  break;
3129  case 'currentweek':
3130  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3131  # int to remove the padding
3132  $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) );
3133  break;
3134  case 'currentdow':
3135  $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) );
3136  break;
3137  case 'localdayname':
3138  $value = $pageLang->getWeekdayName(
3139  (int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1
3140  );
3141  break;
3142  case 'localyear':
3143  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true );
3144  break;
3145  case 'localtime':
3146  $value = $pageLang->time(
3147  MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ),
3148  false,
3149  false
3150  );
3151  break;
3152  case 'localhour':
3153  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true );
3154  break;
3155  case 'localweek':
3156  # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
3157  # int to remove the padding
3158  $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) );
3159  break;
3160  case 'localdow':
3161  $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) );
3162  break;
3163  case 'numberofarticles':
3164  $value = $pageLang->formatNum( SiteStats::articles() );
3165  break;
3166  case 'numberoffiles':
3167  $value = $pageLang->formatNum( SiteStats::images() );
3168  break;
3169  case 'numberofusers':
3170  $value = $pageLang->formatNum( SiteStats::users() );
3171  break;
3172  case 'numberofactiveusers':
3173  $value = $pageLang->formatNum( SiteStats::activeUsers() );
3174  break;
3175  case 'numberofpages':
3176  $value = $pageLang->formatNum( SiteStats::pages() );
3177  break;
3178  case 'numberofadmins':
3179  $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
3180  break;
3181  case 'numberofedits':
3182  $value = $pageLang->formatNum( SiteStats::edits() );
3183  break;
3184  case 'currenttimestamp':
3185  $value = wfTimestamp( TS_MW, $ts );
3186  break;
3187  case 'localtimestamp':
3188  $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' );
3189  break;
3190  case 'currentversion':
3192  break;
3193  case 'articlepath':
3194  return $wgArticlePath;
3195  case 'sitename':
3196  return $wgSitename;
3197  case 'server':
3198  return $wgServer;
3199  case 'servername':
3200  return $wgServerName;
3201  case 'scriptpath':
3202  return $wgScriptPath;
3203  case 'stylepath':
3204  return $wgStylePath;
3205  case 'directionmark':
3206  return $pageLang->getDirMark();
3207  case 'contentlanguage':
3209  return $wgLanguageCode;
3210  case 'cascadingsources':
3212  break;
3213  default:
3214  $ret = null;
3215  Hooks::run(
3216  'ParserGetVariableValueSwitch',
3217  array( &$this, &$this->mVarCache, &$index, &$ret, &$frame )
3218  );
3219 
3220  return $ret;
3221  }
3222 
3223  if ( $index ) {
3224  $this->mVarCache[$index] = $value;
3225  }
3226 
3227  return $value;
3228  }
3229 
3235  public function initialiseVariables() {
3236  $variableIDs = MagicWord::getVariableIDs();
3237  $substIDs = MagicWord::getSubstIDs();
3238 
3239  $this->mVariables = new MagicWordArray( $variableIDs );
3240  $this->mSubstWords = new MagicWordArray( $substIDs );
3241  }
3242 
3265  public function preprocessToDom( $text, $flags = 0 ) {
3266  $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
3267  return $dom;
3268  }
3269 
3277  public static function splitWhitespace( $s ) {
3278  $ltrimmed = ltrim( $s );
3279  $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
3280  $trimmed = rtrim( $ltrimmed );
3281  $diff = strlen( $ltrimmed ) - strlen( $trimmed );
3282  if ( $diff > 0 ) {
3283  $w2 = substr( $ltrimmed, -$diff );
3284  } else {
3285  $w2 = '';
3286  }
3287  return array( $w1, $trimmed, $w2 );
3288  }
3289 
3310  public function replaceVariables( $text, $frame = false, $argsOnly = false ) {
3311  # Is there any text? Also, Prevent too big inclusions!
3312  if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
3313  return $text;
3314  }
3315 
3316  if ( $frame === false ) {
3317  $frame = $this->getPreprocessor()->newFrame();
3318  } elseif ( !( $frame instanceof PPFrame ) ) {
3319  wfDebug( __METHOD__ . " called using plain parameters instead of "
3320  . "a PPFrame instance. Creating custom frame.\n" );
3321  $frame = $this->getPreprocessor()->newCustomFrame( $frame );
3322  }
3323 
3324  $dom = $this->preprocessToDom( $text );
3325  $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
3326  $text = $frame->expand( $dom, $flags );
3327 
3328  return $text;
3329  }
3330 
3338  public static function createAssocArgs( $args ) {
3339  $assocArgs = array();
3340  $index = 1;
3341  foreach ( $args as $arg ) {
3342  $eqpos = strpos( $arg, '=' );
3343  if ( $eqpos === false ) {
3344  $assocArgs[$index++] = $arg;
3345  } else {
3346  $name = trim( substr( $arg, 0, $eqpos ) );
3347  $value = trim( substr( $arg, $eqpos + 1 ) );
3348  if ( $value === false ) {
3349  $value = '';
3350  }
3351  if ( $name !== false ) {
3352  $assocArgs[$name] = $value;
3353  }
3354  }
3355  }
3356 
3357  return $assocArgs;
3358  }
3359 
3384  public function limitationWarn( $limitationType, $current = '', $max = '' ) {
3385  # does no harm if $current and $max are present but are unnecessary for the message
3386  $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max )
3387  ->inLanguage( $this->mOptions->getUserLangObj() )->text();
3388  $this->mOutput->addWarning( $warning );
3389  $this->addTrackingCategory( "$limitationType-category" );
3390  }
3391 
3404  public function braceSubstitution( $piece, $frame ) {
3405 
3406  // Flags
3407 
3408  // $text has been filled
3409  $found = false;
3410  // wiki markup in $text should be escaped
3411  $nowiki = false;
3412  // $text is HTML, armour it against wikitext transformation
3413  $isHTML = false;
3414  // Force interwiki transclusion to be done in raw mode not rendered
3415  $forceRawInterwiki = false;
3416  // $text is a DOM node needing expansion in a child frame
3417  $isChildObj = false;
3418  // $text is a DOM node needing expansion in the current frame
3419  $isLocalObj = false;
3420 
3421  # Title object, where $text came from
3422  $title = false;
3423 
3424  # $part1 is the bit before the first |, and must contain only title characters.
3425  # Various prefixes will be stripped from it later.
3426  $titleWithSpaces = $frame->expand( $piece['title'] );
3427  $part1 = trim( $titleWithSpaces );
3428  $titleText = false;
3429 
3430  # Original title text preserved for various purposes
3431  $originalTitle = $part1;
3432 
3433  # $args is a list of argument nodes, starting from index 0, not including $part1
3434  # @todo FIXME: If piece['parts'] is null then the call to getLength()
3435  # below won't work b/c this $args isn't an object
3436  $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
3437 
3438  $profileSection = null; // profile templates
3439 
3440  # SUBST
3441  if ( !$found ) {
3442 
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 
3501  $colonPos = strpos( $part1, ':' );
3502  if ( $colonPos !== false ) {
3503  $func = substr( $part1, 0, $colonPos );
3504  $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
3505  for ( $i = 0; $i < $args->getLength(); $i++ ) {
3506  $funcArgs[] = $args->item( $i );
3507  }
3508  try {
3509  $result = $this->callParserFunction( $frame, $func, $funcArgs );
3510  } catch ( Exception $ex ) {
3511  throw $ex;
3512  }
3513 
3514  # The interface for parser functions allows for extracting
3515  # flags into the local scope. Extract any forwarded flags
3516  # here.
3517  extract( $result );
3518  }
3519  }
3520 
3521  # Finish mangling title and then check for loops.
3522  # Set $title to a Title object and $titleText to the PDBK
3523  if ( !$found ) {
3524  $ns = NS_TEMPLATE;
3525  # Split the title into page and subpage
3526  $subpage = '';
3527  $relative = $this->maybeDoSubpageLink( $part1, $subpage );
3528  if ( $part1 !== $relative ) {
3529  $part1 = $relative;
3530  $ns = $this->mTitle->getNamespace();
3531  }
3532  $title = Title::newFromText( $part1, $ns );
3533  if ( $title ) {
3534  $titleText = $title->getPrefixedText();
3535  # Check for language variants if the template is not found
3536  if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
3537  $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
3538  }
3539  # Do recursion depth check
3540  $limit = $this->mOptions->getMaxTemplateDepth();
3541  if ( $frame->depth >= $limit ) {
3542  $found = true;
3543  $text = '<span class="error">'
3544  . wfMessage( 'parser-template-recursion-depth-warning' )
3545  ->numParams( $limit )->inContentLanguage()->text()
3546  . '</span>';
3547  }
3548  }
3549  }
3550 
3551  # Load from database
3552  if ( !$found && $title ) {
3553  $profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() );
3554  if ( !$title->isExternal() ) {
3555  if ( $title->isSpecialPage()
3556  && $this->mOptions->getAllowSpecialInclusion()
3557  && $this->ot['html']
3558  ) {
3559  // Pass the template arguments as URL parameters.
3560  // "uselang" will have no effect since the Language object
3561  // is forced to the one defined in ParserOptions.
3562  $pageArgs = array();
3563  $argsLength = $args->getLength();
3564  for ( $i = 0; $i < $argsLength; $i++ ) {
3565  $bits = $args->item( $i )->splitArg();
3566  if ( strval( $bits['index'] ) === '' ) {
3567  $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
3568  $value = trim( $frame->expand( $bits['value'] ) );
3569  $pageArgs[$name] = $value;
3570  }
3571  }
3572 
3573  // Create a new context to execute the special page
3574  $context = new RequestContext;
3575  $context->setTitle( $title );
3576  $context->setRequest( new FauxRequest( $pageArgs ) );
3577  $context->setUser( $this->getUser() );
3578  $context->setLanguage( $this->mOptions->getUserLangObj() );
3579  $ret = SpecialPageFactory::capturePath( $title, $context );
3580  if ( $ret ) {
3581  $text = $context->getOutput()->getHTML();
3582  $this->mOutput->addOutputPageMetadata( $context->getOutput() );
3583  $found = true;
3584  $isHTML = true;
3585  $this->disableCache();
3586  }
3587  } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
3588  $found = false; # access denied
3589  wfDebug( __METHOD__ . ": template inclusion denied for " .
3590  $title->getPrefixedDBkey() . "\n" );
3591  } else {
3592  list( $text, $title ) = $this->getTemplateDom( $title );
3593  if ( $text !== false ) {
3594  $found = true;
3595  $isChildObj = true;
3596  }
3597  }
3598 
3599  # If the title is valid but undisplayable, make a link to it
3600  if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3601  $text = "[[:$titleText]]";
3602  $found = true;
3603  }
3604  } elseif ( $title->isTrans() ) {
3605  # Interwiki transclusion
3606  if ( $this->ot['html'] && !$forceRawInterwiki ) {
3607  $text = $this->interwikiTransclude( $title, 'render' );
3608  $isHTML = true;
3609  } else {
3610  $text = $this->interwikiTransclude( $title, 'raw' );
3611  # Preprocess it like a template
3612  $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3613  $isChildObj = true;
3614  }
3615  $found = true;
3616  }
3617 
3618  # Do infinite loop check
3619  # This has to be done after redirect resolution to avoid infinite loops via redirects
3620  if ( !$frame->loopCheck( $title ) ) {
3621  $found = true;
3622  $text = '<span class="error">'
3623  . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text()
3624  . '</span>';
3625  wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" );
3626  }
3627  }
3628 
3629  # If we haven't found text to substitute by now, we're done
3630  # Recover the source wikitext and return it
3631  if ( !$found ) {
3632  $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
3633  if ( $profileSection ) {
3634  $this->mProfiler->scopedProfileOut( $profileSection );
3635  }
3636  return array( 'object' => $text );
3637  }
3638 
3639  # Expand DOM-style return values in a child frame
3640  if ( $isChildObj ) {
3641  # Clean up argument array
3642  $newFrame = $frame->newChild( $args, $title );
3643 
3644  if ( $nowiki ) {
3645  $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
3646  } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
3647  # Expansion is eligible for the empty-frame cache
3648  $text = $newFrame->cachedExpand( $titleText, $text );
3649  } else {
3650  # Uncached expansion
3651  $text = $newFrame->expand( $text );
3652  }
3653  }
3654  if ( $isLocalObj && $nowiki ) {
3655  $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
3656  $isLocalObj = false;
3657  }
3658 
3659  if ( $profileSection ) {
3660  $this->mProfiler->scopedProfileOut( $profileSection );
3661  }
3662 
3663  # Replace raw HTML by a placeholder
3664  if ( $isHTML ) {
3665  $text = $this->insertStripItem( $text );
3666  } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
3667  # Escape nowiki-style return values
3668  $text = wfEscapeWikiText( $text );
3669  } elseif ( is_string( $text )
3670  && !$piece['lineStart']
3671  && preg_match( '/^(?:{\\||:|;|#|\*)/', $text )
3672  ) {
3673  # Bug 529: if the template begins with a table or block-level
3674  # element, it should be treated as beginning a new line.
3675  # This behavior is somewhat controversial.
3676  $text = "\n" . $text;
3677  }
3678 
3679  if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
3680  # Error, oversize inclusion
3681  if ( $titleText !== false ) {
3682  # Make a working, properly escaped link if possible (bug 23588)
3683  $text = "[[:$titleText]]";
3684  } else {
3685  # This will probably not be a working link, but at least it may
3686  # provide some hint of where the problem is
3687  preg_replace( '/^:/', '', $originalTitle );
3688  $text = "[[:$originalTitle]]";
3689  }
3690  $text .= $this->insertStripItem( '<!-- WARNING: template omitted, '
3691  . 'post-expand include size too large -->' );
3692  $this->limitationWarn( 'post-expand-template-inclusion' );
3693  }
3694 
3695  if ( $isLocalObj ) {
3696  $ret = array( 'object' => $text );
3697  } else {
3698  $ret = array( 'text' => $text );
3699  }
3700 
3701  return $ret;
3702  }
3703 
3723  public function callParserFunction( $frame, $function, array $args = array() ) {
3725 
3726 
3727  # Case sensitive functions
3728  if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
3729  $function = $this->mFunctionSynonyms[1][$function];
3730  } else {
3731  # Case insensitive functions
3732  $function = $wgContLang->lc( $function );
3733  if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
3734  $function = $this->mFunctionSynonyms[0][$function];
3735  } else {
3736  return array( 'found' => false );
3737  }
3738  }
3739 
3740  list( $callback, $flags ) = $this->mFunctionHooks[$function];
3741 
3742  # Workaround for PHP bug 35229 and similar
3743  if ( !is_callable( $callback ) ) {
3744  throw new MWException( "Tag hook for $function is not callable\n" );
3745  }
3746 
3747  $allArgs = array( &$this );
3748  if ( $flags & self::SFH_OBJECT_ARGS ) {
3749  # Convert arguments to PPNodes and collect for appending to $allArgs
3750  $funcArgs = array();
3751  foreach ( $args as $k => $v ) {
3752  if ( $v instanceof PPNode || $k === 0 ) {
3753  $funcArgs[] = $v;
3754  } else {
3755  $funcArgs[] = $this->mPreprocessor->newPartNodeArray( array( $k => $v ) )->item( 0 );
3756  }
3757  }
3758 
3759  # Add a frame parameter, and pass the arguments as an array
3760  $allArgs[] = $frame;
3761  $allArgs[] = $funcArgs;
3762  } else {
3763  # Convert arguments to plain text and append to $allArgs
3764  foreach ( $args as $k => $v ) {
3765  if ( $v instanceof PPNode ) {
3766  $allArgs[] = trim( $frame->expand( $v ) );
3767  } elseif ( is_int( $k ) && $k >= 0 ) {
3768  $allArgs[] = trim( $v );
3769  } else {
3770  $allArgs[] = trim( "$k=$v" );
3771  }
3772  }
3773  }
3774 
3775  $result = call_user_func_array( $callback, $allArgs );
3776 
3777  # The interface for function hooks allows them to return a wikitext
3778  # string or an array containing the string and any flags. This mungs
3779  # things around to match what this method should return.
3780  if ( !is_array( $result ) ) {
3781  $result = array(
3782  'found' => true,
3783  'text' => $result,
3784  );
3785  } else {
3786  if ( isset( $result[0] ) && !isset( $result['text'] ) ) {
3787  $result['text'] = $result[0];
3788  }
3789  unset( $result[0] );
3790  $result += array(
3791  'found' => true,
3792  );
3793  }
3794 
3795  $noparse = true;
3796  $preprocessFlags = 0;
3797  if ( isset( $result['noparse'] ) ) {
3798  $noparse = $result['noparse'];
3799  }
3800  if ( isset( $result['preprocessFlags'] ) ) {
3801  $preprocessFlags = $result['preprocessFlags'];
3802  }
3803 
3804  if ( !$noparse ) {
3805  $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags );
3806  $result['isChildObj'] = true;
3807  }
3808 
3809  return $result;
3810  }
3811 
3820  public function getTemplateDom( $title ) {
3821  $cacheTitle = $title;
3822  $titleText = $title->getPrefixedDBkey();
3823 
3824  if ( isset( $this->mTplRedirCache[$titleText] ) ) {
3825  list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
3826  $title = Title::makeTitle( $ns, $dbk );
3827  $titleText = $title->getPrefixedDBkey();
3828  }
3829  if ( isset( $this->mTplDomCache[$titleText] ) ) {
3830  return array( $this->mTplDomCache[$titleText], $title );
3831  }
3832 
3833  # Cache miss, go to the database
3834  list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
3835 
3836  if ( $text === false ) {
3837  $this->mTplDomCache[$titleText] = false;
3838  return array( false, $title );
3839  }
3840 
3841  $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
3842  $this->mTplDomCache[$titleText] = $dom;
3843 
3844  if ( !$title->equals( $cacheTitle ) ) {
3845  $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
3846  array( $title->getNamespace(), $cdb = $title->getDBkey() );
3847  }
3848 
3849  return array( $dom, $title );
3850  }
3851 
3863  public function fetchCurrentRevisionOfTitle( $title ) {
3864  $cacheKey = $title->getPrefixedDBkey();
3865  if ( !$this->currentRevisionCache ) {
3866  $this->currentRevisionCache = new MapCacheLRU( 100 );
3867  }
3868  if ( !$this->currentRevisionCache->has( $cacheKey ) ) {
3869  $this->currentRevisionCache->set( $cacheKey,
3870  // Defaults to Parser::statelessFetchRevision()
3871  call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this )
3872  );
3873  }
3874  return $this->currentRevisionCache->get( $cacheKey );
3875  }
3876 
3886  public static function statelessFetchRevision( $title, $parser = false ) {
3887  return Revision::newFromTitle( $title );
3888  }
3889 
3895  public function fetchTemplateAndTitle( $title ) {
3896  // Defaults to Parser::statelessFetchTemplate()
3897  $templateCb = $this->mOptions->getTemplateCallback();
3898  $stuff = call_user_func( $templateCb, $title, $this );
3899  $text = $stuff['text'];
3900  $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
3901  if ( isset( $stuff['deps'] ) ) {
3902  foreach ( $stuff['deps'] as $dep ) {
3903  $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
3904  if ( $dep['title']->equals( $this->getTitle() ) ) {
3905  // If we transclude ourselves, the final result
3906  // will change based on the new version of the page
3907  $this->mOutput->setFlag( 'vary-revision' );
3908  }
3909  }
3910  }
3911  return array( $text, $finalTitle );
3912  }
3913 
3919  public function fetchTemplate( $title ) {
3920  $rv = $this->fetchTemplateAndTitle( $title );
3921  return $rv[0];
3922  }
3923 
3933  public static function statelessFetchTemplate( $title, $parser = false ) {
3934  $text = $skip = false;
3935  $finalTitle = $title;
3936  $deps = array();
3937 
3938  # Loop to fetch the article, with up to 1 redirect
3939  for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
3940  # Give extensions a chance to select the revision instead
3941  $id = false; # Assume current
3942  Hooks::run( 'BeforeParserFetchTemplateAndtitle',
3943  array( $parser, $title, &$skip, &$id ) );
3944 
3945  if ( $skip ) {
3946  $text = false;
3947  $deps[] = array(
3948  'title' => $title,
3949  'page_id' => $title->getArticleID(),
3950  'rev_id' => null
3951  );
3952  break;
3953  }
3954  # Get the revision
3955  if ( $id ) {
3956  $rev = Revision::newFromId( $id );
3957  } elseif ( $parser ) {
3958  $rev = $parser->fetchCurrentRevisionOfTitle( $title );
3959  } else {
3960  $rev = Revision::newFromTitle( $title );
3961  }
3962  $rev_id = $rev ? $rev->getId() : 0;
3963  # If there is no current revision, there is no page
3964  if ( $id === false && !$rev ) {
3965  $linkCache = LinkCache::singleton();
3966  $linkCache->addBadLinkObj( $title );
3967  }
3968 
3969  $deps[] = array(
3970  'title' => $title,
3971  'page_id' => $title->getArticleID(),
3972  'rev_id' => $rev_id );
3973  if ( $rev && !$title->equals( $rev->getTitle() ) ) {
3974  # We fetched a rev from a different title; register it too...
3975  $deps[] = array(
3976  'title' => $rev->getTitle(),
3977  'page_id' => $rev->getPage(),
3978  'rev_id' => $rev_id );
3979  }
3980 
3981  if ( $rev ) {
3982  $content = $rev->getContent();
3983  $text = $content ? $content->getWikitextForTransclusion() : null;
3984 
3985  if ( $text === false || $text === null ) {
3986  $text = false;
3987  break;
3988  }
3989  } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
3991  $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
3992  if ( !$message->exists() ) {
3993  $text = false;
3994  break;
3995  }
3996  $content = $message->content();
3997  $text = $message->plain();
3998  } else {
3999  break;
4000  }
4001  if ( !$content ) {
4002  break;
4003  }
4004  # Redirect?
4005  $finalTitle = $title;
4006  $title = $content->getRedirectTarget();
4007  }
4008  return array(
4009  'text' => $text,
4010  'finalTitle' => $finalTitle,
4011  'deps' => $deps );
4012  }
4013 
4021  public function fetchFile( $title, $options = array() ) {
4022  $res = $this->fetchFileAndTitle( $title, $options );
4023  return $res[0];
4024  }
4025 
4033  public function fetchFileAndTitle( $title, $options = array() ) {
4034  $file = $this->fetchFileNoRegister( $title, $options );
4035 
4036  $time = $file ? $file->getTimestamp() : false;
4037  $sha1 = $file ? $file->getSha1() : false;
4038  # Register the file as a dependency...
4039  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4040  if ( $file && !$title->equals( $file->getTitle() ) ) {
4041  # Update fetched file title
4042  $title = $file->getTitle();
4043  $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
4044  }
4045  return array( $file, $title );
4046  }
4047 
4058  protected function fetchFileNoRegister( $title, $options = array() ) {
4059  if ( isset( $options['broken'] ) ) {
4060  $file = false; // broken thumbnail forced by hook
4061  } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp)
4062  $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options );
4063  } else { // get by (name,timestamp)
4064  $file = wfFindFile( $title, $options );
4065  }
4066  return $file;
4067  }
4068 
4077  public function interwikiTransclude( $title, $action ) {
4079 
4080  if ( !$wgEnableScaryTranscluding ) {
4081  return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
4082  }
4083 
4084  $url = $title->getFullURL( array( 'action' => $action ) );
4085 
4086  if ( strlen( $url ) > 255 ) {
4087  return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
4088  }
4089  return $this->fetchScaryTemplateMaybeFromCache( $url );
4090  }
4091 
4096  public function fetchScaryTemplateMaybeFromCache( $url ) {
4098  $dbr = wfGetDB( DB_SLAVE );
4099  $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
4100  $obj = $dbr->selectRow( 'transcache', array( 'tc_time', 'tc_contents' ),
4101  array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
4102  if ( $obj ) {
4103  return $obj->tc_contents;
4104  }
4105 
4106  $req = MWHttpRequest::factory( $url, array(), __METHOD__ );
4107  $status = $req->execute(); // Status object
4108  if ( $status->isOK() ) {
4109  $text = $req->getContent();
4110  } elseif ( $req->getStatus() != 200 ) {
4111  // Though we failed to fetch the content, this status is useless.
4112  return wfMessage( 'scarytranscludefailed-httpstatus' )
4113  ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
4114  } else {
4115  return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
4116  }
4117 
4118  $dbw = wfGetDB( DB_MASTER );
4119  $dbw->replace( 'transcache', array( 'tc_url' ), array(
4120  'tc_url' => $url,
4121  'tc_time' => $dbw->timestamp( time() ),
4122  'tc_contents' => $text
4123  ) );
4124  return $text;
4125  }
4126 
4136  public function argSubstitution( $piece, $frame ) {
4137 
4138  $error = false;
4139  $parts = $piece['parts'];
4140  $nameWithSpaces = $frame->expand( $piece['title'] );
4141  $argName = trim( $nameWithSpaces );
4142  $object = false;
4143  $text = $frame->getArgument( $argName );
4144  if ( $text === false && $parts->getLength() > 0
4145  && ( $this->ot['html']
4146  || $this->ot['pre']
4147  || ( $this->ot['wiki'] && $frame->isTemplate() )
4148  )
4149  ) {
4150  # No match in frame, use the supplied default
4151  $object = $parts->item( 0 )->getChildren();
4152  }
4153  if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
4154  $error = '<!-- WARNING: argument omitted, expansion size too large -->';
4155  $this->limitationWarn( 'post-expand-template-argument' );
4156  }
4157 
4158  if ( $text === false && $object === false ) {
4159  # No match anywhere
4160  $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
4161  }
4162  if ( $error !== false ) {
4163  $text .= $error;
4164  }
4165  if ( $object !== false ) {
4166  $ret = array( 'object' => $object );
4167  } else {
4168  $ret = array( 'text' => $text );
4169  }
4170 
4171  return $ret;
4172  }
4173 
4189  public function extensionSubstitution( $params, $frame ) {
4190  $name = $frame->expand( $params['name'] );
4191  $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
4192  $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
4193  $marker = "{$this->mUniqPrefix}-$name-"
4194  . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
4195 
4196  $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) &&
4197  ( $this->ot['html'] || $this->ot['pre'] );
4198  if ( $isFunctionTag ) {
4199  $markerType = 'none';
4200  } else {
4201  $markerType = 'general';
4202  }
4203  if ( $this->ot['html'] || $isFunctionTag ) {
4204  $name = strtolower( $name );
4205  $attributes = Sanitizer::decodeTagAttributes( $attrText );
4206  if ( isset( $params['attributes'] ) ) {
4207  $attributes = $attributes + $params['attributes'];
4208  }
4209 
4210  if ( isset( $this->mTagHooks[$name] ) ) {
4211  # Workaround for PHP bug 35229 and similar
4212  if ( !is_callable( $this->mTagHooks[$name] ) ) {
4213  throw new MWException( "Tag hook for $name is not callable\n" );
4214  }
4215  $output = call_user_func_array( $this->mTagHooks[$name],
4216  array( $content, $attributes, $this, $frame ) );
4217  } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
4218  list( $callback, ) = $this->mFunctionTagHooks[$name];
4219  if ( !is_callable( $callback ) ) {
4220  throw new MWException( "Tag hook for $name is not callable\n" );
4221  }
4222 
4223  $output = call_user_func_array( $callback, array( &$this, $frame, $content, $attributes ) );
4224  } else {
4225  $output = '<span class="error">Invalid tag extension name: ' .
4226  htmlspecialchars( $name ) . '</span>';
4227  }
4228 
4229  if ( is_array( $output ) ) {
4230  # Extract flags to local scope (to override $markerType)
4231  $flags = $output;
4232  $output = $flags[0];
4233  unset( $flags[0] );
4234  extract( $flags );
4235  }
4236  } else {
4237  if ( is_null( $attrText ) ) {
4238  $attrText = '';
4239  }
4240  if ( isset( $params['attributes'] ) ) {
4241  foreach ( $params['attributes'] as $attrName => $attrValue ) {
4242  $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
4243  htmlspecialchars( $attrValue ) . '"';
4244  }
4245  }
4246  if ( $content === null ) {
4247  $output = "<$name$attrText/>";
4248  } else {
4249  $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
4250  $output = "<$name$attrText>$content$close";
4251  }
4252  }
4253 
4254  if ( $markerType === 'none' ) {
4255  return $output;
4256  } elseif ( $markerType === 'nowiki' ) {
4257  $this->mStripState->addNoWiki( $marker, $output );
4258  } elseif ( $markerType === 'general' ) {
4259  $this->mStripState->addGeneral( $marker, $output );
4260  } else {
4261  throw new MWException( __METHOD__ . ': invalid marker type' );
4262  }
4263  return $marker;
4264  }
4265 
4273  public function incrementIncludeSize( $type, $size ) {
4274  if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
4275  return false;
4276  } else {
4277  $this->mIncludeSizes[$type] += $size;
4278  return true;
4279  }
4280  }
4281 
4287  public function incrementExpensiveFunctionCount() {
4288  $this->mExpensiveFunctionCount++;
4289  return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit();
4290  }
4291 
4300  public function doDoubleUnderscore( $text ) {
4301 
4302  # The position of __TOC__ needs to be recorded
4303  $mw = MagicWord::get( 'toc' );
4304  if ( $mw->match( $text ) ) {
4305  $this->mShowToc = true;
4306  $this->mForceTocPosition = true;
4307 
4308  # Set a placeholder. At the end we'll fill it in with the TOC.
4309  $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
4310 
4311  # Only keep the first one.
4312  $text = $mw->replace( '', $text );
4313  }
4314 
4315  # Now match and remove the rest of them
4317  $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
4318 
4319  if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
4320  $this->mOutput->mNoGallery = true;
4321  }
4322  if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
4323  $this->mShowToc = false;
4324  }
4325  if ( isset( $this->mDoubleUnderscores['hiddencat'] )
4326  && $this->mTitle->getNamespace() == NS_CATEGORY
4327  ) {
4328  $this->addTrackingCategory( 'hidden-category-category' );
4329  }
4330  # (bug 8068) Allow control over whether robots index a page.
4331  #
4332  # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This
4333  # is not desirable, the last one on the page should win.
4334  if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
4335  $this->mOutput->setIndexPolicy( 'noindex' );
4336  $this->addTrackingCategory( 'noindex-category' );
4337  }
4338  if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
4339  $this->mOutput->setIndexPolicy( 'index' );
4340  $this->addTrackingCategory( 'index-category' );
4341  }
4342 
4343  # Cache all double underscores in the database
4344  foreach ( $this->mDoubleUnderscores as $key => $val ) {
4345  $this->mOutput->setProperty( $key, '' );
4346  }
4347 
4348  return $text;
4349  }
4350 
4356  public function addTrackingCategory( $msg ) {
4357  return $this->mOutput->addTrackingCategory( $msg, $this->mTitle );
4358  }
4359 
4376  public function formatHeadings( $text, $origText, $isMain = true ) {
4378 
4379  # Inhibit editsection links if requested in the page
4380  if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
4381  $maybeShowEditLink = $showEditLink = false;
4382  } else {
4383  $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */
4384  $showEditLink = $this->mOptions->getEditSection();
4385  }
4386  if ( $showEditLink ) {
4387  $this->mOutput->setEditSectionTokens( true );
4388  }
4389 
4390  # Get all headlines for numbering them and adding funky stuff like [edit]
4391  # links - this is for later, but we need the number of headlines right now
4392  $matches = array();
4393  $numMatches = preg_match_all(
4394  '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
4395  $text,
4396  $matches
4397  );
4398 
4399  # if there are fewer than 4 headlines in the article, do not show TOC
4400  # unless it's been explicitly enabled.
4401  $enoughToc = $this->mShowToc &&
4402  ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
4403 
4404  # Allow user to stipulate that a page should have a "new section"
4405  # link added via __NEWSECTIONLINK__
4406  if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
4407  $this->mOutput->setNewSection( true );
4408  }
4409 
4410  # Allow user to remove the "new section"
4411  # link via __NONEWSECTIONLINK__
4412  if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
4413  $this->mOutput->hideNewSection( true );
4414  }
4415 
4416  # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
4417  # override above conditions and always show TOC above first header
4418  if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
4419  $this->mShowToc = true;
4420  $enoughToc = true;
4421  }
4422 
4423  # headline counter
4424  $headlineCount = 0;
4425  $numVisible = 0;
4426 
4427  # Ugh .. the TOC should have neat indentation levels which can be
4428  # passed to the skin functions. These are determined here
4429  $toc = '';
4430  $full = '';
4431  $head = array();
4432  $sublevelCount = array();
4433  $levelCount = array();
4434  $level = 0;
4435  $prevlevel = 0;
4436  $toclevel = 0;
4437  $prevtoclevel = 0;
4438  $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
4439  $baseTitleText = $this->mTitle->getPrefixedDBkey();
4440  $oldType = $this->mOutputType;
4441  $this->setOutputType( self::OT_WIKI );
4442  $frame = $this->getPreprocessor()->newFrame();
4443  $root = $this->preprocessToDom( $origText );
4444  $node = $root->getFirstChild();
4445  $byteOffset = 0;
4446  $tocraw = array();
4447  $refers = array();
4448 
4449  foreach ( $matches[3] as $headline ) {
4450  $isTemplate = false;
4451  $titleText = false;
4452  $sectionIndex = false;
4453  $numbering = '';
4454  $markerMatches = array();
4455  if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) {
4456  $serial = $markerMatches[1];
4457  list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
4458  $isTemplate = ( $titleText != $baseTitleText );
4459  $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline );
4460  }
4461 
4462  if ( $toclevel ) {
4463  $prevlevel = $level;
4464  }
4465  $level = $matches[1][$headlineCount];
4466 
4467  if ( $level > $prevlevel ) {
4468  # Increase TOC level
4469  $toclevel++;
4470  $sublevelCount[$toclevel] = 0;
4471  if ( $toclevel < $wgMaxTocLevel ) {
4472  $prevtoclevel = $toclevel;
4473  $toc .= Linker::tocIndent();
4474  $numVisible++;
4475  }
4476  } elseif ( $level < $prevlevel && $toclevel > 1 ) {
4477  # Decrease TOC level, find level to jump to
4478 
4479  for ( $i = $toclevel; $i > 0; $i-- ) {
4480  if ( $levelCount[$i] == $level ) {
4481  # Found last matching level
4482  $toclevel = $i;
4483  break;
4484  } elseif ( $levelCount[$i] < $level ) {
4485  # Found first matching level below current level
4486  $toclevel = $i + 1;
4487  break;
4488  }
4489  }
4490  if ( $i == 0 ) {
4491  $toclevel = 1;
4492  }
4493  if ( $toclevel < $wgMaxTocLevel ) {
4494  if ( $prevtoclevel < $wgMaxTocLevel ) {
4495  # Unindent only if the previous toc level was shown :p
4496  $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
4497  $prevtoclevel = $toclevel;
4498  } else {
4499  $toc .= Linker::tocLineEnd();
4500  }
4501  }
4502  } else {
4503  # No change in level, end TOC line
4504  if ( $toclevel < $wgMaxTocLevel ) {
4505  $toc .= Linker::tocLineEnd();
4506  }
4507  }
4508 
4509  $levelCount[$toclevel] = $level;
4510 
4511  # count number of headlines for each level
4512  $sublevelCount[$toclevel]++;
4513  $dot = 0;
4514  for ( $i = 1; $i <= $toclevel; $i++ ) {
4515  if ( !empty( $sublevelCount[$i] ) ) {
4516  if ( $dot ) {
4517  $numbering .= '.';
4518  }
4519  $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] );
4520  $dot = 1;
4521  }
4522  }
4523 
4524  # The safe header is a version of the header text safe to use for links
4525 
4526  # Remove link placeholders by the link text.
4527  # <!--LINK number-->
4528  # turns into
4529  # link text with suffix
4530  # Do this before unstrip since link text can contain strip markers
4531  $safeHeadline = $this->replaceLinkHoldersText( $headline );
4532 
4533  # Avoid insertion of weird stuff like <math> by expanding the relevant sections
4534  $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
4535 
4536  # Strip out HTML (first regex removes any tag not allowed)
4537  # Allowed tags are:
4538  # * <sup> and <sub> (bug 8393)
4539  # * <i> (bug 26375)
4540  # * <b> (r105284)
4541  # * <bdi> (bug 72884)
4542  # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
4543  #
4544  # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
4545  # to allow setting directionality in toc items.
4546  $tocline = preg_replace(
4547  array(
4548  '#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#',
4549  '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#'
4550  ),
4551  array( '', '<$1>' ),
4552  $safeHeadline
4553  );
4554  $tocline = trim( $tocline );
4555 
4556  # For the anchor, strip out HTML-y stuff period
4557  $safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline );
4558  $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
4559 
4560  # Save headline for section edit hint before it's escaped
4561  $headlineHint = $safeHeadline;
4562 
4563  if ( $wgExperimentalHtmlIds ) {
4564  # For reverse compatibility, provide an id that's
4565  # HTML4-compatible, like we used to.
4566  #
4567  # It may be worth noting, academically, that it's possible for
4568  # the legacy anchor to conflict with a non-legacy headline
4569  # anchor on the page. In this case likely the "correct" thing
4570  # would be to either drop the legacy anchors or make sure
4571  # they're numbered first. However, this would require people
4572  # to type in section names like "abc_.D7.93.D7.90.D7.A4"
4573  # manually, so let's not bother worrying about it.
4574  $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
4575  array( 'noninitial', 'legacy' ) );
4576  $safeHeadline = Sanitizer::escapeId( $safeHeadline );
4577 
4578  if ( $legacyHeadline == $safeHeadline ) {
4579  # No reason to have both (in fact, we can't)
4580  $legacyHeadline = false;
4581  }
4582  } else {
4583  $legacyHeadline = false;
4584  $safeHeadline = Sanitizer::escapeId( $safeHeadline,
4585  'noninitial' );
4586  }
4587 
4588  # HTML names must be case-insensitively unique (bug 10721).
4589  # This does not apply to Unicode characters per
4590  # http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
4591  # @todo FIXME: We may be changing them depending on the current locale.
4592  $arrayKey = strtolower( $safeHeadline );
4593  if ( $legacyHeadline === false ) {
4594  $legacyArrayKey = false;
4595  } else {
4596  $legacyArrayKey = strtolower( $legacyHeadline );
4597  }
4598 
4599  # Create the anchor for linking from the TOC to the section
4600  $anchor = $safeHeadline;
4601  $legacyAnchor = $legacyHeadline;
4602  if ( isset( $refers[$arrayKey] ) ) {
4603  for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
4604  $anchor .= "_$i";
4605  $refers["${arrayKey}_$i"] = true;
4606  } else {
4607  $refers[$arrayKey] = true;
4608  }
4609  if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
4610  for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
4611  $legacyAnchor .= "_$i";
4612  $refers["${legacyArrayKey}_$i"] = true;
4613  } else {
4614  $refers[$legacyArrayKey] = true;
4615  }
4616 
4617  # Don't number the heading if it is the only one (looks silly)
4618  if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
4619  # the two are different if the line contains a link
4620  $headline = Html::element(
4621  'span',
4622  array( 'class' => 'mw-headline-number' ),
4623  $numbering
4624  ) . ' ' . $headline;
4625  }
4626 
4627  if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
4628  $toc .= Linker::tocLine( $anchor, $tocline,
4629  $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
4630  }
4631 
4632  # Add the section to the section tree
4633  # Find the DOM node for this header
4634  $noOffset = ( $isTemplate || $sectionIndex === false );
4635  while ( $node && !$noOffset ) {
4636  if ( $node->getName() === 'h' ) {
4637  $bits = $node->splitHeading();
4638  if ( $bits['i'] == $sectionIndex ) {
4639  break;
4640  }
4641  }
4642  $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
4643  $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
4644  $node = $node->getNextSibling();
4645  }
4646  $tocraw[] = array(
4647  'toclevel' => $toclevel,
4648  'level' => $level,
4649  'line' => $tocline,
4650  'number' => $numbering,
4651  'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
4652  'fromtitle' => $titleText,
4653  'byteoffset' => ( $noOffset ? null : $byteOffset ),
4654  'anchor' => $anchor,
4655  );
4656 
4657  # give headline the correct <h#> tag
4658  if ( $maybeShowEditLink && $sectionIndex !== false ) {
4659  // Output edit section links as markers with styles that can be customized by skins
4660  if ( $isTemplate ) {
4661  # Put a T flag in the section identifier, to indicate to extractSections()
4662  # that sections inside <includeonly> should be counted.
4663  $editsectionPage = $titleText;
4664  $editsectionSection = "T-$sectionIndex";
4665  $editsectionContent = null;
4666  } else {
4667  $editsectionPage = $this->mTitle->getPrefixedText();
4668  $editsectionSection = $sectionIndex;
4669  $editsectionContent = $headlineHint;
4670  }
4671  // We use a bit of pesudo-xml for editsection markers. The
4672  // language converter is run later on. Using a UNIQ style marker
4673  // leads to the converter screwing up the tokens when it
4674  // converts stuff. And trying to insert strip tags fails too. At
4675  // this point all real inputted tags have already been escaped,
4676  // so we don't have to worry about a user trying to input one of
4677  // these markers directly. We use a page and section attribute
4678  // to stop the language converter from converting these
4679  // important bits of data, but put the headline hint inside a
4680  // content block because the language converter is supposed to
4681  // be able to convert that piece of data.
4682  // Gets replaced with html in ParserOutput::getText
4683  $editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage );
4684  $editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"';
4685  if ( $editsectionContent !== null ) {
4686  $editlink .= '>' . $editsectionContent . '</mw:editsection>';
4687  } else {
4688  $editlink .= '/>';
4689  }
4690  } else {
4691  $editlink = '';
4692  }
4693  $head[$headlineCount] = Linker::makeHeadline( $level,
4694  $matches['attrib'][$headlineCount], $anchor, $headline,
4695  $editlink, $legacyAnchor );
4696 
4697  $headlineCount++;
4698  }
4699 
4700  $this->setOutputType( $oldType );
4701 
4702  # Never ever show TOC if no headers
4703  if ( $numVisible < 1 ) {
4704  $enoughToc = false;
4705  }
4706 
4707  if ( $enoughToc ) {
4708  if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
4709  $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
4710  }
4711  $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
4712  $this->mOutput->setTOCHTML( $toc );
4713  $toc = self::TOC_START . $toc . self::TOC_END;
4714  $this->mOutput->addModules( 'mediawiki.toc' );
4715  }
4716 
4717  if ( $isMain ) {
4718  $this->mOutput->setSections( $tocraw );
4719  }
4720 
4721  # split up and insert constructed headlines
4722  $blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text );
4723  $i = 0;
4724 
4725  // build an array of document sections
4726  $sections = array();
4727  foreach ( $blocks as $block ) {
4728  // $head is zero-based, sections aren't.
4729  if ( empty( $head[$i - 1] ) ) {
4730  $sections[$i] = $block;
4731  } else {
4732  $sections[$i] = $head[$i - 1] . $block;
4733  }
4734 
4745  Hooks::run( 'ParserSectionCreate', array( $this, $i, &$sections[$i], $showEditLink ) );
4746 
4747  $i++;
4748  }
4749 
4750  if ( $enoughToc && $isMain && !$this->mForceTocPosition ) {
4751  // append the TOC at the beginning
4752  // Top anchor now in skin
4753  $sections[0] = $sections[0] . $toc . "\n";
4754  }
4755 
4756  $full .= join( '', $sections );
4757 
4758  if ( $this->mForceTocPosition ) {
4759  return str_replace( '<!--MWTOC-->', $toc, $full );
4760  } else {
4761  return $full;
4762  }
4763  }
4764 
4776  public function preSaveTransform( $text, Title $title, User $user,
4777  ParserOptions $options, $clearState = true
4778  ) {
4779  if ( $clearState ) {
4780  $magicScopeVariable = $this->lock();
4781  }
4782  $this->startParse( $title, $options, self::OT_WIKI, $clearState );
4783  $this->setUser( $user );
4785  $pairs = array(
4786  "\r\n" => "\n",
4787  "\r" => "\n",
4788  );
4789  $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
4790  if ( $options->getPreSaveTransform() ) {
4791  $text = $this->pstPass2( $text, $user );
4792  }
4793  $text = $this->mStripState->unstripBoth( $text );
4794 
4795  $this->setUser( null ); #Reset
4796 
4797  return $text;
4798  }
4799 
4808  private function pstPass2( $text, $user ) {
4810 
4811  # Note: This is the timestamp saved as hardcoded wikitext to
4812  # the database, we use $wgContLang here in order to give
4813  # everyone the same signature and use the default one rather
4814  # than the one selected in each user's preferences.
4815  # (see also bug 12815)
4816  $ts = $this->mOptions->getTimestamp();
4818  $ts = $timestamp->format( 'YmdHis' );
4819  $tzMsg = $timestamp->format( 'T' ); # might vary on DST changeover!
4820 
4821  # Allow translation of timezones through wiki. format() can return
4822  # whatever crap the system uses, localised or not, so we cannot
4823  # ship premade translations.
4824  $key = 'timezone-' . strtolower( trim( $tzMsg ) );
4825  $msg = wfMessage( $key )->inContentLanguage();
4826  if ( $msg->exists() ) {
4827  $tzMsg = $msg->text();
4828  }
4829 
4830  $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
4831 
4832  # Variable replacement
4833  # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
4834  $text = $this->replaceVariables( $text );
4835 
4836  # This works almost by chance, as the replaceVariables are done before the getUserSig(),
4837  # which may corrupt this parser instance via its wfMessage()->text() call-
4838 
4839  # Signatures
4840  $sigText = $this->getUserSig( $user );
4841  $text = strtr( $text, array(
4842  '~~~~~' => $d,
4843  '~~~~' => "$sigText $d",
4844  '~~~' => $sigText
4845  ) );
4846 
4847  # Context links ("pipe tricks"): [[|name]] and [[name (context)|]]
4848  $tc = '[' . Title::legalChars() . ']';
4849  $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
4850 
4851  // [[ns:page (context)|]]
4852  $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";
4853  // [[ns:page(context)|]] (double-width brackets, added in r40257)
4854  $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";
4855  // [[ns:page (context), context|]] (using either single or double-width comma)
4856  $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/";
4857  // [[|page]] (reverse pipe trick: add context from page title)
4858  $p2 = "/\[\[\\|($tc+)]]/";
4859 
4860  # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
4861  $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
4862  $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
4863  $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
4864 
4865  $t = $this->mTitle->getText();
4866  $m = array();
4867  if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
4868  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4869  } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
4870  $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
4871  } else {
4872  # if there's no context, don't bother duplicating the title
4873  $text = preg_replace( $p2, '[[\\1]]', $text );
4874  }
4875 
4876  # Trim trailing whitespace
4877  $text = rtrim( $text );
4878 
4879  return $text;
4880  }
4881 
4896  public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
4898 
4899  $username = $user->getName();
4900 
4901  # If not given, retrieve from the user object.
4902  if ( $nickname === false ) {
4903  $nickname = $user->getOption( 'nickname' );
4904  }
4905 
4906  if ( is_null( $fancySig ) ) {
4907  $fancySig = $user->getBoolOption( 'fancysig' );
4908  }
4909 
4910  $nickname = $nickname == null ? $username : $nickname;
4911 
4912  if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
4913  $nickname = $username;
4914  wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
4915  } elseif ( $fancySig !== false ) {
4916  # Sig. might contain markup; validate this
4917  if ( $this->validateSig( $nickname ) !== false ) {
4918  # Validated; clean up (if needed) and return it
4919  return $this->cleanSig( $nickname, true );
4920  } else {
4921  # Failed to validate; fall back to the default
4922  $nickname = $username;
4923  wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" );
4924  }
4925  }
4926 
4927  # Make sure nickname doesnt get a sig in a sig
4928  $nickname = self::cleanSigInSig( $nickname );
4929 
4930  # If we're still here, make it a link to the user page
4931  $userText = wfEscapeWikiText( $username );
4932  $nickText = wfEscapeWikiText( $nickname );
4933  $msgName = $user->isAnon() ? 'signature-anon' : 'signature';
4934 
4935  return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()
4936  ->title( $this->getTitle() )->text();
4937  }
4938 
4945  public function validateSig( $text ) {
4946  return Xml::isWellFormedXmlFragment( $text ) ? $text : false;
4947  }
4948 
4959  public function cleanSig( $text, $parsing = false ) {
4960  if ( !$parsing ) {
4961  global $wgTitle;
4962  $magicScopeVariable = $this->lock();
4963  $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true );
4964  }
4965 
4966  # Option to disable this feature
4967  if ( !$this->mOptions->getCleanSignatures() ) {
4968  return $text;
4969  }
4970 
4971  # @todo FIXME: Regex doesn't respect extension tags or nowiki
4972  # => Move this logic to braceSubstitution()
4973  $substWord = MagicWord::get( 'subst' );
4974  $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
4975  $substText = '{{' . $substWord->getSynonym( 0 );
4976 
4977  $text = preg_replace( $substRegex, $substText, $text );
4978  $text = self::cleanSigInSig( $text );
4979  $dom = $this->preprocessToDom( $text );
4980  $frame = $this->getPreprocessor()->newFrame();
4981  $text = $frame->expand( $dom );
4982 
4983  if ( !$parsing ) {
4984  $text = $this->mStripState->unstripBoth( $text );
4985  }
4987  return $text;
4988  }
4989 
4996  public static function cleanSigInSig( $text ) {
4997  $text = preg_replace( '/~{3,5}/', '', $text );
4998  return $text;
4999  }
5000 
5010  public function startExternalParse( Title $title = null, ParserOptions $options,
5011  $outputType, $clearState = true
5012  ) {
5013  $this->startParse( $title, $options, $outputType, $clearState );
5014  }
5015 
5022  private function startParse( Title $title = null, ParserOptions $options,
5023  $outputType, $clearState = true
5024  ) {
5025  $this->setTitle( $title );
5026  $this->mOptions = $options;
5027  $this->setOutputType( $outputType );
5028  if ( $clearState ) {
5029  $this->clearState();
5030  }
5031  }
5032 
5041  public function transformMsg( $text, $options, $title = null ) {
5042  static $executing = false;
5043 
5044  # Guard against infinite recursion
5045  if ( $executing ) {
5046  return $text;
5047  }
5048  $executing = true;
5049 
5050  if ( !$title ) {
5051  global $wgTitle;
5052  $title = $wgTitle;
5053  }
5054 
5055  $text = $this->preprocess( $text, $title, $options );
5056 
5057  $executing = false;
5058  return $text;
5059  }
5060 
5085  public function setHook( $tag, $callback ) {
5086  $tag = strtolower( $tag );
5087  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5088  throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
5089  }
5090  $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
5091  $this->mTagHooks[$tag] = $callback;
5092  if ( !in_array( $tag, $this->mStripList ) ) {
5093  $this->mStripList[] = $tag;
5094  }
5095 
5096  return $oldVal;
5097  }
5098 
5116  public function setTransparentTagHook( $tag, $callback ) {
5117  $tag = strtolower( $tag );
5118  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5119  throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
5120  }
5121  $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
5122  $this->mTransparentTagHooks[$tag] = $callback;
5123 
5124  return $oldVal;
5125  }
5126 
5130  public function clearTagHooks() {
5131  $this->mTagHooks = array();
5132  $this->mFunctionTagHooks = array();
5133  $this->mStripList = $this->mDefaultStripList;
5134  }
5135 
5179  public function setFunctionHook( $id, $callback, $flags = 0 ) {
5181 
5182  $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
5183  $this->mFunctionHooks[$id] = array( $callback, $flags );
5184 
5185  # Add to function cache
5186  $mw = MagicWord::get( $id );
5187  if ( !$mw ) {
5188  throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
5189  }
5190 
5191  $synonyms = $mw->getSynonyms();
5192  $sensitive = intval( $mw->isCaseSensitive() );
5194  foreach ( $synonyms as $syn ) {
5195  # Case
5196  if ( !$sensitive ) {
5197  $syn = $wgContLang->lc( $syn );
5198  }
5199  # Add leading hash
5200  if ( !( $flags & self::SFH_NO_HASH ) ) {
5201  $syn = '#' . $syn;
5202  }
5203  # Remove trailing colon
5204  if ( substr( $syn, -1, 1 ) === ':' ) {
5205  $syn = substr( $syn, 0, -1 );
5206  }
5207  $this->mFunctionSynonyms[$sensitive][$syn] = $id;
5208  }
5209  return $oldVal;
5210  }
5211 
5217  public function getFunctionHooks() {
5218  return array_keys( $this->mFunctionHooks );
5219  }
5220 
5231  public function setFunctionTagHook( $tag, $callback, $flags ) {
5232  $tag = strtolower( $tag );
5233  if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) {
5234  throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
5235  }
5236  $old = isset( $this->mFunctionTagHooks[$tag] ) ?
5237  $this->mFunctionTagHooks[$tag] : null;
5238  $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
5239 
5240  if ( !in_array( $tag, $this->mStripList ) ) {
5241  $this->mStripList[] = $tag;
5242  }
5243 
5244  return $old;
5245  }
5246 
5255  public function replaceLinkHolders( &$text, $options = 0 ) {
5256  $this->mLinkHolders->replace( $text );
5257  }
5258 
5266  public function replaceLinkHoldersText( $text ) {
5267  return $this->mLinkHolders->replaceText( $text );
5268  }
5269 
5283  public function renderImageGallery( $text, $params ) {
5284 
5285  $mode = false;
5286  if ( isset( $params['mode'] ) ) {
5287  $mode = $params['mode'];
5288  }
5289 
5290  try {
5291  $ig = ImageGalleryBase::factory( $mode );
5292  } catch ( Exception $e ) {
5293  // If invalid type set, fallback to default.
5294  $ig = ImageGalleryBase::factory( false );
5295  }
5296 
5297  $ig->setContextTitle( $this->mTitle );
5298  $ig->setShowBytes( false );
5299  $ig->setShowFilename( false );
5300  $ig->setParser( $this );
5301  $ig->setHideBadImages();
5302  $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
5303 
5304  if ( isset( $params['showfilename'] ) ) {
5305  $ig->setShowFilename( true );
5306  } else {
5307  $ig->setShowFilename( false );
5308  }
5309  if ( isset( $params['caption'] ) ) {
5310  $caption = $params['caption'];
5311  $caption = htmlspecialchars( $caption );
5312  $caption = $this->replaceInternalLinks( $caption );
5313  $ig->setCaptionHtml( $caption );
5314  }
5315  if ( isset( $params['perrow'] ) ) {
5316  $ig->setPerRow( $params['perrow'] );
5317  }
5318  if ( isset( $params['widths'] ) ) {
5319  $ig->setWidths( $params['widths'] );
5320  }
5321  if ( isset( $params['heights'] ) ) {
5322  $ig->setHeights( $params['heights'] );
5323  }
5324  $ig->setAdditionalOptions( $params );
5325 
5326  Hooks::run( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
5327 
5328  $lines = StringUtils::explode( "\n", $text );
5329  foreach ( $lines as $line ) {
5330  # match lines like these:
5331  # Image:someimage.jpg|This is some image
5332  $matches = array();
5333  preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
5334  # Skip empty lines
5335  if ( count( $matches ) == 0 ) {
5336  continue;
5337  }
5338 
5339  if ( strpos( $matches[0], '%' ) !== false ) {
5340  $matches[1] = rawurldecode( $matches[1] );
5341  }
5342  $title = Title::newFromText( $matches[1], NS_FILE );
5343  if ( is_null( $title ) ) {
5344  # Bogus title. Ignore these so we don't bomb out later.
5345  continue;
5346  }
5347 
5348  # We need to get what handler the file uses, to figure out parameters.
5349  # Note, a hook can overide the file name, and chose an entirely different
5350  # file (which potentially could be of a different type and have different handler).
5351  $options = array();
5352  $descQuery = false;
5353  Hooks::run( 'BeforeParserFetchFileAndTitle',
5354  array( $this, $title, &$options, &$descQuery ) );
5355  # Don't register it now, as ImageGallery does that later.
5356  $file = $this->fetchFileNoRegister( $title, $options );
5357  $handler = $file ? $file->getHandler() : false;
5358 
5359  $paramMap = array(
5360  'img_alt' => 'gallery-internal-alt',
5361  'img_link' => 'gallery-internal-link',
5362  );
5363  if ( $handler ) {
5364  $paramMap = $paramMap + $handler->getParamMap();
5365  // We don't want people to specify per-image widths.
5366  // Additionally the width parameter would need special casing anyhow.
5367  unset( $paramMap['img_width'] );
5368  }
5369 
5370  $mwArray = new MagicWordArray( array_keys( $paramMap ) );
5371 
5372  $label = '';
5373  $alt = '';
5374  $link = '';
5375  $handlerOptions = array();
5376  if ( isset( $matches[3] ) ) {
5377  // look for an |alt= definition while trying not to break existing
5378  // captions with multiple pipes (|) in it, until a more sensible grammar
5379  // is defined for images in galleries
5380 
5381  // FIXME: Doing recursiveTagParse at this stage, and the trim before
5382  // splitting on '|' is a bit odd, and different from makeImage.
5383  $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
5384  $parameterMatches = StringUtils::explode( '|', $matches[3] );
5385 
5386  foreach ( $parameterMatches as $parameterMatch ) {
5387  list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
5388  if ( $magicName ) {
5389  $paramName = $paramMap[$magicName];
5390 
5391  switch ( $paramName ) {
5392  case 'gallery-internal-alt':
5393  $alt = $this->stripAltText( $match, false );
5394  break;
5395  case 'gallery-internal-link':
5396  $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
5397  $chars = self::EXT_LINK_URL_CLASS;
5398  $prots = $this->mUrlProtocols;
5399  //check to see if link matches an absolute url, if not then it must be a wiki link.
5400  if ( preg_match( "/^($prots)$chars+$/u", $linkValue ) ) {
5401  $link = $linkValue;
5402  } else {
5403  $localLinkTitle = Title::newFromText( $linkValue );
5404  if ( $localLinkTitle !== null ) {
5405  $link = $localLinkTitle->getLinkURL();
5406  }
5407  }
5408  break;
5409  default:
5410  // Must be a handler specific parameter.
5411  if ( $handler->validateParam( $paramName, $match ) ) {
5412  $handlerOptions[$paramName] = $match;
5413  } else {
5414  // Guess not. Append it to the caption.
5415  wfDebug( "$parameterMatch failed parameter validation\n" );
5416  $label .= '|' . $parameterMatch;
5417  }
5418  }
5419 
5420  } else {
5421  // concatenate all other pipes
5422  $label .= '|' . $parameterMatch;
5423  }
5424  }
5425  // remove the first pipe
5426  $label = substr( $label, 1 );
5427  }
5428 
5429  $ig->add( $title, $label, $alt, $link, $handlerOptions );
5430  }
5431  $html = $ig->toHTML();
5432  Hooks::run( 'AfterParserFetchFileAndTitle', array( $this, $ig, &$html ) );
5433  return $html;
5434  }
5435 
5440  public function getImageParams( $handler ) {
5441  if ( $handler ) {
5442  $handlerClass = get_class( $handler );
5443  } else {
5444  $handlerClass = '';
5445  }
5446  if ( !isset( $this->mImageParams[$handlerClass] ) ) {
5447  # Initialise static lists
5448  static $internalParamNames = array(
5449  'horizAlign' => array( 'left', 'right', 'center', 'none' ),
5450  'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
5451  'bottom', 'text-bottom' ),
5452  'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
5453  'upright', 'border', 'link', 'alt', 'class' ),
5454  );
5455  static $internalParamMap;
5456  if ( !$internalParamMap ) {
5457  $internalParamMap = array();
5458  foreach ( $internalParamNames as $type => $names ) {
5459  foreach ( $names as $name ) {
5460  $magicName = str_replace( '-', '_', "img_$name" );
5461  $internalParamMap[$magicName] = array( $type, $name );
5462  }
5463  }
5464  }
5465 
5466  # Add handler params
5467  $paramMap = $internalParamMap;
5468  if ( $handler ) {
5469  $handlerParamMap = $handler->getParamMap();
5470  foreach ( $handlerParamMap as $magic => $paramName ) {
5471  $paramMap[$magic] = array( 'handler', $paramName );
5472  }
5473  }
5474  $this->mImageParams[$handlerClass] = $paramMap;
5475  $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
5476  }
5477  return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
5478  }
5479 
5488  public function makeImage( $title, $options, $holders = false ) {
5489  # Check if the options text is of the form "options|alt text"
5490  # Options are:
5491  # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
5492  # * left no resizing, just left align. label is used for alt= only
5493  # * right same, but right aligned
5494  # * none same, but not aligned
5495  # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
5496  # * center center the image
5497  # * frame Keep original image size, no magnify-button.
5498  # * framed Same as "frame"
5499  # * frameless like 'thumb' but without a frame. Keeps user preferences for width
5500  # * upright reduce width for upright images, rounded to full __0 px
5501  # * border draw a 1px border around the image
5502  # * alt Text for HTML alt attribute (defaults to empty)
5503  # * class Set a class for img node
5504  # * link Set the target of the image link. Can be external, interwiki, or local
5505  # vertical-align values (no % or length right now):
5506  # * baseline
5507  # * sub
5508  # * super
5509  # * top
5510  # * text-top
5511  # * middle
5512  # * bottom
5513  # * text-bottom
5514 
5515  $parts = StringUtils::explode( "|", $options );
5516 
5517  # Give extensions a chance to select the file revision for us
5518  $options = array();
5519  $descQuery = false;
5520  Hooks::run( 'BeforeParserFetchFileAndTitle',
5521  array( $this, $title, &$options, &$descQuery ) );
5522  # Fetch and register the file (file title may be different via hooks)
5523  list( $file, $title ) = $this->fetchFileAndTitle( $title, $options );
5524 
5525  # Get parameter map
5526  $handler = $file ? $file->getHandler() : false;
5527 
5528  list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
5529 
5530  if ( !$file ) {
5531  $this->addTrackingCategory( 'broken-file-category' );
5532  }
5533 
5534  # Process the input parameters
5535  $caption = '';
5536  $params = array( 'frame' => array(), 'handler' => array(),
5537  'horizAlign' => array(), 'vertAlign' => array() );
5538  $seenformat = false;
5539  foreach ( $parts as $part ) {
5540  $part = trim( $part );
5541  list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
5542  $validated = false;
5543  if ( isset( $paramMap[$magicName] ) ) {
5544  list( $type, $paramName ) = $paramMap[$magicName];
5545 
5546  # Special case; width and height come in one variable together
5547  if ( $type === 'handler' && $paramName === 'width' ) {
5548  $parsedWidthParam = $this->parseWidthParam( $value );
5549  if ( isset( $parsedWidthParam['width'] ) ) {
5550  $width = $parsedWidthParam['width'];
5551  if ( $handler->validateParam( 'width', $width ) ) {
5552  $params[$type]['width'] = $width;
5553  $validated = true;
5554  }
5555  }
5556  if ( isset( $parsedWidthParam['height'] ) ) {
5557  $height = $parsedWidthParam['height'];
5558  if ( $handler->validateParam( 'height', $height ) ) {
5559  $params[$type]['height'] = $height;
5560  $validated = true;
5561  }
5562  }
5563  # else no validation -- bug 13436
5564  } else {
5565  if ( $type === 'handler' ) {
5566  # Validate handler parameter
5567  $validated = $handler->validateParam( $paramName, $value );
5568  } else {
5569  # Validate internal parameters
5570  switch ( $paramName ) {
5571  case 'manualthumb':
5572  case 'alt':
5573  case 'class':
5574  # @todo FIXME: Possibly check validity here for
5575  # manualthumb? downstream behavior seems odd with
5576  # missing manual thumbs.
5577  $validated = true;
5578  $value = $this->stripAltText( $value, $holders );
5579  break;
5580  case 'link':
5581  $chars = self::EXT_LINK_URL_CLASS;
5582  $prots = $this->mUrlProtocols;
5583  if ( $value === '' ) {
5584  $paramName = 'no-link';
5585  $value = true;
5586  $validated = true;
5587  } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
5588  if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) {
5589  $paramName = 'link-url';
5590  $this->mOutput->addExternalLink( $value );
5591  if ( $this->mOptions->getExternalLinkTarget() ) {
5592  $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
5593  }
5594  $validated = true;
5595  }
5596  } else {
5597  $linkTitle = Title::newFromText( $value );
5598  if ( $linkTitle ) {
5599  $paramName = 'link-title';
5600  $value = $linkTitle;
5601  $this->mOutput->addLink( $linkTitle );
5602  $validated = true;
5603  }
5604  }
5605  break;
5606  case 'frameless':
5607  case 'framed':
5608  case 'thumbnail':
5609  // use first appearing option, discard others.
5610  $validated = ! $seenformat;
5611  $seenformat = true;
5612  break;
5613  default:
5614  # Most other things appear to be empty or numeric...
5615  $validated = ( $value === false || is_numeric( trim( $value ) ) );
5616  }
5617  }
5618 
5619  if ( $validated ) {
5620  $params[$type][$paramName] = $value;
5621  }
5622  }
5623  }
5624  if ( !$validated ) {
5625  $caption = $part;
5626  }
5627  }
5628 
5629  # Process alignment parameters
5630  if ( $params['horizAlign'] ) {
5631  $params['frame']['align'] = key( $params['horizAlign'] );
5632  }
5633  if ( $params['vertAlign'] ) {
5634  $params['frame']['valign'] = key( $params['vertAlign'] );
5635  }
5636 
5637  $params['frame']['caption'] = $caption;
5638 
5639  # Will the image be presented in a frame, with the caption below?
5640  $imageIsFramed = isset( $params['frame']['frame'] )
5641  || isset( $params['frame']['framed'] )
5642  || isset( $params['frame']['thumbnail'] )
5643  || isset( $params['frame']['manualthumb'] );
5644 
5645  # In the old days, [[Image:Foo|text...]] would set alt text. Later it
5646  # came to also set the caption, ordinary text after the image -- which
5647  # makes no sense, because that just repeats the text multiple times in
5648  # screen readers. It *also* came to set the title attribute.
5649  #
5650  # Now that we have an alt attribute, we should not set the alt text to
5651  # equal the caption: that's worse than useless, it just repeats the
5652  # text. This is the framed/thumbnail case. If there's no caption, we
5653  # use the unnamed parameter for alt text as well, just for the time be-
5654  # ing, if the unnamed param is set and the alt param is not.
5655  #
5656  # For the future, we need to figure out if we want to tweak this more,
5657  # e.g., introducing a title= parameter for the title; ignoring the un-
5658  # named parameter entirely for images without a caption; adding an ex-
5659  # plicit caption= parameter and preserving the old magic unnamed para-
5660  # meter for BC; ...
5661  if ( $imageIsFramed ) { # Framed image
5662  if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
5663  # No caption or alt text, add the filename as the alt text so
5664  # that screen readers at least get some description of the image
5665  $params['frame']['alt'] = $title->getText();
5666  }
5667  # Do not set $params['frame']['title'] because tooltips don't make sense
5668  # for framed images
5669  } else { # Inline image
5670  if ( !isset( $params['frame']['alt'] ) ) {
5671  # No alt text, use the "caption" for the alt text
5672  if ( $caption !== '' ) {
5673  $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
5674  } else {
5675  # No caption, fall back to using the filename for the
5676  # alt text
5677  $params['frame']['alt'] = $title->getText();
5678  }
5679  }
5680  # Use the "caption" for the tooltip text
5681  $params['frame']['title'] = $this->stripAltText( $caption, $holders );
5682  }
5683 
5684  Hooks::run( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) );
5685 
5686  # Linker does the rest
5687  $time = isset( $options['time'] ) ? $options['time'] : false;
5688  $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
5689  $time, $descQuery, $this->mOptions->getThumbSize() );
5690 
5691  # Give the handler a chance to modify the parser object
5692  if ( $handler ) {
5693  $handler->parserTransformHook( $this, $file );
5694  }
5695 
5696  return $ret;
5697  }
5698 
5704  protected function stripAltText( $caption, $holders ) {
5705  # Strip bad stuff out of the title (tooltip). We can't just use
5706  # replaceLinkHoldersText() here, because if this function is called
5707  # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
5708  if ( $holders ) {
5709  $tooltip = $holders->replaceText( $caption );
5710  } else {
5711  $tooltip = $this->replaceLinkHoldersText( $caption );
5712  }
5713 
5714  # make sure there are no placeholders in thumbnail attributes
5715  # that are later expanded to html- so expand them now and
5716  # remove the tags
5717  $tooltip = $this->mStripState->unstripBoth( $tooltip );
5718  $tooltip = Sanitizer::stripAllTags( $tooltip );
5719 
5720  return $tooltip;
5721  }
5722 
5727  public function disableCache() {
5728  wfDebug( "Parser output marked as uncacheable.\n" );
5729  if ( !$this->mOutput ) {
5730  throw new MWException( __METHOD__ .
5731  " can only be called when actually parsing something" );
5732  }
5733  $this->mOutput->setCacheTime( -1 ); // old style, for compatibility
5734  $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
5735  }
5736 
5745  public function attributeStripCallback( &$text, $frame = false ) {
5746  $text = $this->replaceVariables( $text, $frame );
5747  $text = $this->mStripState->unstripBoth( $text );
5748  return $text;
5749  }
5756  public function getTags() {
5757  return array_merge(
5758  array_keys( $this->mTransparentTagHooks ),
5759  array_keys( $this->mTagHooks ),
5760  array_keys( $this->mFunctionTagHooks )
5761  );
5762  }
5763 
5774  public function replaceTransparentTags( $text ) {
5775  $matches = array();
5776  $elements = array_keys( $this->mTransparentTagHooks );
5777  $text = self::extractTagsAndParams( $elements, $text, $matches, $this->mUniqPrefix );
5778  $replacements = array();
5779 
5780  foreach ( $matches as $marker => $data ) {
5781  list( $element, $content, $params, $tag ) = $data;
5782  $tagName = strtolower( $element );
5783  if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
5784  $output = call_user_func_array(
5785  $this->mTransparentTagHooks[$tagName],
5786  array( $content, $params, $this )
5787  );
5788  } else {
5789  $output = $tag;
5790  }
5791  $replacements[$marker] = $output;
5792  }
5793  return strtr( $text, $replacements );
5794  }
5795 
5825  private function extractSections( $text, $sectionId, $mode, $newText = '' ) {
5826  global $wgTitle; # not generally used but removes an ugly failure mode
5827 
5828  $magicScopeVariable = $this->lock();
5829  $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
5830  $outText = '';
5831  $frame = $this->getPreprocessor()->newFrame();
5832 
5833  # Process section extraction flags
5834  $flags = 0;
5835  $sectionParts = explode( '-', $sectionId );
5836  $sectionIndex = array_pop( $sectionParts );
5837  foreach ( $sectionParts as $part ) {
5838  if ( $part === 'T' ) {
5839  $flags |= self::PTD_FOR_INCLUSION;
5840  }
5841  }
5842 
5843  # Check for empty input
5844  if ( strval( $text ) === '' ) {
5845  # Only sections 0 and T-0 exist in an empty document
5846  if ( $sectionIndex == 0 ) {
5847  if ( $mode === 'get' ) {
5848  return '';
5849  } else {
5850  return $newText;
5851  }
5852  } else {
5853  if ( $mode === 'get' ) {
5854  return $newText;
5855  } else {
5856  return $text;
5857  }
5858  }
5859  }
5860 
5861  # Preprocess the text
5862  $root = $this->preprocessToDom( $text, $flags );
5863 
5864  # <h> nodes indicate section breaks
5865  # They can only occur at the top level, so we can find them by iterating the root's children
5866  $node = $root->getFirstChild();
5867 
5868  # Find the target section
5869  if ( $sectionIndex == 0 ) {
5870  # Section zero doesn't nest, level=big
5871  $targetLevel = 1000;
5872  } else {
5873  while ( $node ) {
5874  if ( $node->getName() === 'h' ) {
5875  $bits = $node->splitHeading();
5876  if ( $bits['i'] == $sectionIndex ) {
5877  $targetLevel = $bits['level'];
5878  break;
5879  }
5880  }
5881  if ( $mode === 'replace' ) {
5882  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5883  }
5884  $node = $node->getNextSibling();
5885  }
5886  }
5887 
5888  if ( !$node ) {
5889  # Not found
5890  if ( $mode === 'get' ) {
5891  return $newText;
5892  } else {
5893  return $text;
5894  }
5895  }
5896 
5897  # Find the end of the section, including nested sections
5898  do {
5899  if ( $node->getName() === 'h' ) {
5900  $bits = $node->splitHeading();
5901  $curLevel = $bits['level'];
5902  if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
5903  break;
5904  }
5905  }
5906  if ( $mode === 'get' ) {
5907  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5908  }
5909  $node = $node->getNextSibling();
5910  } while ( $node );
5911 
5912  # Write out the remainder (in replace mode only)
5913  if ( $mode === 'replace' ) {
5914  # Output the replacement text
5915  # Add two newlines on -- trailing whitespace in $newText is conventionally
5916  # stripped by the editor, so we need both newlines to restore the paragraph gap
5917  # Only add trailing whitespace if there is newText
5918  if ( $newText != "" ) {
5919  $outText .= $newText . "\n\n";
5920  }
5921 
5922  while ( $node ) {
5923  $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
5924  $node = $node->getNextSibling();
5925  }
5926  }
5927 
5928  if ( is_string( $outText ) ) {
5929  # Re-insert stripped tags
5930  $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
5931  }
5932 
5933  return $outText;
5934  }
5935 
5950  public function getSection( $text, $sectionId, $defaultText = '' ) {
5951  return $this->extractSections( $text, $sectionId, 'get', $defaultText );
5952  }
5953 
5966  public function replaceSection( $oldText, $sectionId, $newText ) {
5967  return $this->extractSections( $oldText, $sectionId, 'replace', $newText );
5968  }
5969 
5975  public function getRevisionId() {
5976  return $this->mRevisionId;
5977  }
5978 
5985  public function getRevisionObject() {
5986  if ( !is_null( $this->mRevisionObject ) ) {
5987  return $this->mRevisionObject;
5988  }
5989  if ( is_null( $this->mRevisionId ) ) {
5990  return null;
5991  }
5992 
5993  $rev = call_user_func(
5994  $this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
5995  );
5996 
5997  # If the parse is for a new revision, then the callback should have
5998  # already been set to force the object and should match mRevisionId.
5999  # If not, try to fetch by mRevisionId for sanity.
6000  if ( $rev && $rev->getId() != $this->mRevisionId ) {
6001  $rev = Revision::newFromId( $this->mRevisionId );
6002  }
6003 
6004  $this->mRevisionObject = $rev;
6005 
6006  return $this->mRevisionObject;
6007  }
6008 
6014  public function getRevisionTimestamp() {
6015  if ( is_null( $this->mRevisionTimestamp ) ) {
6017 
6018  $revObject = $this->getRevisionObject();
6019  $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
6020 
6021  # The cryptic '' timezone parameter tells to use the site-default
6022  # timezone offset instead of the user settings.
6023  #
6024  # Since this value will be saved into the parser cache, served
6025  # to other users, and potentially even used inside links and such,
6026  # it needs to be consistent for all visitors.
6027  $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
6028 
6029  }
6030  return $this->mRevisionTimestamp;
6031  }
6032 
6038  public function getRevisionUser() {
6039  if ( is_null( $this->mRevisionUser ) ) {
6040  $revObject = $this->getRevisionObject();
6041 
6042  # if this template is subst: the revision id will be blank,
6043  # so just use the current user's name
6044  if ( $revObject ) {
6045  $this->mRevisionUser = $revObject->getUserText();
6046  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6047  $this->mRevisionUser = $this->getUser()->getName();
6048  }
6049  }
6050  return $this->mRevisionUser;
6051  }
6052 
6058  public function getRevisionSize() {
6059  if ( is_null( $this->mRevisionSize ) ) {
6060  $revObject = $this->getRevisionObject();
6061 
6062  # if this variable is subst: the revision id will be blank,
6063  # so just use the parser input size, because the own substituation
6064  # will change the size.
6065  if ( $revObject ) {
6066  $this->mRevisionSize = $revObject->getSize();
6067  } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) {
6068  $this->mRevisionSize = $this->mInputSize;
6069  }
6070  }
6071  return $this->mRevisionSize;
6072  }
6073 
6079  public function setDefaultSort( $sort ) {
6080  $this->mDefaultSort = $sort;
6081  $this->mOutput->setProperty( 'defaultsort', $sort );
6082  }
6083 
6094  public function getDefaultSort() {
6095  if ( $this->mDefaultSort !== false ) {
6096  return $this->mDefaultSort;
6097  } else {
6098  return '';
6099  }
6100  }
6101 
6108  public function getCustomDefaultSort() {
6109  return $this->mDefaultSort;
6110  }
6111 
6121  public function guessSectionNameFromWikiText( $text ) {
6122  # Strip out wikitext links(they break the anchor)
6123  $text = $this->stripSectionName( $text );
6125  return '#' . Sanitizer::escapeId( $text, 'noninitial' );
6126  }
6127 
6136  public function guessLegacySectionNameFromWikiText( $text ) {
6137  # Strip out wikitext links(they break the anchor)
6138  $text = $this->stripSectionName( $text );
6140  return '#' . Sanitizer::escapeId( $text, array( 'noninitial', 'legacy' ) );
6141  }
6142 
6157  public function stripSectionName( $text ) {
6158  # Strip internal link markup
6159  $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
6160  $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
6161 
6162  # Strip external link markup
6163  # @todo FIXME: Not tolerant to blank link text
6164  # I.E. [https://www.mediawiki.org] will render as [1] or something depending
6165  # on how many empty links there are on the page - need to figure that out.
6166  $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
6167 
6168  # Parse wikitext quotes (italics & bold)
6169  $text = $this->doQuotes( $text );
6170 
6171  # Strip HTML tags
6172  $text = StringUtils::delimiterReplace( '<', '>', '', $text );
6173  return $text;
6174  }
6175 
6186  public function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) {
6187  $magicScopeVariable = $this->lock();
6188  $this->startParse( $title, $options, $outputType, true );
6189 
6190  $text = $this->replaceVariables( $text );
6191  $text = $this->mStripState->unstripBoth( $text );
6192  $text = Sanitizer::removeHTMLtags( $text );
6193  return $text;
6194  }
6195 
6202  public function testPst( $text, Title $title, ParserOptions $options ) {
6203  return $this->preSaveTransform( $text, $title, $options->getUser(), $options );
6204  }
6205 
6212  public function testPreprocess( $text, Title $title, ParserOptions $options ) {
6213  return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
6214  }
6215 
6232  public function markerSkipCallback( $s, $callback ) {
6233  $i = 0;
6234  $out = '';
6235  while ( $i < strlen( $s ) ) {
6236  $markerStart = strpos( $s, $this->mUniqPrefix, $i );
6237  if ( $markerStart === false ) {
6238  $out .= call_user_func( $callback, substr( $s, $i ) );
6239  break;
6240  } else {
6241  $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
6242  $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
6243  if ( $markerEnd === false ) {
6244  $out .= substr( $s, $markerStart );
6245  break;
6246  } else {
6247  $markerEnd += strlen( self::MARKER_SUFFIX );
6248  $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
6249  $i = $markerEnd;
6250  }
6251  }
6252  }
6253  return $out;
6254  }
6255 
6262  public function killMarkers( $text ) {
6263  return $this->mStripState->killMarkers( $text );
6264  }
6265 
6282  public function serializeHalfParsedText( $text ) {
6283  $data = array(
6284  'text' => $text,
6285  'version' => self::HALF_PARSED_VERSION,
6286  'stripState' => $this->mStripState->getSubState( $text ),
6287  'linkHolders' => $this->mLinkHolders->getSubArray( $text )
6288  );
6289  return $data;
6290  }
6291 
6307  public function unserializeHalfParsedText( $data ) {
6308  if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
6309  throw new MWException( __METHOD__ . ': invalid version' );
6310  }
6311 
6312  # First, extract the strip state.
6313  $texts = array( $data['text'] );
6314  $texts = $this->mStripState->merge( $data['stripState'], $texts );
6315 
6316  # Now renumber links
6317  $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
6318 
6319  # Should be good to go.
6320  return $texts[0];
6321  }
6322 
6332  public function isValidHalfParsedText( $data ) {
6333  return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
6334  }
6335 
6344  public function parseWidthParam( $value ) {
6345  $parsedWidthParam = array();
6346  if ( $value === '' ) {
6347  return $parsedWidthParam;
6348  }
6349  $m = array();
6350  # (bug 13500) In both cases (width/height and width only),
6351  # permit trailing "px" for backward compatibility.
6352  if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
6353  $width = intval( $m[1] );
6354  $height = intval( $m[2] );
6355  $parsedWidthParam['width'] = $width;
6356  $parsedWidthParam['height'] = $height;
6357  } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
6358  $width = intval( $value );
6359  $parsedWidthParam['width'] = $width;
6360  }
6361  return $parsedWidthParam;
6362  }
6363 
6373  protected function lock() {
6374  if ( $this->mInParse ) {
6375  throw new MWException( "Parser state cleared while parsing. "
6376  . "Did you call Parser::parse recursively?" );
6377  }
6378  $this->mInParse = true;
6379 
6380  $that = $this;
6381  $recursiveCheck = new ScopedCallback( function() use ( $that ) {
6382  $that->mInParse = false;
6383  } );
6384 
6385  return $recursiveCheck;
6386  }
6387 
6398  public static function stripOuterParagraph( $html ) {
6399  $m = array();
6400  if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) {
6401  if ( strpos( $m[1], '</p>' ) === false ) {
6402  $html = $m[1];
6403  }
6404  }
6405 
6406  return $html;
6407  }
6408 
6419  public function getFreshParser() {
6421  if ( $this->mInParse ) {
6422  return new $wgParserConf['class']( $wgParserConf );
6423  } else {
6424  return $this;
6425  }
6426  }
6427 }
setTitle($t)
Set the context title.
Definition: Parser.php:703
$mAutonumber
Definition: Parser.php:154
$mPPNodeCount
Definition: Parser.php:164
replaceInternalLinks2(&$s)
Process [[ ]] wikilinks (RIL)
Definition: Parser.php:1987
static getVariableIDs()
Get an array of parser variable IDs.
Definition: MagicWord.php:262
null means default in associative array form
Definition: hooks.txt:1714
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:1714
static tocLineEnd()
End a Table Of Contents line.
Definition: Linker.php:1686
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
static decodeTagAttributes($text)
Return an associative array of attribute names and values from a partial tag string.
Definition: Sanitizer.php:1186
$mTplRedirCache
Definition: Parser.php:166
static tocList($toc, $lang=false)
Wraps the TOC in a table and provides the hide/collapse javascript.
Definition: Linker.php:1697
static makeExternalLink($url, $text, $escape=true, $linktype= '', $attribs=array(), $title=null)
Make an external link.
Definition: Linker.php:1056
although this is appropriate in some e g manual creation of blank tables prior to an import Most of the PHP scripts need to be run from the command line Prior to doing so
Definition: README:1
getBoolOption($oname)
Get the user's current setting for a given option, as a boolean value.
Definition: User.php:2683
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList use this to change the tables headers temp or archived zone change it to an object instance and return false override the list derivative used the name of the old file when set the default code will be skipped true if there is text before this autocomment true if there is text after this autocomment add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1275
const OT_PREPROCESS
Definition: Defines.php:227
$mLastSection
Definition: Parser.php:159
$mDoubleUnderscores
Definition: Parser.php:166
Group all the pieces relevant to the context of a request into one instance.
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 instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content $content
Definition: hooks.txt:954
or
false for read/write
MapCacheLRU null $currentRevisionCache
Definition: Parser.php:204
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:627
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:896
replaceExternalLinks($text)
Replace external links (REL)
Definition: Parser.php:1725
static isNonincludable($index)
It is not possible to use pages from this namespace as template?
nextLinkID()
Definition: Parser.php:793
const SPACE_NOT_NL
Definition: Parser.php:94
static replaceUnusualEscapes($url)
Replace unusual escape codes in a URL with their equivalent characters.
Definition: Parser.php:1842
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:325
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
doHeadings($text)
Parse headers and return html.
Definition: Parser.php:1502
const OT_PLAIN
Definition: Parser.php:115
static removeHTMLtags($text, $processCallback=null, $args=array(), $extratags=array(), $removetags=array())
Cleans up HTML, removes dangerous tags and attributes, and removes HTML comments. ...
Definition: Sanitizer.php:372
static isWellFormedXmlFragment($text)
Check if a string is a well-formed XML fragment.
Definition: Xml.php:742
const OT_WIKI
Definition: Parser.php:112
User $mUser
Definition: Parser.php:171
initialiseVariables()
initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers ...
Definition: Parser.php:3211
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1714
=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:127
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:954
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:808
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:188
uniqPrefix()
Accessor for mUniqPrefix.
Definition: Parser.php:685
const TOC_START
Definition: Parser.php:121
Title($x=null)
Accessor/mutator for the Title object.
Definition: Parser.php:732
SectionProfiler $mProfiler
Definition: Parser.php:211
$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:
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1538
has been added to your &Future changes to this page and its associated Talk page will be listed there
$mHeadings
Definition: Parser.php:166
$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:294
$mFirstCall
Definition: Parser.php:136
getPreloadText($text, Title $title, ParserOptions $options, $params=array())
Process the wikitext for the "?preload=" feature.
Definition: Parser.php:646
Options($x=null)
Accessor/mutator for the ParserOptions object.
Definition: Parser.php:786
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2397
replaceLinkHolders(&$text, $options=0)
Definition: Parser.php:5231
static activeUsers()
Definition: SiteStats.php:164
$mLinkID
Definition: Parser.php:163
doQuotes($text)
Helper function for doAllQuotes()
Definition: Parser.php:1535
preprocessToDom($text, $flags=0)
Preprocess some wikitext and return the document tree.
Definition: Parser.php:3241
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1433
limitationWarn($limitationType, $current= '', $max= '')
Warn the user when a parser limitation is reached Will warn at most once the user per limitation type...
Definition: Parser.php:3360
static cleanUrl($url)
Definition: Sanitizer.php:1752
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:164
Represents a title within MediaWiki.
Definition: Title.php:33
static getRandomString()
Get a random string.
Definition: Parser.php:666
$mRevisionId
Definition: Parser.php:184
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:1719
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
doBlockLevels($text, $linestart)
#@-
Definition: Parser.php:2487
$wgArticlePath
Definition: img_auth.php:45
OutputType($x=null)
Accessor/mutator for the output type.
Definition: Parser.php:758
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:562
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:325
MagicWordArray $mVariables
Definition: Parser.php:141
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:694
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:168
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:603
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2053
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
maybeDoSubpageLink($target, &$text)
Handle link to subpage if necessary.
Definition: Parser.php:2354
$mFunctionSynonyms
Definition: Parser.php:128
If you want to remove the page from your watchlist later
setLinkID($id)
Definition: Parser.php:800
$mOutputType
Definition: Parser.php:181
Apache License January http
$mDefaultStripList
Definition: Parser.php:131
$mExtLinkBracketedRegex
Definition: Parser.php:146
if($line===false) $args
Definition: cdb.php:64
set to $title object and return false for a match for latest to be modified or replaced by the hook handler after cache objects are set use the ContentGetParserOutput hook instead for highlighting & $link
Definition: hooks.txt:2427
static getLocalInstance($ts=false)
Get a timestamp instance in the server local timezone ($wgLocaltimezone)
$wgMaxSigChars
Maximum number of Unicode characters in signature.
const COLON_STATE_TAG
Definition: Parser.php:98
static getDoubleUnderscoreArray()
Get a MagicWordArray of double-underscore entities.
Definition: MagicWord.php:298
static splitTrail($trail)
Split a link trail, return the "inside" portion and the remainder of the trail as a two-element array...
Definition: Linker.php:1771
The User object encapsulates all of the user-specific settings (user_id, name, rights, password, email address, options, last login time).
Definition: User.php:39
static decodeCharReferences($text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string...
Definition: Sanitizer.php:1397
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing after in associative array form externallinks including delete and has completed for all link tables default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock()-offset Set to overwrite offset parameter in $wgRequest set to ''to unsetoffset-wrap String Wrap the message in html(usually something like"&lt
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global then executing the whole list after the page is displayed We don t do anything smart like collating updates to the same table or such because the list is almost always going to have just one item on if that
pull multiple revisions may often pull multiple times from the same blob.
Definition: deferred.txt:11
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$wgNoFollowNsExceptions
Namespaces in which $wgNoFollowLinks doesn't apply.
static factory($mode=false, IContextSource $context=null)
Get a new image gallery.
$wgLanguageCode
Site language code.
Custom PHP profiler for parser/DB type section names that xhprof/xdebug can't handle.
static edits()
Definition: SiteStats.php:132
Class for asserting that a callback happens when an dummy object leaves scope.
$wgExtraInterlanguageLinkPrefixes
List of additional interwiki prefixes that should be treated as interlanguage links (i...
wfCgiToArray($query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
static capturePath(Title $title, IContextSource $context)
Just like executePath() but will override global variables and execute the page in "inclusion" mode...
const NO_TEMPLATES
replaceInternalLinks($s)
Process [[ ]] wikilinks.
Definition: Parser.php:1974
$mVarCache
Definition: Parser.php:132
$wgStylePath
The URL path of the skins directory.
$mRevisionObject
Definition: Parser.php:183
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:1282
internalParse($text, $isMain=true, $frame=false)
Helper function for parse() that transforms wiki markup into half-parsed HTML.
Definition: Parser.php:1185
Title $mTitle
Definition: Parser.php:180
fetchFileNoRegister($title, $options=array())
Helper function for fetchFileAndTitle.
Definition: Parser.php:4034
__destruct()
Reduce memory usage to reduce the impact of circular references.
Definition: Parser.php:241
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:209
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:872
static register($parser)
magic word use ParserLimitReportPrepare and ParserLimitReportFormat instead Called at the end of 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:2199
$mRevIdForTs
Definition: Parser.php:188
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition: design.txt:25
static normalizeSubpageLink($contextTitle, $target, &$text)
Definition: Linker.php:1503
$mStripList
Definition: Parser.php:130
$mFunctionTagHooks
Definition: Parser.php:129
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:185
$mImageParams
Definition: Parser.php:133
getDBkey()
Get the main part with underscores.
Definition: Title.php:914
doAllQuotes($text)
Replace single quotes with HTML markup.
Definition: Parser.php:1518
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.$reader:XMLReader object $logInfo:Array of information Return false to stop further processing of the tag 'ImportHandlePageXMLTag':When parsing a XML tag in a page.$reader:XMLReader object $pageInfo:Array of information Return false to stop further processing of the tag 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.$reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information Return false to stop further processing of the tag 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.$reader:XMLReader object Return false to stop further processing of the tag 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.$reader:XMLReader object $revisionInfo:Array of information Return false to stop further processing of the tag 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.$title:Title object for the current page $request:WebRequest $ignoreRedirect:boolean to skip redirect check $target:Title/string of redirect target $article:Article object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) $article:article(object) being checked 'IsTrustedProxy':Override the result of wfIsTrustedProxy() $ip:IP being check $result:Change this value to override the result of wfIsTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetMagic':DEPRECATED, use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetSpecialPageAliases':DEPRECATED, use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1712
static normalizeUrlComponent($component, $unsafe)
Definition: Parser.php:1892
isAnon()
Get whether the user is anonymous.
Definition: User.php:3216
if($limit) $timestamp
const VERSION
Update this version number when the ParserOutput format changes in an incompatible way...
Definition: Parser.php:73
wfGetDB($db, $groups=array(), $wiki=false)
Get a Database object.
namespace and then decline to actually register it file or subcat img or subcat RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok 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:954
$mInPre
Definition: Parser.php:159
const OT_WIKI
Definition: Defines.php:226
Preprocessor $mPreprocessor
Definition: Parser.php:149
getPreprocessor()
Get a preprocessor object.
Definition: Parser.php:861
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.
$limit
const NS_MEDIA
Definition: Defines.php:57
$res
Definition: database.txt:21
static linkKnown($target, $html=null, $customAttribs=array(), $query=array(), $options=array( 'known', 'noclasses'))
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:262
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:53
replaceVariables($text, $frame=false, $argsOnly=false)
Replace magic variables, templates, and template arguments with the appropriate text.
Definition: Parser.php:3286
const RECOVER_ORIG
wfMatchesDomainList($url, $domains)
Check whether a given URL has a domain that occurs in a given set of domains.
MediaWiki exception.
Definition: MWException.php:26
StripState $mStripState
Definition: Parser.php:157
$mDefaultSort
Definition: Parser.php:165
getUser()
Get a User object either from $this->mUser, if set, or from the ParserOptions object otherwise...
Definition: Parser.php:849
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:4998
$params
const NS_CATEGORY
Definition: Defines.php:83
static makeHeadline($level, $attribs, $anchor, $html, $link, $legacyAnchor=false)
Create a headline for content.
Definition: Linker.php:1752
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:134
LinkHolderArray $mLinkHolders
Definition: Parser.php:161
$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
$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
if(!function_exists( 'version_compare')||version_compare(PHP_VERSION, '5.3.3')< 0)
Definition: api.php:39
getTargetLanguage()
Get the target language for the content being parsed.
Definition: Parser.php:821
static extractTagsAndParams($elements, $text, &$matches, $uniq_prefix= '')
Replaces all occurrences of HTML-style comments and the given tags in the text with a random marker a...
Definition: Parser.php:889
while(($__line=Maintenance::readconsole())!==false) print n
Definition: eval.php:64
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:872
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:4352
getConverterLanguage()
Get the language object for language conversion.
Definition: Parser.php:839
static tocUnindent($level)
Finish one or more sublevels on the Table of Contents.
Definition: Linker.php:1655
if(!$wgRequest->checkUrlExtension()) if(!$wgEnableAPI) $wgTitle
Definition: api.php:64
static tocLine($anchor, $tocline, $tocnumber, $level, $sectionIndex=false)
parameter level defines if we are on an indentation level
Definition: Linker.php:1669
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:937
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition: design.txt:12
getExternalLinkAttribs($url=false)
Get an associative array of additional HTML attributes appropriate for a particular external link...
Definition: Parser.php:1825
__construct($conf=array())
Definition: Parser.php:216
$mInputSize
Definition: Parser.php:189
equals(Title $title)
Compare with another title.
Definition: Title.php:4184
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:276
$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:1102
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:1714
armorLinks($text)
Insert a NOPARSE hacky thing into any inline links in a chunk that's going to go through further pars...
Definition: Parser.php:2332
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1513
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:1890
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:741
$mTagHooks
Definition: Parser.php:125
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
the value to return A Title object or null whereas SearchGetNearMatch runs after $term
Definition: hooks.txt:2409
#define the
maybeMakeExternalImage($url)
make an image if it's allowed, either through the global option, through the exception, or through the on-wiki whitelist
Definition: Parser.php:1915
getOption($oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2624
areSubpagesAllowed()
Return true if subpage links should be expanded on this page.
Definition: Parser.php:2341
const MARKER_SUFFIX
Definition: Parser.php:118
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 instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok 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:954
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:126
$mExpensiveFunctionCount
Definition: Parser.php:167
$mUrlProtocols
Definition: Parser.php:146
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
static getVersion($flags= '')
Return a string of the MediaWiki version with SVN revision if available.
static newFromTitle($title, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given title.
Definition: Revision.php:104
$mConf
Definition: Parser.php:146
static newFromId($id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:86
wfUrlProtocols($includeProtocolRelative=true)
Returns a regular expression of url protocols.
__clone()
Allow extensions to clean up when the parser is cloned.
Definition: Parser.php:253
static getExternalLinkRel($url=false, $title=null)
Get the rel attribute for a particular external link.
Definition: Parser.php:1804
static & singleton()
Get an instance of this class.
Definition: LinkCache.php:49
string $mUniqPrefix
Definition: Parser.php:192
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:198
const OT_MSG
Definition: Parser.php:114
replaceTransparentTags($text)
Replace transparent tags in $text with the values given by the callbacks.
Definition: Parser.php:5750
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