00001 <?php
00046 class Parser
00047 {
00053 const VERSION = '1.6.4';
00054
00055 # Flags for Parser::setFunctionHook
00056 # Also available as global constants from Defines.php
00057 const SFH_NO_HASH = 1;
00058 const SFH_OBJECT_ARGS = 2;
00059
00060 # Constants needed for external link processing
00061 # Everything except bracket, space, or control characters
00062 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
00063 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
00064 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
00065
00066
00067 const COLON_STATE_TEXT = 0;
00068 const COLON_STATE_TAG = 1;
00069 const COLON_STATE_TAGSTART = 2;
00070 const COLON_STATE_CLOSETAG = 3;
00071 const COLON_STATE_TAGSLASH = 4;
00072 const COLON_STATE_COMMENT = 5;
00073 const COLON_STATE_COMMENTDASH = 6;
00074 const COLON_STATE_COMMENTDASHDASH = 7;
00075
00076
00077 const PTD_FOR_INCLUSION = 1;
00078
00079
00080
00081 const OT_HTML = 1;
00082 const OT_WIKI = 2;
00083 const OT_PREPROCESS = 3;
00084 const OT_MSG = 3;
00085
00086
00087 const MARKER_SUFFIX = "-QINU\x7f";
00088
00092 # Persistent:
00093 var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
00094 $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
00095 $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
00096
00097
00098 # Cleared with clearState():
00099 var $mOutput, $mAutonumber, $mDTopen, $mStripState;
00100 var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
00101 var $mLinkHolders, $mLinkID;
00102 var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
00103 var $mTplExpandCache;
00104 var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
00105 var $mExpensiveFunctionCount;
00106 var $mFileCache;
00107
00108 # Temporary
00109 # These are variables reset at least once per parse regardless of $clearState
00110 var $mOptions,
00111 $mTitle,
00112 $mOutputType,
00113 $ot,
00114 $mRevisionId,
00115 $mRevisionTimestamp,
00116 $mRevIdForTs;
00117
00125 function __construct( $conf = array() ) {
00126 $this->mConf = $conf;
00127 $this->mTagHooks = array();
00128 $this->mTransparentTagHooks = array();
00129 $this->mFunctionHooks = array();
00130 $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
00131 $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
00132 $this->mUrlProtocols = wfUrlProtocols();
00133 $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
00134 '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
00135 $this->mVarCache = array();
00136 if ( isset( $conf['preprocessorClass'] ) ) {
00137 $this->mPreprocessorClass = $conf['preprocessorClass'];
00138 } elseif ( extension_loaded( 'domxml' ) ) {
00139
00140 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
00141 $this->mPreprocessorClass = 'Preprocessor_Hash';
00142 } elseif ( extension_loaded( 'dom' ) ) {
00143 $this->mPreprocessorClass = 'Preprocessor_DOM';
00144 } else {
00145 $this->mPreprocessorClass = 'Preprocessor_Hash';
00146 }
00147 $this->mMarkerIndex = 0;
00148 $this->mFirstCall = true;
00149 }
00150
00154 function __destruct() {
00155 if ( isset( $this->mLinkHolders ) ) {
00156 $this->mLinkHolders->__destruct();
00157 }
00158 foreach ( $this as $name => $value ) {
00159 unset( $this->$name );
00160 }
00161 }
00162
00166 function firstCallInit() {
00167 if ( !$this->mFirstCall ) {
00168 return;
00169 }
00170 $this->mFirstCall = false;
00171
00172 wfProfileIn( __METHOD__ );
00173
00174 $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
00175 CoreParserFunctions::register( $this );
00176 $this->initialiseVariables();
00177
00178 wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
00179 wfProfileOut( __METHOD__ );
00180 }
00181
00187 function clearState() {
00188 wfProfileIn( __METHOD__ );
00189 if ( $this->mFirstCall ) {
00190 $this->firstCallInit();
00191 }
00192 $this->mOutput = new ParserOutput;
00193 $this->mAutonumber = 0;
00194 $this->mLastSection = '';
00195 $this->mDTopen = false;
00196 $this->mIncludeCount = array();
00197 $this->mStripState = new StripState;
00198 $this->mArgStack = false;
00199 $this->mInPre = false;
00200 $this->mLinkHolders = new LinkHolderArray( $this );
00201 $this->mLinkID = 0;
00202 $this->mRevisionTimestamp = $this->mRevisionId = null;
00203
00214 #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
00215 # Changed to \x7f to allow XML double-parsing -- TS
00216 $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
00217
00218
00219 # Clear these on every parse, bug 4549
00220 $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
00221
00222 $this->mShowToc = true;
00223 $this->mForceTocPosition = false;
00224 $this->mIncludeSizes = array(
00225 'post-expand' => 0,
00226 'arg' => 0,
00227 );
00228 $this->mPPNodeCount = 0;
00229 $this->mDefaultSort = false;
00230 $this->mHeadings = array();
00231 $this->mDoubleUnderscores = array();
00232 $this->mExpensiveFunctionCount = 0;
00233 $this->mFileCache = array();
00234
00235 # Fix cloning
00236 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
00237 $this->mPreprocessor = null;
00238 }
00239
00240 wfRunHooks( 'ParserClearState', array( &$this ) );
00241 wfProfileOut( __METHOD__ );
00242 }
00243
00244 function setOutputType( $ot ) {
00245 $this->mOutputType = $ot;
00246
00247 $this->ot = array(
00248 'html' => $ot == self::OT_HTML,
00249 'wiki' => $ot == self::OT_WIKI,
00250 'pre' => $ot == self::OT_PREPROCESS,
00251 );
00252 }
00253
00257 function setTitle( $t ) {
00258 if ( !$t || $t instanceof FakeTitle ) {
00259 $t = Title::newFromText( 'NO TITLE' );
00260 }
00261 if ( strval( $t->getFragment() ) !== '' ) {
00262 # Strip the fragment to avoid various odd effects
00263 $this->mTitle = clone $t;
00264 $this->mTitle->setFragment( '' );
00265 } else {
00266 $this->mTitle = $t;
00267 }
00268 }
00269
00275 function uniqPrefix() {
00276 if( !isset( $this->mUniqPrefix ) ) {
00277
00278
00279
00280
00281
00282 return '';
00283
00284 }
00285 return $this->mUniqPrefix;
00286 }
00287
00300 public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) {
00306 global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
00307 $fname = __METHOD__.'-' . wfGetCaller();
00308 wfProfileIn( __METHOD__ );
00309 wfProfileIn( $fname );
00310
00311 if ( $clearState ) {
00312 $this->clearState();
00313 }
00314
00315 $this->mOptions = $options;
00316 $this->setTitle( $title );
00317 $oldRevisionId = $this->mRevisionId;
00318 $oldRevisionTimestamp = $this->mRevisionTimestamp;
00319 if( $revid !== null ) {
00320 $this->mRevisionId = $revid;
00321 $this->mRevisionTimestamp = null;
00322 }
00323 $this->setOutputType( self::OT_HTML );
00324 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
00325 # No more strip!
00326 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
00327 $text = $this->internalParse( $text );
00328 $text = $this->mStripState->unstripGeneral( $text );
00329
00330 # Clean up special characters, only run once, next-to-last before doBlockLevels
00331 $fixtags = array(
00332 # french spaces, last one Guillemet-left
00333 # only if there is something before the space
00334 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2',
00335 # french spaces, Guillemet-right
00336 '/(\\302\\253) /' => '\\1 ',
00337 '/ (!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
00338 );
00339 $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
00340
00341 $text = $this->doBlockLevels( $text, $linestart );
00342
00343 $this->replaceLinkHolders( $text );
00344
00345 # the position of the parserConvert() call should not be changed. it
00346 # assumes that the links are all replaced and the only thing left
00347 # is the <nowiki> mark.
00348 # Side-effects: this calls $this->mOutput->setTitleText()
00349 $text = $wgContLang->parserConvert( $text, $this );
00350
00351 $text = $this->mStripState->unstripNoWiki( $text );
00352
00353 wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
00354
00356
00357 $uniq_prefix = $this->mUniqPrefix;
00358 $matches = array();
00359 $elements = array_keys( $this->mTransparentTagHooks );
00360 $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
00361
00362 foreach( $matches as $marker => $data ) {
00363 list( $element, $content, $params, $tag ) = $data;
00364 $tagName = strtolower( $element );
00365 if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
00366 $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
00367 array( $content, $params, $this ) );
00368 } else {
00369 $output = $tag;
00370 }
00371 $this->mStripState->general->setPair( $marker, $output );
00372 }
00373 $text = $this->mStripState->unstripGeneral( $text );
00374
00375 $text = Sanitizer::normalizeCharReferences( $text );
00376
00377 if ( ( $wgUseTidy && $this->mOptions->mTidy ) || $wgAlwaysUseTidy ) {
00378 $text = MWTidy::tidy( $text );
00379 } else {
00380 # attempt to sanitize at least some nesting problems
00381 # (bug #2702 and quite a few others)
00382 $tidyregs = array(
00383 # ''Something [http:
00384 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
00385 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
00386 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
00387 # fix up an anchor inside another anchor, only
00388 # at least for a single single nested link (bug 3695)
00389 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
00390 '\\1\\2</a>\\3</a>\\1\\4</a>',
00391 # fix div inside inline elements- doBlockLevels won't wrap a line which
00392 # contains a div, so fix it up here; replace
00393 # div with escaped text
00394 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
00395 '\\1\\3<div\\5>\\6</div>\\8\\9',
00396 # remove empty italic or bold tag pairs, some
00397 # introduced by rules above
00398 '/<([bi])><\/\\1>/' => '',
00399 );
00400
00401 $text = preg_replace(
00402 array_keys( $tidyregs ),
00403 array_values( $tidyregs ),
00404 $text );
00405 }
00406 global $wgExpensiveParserFunctionLimit;
00407 if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
00408 $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
00409 }
00410
00411 wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
00412
00413 # Information on include size limits, for the benefit of users who try to skirt them
00414 if ( $this->mOptions->getEnableLimitReport() ) {
00415 global $wgExpensiveParserFunctionLimit;
00416 $max = $this->mOptions->getMaxIncludeSize();
00417 $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
00418 $limitReport =
00419 "NewPP limit report\n" .
00420 "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
00421 "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
00422 "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
00423 $PFreport;
00424 wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
00425 $text .= "\n<!-- \n$limitReport-->\n";
00426 }
00427 $this->mOutput->setText( $text );
00428 $this->mRevisionId = $oldRevisionId;
00429 $this->mRevisionTimestamp = $oldRevisionTimestamp;
00430 wfProfileOut( $fname );
00431 wfProfileOut( __METHOD__ );
00432
00433 return $this->mOutput;
00434 }
00435
00440 function recursiveTagParse( $text ) {
00441 wfProfileIn( __METHOD__ );
00442 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
00443 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
00444 $text = $this->internalParse( $text );
00445 wfProfileOut( __METHOD__ );
00446 return $text;
00447 }
00448
00453 function preprocess( $text, $title, $options, $revid = null ) {
00454 wfProfileIn( __METHOD__ );
00455 $this->clearState();
00456 $this->setOutputType( self::OT_PREPROCESS );
00457 $this->mOptions = $options;
00458 $this->setTitle( $title );
00459 if( $revid !== null ) {
00460 $this->mRevisionId = $revid;
00461 }
00462 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
00463 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
00464 $text = $this->replaceVariables( $text );
00465 $text = $this->mStripState->unstripBoth( $text );
00466 wfProfileOut( __METHOD__ );
00467 return $text;
00468 }
00469
00476 function getRandomString() {
00477 return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
00478 }
00479
00480 function &getTitle() { return $this->mTitle; }
00481 function getOptions() { return $this->mOptions; }
00482 function getRevisionId() { return $this->mRevisionId; }
00483 function getOutput() { return $this->mOutput; }
00484 function nextLinkID() { return $this->mLinkID++; }
00485
00486 function getFunctionLang() {
00487 global $wgLang, $wgContLang;
00488
00489 $target = $this->mOptions->getTargetLanguage();
00490 if ( $target !== null ) {
00491 return $target;
00492 } else {
00493 return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
00494 }
00495 }
00496
00500 function getPreprocessor() {
00501 if ( !isset( $this->mPreprocessor ) ) {
00502 $class = $this->mPreprocessorClass;
00503 $this->mPreprocessor = new $class( $this );
00504 }
00505 return $this->mPreprocessor;
00506 }
00507
00526 function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
00527 static $n = 1;
00528 $stripped = '';
00529 $matches = array();
00530
00531 $taglist = implode( '|', $elements );
00532 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
00533
00534 while ( '' != $text ) {
00535 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
00536 $stripped .= $p[0];
00537 if( count( $p ) < 5 ) {
00538 break;
00539 }
00540 if( count( $p ) > 5 ) {
00541 // comment
00542 $element = $p[4];
00543 $attributes = '';
00544 $close = '';
00545 $inside = $p[5];
00546 } else {
00547 // tag
00548 $element = $p[1];
00549 $attributes = $p[2];
00550 $close = $p[3];
00551 $inside = $p[4];
00552 }
00553
00554 $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
00555 $stripped .= $marker;
00556
00557 if ( $close === '/>' ) {
00558 // Empty element tag, <tag />
00559 $content = null;
00560 $text = $inside;
00561 $tail = null;
00562 } else {
00563 if( $element === '!--' ) {
00564 $end = '/(-->)/';
00565 } else {
00566 $end = "/(<\\/$element\\s*>)/i";
00567 }
00568 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
00569 $content = $q[0];
00570 if( count( $q ) < 3 ) {
00571 # No end tag -- let it run out to the end of the text.
00572 $tail = '';
00573 $text = '';
00574 } else {
00575 $tail = $q[1];
00576 $text = $q[2];
00577 }
00578 }
00579
00580 $matches[$marker] = array( $element,
00581 $content,
00582 Sanitizer::decodeTagAttributes( $attributes ),
00583 "<$element$attributes$close$content$tail" );
00584 }
00585 return $stripped;
00586 }
00587
00591 function getStripList() {
00592 global $wgRawHtml;
00593 $elements = $this->mStripList;
00594 if( $wgRawHtml ) {
00595 $elements[] = 'html';
00596 }
00597 if( $this->mOptions->getUseTeX() ) {
00598 $elements[] = 'math';
00599 }
00600 return $elements;
00601 }
00602
00606 function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
00607 return $text;
00608 }
00609
00617 function unstrip( $text, $state ) {
00618 return $state->unstripGeneral( $text );
00619 }
00620
00627 function unstripNoWiki( $text, $state ) {
00628 return $state->unstripNoWiki( $text );
00629 }
00630
00634 function unstripForHTML( $text ) {
00635 return $this->mStripState->unstripBoth( $text );
00636 }
00637
00645 function insertStripItem( $text ) {
00646 $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
00647 $this->mMarkerIndex++;
00648 $this->mStripState->general->setPair( $rnd, $text );
00649 return $rnd;
00650 }
00651
00656 public static function tidy( $text ) {
00657 wfDeprecated( __METHOD__ );
00658 return MWTidy::tidy( $text );
00659 }
00660
00666 function doTableStuff ( $text ) {
00667 wfProfileIn( __METHOD__ );
00668
00669 $lines = StringUtils::explode( "\n", $text );
00670 $out = '';
00671 $td_history = array (); // Is currently a td tag open?
00672 $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
00673 $tr_history = array (); // Is currently a tr tag open?
00674 $tr_attributes = array (); // history of tr attributes
00675 $has_opened_tr = array(); // Did this table open a <tr> element?
00676 $indent_level = 0; // indent level of the table
00677
00678 foreach ( $lines as $outLine ) {
00679 $line = trim( $outLine );
00680
00681 if( $line == '' ) { // empty line, go to next line
00682 $out .= $outLine."\n";
00683 continue;
00684 }
00685 $first_character = $line[0];
00686 $matches = array();
00687
00688 if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
00689 // First check if we are starting a new table
00690 $indent_level = strlen( $matches[1] );
00691
00692 $attributes = $this->mStripState->unstripBoth( $matches[2] );
00693 $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
00694
00695 $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
00696 array_push ( $td_history , false );
00697 array_push ( $last_tag_history , '' );
00698 array_push ( $tr_history , false );
00699 array_push ( $tr_attributes , '' );
00700 array_push ( $has_opened_tr , false );
00701 } else if ( count ( $td_history ) == 0 ) {
00702 // Don't do any of the following
00703 $out .= $outLine."\n";
00704 continue;
00705 } else if ( substr ( $line , 0 , 2 ) === '|}' ) {
00706
00707 $line = '</table>' . substr ( $line , 2 );
00708 $last_tag = array_pop ( $last_tag_history );
00709
00710 if ( !array_pop ( $has_opened_tr ) ) {
00711 $line = "<tr><td></td></tr>{$line}";
00712 }
00713
00714 if ( array_pop ( $tr_history ) ) {
00715 $line = "</tr>{$line}";
00716 }
00717
00718 if ( array_pop ( $td_history ) ) {
00719 $line = "</{$last_tag}>{$line}";
00720 }
00721 array_pop ( $tr_attributes );
00722 $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
00723 } else if ( substr ( $line , 0 , 2 ) === '|-' ) {
00724
00725 $line = preg_replace( '#^\|-+#', '', $line );
00726
00727
00728 $attributes = $this->mStripState->unstripBoth( $line );
00729 $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
00730 array_pop ( $tr_attributes );
00731 array_push ( $tr_attributes , $attributes );
00732
00733 $line = '';
00734 $last_tag = array_pop ( $last_tag_history );
00735 array_pop ( $has_opened_tr );
00736 array_push ( $has_opened_tr , true );
00737
00738 if ( array_pop ( $tr_history ) ) {
00739 $line = '</tr>';
00740 }
00741
00742 if ( array_pop ( $td_history ) ) {
00743 $line = "</{$last_tag}>{$line}";
00744 }
00745
00746 $outLine = $line;
00747 array_push ( $tr_history , false );
00748 array_push ( $td_history , false );
00749 array_push ( $last_tag_history , '' );
00750 }
00751 else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 ) === '|+' ) {
00752
00753 if ( substr ( $line , 0 , 2 ) === '|+' ) {
00754 $first_character = '+';
00755 $line = substr ( $line , 1 );
00756 }
00757
00758 $line = substr ( $line , 1 );
00759
00760 if ( $first_character === '!' ) {
00761 $line = str_replace ( '!!' , '||' , $line );
00762 }
00763
00764
00765
00766
00767
00768 $cells = StringUtils::explodeMarkup( '||' , $line );
00769
00770 $outLine = '';
00771
00772
00773 foreach ( $cells as $cell )
00774 {
00775 $previous = '';
00776 if ( $first_character !== '+' )
00777 {
00778 $tr_after = array_pop ( $tr_attributes );
00779 if ( !array_pop ( $tr_history ) ) {
00780 $previous = "<tr{$tr_after}>\n";
00781 }
00782 array_push ( $tr_history , true );
00783 array_push ( $tr_attributes , '' );
00784 array_pop ( $has_opened_tr );
00785 array_push ( $has_opened_tr , true );
00786 }
00787
00788 $last_tag = array_pop ( $last_tag_history );
00789
00790 if ( array_pop ( $td_history ) ) {
00791 $previous = "</{$last_tag}>{$previous}";
00792 }
00793
00794 if ( $first_character === '|' ) {
00795 $last_tag = 'td';
00796 } else if ( $first_character === '!' ) {
00797 $last_tag = 'th';
00798 } else if ( $first_character === '+' ) {
00799 $last_tag = 'caption';
00800 } else {
00801 $last_tag = '';
00802 }
00803
00804 array_push ( $last_tag_history , $last_tag );
00805
00806
00807 $cell_data = explode ( '|' , $cell , 2 );
00808
00809
00810
00811 if ( strpos( $cell_data[0], '[[' ) !== false ) {
00812 $cell = "{$previous}<{$last_tag}>{$cell}";
00813 } else if ( count ( $cell_data ) == 1 )
00814 $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
00815 else {
00816 $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
00817 $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
00818 $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
00819 }
00820
00821 $outLine .= $cell;
00822 array_push ( $td_history , true );
00823 }
00824 }
00825 $out .= $outLine . "\n";
00826 }
00827
00828
00829 while ( count ( $td_history ) > 0 )
00830 {
00831 if ( array_pop ( $td_history ) ) {
00832 $out .= "</td>\n";
00833 }
00834 if ( array_pop ( $tr_history ) ) {
00835 $out .= "</tr>\n";
00836 }
00837 if ( !array_pop ( $has_opened_tr ) ) {
00838 $out .= "<tr><td></td></tr>\n" ;
00839 }
00840
00841 $out .= "</table>\n";
00842 }
00843
00844
00845 if ( substr( $out, -1 ) === "\n" ) {
00846 $out = substr( $out, 0, -1 );
00847 }
00848
00849
00850 if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
00851 $out = '';
00852 }
00853
00854 wfProfileOut( __METHOD__ );
00855
00856 return $out;
00857 }
00858
00865 function internalParse( $text ) {
00866 $isMain = true;
00867 wfProfileIn( __METHOD__ );
00868
00869 # Hook to suspend the parser in this state
00870 if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
00871 wfProfileOut( __METHOD__ );
00872 return $text ;
00873 }
00874
00875 $text = $this->replaceVariables( $text );
00876 $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
00877 wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
00878
00879
00880
00881
00882
00883 $text = $this->doTableStuff( $text );
00884
00885 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
00886
00887 $text = $this->doDoubleUnderscore( $text );
00888 $text = $this->doHeadings( $text );
00889 if( $this->mOptions->getUseDynamicDates() ) {
00890 $df = DateFormatter::getInstance();
00891 $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
00892 }
00893 $text = $this->doAllQuotes( $text );
00894 $text = $this->replaceInternalLinks( $text );
00895 $text = $this->replaceExternalLinks( $text );
00896
00897 # replaceInternalLinks may sometimes leave behind
00898 # absolute URLs, which have to be masked to hide them from replaceExternalLinks
00899 $text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
00900
00901 $text = $this->doMagicLinks( $text );
00902 $text = $this->formatHeadings( $text, $isMain );
00903
00904 wfProfileOut( __METHOD__ );
00905 return $text;
00906 }
00907
00915 function doMagicLinks( $text ) {
00916 wfProfileIn( __METHOD__ );
00917 $prots = $this->mUrlProtocols;
00918 $urlChar = self::EXT_LINK_URL_CLASS;
00919 $text = preg_replace_callback(
00920 '!(?: # Start cases
00921 (<a.*?</a>) | # m[1]: Skip link text
00922 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
00923 (\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . '
00924 (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
00925 ISBN\s+(\b # m[5]: ISBN, capture number
00926 (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix
00927 (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters
00928 [0-9Xx] # check digit
00929 \b)
00930 )!x', array( &$this, 'magicLinkCallback' ), $text );
00931 wfProfileOut( __METHOD__ );
00932 return $text;
00933 }
00934
00935 function magicLinkCallback( $m ) {
00936 if ( isset( $m[1] ) && $m[1] !== '' ) {
00937 # Skip anchor
00938 return $m[0];
00939 } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
00940 # Skip HTML element
00941 return $m[0];
00942 } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
00943 # Free external link
00944 return $this->makeFreeExternalLink( $m[0] );
00945 } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
00946 # RFC or PMID
00947 if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
00948 $keyword = 'RFC';
00949 $urlmsg = 'rfcurl';
00950 $id = $m[4];
00951 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
00952 $keyword = 'PMID';
00953 $urlmsg = 'pubmedurl';
00954 $id = $m[4];
00955 } else {
00956 throw new MWException( __METHOD__.': unrecognised match type "' .
00957 substr($m[0], 0, 20 ) . '"' );
00958 }
00959 $url = wfMsg( $urlmsg, $id);
00960 $sk = $this->mOptions->getSkin();
00961 $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
00962 return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
00963 } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
00964 # ISBN
00965 $isbn = $m[5];
00966 $num = strtr( $isbn, array(
00967 '-' => '',
00968 ' ' => '',
00969 'x' => 'X',
00970 ));
00971 $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
00972 return'<a href="' .
00973 $titleObj->escapeLocalUrl() .
00974 "\" class=\"internal\">ISBN $isbn</a>";
00975 } else {
00976 return $m[0];
00977 }
00978 }
00979
00985 function makeFreeExternalLink( $url ) {
00986 global $wgContLang;
00987 wfProfileIn( __METHOD__ );
00988
00989 $sk = $this->mOptions->getSkin();
00990 $trail = '';
00991
00992 # The characters '<' and '>' (which were escaped by
00993 # removeHTMLtags()) should not be included in
00994 # URLs, per RFC 2396.
00995 $m2 = array();
00996 if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
00997 $trail = substr($url, $m2[0][1]) . $trail;
00998 $url = substr($url, 0, $m2[0][1]);
00999 }
01000
01001 # Move trailing punctuation to $trail
01002 $sep = ',;\.:!?';
01003 # If there is no left bracket, then consider right brackets fair game too
01004 if ( strpos( $url, '(' ) === false ) {
01005 $sep .= ')';
01006 }
01007
01008 $numSepChars = strspn( strrev( $url ), $sep );
01009 if ( $numSepChars ) {
01010 $trail = substr( $url, -$numSepChars ) . $trail;
01011 $url = substr( $url, 0, -$numSepChars );
01012 }
01013
01014 $url = Sanitizer::cleanUrl( $url );
01015
01016 # Is this an external image?
01017 $text = $this->maybeMakeExternalImage( $url );
01018 if ( $text === false ) {
01019 # Not an image, make a link
01020 $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
01021 $this->getExternalLinkAttribs( $url ) );
01022 # Register it in the output object...
01023 # Replace unnecessary URL escape codes with their equivalent characters
01024 $pasteurized = self::replaceUnusualEscapes( $url );
01025 $this->mOutput->addExternalLink( $pasteurized );
01026 }
01027 wfProfileOut( __METHOD__ );
01028 return $text . $trail;
01029 }
01030
01031
01037 function doHeadings( $text ) {
01038 wfProfileIn( __METHOD__ );
01039 for ( $i = 6; $i >= 1; --$i ) {
01040 $h = str_repeat( '=', $i );
01041 $text = preg_replace( "/^$h(.+)$h\\s*$/m",
01042 "<h$i>\\1</h$i>", $text );
01043 }
01044 wfProfileOut( __METHOD__ );
01045 return $text;
01046 }
01047
01053 function doAllQuotes( $text ) {
01054 wfProfileIn( __METHOD__ );
01055 $outtext = '';
01056 $lines = StringUtils::explode( "\n", $text );
01057 foreach ( $lines as $line ) {
01058 $outtext .= $this->doQuotes( $line ) . "\n";
01059 }
01060 $outtext = substr($outtext, 0,-1);
01061 wfProfileOut( __METHOD__ );
01062 return $outtext;
01063 }
01064
01068 public function doQuotes( $text ) {
01069 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
01070 if ( count( $arr ) == 1 )
01071 return $text;
01072 else
01073 {
01074 # First, do some preliminary work. This may shift some apostrophes from
01075 # being mark-up to being text. It also counts the number of occurrences
01076 # of bold and italics mark-ups.
01077 $i = 0;
01078 $numbold = 0;
01079 $numitalics = 0;
01080 foreach ( $arr as $r )
01081 {
01082 if ( ( $i % 2 ) == 1 )
01083 {
01084 # If there are ever four apostrophes, assume the first is supposed to
01085 # be text, and the remaining three constitute mark-up for bold text.
01086 if ( strlen( $arr[$i] ) == 4 )
01087 {
01088 $arr[$i-1] .= "'";
01089 $arr[$i] = "'''";
01090 }
01091 # If there are more than 5 apostrophes in a row, assume they're all
01092 # text except for the last 5.
01093 else if ( strlen( $arr[$i] ) > 5 )
01094 {
01095 $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
01096 $arr[$i] = "'''''";
01097 }
01098 # Count the number of occurrences of bold and italics mark-ups.
01099 # We are not counting sequences of five apostrophes.
01100 if ( strlen( $arr[$i] ) == 2 ) { $numitalics++; }
01101 else if ( strlen( $arr[$i] ) == 3 ) { $numbold++; }
01102 else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
01103 }
01104 $i++;
01105 }
01106
01107 # If there is an odd number of both bold and italics, it is likely
01108 # that one of the bold ones was meant to be an apostrophe followed
01109 # by italics. Which one we cannot know for certain, but it is more
01110 # likely to be one that has a single-letter word before it.
01111 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
01112 {
01113 $i = 0;
01114 $firstsingleletterword = -1;
01115 $firstmultiletterword = -1;
01116 $firstspace = -1;
01117 foreach ( $arr as $r )
01118 {
01119 if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
01120 {
01121 $x1 = substr ($arr[$i-1], -1);
01122 $x2 = substr ($arr[$i-1], -2, 1);
01123 if ($x1 === ' ') {
01124 if ($firstspace == -1) $firstspace = $i;
01125 } else if ($x2 === ' ') {
01126 if ($firstsingleletterword == -1) $firstsingleletterword = $i;
01127 } else {
01128 if ($firstmultiletterword == -1) $firstmultiletterword = $i;
01129 }
01130 }
01131 $i++;
01132 }
01133
01134 # If there is a single-letter word, use it!
01135 if ($firstsingleletterword > -1)
01136 {
01137 $arr [ $firstsingleletterword ] = "''";
01138 $arr [ $firstsingleletterword-1 ] .= "'";
01139 }
01140 # If not, but there's a multi-letter word, use that one.
01141 else if ($firstmultiletterword > -1)
01142 {
01143 $arr [ $firstmultiletterword ] = "''";
01144 $arr [ $firstmultiletterword-1 ] .= "'";
01145 }
01146 # ... otherwise use the first one that has neither.
01147 # (notice that it is possible for all three to be -1 if, for example,
01148 # there is only one pentuple-apostrophe in the line)
01149 else if ($firstspace > -1)
01150 {
01151 $arr [ $firstspace ] = "''";
01152 $arr [ $firstspace-1 ] .= "'";
01153 }
01154 }
01155
01156 # Now let's actually convert our apostrophic mush to HTML!
01157 $output = '';
01158 $buffer = '';
01159 $state = '';
01160 $i = 0;
01161 foreach ($arr as $r)
01162 {
01163 if (($i % 2) == 0)
01164 {
01165 if ($state === 'both')
01166 $buffer .= $r;
01167 else
01168 $output .= $r;
01169 }
01170 else
01171 {
01172 if (strlen ($r) == 2)
01173 {
01174 if ($state === 'i')
01175 { $output .= '</i>'; $state = ''; }
01176 else if ($state === 'bi')
01177 { $output .= '</i>'; $state = 'b'; }
01178 else if ($state === 'ib')
01179 { $output .= '</b></i><b>'; $state = 'b'; }
01180 else if ($state === 'both')
01181 { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
01182 else # $state can be 'b' or ''
01183 { $output .= '<i>'; $state .= 'i'; }
01184 }
01185 else if (strlen ($r) == 3)
01186 {
01187 if ($state === 'b')
01188 { $output .= '</b>'; $state = ''; }
01189 else if ($state === 'bi')
01190 { $output .= '</i></b><i>'; $state = 'i'; }
01191 else if ($state === 'ib')
01192 { $output .= '</b>'; $state = 'i'; }
01193 else if ($state === 'both')
01194 { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
01195 else # $state can be 'i' or ''
01196 { $output .= '<b>'; $state .= 'b'; }
01197 }
01198 else if (strlen ($r) == 5)
01199 {
01200 if ($state === 'b')
01201 { $output .= '</b><i>'; $state = 'i'; }
01202 else if ($state === 'i')
01203 { $output .= '</i><b>'; $state = 'b'; }
01204 else if ($state === 'bi')
01205 { $output .= '</i></b>'; $state = ''; }
01206 else if ($state === 'ib')
01207 { $output .= '</b></i>'; $state = ''; }
01208 else if ($state === 'both')
01209 { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
01210 else # ($state == '')
01211 { $buffer = ''; $state = 'both'; }
01212 }
01213 }
01214 $i++;
01215 }
01216 # Now close all remaining tags. Notice that the order is important.
01217 if ($state === 'b' || $state === 'ib')
01218 $output .= '</b>';
01219 if ($state === 'i' || $state === 'bi' || $state === 'ib')
01220 $output .= '</i>';
01221 if ($state === 'bi')
01222 $output .= '</b>';
01223 # There might be lonely ''''', so make sure we have a buffer
01224 if ($state === 'both' && $buffer)
01225 $output .= '<b><i>'.$buffer.'</i></b>';
01226 return $output;
01227 }
01228 }
01229
01238 function replaceExternalLinks( $text ) {
01239 global $wgContLang;
01240 wfProfileIn( __METHOD__ );
01241
01242 $sk = $this->mOptions->getSkin();
01243
01244 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
01245 $s = array_shift( $bits );
01246
01247 $i = 0;
01248 while ( $i<count( $bits ) ) {
01249 $url = $bits[$i++];
01250 $protocol = $bits[$i++];
01251 $text = $bits[$i++];
01252 $trail = $bits[$i++];
01253
01254 # The characters '<' and '>' (which were escaped by
01255 # removeHTMLtags()) should not be included in
01256 # URLs, per RFC 2396.
01257 $m2 = array();
01258 if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
01259 $text = substr($url, $m2[0][1]) . ' ' . $text;
01260 $url = substr($url, 0, $m2[0][1]);
01261 }
01262
01263 # If the link text is an image URL, replace it with an <img> tag
01264 # This happened by accident in the original parser, but some people used it extensively
01265 $img = $this->maybeMakeExternalImage( $text );
01266 if ( $img !== false ) {
01267 $text = $img;
01268 }
01269
01270 $dtrail = '';
01271
01272 # Set linktype for CSS - if URL==text, link is essentially free
01273 $linktype = ($text === $url) ? 'free' : 'text';
01274
01275 # No link text, e.g. [http://domain.tld/some.link]
01276 if ( $text == '' ) {
01277 # Autonumber if allowed. See bug #5918
01278 if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
01279 $langObj = $this->getFunctionLang();
01280 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
01281 $linktype = 'autonumber';
01282 } else {
01283 # Otherwise just use the URL
01284 $text = htmlspecialchars( $url );
01285 $linktype = 'free';
01286 }
01287 } else {
01288 # Have link text, e.g. [http://domain.tld/some.link text]s
01289 # Check for trail
01290 list( $dtrail, $trail ) = Linker::splitTrail( $trail );
01291 }
01292
01293 $text = $wgContLang->markNoConversion($text);
01294
01295 $url = Sanitizer::cleanUrl( $url );
01296
01297 # Use the encoded URL
01298 # This means that users can paste URLs directly into the text
01299 # Funny characters like ö aren't valid in URLs anyway
01300 # This was changed in August 2004
01301 $s .= $sk->makeExternalLink( $url, $text, false, $linktype,
01302 $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
01303
01304 # Register link in the output object.
01305 # Replace unnecessary URL escape codes with the referenced character
01306 # This prevents spammers from hiding links from the filters
01307 $pasteurized = self::replaceUnusualEscapes( $url );
01308 $this->mOutput->addExternalLink( $pasteurized );
01309 }
01310
01311 wfProfileOut( __METHOD__ );
01312 return $s;
01313 }
01314
01325 function getExternalLinkAttribs( $url = false ) {
01326 $attribs = array();
01327 global $wgNoFollowLinks, $wgNoFollowNsExceptions;
01328 $ns = $this->mTitle->getNamespace();
01329 if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) {
01330 $attribs['rel'] = 'nofollow';
01331
01332 global $wgNoFollowDomainExceptions;
01333 if ( $wgNoFollowDomainExceptions ) {
01334 $bits = wfParseUrl( $url );
01335 if ( is_array( $bits ) && isset( $bits['host'] ) ) {
01336 foreach ( $wgNoFollowDomainExceptions as $domain ) {
01337 if( substr( $bits['host'], -strlen( $domain ) )
01338 == $domain ) {
01339 unset( $attribs['rel'] );
01340 break;
01341 }
01342 }
01343 }
01344 }
01345 }
01346 if ( $this->mOptions->getExternalLinkTarget() ) {
01347 $attribs['target'] = $this->mOptions->getExternalLinkTarget();
01348 }
01349 return $attribs;
01350 }
01351
01352
01363 static function replaceUnusualEscapes( $url ) {
01364 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
01365 array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
01366 }
01367
01374 private static function replaceUnusualEscapesCallback( $matches ) {
01375 $char = urldecode( $matches[0] );
01376 $ord = ord( $char );
01377
01378 if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
01379
01380 return $char;
01381 } else {
01382
01383 return $matches[0];
01384 }
01385 }
01386
01392 function maybeMakeExternalImage( $url ) {
01393 $sk = $this->mOptions->getSkin();
01394 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
01395 $imagesexception = !empty($imagesfrom);
01396 $text = false;
01397 # $imagesfrom could be either a single string or an array of strings, parse out the latter
01398 if( $imagesexception && is_array( $imagesfrom ) ) {
01399 $imagematch = false;
01400 foreach( $imagesfrom as $match ) {
01401 if( strpos( $url, $match ) === 0 ) {
01402 $imagematch = true;
01403 break;
01404 }
01405 }
01406 } elseif( $imagesexception ) {
01407 $imagematch = (strpos( $url, $imagesfrom ) === 0);
01408 } else {
01409 $imagematch = false;
01410 }
01411 if ( $this->mOptions->getAllowExternalImages()
01412 || ( $imagesexception && $imagematch ) ) {
01413 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
01414 # Image found
01415 $text = $sk->makeExternalImage( $url );
01416 }
01417 }
01418 if( !$text && $this->mOptions->getEnableImageWhitelist()
01419 && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
01420 $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
01421 foreach( $whitelist as $entry ) {
01422 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
01423 if( strpos( $entry, '#' ) === 0 || $entry === '' )
01424 continue;
01425 if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
01426 # Image matches a whitelist entry
01427 $text = $sk->makeExternalImage( $url );
01428 break;
01429 }
01430 }
01431 }
01432 return $text;
01433 }
01434
01441 function replaceInternalLinks( $s ) {
01442 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
01443 return $s;
01444 }
01445
01452 function replaceInternalLinks2( &$s ) {
01453 global $wgContLang;
01454
01455 wfProfileIn( __METHOD__ );
01456
01457 wfProfileIn( __METHOD__.'-setup' );
01458 static $tc = FALSE, $e1, $e1_img;
01459 # the % is needed to support urlencoded titles as well
01460 if ( !$tc ) {
01461 $tc = Title::legalChars() . '#%';
01462 # Match a link having the form [[namespace:link|alternate]]trail
01463 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
01464 # Match cases where there is no "]]", which might still be images
01465 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
01466 }
01467
01468 $sk = $this->mOptions->getSkin();
01469 $holders = new LinkHolderArray( $this );
01470
01471 #split the entire text string on occurences of [[
01472 $a = StringUtils::explode( '[[', ' ' . $s );
01473 #get the first element (all text up to first [[), and remove the space we added
01474 $s = $a->current();
01475 $a->next();
01476 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
01477 $s = substr( $s, 1 );
01478
01479 $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
01480 $e2 = null;
01481 if ( $useLinkPrefixExtension ) {
01482 # Match the end of a line for a word that's not followed by whitespace,
01483 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
01484 $e2 = wfMsgForContent( 'linkprefix' );
01485 }
01486
01487 if( is_null( $this->mTitle ) ) {
01488 wfProfileOut( __METHOD__.'-setup' );
01489 wfProfileOut( __METHOD__ );
01490 throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
01491 }
01492 $nottalk = !$this->mTitle->isTalkPage();
01493
01494 if ( $useLinkPrefixExtension ) {
01495 $m = array();
01496 if ( preg_match( $e2, $s, $m ) ) {
01497 $first_prefix = $m[2];
01498 } else {
01499 $first_prefix = false;
01500 }
01501 } else {
01502 $prefix = '';
01503 }
01504
01505 if($wgContLang->hasVariants()) {
01506 $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
01507 } else {
01508 $selflink = array($this->mTitle->getPrefixedText());
01509 }
01510 $useSubpages = $this->areSubpagesAllowed();
01511 wfProfileOut( __METHOD__.'-setup' );
01512
01513 # Loop for each link
01514 for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
01515 # Check for excessive memory usage
01516 if ( $holders->isBig() ) {
01517 # Too big
01518 # Do the existence check, replace the link holders and clear the array
01519 $holders->replace( $s );
01520 $holders->clear();
01521 }
01522
01523 if ( $useLinkPrefixExtension ) {
01524 wfProfileIn( __METHOD__.'-prefixhandling' );
01525 if ( preg_match( $e2, $s, $m ) ) {
01526 $prefix = $m[2];
01527 $s = $m[1];
01528 } else {
01529 $prefix='';
01530 }
01531 # first link
01532 if($first_prefix) {
01533 $prefix = $first_prefix;
01534 $first_prefix = false;
01535 }
01536 wfProfileOut( __METHOD__.'-prefixhandling' );
01537 }
01538
01539 $might_be_img = false;
01540
01541 wfProfileIn( __METHOD__."-e1" );
01542 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
01543 $text = $m[2];
01544 # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
01545 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
01546 # the real problem is with the $e1 regex
01547 # See bug 1300.
01548 #
01549 # Still some problems for cases where the ] is meant to be outside punctuation,
01550 # and no image is in sight. See bug 2095.
01551 #
01552 if( $text !== '' &&
01553 substr( $m[3], 0, 1 ) === ']' &&
01554 strpos($text, '[') !== false
01555 )
01556 {
01557 $text .= ']'; # so that replaceExternalLinks($text) works later
01558 $m[3] = substr( $m[3], 1 );
01559 }
01560 # fix up urlencoded title texts
01561 if( strpos( $m[1], '%' ) !== false ) {
01562 # Should anchors '#' also be rejected?
01563 $m[1] = str_replace( array('<', '>'), array('<', '>'), urldecode($m[1]) );
01564 }
01565 $trail = $m[3];
01566 } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
01567 $might_be_img = true;
01568 $text = $m[2];
01569 if ( strpos( $m[1], '%' ) !== false ) {
01570 $m[1] = urldecode($m[1]);
01571 }
01572 $trail = "";
01573 } else { # Invalid form; output directly
01574 $s .= $prefix . '[[' . $line ;
01575 wfProfileOut( __METHOD__."-e1" );
01576 continue;
01577 }
01578 wfProfileOut( __METHOD__."-e1" );
01579 wfProfileIn( __METHOD__."-misc" );
01580
01581 # Don't allow internal links to pages containing
01582 # PROTO: where PROTO is a valid URL protocol; these
01583 # should be external links.
01584 if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
01585 $s .= $prefix . '[[' . $line ;
01586 wfProfileOut( __METHOD__."-misc" );
01587 continue;
01588 }
01589
01590 # Make subpage if necessary
01591 if( $useSubpages ) {
01592 $link = $this->maybeDoSubpageLink( $m[1], $text );
01593 } else {
01594 $link = $m[1];
01595 }
01596
01597 $noforce = (substr($m[1], 0, 1) !== ':');
01598 if (!$noforce) {
01599 # Strip off leading ':'
01600 $link = substr($link, 1);
01601 }
01602
01603 wfProfileOut( __METHOD__."-misc" );
01604 wfProfileIn( __METHOD__."-title" );
01605 $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
01606 if( $nt === NULL ) {
01607 $s .= $prefix . '[[' . $line;
01608 wfProfileOut( __METHOD__."-title" );
01609 continue;
01610 }
01611
01612 $ns = $nt->getNamespace();
01613 $iw = $nt->getInterWiki();
01614 wfProfileOut( __METHOD__."-title" );
01615
01616 if ($might_be_img) { # if this is actually an invalid link
01617 wfProfileIn( __METHOD__."-might_be_img" );
01618 if ($ns == NS_FILE && $noforce) { #but might be an image
01619 $found = false;
01620 while ( true ) {
01621 #look at the next 'line' to see if we can close it there
01622 $a->next();
01623 $next_line = $a->current();
01624 if ( $next_line === false || $next_line === null ) {
01625 break;
01626 }
01627 $m = explode( ']]', $next_line, 3 );
01628 if ( count( $m ) == 3 ) {
01629 # the first ]] closes the inner link, the second the image
01630 $found = true;
01631 $text .= "[[{$m[0]}]]{$m[1]}";
01632 $trail = $m[2];
01633 break;
01634 } elseif ( count( $m ) == 2 ) {
01635 #if there's exactly one ]] that's fine, we'll keep looking
01636 $text .= "[[{$m[0]}]]{$m[1]}";
01637 } else {
01638 #if $next_line is invalid too, we need look no further
01639 $text .= '[[' . $next_line;
01640 break;
01641 }
01642 }
01643 if ( !$found ) {
01644 # we couldn't find the end of this imageLink, so output it raw
01645 #but don't ignore what might be perfectly normal links in the text we've examined
01646 $holders->merge( $this->replaceInternalLinks2( $text ) );
01647 $s .= "{$prefix}[[$link|$text";
01648 # note: no $trail, because without an end, there *is* no trail
01649 wfProfileOut( __METHOD__."-might_be_img" );
01650 continue;
01651 }
01652 } else { #it's not an image, so output it raw
01653 $s .= "{$prefix}[[$link|$text";
01654 # note: no $trail, because without an end, there *is* no trail
01655 wfProfileOut( __METHOD__."-might_be_img" );
01656 continue;
01657 }
01658 wfProfileOut( __METHOD__."-might_be_img" );
01659 }
01660
01661 $wasblank = ( '' == $text );
01662 if( $wasblank ) $text = $link;
01663
01664 # Link not escaped by : , create the various objects
01665 if( $noforce ) {
01666
01667 # Interwikis
01668 wfProfileIn( __METHOD__."-interwiki" );
01669 if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
01670 $this->mOutput->addLanguageLink( $nt->getFullText() );
01671 $s = rtrim($s . $prefix);
01672 $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
01673 wfProfileOut( __METHOD__."-interwiki" );
01674 continue;
01675 }
01676 wfProfileOut( __METHOD__."-interwiki" );
01677
01678 if ( $ns == NS_FILE ) {
01679 wfProfileIn( __METHOD__."-image" );
01680 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
01681 # recursively parse links inside the image caption
01682 # actually, this will parse them in any other parameters, too,
01683 # but it might be hard to fix that, and it doesn't matter ATM
01684 $text = $this->replaceExternalLinks($text);
01685 $holders->merge( $this->replaceInternalLinks2( $text ) );
01686
01687 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
01688 $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
01689 }
01690 $this->mOutput->addImage( $nt->getDBkey() );
01691 wfProfileOut( __METHOD__."-image" );
01692 continue;
01693
01694 }
01695
01696 if ( $ns == NS_CATEGORY ) {
01697 wfProfileIn( __METHOD__."-category" );
01698 $s = rtrim($s . "\n"); # bug 87
01699
01700 if ( $wasblank ) {
01701 $sortkey = $this->getDefaultSort();
01702 } else {
01703 $sortkey = $text;
01704 }
01705 $sortkey = Sanitizer::decodeCharReferences( $sortkey );
01706 $sortkey = str_replace( "\n", '', $sortkey );
01707 $sortkey = $wgContLang->convertCategoryKey( $sortkey );
01708 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
01709
01714 $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
01715
01716 wfProfileOut( __METHOD__."-category" );
01717 continue;
01718 }
01719 }
01720
01721 # Self-link checking
01722 if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
01723 if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
01724 $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
01725 continue;
01726 }
01727 }
01728
01729 # NS_MEDIA is a pseudo-namespace for linking directly to a file
01730 # FIXME: Should do batch file existence checks, see comment below
01731 if( $ns == NS_MEDIA ) {
01732 wfProfileIn( __METHOD__."-media" );
01733 # Give extensions a chance to select the file revision for us
01734 $skip = $time = false;
01735 wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
01736 if ( $skip ) {
01737 $link = $sk->link( $nt );
01738 } else {
01739 $link = $sk->makeMediaLinkObj( $nt, $text, $time );
01740 }
01741 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
01742 $s .= $prefix . $this->armorLinks( $link ) . $trail;
01743 $this->mOutput->addImage( $nt->getDBkey() );
01744 wfProfileOut( __METHOD__."-media" );
01745 continue;
01746 }
01747
01748 wfProfileIn( __METHOD__."-always_known" );
01749 # Some titles, such as valid special pages or files in foreign repos, should
01750 # be shown as bluelinks even though they're not included in the page table
01751 #
01752 # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
01753 # batch file existence checks for NS_FILE and NS_MEDIA
01754 if( $iw == '' && $nt->isAlwaysKnown() ) {
01755 $this->mOutput->addLink( $nt );
01756 $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
01757 } else {
01758 # Links will be added to the output link list after checking
01759 $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
01760 }
01761 wfProfileOut( __METHOD__."-always_known" );
01762 }
01763 wfProfileOut( __METHOD__ );
01764 return $holders;
01765 }
01766
01775 function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
01776 return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix );
01777 }
01778
01793 function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
01794 list( $inside, $trail ) = Linker::splitTrail( $trail );
01795 $sk = $this->mOptions->getSkin();
01796 $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
01797 return $this->armorLinks( $link ) . $trail;
01798 }
01799
01810 function armorLinks( $text ) {
01811 return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
01812 "{$this->mUniqPrefix}NOPARSE$1", $text );
01813 }
01814
01819 function areSubpagesAllowed() {
01820 # Some namespaces don't allow subpages
01821 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
01822 }
01823
01831 function maybeDoSubpageLink($target, &$text) {
01832 # Valid link forms:
01833 # Foobar -- normal
01834 # :Foobar -- override special treatment of prefix (images, language links)
01835 # /Foobar -- convert to CurrentPage/Foobar
01836 # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
01837 # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
01838 # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
01839
01840 wfProfileIn( __METHOD__ );
01841 $ret = $target; # default return value is no change
01842
01843 # Some namespaces don't allow subpages,
01844 # so only perform processing if subpages are allowed
01845 if( $this->areSubpagesAllowed() ) {
01846 $hash = strpos( $target, '#' );
01847 if( $hash !== false ) {
01848 $suffix = substr( $target, $hash );
01849 $target = substr( $target, 0, $hash );
01850 } else {
01851 $suffix = '';
01852 }
01853 # bug 7425
01854 $target = trim( $target );
01855 # Look at the first character
01856 if( $target != '' && $target{0} === '/' ) {
01857 # / at end means we don't want the slash to be shown
01858 $m = array();
01859 $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
01860 if( $trailingSlashes ) {
01861 $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
01862 } else {
01863 $noslash = substr( $target, 1 );
01864 }
01865
01866 $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
01867 if( '' === $text ) {
01868 $text = $target . $suffix;
01869 } # this might be changed for ugliness reasons
01870 } else {
01871 # check for .. subpage backlinks
01872 $dotdotcount = 0;
01873 $nodotdot = $target;
01874 while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
01875 ++$dotdotcount;
01876 $nodotdot = substr( $nodotdot, 3 );
01877 }
01878 if($dotdotcount > 0) {
01879 $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
01880 if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
01881 $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
01882 # / at the end means don't show full path
01883 if( substr( $nodotdot, -1, 1 ) === '/' ) {
01884 $nodotdot = substr( $nodotdot, 0, -1 );
01885 if( '' === $text ) {
01886 $text = $nodotdot . $suffix;
01887 }
01888 }
01889 $nodotdot = trim( $nodotdot );
01890 if( $nodotdot != '' ) {
01891 $ret .= '/' . $nodotdot;
01892 }
01893 $ret .= $suffix;
01894 }
01895 }
01896 }
01897 }
01898
01899 wfProfileOut( __METHOD__ );
01900 return $ret;
01901 }
01902
01907 function closeParagraph() {
01908 $result = '';
01909 if ( '' != $this->mLastSection ) {
01910 $result = '</' . $this->mLastSection . ">\n";
01911 }
01912 $this->mInPre = false;
01913 $this->mLastSection = '';
01914 return $result;
01915 }
01916 # getCommon() returns the length of the longest common substring
01917 # of both arguments, starting at the beginning of both.
01918 #
01919 function getCommon( $st1, $st2 ) {
01920 $fl = strlen( $st1 );
01921 $shorter = strlen( $st2 );
01922 if ( $fl < $shorter ) { $shorter = $fl; }
01923
01924 for ( $i = 0; $i < $shorter; ++$i ) {
01925 if ( $st1{$i} != $st2{$i} ) { break; }
01926 }
01927 return $i;
01928 }
01929 # These next three functions open, continue, and close the list
01930 # element appropriate to the prefix character passed into them.
01931 #
01932 function openList( $char ) {
01933 $result = $this->closeParagraph();
01934
01935 if ( '*' === $char ) { $result .= '<ul><li>'; }
01936 else if ( '#' === $char ) { $result .= '<ol><li>'; }
01937 else if ( ':' === $char ) { $result .= '<dl><dd>'; }
01938 else if ( ';' === $char ) {
01939 $result .= '<dl><dt>';
01940 $this->mDTopen = true;
01941 }
01942 else { $result = '<!-- ERR 1 -->'; }
01943
01944 return $result;
01945 }
01946
01947 function nextItem( $char ) {
01948 if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
01949 else if ( ':' === $char || ';' === $char ) {
01950 $close = '</dd>';
01951 if ( $this->mDTopen ) { $close = '</dt>'; }
01952 if ( ';' === $char ) {
01953 $this->mDTopen = true;
01954 return $close . '<dt>';
01955 } else {
01956 $this->mDTopen = false;
01957 return $close . '<dd>';
01958 }
01959 }
01960 return '<!-- ERR 2 -->';
01961 }
01962
01963 function closeList( $char ) {
01964 if ( '*' === $char ) { $text = '</li></ul>'; }
01965 else if ( '#' === $char ) { $text = '</li></ol>'; }
01966 else if ( ':' === $char ) {
01967 if ( $this->mDTopen ) {
01968 $this->mDTopen = false;
01969 $text = '</dt></dl>';
01970 } else {
01971 $text = '</dd></dl>';
01972 }
01973 }
01974 else { return '<!-- ERR 3 -->'; }
01975 return $text."\n";
01976 }
01985 function doBlockLevels( $text, $linestart ) {
01986 wfProfileIn( __METHOD__ );
01987
01988 # Parsing through the text line by line. The main thing
01989 # happening here is handling of block-level elements p, pre,
01990 # and making lists from lines starting with * # : etc.
01991 #
01992 $textLines = StringUtils::explode( "\n", $text );
01993
01994 $lastPrefix = $output = '';
01995 $this->mDTopen = $inBlockElem = false;
01996 $prefixLength = 0;
01997 $paragraphStack = false;
01998
01999 foreach ( $textLines as $oLine ) {
02000 # Fix up $linestart
02001 if ( !$linestart ) {
02002 $output .= $oLine;
02003 $linestart = true;
02004 continue;
02005 }
02006
02007 $lastPrefixLength = strlen( $lastPrefix );
02008 $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
02009 $preOpenMatch = preg_match('/<pre/i', $oLine );
02010 if ( !$this->mInPre ) {
02011 # Multiple prefixes may abut each other for nested lists.
02012 $prefixLength = strspn( $oLine, '*#:;' );
02013 $prefix = substr( $oLine, 0, $prefixLength );
02014
02015 # eh?
02016 $prefix2 = str_replace( ';', ':', $prefix );
02017 $t = substr( $oLine, $prefixLength );
02018 $this->mInPre = (bool)$preOpenMatch;
02019 } else {
02020 # Don't interpret any other prefixes in preformatted text
02021 $prefixLength = 0;
02022 $prefix = $prefix2 = '';
02023 $t = $oLine;
02024 }
02025
02026 # List generation
02027 if( $prefixLength && $lastPrefix === $prefix2 ) {
02028 # Same as the last item, so no need to deal with nesting or opening stuff
02029 $output .= $this->nextItem( substr( $prefix, -1 ) );
02030 $paragraphStack = false;
02031
02032 if ( substr( $prefix, -1 ) === ';') {
02033 # The one nasty exception: definition lists work like this:
02034 # ; title : definition text
02035 # So we check for : in the remainder text to split up the
02036 # title and definition, without b0rking links.
02037 $term = $t2 = '';
02038 if ($this->findColonNoLinks($t, $term, $t2) !== false) {
02039 $t = $t2;
02040 $output .= $term . $this->nextItem( ':' );
02041 }
02042 }
02043 } elseif( $prefixLength || $lastPrefixLength ) {
02044 # Either open or close a level...
02045 $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
02046 $paragraphStack = false;
02047
02048 while( $commonPrefixLength < $lastPrefixLength ) {
02049 $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
02050 --$lastPrefixLength;
02051 }
02052 if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
02053 $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
02054 }
02055 while ( $prefixLength > $commonPrefixLength ) {
02056 $char = substr( $prefix, $commonPrefixLength, 1 );
02057 $output .= $this->openList( $char );
02058
02059 if ( ';' === $char ) {
02060 # FIXME: This is dupe of code above
02061 if ($this->findColonNoLinks($t, $term, $t2) !== false) {
02062 $t = $t2;
02063 $output .= $term . $this->nextItem( ':' );
02064 }
02065 }
02066 ++$commonPrefixLength;
02067 }
02068 $lastPrefix = $prefix2;
02069 }
02070 if( 0 == $prefixLength ) {
02071 wfProfileIn( __METHOD__."-paragraph" );
02072 # No prefix (not in list)--go to paragraph mode
02073
02074 $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
02075 $closematch = preg_match(
02076 '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
02077 '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
02078 if ( $openmatch or $closematch ) {
02079 $paragraphStack = false;
02080 #Â TODO bug 5718: paragraph closed
02081 $output .= $this->closeParagraph();
02082 if ( $preOpenMatch and !$preCloseMatch ) {
02083 $this->mInPre = true;
02084 }
02085 if ( $closematch ) {
02086 $inBlockElem = false;
02087 } else {
02088 $inBlockElem = true;
02089 }
02090 } else if ( !$inBlockElem && !$this->mInPre ) {
02091 if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
02092
02093 if ($this->mLastSection !== 'pre') {
02094 $paragraphStack = false;
02095 $output .= $this->closeParagraph().'<pre>';
02096 $this->mLastSection = 'pre';
02097 }
02098 $t = substr( $t, 1 );
02099 } else {
02100
02101 if ( '' == trim($t) ) {
02102 if ( $paragraphStack ) {
02103 $output .= $paragraphStack.'<br />';
02104 $paragraphStack = false;
02105 $this->mLastSection = 'p';
02106 } else {
02107 if ($this->mLastSection !== 'p' ) {
02108 $output .= $this->closeParagraph();
02109 $this->mLastSection = '';
02110 $paragraphStack = '<p>';
02111 } else {
02112 $paragraphStack = '</p><p>';
02113 }
02114 }
02115 } else {
02116 if ( $paragraphStack ) {
02117 $output .= $paragraphStack;
02118 $paragraphStack = false;
02119 $this->mLastSection = 'p';
02120 } else if ($this->mLastSection !== 'p') {
02121 $output .= $this->closeParagraph().'<p>';
02122 $this->mLastSection = 'p';
02123 }
02124 }
02125 }
02126 }
02127 wfProfileOut( __METHOD__."-paragraph" );
02128 }
02129
02130 if($preCloseMatch && $this->mInPre) {
02131 $this->mInPre = false;
02132 }
02133 if ($paragraphStack === false) {
02134 $output .= $t."\n";
02135 }
02136 }
02137 while ( $prefixLength ) {
02138 $output .= $this->closeList( $prefix2[$prefixLength-1] );
02139 --$prefixLength;
02140 }
02141 if ( '' != $this->mLastSection ) {
02142 $output .= '</' . $this->mLastSection . '>';
02143 $this->mLastSection = '';
02144 }
02145
02146 wfProfileOut( __METHOD__ );
02147 return $output;
02148 }
02149
02158 function findColonNoLinks($str, &$before, &$after) {
02159 wfProfileIn( __METHOD__ );
02160
02161 $pos = strpos( $str, ':' );
02162 if( $pos === false ) {
02163
02164 wfProfileOut( __METHOD__ );
02165 return false;
02166 }
02167
02168 $lt = strpos( $str, '<' );
02169 if( $lt === false || $lt > $pos ) {
02170
02171 $before = substr( $str, 0, $pos );
02172 $after = substr( $str, $pos+1 );
02173 wfProfileOut( __METHOD__ );
02174 return $pos;
02175 }
02176
02177
02178 $state = self::COLON_STATE_TEXT;
02179 $stack = 0;
02180 $len = strlen( $str );
02181 for( $i = 0; $i < $len; $i++ ) {
02182 $c = $str{$i};
02183
02184 switch( $state ) {
02185
02186 case 0:
02187 switch( $c ) {
02188 case "<":
02189
02190 $state = self::COLON_STATE_TAGSTART;
02191 break;
02192 case ":":
02193 if( $stack == 0 ) {
02194
02195 $before = substr( $str, 0, $i );
02196 $after = substr( $str, $i + 1 );
02197 wfProfileOut( __METHOD__ );
02198 return $i;
02199 }
02200
02201 break;
02202 default:
02203
02204 $colon = strpos( $str, ':', $i );
02205 if( $colon === false ) {
02206
02207 wfProfileOut( __METHOD__ );
02208 return false;
02209 }
02210 $lt = strpos( $str, '<', $i );
02211 if( $stack === 0 ) {
02212 if( $lt === false || $colon < $lt ) {
02213
02214 $before = substr( $str, 0, $colon );
02215 $after = substr( $str, $colon + 1 );
02216 wfProfileOut( __METHOD__ );
02217 return $i;
02218 }
02219 }
02220 if( $lt === false ) {
02221
02222
02223 break 2;
02224 }
02225
02226 $i = $lt;
02227 $state = self::COLON_STATE_TAGSTART;
02228 }
02229 break;
02230 case 1:
02231
02232 switch( $c ) {
02233 case ">":
02234 $stack++;
02235 $state = self::COLON_STATE_TEXT;
02236 break;
02237 case "/":
02238
02239 $state = self::COLON_STATE_TAGSLASH;
02240 break;
02241 default:
02242
02243 }
02244 break;
02245 case 2:
02246 switch( $c ) {
02247 case "/":
02248 $state = self::COLON_STATE_CLOSETAG;
02249 break;
02250 case "!":
02251 $state = self::COLON_STATE_COMMENT;
02252 break;
02253 case ">":
02254
02255 $state = self::COLON_STATE_TEXT;
02256 break;
02257 default:
02258 $state = self::COLON_STATE_TAG;
02259 }
02260 break;
02261 case 3:
02262
02263 if( $c === ">" ) {
02264 $stack--;
02265 if( $stack < 0 ) {
02266 wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
02267 wfProfileOut( __METHOD__ );
02268 return false;
02269 }
02270 $state = self::COLON_STATE_TEXT;
02271 }
02272 break;
02273 case self::COLON_STATE_TAGSLASH:
02274 if( $c === ">" ) {
02275
02276 $state = self::COLON_STATE_TEXT;
02277 } else {
02278
02279 $state = self::COLON_STATE_TAG;
02280 }
02281 break;
02282 case 5:
02283 if( $c === "-" ) {
02284 $state = self::COLON_STATE_COMMENTDASH;
02285 }
02286 break;
02287 case self::COLON_STATE_COMMENTDASH:
02288 if( $c === "-" ) {
02289 $state = self::COLON_STATE_COMMENTDASHDASH;
02290 } else {
02291 $state = self::COLON_STATE_COMMENT;
02292 }
02293 break;
02294 case self::COLON_STATE_COMMENTDASHDASH:
02295 if( $c === ">" ) {
02296 $state = self::COLON_STATE_TEXT;
02297 } else {
02298 $state = self::COLON_STATE_COMMENT;
02299 }
02300 break;
02301 default:
02302 throw new MWException( "State machine error in " . __METHOD__ );
02303 }
02304 }
02305 if( $stack > 0 ) {
02306 wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
02307 return false;
02308 }
02309 wfProfileOut( __METHOD__ );
02310 return false;
02311 }
02312
02318 function getVariableValue( $index ) {
02319 global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
02320
02325 if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
02326 if ( isset( $this->mVarCache[$index] ) ) {
02327 return $this->mVarCache[$index];
02328 }
02329 }
02330
02331 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
02332 wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
02333
02334 # Use the time zone
02335 global $wgLocaltimezone;
02336 if ( isset( $wgLocaltimezone ) ) {
02337 $oldtz = getenv( 'TZ' );
02338 putenv( 'TZ='.$wgLocaltimezone );
02339 }
02340
02341 wfSuppressWarnings();
02342 $localTimestamp = date( 'YmdHis', $ts );
02343 $localMonth = date( 'm', $ts );
02344 $localMonthName = date( 'n', $ts );
02345 $localDay = date( 'j', $ts );
02346 $localDay2 = date( 'd', $ts );
02347 $localDayOfWeek = date( 'w', $ts );
02348 $localWeek = date( 'W', $ts );
02349 $localYear = date( 'Y', $ts );
02350 $localHour = date( 'H', $ts );
02351 if ( isset( $wgLocaltimezone ) ) {
02352 putenv( 'TZ='.$oldtz );
02353 }
02354 wfRestoreWarnings();
02355
02356 switch ( $index ) {
02357 case 'currentmonth':
02358 return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
02359 case 'currentmonthname':
02360 return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
02361 case 'currentmonthnamegen':
02362 return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
02363 case 'currentmonthabbrev':
02364 return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
02365 case 'currentday':
02366 return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
02367 case 'currentday2':
02368 return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
02369 case 'localmonth':
02370 return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
02371 case 'localmonthname':
02372 return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
02373 case 'localmonthnamegen':
02374 return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
02375 case 'localmonthabbrev':
02376 return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
02377 case 'localday':
02378 return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
02379 case 'localday2':
02380 return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 );
02381 case 'pagename':
02382 return wfEscapeWikiText( $this->mTitle->getText() );
02383 case 'pagenamee':
02384 return $this->mTitle->getPartialURL();
02385 case 'fullpagename':
02386 return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
02387 case 'fullpagenamee':
02388 return $this->mTitle->getPrefixedURL();
02389 case 'subpagename':
02390 return wfEscapeWikiText( $this->mTitle->getSubpageText() );
02391 case 'subpagenamee':
02392 return $this->mTitle->getSubpageUrlForm();
02393 case 'basepagename':
02394 return wfEscapeWikiText( $this->mTitle->getBaseText() );
02395 case 'basepagenamee':
02396 return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
02397 case 'talkpagename':
02398 if( $this->mTitle->canTalk() ) {
02399 $talkPage = $this->mTitle->getTalkPage();
02400 return wfEscapeWikiText( $talkPage->getPrefixedText() );
02401 } else {
02402 return '';
02403 }
02404 case 'talkpagenamee':
02405 if( $this->mTitle->canTalk() ) {
02406 $talkPage = $this->mTitle->getTalkPage();
02407 return $talkPage->getPrefixedUrl();
02408 } else {
02409 return '';
02410 }
02411 case 'subjectpagename':
02412 $subjPage = $this->mTitle->getSubjectPage();
02413 return wfEscapeWikiText( $subjPage->getPrefixedText() );
02414 case 'subjectpagenamee':
02415 $subjPage = $this->mTitle->getSubjectPage();
02416 return $subjPage->getPrefixedUrl();
02417 case 'revisionid':
02418
02419
02420 $this->mOutput->setFlag( 'vary-revision' );
02421 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
02422 return $this->mRevisionId;
02423 case 'revisionday':
02424
02425
02426 $this->mOutput->setFlag( 'vary-revision' );
02427 wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
02428 return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
02429 case 'revisionday2':
02430
02431
02432 $this->mOutput->setFlag( 'vary-revision' );
02433 wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
02434 return substr( $this->getRevisionTimestamp(), 6, 2 );
02435 case 'revisionmonth':
02436
02437
02438 $this->mOutput->setFlag( 'vary-revision' );
02439 wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
02440 return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
02441 case 'revisionyear':
02442
02443
02444 $this->mOutput->setFlag( 'vary-revision' );
02445 wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
02446 return substr( $this->getRevisionTimestamp(), 0, 4 );
02447 case 'revisiontimestamp':
02448
02449
02450 $this->mOutput->setFlag( 'vary-revision' );
02451 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
02452 return $this->getRevisionTimestamp();
02453 case 'revisionuser':
02454
02455
02456 $this->mOutput->setFlag( 'vary-revision' );
02457 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
02458 return $this->getRevisionUser();
02459 case 'namespace':
02460 return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
02461 case 'namespacee':
02462 return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
02463 case 'talkspace':
02464 return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
02465 case 'talkspacee':
02466 return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
02467 case 'subjectspace':
02468 return $this->mTitle->getSubjectNsText();
02469 case 'subjectspacee':
02470 return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
02471 case 'currentdayname':
02472 return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
02473 case 'currentyear':
02474 return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
02475 case 'currenttime':
02476 return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
02477 case 'currenthour':
02478 return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
02479 case 'currentweek':
02480
02481
02482 return $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
02483 case 'currentdow':
02484 return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
02485 case 'localdayname':
02486 return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
02487 case 'localyear':
02488 return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
02489 case 'localtime':
02490 return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
02491 case 'localhour':
02492 return $this->mVarCache[$index] = $wgContLang->formatNum( $localHour, true );
02493 case 'localweek':
02494
02495
02496 return $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek );
02497 case 'localdow':
02498 return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
02499 case 'numberofarticles':
02500 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
02501 case 'numberoffiles':
02502 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
02503 case 'numberofusers':
02504 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
02505 case 'numberofactiveusers':
02506 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::activeUsers() );
02507 case 'numberofpages':
02508 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
02509 case 'numberofadmins':
02510 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
02511 case 'numberofedits':
02512 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
02513 case 'numberofviews':
02514 return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() );
02515 case 'currenttimestamp':
02516 return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
02517 case 'localtimestamp':
02518 return $this->mVarCache[$index] = $localTimestamp;
02519 case 'currentversion':
02520 return $this->mVarCache[$index] = SpecialVersion::getVersion();
02521 case 'sitename':
02522 return $wgSitename;
02523 case 'server':
02524 return $wgServer;
02525 case 'servername':
02526 return $wgServerName;
02527 case 'scriptpath':
02528 return $wgScriptPath;
02529 case 'directionmark':
02530 return $wgContLang->getDirMark();
02531 case 'contentlanguage':
02532 global $wgContLanguageCode;
02533 return $wgContLanguageCode;
02534 default:
02535 $ret = null;
02536 if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) )
02537 return $ret;
02538 else
02539 return null;
02540 }
02541 }
02542
02548 function initialiseVariables() {
02549 wfProfileIn( __METHOD__ );
02550 $variableIDs = MagicWord::getVariableIDs();
02551
02552 $this->mVariables = new MagicWordArray( $variableIDs );
02553 wfProfileOut( __METHOD__ );
02554 }
02555
02578 function preprocessToDom ( $text, $flags = 0 ) {
02579 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
02580 return $dom;
02581 }
02582
02583
02584
02585
02586 public static function splitWhitespace( $s ) {
02587 $ltrimmed = ltrim( $s );
02588 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
02589 $trimmed = rtrim( $ltrimmed );
02590 $diff = strlen( $ltrimmed ) - strlen( $trimmed );
02591 if ( $diff > 0 ) {
02592 $w2 = substr( $ltrimmed, -$diff );
02593 } else {
02594 $w2 = '';
02595 }
02596 return array( $w1, $trimmed, $w2 );
02597 }
02598
02616 function replaceVariables( $text, $frame = false, $argsOnly = false ) {
02617 # Is there any text? Also, Prevent too big inclusions!
02618 if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
02619 return $text;
02620 }
02621 wfProfileIn( __METHOD__ );
02622
02623 if ( $frame === false ) {
02624 $frame = $this->getPreprocessor()->newFrame();
02625 } elseif ( !( $frame instanceof PPFrame ) ) {
02626 wfDebug( __METHOD__." called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
02627 $frame = $this->getPreprocessor()->newCustomFrame($frame);
02628 }
02629
02630 $dom = $this->preprocessToDom( $text );
02631 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
02632 $text = $frame->expand( $dom, $flags );
02633
02634 wfProfileOut( __METHOD__ );
02635 return $text;
02636 }
02637
02639 static function createAssocArgs( $args ) {
02640 $assocArgs = array();
02641 $index = 1;
02642 foreach( $args as $arg ) {
02643 $eqpos = strpos( $arg, '=' );
02644 if ( $eqpos === false ) {
02645 $assocArgs[$index++] = $arg;
02646 } else {
02647 $name = trim( substr( $arg, 0, $eqpos ) );
02648 $value = trim( substr( $arg, $eqpos+1 ) );
02649 if ( $value === false ) {
02650 $value = '';
02651 }
02652 if ( $name !== false ) {
02653 $assocArgs[$name] = $value;
02654 }
02655 }
02656 }
02657
02658 return $assocArgs;
02659 }
02660
02672 function limitationWarn( $limitationType, $current=null, $max=null) {
02673 $msgName = $limitationType . '-warning';
02674
02675 $warning = wfMsgExt( $msgName, array( 'parsemag', 'escape' ), $current, $max );
02676 $this->mOutput->addWarning( $warning );
02677 $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
02678 if ( $cat ) {
02679 $this->mOutput->addCategory( $cat->getDBkey(), $this->getDefaultSort() );
02680 }
02681 }
02682
02695 function braceSubstitution( $piece, $frame ) {
02696 global $wgContLang, $wgNonincludableNamespaces;
02697 wfProfileIn( __METHOD__ );
02698 wfProfileIn( __METHOD__.'-setup' );
02699
02700 # Flags
02701 $found = false; # $text has been filled
02702 $nowiki = false; # wiki markup in $text should be escaped
02703 $isHTML = false; # $text is HTML, armour it against wikitext transformation
02704 $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
02705 $isChildObj = false; # $text is a DOM node needing expansion in a child frame
02706 $isLocalObj = false; # $text is a DOM node needing expansion in the current frame
02707
02708 # Title object, where $text came from
02709 $title = NULL;
02710
02711 # $part1 is the bit before the first |, and must contain only title characters.
02712 # Various prefixes will be stripped from it later.
02713 $titleWithSpaces = $frame->expand( $piece['title'] );
02714 $part1 = trim( $titleWithSpaces );
02715 $titleText = false;
02716
02717 # Original title text preserved for various purposes
02718 $originalTitle = $part1;
02719
02720 # $args is a list of argument nodes, starting from index 0, not including $part1
02721 $args = (null == $piece['parts']) ? array() : $piece['parts'];
02722 wfProfileOut( __METHOD__.'-setup' );
02723
02724 # SUBST
02725 wfProfileIn( __METHOD__.'-modifiers' );
02726 if ( !$found ) {
02727 $mwSubst = MagicWord::get( 'subst' );
02728 if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
02729 # One of two possibilities is true:
02730 # 1) Found SUBST but not in the PST phase
02731 # 2) Didn't find SUBST and in the PST phase
02732 # In either case, return without further processing
02733 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
02734 $isLocalObj = true;
02735 $found = true;
02736 }
02737 }
02738
02739 # Variables
02740 if ( !$found && $args->getLength() == 0 ) {
02741 $id = $this->mVariables->matchStartToEnd( $part1 );
02742 if ( $id !== false ) {
02743 $text = $this->getVariableValue( $id );
02744 if (MagicWord::getCacheTTL($id)>-1)
02745 $this->mOutput->mContainsOldMagic = true;
02746 $found = true;
02747 }
02748 }
02749
02750 # MSG, MSGNW and RAW
02751 if ( !$found ) {
02752 # Check for MSGNW:
02753 $mwMsgnw = MagicWord::get( 'msgnw' );
02754 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
02755 $nowiki = true;
02756 } else {
02757 # Remove obsolete MSG:
02758 $mwMsg = MagicWord::get( 'msg' );
02759 $mwMsg->matchStartAndRemove( $part1 );
02760 }
02761
02762 # Check for RAW:
02763 $mwRaw = MagicWord::get( 'raw' );
02764 if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
02765 $forceRawInterwiki = true;
02766 }
02767 }
02768 wfProfileOut( __METHOD__.'-modifiers' );
02769
02770 # Parser functions
02771 if ( !$found ) {
02772 wfProfileIn( __METHOD__ . '-pfunc' );
02773
02774 $colonPos = strpos( $part1, ':' );
02775 if ( $colonPos !== false ) {
02776 # Case sensitive functions
02777 $function = substr( $part1, 0, $colonPos );
02778 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
02779 $function = $this->mFunctionSynonyms[1][$function];
02780 } else {
02781 # Case insensitive functions
02782 $function = strtolower( $function );
02783 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
02784 $function = $this->mFunctionSynonyms[0][$function];
02785 } else {
02786 $function = false;
02787 }
02788 }
02789 if ( $function ) {
02790 list( $callback, $flags ) = $this->mFunctionHooks[$function];
02791 $initialArgs = array( &$this );
02792 $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
02793 if ( $flags & SFH_OBJECT_ARGS ) {
02794 # Add a frame parameter, and pass the arguments as an array
02795 $allArgs = $initialArgs;
02796 $allArgs[] = $frame;
02797 for ( $i = 0; $i < $args->getLength(); $i++ ) {
02798 $funcArgs[] = $args->item( $i );
02799 }
02800 $allArgs[] = $funcArgs;
02801 } else {
02802 # Convert arguments to plain text
02803 for ( $i = 0; $i < $args->getLength(); $i++ ) {
02804 $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
02805 }
02806 $allArgs = array_merge( $initialArgs, $funcArgs );
02807 }
02808
02809 # Workaround for PHP bug 35229 and similar
02810 if ( !is_callable( $callback ) ) {
02811 throw new MWException( "Tag hook for $function is not callable\n" );
02812 }
02813 $result = call_user_func_array( $callback, $allArgs );
02814 $found = true;
02815 $noparse = true;
02816 $preprocessFlags = 0;
02817
02818 if ( is_array( $result ) ) {
02819 if ( isset( $result[0] ) ) {
02820 $text = $result[0];
02821 unset( $result[0] );
02822 }
02823
02824
02825
02826 extract( $result );
02827 } else {
02828 $text = $result;
02829 }
02830 if ( !$noparse ) {
02831 $text = $this->preprocessToDom( $text, $preprocessFlags );
02832 $isChildObj = true;
02833 }
02834 }
02835 }
02836 wfProfileOut( __METHOD__ . '-pfunc' );
02837 }
02838
02839 # Finish mangling title and then check for loops.
02840 # Set $title to a Title object and $titleText to the PDBK
02841 if ( !$found ) {
02842 $ns = NS_TEMPLATE;
02843 # Split the title into page and subpage
02844 $subpage = '';
02845 $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
02846 if ($subpage !== '') {
02847 $ns = $this->mTitle->getNamespace();
02848 }
02849 $title = Title::newFromText( $part1, $ns );
02850 if ( $title ) {
02851 $titleText = $title->getPrefixedText();
02852 # Check for language variants if the template is not found
02853 if($wgContLang->hasVariants() && $title->getArticleID() == 0){
02854 $wgContLang->findVariantLink( $part1, $title, true );
02855 }
02856 # Do recursion depth check
02857 $limit = $this->mOptions->getMaxTemplateDepth();
02858 if ( $frame->depth >= $limit ) {
02859 $found = true;
02860 $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>';
02861 }
02862 }
02863 }
02864
02865 # Load from database
02866 if ( !$found && $title ) {
02867 wfProfileIn( __METHOD__ . '-loadtpl' );
02868 if ( !$title->isExternal() ) {
02869 if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
02870 $text = SpecialPage::capturePath( $title );
02871 if ( is_string( $text ) ) {
02872 $found = true;
02873 $isHTML = true;
02874 $this->disableCache();
02875 }
02876 } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
02877 $found = false;
02878 wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
02879 } else {
02880 list( $text, $title ) = $this->getTemplateDom( $title );
02881 if ( $text !== false ) {
02882 $found = true;
02883 $isChildObj = true;
02884 }
02885 }
02886
02887 # If the title is valid but undisplayable, make a link to it
02888 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
02889 $text = "[[:$titleText]]";
02890 $found = true;
02891 }
02892 } elseif ( $title->isTrans() ) {
02893
02894 if ( $this->ot['html'] && !$forceRawInterwiki ) {
02895 $text = $this->interwikiTransclude( $title, 'render' );
02896 $isHTML = true;
02897 } else {
02898 $text = $this->interwikiTransclude( $title, 'raw' );
02899
02900 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
02901 $isChildObj = true;
02902 }
02903 $found = true;
02904 }
02905
02906 # Do infinite loop check
02907 # This has to be done after redirect resolution to avoid infinite loops via redirects
02908 if ( !$frame->loopCheck( $title ) ) {
02909 $found = true;
02910 $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
02911 wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
02912 }
02913 wfProfileOut( __METHOD__ . '-loadtpl' );
02914 }
02915
02916 # If we haven't found text to substitute by now, we're done
02917 # Recover the source wikitext and return it
02918 if ( !$found ) {
02919 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
02920 wfProfileOut( __METHOD__ );
02921 return array( 'object' => $text );
02922 }
02923
02924 # Expand DOM-style return values in a child frame
02925 if ( $isChildObj ) {
02926 # Clean up argument array
02927 $newFrame = $frame->newChild( $args, $title );
02928
02929 if ( $nowiki ) {
02930 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
02931 } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
02932 # Expansion is eligible for the empty-frame cache
02933 if ( isset( $this->mTplExpandCache[$titleText] ) ) {
02934 $text = $this->mTplExpandCache[$titleText];
02935 } else {
02936 $text = $newFrame->expand( $text );
02937 $this->mTplExpandCache[$titleText] = $text;
02938 }
02939 } else {
02940 # Uncached expansion
02941 $text = $newFrame->expand( $text );
02942 }
02943 }
02944 if ( $isLocalObj && $nowiki ) {
02945 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
02946 $isLocalObj = false;
02947 }
02948
02949 # Replace raw HTML by a placeholder
02950 # Add a blank line preceding, to prevent it from mucking up
02951 # immediately preceding headings
02952 if ( $isHTML ) {
02953 $text = "\n\n" . $this->insertStripItem( $text );
02954 }
02955 # Escape nowiki-style return values
02956 elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
02957 $text = wfEscapeWikiText( $text );
02958 }
02959 # Bug 529: if the template begins with a table or block-level
02960 # element, it should be treated as beginning a new line.
02961 # This behaviour is somewhat controversial.
02962 elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) {
02963 $text = "\n" . $text;
02964 }
02965
02966 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
02967 # Error, oversize inclusion
02968 $text = "[[$originalTitle]]" .
02969 $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
02970 $this->limitationWarn( 'post-expand-template-inclusion' );
02971 }
02972
02973 if ( $isLocalObj ) {
02974 $ret = array( 'object' => $text );
02975 } else {
02976 $ret = array( 'text' => $text );
02977 }
02978
02979 wfProfileOut( __METHOD__ );
02980 return $ret;
02981 }
02982
02987 function getTemplateDom( $title ) {
02988 $cacheTitle = $title;
02989 $titleText = $title->getPrefixedDBkey();
02990
02991 if ( isset( $this->mTplRedirCache[$titleText] ) ) {
02992 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
02993 $title = Title::makeTitle( $ns, $dbk );
02994 $titleText = $title->getPrefixedDBkey();
02995 }
02996 if ( isset( $this->mTplDomCache[$titleText] ) ) {
02997 return array( $this->mTplDomCache[$titleText], $title );
02998 }
02999
03000
03001 list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
03002
03003 if ( $text === false ) {
03004 $this->mTplDomCache[$titleText] = false;
03005 return array( false, $title );
03006 }
03007
03008 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
03009 $this->mTplDomCache[ $titleText ] = $dom;
03010
03011 if (! $title->equals($cacheTitle)) {
03012 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
03013 array( $title->getNamespace(),$cdb = $title->getDBkey() );
03014 }
03015
03016 return array( $dom, $title );
03017 }
03018
03022 function fetchTemplateAndTitle( $title ) {
03023 $templateCb = $this->mOptions->getTemplateCallback();
03024 $stuff = call_user_func( $templateCb, $title, $this );
03025 $text = $stuff['text'];
03026 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
03027 if ( isset( $stuff['deps'] ) ) {
03028 foreach ( $stuff['deps'] as $dep ) {
03029 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
03030 }
03031 }
03032 return array($text,$finalTitle);
03033 }
03034
03035 function fetchTemplate( $title ) {
03036 $rv = $this->fetchTemplateAndTitle($title);
03037 return $rv[0];
03038 }
03039
03044 static function statelessFetchTemplate( $title, $parser=false ) {
03045 $text = $skip = false;
03046 $finalTitle = $title;
03047 $deps = array();
03048
03049
03050 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
03051 # Give extensions a chance to select the revision instead
03052 $id = false;
03053 wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
03054
03055 if( $skip ) {
03056 $text = false;
03057 $deps[] = array(
03058 'title' => $title,
03059 'page_id' => $title->getArticleID(),
03060 'rev_id' => null );
03061 break;
03062 }
03063 $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
03064 $rev_id = $rev ? $rev->getId() : 0;
03065
03066 if( $id === false && !$rev ) {
03067 $linkCache = LinkCache::singleton();
03068 $linkCache->addBadLinkObj( $title );
03069 }
03070
03071 $deps[] = array(
03072 'title' => $title,
03073 'page_id' => $title->getArticleID(),
03074 'rev_id' => $rev_id );
03075
03076 if( $rev ) {
03077 $text = $rev->getText();
03078 } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
03079 global $wgContLang;
03080 $message = $wgContLang->lcfirst( $title->getText() );
03081 $text = wfMsgForContentNoTrans( $message );
03082 if( wfEmptyMsg( $message, $text ) ) {
03083 $text = false;
03084 break;
03085 }
03086 } else {
03087 break;
03088 }
03089 if ( $text === false ) {
03090 break;
03091 }
03092
03093 $finalTitle = $title;
03094 $title = Title::newFromRedirect( $text );
03095 }
03096 return array(
03097 'text' => $text,
03098 'finalTitle' => $finalTitle,
03099 'deps' => $deps );
03100 }
03101
03105 function interwikiTransclude( $title, $action ) {
03106 global $wgEnableScaryTranscluding;
03107
03108 if (!$wgEnableScaryTranscluding)
03109 return wfMsg('scarytranscludedisabled');
03110
03111 $url = $title->getFullUrl( "action=$action" );
03112
03113 if (strlen($url) > 255)
03114 return wfMsg('scarytranscludetoolong');
03115 return $this->fetchScaryTemplateMaybeFromCache($url);
03116 }
03117
03118 function fetchScaryTemplateMaybeFromCache($url) {
03119 global $wgTranscludeCacheExpiry;
03120 $dbr = wfGetDB(DB_SLAVE);
03121 $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
03122 array('tc_url' => $url));
03123 if ($obj) {
03124 $time = $obj->tc_time;
03125 $text = $obj->tc_contents;
03126 if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
03127 return $text;
03128 }
03129 }
03130
03131 $text = Http::get($url);
03132 if (!$text)
03133 return wfMsg('scarytranscludefailed', $url);
03134
03135 $dbw = wfGetDB(DB_MASTER);
03136 $dbw->replace('transcache', array('tc_url'), array(
03137 'tc_url' => $url,
03138 'tc_time' => time(),
03139 'tc_contents' => $text));
03140 return $text;
03141 }
03142
03143
03148 function argSubstitution( $piece, $frame ) {
03149 wfProfileIn( __METHOD__ );
03150
03151 $error = false;
03152 $parts = $piece['parts'];
03153 $nameWithSpaces = $frame->expand( $piece['title'] );
03154 $argName = trim( $nameWithSpaces );
03155 $object = false;
03156 $text = $frame->getArgument( $argName );
03157 if ( $text === false && $parts->getLength() > 0
03158 && (
03159 $this->ot['html']
03160 || $this->ot['pre']
03161 || ( $this->ot['wiki'] && $frame->isTemplate() )
03162 )
03163 ) {
03164 # No match in frame, use the supplied default
03165 $object = $parts->item( 0 )->getChildren();
03166 }
03167 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
03168 $error = '<!-- WARNING: argument omitted, expansion size too large -->';
03169 $this->limitationWarn( 'post-expand-template-argument' );
03170 }
03171
03172 if ( $text === false && $object === false ) {
03173 # No match anywhere
03174 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
03175 }
03176 if ( $error !== false ) {
03177 $text .= $error;
03178 }
03179 if ( $object !== false ) {
03180 $ret = array( 'object' => $object );
03181 } else {
03182 $ret = array( 'text' => $text );
03183 }
03184
03185 wfProfileOut( __METHOD__ );
03186 return $ret;
03187 }
03188
03201 function extensionSubstitution( $params, $frame ) {
03202 global $wgRawHtml, $wgContLang;
03203
03204 $name = $frame->expand( $params['name'] );
03205 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
03206 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
03207
03208 $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
03209
03210 if ( $this->ot['html'] ) {
03211 $name = strtolower( $name );
03212
03213 $attributes = Sanitizer::decodeTagAttributes( $attrText );
03214 if ( isset( $params['attributes'] ) ) {
03215 $attributes = $attributes + $params['attributes'];
03216 }
03217 switch ( $name ) {
03218 case 'html':
03219 if( $wgRawHtml ) {
03220 $output = $content;
03221 break;
03222 } else {
03223 throw new MWException( '<html> extension tag encountered unexpectedly' );
03224 }
03225 case 'nowiki':
03226 $content = strtr($content, array('-{' => '-{', '}-' => '}-'));
03227 $output = Xml::escapeTagsOnly( $content );
03228 break;
03229 case 'math':
03230 $output = $wgContLang->armourMath(
03231 MathRenderer::renderMath( $content, $attributes ) );
03232 break;
03233 case 'gallery':
03234 $output = $this->renderImageGallery( $content, $attributes );
03235 break;
03236 default:
03237 if( isset( $this->mTagHooks[$name] ) ) {
03238 # Workaround for PHP bug 35229 and similar
03239 if ( !is_callable( $this->mTagHooks[$name] ) ) {
03240 throw new MWException( "Tag hook for $name is not callable\n" );
03241 }
03242 $output = call_user_func_array( $this->mTagHooks[$name],
03243 array( $content, $attributes, $this ) );
03244 } else {
03245 $output = '<span class="error">Invalid tag extension name: ' .
03246 htmlspecialchars( $name ) . '</span>';
03247 }
03248 }
03249 } else {
03250 if ( is_null( $attrText ) ) {
03251 $attrText = '';
03252 }
03253 if ( isset( $params['attributes'] ) ) {
03254 foreach ( $params['attributes'] as $attrName => $attrValue ) {
03255 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
03256 htmlspecialchars( $attrValue ) . '"';
03257 }
03258 }
03259 if ( $content === null ) {
03260 $output = "<$name$attrText/>";
03261 } else {
03262 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
03263 $output = "<$name$attrText>$content$close";
03264 }
03265 }
03266
03267 if ( $name === 'html' || $name === 'nowiki' ) {
03268 $this->mStripState->nowiki->setPair( $marker, $output );
03269 } else {
03270 $this->mStripState->general->setPair( $marker, $output );
03271 }
03272 return $marker;
03273 }
03274
03282 function incrementIncludeSize( $type, $size ) {
03283 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) {
03284 return false;
03285 } else {
03286 $this->mIncludeSizes[$type] += $size;
03287 return true;
03288 }
03289 }
03290
03296 function incrementExpensiveFunctionCount() {
03297 global $wgExpensiveParserFunctionLimit;
03298 $this->mExpensiveFunctionCount++;
03299 if($this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit) {
03300 return true;
03301 }
03302 return false;
03303 }
03304
03309 function doDoubleUnderscore( $text ) {
03310 wfProfileIn( __METHOD__ );
03311
03312 $mw = MagicWord::get( 'toc' );
03313 if( $mw->match( $text ) ) {
03314 $this->mShowToc = true;
03315 $this->mForceTocPosition = true;
03316
03317
03318 $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
03319
03320
03321 $text = $mw->replace( '', $text );
03322 }
03323
03324
03325 $mwa = MagicWord::getDoubleUnderscoreArray();
03326 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
03327
03328 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
03329 $this->mOutput->mNoGallery = true;
03330 }
03331 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
03332 $this->mShowToc = false;
03333 }
03334 if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
03335 $this->mOutput->setProperty( 'hiddencat', 'y' );
03336
03337 $containerCategory = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( 'hidden-category-category' ) );
03338 if ( $containerCategory ) {
03339 $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
03340 } else {
03341 wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" );
03342 }
03343 }
03344 # (bug 8068) Allow control over whether robots index a page.
03345 #
03346 # FIXME (bug 14899): __INDEX__ always overrides __NOINDEX__ here! This
03347 # is not desirable, the last one on the page should win.
03348 if( isset( $this->mDoubleUnderscores['noindex'] ) ) {
03349 $this->mOutput->setIndexPolicy( 'noindex' );
03350 } elseif( isset( $this->mDoubleUnderscores['index'] ) ) {
03351 $this->mOutput->setIndexPolicy( 'index' );
03352 }
03353 wfProfileOut( __METHOD__ );
03354 return $text;
03355 }
03356
03371 function formatHeadings( $text, $isMain=true ) {
03372 global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds;
03373
03374 $doNumberHeadings = $this->mOptions->getNumberHeadings();
03375 $showEditLink = $this->mOptions->getEditSection();
03376
03377
03378 if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) {
03379 $showEditLink = 0;
03380 }
03381
03382 # Inhibit editsection links if requested in the page
03383 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) || $this->mOptions->getIsPrintable() ) {
03384 $showEditLink = 0;
03385 }
03386
03387 # Get all headlines for numbering them and adding funky stuff like [edit]
03388 # links - this is for later, but we need the number of headlines right now
03389 $matches = array();
03390 $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
03391
03392 # if there are fewer than 4 headlines in the article, do not show TOC
03393 # unless it's been explicitly enabled.
03394 $enoughToc = $this->mShowToc &&
03395 (($numMatches >= 4) || $this->mForceTocPosition);
03396
03397 # Allow user to stipulate that a page should have a "new section"
03398 # link added via __NEWSECTIONLINK__
03399 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
03400 $this->mOutput->setNewSection( true );
03401 }
03402
03403 # Allow user to remove the "new section"
03404 # link via __NONEWSECTIONLINK__
03405 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
03406 $this->mOutput->hideNewSection( true );
03407 }
03408
03409 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
03410 # override above conditions and always show TOC above first header
03411 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
03412 $this->mShowToc = true;
03413 $enoughToc = true;
03414 }
03415
03416 # We need this to perform operations on the HTML
03417 $sk = $this->mOptions->getSkin();
03418
03419 # headline counter
03420 $headlineCount = 0;
03421 $numVisible = 0;
03422
03423 # Ugh .. the TOC should have neat indentation levels which can be
03424 # passed to the skin functions. These are determined here
03425 $toc = '';
03426 $full = '';
03427 $head = array();
03428 $sublevelCount = array();
03429 $levelCount = array();
03430 $toclevel = 0;
03431 $level = 0;
03432 $prevlevel = 0;
03433 $toclevel = 0;
03434 $prevtoclevel = 0;
03435 $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
03436 $baseTitleText = $this->mTitle->getPrefixedDBkey();
03437 $tocraw = array();
03438
03439 foreach( $matches[3] as $headline ) {
03440 $isTemplate = false;
03441 $titleText = false;
03442 $sectionIndex = false;
03443 $numbering = '';
03444 $markerMatches = array();
03445 if (preg_match("/^$markerRegex/", $headline, $markerMatches)) {
03446 $serial = $markerMatches[1];
03447 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
03448 $isTemplate = ($titleText != $baseTitleText);
03449 $headline = preg_replace("/^$markerRegex/", "", $headline);
03450 }
03451
03452 if( $toclevel ) {
03453 $prevlevel = $level;
03454 $prevtoclevel = $toclevel;
03455 }
03456 $level = $matches[1][$headlineCount];
03457
03458 if( $doNumberHeadings || $enoughToc ) {
03459
03460 if ( $level > $prevlevel ) {
03461 # Increase TOC level
03462 $toclevel++;
03463 $sublevelCount[$toclevel] = 0;
03464 if( $toclevel<$wgMaxTocLevel ) {
03465 $prevtoclevel = $toclevel;
03466 $toc .= $sk->tocIndent();
03467 $numVisible++;
03468 }
03469 }
03470 elseif ( $level < $prevlevel && $toclevel > 1 ) {
03471 # Decrease TOC level, find level to jump to
03472
03473 if ( $toclevel == 2 && $level <= $levelCount[1] ) {
03474 # Can only go down to level 1
03475 $toclevel = 1;
03476 } else {
03477 for ($i = $toclevel; $i > 0; $i--) {
03478 if ( $levelCount[$i] == $level ) {
03479 # Found last matching level
03480 $toclevel = $i;
03481 break;
03482 }
03483 elseif ( $levelCount[$i] < $level ) {
03484 # Found first matching level below current level
03485 $toclevel = $i + 1;
03486 break;
03487 }
03488 }
03489 }
03490 if( $toclevel<$wgMaxTocLevel ) {
03491 if($prevtoclevel < $wgMaxTocLevel) {
03492 # Unindent only if the previous toc level was shown :p
03493 $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
03494 $prevtoclevel = $toclevel;
03495 } else {
03496 $toc .= $sk->tocLineEnd();
03497 }
03498 }
03499 }
03500 else {
03501 # No change in level, end TOC line
03502 if( $toclevel<$wgMaxTocLevel ) {
03503 $toc .= $sk->tocLineEnd();
03504 }
03505 }
03506
03507 $levelCount[$toclevel] = $level;
03508
03509 # count number of headlines for each level
03510 @$sublevelCount[$toclevel]++;
03511 $dot = 0;
03512 for( $i = 1; $i <= $toclevel; $i++ ) {
03513 if( !empty( $sublevelCount[$i] ) ) {
03514 if( $dot ) {
03515 $numbering .= '.';
03516 }
03517 $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
03518 $dot = 1;
03519 }
03520 }
03521 }
03522
03523 # The safe header is a version of the header text safe to use for links
03524 # Avoid insertion of weird stuff like <math> by expanding the relevant sections
03525 $safeHeadline = $this->mStripState->unstripBoth( $headline );
03526
03527 # Remove link placeholders by the link text.
03528 # <!--LINK number-->
03529 # turns into
03530 # link text with suffix
03531 $safeHeadline = $this->replaceLinkHoldersText( $safeHeadline );
03532
03533 # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
03534 $tocline = preg_replace(
03535 array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
03536 array( '', '<$1>'),
03537 $safeHeadline
03538 );
03539 $tocline = trim( $tocline );
03540
03541 # For the anchor, strip out HTML-y stuff period
03542 $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
03543 $safeHeadline = trim( $safeHeadline );
03544
03545 # Save headline for section edit hint before it's escaped
03546 $headlineHint = $safeHeadline;
03547
03548 if ( $wgEnforceHtmlIds ) {
03549 $legacyHeadline = false;
03550 $safeHeadline = Sanitizer::escapeId( $safeHeadline,
03551 'noninitial' );
03552 } else {
03553 # For reverse compatibility, provide an id that's
03554 # HTML4-compatible, like we used to.
03555 #
03556 # It may be worth noting, academically, that it's possible for
03557 # the legacy anchor to conflict with a non-legacy headline
03558 # anchor on the page. In this case likely the "correct" thing
03559 # would be to either drop the legacy anchors or make sure
03560 # they're numbered first. However, this would require people
03561 # to type in section names like "abc_.D7.93.D7.90.D7.A4"
03562 # manually, so let's not bother worrying about it.
03563 $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
03564 'noninitial' );
03565 $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' );
03566
03567 if ( $legacyHeadline == $safeHeadline ) {
03568 # No reason to have both (in fact, we can't)
03569 $legacyHeadline = false;
03570 } elseif ( $legacyHeadline != Sanitizer::escapeId(
03571 $legacyHeadline, 'xml' ) ) {
03572 # The legacy id is invalid XML. We used to allow this, but
03573 # there's no reason to do so anymore. Backward
03574 # compatibility will fail slightly in this case, but it's
03575 # no big deal.
03576 $legacyHeadline = false;
03577 }
03578 }
03579
03580 # HTML names must be case-insensitively unique (bug 10721). FIXME:
03581 # Does this apply to Unicode characters? Because we aren't
03582 # handling those here.
03583 $arrayKey = strtolower( $safeHeadline );
03584 if ( $legacyHeadline === false ) {
03585 $legacyArrayKey = false;
03586 } else {
03587 $legacyArrayKey = strtolower( $legacyHeadline );
03588 }
03589
03590 # count how many in assoc. array so we can track dupes in anchors
03591 if ( isset( $refers[$arrayKey] ) ) {
03592 $refers[$arrayKey]++;
03593 } else {
03594 $refers[$arrayKey] = 1;
03595 }
03596 if ( isset( $refers[$legacyArrayKey] ) ) {
03597 $refers[$legacyArrayKey]++;
03598 } else {
03599 $refers[$legacyArrayKey] = 1;
03600 }
03601
03602 # Don't number the heading if it is the only one (looks silly)
03603 if( $doNumberHeadings && count( $matches[3] ) > 1) {
03604 # the two are different if the line contains a link
03605 $headline=$numbering . ' ' . $headline;
03606 }
03607
03608 # Create the anchor for linking from the TOC to the section
03609 $anchor = $safeHeadline;
03610 $legacyAnchor = $legacyHeadline;
03611 if ( $refers[$arrayKey] > 1 ) {
03612 $anchor .= '_' . $refers[$arrayKey];
03613 }
03614 if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
03615 $legacyAnchor .= '_' . $refers[$legacyArrayKey];
03616 }
03617 if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
03618 $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
03619 $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
03620 }
03621 # give headline the correct <h#> tag
03622 if( $showEditLink && $sectionIndex !== false ) {
03623 if( $isTemplate ) {
03624 # Put a T flag in the section identifier, to indicate to extractSections()
03625 # that sections inside <includeonly> should be counted.
03626 $editlink = $sk->doEditSectionLink(Title::newFromText( $titleText ), "T-$sectionIndex");
03627 } else {
03628 $editlink = $sk->doEditSectionLink($this->mTitle, $sectionIndex, $headlineHint);
03629 }
03630 } else {
03631 $editlink = '';
03632 }
03633 $head[$headlineCount] = $sk->makeHeadline( $level,
03634 $matches['attrib'][$headlineCount], $anchor, $headline,
03635 $editlink, $legacyAnchor );
03636
03637 $headlineCount++;
03638 }
03639
03640 $this->mOutput->setSections( $tocraw );
03641
03642 # Never ever show TOC if no headers
03643 if( $numVisible < 1 ) {
03644 $enoughToc = false;
03645 }
03646
03647 if( $enoughToc ) {
03648 if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
03649 $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
03650 }
03651 $toc = $sk->tocList( $toc );
03652 }
03653
03654 # split up and insert constructed headlines
03655
03656 $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
03657 $i = 0;
03658
03659 foreach( $blocks as $block ) {
03660 if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) {
03661 # This is the [edit] link that appears for the top block of text when
03662 # section editing is enabled
03663
03664 # Disabled because it broke block formatting
03665 # For example, a bullet point in the top line
03666 # $full .= $sk->editSectionLink(0);
03667 }
03668 $full .= $block;
03669 if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
03670 # Top anchor now in skin
03671 $full = $full.$toc;
03672 }
03673
03674 if( !empty( $head[$i] ) ) {
03675 $full .= $head[$i];
03676 }
03677 $i++;
03678 }
03679 if( $this->mForceTocPosition ) {
03680 return str_replace( '<!--MWTOC-->', $toc, $full );
03681 } else {
03682 return $full;
03683 }
03684 }
03685
03698 function preSaveTransform( $text, Title $title, $user, $options, $clearState = true ) {
03699 $this->mOptions = $options;
03700 $this->setTitle( $title );
03701 $this->setOutputType( self::OT_WIKI );
03702
03703 if ( $clearState ) {
03704 $this->clearState();
03705 }
03706
03707 $pairs = array(
03708 "\r\n" => "\n",
03709 );
03710 $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
03711 $text = $this->pstPass2( $text, $user );
03712 $text = $this->mStripState->unstripBoth( $text );
03713 return $text;
03714 }
03715
03720 function pstPass2( $text, $user ) {
03721 global $wgContLang, $wgLocaltimezone;
03722
03723
03724
03725
03726
03727
03728
03729
03730 $ts = $this->mOptions->getTimestamp();
03731 $tz = wfMsgForContent( 'timezone-utc' );
03732 if ( isset( $wgLocaltimezone ) ) {
03733 $unixts = wfTimestamp( TS_UNIX, $ts );
03734 $oldtz = getenv( 'TZ' );
03735 putenv( 'TZ='.$wgLocaltimezone );
03736 $ts = date( 'YmdHis', $unixts );
03737 $tz = date( 'T', $unixts ); # might vary on DST changeover!
03738
03739
03740
03741
03742
03743 $key = 'timezone-' . strtolower( trim( $tz ) );
03744 $value = wfMsgForContent( $key );
03745 if ( !wfEmptyMsg( $key, $value ) ) $tz = $value;
03746
03747 putenv( 'TZ='.$oldtz );
03748 }
03749
03750 $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)";
03751
03752 # Variable replacement
03753 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
03754 $text = $this->replaceVariables( $text );
03755
03756 # Signatures
03757 $sigText = $this->getUserSig( $user );
03758 $text = strtr( $text, array(
03759 '~~~~~' => $d,
03760 '~~~~' => "$sigText $d",
03761 '~~~' => $sigText
03762 ) );
03763
03764 # Context links: [[|name]] and [[name (context)|]]
03765 #
03766 global $wgLegalTitleChars;
03767 $tc = "[$wgLegalTitleChars]";
03768 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
03769
03770 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/"; # [[ns:page (context)|]]
03771 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)(($tc+))\\|]]/"; # [[ns:page(context)|]]
03772 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/"; # [[ns:page (context), context|]]
03773 $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]]
03774
03775 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
03776 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
03777 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text );
03778 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
03779
03780 $t = $this->mTitle->getText();
03781 $m = array();
03782 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
03783 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
03784 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
03785 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
03786 } else {
03787 # if there's no context, don't bother duplicating the title
03788 $text = preg_replace( $p2, '[[\\1]]', $text );
03789 }
03790
03791 # Trim trailing whitespace
03792 $text = rtrim( $text );
03793
03794 return $text;
03795 }
03796
03805 function getUserSig( &$user ) {
03806 global $wgMaxSigChars;
03807
03808 $username = $user->getName();
03809 $nickname = $user->getOption( 'nickname' );
03810 $nickname = $nickname === '' ? $username : $nickname;
03811
03812 if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
03813 $nickname = $username;
03814 wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
03815 } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
03816 # Sig. might contain markup; validate this
03817 if( $this->validateSig( $nickname ) !== false ) {
03818 # Validated; clean up (if needed) and return it
03819 return $this->cleanSig( $nickname, true );
03820 } else {
03821 # Failed to validate; fall back to the default
03822 $nickname = $username;
03823 wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
03824 }
03825 }
03826
03827
03828 $nickname = $this->cleanSigInSig( $nickname );
03829
03830 # If we're still here, make it a link to the user page
03831 $userText = wfEscapeWikiText( $username );
03832 $nickText = wfEscapeWikiText( $nickname );
03833 if ( $user->isAnon() ) {
03834 return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
03835 } else {
03836 return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
03837 }
03838 }
03839
03846 function validateSig( $text ) {
03847 return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
03848 }
03849
03860 function cleanSig( $text, $parsing = false ) {
03861 if ( !$parsing ) {
03862 global $wgTitle;
03863 $this->clearState();
03864 $this->setTitle( $wgTitle );
03865 $this->mOptions = new ParserOptions;
03866 $this->setOutputType = self::OT_PREPROCESS;
03867 }
03868
03869 # Option to disable this feature
03870 if ( !$this->mOptions->getCleanSignatures() ) {
03871 return $text;
03872 }
03873
03874 # FIXME: regex doesn't respect extension tags or nowiki
03875 # => Move this logic to braceSubstitution()
03876 $substWord = MagicWord::get( 'subst' );
03877 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
03878 $substText = '{{' . $substWord->getSynonym( 0 );
03879
03880 $text = preg_replace( $substRegex, $substText, $text );
03881 $text = $this->cleanSigInSig( $text );
03882 $dom = $this->preprocessToDom( $text );
03883 $frame = $this->getPreprocessor()->newFrame();
03884 $text = $frame->expand( $dom );
03885
03886 if ( !$parsing ) {
03887 $text = $this->mStripState->unstripBoth( $text );
03888 }
03889
03890 return $text;
03891 }
03892
03898 function cleanSigInSig( $text ) {
03899 $text = preg_replace( '/~{3,5}/', '', $text );
03900 return $text;
03901 }
03902
03908 function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
03909 $this->setTitle( $title );
03910 $this->mOptions = $options;
03911 $this->setOutputType( $outputType );
03912 if ( $clearState ) {
03913 $this->clearState();
03914 }
03915 }
03916
03925 function transformMsg( $text, $options ) {
03926 global $wgTitle;
03927 static $executing = false;
03928
03929 # Guard against infinite recursion
03930 if ( $executing ) {
03931 return $text;
03932 }
03933 $executing = true;
03934
03935 wfProfileIn(__METHOD__);
03936 $text = $this->preprocess( $text, $wgTitle, $options );
03937
03938 $executing = false;
03939 wfProfileOut(__METHOD__);
03940 return $text;
03941 }
03942
03958 function setHook( $tag, $callback ) {
03959 $tag = strtolower( $tag );
03960 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
03961 $this->mTagHooks[$tag] = $callback;
03962 if( !in_array( $tag, $this->mStripList ) ) {
03963 $this->mStripList[] = $tag;
03964 }
03965
03966 return $oldVal;
03967 }
03968
03969 function setTransparentTagHook( $tag, $callback ) {
03970 $tag = strtolower( $tag );
03971 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
03972 $this->mTransparentTagHooks[$tag] = $callback;
03973
03974 return $oldVal;
03975 }
03976
03980 function clearTagHooks() {
03981 $this->mTagHooks = array();
03982 $this->mStripList = $this->mDefaultStripList;
03983 }
03984
04029 function setFunctionHook( $id, $callback, $flags = 0 ) {
04030 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
04031 $this->mFunctionHooks[$id] = array( $callback, $flags );
04032
04033 # Add to function cache
04034 $mw = MagicWord::get( $id );
04035 if( !$mw )
04036 throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
04037
04038 $synonyms = $mw->getSynonyms();
04039 $sensitive = intval( $mw->isCaseSensitive() );
04040
04041 foreach ( $synonyms as $syn ) {
04042 # Case
04043 if ( !$sensitive ) {
04044 $syn = strtolower( $syn );
04045 }
04046 # Add leading hash
04047 if ( !( $flags & SFH_NO_HASH ) ) {
04048 $syn = '#' . $syn;
04049 }
04050 # Remove trailing colon
04051 if ( substr( $syn, -1, 1 ) === ':' ) {
04052 $syn = substr( $syn, 0, -1 );
04053 }
04054 $this->mFunctionSynonyms[$sensitive][$syn] = $id;
04055 }
04056 return $oldVal;
04057 }
04058
04064 function getFunctionHooks() {
04065 return array_keys( $this->mFunctionHooks );
04066 }
04067
04073 function replaceLinkHolders( &$text, $options = 0 ) {
04074 return $this->mLinkHolders->replace( $text );
04075 }
04076
04083 function replaceLinkHoldersText( $text ) {
04084 return $this->mLinkHolders->replaceText( $text );
04085 }
04086
04090 function renderPreTag( $text, $attribs ) {
04091
04092 $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
04093
04094 $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
04095 return Xml::openElement( 'pre', $attribs ) .
04096 Xml::escapeTagsOnly( $content ) .
04097 '</pre>';
04098 }
04099
04109 function renderImageGallery( $text, $params ) {
04110 $ig = new ImageGallery();
04111 $ig->setContextTitle( $this->mTitle );
04112 $ig->setShowBytes( false );
04113 $ig->setShowFilename( false );
04114 $ig->setParser( $this );
04115 $ig->setHideBadImages();
04116 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
04117 $ig->useSkin( $this->mOptions->getSkin() );
04118 $ig->mRevisionId = $this->mRevisionId;
04119
04120 if( isset( $params['caption'] ) ) {
04121 $caption = $params['caption'];
04122 $caption = htmlspecialchars( $caption );
04123 $caption = $this->replaceInternalLinks( $caption );
04124 $ig->setCaptionHtml( $caption );
04125 }
04126 if( isset( $params['perrow'] ) ) {
04127 $ig->setPerRow( $params['perrow'] );
04128 }
04129 if( isset( $params['widths'] ) ) {
04130 $ig->setWidths( $params['widths'] );
04131 }
04132 if( isset( $params['heights'] ) ) {
04133 $ig->setHeights( $params['heights'] );
04134 }
04135
04136 wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
04137
04138 $lines = StringUtils::explode( "\n", $text );
04139 foreach ( $lines as $line ) {
04140 # match lines like these:
04141 # Image:someimage.jpg|This is some image
04142 $matches = array();
04143 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
04144 # Skip empty lines
04145 if ( count( $matches ) == 0 ) {
04146 continue;
04147 }
04148
04149 if ( strpos( $matches[0], '%' ) !== false )
04150 $matches[1] = urldecode( $matches[1] );
04151 $tp = Title::newFromText( $matches[1] );
04152 $nt =& $tp;
04153 if( is_null( $nt ) ) {
04154 # Bogus title. Ignore these so we don't bomb out later.
04155 continue;
04156 }
04157 if ( isset( $matches[3] ) ) {
04158 $label = $matches[3];
04159 } else {
04160 $label = '';
04161 }
04162
04163 $html = $this->recursiveTagParse( trim( $label ) );
04164
04165 $ig->add( $nt, $html );
04166
04167 # Only add real images (bug #5586)
04168 if ( $nt->getNamespace() == NS_FILE ) {
04169 $this->mOutput->addImage( $nt->getDBkey() );
04170 }
04171 }
04172 return $ig->toHTML();
04173 }
04174
04175 function getImageParams( $handler ) {
04176 if ( $handler ) {
04177 $handlerClass = get_class( $handler );
04178 } else {
04179 $handlerClass = '';
04180 }
04181 if ( !isset( $this->mImageParams[$handlerClass] ) ) {
04182
04183 static $internalParamNames = array(
04184 'horizAlign' => array( 'left', 'right', 'center', 'none' ),
04185 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
04186 'bottom', 'text-bottom' ),
04187 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
04188 'upright', 'border', 'link', 'alt' ),
04189 );
04190 static $internalParamMap;
04191 if ( !$internalParamMap ) {
04192 $internalParamMap = array();
04193 foreach ( $internalParamNames as $type => $names ) {
04194 foreach ( $names as $name ) {
04195 $magicName = str_replace( '-', '_', "img_$name" );
04196 $internalParamMap[$magicName] = array( $type, $name );
04197 }
04198 }
04199 }
04200
04201
04202 $paramMap = $internalParamMap;
04203 if ( $handler ) {
04204 $handlerParamMap = $handler->getParamMap();
04205 foreach ( $handlerParamMap as $magic => $paramName ) {
04206 $paramMap[$magic] = array( 'handler', $paramName );
04207 }
04208 }
04209 $this->mImageParams[$handlerClass] = $paramMap;
04210 $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
04211 }
04212 return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
04213 }
04214
04221 function makeImage( $title, $options, $holders = false ) {
04222 # Check if the options text is of the form "options|alt text"
04223 # Options are:
04224 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
04225 # * left no resizing, just left align. label is used for alt= only
04226 # * right same, but right aligned
04227 # * none same, but not aligned
04228 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
04229 # * center center the image
04230 # * framed Keep original image size, no magnify-button.
04231 # * frameless like 'thumb' but without a frame. Keeps user preferences for width
04232 # * upright reduce width for upright images, rounded to full __0 px
04233 # * border draw a 1px border around the image
04234 # * alt Text for HTML alt attribute (defaults to empty)
04235 # vertical-align values (no % or length right now):
04236 # * baseline
04237 # * sub
04238 # * super
04239 # * top
04240 # * text-top
04241 # * middle
04242 # * bottom
04243 # * text-bottom
04244
04245 $parts = StringUtils::explode( "|", $options );
04246 $sk = $this->mOptions->getSkin();
04247
04248 # Give extensions a chance to select the file revision for us
04249 $skip = $time = $descQuery = false;
04250 wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
04251
04252 if ( $skip ) {
04253 return $sk->link( $title );
04254 }
04255
04256 # Get the file
04257 $imagename = $title->getDBkey();
04258 if ( isset( $this->mFileCache[$imagename][$time] ) ) {
04259 $file = $this->mFileCache[$imagename][$time];
04260 } else {
04261 $file = wfFindFile( $title, $time );
04262 if ( count( $this->mFileCache ) > 1000 ) {
04263 $this->mFileCache = array();
04264 }
04265 $this->mFileCache[$imagename][$time] = $file;
04266 }
04267 # Get parameter map
04268 $handler = $file ? $file->getHandler() : false;
04269
04270 list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
04271
04272 # Process the input parameters
04273 $caption = '';
04274 $params = array( 'frame' => array(), 'handler' => array(),
04275 'horizAlign' => array(), 'vertAlign' => array() );
04276 foreach( $parts as $part ) {
04277 $part = trim( $part );
04278 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
04279 $validated = false;
04280 if( isset( $paramMap[$magicName] ) ) {
04281 list( $type, $paramName ) = $paramMap[$magicName];
04282
04283
04284 if( $type === 'handler' && $paramName === 'width' ) {
04285 $m = array();
04286 # (bug 13500) In both cases (width/height and width only),
04287 # permit trailing "px" for backward compatibility.
04288 if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
04289 $width = intval( $m[1] );
04290 $height = intval( $m[2] );
04291 if ( $handler->validateParam( 'width', $width ) ) {
04292 $params[$type]['width'] = $width;
04293 $validated = true;
04294 }
04295 if ( $handler->validateParam( 'height', $height ) ) {
04296 $params[$type]['height'] = $height;
04297 $validated = true;
04298 }
04299 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
04300 $width = intval( $value );
04301 if ( $handler->validateParam( 'width', $width ) ) {
04302 $params[$type]['width'] = $width;
04303 $validated = true;
04304 }
04305 }
04306 } else {
04307 if ( $type === 'handler' ) {
04308 # Validate handler parameter
04309 $validated = $handler->validateParam( $paramName, $value );
04310 } else {
04311 # Validate internal parameters
04312 switch( $paramName ) {
04313 case 'manualthumb':
04314 case 'alt':
04315
04316
04317
04318 $validated = true;
04319 $value = $this->stripAltText( $value, $holders );
04320 break;
04321 case 'link':
04322 $chars = self::EXT_LINK_URL_CLASS;
04323 $prots = $this->mUrlProtocols;
04324 if ( $value === '' ) {
04325 $paramName = 'no-link';
04326 $value = true;
04327 $validated = true;
04328 } elseif ( preg_match( "/^$prots/", $value ) ) {
04329 if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
04330 $paramName = 'link-url';
04331 $this->mOutput->addExternalLink( $value );
04332 $validated = true;
04333 }
04334 } else {
04335 $linkTitle = Title::newFromText( $value );
04336 if ( $linkTitle ) {
04337 $paramName = 'link-title';
04338 $value = $linkTitle;
04339 $this->mOutput->addLink( $linkTitle );
04340 $validated = true;
04341 }
04342 }
04343 break;
04344 default:
04345
04346 $validated = ( $value === false || is_numeric( trim( $value ) ) );
04347 }
04348 }
04349
04350 if ( $validated ) {
04351 $params[$type][$paramName] = $value;
04352 }
04353 }
04354 }
04355 if ( !$validated ) {
04356 $caption = $part;
04357 }
04358 }
04359
04360 # Process alignment parameters
04361 if ( $params['horizAlign'] ) {
04362 $params['frame']['align'] = key( $params['horizAlign'] );
04363 }
04364 if ( $params['vertAlign'] ) {
04365 $params['frame']['valign'] = key( $params['vertAlign'] );
04366 }
04367
04368 $params['frame']['caption'] = $caption;
04369
04370 $params['frame']['title'] = $this->stripAltText( $caption, $holders );
04371
04372 # In the old days, [[Image:Foo|text...]] would set alt text. Later it
04373 # came to also set the caption, ordinary text after the image -- which
04374 # makes no sense, because that just repeats the text multiple times in
04375 # screen readers. It *also* came to set the title attribute.
04376 #
04377 # Now that we have an alt attribute, we should not set the alt text to
04378 # equal the caption: that's worse than useless, it just repeats the
04379 # text. This is the framed/thumbnail case. If there's no caption, we
04380 # use the unnamed parameter for alt text as well, just for the time be-
04381 # ing, if the unnamed param is set and the alt param is not.
04382 #
04383 # For the future, we need to figure out if we want to tweak this more,
04384 # e.g., introducing a title= parameter for the title; ignoring the un-
04385 # named parameter entirely for images without a caption; adding an ex-
04386 # plicit caption= parameter and preserving the old magic unnamed para-
04387 # meter for BC; ...
04388 if( $caption !== '' && !isset( $params['frame']['alt'] )
04389 && !isset( $params['frame']['framed'] )
04390 && !isset( $params['frame']['thumbnail'] )
04391 && !isset( $params['frame']['manualthumb'] ) ) {
04392 $params['frame']['alt'] = $params['frame']['title'];
04393 }
04394
04395 wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
04396
04397 # Linker does the rest
04398 $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery );
04399
04400 # Give the handler a chance to modify the parser object
04401 if ( $handler ) {
04402 $handler->parserTransformHook( $this, $file );
04403 }
04404
04405 return $ret;
04406 }
04407
04408 protected function stripAltText( $caption, $holders ) {
04409 # Strip bad stuff out of the title (tooltip). We can't just use
04410 # replaceLinkHoldersText() here, because if this function is called
04411 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
04412 if ( $holders ) {
04413 $tooltip = $holders->replaceText( $caption );
04414 } else {
04415 $tooltip = $this->replaceLinkHoldersText( $caption );
04416 }
04417
04418 # make sure there are no placeholders in thumbnail attributes
04419 # that are later expanded to html- so expand them now and
04420 # remove the tags
04421 $tooltip = $this->mStripState->unstripBoth( $tooltip );
04422 $tooltip = Sanitizer::stripAllTags( $tooltip );
04423
04424 return $tooltip;
04425 }
04426
04431 function disableCache() {
04432 wfDebug( "Parser output marked as uncacheable.\n" );
04433 $this->mOutput->mCacheTime = -1;
04434 }
04435
04444 function attributeStripCallback( &$text, $frame = false ) {
04445 $text = $this->replaceVariables( $text, $frame );
04446 $text = $this->mStripState->unstripBoth( $text );
04447 return $text;
04448 }
04449
04455 function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
04456 function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
04457 function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
04463 function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
04492 private function extractSections( $text, $section, $mode, $newText='' ) {
04493 global $wgTitle;
04494 $this->clearState();
04495 $this->setTitle( $wgTitle );
04496 $this->mOptions = new ParserOptions;
04497 $this->setOutputType( self::OT_WIKI );
04498 $outText = '';
04499 $frame = $this->getPreprocessor()->newFrame();
04500
04501
04502 $flags = 0;
04503 $sectionParts = explode( '-', $section );
04504 $sectionIndex = array_pop( $sectionParts );
04505 foreach ( $sectionParts as $part ) {
04506 if ( $part === 'T' ) {
04507 $flags |= self::PTD_FOR_INCLUSION;
04508 }
04509 }
04510
04511 $root = $this->preprocessToDom( $text, $flags );
04512
04513
04514
04515 $node = $root->getFirstChild();
04516
04517
04518 if ( $sectionIndex == 0 ) {
04519
04520 $targetLevel = 1000;
04521 } else {
04522 while ( $node ) {
04523 if ( $node->getName() === 'h' ) {
04524 $bits = $node->splitHeading();
04525 if ( $bits['i'] == $sectionIndex ) {
04526 $targetLevel = $bits['level'];
04527 break;
04528 }
04529 }
04530 if ( $mode === 'replace' ) {
04531 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
04532 }
04533 $node = $node->getNextSibling();
04534 }
04535 }
04536
04537 if ( !$node ) {
04538
04539 if ( $mode === 'get' ) {
04540 return $newText;
04541 } else {
04542 return $text;
04543 }
04544 }
04545
04546
04547 do {
04548 if ( $node->getName() === 'h' ) {
04549 $bits = $node->splitHeading();
04550 $curLevel = $bits['level'];
04551 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
04552 break;
04553 }
04554 }
04555 if ( $mode === 'get' ) {
04556 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
04557 }
04558 $node = $node->getNextSibling();
04559 } while ( $node );
04560
04561
04562 if ( $mode === 'replace' ) {
04563
04564
04565
04566
04567 if($newText != "") {
04568 $outText .= $newText . "\n\n";
04569 }
04570
04571 while ( $node ) {
04572 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
04573 $node = $node->getNextSibling();
04574 }
04575 }
04576
04577 if ( is_string( $outText ) ) {
04578
04579 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
04580 }
04581
04582 return $outText;
04583 }
04584
04597 public function getSection( $text, $section, $deftext='' ) {
04598 return $this->extractSections( $text, $section, "get", $deftext );
04599 }
04600
04601 public function replaceSection( $oldtext, $section, $text ) {
04602 return $this->extractSections( $oldtext, $section, "replace", $text );
04603 }
04604
04609 function getRevisionTimestamp() {
04610 if ( is_null( $this->mRevisionTimestamp ) ) {
04611 wfProfileIn( __METHOD__ );
04612 global $wgContLang;
04613 $dbr = wfGetDB( DB_SLAVE );
04614 $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
04615 array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
04616
04617
04618
04619
04620
04621 $timestamp = wfTimestamp( TS_MW, $timestamp );
04622
04623
04624
04625
04626
04627
04628
04629 $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
04630
04631 wfProfileOut( __METHOD__ );
04632 }
04633 return $this->mRevisionTimestamp;
04634 }
04635
04639 function getRevisionUser() {
04640
04641
04642 if( $this->mRevisionId ) {
04643 $revision = Revision::newFromId( $this->mRevisionId );
04644 $revuser = $revision->getUserText();
04645 } else {
04646 global $wgUser;
04647 $revuser = $wgUser->getName();
04648 }
04649 return $revuser;
04650 }
04651
04657 public function setDefaultSort( $sort ) {
04658 $this->mDefaultSort = $sort;
04659 }
04660
04667 public function getDefaultSort() {
04668 global $wgCategoryPrefixedDefaultSortkey;
04669 if( $this->mDefaultSort !== false ) {
04670 return $this->mDefaultSort;
04671 } elseif ($this->mTitle->getNamespace() == NS_CATEGORY ||
04672 !$wgCategoryPrefixedDefaultSortkey) {
04673 return $this->mTitle->getText();
04674 } else {
04675 return $this->mTitle->getPrefixedText();
04676 }
04677 }
04678
04685 public function getCustomDefaultSort() {
04686 return $this->mDefaultSort;
04687 }
04688
04694 public function guessSectionNameFromWikiText( $text ) {
04695 # Strip out wikitext links(they break the anchor)
04696 $text = $this->stripSectionName( $text );
04697 $headline = Sanitizer::decodeCharReferences( $text );
04698 # strip out HTML
04699 $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
04700 $headline = trim( $headline );
04701 $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
04702 $replacearray = array(
04703 '%3A' => ':',
04704 '%' => '.'
04705 );
04706 return str_replace(
04707 array_keys( $replacearray ),
04708 array_values( $replacearray ),
04709 $sectionanchor );
04710 }
04711
04726 public function stripSectionName( $text ) {
04727 # Strip internal link markup
04728 $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
04729 $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
04730
04731 # Strip external link markup (FIXME: Not Tolerant to blank link text
04732 # I.E. [http://www.mediawiki.org] will render as [1] or something depending
04733 # on how many empty links there are on the page - need to figure that out.
04734 $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
04735
04736 # Parse wikitext quotes (italics & bold)
04737 $text = $this->doQuotes($text);
04738
04739 # Strip HTML tags
04740 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
04741 return $text;
04742 }
04743
04744 function srvus( $text ) {
04745 return $this->testSrvus( $text, $this->mOutputType );
04746 }
04747
04751 function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
04752 $this->clearState();
04753 if ( ! ( $title instanceof Title ) ) {
04754 $title = Title::newFromText( $title );
04755 }
04756 $this->mTitle = $title;
04757 $this->mOptions = $options;
04758 $this->setOutputType( $outputType );
04759 $text = $this->replaceVariables( $text );
04760 $text = $this->mStripState->unstripBoth( $text );
04761 $text = Sanitizer::removeHTMLtags( $text );
04762 return $text;
04763 }
04764
04765 function testPst( $text, $title, $options ) {
04766 global $wgUser;
04767 if ( ! ( $title instanceof Title ) ) {
04768 $title = Title::newFromText( $title );
04769 }
04770 return $this->preSaveTransform( $text, $title, $wgUser, $options );
04771 }
04772
04773 function testPreprocess( $text, $title, $options ) {
04774 if ( ! ( $title instanceof Title ) ) {
04775 $title = Title::newFromText( $title );
04776 }
04777 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
04778 }
04779
04780 function markerSkipCallback( $s, $callback ) {
04781 $i = 0;
04782 $out = '';
04783 while ( $i < strlen( $s ) ) {
04784 $markerStart = strpos( $s, $this->mUniqPrefix, $i );
04785 if ( $markerStart === false ) {
04786 $out .= call_user_func( $callback, substr( $s, $i ) );
04787 break;
04788 } else {
04789 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
04790 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
04791 if ( $markerEnd === false ) {
04792 $out .= substr( $s, $markerStart );
04793 break;
04794 } else {
04795 $markerEnd += strlen( self::MARKER_SUFFIX );
04796 $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
04797 $i = $markerEnd;
04798 }
04799 }
04800 }
04801 return $out;
04802 }
04803
04804 function serialiseHalfParsedText( $text ) {
04805 $data = array();
04806 $data['text'] = $text;
04807
04808
04809
04810 $stripState = new StripState;
04811 $pos = 0;
04812 while( ( $start_pos = strpos( $text, $this->mUniqPrefix, $pos ) ) && ( $end_pos = strpos( $text, self::MARKER_SUFFIX, $pos ) ) ) {
04813 $end_pos += strlen( self::MARKER_SUFFIX );
04814 $marker = substr( $text, $start_pos, $end_pos-$start_pos );
04815
04816 if ( !empty( $this->mStripState->general->data[$marker] ) ) {
04817 $replaceArray = $stripState->general;
04818 $stripText = $this->mStripState->general->data[$marker];
04819 } elseif ( !empty( $this->mStripState->nowiki->data[$marker] ) ) {
04820 $replaceArray = $stripState->nowiki;
04821 $stripText = $this->mStripState->nowiki->data[$marker];
04822 } else {
04823 throw new MWException( "Hanging strip marker: '$marker'." );
04824 }
04825
04826 $replaceArray->setPair( $marker, $stripText );
04827 $pos = $end_pos;
04828 }
04829 $data['stripstate'] = $stripState;
04830
04831
04832
04833 $links = array( 'internal' => array(), 'interwiki' => array() );
04834 $pos = 0;
04835
04836
04837 while( ( $start_pos = strpos( $text, '<!--LINK ', $pos ) ) ) {
04838 list( $ns, $trail ) = explode( ':', substr( $text, $start_pos + strlen( '<!--LINK ' ) ), 2 );
04839
04840 $ns = trim($ns);
04841 if (empty( $links['internal'][$ns] )) {
04842 $links['internal'][$ns] = array();
04843 }
04844
04845 $key = trim( substr( $trail, 0, strpos( $trail, '-->' ) ) );
04846 $links['internal'][$ns][] = $this->mLinkHolders->internals[$ns][$key];
04847 $pos = $start_pos + strlen( "<!--LINK $ns:$key-->" );
04848 }
04849
04850 $pos = 0;
04851
04852
04853 while( ( $start_pos = strpos( $text, '<!--IWLINK ', $pos ) ) ) {
04854 $data = substr( $text, $start_pos );
04855 $key = trim( substr( $data, 0, strpos( $data, '-->' ) ) );
04856 $links['interwiki'][] = $this->mLinkHolders->interwiki[$key];
04857 $pos = $start_pos + strlen( "<!--IWLINK $key-->" );
04858 }
04859
04860 $data['linkholder'] = $links;
04861
04862 return $data;
04863 }
04864
04865 function unserialiseHalfParsedText( $data, $intPrefix = null ) {
04866 if (!$intPrefix)
04867 $intPrefix = $this->getRandomString();
04868
04869
04870 $stripState = $data['stripstate'];
04871 $this->mStripState->general->merge( $stripState->general );
04872 $this->mStripState->nowiki->merge( $stripState->nowiki );
04873
04874
04875 $text = $data['text'];
04876 $links = $data['linkholder'];
04877
04878
04879 foreach( $links['internal'] as $ns => $nsLinks ) {
04880 foreach( $nsLinks as $key => $entry ) {
04881 $newKey = $intPrefix . '-' . $key;
04882 $this->mLinkHolders->internals[$ns][$newKey] = $entry;
04883
04884 $text = str_replace( "<!--LINK $ns:$key-->", "<!--LINK $ns:$newKey-->", $text );
04885 }
04886 }
04887
04888
04889 foreach( $links['interwiki'] as $key => $entry ) {
04890 $newKey = "$intPrefix-$key";
04891 $this->mLinkHolders->interwikis[$newKey] = $entry;
04892
04893 $text = str_replace( "<!--IWLINK $key-->", "<!--IWLINK $newKey-->", $text );
04894 }
04895
04896
04897 return $text;
04898 }
04899 }
04900
04905 class StripState {
04906 var $general, $nowiki;
04907
04908 function __construct() {
04909 $this->general = new ReplacementArray;
04910 $this->nowiki = new ReplacementArray;
04911 }
04912
04913 function unstripGeneral( $text ) {
04914 wfProfileIn( __METHOD__ );
04915 do {
04916 $oldText = $text;
04917 $text = $this->general->replace( $text );
04918 } while ( $text !== $oldText );
04919 wfProfileOut( __METHOD__ );
04920 return $text;
04921 }
04922
04923 function unstripNoWiki( $text ) {
04924 wfProfileIn( __METHOD__ );
04925 do {
04926 $oldText = $text;
04927 $text = $this->nowiki->replace( $text );
04928 } while ( $text !== $oldText );
04929 wfProfileOut( __METHOD__ );
04930 return $text;
04931 }
04932
04933 function unstripBoth( $text ) {
04934 wfProfileIn( __METHOD__ );
04935 do {
04936 $oldText = $text;
04937 $text = $this->general->replace( $text );
04938 $text = $this->nowiki->replace( $text );
04939 } while ( $text !== $oldText );
04940 wfProfileOut( __METHOD__ );
04941 return $text;
04942 }
04943 }
04944
04949 class OnlyIncludeReplacer {
04950 var $output = '';
04951
04952 function replace( $matches ) {
04953 if ( substr( $matches[1], -1 ) === "\n" ) {
04954 $this->output .= substr( $matches[1], 0, -1 );
04955 } else {
04956 $this->output .= $matches[1];
04957 }
04958 }
04959 }