MediaWiki
REL1_22
|
00001 <?php 00067 class Parser { 00073 const VERSION = '1.6.4'; 00074 00079 const HALF_PARSED_VERSION = 2; 00080 00081 # Flags for Parser::setFunctionHook 00082 # Also available as global constants from Defines.php 00083 const SFH_NO_HASH = 1; 00084 const SFH_OBJECT_ARGS = 2; 00085 00086 # Constants needed for external link processing 00087 # Everything except bracket, space, or control characters 00088 # \p{Zs} is unicode 'separator, space' category. It covers the space 0x20 00089 # as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052 00090 const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]'; 00091 const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F\p{Zs}]+) 00092 \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu'; 00093 00094 # State constants for the definition list colon extraction 00095 const COLON_STATE_TEXT = 0; 00096 const COLON_STATE_TAG = 1; 00097 const COLON_STATE_TAGSTART = 2; 00098 const COLON_STATE_CLOSETAG = 3; 00099 const COLON_STATE_TAGSLASH = 4; 00100 const COLON_STATE_COMMENT = 5; 00101 const COLON_STATE_COMMENTDASH = 6; 00102 const COLON_STATE_COMMENTDASHDASH = 7; 00103 00104 # Flags for preprocessToDom 00105 const PTD_FOR_INCLUSION = 1; 00106 00107 # Allowed values for $this->mOutputType 00108 # Parameter to startExternalParse(). 00109 const OT_HTML = 1; # like parse() 00110 const OT_WIKI = 2; # like preSaveTransform() 00111 const OT_PREPROCESS = 3; # like preprocess() 00112 const OT_MSG = 3; 00113 const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged. 00114 00115 # Marker Suffix needs to be accessible staticly. 00116 const MARKER_SUFFIX = "-QINU\x7f"; 00117 00118 # Markers used for wrapping the table of contents 00119 const TOC_START = '<mw:toc>'; 00120 const TOC_END = '</mw:toc>'; 00121 00122 # Persistent: 00123 var $mTagHooks = array(); 00124 var $mTransparentTagHooks = array(); 00125 var $mFunctionHooks = array(); 00126 var $mFunctionSynonyms = array( 0 => array(), 1 => array() ); 00127 var $mFunctionTagHooks = array(); 00128 var $mStripList = array(); 00129 var $mDefaultStripList = array(); 00130 var $mVarCache = array(); 00131 var $mImageParams = array(); 00132 var $mImageParamsMagicArray = array(); 00133 var $mMarkerIndex = 0; 00134 var $mFirstCall = true; 00135 00136 # Initialised by initialiseVariables() 00137 00141 var $mVariables; 00142 00146 var $mSubstWords; 00147 var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised in constructor 00148 00149 # Cleared with clearState(): 00150 00153 var $mOutput; 00154 var $mAutonumber, $mDTopen; 00155 00159 var $mStripState; 00160 00161 var $mIncludeCount, $mArgStack, $mLastSection, $mInPre; 00165 var $mLinkHolders; 00166 00167 var $mLinkID; 00168 var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth; 00169 var $mDefaultSort; 00170 var $mTplExpandCache; # empty-frame expansion cache 00171 var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores; 00172 var $mExpensiveFunctionCount; # number of expensive parser function calls 00173 var $mShowToc, $mForceTocPosition; 00174 00178 var $mUser; # User object; only used when doing pre-save transform 00179 00180 # Temporary 00181 # These are variables reset at least once per parse regardless of $clearState 00182 00186 var $mOptions; 00187 00191 var $mTitle; # Title context, used for self-link rendering and similar things 00192 var $mOutputType; # Output type, one of the OT_xxx constants 00193 var $ot; # Shortcut alias, see setOutputType() 00194 var $mRevisionObject; # The revision object of the specified revision ID 00195 var $mRevisionId; # ID to display in {{REVISIONID}} tags 00196 var $mRevisionTimestamp; # The timestamp of the specified revision ID 00197 var $mRevisionUser; # User to display in {{REVISIONUSER}} tag 00198 var $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable 00199 var $mRevIdForTs; # The revision ID which was used to fetch the timestamp 00200 var $mInputSize = false; # For {{PAGESIZE}} on current page. 00201 00205 var $mUniqPrefix; 00206 00212 var $mLangLinkLanguages; 00213 00219 public function __construct( $conf = array() ) { 00220 $this->mConf = $conf; 00221 $this->mUrlProtocols = wfUrlProtocols(); 00222 $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' . 00223 self::EXT_LINK_URL_CLASS . '+)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su'; 00224 if ( isset( $conf['preprocessorClass'] ) ) { 00225 $this->mPreprocessorClass = $conf['preprocessorClass']; 00226 } elseif ( defined( 'HPHP_VERSION' ) ) { 00227 # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop 00228 $this->mPreprocessorClass = 'Preprocessor_Hash'; 00229 } elseif ( extension_loaded( 'domxml' ) ) { 00230 # PECL extension that conflicts with the core DOM extension (bug 13770) 00231 wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" ); 00232 $this->mPreprocessorClass = 'Preprocessor_Hash'; 00233 } elseif ( extension_loaded( 'dom' ) ) { 00234 $this->mPreprocessorClass = 'Preprocessor_DOM'; 00235 } else { 00236 $this->mPreprocessorClass = 'Preprocessor_Hash'; 00237 } 00238 wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" ); 00239 } 00240 00244 function __destruct() { 00245 if ( isset( $this->mLinkHolders ) ) { 00246 unset( $this->mLinkHolders ); 00247 } 00248 foreach ( $this as $name => $value ) { 00249 unset( $this->$name ); 00250 } 00251 } 00252 00256 function __clone() { 00257 wfRunHooks( 'ParserCloned', array( $this ) ); 00258 } 00259 00263 function firstCallInit() { 00264 if ( !$this->mFirstCall ) { 00265 return; 00266 } 00267 $this->mFirstCall = false; 00268 00269 wfProfileIn( __METHOD__ ); 00270 00271 CoreParserFunctions::register( $this ); 00272 CoreTagHooks::register( $this ); 00273 $this->initialiseVariables(); 00274 00275 wfRunHooks( 'ParserFirstCallInit', array( &$this ) ); 00276 wfProfileOut( __METHOD__ ); 00277 } 00278 00284 function clearState() { 00285 wfProfileIn( __METHOD__ ); 00286 if ( $this->mFirstCall ) { 00287 $this->firstCallInit(); 00288 } 00289 $this->mOutput = new ParserOutput; 00290 $this->mOptions->registerWatcher( array( $this->mOutput, 'recordOption' ) ); 00291 $this->mAutonumber = 0; 00292 $this->mLastSection = ''; 00293 $this->mDTopen = false; 00294 $this->mIncludeCount = array(); 00295 $this->mArgStack = false; 00296 $this->mInPre = false; 00297 $this->mLinkHolders = new LinkHolderArray( $this ); 00298 $this->mLinkID = 0; 00299 $this->mRevisionObject = $this->mRevisionTimestamp = 00300 $this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null; 00301 $this->mVarCache = array(); 00302 $this->mUser = null; 00303 $this->mLangLinkLanguages = array(); 00304 00315 $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString(); 00316 $this->mStripState = new StripState( $this->mUniqPrefix ); 00317 00318 # Clear these on every parse, bug 4549 00319 $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array(); 00320 00321 $this->mShowToc = true; 00322 $this->mForceTocPosition = false; 00323 $this->mIncludeSizes = array( 00324 'post-expand' => 0, 00325 'arg' => 0, 00326 ); 00327 $this->mPPNodeCount = 0; 00328 $this->mGeneratedPPNodeCount = 0; 00329 $this->mHighestExpansionDepth = 0; 00330 $this->mDefaultSort = false; 00331 $this->mHeadings = array(); 00332 $this->mDoubleUnderscores = array(); 00333 $this->mExpensiveFunctionCount = 0; 00334 00335 # Fix cloning 00336 if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) { 00337 $this->mPreprocessor = null; 00338 } 00339 00340 wfRunHooks( 'ParserClearState', array( &$this ) ); 00341 wfProfileOut( __METHOD__ ); 00342 } 00343 00356 public function parse( $text, Title $title, ParserOptions $options, $linestart = true, $clearState = true, $revid = null ) { 00362 global $wgUseTidy, $wgAlwaysUseTidy, $wgShowHostnames; 00363 $fname = __METHOD__ . '-' . wfGetCaller(); 00364 wfProfileIn( __METHOD__ ); 00365 wfProfileIn( $fname ); 00366 00367 $this->startParse( $title, $options, self::OT_HTML, $clearState ); 00368 00369 $this->mInputSize = strlen( $text ); 00370 if ( $this->mOptions->getEnableLimitReport() ) { 00371 $this->mOutput->resetParseStartTime(); 00372 } 00373 00374 # Remove the strip marker tag prefix from the input, if present. 00375 if ( $clearState ) { 00376 $text = str_replace( $this->mUniqPrefix, '', $text ); 00377 } 00378 00379 $oldRevisionId = $this->mRevisionId; 00380 $oldRevisionObject = $this->mRevisionObject; 00381 $oldRevisionTimestamp = $this->mRevisionTimestamp; 00382 $oldRevisionUser = $this->mRevisionUser; 00383 $oldRevisionSize = $this->mRevisionSize; 00384 if ( $revid !== null ) { 00385 $this->mRevisionId = $revid; 00386 $this->mRevisionObject = null; 00387 $this->mRevisionTimestamp = null; 00388 $this->mRevisionUser = null; 00389 $this->mRevisionSize = null; 00390 } 00391 00392 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); 00393 # No more strip! 00394 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); 00395 $text = $this->internalParse( $text ); 00396 wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) ); 00397 00398 $text = $this->mStripState->unstripGeneral( $text ); 00399 00400 # Clean up special characters, only run once, next-to-last before doBlockLevels 00401 $fixtags = array( 00402 # french spaces, last one Guillemet-left 00403 # only if there is something before the space 00404 '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 ', 00405 # french spaces, Guillemet-right 00406 '/(\\302\\253) /' => '\\1 ', 00407 '/ (!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874. 00408 ); 00409 $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text ); 00410 00411 $text = $this->doBlockLevels( $text, $linestart ); 00412 00413 $this->replaceLinkHolders( $text ); 00414 00422 if ( !( $options->getDisableContentConversion() 00423 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) ) 00424 { 00425 if ( !$this->mOptions->getInterfaceMessage() ) { 00426 # The position of the convert() call should not be changed. it 00427 # assumes that the links are all replaced and the only thing left 00428 # is the <nowiki> mark. 00429 $text = $this->getConverterLanguage()->convert( $text ); 00430 } 00431 } 00432 00440 if ( !( $options->getDisableTitleConversion() 00441 || isset( $this->mDoubleUnderscores['nocontentconvert'] ) 00442 || isset( $this->mDoubleUnderscores['notitleconvert'] ) 00443 || $this->mOutput->getDisplayTitle() !== false ) ) 00444 { 00445 $convruletitle = $this->getConverterLanguage()->getConvRuleTitle(); 00446 if ( $convruletitle ) { 00447 $this->mOutput->setTitleText( $convruletitle ); 00448 } else { 00449 $titleText = $this->getConverterLanguage()->convertTitle( $title ); 00450 $this->mOutput->setTitleText( $titleText ); 00451 } 00452 } 00453 00454 $text = $this->mStripState->unstripNoWiki( $text ); 00455 00456 wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); 00457 00458 $text = $this->replaceTransparentTags( $text ); 00459 $text = $this->mStripState->unstripGeneral( $text ); 00460 00461 $text = Sanitizer::normalizeCharReferences( $text ); 00462 00463 if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) { 00464 $text = MWTidy::tidy( $text ); 00465 } else { 00466 # attempt to sanitize at least some nesting problems 00467 # (bug #2702 and quite a few others) 00468 $tidyregs = array( 00469 # ''Something [http://www.cool.com cool''] --> 00470 # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a> 00471 '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' => 00472 '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9', 00473 # fix up an anchor inside another anchor, only 00474 # at least for a single single nested link (bug 3695) 00475 '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' => 00476 '\\1\\2</a>\\3</a>\\1\\4</a>', 00477 # fix div inside inline elements- doBlockLevels won't wrap a line which 00478 # contains a div, so fix it up here; replace 00479 # div with escaped text 00480 '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' => 00481 '\\1\\3<div\\5>\\6</div>\\8\\9', 00482 # remove empty italic or bold tag pairs, some 00483 # introduced by rules above 00484 '/<([bi])><\/\\1>/' => '', 00485 ); 00486 00487 $text = preg_replace( 00488 array_keys( $tidyregs ), 00489 array_values( $tidyregs ), 00490 $text ); 00491 } 00492 00493 if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) { 00494 $this->limitationWarn( 'expensive-parserfunction', 00495 $this->mExpensiveFunctionCount, 00496 $this->mOptions->getExpensiveParserFunctionLimit() 00497 ); 00498 } 00499 00500 wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) ); 00501 00502 # Information on include size limits, for the benefit of users who try to skirt them 00503 if ( $this->mOptions->getEnableLimitReport() ) { 00504 $max = $this->mOptions->getMaxIncludeSize(); 00505 00506 $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' ); 00507 if ( $cpuTime !== null ) { 00508 $this->mOutput->setLimitReportData( 'limitreport-cputime', 00509 sprintf( "%.3f", $cpuTime ) 00510 ); 00511 } 00512 00513 $wallTime = $this->mOutput->getTimeSinceStart( 'wall' ); 00514 $this->mOutput->setLimitReportData( 'limitreport-walltime', 00515 sprintf( "%.3f", $wallTime ) 00516 ); 00517 00518 $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes', 00519 array( $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ) 00520 ); 00521 $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes', 00522 array( $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ) 00523 ); 00524 $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize', 00525 array( $this->mIncludeSizes['post-expand'], $max ) 00526 ); 00527 $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize', 00528 array( $this->mIncludeSizes['arg'], $max ) 00529 ); 00530 $this->mOutput->setLimitReportData( 'limitreport-expansiondepth', 00531 array( $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ) 00532 ); 00533 $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount', 00534 array( $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ) 00535 ); 00536 wfRunHooks( 'ParserLimitReportPrepare', array( $this, $this->mOutput ) ); 00537 00538 $limitReport = "NewPP limit report\n"; 00539 if ( $wgShowHostnames ) { 00540 $limitReport .= 'Parsed by ' . wfHostname() . "\n"; 00541 } 00542 foreach ( $this->mOutput->getLimitReportData() as $key => $value ) { 00543 if ( wfRunHooks( 'ParserLimitReportFormat', 00544 array( $key, $value, &$limitReport, false, false ) 00545 ) ) { 00546 $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false ); 00547 $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) ) 00548 ->inLanguage( 'en' )->useDatabase( false ); 00549 if ( !$valueMsg->exists() ) { 00550 $valueMsg = new RawMessage( '$1' ); 00551 } 00552 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) { 00553 $valueMsg->params( $value ); 00554 $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n"; 00555 } 00556 } 00557 } 00558 // Since we're not really outputting HTML, decode the entities and 00559 // then re-encode the things that need hiding inside HTML comments. 00560 $limitReport = htmlspecialchars_decode( $limitReport ); 00561 wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) ); 00562 00563 // Sanitize for comment. Note '‐' in the replacement is U+2010, 00564 // which looks much like the problematic '-'. 00565 $limitReport = str_replace( array( '-', '&' ), array( '‐', '&' ), $limitReport ); 00566 $text .= "\n<!-- \n$limitReport-->\n"; 00567 00568 if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) { 00569 wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' . 00570 $this->mTitle->getPrefixedDBkey() ); 00571 } 00572 } 00573 $this->mOutput->setText( $text ); 00574 00575 $this->mRevisionId = $oldRevisionId; 00576 $this->mRevisionObject = $oldRevisionObject; 00577 $this->mRevisionTimestamp = $oldRevisionTimestamp; 00578 $this->mRevisionUser = $oldRevisionUser; 00579 $this->mRevisionSize = $oldRevisionSize; 00580 $this->mInputSize = false; 00581 wfProfileOut( $fname ); 00582 wfProfileOut( __METHOD__ ); 00583 00584 return $this->mOutput; 00585 } 00586 00598 function recursiveTagParse( $text, $frame = false ) { 00599 wfProfileIn( __METHOD__ ); 00600 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); 00601 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); 00602 $text = $this->internalParse( $text, false, $frame ); 00603 wfProfileOut( __METHOD__ ); 00604 return $text; 00605 } 00606 00612 function preprocess( $text, Title $title = null, ParserOptions $options, $revid = null ) { 00613 wfProfileIn( __METHOD__ ); 00614 $this->startParse( $title, $options, self::OT_PREPROCESS, true ); 00615 if ( $revid !== null ) { 00616 $this->mRevisionId = $revid; 00617 } 00618 wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) ); 00619 wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) ); 00620 $text = $this->replaceVariables( $text ); 00621 $text = $this->mStripState->unstripBoth( $text ); 00622 wfProfileOut( __METHOD__ ); 00623 return $text; 00624 } 00625 00635 public function recursivePreprocess( $text, $frame = false ) { 00636 wfProfileIn( __METHOD__ ); 00637 $text = $this->replaceVariables( $text, $frame ); 00638 $text = $this->mStripState->unstripBoth( $text ); 00639 wfProfileOut( __METHOD__ ); 00640 return $text; 00641 } 00642 00655 public function getPreloadText( $text, Title $title, ParserOptions $options ) { 00656 # Parser (re)initialisation 00657 $this->startParse( $title, $options, self::OT_PLAIN, true ); 00658 00659 $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES; 00660 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); 00661 $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags ); 00662 $text = $this->mStripState->unstripBoth( $text ); 00663 return $text; 00664 } 00665 00671 public static function getRandomString() { 00672 return wfRandomString( 16 ); 00673 } 00674 00681 function setUser( $user ) { 00682 $this->mUser = $user; 00683 } 00684 00690 public function uniqPrefix() { 00691 if ( !isset( $this->mUniqPrefix ) ) { 00692 # @todo FIXME: This is probably *horribly wrong* 00693 # LanguageConverter seems to want $wgParser's uniqPrefix, however 00694 # if this is called for a parser cache hit, the parser may not 00695 # have ever been initialized in the first place. 00696 # Not really sure what the heck is supposed to be going on here. 00697 return ''; 00698 # throw new MWException( "Accessing uninitialized mUniqPrefix" ); 00699 } 00700 return $this->mUniqPrefix; 00701 } 00702 00708 function setTitle( $t ) { 00709 if ( !$t || $t instanceof FakeTitle ) { 00710 $t = Title::newFromText( 'NO TITLE' ); 00711 } 00712 00713 if ( strval( $t->getFragment() ) !== '' ) { 00714 # Strip the fragment to avoid various odd effects 00715 $this->mTitle = clone $t; 00716 $this->mTitle->setFragment( '' ); 00717 } else { 00718 $this->mTitle = $t; 00719 } 00720 } 00721 00727 function getTitle() { 00728 return $this->mTitle; 00729 } 00730 00737 function Title( $x = null ) { 00738 return wfSetVar( $this->mTitle, $x ); 00739 } 00740 00746 function setOutputType( $ot ) { 00747 $this->mOutputType = $ot; 00748 # Shortcut alias 00749 $this->ot = array( 00750 'html' => $ot == self::OT_HTML, 00751 'wiki' => $ot == self::OT_WIKI, 00752 'pre' => $ot == self::OT_PREPROCESS, 00753 'plain' => $ot == self::OT_PLAIN, 00754 ); 00755 } 00756 00763 function OutputType( $x = null ) { 00764 return wfSetVar( $this->mOutputType, $x ); 00765 } 00766 00772 function getOutput() { 00773 return $this->mOutput; 00774 } 00775 00781 function getOptions() { 00782 return $this->mOptions; 00783 } 00784 00791 function Options( $x = null ) { 00792 return wfSetVar( $this->mOptions, $x ); 00793 } 00794 00798 function nextLinkID() { 00799 return $this->mLinkID++; 00800 } 00801 00805 function setLinkID( $id ) { 00806 $this->mLinkID = $id; 00807 } 00808 00813 function getFunctionLang() { 00814 return $this->getTargetLanguage(); 00815 } 00816 00826 public function getTargetLanguage() { 00827 $target = $this->mOptions->getTargetLanguage(); 00828 00829 if ( $target !== null ) { 00830 return $target; 00831 } elseif ( $this->mOptions->getInterfaceMessage() ) { 00832 return $this->mOptions->getUserLangObj(); 00833 } elseif ( is_null( $this->mTitle ) ) { 00834 throw new MWException( __METHOD__ . ': $this->mTitle is null' ); 00835 } 00836 00837 return $this->mTitle->getPageLanguage(); 00838 } 00839 00843 function getConverterLanguage() { 00844 return $this->getTargetLanguage(); 00845 } 00846 00853 function getUser() { 00854 if ( !is_null( $this->mUser ) ) { 00855 return $this->mUser; 00856 } 00857 return $this->mOptions->getUser(); 00858 } 00859 00865 function getPreprocessor() { 00866 if ( !isset( $this->mPreprocessor ) ) { 00867 $class = $this->mPreprocessorClass; 00868 $this->mPreprocessor = new $class( $this ); 00869 } 00870 return $this->mPreprocessor; 00871 } 00872 00893 public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = '' ) { 00894 static $n = 1; 00895 $stripped = ''; 00896 $matches = array(); 00897 00898 $taglist = implode( '|', $elements ); 00899 $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i"; 00900 00901 while ( $text != '' ) { 00902 $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); 00903 $stripped .= $p[0]; 00904 if ( count( $p ) < 5 ) { 00905 break; 00906 } 00907 if ( count( $p ) > 5 ) { 00908 # comment 00909 $element = $p[4]; 00910 $attributes = ''; 00911 $close = ''; 00912 $inside = $p[5]; 00913 } else { 00914 # tag 00915 $element = $p[1]; 00916 $attributes = $p[2]; 00917 $close = $p[3]; 00918 $inside = $p[4]; 00919 } 00920 00921 $marker = "$uniq_prefix-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX; 00922 $stripped .= $marker; 00923 00924 if ( $close === '/>' ) { 00925 # Empty element tag, <tag /> 00926 $content = null; 00927 $text = $inside; 00928 $tail = null; 00929 } else { 00930 if ( $element === '!--' ) { 00931 $end = '/(-->)/'; 00932 } else { 00933 $end = "/(<\\/$element\\s*>)/i"; 00934 } 00935 $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE ); 00936 $content = $q[0]; 00937 if ( count( $q ) < 3 ) { 00938 # No end tag -- let it run out to the end of the text. 00939 $tail = ''; 00940 $text = ''; 00941 } else { 00942 $tail = $q[1]; 00943 $text = $q[2]; 00944 } 00945 } 00946 00947 $matches[$marker] = array( $element, 00948 $content, 00949 Sanitizer::decodeTagAttributes( $attributes ), 00950 "<$element$attributes$close$content$tail" ); 00951 } 00952 return $stripped; 00953 } 00954 00960 function getStripList() { 00961 return $this->mStripList; 00962 } 00963 00973 function insertStripItem( $text ) { 00974 $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX; 00975 $this->mMarkerIndex++; 00976 $this->mStripState->addGeneral( $rnd, $text ); 00977 return $rnd; 00978 } 00979 00986 function doTableStuff( $text ) { 00987 wfProfileIn( __METHOD__ ); 00988 00989 $lines = StringUtils::explode( "\n", $text ); 00990 $out = ''; 00991 $td_history = array(); # Is currently a td tag open? 00992 $last_tag_history = array(); # Save history of last lag activated (td, th or caption) 00993 $tr_history = array(); # Is currently a tr tag open? 00994 $tr_attributes = array(); # history of tr attributes 00995 $has_opened_tr = array(); # Did this table open a <tr> element? 00996 $indent_level = 0; # indent level of the table 00997 00998 foreach ( $lines as $outLine ) { 00999 $line = trim( $outLine ); 01000 01001 if ( $line === '' ) { # empty line, go to next line 01002 $out .= $outLine . "\n"; 01003 continue; 01004 } 01005 01006 $first_character = $line[0]; 01007 $matches = array(); 01008 01009 if ( preg_match( '/^(:*)\{\|(.*)$/', $line, $matches ) ) { 01010 # First check if we are starting a new table 01011 $indent_level = strlen( $matches[1] ); 01012 01013 $attributes = $this->mStripState->unstripBoth( $matches[2] ); 01014 $attributes = Sanitizer::fixTagAttributes( $attributes, 'table' ); 01015 01016 $outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>"; 01017 array_push( $td_history, false ); 01018 array_push( $last_tag_history, '' ); 01019 array_push( $tr_history, false ); 01020 array_push( $tr_attributes, '' ); 01021 array_push( $has_opened_tr, false ); 01022 } elseif ( count( $td_history ) == 0 ) { 01023 # Don't do any of the following 01024 $out .= $outLine . "\n"; 01025 continue; 01026 } elseif ( substr( $line, 0, 2 ) === '|}' ) { 01027 # We are ending a table 01028 $line = '</table>' . substr( $line, 2 ); 01029 $last_tag = array_pop( $last_tag_history ); 01030 01031 if ( !array_pop( $has_opened_tr ) ) { 01032 $line = "<tr><td></td></tr>{$line}"; 01033 } 01034 01035 if ( array_pop( $tr_history ) ) { 01036 $line = "</tr>{$line}"; 01037 } 01038 01039 if ( array_pop( $td_history ) ) { 01040 $line = "</{$last_tag}>{$line}"; 01041 } 01042 array_pop( $tr_attributes ); 01043 $outLine = $line . str_repeat( '</dd></dl>', $indent_level ); 01044 } elseif ( substr( $line, 0, 2 ) === '|-' ) { 01045 # Now we have a table row 01046 $line = preg_replace( '#^\|-+#', '', $line ); 01047 01048 # Whats after the tag is now only attributes 01049 $attributes = $this->mStripState->unstripBoth( $line ); 01050 $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' ); 01051 array_pop( $tr_attributes ); 01052 array_push( $tr_attributes, $attributes ); 01053 01054 $line = ''; 01055 $last_tag = array_pop( $last_tag_history ); 01056 array_pop( $has_opened_tr ); 01057 array_push( $has_opened_tr, true ); 01058 01059 if ( array_pop( $tr_history ) ) { 01060 $line = '</tr>'; 01061 } 01062 01063 if ( array_pop( $td_history ) ) { 01064 $line = "</{$last_tag}>{$line}"; 01065 } 01066 01067 $outLine = $line; 01068 array_push( $tr_history, false ); 01069 array_push( $td_history, false ); 01070 array_push( $last_tag_history, '' ); 01071 } elseif ( $first_character === '|' || $first_character === '!' || substr( $line, 0, 2 ) === '|+' ) { 01072 # This might be cell elements, td, th or captions 01073 if ( substr( $line, 0, 2 ) === '|+' ) { 01074 $first_character = '+'; 01075 $line = substr( $line, 1 ); 01076 } 01077 01078 $line = substr( $line, 1 ); 01079 01080 if ( $first_character === '!' ) { 01081 $line = str_replace( '!!', '||', $line ); 01082 } 01083 01084 # Split up multiple cells on the same line. 01085 # FIXME : This can result in improper nesting of tags processed 01086 # by earlier parser steps, but should avoid splitting up eg 01087 # attribute values containing literal "||". 01088 $cells = StringUtils::explodeMarkup( '||', $line ); 01089 01090 $outLine = ''; 01091 01092 # Loop through each table cell 01093 foreach ( $cells as $cell ) { 01094 $previous = ''; 01095 if ( $first_character !== '+' ) { 01096 $tr_after = array_pop( $tr_attributes ); 01097 if ( !array_pop( $tr_history ) ) { 01098 $previous = "<tr{$tr_after}>\n"; 01099 } 01100 array_push( $tr_history, true ); 01101 array_push( $tr_attributes, '' ); 01102 array_pop( $has_opened_tr ); 01103 array_push( $has_opened_tr, true ); 01104 } 01105 01106 $last_tag = array_pop( $last_tag_history ); 01107 01108 if ( array_pop( $td_history ) ) { 01109 $previous = "</{$last_tag}>\n{$previous}"; 01110 } 01111 01112 if ( $first_character === '|' ) { 01113 $last_tag = 'td'; 01114 } elseif ( $first_character === '!' ) { 01115 $last_tag = 'th'; 01116 } elseif ( $first_character === '+' ) { 01117 $last_tag = 'caption'; 01118 } else { 01119 $last_tag = ''; 01120 } 01121 01122 array_push( $last_tag_history, $last_tag ); 01123 01124 # A cell could contain both parameters and data 01125 $cell_data = explode( '|', $cell, 2 ); 01126 01127 # Bug 553: Note that a '|' inside an invalid link should not 01128 # be mistaken as delimiting cell parameters 01129 if ( strpos( $cell_data[0], '[[' ) !== false ) { 01130 $cell = "{$previous}<{$last_tag}>{$cell}"; 01131 } elseif ( count( $cell_data ) == 1 ) { 01132 $cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; 01133 } else { 01134 $attributes = $this->mStripState->unstripBoth( $cell_data[0] ); 01135 $attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag ); 01136 $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; 01137 } 01138 01139 $outLine .= $cell; 01140 array_push( $td_history, true ); 01141 } 01142 } 01143 $out .= $outLine . "\n"; 01144 } 01145 01146 # Closing open td, tr && table 01147 while ( count( $td_history ) > 0 ) { 01148 if ( array_pop( $td_history ) ) { 01149 $out .= "</td>\n"; 01150 } 01151 if ( array_pop( $tr_history ) ) { 01152 $out .= "</tr>\n"; 01153 } 01154 if ( !array_pop( $has_opened_tr ) ) { 01155 $out .= "<tr><td></td></tr>\n"; 01156 } 01157 01158 $out .= "</table>\n"; 01159 } 01160 01161 # Remove trailing line-ending (b/c) 01162 if ( substr( $out, -1 ) === "\n" ) { 01163 $out = substr( $out, 0, -1 ); 01164 } 01165 01166 # special case: don't return empty table 01167 if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) { 01168 $out = ''; 01169 } 01170 01171 wfProfileOut( __METHOD__ ); 01172 01173 return $out; 01174 } 01175 01188 function internalParse( $text, $isMain = true, $frame = false ) { 01189 wfProfileIn( __METHOD__ ); 01190 01191 $origText = $text; 01192 01193 # Hook to suspend the parser in this state 01194 if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) { 01195 wfProfileOut( __METHOD__ ); 01196 return $text; 01197 } 01198 01199 # if $frame is provided, then use $frame for replacing any variables 01200 if ( $frame ) { 01201 # use frame depth to infer how include/noinclude tags should be handled 01202 # depth=0 means this is the top-level document; otherwise it's an included document 01203 if ( !$frame->depth ) { 01204 $flag = 0; 01205 } else { 01206 $flag = Parser::PTD_FOR_INCLUSION; 01207 } 01208 $dom = $this->preprocessToDom( $text, $flag ); 01209 $text = $frame->expand( $dom ); 01210 } else { 01211 # if $frame is not provided, then use old-style replaceVariables 01212 $text = $this->replaceVariables( $text ); 01213 } 01214 01215 wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) ); 01216 $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) ); 01217 wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); 01218 01219 # Tables need to come after variable replacement for things to work 01220 # properly; putting them before other transformations should keep 01221 # exciting things like link expansions from showing up in surprising 01222 # places. 01223 $text = $this->doTableStuff( $text ); 01224 01225 $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text ); 01226 01227 $text = $this->doDoubleUnderscore( $text ); 01228 01229 $text = $this->doHeadings( $text ); 01230 $text = $this->replaceInternalLinks( $text ); 01231 $text = $this->doAllQuotes( $text ); 01232 $text = $this->replaceExternalLinks( $text ); 01233 01234 # replaceInternalLinks may sometimes leave behind 01235 # absolute URLs, which have to be masked to hide them from replaceExternalLinks 01236 $text = str_replace( $this->mUniqPrefix . 'NOPARSE', '', $text ); 01237 01238 $text = $this->doMagicLinks( $text ); 01239 $text = $this->formatHeadings( $text, $origText, $isMain ); 01240 01241 wfProfileOut( __METHOD__ ); 01242 return $text; 01243 } 01244 01256 function doMagicLinks( $text ) { 01257 wfProfileIn( __METHOD__ ); 01258 $prots = wfUrlProtocolsWithoutProtRel(); 01259 $urlChar = self::EXT_LINK_URL_CLASS; 01260 $text = preg_replace_callback( 01261 '!(?: # Start cases 01262 (<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text 01263 (<.*?>) | # m[2]: Skip stuff inside HTML elements' . " 01264 (\\b(?i:$prots)$urlChar+) | # m[3]: Free external links" . ' 01265 (?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number 01266 ISBN\s+(\b # m[5]: ISBN, capture number 01267 (?: 97[89] [\ \-]? )? # optional 13-digit ISBN prefix 01268 (?: [0-9] [\ \-]? ){9} # 9 digits with opt. delimiters 01269 [0-9Xx] # check digit 01270 \b) 01271 )!xu', array( &$this, 'magicLinkCallback' ), $text ); 01272 wfProfileOut( __METHOD__ ); 01273 return $text; 01274 } 01275 01281 function magicLinkCallback( $m ) { 01282 if ( isset( $m[1] ) && $m[1] !== '' ) { 01283 # Skip anchor 01284 return $m[0]; 01285 } elseif ( isset( $m[2] ) && $m[2] !== '' ) { 01286 # Skip HTML element 01287 return $m[0]; 01288 } elseif ( isset( $m[3] ) && $m[3] !== '' ) { 01289 # Free external link 01290 return $this->makeFreeExternalLink( $m[0] ); 01291 } elseif ( isset( $m[4] ) && $m[4] !== '' ) { 01292 # RFC or PMID 01293 if ( substr( $m[0], 0, 3 ) === 'RFC' ) { 01294 $keyword = 'RFC'; 01295 $urlmsg = 'rfcurl'; 01296 $CssClass = 'mw-magiclink-rfc'; 01297 $id = $m[4]; 01298 } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) { 01299 $keyword = 'PMID'; 01300 $urlmsg = 'pubmedurl'; 01301 $CssClass = 'mw-magiclink-pmid'; 01302 $id = $m[4]; 01303 } else { 01304 throw new MWException( __METHOD__ . ': unrecognised match type "' . 01305 substr( $m[0], 0, 20 ) . '"' ); 01306 } 01307 $url = wfMessage( $urlmsg, $id )->inContentLanguage()->text(); 01308 return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass ); 01309 } elseif ( isset( $m[5] ) && $m[5] !== '' ) { 01310 # ISBN 01311 $isbn = $m[5]; 01312 $num = strtr( $isbn, array( 01313 '-' => '', 01314 ' ' => '', 01315 'x' => 'X', 01316 )); 01317 $titleObj = SpecialPage::getTitleFor( 'Booksources', $num ); 01318 return '<a href="' . 01319 htmlspecialchars( $titleObj->getLocalURL() ) . 01320 "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>"; 01321 } else { 01322 return $m[0]; 01323 } 01324 } 01325 01334 function makeFreeExternalLink( $url ) { 01335 wfProfileIn( __METHOD__ ); 01336 01337 $trail = ''; 01338 01339 # The characters '<' and '>' (which were escaped by 01340 # removeHTMLtags()) should not be included in 01341 # URLs, per RFC 2396. 01342 $m2 = array(); 01343 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) { 01344 $trail = substr( $url, $m2[0][1] ) . $trail; 01345 $url = substr( $url, 0, $m2[0][1] ); 01346 } 01347 01348 # Move trailing punctuation to $trail 01349 $sep = ',;\.:!?'; 01350 # If there is no left bracket, then consider right brackets fair game too 01351 if ( strpos( $url, '(' ) === false ) { 01352 $sep .= ')'; 01353 } 01354 01355 $numSepChars = strspn( strrev( $url ), $sep ); 01356 if ( $numSepChars ) { 01357 $trail = substr( $url, -$numSepChars ) . $trail; 01358 $url = substr( $url, 0, -$numSepChars ); 01359 } 01360 01361 $url = Sanitizer::cleanUrl( $url ); 01362 01363 # Is this an external image? 01364 $text = $this->maybeMakeExternalImage( $url ); 01365 if ( $text === false ) { 01366 # Not an image, make a link 01367 $text = Linker::makeExternalLink( $url, 01368 $this->getConverterLanguage()->markNoConversion( $url, true ), 01369 true, 'free', 01370 $this->getExternalLinkAttribs( $url ) ); 01371 # Register it in the output object... 01372 # Replace unnecessary URL escape codes with their equivalent characters 01373 $pasteurized = self::replaceUnusualEscapes( $url ); 01374 $this->mOutput->addExternalLink( $pasteurized ); 01375 } 01376 wfProfileOut( __METHOD__ ); 01377 return $text . $trail; 01378 } 01379 01389 function doHeadings( $text ) { 01390 wfProfileIn( __METHOD__ ); 01391 for ( $i = 6; $i >= 1; --$i ) { 01392 $h = str_repeat( '=', $i ); 01393 $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text ); 01394 } 01395 wfProfileOut( __METHOD__ ); 01396 return $text; 01397 } 01398 01407 function doAllQuotes( $text ) { 01408 wfProfileIn( __METHOD__ ); 01409 $outtext = ''; 01410 $lines = StringUtils::explode( "\n", $text ); 01411 foreach ( $lines as $line ) { 01412 $outtext .= $this->doQuotes( $line ) . "\n"; 01413 } 01414 $outtext = substr( $outtext, 0, -1 ); 01415 wfProfileOut( __METHOD__ ); 01416 return $outtext; 01417 } 01418 01426 public function doQuotes( $text ) { 01427 $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE ); 01428 $countarr = count( $arr ); 01429 if ( $countarr == 1 ) { 01430 return $text; 01431 } 01432 01433 // First, do some preliminary work. This may shift some apostrophes from 01434 // being mark-up to being text. It also counts the number of occurrences 01435 // of bold and italics mark-ups. 01436 $numbold = 0; 01437 $numitalics = 0; 01438 for ( $i = 1; $i < $countarr; $i += 2 ) { 01439 $thislen = strlen( $arr[$i] ); 01440 // If there are ever four apostrophes, assume the first is supposed to 01441 // be text, and the remaining three constitute mark-up for bold text. 01442 // (bug 13227: ''''foo'''' turns into ' ''' foo ' ''') 01443 if ( $thislen == 4 ) { 01444 $arr[$i - 1] .= "'"; 01445 $arr[$i] = "'''"; 01446 $thislen = 3; 01447 } elseif ( $thislen > 5 ) { 01448 // If there are more than 5 apostrophes in a row, assume they're all 01449 // text except for the last 5. 01450 // (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''') 01451 $arr[$i - 1] .= str_repeat( "'", $thislen - 5 ); 01452 $arr[$i] = "'''''"; 01453 $thislen = 5; 01454 } 01455 // Count the number of occurrences of bold and italics mark-ups. 01456 if ( $thislen == 2 ) { 01457 $numitalics++; 01458 } elseif ( $thislen == 3 ) { 01459 $numbold++; 01460 } elseif ( $thislen == 5 ) { 01461 $numitalics++; 01462 $numbold++; 01463 } 01464 } 01465 01466 // If there is an odd number of both bold and italics, it is likely 01467 // that one of the bold ones was meant to be an apostrophe followed 01468 // by italics. Which one we cannot know for certain, but it is more 01469 // likely to be one that has a single-letter word before it. 01470 if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) { 01471 $firstsingleletterword = -1; 01472 $firstmultiletterword = -1; 01473 $firstspace = -1; 01474 for ( $i = 1; $i < $countarr; $i += 2 ) { 01475 if ( strlen( $arr[$i] ) == 3 ) { 01476 $x1 = substr( $arr[$i - 1], -1 ); 01477 $x2 = substr( $arr[$i - 1], -2, 1 ); 01478 if ( $x1 === ' ' ) { 01479 if ( $firstspace == -1 ) { 01480 $firstspace = $i; 01481 } 01482 } elseif ( $x2 === ' ' ) { 01483 if ( $firstsingleletterword == -1 ) { 01484 $firstsingleletterword = $i; 01485 // if $firstsingleletterword is set, we don't 01486 // look at the other options, so we can bail early. 01487 break; 01488 } 01489 } else { 01490 if ( $firstmultiletterword == -1 ) { 01491 $firstmultiletterword = $i; 01492 } 01493 } 01494 } 01495 } 01496 01497 // If there is a single-letter word, use it! 01498 if ( $firstsingleletterword > -1 ) { 01499 $arr[$firstsingleletterword] = "''"; 01500 $arr[$firstsingleletterword - 1] .= "'"; 01501 } elseif ( $firstmultiletterword > -1 ) { 01502 // If not, but there's a multi-letter word, use that one. 01503 $arr[$firstmultiletterword] = "''"; 01504 $arr[$firstmultiletterword - 1] .= "'"; 01505 } elseif ( $firstspace > -1 ) { 01506 // ... otherwise use the first one that has neither. 01507 // (notice that it is possible for all three to be -1 if, for example, 01508 // there is only one pentuple-apostrophe in the line) 01509 $arr[$firstspace] = "''"; 01510 $arr[$firstspace - 1] .= "'"; 01511 } 01512 } 01513 01514 // Now let's actually convert our apostrophic mush to HTML! 01515 $output = ''; 01516 $buffer = ''; 01517 $state = ''; 01518 $i = 0; 01519 foreach ( $arr as $r ) { 01520 if ( ( $i % 2 ) == 0 ) { 01521 if ( $state === 'both' ) { 01522 $buffer .= $r; 01523 } else { 01524 $output .= $r; 01525 } 01526 } else { 01527 $thislen = strlen( $r ); 01528 if ( $thislen == 2 ) { 01529 if ( $state === 'i' ) { 01530 $output .= '</i>'; 01531 $state = ''; 01532 } elseif ( $state === 'bi' ) { 01533 $output .= '</i>'; 01534 $state = 'b'; 01535 } elseif ( $state === 'ib' ) { 01536 $output .= '</b></i><b>'; 01537 $state = 'b'; 01538 } elseif ( $state === 'both' ) { 01539 $output .= '<b><i>' . $buffer . '</i>'; 01540 $state = 'b'; 01541 } else { // $state can be 'b' or '' 01542 $output .= '<i>'; 01543 $state .= 'i'; 01544 } 01545 } elseif ( $thislen == 3 ) { 01546 if ( $state === 'b' ) { 01547 $output .= '</b>'; 01548 $state = ''; 01549 } elseif ( $state === 'bi' ) { 01550 $output .= '</i></b><i>'; 01551 $state = 'i'; 01552 } elseif ( $state === 'ib' ) { 01553 $output .= '</b>'; 01554 $state = 'i'; 01555 } elseif ( $state === 'both' ) { 01556 $output .= '<i><b>' . $buffer . '</b>'; 01557 $state = 'i'; 01558 } else { // $state can be 'i' or '' 01559 $output .= '<b>'; 01560 $state .= 'b'; 01561 } 01562 } elseif ( $thislen == 5 ) { 01563 if ( $state === 'b' ) { 01564 $output .= '</b><i>'; 01565 $state = 'i'; 01566 } elseif ( $state === 'i' ) { 01567 $output .= '</i><b>'; 01568 $state = 'b'; 01569 } elseif ( $state === 'bi' ) { 01570 $output .= '</i></b>'; 01571 $state = ''; 01572 } elseif ( $state === 'ib' ) { 01573 $output .= '</b></i>'; 01574 $state = ''; 01575 } elseif ( $state === 'both' ) { 01576 $output .= '<i><b>' . $buffer . '</b></i>'; 01577 $state = ''; 01578 } else { // ($state == '') 01579 $buffer = ''; 01580 $state = 'both'; 01581 } 01582 } 01583 } 01584 $i++; 01585 } 01586 // Now close all remaining tags. Notice that the order is important. 01587 if ( $state === 'b' || $state === 'ib' ) { 01588 $output .= '</b>'; 01589 } 01590 if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) { 01591 $output .= '</i>'; 01592 } 01593 if ( $state === 'bi' ) { 01594 $output .= '</b>'; 01595 } 01596 // There might be lonely ''''', so make sure we have a buffer 01597 if ( $state === 'both' && $buffer ) { 01598 $output .= '<b><i>' . $buffer . '</i></b>'; 01599 } 01600 return $output; 01601 } 01602 01616 function replaceExternalLinks( $text ) { 01617 wfProfileIn( __METHOD__ ); 01618 01619 $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); 01620 if ( $bits === false ) { 01621 wfProfileOut( __METHOD__ ); 01622 throw new MWException( "PCRE needs to be compiled with --enable-unicode-properties in order for MediaWiki to function" ); 01623 } 01624 $s = array_shift( $bits ); 01625 01626 $i = 0; 01627 while ( $i < count( $bits ) ) { 01628 $url = $bits[$i++]; 01629 $i++; // protocol 01630 $text = $bits[$i++]; 01631 $trail = $bits[$i++]; 01632 01633 # The characters '<' and '>' (which were escaped by 01634 # removeHTMLtags()) should not be included in 01635 # URLs, per RFC 2396. 01636 $m2 = array(); 01637 if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) { 01638 $text = substr( $url, $m2[0][1] ) . ' ' . $text; 01639 $url = substr( $url, 0, $m2[0][1] ); 01640 } 01641 01642 # If the link text is an image URL, replace it with an <img> tag 01643 # This happened by accident in the original parser, but some people used it extensively 01644 $img = $this->maybeMakeExternalImage( $text ); 01645 if ( $img !== false ) { 01646 $text = $img; 01647 } 01648 01649 $dtrail = ''; 01650 01651 # Set linktype for CSS - if URL==text, link is essentially free 01652 $linktype = ( $text === $url ) ? 'free' : 'text'; 01653 01654 # No link text, e.g. [http://domain.tld/some.link] 01655 if ( $text == '' ) { 01656 # Autonumber 01657 $langObj = $this->getTargetLanguage(); 01658 $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']'; 01659 $linktype = 'autonumber'; 01660 } else { 01661 # Have link text, e.g. [http://domain.tld/some.link text]s 01662 # Check for trail 01663 list( $dtrail, $trail ) = Linker::splitTrail( $trail ); 01664 } 01665 01666 $text = $this->getConverterLanguage()->markNoConversion( $text ); 01667 01668 $url = Sanitizer::cleanUrl( $url ); 01669 01670 # Use the encoded URL 01671 # This means that users can paste URLs directly into the text 01672 # Funny characters like ö aren't valid in URLs anyway 01673 # This was changed in August 2004 01674 $s .= Linker::makeExternalLink( $url, $text, false, $linktype, 01675 $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail; 01676 01677 # Register link in the output object. 01678 # Replace unnecessary URL escape codes with the referenced character 01679 # This prevents spammers from hiding links from the filters 01680 $pasteurized = self::replaceUnusualEscapes( $url ); 01681 $this->mOutput->addExternalLink( $pasteurized ); 01682 } 01683 01684 wfProfileOut( __METHOD__ ); 01685 return $s; 01686 } 01696 public static function getExternalLinkRel( $url = false, $title = null ) { 01697 global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions; 01698 $ns = $title ? $title->getNamespace() : false; 01699 if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) && 01700 !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) ) 01701 { 01702 return 'nofollow'; 01703 } 01704 return null; 01705 } 01716 function getExternalLinkAttribs( $url = false ) { 01717 $attribs = array(); 01718 $attribs['rel'] = self::getExternalLinkRel( $url, $this->mTitle ); 01719 01720 if ( $this->mOptions->getExternalLinkTarget() ) { 01721 $attribs['target'] = $this->mOptions->getExternalLinkTarget(); 01722 } 01723 return $attribs; 01724 } 01725 01737 static function replaceUnusualEscapes( $url ) { 01738 return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', 01739 array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url ); 01740 } 01741 01750 private static function replaceUnusualEscapesCallback( $matches ) { 01751 $char = urldecode( $matches[0] ); 01752 $ord = ord( $char ); 01753 # Is it an unsafe or HTTP reserved character according to RFC 1738? 01754 if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) { 01755 # No, shouldn't be escaped 01756 return $char; 01757 } else { 01758 # Yes, leave it escaped 01759 return $matches[0]; 01760 } 01761 } 01762 01772 function maybeMakeExternalImage( $url ) { 01773 $imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); 01774 $imagesexception = !empty( $imagesfrom ); 01775 $text = false; 01776 # $imagesfrom could be either a single string or an array of strings, parse out the latter 01777 if ( $imagesexception && is_array( $imagesfrom ) ) { 01778 $imagematch = false; 01779 foreach ( $imagesfrom as $match ) { 01780 if ( strpos( $url, $match ) === 0 ) { 01781 $imagematch = true; 01782 break; 01783 } 01784 } 01785 } elseif ( $imagesexception ) { 01786 $imagematch = ( strpos( $url, $imagesfrom ) === 0 ); 01787 } else { 01788 $imagematch = false; 01789 } 01790 if ( $this->mOptions->getAllowExternalImages() 01791 || ( $imagesexception && $imagematch ) ) { 01792 if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { 01793 # Image found 01794 $text = Linker::makeExternalImage( $url ); 01795 } 01796 } 01797 if ( !$text && $this->mOptions->getEnableImageWhitelist() 01798 && preg_match( self::EXT_IMAGE_REGEX, $url ) ) { 01799 $whitelist = explode( "\n", wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() ); 01800 foreach ( $whitelist as $entry ) { 01801 # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments 01802 if ( strpos( $entry, '#' ) === 0 || $entry === '' ) { 01803 continue; 01804 } 01805 if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) { 01806 # Image matches a whitelist entry 01807 $text = Linker::makeExternalImage( $url ); 01808 break; 01809 } 01810 } 01811 } 01812 return $text; 01813 } 01814 01824 function replaceInternalLinks( $s ) { 01825 $this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) ); 01826 return $s; 01827 } 01828 01837 function replaceInternalLinks2( &$s ) { 01838 wfProfileIn( __METHOD__ ); 01839 01840 wfProfileIn( __METHOD__ . '-setup' ); 01841 static $tc = false, $e1, $e1_img; 01842 # the % is needed to support urlencoded titles as well 01843 if ( !$tc ) { 01844 $tc = Title::legalChars() . '#%'; 01845 # Match a link having the form [[namespace:link|alternate]]trail 01846 $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; 01847 # Match cases where there is no "]]", which might still be images 01848 $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; 01849 } 01850 01851 $holders = new LinkHolderArray( $this ); 01852 01853 # split the entire text string on occurrences of [[ 01854 $a = StringUtils::explode( '[[', ' ' . $s ); 01855 # get the first element (all text up to first [[), and remove the space we added 01856 $s = $a->current(); 01857 $a->next(); 01858 $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void" 01859 $s = substr( $s, 1 ); 01860 01861 $useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension(); 01862 $e2 = null; 01863 if ( $useLinkPrefixExtension ) { 01864 # Match the end of a line for a word that's not followed by whitespace, 01865 # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched 01866 $e2 = wfMessage( 'linkprefix' )->inContentLanguage()->text(); 01867 } 01868 01869 if ( is_null( $this->mTitle ) ) { 01870 wfProfileOut( __METHOD__ . '-setup' ); 01871 wfProfileOut( __METHOD__ ); 01872 throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" ); 01873 } 01874 $nottalk = !$this->mTitle->isTalkPage(); 01875 01876 if ( $useLinkPrefixExtension ) { 01877 $m = array(); 01878 if ( preg_match( $e2, $s, $m ) ) { 01879 $first_prefix = $m[2]; 01880 } else { 01881 $first_prefix = false; 01882 } 01883 } else { 01884 $prefix = ''; 01885 } 01886 01887 $useSubpages = $this->areSubpagesAllowed(); 01888 wfProfileOut( __METHOD__ . '-setup' ); 01889 01890 # Loop for each link 01891 for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) { 01892 # Check for excessive memory usage 01893 if ( $holders->isBig() ) { 01894 # Too big 01895 # Do the existence check, replace the link holders and clear the array 01896 $holders->replace( $s ); 01897 $holders->clear(); 01898 } 01899 01900 if ( $useLinkPrefixExtension ) { 01901 wfProfileIn( __METHOD__ . '-prefixhandling' ); 01902 if ( preg_match( $e2, $s, $m ) ) { 01903 $prefix = $m[2]; 01904 $s = $m[1]; 01905 } else { 01906 $prefix = ''; 01907 } 01908 # first link 01909 if ( $first_prefix ) { 01910 $prefix = $first_prefix; 01911 $first_prefix = false; 01912 } 01913 wfProfileOut( __METHOD__ . '-prefixhandling' ); 01914 } 01915 01916 $might_be_img = false; 01917 01918 wfProfileIn( __METHOD__ . "-e1" ); 01919 if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt 01920 $text = $m[2]; 01921 # If we get a ] at the beginning of $m[3] that means we have a link that's something like: 01922 # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up, 01923 # the real problem is with the $e1 regex 01924 # See bug 1300. 01925 # 01926 # Still some problems for cases where the ] is meant to be outside punctuation, 01927 # and no image is in sight. See bug 2095. 01928 # 01929 if ( $text !== '' && 01930 substr( $m[3], 0, 1 ) === ']' && 01931 strpos( $text, '[' ) !== false 01932 ) 01933 { 01934 $text .= ']'; # so that replaceExternalLinks($text) works later 01935 $m[3] = substr( $m[3], 1 ); 01936 } 01937 # fix up urlencoded title texts 01938 if ( strpos( $m[1], '%' ) !== false ) { 01939 # Should anchors '#' also be rejected? 01940 $m[1] = str_replace( array( '<', '>' ), array( '<', '>' ), rawurldecode( $m[1] ) ); 01941 } 01942 $trail = $m[3]; 01943 } elseif ( preg_match( $e1_img, $line, $m ) ) { # Invalid, but might be an image with a link in its caption 01944 $might_be_img = true; 01945 $text = $m[2]; 01946 if ( strpos( $m[1], '%' ) !== false ) { 01947 $m[1] = rawurldecode( $m[1] ); 01948 } 01949 $trail = ""; 01950 } else { # Invalid form; output directly 01951 $s .= $prefix . '[[' . $line; 01952 wfProfileOut( __METHOD__ . "-e1" ); 01953 continue; 01954 } 01955 wfProfileOut( __METHOD__ . "-e1" ); 01956 wfProfileIn( __METHOD__ . "-misc" ); 01957 01958 # Don't allow internal links to pages containing 01959 # PROTO: where PROTO is a valid URL protocol; these 01960 # should be external links. 01961 if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $m[1] ) ) { 01962 $s .= $prefix . '[[' . $line; 01963 wfProfileOut( __METHOD__ . "-misc" ); 01964 continue; 01965 } 01966 01967 # Make subpage if necessary 01968 if ( $useSubpages ) { 01969 $link = $this->maybeDoSubpageLink( $m[1], $text ); 01970 } else { 01971 $link = $m[1]; 01972 } 01973 01974 $noforce = ( substr( $m[1], 0, 1 ) !== ':' ); 01975 if ( !$noforce ) { 01976 # Strip off leading ':' 01977 $link = substr( $link, 1 ); 01978 } 01979 01980 wfProfileOut( __METHOD__ . "-misc" ); 01981 wfProfileIn( __METHOD__ . "-title" ); 01982 $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) ); 01983 if ( $nt === null ) { 01984 $s .= $prefix . '[[' . $line; 01985 wfProfileOut( __METHOD__ . "-title" ); 01986 continue; 01987 } 01988 01989 $ns = $nt->getNamespace(); 01990 $iw = $nt->getInterWiki(); 01991 wfProfileOut( __METHOD__ . "-title" ); 01992 01993 if ( $might_be_img ) { # if this is actually an invalid link 01994 wfProfileIn( __METHOD__ . "-might_be_img" ); 01995 if ( $ns == NS_FILE && $noforce ) { # but might be an image 01996 $found = false; 01997 while ( true ) { 01998 # look at the next 'line' to see if we can close it there 01999 $a->next(); 02000 $next_line = $a->current(); 02001 if ( $next_line === false || $next_line === null ) { 02002 break; 02003 } 02004 $m = explode( ']]', $next_line, 3 ); 02005 if ( count( $m ) == 3 ) { 02006 # the first ]] closes the inner link, the second the image 02007 $found = true; 02008 $text .= "[[{$m[0]}]]{$m[1]}"; 02009 $trail = $m[2]; 02010 break; 02011 } elseif ( count( $m ) == 2 ) { 02012 # if there's exactly one ]] that's fine, we'll keep looking 02013 $text .= "[[{$m[0]}]]{$m[1]}"; 02014 } else { 02015 # if $next_line is invalid too, we need look no further 02016 $text .= '[[' . $next_line; 02017 break; 02018 } 02019 } 02020 if ( !$found ) { 02021 # we couldn't find the end of this imageLink, so output it raw 02022 # but don't ignore what might be perfectly normal links in the text we've examined 02023 $holders->merge( $this->replaceInternalLinks2( $text ) ); 02024 $s .= "{$prefix}[[$link|$text"; 02025 # note: no $trail, because without an end, there *is* no trail 02026 wfProfileOut( __METHOD__ . "-might_be_img" ); 02027 continue; 02028 } 02029 } else { # it's not an image, so output it raw 02030 $s .= "{$prefix}[[$link|$text"; 02031 # note: no $trail, because without an end, there *is* no trail 02032 wfProfileOut( __METHOD__ . "-might_be_img" ); 02033 continue; 02034 } 02035 wfProfileOut( __METHOD__ . "-might_be_img" ); 02036 } 02037 02038 $wasblank = ( $text == '' ); 02039 if ( $wasblank ) { 02040 $text = $link; 02041 } else { 02042 # Bug 4598 madness. Handle the quotes only if they come from the alternate part 02043 # [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a> 02044 # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']] 02045 # -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a> 02046 $text = $this->doQuotes( $text ); 02047 } 02048 02049 # Link not escaped by : , create the various objects 02050 if ( $noforce ) { 02051 # Interwikis 02052 wfProfileIn( __METHOD__ . "-interwiki" ); 02053 if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) { 02054 // XXX: the above check prevents links to sites with identifiers that are not language codes 02055 02056 # Bug 24502: filter duplicates 02057 if ( !isset( $this->mLangLinkLanguages[$iw] ) ) { 02058 $this->mLangLinkLanguages[$iw] = true; 02059 $this->mOutput->addLanguageLink( $nt->getFullText() ); 02060 } 02061 02062 $s = rtrim( $s . $prefix ); 02063 $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail; 02064 wfProfileOut( __METHOD__ . "-interwiki" ); 02065 continue; 02066 } 02067 wfProfileOut( __METHOD__ . "-interwiki" ); 02068 02069 if ( $ns == NS_FILE ) { 02070 wfProfileIn( __METHOD__ . "-image" ); 02071 if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { 02072 if ( $wasblank ) { 02073 # if no parameters were passed, $text 02074 # becomes something like "File:Foo.png", 02075 # which we don't want to pass on to the 02076 # image generator 02077 $text = ''; 02078 } else { 02079 # recursively parse links inside the image caption 02080 # actually, this will parse them in any other parameters, too, 02081 # but it might be hard to fix that, and it doesn't matter ATM 02082 $text = $this->replaceExternalLinks( $text ); 02083 $holders->merge( $this->replaceInternalLinks2( $text ) ); 02084 } 02085 # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them 02086 $s .= $prefix . $this->armorLinks( 02087 $this->makeImage( $nt, $text, $holders ) ) . $trail; 02088 } else { 02089 $s .= $prefix . $trail; 02090 } 02091 wfProfileOut( __METHOD__ . "-image" ); 02092 continue; 02093 } 02094 02095 if ( $ns == NS_CATEGORY ) { 02096 wfProfileIn( __METHOD__ . "-category" ); 02097 $s = rtrim( $s . "\n" ); # bug 87 02098 02099 if ( $wasblank ) { 02100 $sortkey = $this->getDefaultSort(); 02101 } else { 02102 $sortkey = $text; 02103 } 02104 $sortkey = Sanitizer::decodeCharReferences( $sortkey ); 02105 $sortkey = str_replace( "\n", '', $sortkey ); 02106 $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey ); 02107 $this->mOutput->addCategory( $nt->getDBkey(), $sortkey ); 02108 02113 $s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail; 02114 02115 wfProfileOut( __METHOD__ . "-category" ); 02116 continue; 02117 } 02118 } 02119 02120 # Self-link checking. For some languages, variants of the title are checked in 02121 # LinkHolderArray::doVariants() to allow batching the existence checks necessary 02122 # for linking to a different variant. 02123 if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && $nt->getFragment() === '' ) { 02124 $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail ); 02125 continue; 02126 } 02127 02128 # NS_MEDIA is a pseudo-namespace for linking directly to a file 02129 # @todo FIXME: Should do batch file existence checks, see comment below 02130 if ( $ns == NS_MEDIA ) { 02131 wfProfileIn( __METHOD__ . "-media" ); 02132 # Give extensions a chance to select the file revision for us 02133 $options = array(); 02134 $descQuery = false; 02135 wfRunHooks( 'BeforeParserFetchFileAndTitle', 02136 array( $this, $nt, &$options, &$descQuery ) ); 02137 # Fetch and register the file (file title may be different via hooks) 02138 list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options ); 02139 # Cloak with NOPARSE to avoid replacement in replaceExternalLinks 02140 $s .= $prefix . $this->armorLinks( 02141 Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail; 02142 wfProfileOut( __METHOD__ . "-media" ); 02143 continue; 02144 } 02145 02146 wfProfileIn( __METHOD__ . "-always_known" ); 02147 # Some titles, such as valid special pages or files in foreign repos, should 02148 # be shown as bluelinks even though they're not included in the page table 02149 # 02150 # @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do 02151 # batch file existence checks for NS_FILE and NS_MEDIA 02152 if ( $iw == '' && $nt->isAlwaysKnown() ) { 02153 $this->mOutput->addLink( $nt ); 02154 $s .= $this->makeKnownLinkHolder( $nt, $text, array(), $trail, $prefix ); 02155 } else { 02156 # Links will be added to the output link list after checking 02157 $s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix ); 02158 } 02159 wfProfileOut( __METHOD__ . "-always_known" ); 02160 } 02161 wfProfileOut( __METHOD__ ); 02162 return $holders; 02163 } 02164 02179 function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) { 02180 list( $inside, $trail ) = Linker::splitTrail( $trail ); 02181 02182 if ( is_string( $query ) ) { 02183 $query = wfCgiToArray( $query ); 02184 } 02185 if ( $text == '' ) { 02186 $text = htmlspecialchars( $nt->getPrefixedText() ); 02187 } 02188 02189 $link = Linker::linkKnown( $nt, "$prefix$text$inside", array(), $query ); 02190 02191 return $this->armorLinks( $link ) . $trail; 02192 } 02193 02204 function armorLinks( $text ) { 02205 return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/', 02206 "{$this->mUniqPrefix}NOPARSE$1", $text ); 02207 } 02208 02213 function areSubpagesAllowed() { 02214 # Some namespaces don't allow subpages 02215 return MWNamespace::hasSubpages( $this->mTitle->getNamespace() ); 02216 } 02217 02226 function maybeDoSubpageLink( $target, &$text ) { 02227 return Linker::normalizeSubpageLink( $this->mTitle, $target, $text ); 02228 } 02229 02236 function closeParagraph() { 02237 $result = ''; 02238 if ( $this->mLastSection != '' ) { 02239 $result = '</' . $this->mLastSection . ">\n"; 02240 } 02241 $this->mInPre = false; 02242 $this->mLastSection = ''; 02243 return $result; 02244 } 02245 02256 function getCommon( $st1, $st2 ) { 02257 $fl = strlen( $st1 ); 02258 $shorter = strlen( $st2 ); 02259 if ( $fl < $shorter ) { 02260 $shorter = $fl; 02261 } 02262 02263 for ( $i = 0; $i < $shorter; ++$i ) { 02264 if ( $st1[$i] != $st2[$i] ) { 02265 break; 02266 } 02267 } 02268 return $i; 02269 } 02270 02280 function openList( $char ) { 02281 $result = $this->closeParagraph(); 02282 02283 if ( '*' === $char ) { 02284 $result .= "<ul>\n<li>"; 02285 } elseif ( '#' === $char ) { 02286 $result .= "<ol>\n<li>"; 02287 } elseif ( ':' === $char ) { 02288 $result .= "<dl>\n<dd>"; 02289 } elseif ( ';' === $char ) { 02290 $result .= "<dl>\n<dt>"; 02291 $this->mDTopen = true; 02292 } else { 02293 $result = '<!-- ERR 1 -->'; 02294 } 02295 02296 return $result; 02297 } 02298 02306 function nextItem( $char ) { 02307 if ( '*' === $char || '#' === $char ) { 02308 return "</li>\n<li>"; 02309 } elseif ( ':' === $char || ';' === $char ) { 02310 $close = "</dd>\n"; 02311 if ( $this->mDTopen ) { 02312 $close = "</dt>\n"; 02313 } 02314 if ( ';' === $char ) { 02315 $this->mDTopen = true; 02316 return $close . '<dt>'; 02317 } else { 02318 $this->mDTopen = false; 02319 return $close . '<dd>'; 02320 } 02321 } 02322 return '<!-- ERR 2 -->'; 02323 } 02324 02332 function closeList( $char ) { 02333 if ( '*' === $char ) { 02334 $text = "</li>\n</ul>"; 02335 } elseif ( '#' === $char ) { 02336 $text = "</li>\n</ol>"; 02337 } elseif ( ':' === $char ) { 02338 if ( $this->mDTopen ) { 02339 $this->mDTopen = false; 02340 $text = "</dt>\n</dl>"; 02341 } else { 02342 $text = "</dd>\n</dl>"; 02343 } 02344 } else { 02345 return '<!-- ERR 3 -->'; 02346 } 02347 return $text . "\n"; 02348 } 02359 function doBlockLevels( $text, $linestart ) { 02360 wfProfileIn( __METHOD__ ); 02361 02362 # Parsing through the text line by line. The main thing 02363 # happening here is handling of block-level elements p, pre, 02364 # and making lists from lines starting with * # : etc. 02365 # 02366 $textLines = StringUtils::explode( "\n", $text ); 02367 02368 $lastPrefix = $output = ''; 02369 $this->mDTopen = $inBlockElem = false; 02370 $prefixLength = 0; 02371 $paragraphStack = false; 02372 $inBlockquote = false; 02373 02374 foreach ( $textLines as $oLine ) { 02375 # Fix up $linestart 02376 if ( !$linestart ) { 02377 $output .= $oLine; 02378 $linestart = true; 02379 continue; 02380 } 02381 # * = ul 02382 # # = ol 02383 # ; = dt 02384 # : = dd 02385 02386 $lastPrefixLength = strlen( $lastPrefix ); 02387 $preCloseMatch = preg_match( '/<\\/pre/i', $oLine ); 02388 $preOpenMatch = preg_match( '/<pre/i', $oLine ); 02389 # If not in a <pre> element, scan for and figure out what prefixes are there. 02390 if ( !$this->mInPre ) { 02391 # Multiple prefixes may abut each other for nested lists. 02392 $prefixLength = strspn( $oLine, '*#:;' ); 02393 $prefix = substr( $oLine, 0, $prefixLength ); 02394 02395 # eh? 02396 # ; and : are both from definition-lists, so they're equivalent 02397 # for the purposes of determining whether or not we need to open/close 02398 # elements. 02399 $prefix2 = str_replace( ';', ':', $prefix ); 02400 $t = substr( $oLine, $prefixLength ); 02401 $this->mInPre = (bool)$preOpenMatch; 02402 } else { 02403 # Don't interpret any other prefixes in preformatted text 02404 $prefixLength = 0; 02405 $prefix = $prefix2 = ''; 02406 $t = $oLine; 02407 } 02408 02409 # List generation 02410 if ( $prefixLength && $lastPrefix === $prefix2 ) { 02411 # Same as the last item, so no need to deal with nesting or opening stuff 02412 $output .= $this->nextItem( substr( $prefix, -1 ) ); 02413 $paragraphStack = false; 02414 02415 if ( substr( $prefix, -1 ) === ';' ) { 02416 # The one nasty exception: definition lists work like this: 02417 # ; title : definition text 02418 # So we check for : in the remainder text to split up the 02419 # title and definition, without b0rking links. 02420 $term = $t2 = ''; 02421 if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) { 02422 $t = $t2; 02423 $output .= $term . $this->nextItem( ':' ); 02424 } 02425 } 02426 } elseif ( $prefixLength || $lastPrefixLength ) { 02427 # We need to open or close prefixes, or both. 02428 02429 # Either open or close a level... 02430 $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix ); 02431 $paragraphStack = false; 02432 02433 # Close all the prefixes which aren't shared. 02434 while ( $commonPrefixLength < $lastPrefixLength ) { 02435 $output .= $this->closeList( $lastPrefix[$lastPrefixLength - 1] ); 02436 --$lastPrefixLength; 02437 } 02438 02439 # Continue the current prefix if appropriate. 02440 if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) { 02441 $output .= $this->nextItem( $prefix[$commonPrefixLength - 1] ); 02442 } 02443 02444 # Open prefixes where appropriate. 02445 while ( $prefixLength > $commonPrefixLength ) { 02446 $char = substr( $prefix, $commonPrefixLength, 1 ); 02447 $output .= $this->openList( $char ); 02448 02449 if ( ';' === $char ) { 02450 # @todo FIXME: This is dupe of code above 02451 if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) { 02452 $t = $t2; 02453 $output .= $term . $this->nextItem( ':' ); 02454 } 02455 } 02456 ++$commonPrefixLength; 02457 } 02458 $lastPrefix = $prefix2; 02459 } 02460 02461 # If we have no prefixes, go to paragraph mode. 02462 if ( 0 == $prefixLength ) { 02463 wfProfileIn( __METHOD__ . "-paragraph" ); 02464 # No prefix (not in list)--go to paragraph mode 02465 # XXX: use a stack for nestable elements like span, table and div 02466 $openmatch = preg_match( '/(?:<table|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<dl|<li|<\\/tr|<\\/td|<\\/th)/iS', $t ); 02467 $closematch = preg_match( 02468 '/(?:<\\/table|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|' . 02469 '<td|<th|<\\/?blockquote|<\\/?div|<hr|<\\/pre|<\\/p|<\\/mw:|' . $this->mUniqPrefix . '-pre|<\\/li|<\\/ul|<\\/ol|<\\/dl|<\\/?center)/iS', $t ); 02470 if ( $openmatch or $closematch ) { 02471 $paragraphStack = false; 02472 # TODO bug 5718: paragraph closed 02473 $output .= $this->closeParagraph(); 02474 if ( $preOpenMatch and !$preCloseMatch ) { 02475 $this->mInPre = true; 02476 } 02477 $bqOffset = 0; 02478 while ( preg_match( '/<(\\/?)blockquote[\s>]/i', $t, $bqMatch, PREG_OFFSET_CAPTURE, $bqOffset ) ) { 02479 $inBlockquote = !$bqMatch[1][0]; // is this a close tag? 02480 $bqOffset = $bqMatch[0][1] + strlen( $bqMatch[0][0] ); 02481 } 02482 $inBlockElem = !$closematch; 02483 } elseif ( !$inBlockElem && !$this->mInPre ) { 02484 if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) and !$inBlockquote ) { 02485 # pre 02486 if ( $this->mLastSection !== 'pre' ) { 02487 $paragraphStack = false; 02488 $output .= $this->closeParagraph() . '<pre>'; 02489 $this->mLastSection = 'pre'; 02490 } 02491 $t = substr( $t, 1 ); 02492 } else { 02493 # paragraph 02494 if ( trim( $t ) === '' ) { 02495 if ( $paragraphStack ) { 02496 $output .= $paragraphStack . '<br />'; 02497 $paragraphStack = false; 02498 $this->mLastSection = 'p'; 02499 } else { 02500 if ( $this->mLastSection !== 'p' ) { 02501 $output .= $this->closeParagraph(); 02502 $this->mLastSection = ''; 02503 $paragraphStack = '<p>'; 02504 } else { 02505 $paragraphStack = '</p><p>'; 02506 } 02507 } 02508 } else { 02509 if ( $paragraphStack ) { 02510 $output .= $paragraphStack; 02511 $paragraphStack = false; 02512 $this->mLastSection = 'p'; 02513 } elseif ( $this->mLastSection !== 'p' ) { 02514 $output .= $this->closeParagraph() . '<p>'; 02515 $this->mLastSection = 'p'; 02516 } 02517 } 02518 } 02519 } 02520 wfProfileOut( __METHOD__ . "-paragraph" ); 02521 } 02522 # somewhere above we forget to get out of pre block (bug 785) 02523 if ( $preCloseMatch && $this->mInPre ) { 02524 $this->mInPre = false; 02525 } 02526 if ( $paragraphStack === false ) { 02527 $output .= $t . "\n"; 02528 } 02529 } 02530 while ( $prefixLength ) { 02531 $output .= $this->closeList( $prefix2[$prefixLength - 1] ); 02532 --$prefixLength; 02533 } 02534 if ( $this->mLastSection != '' ) { 02535 $output .= '</' . $this->mLastSection . '>'; 02536 $this->mLastSection = ''; 02537 } 02538 02539 wfProfileOut( __METHOD__ ); 02540 return $output; 02541 } 02542 02553 function findColonNoLinks( $str, &$before, &$after ) { 02554 wfProfileIn( __METHOD__ ); 02555 02556 $pos = strpos( $str, ':' ); 02557 if ( $pos === false ) { 02558 # Nothing to find! 02559 wfProfileOut( __METHOD__ ); 02560 return false; 02561 } 02562 02563 $lt = strpos( $str, '<' ); 02564 if ( $lt === false || $lt > $pos ) { 02565 # Easy; no tag nesting to worry about 02566 $before = substr( $str, 0, $pos ); 02567 $after = substr( $str, $pos + 1 ); 02568 wfProfileOut( __METHOD__ ); 02569 return $pos; 02570 } 02571 02572 # Ugly state machine to walk through avoiding tags. 02573 $state = self::COLON_STATE_TEXT; 02574 $stack = 0; 02575 $len = strlen( $str ); 02576 for ( $i = 0; $i < $len; $i++ ) { 02577 $c = $str[$i]; 02578 02579 switch ( $state ) { 02580 # (Using the number is a performance hack for common cases) 02581 case 0: # self::COLON_STATE_TEXT: 02582 switch ( $c ) { 02583 case "<": 02584 # Could be either a <start> tag or an </end> tag 02585 $state = self::COLON_STATE_TAGSTART; 02586 break; 02587 case ":": 02588 if ( $stack == 0 ) { 02589 # We found it! 02590 $before = substr( $str, 0, $i ); 02591 $after = substr( $str, $i + 1 ); 02592 wfProfileOut( __METHOD__ ); 02593 return $i; 02594 } 02595 # Embedded in a tag; don't break it. 02596 break; 02597 default: 02598 # Skip ahead looking for something interesting 02599 $colon = strpos( $str, ':', $i ); 02600 if ( $colon === false ) { 02601 # Nothing else interesting 02602 wfProfileOut( __METHOD__ ); 02603 return false; 02604 } 02605 $lt = strpos( $str, '<', $i ); 02606 if ( $stack === 0 ) { 02607 if ( $lt === false || $colon < $lt ) { 02608 # We found it! 02609 $before = substr( $str, 0, $colon ); 02610 $after = substr( $str, $colon + 1 ); 02611 wfProfileOut( __METHOD__ ); 02612 return $i; 02613 } 02614 } 02615 if ( $lt === false ) { 02616 # Nothing else interesting to find; abort! 02617 # We're nested, but there's no close tags left. Abort! 02618 break 2; 02619 } 02620 # Skip ahead to next tag start 02621 $i = $lt; 02622 $state = self::COLON_STATE_TAGSTART; 02623 } 02624 break; 02625 case 1: # self::COLON_STATE_TAG: 02626 # In a <tag> 02627 switch ( $c ) { 02628 case ">": 02629 $stack++; 02630 $state = self::COLON_STATE_TEXT; 02631 break; 02632 case "/": 02633 # Slash may be followed by >? 02634 $state = self::COLON_STATE_TAGSLASH; 02635 break; 02636 default: 02637 # ignore 02638 } 02639 break; 02640 case 2: # self::COLON_STATE_TAGSTART: 02641 switch ( $c ) { 02642 case "/": 02643 $state = self::COLON_STATE_CLOSETAG; 02644 break; 02645 case "!": 02646 $state = self::COLON_STATE_COMMENT; 02647 break; 02648 case ">": 02649 # Illegal early close? This shouldn't happen D: 02650 $state = self::COLON_STATE_TEXT; 02651 break; 02652 default: 02653 $state = self::COLON_STATE_TAG; 02654 } 02655 break; 02656 case 3: # self::COLON_STATE_CLOSETAG: 02657 # In a </tag> 02658 if ( $c === ">" ) { 02659 $stack--; 02660 if ( $stack < 0 ) { 02661 wfDebug( __METHOD__ . ": Invalid input; too many close tags\n" ); 02662 wfProfileOut( __METHOD__ ); 02663 return false; 02664 } 02665 $state = self::COLON_STATE_TEXT; 02666 } 02667 break; 02668 case self::COLON_STATE_TAGSLASH: 02669 if ( $c === ">" ) { 02670 # Yes, a self-closed tag <blah/> 02671 $state = self::COLON_STATE_TEXT; 02672 } else { 02673 # Probably we're jumping the gun, and this is an attribute 02674 $state = self::COLON_STATE_TAG; 02675 } 02676 break; 02677 case 5: # self::COLON_STATE_COMMENT: 02678 if ( $c === "-" ) { 02679 $state = self::COLON_STATE_COMMENTDASH; 02680 } 02681 break; 02682 case self::COLON_STATE_COMMENTDASH: 02683 if ( $c === "-" ) { 02684 $state = self::COLON_STATE_COMMENTDASHDASH; 02685 } else { 02686 $state = self::COLON_STATE_COMMENT; 02687 } 02688 break; 02689 case self::COLON_STATE_COMMENTDASHDASH: 02690 if ( $c === ">" ) { 02691 $state = self::COLON_STATE_TEXT; 02692 } else { 02693 $state = self::COLON_STATE_COMMENT; 02694 } 02695 break; 02696 default: 02697 wfProfileOut( __METHOD__ ); 02698 throw new MWException( "State machine error in " . __METHOD__ ); 02699 } 02700 } 02701 if ( $stack > 0 ) { 02702 wfDebug( __METHOD__ . ": Invalid input; not enough close tags (stack $stack, state $state)\n" ); 02703 wfProfileOut( __METHOD__ ); 02704 return false; 02705 } 02706 wfProfileOut( __METHOD__ ); 02707 return false; 02708 } 02709 02721 function getVariableValue( $index, $frame = false ) { 02722 global $wgContLang, $wgSitename, $wgServer; 02723 global $wgArticlePath, $wgScriptPath, $wgStylePath; 02724 02725 if ( is_null( $this->mTitle ) ) { 02726 // If no title set, bad things are going to happen 02727 // later. Title should always be set since this 02728 // should only be called in the middle of a parse 02729 // operation (but the unit-tests do funky stuff) 02730 throw new MWException( __METHOD__ . ' Should only be ' 02731 . ' called while parsing (no title set)' ); 02732 } 02733 02738 if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) { 02739 if ( isset( $this->mVarCache[$index] ) ) { 02740 return $this->mVarCache[$index]; 02741 } 02742 } 02743 02744 $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() ); 02745 wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) ); 02746 02747 $pageLang = $this->getFunctionLang(); 02748 02749 switch ( $index ) { 02750 case 'currentmonth': 02751 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) ); 02752 break; 02753 case 'currentmonth1': 02754 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) ); 02755 break; 02756 case 'currentmonthname': 02757 $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) ); 02758 break; 02759 case 'currentmonthnamegen': 02760 $value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) ); 02761 break; 02762 case 'currentmonthabbrev': 02763 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) ); 02764 break; 02765 case 'currentday': 02766 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) ); 02767 break; 02768 case 'currentday2': 02769 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) ); 02770 break; 02771 case 'localmonth': 02772 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) ); 02773 break; 02774 case 'localmonth1': 02775 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); 02776 break; 02777 case 'localmonthname': 02778 $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); 02779 break; 02780 case 'localmonthnamegen': 02781 $value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); 02782 break; 02783 case 'localmonthabbrev': 02784 $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); 02785 break; 02786 case 'localday': 02787 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) ); 02788 break; 02789 case 'localday2': 02790 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) ); 02791 break; 02792 case 'pagename': 02793 $value = wfEscapeWikiText( $this->mTitle->getText() ); 02794 break; 02795 case 'pagenamee': 02796 $value = wfEscapeWikiText( $this->mTitle->getPartialURL() ); 02797 break; 02798 case 'fullpagename': 02799 $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); 02800 break; 02801 case 'fullpagenamee': 02802 $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() ); 02803 break; 02804 case 'subpagename': 02805 $value = wfEscapeWikiText( $this->mTitle->getSubpageText() ); 02806 break; 02807 case 'subpagenamee': 02808 $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() ); 02809 break; 02810 case 'rootpagename': 02811 $value = wfEscapeWikiText( $this->mTitle->getRootText() ); 02812 break; 02813 case 'rootpagenamee': 02814 $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getRootText() ) ) ); 02815 break; 02816 case 'basepagename': 02817 $value = wfEscapeWikiText( $this->mTitle->getBaseText() ); 02818 break; 02819 case 'basepagenamee': 02820 $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ) ); 02821 break; 02822 case 'talkpagename': 02823 if ( $this->mTitle->canTalk() ) { 02824 $talkPage = $this->mTitle->getTalkPage(); 02825 $value = wfEscapeWikiText( $talkPage->getPrefixedText() ); 02826 } else { 02827 $value = ''; 02828 } 02829 break; 02830 case 'talkpagenamee': 02831 if ( $this->mTitle->canTalk() ) { 02832 $talkPage = $this->mTitle->getTalkPage(); 02833 $value = wfEscapeWikiText( $talkPage->getPrefixedURL() ); 02834 } else { 02835 $value = ''; 02836 } 02837 break; 02838 case 'subjectpagename': 02839 $subjPage = $this->mTitle->getSubjectPage(); 02840 $value = wfEscapeWikiText( $subjPage->getPrefixedText() ); 02841 break; 02842 case 'subjectpagenamee': 02843 $subjPage = $this->mTitle->getSubjectPage(); 02844 $value = wfEscapeWikiText( $subjPage->getPrefixedURL() ); 02845 break; 02846 case 'pageid': // requested in bug 23427 02847 $pageid = $this->getTitle()->getArticleID(); 02848 if ( $pageid == 0 ) { 02849 # 0 means the page doesn't exist in the database, 02850 # which means the user is previewing a new page. 02851 # The vary-revision flag must be set, because the magic word 02852 # will have a different value once the page is saved. 02853 $this->mOutput->setFlag( 'vary-revision' ); 02854 wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" ); 02855 } 02856 $value = $pageid ? $pageid : null; 02857 break; 02858 case 'revisionid': 02859 # Let the edit saving system know we should parse the page 02860 # *after* a revision ID has been assigned. 02861 $this->mOutput->setFlag( 'vary-revision' ); 02862 wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" ); 02863 $value = $this->mRevisionId; 02864 break; 02865 case 'revisionday': 02866 # Let the edit saving system know we should parse the page 02867 # *after* a revision ID has been assigned. This is for null edits. 02868 $this->mOutput->setFlag( 'vary-revision' ); 02869 wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" ); 02870 $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); 02871 break; 02872 case 'revisionday2': 02873 # Let the edit saving system know we should parse the page 02874 # *after* a revision ID has been assigned. This is for null edits. 02875 $this->mOutput->setFlag( 'vary-revision' ); 02876 wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" ); 02877 $value = substr( $this->getRevisionTimestamp(), 6, 2 ); 02878 break; 02879 case 'revisionmonth': 02880 # Let the edit saving system know we should parse the page 02881 # *after* a revision ID has been assigned. This is for null edits. 02882 $this->mOutput->setFlag( 'vary-revision' ); 02883 wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" ); 02884 $value = substr( $this->getRevisionTimestamp(), 4, 2 ); 02885 break; 02886 case 'revisionmonth1': 02887 # Let the edit saving system know we should parse the page 02888 # *after* a revision ID has been assigned. This is for null edits. 02889 $this->mOutput->setFlag( 'vary-revision' ); 02890 wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" ); 02891 $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); 02892 break; 02893 case 'revisionyear': 02894 # Let the edit saving system know we should parse the page 02895 # *after* a revision ID has been assigned. This is for null edits. 02896 $this->mOutput->setFlag( 'vary-revision' ); 02897 wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" ); 02898 $value = substr( $this->getRevisionTimestamp(), 0, 4 ); 02899 break; 02900 case 'revisiontimestamp': 02901 # Let the edit saving system know we should parse the page 02902 # *after* a revision ID has been assigned. This is for null edits. 02903 $this->mOutput->setFlag( 'vary-revision' ); 02904 wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" ); 02905 $value = $this->getRevisionTimestamp(); 02906 break; 02907 case 'revisionuser': 02908 # Let the edit saving system know we should parse the page 02909 # *after* a revision ID has been assigned. This is for null edits. 02910 $this->mOutput->setFlag( 'vary-revision' ); 02911 wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" ); 02912 $value = $this->getRevisionUser(); 02913 break; 02914 case 'revisionsize': 02915 # Let the edit saving system know we should parse the page 02916 # *after* a revision ID has been assigned. This is for null edits. 02917 $this->mOutput->setFlag( 'vary-revision' ); 02918 wfDebug( __METHOD__ . ": {{REVISIONSIZE}} used, setting vary-revision...\n" ); 02919 $value = $this->getRevisionSize(); 02920 break; 02921 case 'namespace': 02922 $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); 02923 break; 02924 case 'namespacee': 02925 $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); 02926 break; 02927 case 'namespacenumber': 02928 $value = $this->mTitle->getNamespace(); 02929 break; 02930 case 'talkspace': 02931 $value = $this->mTitle->canTalk() ? str_replace( '_', ' ', $this->mTitle->getTalkNsText() ) : ''; 02932 break; 02933 case 'talkspacee': 02934 $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; 02935 break; 02936 case 'subjectspace': 02937 $value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() ); 02938 break; 02939 case 'subjectspacee': 02940 $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); 02941 break; 02942 case 'currentdayname': 02943 $value = $pageLang->getWeekdayName( MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 ); 02944 break; 02945 case 'currentyear': 02946 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true ); 02947 break; 02948 case 'currenttime': 02949 $value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false ); 02950 break; 02951 case 'currenthour': 02952 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true ); 02953 break; 02954 case 'currentweek': 02955 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to 02956 # int to remove the padding 02957 $value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) ); 02958 break; 02959 case 'currentdow': 02960 $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) ); 02961 break; 02962 case 'localdayname': 02963 $value = $pageLang->getWeekdayName( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1 ); 02964 break; 02965 case 'localyear': 02966 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true ); 02967 break; 02968 case 'localtime': 02969 $value = $pageLang->time( MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ), false, false ); 02970 break; 02971 case 'localhour': 02972 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true ); 02973 break; 02974 case 'localweek': 02975 # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to 02976 # int to remove the padding 02977 $value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) ); 02978 break; 02979 case 'localdow': 02980 $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) ); 02981 break; 02982 case 'numberofarticles': 02983 $value = $pageLang->formatNum( SiteStats::articles() ); 02984 break; 02985 case 'numberoffiles': 02986 $value = $pageLang->formatNum( SiteStats::images() ); 02987 break; 02988 case 'numberofusers': 02989 $value = $pageLang->formatNum( SiteStats::users() ); 02990 break; 02991 case 'numberofactiveusers': 02992 $value = $pageLang->formatNum( SiteStats::activeUsers() ); 02993 break; 02994 case 'numberofpages': 02995 $value = $pageLang->formatNum( SiteStats::pages() ); 02996 break; 02997 case 'numberofadmins': 02998 $value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) ); 02999 break; 03000 case 'numberofedits': 03001 $value = $pageLang->formatNum( SiteStats::edits() ); 03002 break; 03003 case 'numberofviews': 03004 global $wgDisableCounters; 03005 $value = !$wgDisableCounters ? $pageLang->formatNum( SiteStats::views() ) : ''; 03006 break; 03007 case 'currenttimestamp': 03008 $value = wfTimestamp( TS_MW, $ts ); 03009 break; 03010 case 'localtimestamp': 03011 $value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ); 03012 break; 03013 case 'currentversion': 03014 $value = SpecialVersion::getVersion(); 03015 break; 03016 case 'articlepath': 03017 return $wgArticlePath; 03018 case 'sitename': 03019 return $wgSitename; 03020 case 'server': 03021 return $wgServer; 03022 case 'servername': 03023 $serverParts = wfParseUrl( $wgServer ); 03024 return $serverParts && isset( $serverParts['host'] ) ? $serverParts['host'] : $wgServer; 03025 case 'scriptpath': 03026 return $wgScriptPath; 03027 case 'stylepath': 03028 return $wgStylePath; 03029 case 'directionmark': 03030 return $pageLang->getDirMark(); 03031 case 'contentlanguage': 03032 global $wgLanguageCode; 03033 return $wgLanguageCode; 03034 default: 03035 $ret = null; 03036 if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) ) ) { 03037 return $ret; 03038 } else { 03039 return null; 03040 } 03041 } 03042 03043 if ( $index ) { 03044 $this->mVarCache[$index] = $value; 03045 } 03046 03047 return $value; 03048 } 03049 03055 function initialiseVariables() { 03056 wfProfileIn( __METHOD__ ); 03057 $variableIDs = MagicWord::getVariableIDs(); 03058 $substIDs = MagicWord::getSubstIDs(); 03059 03060 $this->mVariables = new MagicWordArray( $variableIDs ); 03061 $this->mSubstWords = new MagicWordArray( $substIDs ); 03062 wfProfileOut( __METHOD__ ); 03063 } 03064 03089 function preprocessToDom( $text, $flags = 0 ) { 03090 $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags ); 03091 return $dom; 03092 } 03093 03101 public static function splitWhitespace( $s ) { 03102 $ltrimmed = ltrim( $s ); 03103 $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) ); 03104 $trimmed = rtrim( $ltrimmed ); 03105 $diff = strlen( $ltrimmed ) - strlen( $trimmed ); 03106 if ( $diff > 0 ) { 03107 $w2 = substr( $ltrimmed, -$diff ); 03108 } else { 03109 $w2 = ''; 03110 } 03111 return array( $w1, $trimmed, $w2 ); 03112 } 03113 03133 function replaceVariables( $text, $frame = false, $argsOnly = false ) { 03134 # Is there any text? Also, Prevent too big inclusions! 03135 if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) { 03136 return $text; 03137 } 03138 wfProfileIn( __METHOD__ ); 03139 03140 if ( $frame === false ) { 03141 $frame = $this->getPreprocessor()->newFrame(); 03142 } elseif ( !( $frame instanceof PPFrame ) ) { 03143 wfDebug( __METHOD__ . " called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" ); 03144 $frame = $this->getPreprocessor()->newCustomFrame( $frame ); 03145 } 03146 03147 $dom = $this->preprocessToDom( $text ); 03148 $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0; 03149 $text = $frame->expand( $dom, $flags ); 03150 03151 wfProfileOut( __METHOD__ ); 03152 return $text; 03153 } 03154 03162 static function createAssocArgs( $args ) { 03163 $assocArgs = array(); 03164 $index = 1; 03165 foreach ( $args as $arg ) { 03166 $eqpos = strpos( $arg, '=' ); 03167 if ( $eqpos === false ) { 03168 $assocArgs[$index++] = $arg; 03169 } else { 03170 $name = trim( substr( $arg, 0, $eqpos ) ); 03171 $value = trim( substr( $arg, $eqpos + 1 ) ); 03172 if ( $value === false ) { 03173 $value = ''; 03174 } 03175 if ( $name !== false ) { 03176 $assocArgs[$name] = $value; 03177 } 03178 } 03179 } 03180 03181 return $assocArgs; 03182 } 03183 03208 function limitationWarn( $limitationType, $current = '', $max = '' ) { 03209 # does no harm if $current and $max are present but are unnecessary for the message 03210 $warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max ) 03211 ->inLanguage( $this->mOptions->getUserLangObj() )->text(); 03212 $this->mOutput->addWarning( $warning ); 03213 $this->addTrackingCategory( "$limitationType-category" ); 03214 } 03215 03229 function braceSubstitution( $piece, $frame ) { 03230 wfProfileIn( __METHOD__ ); 03231 wfProfileIn( __METHOD__ . '-setup' ); 03232 03233 # Flags 03234 $found = false; # $text has been filled 03235 $nowiki = false; # wiki markup in $text should be escaped 03236 $isHTML = false; # $text is HTML, armour it against wikitext transformation 03237 $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered 03238 $isChildObj = false; # $text is a DOM node needing expansion in a child frame 03239 $isLocalObj = false; # $text is a DOM node needing expansion in the current frame 03240 03241 # Title object, where $text came from 03242 $title = false; 03243 03244 # $part1 is the bit before the first |, and must contain only title characters. 03245 # Various prefixes will be stripped from it later. 03246 $titleWithSpaces = $frame->expand( $piece['title'] ); 03247 $part1 = trim( $titleWithSpaces ); 03248 $titleText = false; 03249 03250 # Original title text preserved for various purposes 03251 $originalTitle = $part1; 03252 03253 # $args is a list of argument nodes, starting from index 0, not including $part1 03254 # @todo FIXME: If piece['parts'] is null then the call to getLength() below won't work b/c this $args isn't an object 03255 $args = ( null == $piece['parts'] ) ? array() : $piece['parts']; 03256 wfProfileOut( __METHOD__ . '-setup' ); 03257 03258 $titleProfileIn = null; // profile templates 03259 03260 # SUBST 03261 wfProfileIn( __METHOD__ . '-modifiers' ); 03262 if ( !$found ) { 03263 03264 $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 ); 03265 03266 # Possibilities for substMatch: "subst", "safesubst" or FALSE 03267 # Decide whether to expand template or keep wikitext as-is. 03268 if ( $this->ot['wiki'] ) { 03269 if ( $substMatch === false ) { 03270 $literal = true; # literal when in PST with no prefix 03271 } else { 03272 $literal = false; # expand when in PST with subst: or safesubst: 03273 } 03274 } else { 03275 if ( $substMatch == 'subst' ) { 03276 $literal = true; # literal when not in PST with plain subst: 03277 } else { 03278 $literal = false; # expand when not in PST with safesubst: or no prefix 03279 } 03280 } 03281 if ( $literal ) { 03282 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); 03283 $isLocalObj = true; 03284 $found = true; 03285 } 03286 } 03287 03288 # Variables 03289 if ( !$found && $args->getLength() == 0 ) { 03290 $id = $this->mVariables->matchStartToEnd( $part1 ); 03291 if ( $id !== false ) { 03292 $text = $this->getVariableValue( $id, $frame ); 03293 if ( MagicWord::getCacheTTL( $id ) > -1 ) { 03294 $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) ); 03295 } 03296 $found = true; 03297 } 03298 } 03299 03300 # MSG, MSGNW and RAW 03301 if ( !$found ) { 03302 # Check for MSGNW: 03303 $mwMsgnw = MagicWord::get( 'msgnw' ); 03304 if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { 03305 $nowiki = true; 03306 } else { 03307 # Remove obsolete MSG: 03308 $mwMsg = MagicWord::get( 'msg' ); 03309 $mwMsg->matchStartAndRemove( $part1 ); 03310 } 03311 03312 # Check for RAW: 03313 $mwRaw = MagicWord::get( 'raw' ); 03314 if ( $mwRaw->matchStartAndRemove( $part1 ) ) { 03315 $forceRawInterwiki = true; 03316 } 03317 } 03318 wfProfileOut( __METHOD__ . '-modifiers' ); 03319 03320 # Parser functions 03321 if ( !$found ) { 03322 wfProfileIn( __METHOD__ . '-pfunc' ); 03323 03324 $colonPos = strpos( $part1, ':' ); 03325 if ( $colonPos !== false ) { 03326 $func = substr( $part1, 0, $colonPos ); 03327 $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) ); 03328 for ( $i = 0; $i < $args->getLength(); $i++ ) { 03329 $funcArgs[] = $args->item( $i ); 03330 } 03331 try { 03332 $result = $this->callParserFunction( $frame, $func, $funcArgs ); 03333 } catch ( Exception $ex ) { 03334 wfProfileOut( __METHOD__ . '-pfunc' ); 03335 wfProfileOut( __METHOD__ ); 03336 throw $ex; 03337 } 03338 03339 # The interface for parser functions allows for extracting 03340 # flags into the local scope. Extract any forwarded flags 03341 # here. 03342 extract( $result ); 03343 } 03344 wfProfileOut( __METHOD__ . '-pfunc' ); 03345 } 03346 03347 # Finish mangling title and then check for loops. 03348 # Set $title to a Title object and $titleText to the PDBK 03349 if ( !$found ) { 03350 $ns = NS_TEMPLATE; 03351 # Split the title into page and subpage 03352 $subpage = ''; 03353 $relative = $this->maybeDoSubpageLink( $part1, $subpage ); 03354 if ( $part1 !== $relative ) { 03355 $part1 = $relative; 03356 $ns = $this->mTitle->getNamespace(); 03357 } 03358 $title = Title::newFromText( $part1, $ns ); 03359 if ( $title ) { 03360 $titleText = $title->getPrefixedText(); 03361 # Check for language variants if the template is not found 03362 if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) { 03363 $this->getConverterLanguage()->findVariantLink( $part1, $title, true ); 03364 } 03365 # Do recursion depth check 03366 $limit = $this->mOptions->getMaxTemplateDepth(); 03367 if ( $frame->depth >= $limit ) { 03368 $found = true; 03369 $text = '<span class="error">' 03370 . wfMessage( 'parser-template-recursion-depth-warning' ) 03371 ->numParams( $limit )->inContentLanguage()->text() 03372 . '</span>'; 03373 } 03374 } 03375 } 03376 03377 # Load from database 03378 if ( !$found && $title ) { 03379 if ( !Profiler::instance()->isPersistent() ) { 03380 # Too many unique items can kill profiling DBs/collectors 03381 $titleProfileIn = __METHOD__ . "-title-" . $title->getPrefixedDBkey(); 03382 wfProfileIn( $titleProfileIn ); // template in 03383 } 03384 wfProfileIn( __METHOD__ . '-loadtpl' ); 03385 if ( !$title->isExternal() ) { 03386 if ( $title->isSpecialPage() 03387 && $this->mOptions->getAllowSpecialInclusion() 03388 && $this->ot['html'] ) 03389 { 03390 // Pass the template arguments as URL parameters. 03391 // "uselang" will have no effect since the Language object 03392 // is forced to the one defined in ParserOptions. 03393 $pageArgs = array(); 03394 for ( $i = 0; $i < $args->getLength(); $i++ ) { 03395 $bits = $args->item( $i )->splitArg(); 03396 if ( strval( $bits['index'] ) === '' ) { 03397 $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); 03398 $value = trim( $frame->expand( $bits['value'] ) ); 03399 $pageArgs[$name] = $value; 03400 } 03401 } 03402 03403 // Create a new context to execute the special page 03404 $context = new RequestContext; 03405 $context->setTitle( $title ); 03406 $context->setRequest( new FauxRequest( $pageArgs ) ); 03407 $context->setUser( $this->getUser() ); 03408 $context->setLanguage( $this->mOptions->getUserLangObj() ); 03409 $ret = SpecialPageFactory::capturePath( $title, $context ); 03410 if ( $ret ) { 03411 $text = $context->getOutput()->getHTML(); 03412 $this->mOutput->addOutputPageMetadata( $context->getOutput() ); 03413 $found = true; 03414 $isHTML = true; 03415 $this->disableCache(); 03416 } 03417 } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) { 03418 $found = false; # access denied 03419 wfDebug( __METHOD__ . ": template inclusion denied for " . $title->getPrefixedDBkey() ); 03420 } else { 03421 list( $text, $title ) = $this->getTemplateDom( $title ); 03422 if ( $text !== false ) { 03423 $found = true; 03424 $isChildObj = true; 03425 } 03426 } 03427 03428 # If the title is valid but undisplayable, make a link to it 03429 if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { 03430 $text = "[[:$titleText]]"; 03431 $found = true; 03432 } 03433 } elseif ( $title->isTrans() ) { 03434 # Interwiki transclusion 03435 if ( $this->ot['html'] && !$forceRawInterwiki ) { 03436 $text = $this->interwikiTransclude( $title, 'render' ); 03437 $isHTML = true; 03438 } else { 03439 $text = $this->interwikiTransclude( $title, 'raw' ); 03440 # Preprocess it like a template 03441 $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); 03442 $isChildObj = true; 03443 } 03444 $found = true; 03445 } 03446 03447 # Do infinite loop check 03448 # This has to be done after redirect resolution to avoid infinite loops via redirects 03449 if ( !$frame->loopCheck( $title ) ) { 03450 $found = true; 03451 $text = '<span class="error">' 03452 . wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text() 03453 . '</span>'; 03454 wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" ); 03455 } 03456 wfProfileOut( __METHOD__ . '-loadtpl' ); 03457 } 03458 03459 # If we haven't found text to substitute by now, we're done 03460 # Recover the source wikitext and return it 03461 if ( !$found ) { 03462 $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); 03463 if ( $titleProfileIn ) { 03464 wfProfileOut( $titleProfileIn ); // template out 03465 } 03466 wfProfileOut( __METHOD__ ); 03467 return array( 'object' => $text ); 03468 } 03469 03470 # Expand DOM-style return values in a child frame 03471 if ( $isChildObj ) { 03472 # Clean up argument array 03473 $newFrame = $frame->newChild( $args, $title ); 03474 03475 if ( $nowiki ) { 03476 $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG ); 03477 } elseif ( $titleText !== false && $newFrame->isEmpty() ) { 03478 # Expansion is eligible for the empty-frame cache 03479 if ( isset( $this->mTplExpandCache[$titleText] ) ) { 03480 $text = $this->mTplExpandCache[$titleText]; 03481 } else { 03482 $text = $newFrame->expand( $text ); 03483 $this->mTplExpandCache[$titleText] = $text; 03484 } 03485 } else { 03486 # Uncached expansion 03487 $text = $newFrame->expand( $text ); 03488 } 03489 } 03490 if ( $isLocalObj && $nowiki ) { 03491 $text = $frame->expand( $text, PPFrame::RECOVER_ORIG ); 03492 $isLocalObj = false; 03493 } 03494 03495 if ( $titleProfileIn ) { 03496 wfProfileOut( $titleProfileIn ); // template out 03497 } 03498 03499 # Replace raw HTML by a placeholder 03500 if ( $isHTML ) { 03501 $text = $this->insertStripItem( $text ); 03502 } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) { 03503 # Escape nowiki-style return values 03504 $text = wfEscapeWikiText( $text ); 03505 } elseif ( is_string( $text ) 03506 && !$piece['lineStart'] 03507 && preg_match( '/^(?:{\\||:|;|#|\*)/', $text ) ) 03508 { 03509 # Bug 529: if the template begins with a table or block-level 03510 # element, it should be treated as beginning a new line. 03511 # This behavior is somewhat controversial. 03512 $text = "\n" . $text; 03513 } 03514 03515 if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { 03516 # Error, oversize inclusion 03517 if ( $titleText !== false ) { 03518 # Make a working, properly escaped link if possible (bug 23588) 03519 $text = "[[:$titleText]]"; 03520 } else { 03521 # This will probably not be a working link, but at least it may 03522 # provide some hint of where the problem is 03523 preg_replace( '/^:/', '', $originalTitle ); 03524 $text = "[[:$originalTitle]]"; 03525 } 03526 $text .= $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' ); 03527 $this->limitationWarn( 'post-expand-template-inclusion' ); 03528 } 03529 03530 if ( $isLocalObj ) { 03531 $ret = array( 'object' => $text ); 03532 } else { 03533 $ret = array( 'text' => $text ); 03534 } 03535 03536 wfProfileOut( __METHOD__ ); 03537 return $ret; 03538 } 03539 03558 public function callParserFunction( $frame, $function, array $args = array() ) { 03559 global $wgContLang; 03560 03561 wfProfileIn( __METHOD__ ); 03562 03563 # Case sensitive functions 03564 if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { 03565 $function = $this->mFunctionSynonyms[1][$function]; 03566 } else { 03567 # Case insensitive functions 03568 $function = $wgContLang->lc( $function ); 03569 if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { 03570 $function = $this->mFunctionSynonyms[0][$function]; 03571 } else { 03572 wfProfileOut( __METHOD__ ); 03573 return array( 'found' => false ); 03574 } 03575 } 03576 03577 wfProfileIn( __METHOD__ . '-pfunc-' . $function ); 03578 list( $callback, $flags ) = $this->mFunctionHooks[$function]; 03579 03580 # Workaround for PHP bug 35229 and similar 03581 if ( !is_callable( $callback ) ) { 03582 wfProfileOut( __METHOD__ . '-pfunc-' . $function ); 03583 wfProfileOut( __METHOD__ ); 03584 throw new MWException( "Tag hook for $function is not callable\n" ); 03585 } 03586 03587 $allArgs = array( &$this ); 03588 if ( $flags & SFH_OBJECT_ARGS ) { 03589 # Convert arguments to PPNodes and collect for appending to $allArgs 03590 $funcArgs = array(); 03591 foreach ( $args as $k => $v ) { 03592 if ( $v instanceof PPNode || $k === 0 ) { 03593 $funcArgs[] = $v; 03594 } else { 03595 $funcArgs[] = $this->mPreprocessor->newPartNodeArray( array( $k => $v ) )->item( 0 ); 03596 } 03597 } 03598 03599 # Add a frame parameter, and pass the arguments as an array 03600 $allArgs[] = $frame; 03601 $allArgs[] = $funcArgs; 03602 } else { 03603 # Convert arguments to plain text and append to $allArgs 03604 foreach ( $args as $k => $v ) { 03605 if ( $v instanceof PPNode ) { 03606 $allArgs[] = trim( $frame->expand( $v ) ); 03607 } elseif ( is_int( $k ) && $k >= 0 ) { 03608 $allArgs[] = trim( $v ); 03609 } else { 03610 $allArgs[] = trim( "$k=$v" ); 03611 } 03612 } 03613 } 03614 03615 $result = call_user_func_array( $callback, $allArgs ); 03616 03617 # The interface for function hooks allows them to return a wikitext 03618 # string or an array containing the string and any flags. This mungs 03619 # things around to match what this method should return. 03620 if ( !is_array( $result ) ) { 03621 $result = array( 03622 'found' => true, 03623 'text' => $result, 03624 ); 03625 } else { 03626 if ( isset( $result[0] ) && !isset( $result['text'] ) ) { 03627 $result['text'] = $result[0]; 03628 } 03629 unset( $result[0] ); 03630 $result += array( 03631 'found' => true, 03632 ); 03633 } 03634 03635 $noparse = true; 03636 $preprocessFlags = 0; 03637 if ( isset( $result['noparse'] ) ) { 03638 $noparse = $result['noparse']; 03639 } 03640 if ( isset( $result['preprocessFlags'] ) ) { 03641 $preprocessFlags = $result['preprocessFlags']; 03642 } 03643 03644 if ( !$noparse ) { 03645 $result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags ); 03646 $result['isChildObj'] = true; 03647 } 03648 wfProfileOut( __METHOD__ . '-pfunc-' . $function ); 03649 wfProfileOut( __METHOD__ ); 03650 03651 return $result; 03652 } 03653 03662 function getTemplateDom( $title ) { 03663 $cacheTitle = $title; 03664 $titleText = $title->getPrefixedDBkey(); 03665 03666 if ( isset( $this->mTplRedirCache[$titleText] ) ) { 03667 list( $ns, $dbk ) = $this->mTplRedirCache[$titleText]; 03668 $title = Title::makeTitle( $ns, $dbk ); 03669 $titleText = $title->getPrefixedDBkey(); 03670 } 03671 if ( isset( $this->mTplDomCache[$titleText] ) ) { 03672 return array( $this->mTplDomCache[$titleText], $title ); 03673 } 03674 03675 # Cache miss, go to the database 03676 list( $text, $title ) = $this->fetchTemplateAndTitle( $title ); 03677 03678 if ( $text === false ) { 03679 $this->mTplDomCache[$titleText] = false; 03680 return array( false, $title ); 03681 } 03682 03683 $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); 03684 $this->mTplDomCache[$titleText] = $dom; 03685 03686 if ( !$title->equals( $cacheTitle ) ) { 03687 $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] = 03688 array( $title->getNamespace(), $cdb = $title->getDBkey() ); 03689 } 03690 03691 return array( $dom, $title ); 03692 } 03693 03699 function fetchTemplateAndTitle( $title ) { 03700 $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate() 03701 $stuff = call_user_func( $templateCb, $title, $this ); 03702 $text = $stuff['text']; 03703 $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; 03704 if ( isset( $stuff['deps'] ) ) { 03705 foreach ( $stuff['deps'] as $dep ) { 03706 $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); 03707 if ( $dep['title']->equals( $this->getTitle() ) ) { 03708 // If we transclude ourselves, the final result 03709 // will change based on the new version of the page 03710 $this->mOutput->setFlag( 'vary-revision' ); 03711 } 03712 } 03713 } 03714 return array( $text, $finalTitle ); 03715 } 03716 03722 function fetchTemplate( $title ) { 03723 $rv = $this->fetchTemplateAndTitle( $title ); 03724 return $rv[0]; 03725 } 03726 03736 static function statelessFetchTemplate( $title, $parser = false ) { 03737 $text = $skip = false; 03738 $finalTitle = $title; 03739 $deps = array(); 03740 03741 # Loop to fetch the article, with up to 1 redirect 03742 for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { 03743 # Give extensions a chance to select the revision instead 03744 $id = false; # Assume current 03745 wfRunHooks( 'BeforeParserFetchTemplateAndtitle', 03746 array( $parser, $title, &$skip, &$id ) ); 03747 03748 if ( $skip ) { 03749 $text = false; 03750 $deps[] = array( 03751 'title' => $title, 03752 'page_id' => $title->getArticleID(), 03753 'rev_id' => null 03754 ); 03755 break; 03756 } 03757 # Get the revision 03758 $rev = $id 03759 ? Revision::newFromId( $id ) 03760 : Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); 03761 $rev_id = $rev ? $rev->getId() : 0; 03762 # If there is no current revision, there is no page 03763 if ( $id === false && !$rev ) { 03764 $linkCache = LinkCache::singleton(); 03765 $linkCache->addBadLinkObj( $title ); 03766 } 03767 03768 $deps[] = array( 03769 'title' => $title, 03770 'page_id' => $title->getArticleID(), 03771 'rev_id' => $rev_id ); 03772 if ( $rev && !$title->equals( $rev->getTitle() ) ) { 03773 # We fetched a rev from a different title; register it too... 03774 $deps[] = array( 03775 'title' => $rev->getTitle(), 03776 'page_id' => $rev->getPage(), 03777 'rev_id' => $rev_id ); 03778 } 03779 03780 if ( $rev ) { 03781 $content = $rev->getContent(); 03782 $text = $content ? $content->getWikitextForTransclusion() : null; 03783 03784 if ( $text === false || $text === null ) { 03785 $text = false; 03786 break; 03787 } 03788 } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) { 03789 global $wgContLang; 03790 $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage(); 03791 if ( !$message->exists() ) { 03792 $text = false; 03793 break; 03794 } 03795 $content = $message->content(); 03796 $text = $message->plain(); 03797 } else { 03798 break; 03799 } 03800 if ( !$content ) { 03801 break; 03802 } 03803 # Redirect? 03804 $finalTitle = $title; 03805 $title = $content->getRedirectTarget(); 03806 } 03807 return array( 03808 'text' => $text, 03809 'finalTitle' => $finalTitle, 03810 'deps' => $deps ); 03811 } 03812 03820 function fetchFile( $title, $options = array() ) { 03821 $res = $this->fetchFileAndTitle( $title, $options ); 03822 return $res[0]; 03823 } 03824 03832 function fetchFileAndTitle( $title, $options = array() ) { 03833 $file = $this->fetchFileNoRegister( $title, $options ); 03834 03835 $time = $file ? $file->getTimestamp() : false; 03836 $sha1 = $file ? $file->getSha1() : false; 03837 # Register the file as a dependency... 03838 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 ); 03839 if ( $file && !$title->equals( $file->getTitle() ) ) { 03840 # Update fetched file title 03841 $title = $file->getTitle(); 03842 if ( is_null( $file->getRedirectedTitle() ) ) { 03843 # This file was not a redirect, but the title does not match. 03844 # Register under the new name because otherwise the link will 03845 # get lost. 03846 $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 ); 03847 } 03848 } 03849 return array( $file, $title ); 03850 } 03851 03862 protected function fetchFileNoRegister( $title, $options = array() ) { 03863 if ( isset( $options['broken'] ) ) { 03864 $file = false; // broken thumbnail forced by hook 03865 } elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp) 03866 $file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options ); 03867 } else { // get by (name,timestamp) 03868 $file = wfFindFile( $title, $options ); 03869 } 03870 return $file; 03871 } 03872 03881 function interwikiTransclude( $title, $action ) { 03882 global $wgEnableScaryTranscluding; 03883 03884 if ( !$wgEnableScaryTranscluding ) { 03885 return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text(); 03886 } 03887 03888 $url = $title->getFullURL( array( 'action' => $action ) ); 03889 03890 if ( strlen( $url ) > 255 ) { 03891 return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text(); 03892 } 03893 return $this->fetchScaryTemplateMaybeFromCache( $url ); 03894 } 03895 03900 function fetchScaryTemplateMaybeFromCache( $url ) { 03901 global $wgTranscludeCacheExpiry; 03902 $dbr = wfGetDB( DB_SLAVE ); 03903 $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry ); 03904 $obj = $dbr->selectRow( 'transcache', array( 'tc_time', 'tc_contents' ), 03905 array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) ); 03906 if ( $obj ) { 03907 return $obj->tc_contents; 03908 } 03909 03910 $req = MWHttpRequest::factory( $url ); 03911 $status = $req->execute(); // Status object 03912 if ( $status->isOK() ) { 03913 $text = $req->getContent(); 03914 } elseif ( $req->getStatus() != 200 ) { // Though we failed to fetch the content, this status is useless. 03915 return wfMessage( 'scarytranscludefailed-httpstatus', $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text(); 03916 } else { 03917 return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text(); 03918 } 03919 03920 $dbw = wfGetDB( DB_MASTER ); 03921 $dbw->replace( 'transcache', array( 'tc_url' ), array( 03922 'tc_url' => $url, 03923 'tc_time' => $dbw->timestamp( time() ), 03924 'tc_contents' => $text 03925 ) ); 03926 return $text; 03927 } 03928 03938 function argSubstitution( $piece, $frame ) { 03939 wfProfileIn( __METHOD__ ); 03940 03941 $error = false; 03942 $parts = $piece['parts']; 03943 $nameWithSpaces = $frame->expand( $piece['title'] ); 03944 $argName = trim( $nameWithSpaces ); 03945 $object = false; 03946 $text = $frame->getArgument( $argName ); 03947 if ( $text === false && $parts->getLength() > 0 03948 && ( 03949 $this->ot['html'] 03950 || $this->ot['pre'] 03951 || ( $this->ot['wiki'] && $frame->isTemplate() ) 03952 ) 03953 ) { 03954 # No match in frame, use the supplied default 03955 $object = $parts->item( 0 )->getChildren(); 03956 } 03957 if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) { 03958 $error = '<!-- WARNING: argument omitted, expansion size too large -->'; 03959 $this->limitationWarn( 'post-expand-template-argument' ); 03960 } 03961 03962 if ( $text === false && $object === false ) { 03963 # No match anywhere 03964 $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts ); 03965 } 03966 if ( $error !== false ) { 03967 $text .= $error; 03968 } 03969 if ( $object !== false ) { 03970 $ret = array( 'object' => $object ); 03971 } else { 03972 $ret = array( 'text' => $text ); 03973 } 03974 03975 wfProfileOut( __METHOD__ ); 03976 return $ret; 03977 } 03978 03994 function extensionSubstitution( $params, $frame ) { 03995 $name = $frame->expand( $params['name'] ); 03996 $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] ); 03997 $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] ); 03998 $marker = "{$this->mUniqPrefix}-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX; 03999 04000 $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) && 04001 ( $this->ot['html'] || $this->ot['pre'] ); 04002 if ( $isFunctionTag ) { 04003 $markerType = 'none'; 04004 } else { 04005 $markerType = 'general'; 04006 } 04007 if ( $this->ot['html'] || $isFunctionTag ) { 04008 $name = strtolower( $name ); 04009 $attributes = Sanitizer::decodeTagAttributes( $attrText ); 04010 if ( isset( $params['attributes'] ) ) { 04011 $attributes = $attributes + $params['attributes']; 04012 } 04013 04014 if ( isset( $this->mTagHooks[$name] ) ) { 04015 # Workaround for PHP bug 35229 and similar 04016 if ( !is_callable( $this->mTagHooks[$name] ) ) { 04017 throw new MWException( "Tag hook for $name is not callable\n" ); 04018 } 04019 $output = call_user_func_array( $this->mTagHooks[$name], 04020 array( $content, $attributes, $this, $frame ) ); 04021 } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) { 04022 list( $callback, ) = $this->mFunctionTagHooks[$name]; 04023 if ( !is_callable( $callback ) ) { 04024 throw new MWException( "Tag hook for $name is not callable\n" ); 04025 } 04026 04027 $output = call_user_func_array( $callback, array( &$this, $frame, $content, $attributes ) ); 04028 } else { 04029 $output = '<span class="error">Invalid tag extension name: ' . 04030 htmlspecialchars( $name ) . '</span>'; 04031 } 04032 04033 if ( is_array( $output ) ) { 04034 # Extract flags to local scope (to override $markerType) 04035 $flags = $output; 04036 $output = $flags[0]; 04037 unset( $flags[0] ); 04038 extract( $flags ); 04039 } 04040 } else { 04041 if ( is_null( $attrText ) ) { 04042 $attrText = ''; 04043 } 04044 if ( isset( $params['attributes'] ) ) { 04045 foreach ( $params['attributes'] as $attrName => $attrValue ) { 04046 $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' . 04047 htmlspecialchars( $attrValue ) . '"'; 04048 } 04049 } 04050 if ( $content === null ) { 04051 $output = "<$name$attrText/>"; 04052 } else { 04053 $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] ); 04054 $output = "<$name$attrText>$content$close"; 04055 } 04056 } 04057 04058 if ( $markerType === 'none' ) { 04059 return $output; 04060 } elseif ( $markerType === 'nowiki' ) { 04061 $this->mStripState->addNoWiki( $marker, $output ); 04062 } elseif ( $markerType === 'general' ) { 04063 $this->mStripState->addGeneral( $marker, $output ); 04064 } else { 04065 throw new MWException( __METHOD__ . ': invalid marker type' ); 04066 } 04067 return $marker; 04068 } 04069 04077 function incrementIncludeSize( $type, $size ) { 04078 if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { 04079 return false; 04080 } else { 04081 $this->mIncludeSizes[$type] += $size; 04082 return true; 04083 } 04084 } 04085 04091 function incrementExpensiveFunctionCount() { 04092 $this->mExpensiveFunctionCount++; 04093 return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit(); 04094 } 04095 04104 function doDoubleUnderscore( $text ) { 04105 wfProfileIn( __METHOD__ ); 04106 04107 # The position of __TOC__ needs to be recorded 04108 $mw = MagicWord::get( 'toc' ); 04109 if ( $mw->match( $text ) ) { 04110 $this->mShowToc = true; 04111 $this->mForceTocPosition = true; 04112 04113 # Set a placeholder. At the end we'll fill it in with the TOC. 04114 $text = $mw->replace( '<!--MWTOC-->', $text, 1 ); 04115 04116 # Only keep the first one. 04117 $text = $mw->replace( '', $text ); 04118 } 04119 04120 # Now match and remove the rest of them 04121 $mwa = MagicWord::getDoubleUnderscoreArray(); 04122 $this->mDoubleUnderscores = $mwa->matchAndRemove( $text ); 04123 04124 if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) { 04125 $this->mOutput->mNoGallery = true; 04126 } 04127 if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) { 04128 $this->mShowToc = false; 04129 } 04130 if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) { 04131 $this->addTrackingCategory( 'hidden-category-category' ); 04132 } 04133 # (bug 8068) Allow control over whether robots index a page. 04134 # 04135 # @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This 04136 # is not desirable, the last one on the page should win. 04137 if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) { 04138 $this->mOutput->setIndexPolicy( 'noindex' ); 04139 $this->addTrackingCategory( 'noindex-category' ); 04140 } 04141 if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) { 04142 $this->mOutput->setIndexPolicy( 'index' ); 04143 $this->addTrackingCategory( 'index-category' ); 04144 } 04145 04146 # Cache all double underscores in the database 04147 foreach ( $this->mDoubleUnderscores as $key => $val ) { 04148 $this->mOutput->setProperty( $key, '' ); 04149 } 04150 04151 wfProfileOut( __METHOD__ ); 04152 return $text; 04153 } 04154 04162 public function addTrackingCategory( $msg ) { 04163 if ( $this->mTitle->getNamespace() === NS_SPECIAL ) { 04164 wfDebug( __METHOD__ . ": Not adding tracking category $msg to special page!\n" ); 04165 return false; 04166 } 04167 // Important to parse with correct title (bug 31469) 04168 $cat = wfMessage( $msg ) 04169 ->title( $this->getTitle() ) 04170 ->inContentLanguage() 04171 ->text(); 04172 04173 # Allow tracking categories to be disabled by setting them to "-" 04174 if ( $cat === '-' ) { 04175 return false; 04176 } 04177 04178 $containerCategory = Title::makeTitleSafe( NS_CATEGORY, $cat ); 04179 if ( $containerCategory ) { 04180 $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() ); 04181 return true; 04182 } else { 04183 wfDebug( __METHOD__ . ": [[MediaWiki:$msg]] is not a valid title!\n" ); 04184 return false; 04185 } 04186 } 04187 04204 function formatHeadings( $text, $origText, $isMain = true ) { 04205 global $wgMaxTocLevel, $wgExperimentalHtmlIds; 04206 04207 # Inhibit editsection links if requested in the page 04208 if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) { 04209 $maybeShowEditLink = $showEditLink = false; 04210 } else { 04211 $maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */ 04212 $showEditLink = $this->mOptions->getEditSection(); 04213 } 04214 if ( $showEditLink ) { 04215 $this->mOutput->setEditSectionTokens( true ); 04216 } 04217 04218 # Get all headlines for numbering them and adding funky stuff like [edit] 04219 # links - this is for later, but we need the number of headlines right now 04220 $matches = array(); 04221 $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?' . '>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i', $text, $matches ); 04222 04223 # if there are fewer than 4 headlines in the article, do not show TOC 04224 # unless it's been explicitly enabled. 04225 $enoughToc = $this->mShowToc && 04226 ( ( $numMatches >= 4 ) || $this->mForceTocPosition ); 04227 04228 # Allow user to stipulate that a page should have a "new section" 04229 # link added via __NEWSECTIONLINK__ 04230 if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) { 04231 $this->mOutput->setNewSection( true ); 04232 } 04233 04234 # Allow user to remove the "new section" 04235 # link via __NONEWSECTIONLINK__ 04236 if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) { 04237 $this->mOutput->hideNewSection( true ); 04238 } 04239 04240 # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, 04241 # override above conditions and always show TOC above first header 04242 if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) { 04243 $this->mShowToc = true; 04244 $enoughToc = true; 04245 } 04246 04247 # headline counter 04248 $headlineCount = 0; 04249 $numVisible = 0; 04250 04251 # Ugh .. the TOC should have neat indentation levels which can be 04252 # passed to the skin functions. These are determined here 04253 $toc = ''; 04254 $full = ''; 04255 $head = array(); 04256 $sublevelCount = array(); 04257 $levelCount = array(); 04258 $level = 0; 04259 $prevlevel = 0; 04260 $toclevel = 0; 04261 $prevtoclevel = 0; 04262 $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX; 04263 $baseTitleText = $this->mTitle->getPrefixedDBkey(); 04264 $oldType = $this->mOutputType; 04265 $this->setOutputType( self::OT_WIKI ); 04266 $frame = $this->getPreprocessor()->newFrame(); 04267 $root = $this->preprocessToDom( $origText ); 04268 $node = $root->getFirstChild(); 04269 $byteOffset = 0; 04270 $tocraw = array(); 04271 $refers = array(); 04272 04273 foreach ( $matches[3] as $headline ) { 04274 $isTemplate = false; 04275 $titleText = false; 04276 $sectionIndex = false; 04277 $numbering = ''; 04278 $markerMatches = array(); 04279 if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) { 04280 $serial = $markerMatches[1]; 04281 list( $titleText, $sectionIndex ) = $this->mHeadings[$serial]; 04282 $isTemplate = ( $titleText != $baseTitleText ); 04283 $headline = preg_replace( "/^$markerRegex\\s*/", "", $headline ); 04284 } 04285 04286 if ( $toclevel ) { 04287 $prevlevel = $level; 04288 } 04289 $level = $matches[1][$headlineCount]; 04290 04291 if ( $level > $prevlevel ) { 04292 # Increase TOC level 04293 $toclevel++; 04294 $sublevelCount[$toclevel] = 0; 04295 if ( $toclevel < $wgMaxTocLevel ) { 04296 $prevtoclevel = $toclevel; 04297 $toc .= Linker::tocIndent(); 04298 $numVisible++; 04299 } 04300 } elseif ( $level < $prevlevel && $toclevel > 1 ) { 04301 # Decrease TOC level, find level to jump to 04302 04303 for ( $i = $toclevel; $i > 0; $i-- ) { 04304 if ( $levelCount[$i] == $level ) { 04305 # Found last matching level 04306 $toclevel = $i; 04307 break; 04308 } elseif ( $levelCount[$i] < $level ) { 04309 # Found first matching level below current level 04310 $toclevel = $i + 1; 04311 break; 04312 } 04313 } 04314 if ( $i == 0 ) { 04315 $toclevel = 1; 04316 } 04317 if ( $toclevel < $wgMaxTocLevel ) { 04318 if ( $prevtoclevel < $wgMaxTocLevel ) { 04319 # Unindent only if the previous toc level was shown :p 04320 $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel ); 04321 $prevtoclevel = $toclevel; 04322 } else { 04323 $toc .= Linker::tocLineEnd(); 04324 } 04325 } 04326 } else { 04327 # No change in level, end TOC line 04328 if ( $toclevel < $wgMaxTocLevel ) { 04329 $toc .= Linker::tocLineEnd(); 04330 } 04331 } 04332 04333 $levelCount[$toclevel] = $level; 04334 04335 # count number of headlines for each level 04336 $sublevelCount[$toclevel]++; 04337 $dot = 0; 04338 for ( $i = 1; $i <= $toclevel; $i++ ) { 04339 if ( !empty( $sublevelCount[$i] ) ) { 04340 if ( $dot ) { 04341 $numbering .= '.'; 04342 } 04343 $numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] ); 04344 $dot = 1; 04345 } 04346 } 04347 04348 # The safe header is a version of the header text safe to use for links 04349 04350 # Remove link placeholders by the link text. 04351 # <!--LINK number--> 04352 # turns into 04353 # link text with suffix 04354 # Do this before unstrip since link text can contain strip markers 04355 $safeHeadline = $this->replaceLinkHoldersText( $headline ); 04356 04357 # Avoid insertion of weird stuff like <math> by expanding the relevant sections 04358 $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline ); 04359 04360 # Strip out HTML (first regex removes any tag not allowed) 04361 # Allowed tags are: 04362 # * <sup> and <sub> (bug 8393) 04363 # * <i> (bug 26375) 04364 # * <b> (r105284) 04365 # * <span dir="rtl"> and <span dir="ltr"> (bug 35167) 04366 # 04367 # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>, 04368 # to allow setting directionality in toc items. 04369 $tocline = preg_replace( 04370 array( '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?' . '>#', '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?' . '>#' ), 04371 array( '', '<$1>' ), 04372 $safeHeadline 04373 ); 04374 $tocline = trim( $tocline ); 04375 04376 # For the anchor, strip out HTML-y stuff period 04377 $safeHeadline = preg_replace( '/<.*?' . '>/', '', $safeHeadline ); 04378 $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline ); 04379 04380 # Save headline for section edit hint before it's escaped 04381 $headlineHint = $safeHeadline; 04382 04383 if ( $wgExperimentalHtmlIds ) { 04384 # For reverse compatibility, provide an id that's 04385 # HTML4-compatible, like we used to. 04386 # 04387 # It may be worth noting, academically, that it's possible for 04388 # the legacy anchor to conflict with a non-legacy headline 04389 # anchor on the page. In this case likely the "correct" thing 04390 # would be to either drop the legacy anchors or make sure 04391 # they're numbered first. However, this would require people 04392 # to type in section names like "abc_.D7.93.D7.90.D7.A4" 04393 # manually, so let's not bother worrying about it. 04394 $legacyHeadline = Sanitizer::escapeId( $safeHeadline, 04395 array( 'noninitial', 'legacy' ) ); 04396 $safeHeadline = Sanitizer::escapeId( $safeHeadline ); 04397 04398 if ( $legacyHeadline == $safeHeadline ) { 04399 # No reason to have both (in fact, we can't) 04400 $legacyHeadline = false; 04401 } 04402 } else { 04403 $legacyHeadline = false; 04404 $safeHeadline = Sanitizer::escapeId( $safeHeadline, 04405 'noninitial' ); 04406 } 04407 04408 # HTML names must be case-insensitively unique (bug 10721). 04409 # This does not apply to Unicode characters per 04410 # http://dev.w3.org/html5/spec/infrastructure.html#case-sensitivity-and-string-comparison 04411 # @todo FIXME: We may be changing them depending on the current locale. 04412 $arrayKey = strtolower( $safeHeadline ); 04413 if ( $legacyHeadline === false ) { 04414 $legacyArrayKey = false; 04415 } else { 04416 $legacyArrayKey = strtolower( $legacyHeadline ); 04417 } 04418 04419 # count how many in assoc. array so we can track dupes in anchors 04420 if ( isset( $refers[$arrayKey] ) ) { 04421 $refers[$arrayKey]++; 04422 } else { 04423 $refers[$arrayKey] = 1; 04424 } 04425 if ( isset( $refers[$legacyArrayKey] ) ) { 04426 $refers[$legacyArrayKey]++; 04427 } else { 04428 $refers[$legacyArrayKey] = 1; 04429 } 04430 04431 # Don't number the heading if it is the only one (looks silly) 04432 if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) { 04433 # the two are different if the line contains a link 04434 $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline; 04435 } 04436 04437 # Create the anchor for linking from the TOC to the section 04438 $anchor = $safeHeadline; 04439 $legacyAnchor = $legacyHeadline; 04440 if ( $refers[$arrayKey] > 1 ) { 04441 $anchor .= '_' . $refers[$arrayKey]; 04442 } 04443 if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) { 04444 $legacyAnchor .= '_' . $refers[$legacyArrayKey]; 04445 } 04446 if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) { 04447 $toc .= Linker::tocLine( $anchor, $tocline, 04448 $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) ); 04449 } 04450 04451 # Add the section to the section tree 04452 # Find the DOM node for this header 04453 $noOffset = ( $isTemplate || $sectionIndex === false ); 04454 while ( $node && !$noOffset ) { 04455 if ( $node->getName() === 'h' ) { 04456 $bits = $node->splitHeading(); 04457 if ( $bits['i'] == $sectionIndex ) { 04458 break; 04459 } 04460 } 04461 $byteOffset += mb_strlen( $this->mStripState->unstripBoth( 04462 $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) ); 04463 $node = $node->getNextSibling(); 04464 } 04465 $tocraw[] = array( 04466 'toclevel' => $toclevel, 04467 'level' => $level, 04468 'line' => $tocline, 04469 'number' => $numbering, 04470 'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex, 04471 'fromtitle' => $titleText, 04472 'byteoffset' => ( $noOffset ? null : $byteOffset ), 04473 'anchor' => $anchor, 04474 ); 04475 04476 # give headline the correct <h#> tag 04477 if ( $maybeShowEditLink && $sectionIndex !== false ) { 04478 // Output edit section links as markers with styles that can be customized by skins 04479 if ( $isTemplate ) { 04480 # Put a T flag in the section identifier, to indicate to extractSections() 04481 # that sections inside <includeonly> should be counted. 04482 $editlinkArgs = array( $titleText, "T-$sectionIndex"/*, null */ ); 04483 } else { 04484 $editlinkArgs = array( $this->mTitle->getPrefixedText(), $sectionIndex, $headlineHint ); 04485 } 04486 // We use a bit of pesudo-xml for editsection markers. The language converter is run later on 04487 // Using a UNIQ style marker leads to the converter screwing up the tokens when it converts stuff 04488 // And trying to insert strip tags fails too. At this point all real inputted tags have already been escaped 04489 // so we don't have to worry about a user trying to input one of these markers directly. 04490 // We use a page and section attribute to stop the language converter from converting these important bits 04491 // of data, but put the headline hint inside a content block because the language converter is supposed to 04492 // be able to convert that piece of data. 04493 $editlink = '<mw:editsection page="' . htmlspecialchars( $editlinkArgs[0] ); 04494 $editlink .= '" section="' . htmlspecialchars( $editlinkArgs[1] ) . '"'; 04495 if ( isset( $editlinkArgs[2] ) ) { 04496 $editlink .= '>' . $editlinkArgs[2] . '</mw:editsection>'; 04497 } else { 04498 $editlink .= '/>'; 04499 } 04500 } else { 04501 $editlink = ''; 04502 } 04503 $head[$headlineCount] = Linker::makeHeadline( $level, 04504 $matches['attrib'][$headlineCount], $anchor, $headline, 04505 $editlink, $legacyAnchor ); 04506 04507 $headlineCount++; 04508 } 04509 04510 $this->setOutputType( $oldType ); 04511 04512 # Never ever show TOC if no headers 04513 if ( $numVisible < 1 ) { 04514 $enoughToc = false; 04515 } 04516 04517 if ( $enoughToc ) { 04518 if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) { 04519 $toc .= Linker::tocUnindent( $prevtoclevel - 1 ); 04520 } 04521 $toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() ); 04522 $this->mOutput->setTOCHTML( $toc ); 04523 $toc = self::TOC_START . $toc . self::TOC_END; 04524 } 04525 04526 if ( $isMain ) { 04527 $this->mOutput->setSections( $tocraw ); 04528 } 04529 04530 # split up and insert constructed headlines 04531 $blocks = preg_split( '/<H[1-6].*?' . '>[\s\S]*?<\/H[1-6]>/i', $text ); 04532 $i = 0; 04533 04534 // build an array of document sections 04535 $sections = array(); 04536 foreach ( $blocks as $block ) { 04537 // $head is zero-based, sections aren't. 04538 if ( empty( $head[$i - 1] ) ) { 04539 $sections[$i] = $block; 04540 } else { 04541 $sections[$i] = $head[$i - 1] . $block; 04542 } 04543 04554 wfRunHooks( 'ParserSectionCreate', array( $this, $i, &$sections[$i], $showEditLink ) ); 04555 04556 $i++; 04557 } 04558 04559 if ( $enoughToc && $isMain && !$this->mForceTocPosition ) { 04560 // append the TOC at the beginning 04561 // Top anchor now in skin 04562 $sections[0] = $sections[0] . $toc . "\n"; 04563 } 04564 04565 $full .= join( '', $sections ); 04566 04567 if ( $this->mForceTocPosition ) { 04568 return str_replace( '<!--MWTOC-->', $toc, $full ); 04569 } else { 04570 return $full; 04571 } 04572 } 04573 04585 public function preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState = true ) { 04586 $this->startParse( $title, $options, self::OT_WIKI, $clearState ); 04587 $this->setUser( $user ); 04588 04589 $pairs = array( 04590 "\r\n" => "\n", 04591 ); 04592 $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); 04593 if ( $options->getPreSaveTransform() ) { 04594 $text = $this->pstPass2( $text, $user ); 04595 } 04596 $text = $this->mStripState->unstripBoth( $text ); 04597 04598 $this->setUser( null ); #Reset 04599 04600 return $text; 04601 } 04602 04612 function pstPass2( $text, $user ) { 04613 global $wgContLang; 04614 04615 # Note: This is the timestamp saved as hardcoded wikitext to 04616 # the database, we use $wgContLang here in order to give 04617 # everyone the same signature and use the default one rather 04618 # than the one selected in each user's preferences. 04619 # (see also bug 12815) 04620 $ts = $this->mOptions->getTimestamp(); 04621 $timestamp = MWTimestamp::getLocalInstance( $ts ); 04622 $ts = $timestamp->format( 'YmdHis' ); 04623 $tzMsg = $timestamp->format( 'T' ); # might vary on DST changeover! 04624 04625 # Allow translation of timezones through wiki. format() can return 04626 # whatever crap the system uses, localised or not, so we cannot 04627 # ship premade translations. 04628 $key = 'timezone-' . strtolower( trim( $tzMsg ) ); 04629 $msg = wfMessage( $key )->inContentLanguage(); 04630 if ( $msg->exists() ) { 04631 $tzMsg = $msg->text(); 04632 } 04633 04634 $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)"; 04635 04636 # Variable replacement 04637 # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags 04638 $text = $this->replaceVariables( $text ); 04639 04640 # This works almost by chance, as the replaceVariables are done before the getUserSig(), 04641 # which may corrupt this parser instance via its wfMessage()->text() call- 04642 04643 # Signatures 04644 $sigText = $this->getUserSig( $user ); 04645 $text = strtr( $text, array( 04646 '~~~~~' => $d, 04647 '~~~~' => "$sigText $d", 04648 '~~~' => $sigText 04649 ) ); 04650 04651 # Context links ("pipe tricks"): [[|name]] and [[name (context)|]] 04652 $tc = '[' . Title::legalChars() . ']'; 04653 $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii! 04654 04655 $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; # [[ns:page (context)|]] 04656 $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; # [[ns:page(context)|]] (double-width brackets, added in r40257) 04657 $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]] (using either single or double-width comma) 04658 $p2 = "/\[\[\\|($tc+)]]/"; # [[|page]] (reverse pipe trick: add context from page title) 04659 04660 # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" 04661 $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); 04662 $text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text ); 04663 $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); 04664 04665 $t = $this->mTitle->getText(); 04666 $m = array(); 04667 if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { 04668 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); 04669 } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) { 04670 $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); 04671 } else { 04672 # if there's no context, don't bother duplicating the title 04673 $text = preg_replace( $p2, '[[\\1]]', $text ); 04674 } 04675 04676 # Trim trailing whitespace 04677 $text = rtrim( $text ); 04678 04679 return $text; 04680 } 04681 04696 function getUserSig( &$user, $nickname = false, $fancySig = null ) { 04697 global $wgMaxSigChars; 04698 04699 $username = $user->getName(); 04700 04701 # If not given, retrieve from the user object. 04702 if ( $nickname === false ) { 04703 $nickname = $user->getOption( 'nickname' ); 04704 } 04705 04706 if ( is_null( $fancySig ) ) { 04707 $fancySig = $user->getBoolOption( 'fancysig' ); 04708 } 04709 04710 $nickname = $nickname == null ? $username : $nickname; 04711 04712 if ( mb_strlen( $nickname ) > $wgMaxSigChars ) { 04713 $nickname = $username; 04714 wfDebug( __METHOD__ . ": $username has overlong signature.\n" ); 04715 } elseif ( $fancySig !== false ) { 04716 # Sig. might contain markup; validate this 04717 if ( $this->validateSig( $nickname ) !== false ) { 04718 # Validated; clean up (if needed) and return it 04719 return $this->cleanSig( $nickname, true ); 04720 } else { 04721 # Failed to validate; fall back to the default 04722 $nickname = $username; 04723 wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" ); 04724 } 04725 } 04726 04727 # Make sure nickname doesnt get a sig in a sig 04728 $nickname = self::cleanSigInSig( $nickname ); 04729 04730 # If we're still here, make it a link to the user page 04731 $userText = wfEscapeWikiText( $username ); 04732 $nickText = wfEscapeWikiText( $nickname ); 04733 $msgName = $user->isAnon() ? 'signature-anon' : 'signature'; 04734 04735 return wfMessage( $msgName, $userText, $nickText )->inContentLanguage()->title( $this->getTitle() )->text(); 04736 } 04737 04744 function validateSig( $text ) { 04745 return Xml::isWellFormedXmlFragment( $text ) ? $text : false; 04746 } 04747 04758 public function cleanSig( $text, $parsing = false ) { 04759 if ( !$parsing ) { 04760 global $wgTitle; 04761 $this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true ); 04762 } 04763 04764 # Option to disable this feature 04765 if ( !$this->mOptions->getCleanSignatures() ) { 04766 return $text; 04767 } 04768 04769 # @todo FIXME: Regex doesn't respect extension tags or nowiki 04770 # => Move this logic to braceSubstitution() 04771 $substWord = MagicWord::get( 'subst' ); 04772 $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); 04773 $substText = '{{' . $substWord->getSynonym( 0 ); 04774 04775 $text = preg_replace( $substRegex, $substText, $text ); 04776 $text = self::cleanSigInSig( $text ); 04777 $dom = $this->preprocessToDom( $text ); 04778 $frame = $this->getPreprocessor()->newFrame(); 04779 $text = $frame->expand( $dom ); 04780 04781 if ( !$parsing ) { 04782 $text = $this->mStripState->unstripBoth( $text ); 04783 } 04784 04785 return $text; 04786 } 04787 04794 public static function cleanSigInSig( $text ) { 04795 $text = preg_replace( '/~{3,5}/', '', $text ); 04796 return $text; 04797 } 04798 04808 public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) { 04809 $this->startParse( $title, $options, $outputType, $clearState ); 04810 } 04811 04818 private function startParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) { 04819 $this->setTitle( $title ); 04820 $this->mOptions = $options; 04821 $this->setOutputType( $outputType ); 04822 if ( $clearState ) { 04823 $this->clearState(); 04824 } 04825 } 04826 04835 public function transformMsg( $text, $options, $title = null ) { 04836 static $executing = false; 04837 04838 # Guard against infinite recursion 04839 if ( $executing ) { 04840 return $text; 04841 } 04842 $executing = true; 04843 04844 wfProfileIn( __METHOD__ ); 04845 if ( !$title ) { 04846 global $wgTitle; 04847 $title = $wgTitle; 04848 } 04849 04850 $text = $this->preprocess( $text, $title, $options ); 04851 04852 $executing = false; 04853 wfProfileOut( __METHOD__ ); 04854 return $text; 04855 } 04856 04881 public function setHook( $tag, $callback ) { 04882 $tag = strtolower( $tag ); 04883 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { 04884 throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" ); 04885 } 04886 $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; 04887 $this->mTagHooks[$tag] = $callback; 04888 if ( !in_array( $tag, $this->mStripList ) ) { 04889 $this->mStripList[] = $tag; 04890 } 04891 04892 return $oldVal; 04893 } 04894 04912 function setTransparentTagHook( $tag, $callback ) { 04913 $tag = strtolower( $tag ); 04914 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { 04915 throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" ); 04916 } 04917 $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null; 04918 $this->mTransparentTagHooks[$tag] = $callback; 04919 04920 return $oldVal; 04921 } 04922 04926 function clearTagHooks() { 04927 $this->mTagHooks = array(); 04928 $this->mFunctionTagHooks = array(); 04929 $this->mStripList = $this->mDefaultStripList; 04930 } 04931 04975 public function setFunctionHook( $id, $callback, $flags = 0 ) { 04976 global $wgContLang; 04977 04978 $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null; 04979 $this->mFunctionHooks[$id] = array( $callback, $flags ); 04980 04981 # Add to function cache 04982 $mw = MagicWord::get( $id ); 04983 if ( !$mw ) { 04984 throw new MWException( __METHOD__ . '() expecting a magic word identifier.' ); 04985 } 04986 04987 $synonyms = $mw->getSynonyms(); 04988 $sensitive = intval( $mw->isCaseSensitive() ); 04989 04990 foreach ( $synonyms as $syn ) { 04991 # Case 04992 if ( !$sensitive ) { 04993 $syn = $wgContLang->lc( $syn ); 04994 } 04995 # Add leading hash 04996 if ( !( $flags & SFH_NO_HASH ) ) { 04997 $syn = '#' . $syn; 04998 } 04999 # Remove trailing colon 05000 if ( substr( $syn, -1, 1 ) === ':' ) { 05001 $syn = substr( $syn, 0, -1 ); 05002 } 05003 $this->mFunctionSynonyms[$sensitive][$syn] = $id; 05004 } 05005 return $oldVal; 05006 } 05007 05013 function getFunctionHooks() { 05014 return array_keys( $this->mFunctionHooks ); 05015 } 05016 05027 function setFunctionTagHook( $tag, $callback, $flags ) { 05028 $tag = strtolower( $tag ); 05029 if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { 05030 throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" ); 05031 } 05032 $old = isset( $this->mFunctionTagHooks[$tag] ) ? 05033 $this->mFunctionTagHooks[$tag] : null; 05034 $this->mFunctionTagHooks[$tag] = array( $callback, $flags ); 05035 05036 if ( !in_array( $tag, $this->mStripList ) ) { 05037 $this->mStripList[] = $tag; 05038 } 05039 05040 return $old; 05041 } 05042 05053 function replaceLinkHolders( &$text, $options = 0 ) { 05054 return $this->mLinkHolders->replace( $text ); 05055 } 05056 05064 function replaceLinkHoldersText( $text ) { 05065 return $this->mLinkHolders->replaceText( $text ); 05066 } 05067 05081 function renderImageGallery( $text, $params ) { 05082 wfProfileIn( __METHOD__ ); 05083 05084 $mode = false; 05085 if ( isset( $params['mode'] ) ) { 05086 $mode = $params['mode']; 05087 } 05088 05089 try { 05090 $ig = ImageGalleryBase::factory( $mode ); 05091 } catch ( MWException $e ) { 05092 // If invalid type set, fallback to default. 05093 $ig = ImageGalleryBase::factory( false ); 05094 } 05095 05096 $ig->setContextTitle( $this->mTitle ); 05097 $ig->setShowBytes( false ); 05098 $ig->setShowFilename( false ); 05099 $ig->setParser( $this ); 05100 $ig->setHideBadImages(); 05101 $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) ); 05102 05103 if ( isset( $params['showfilename'] ) ) { 05104 $ig->setShowFilename( true ); 05105 } else { 05106 $ig->setShowFilename( false ); 05107 } 05108 if ( isset( $params['caption'] ) ) { 05109 $caption = $params['caption']; 05110 $caption = htmlspecialchars( $caption ); 05111 $caption = $this->replaceInternalLinks( $caption ); 05112 $ig->setCaptionHtml( $caption ); 05113 } 05114 if ( isset( $params['perrow'] ) ) { 05115 $ig->setPerRow( $params['perrow'] ); 05116 } 05117 if ( isset( $params['widths'] ) ) { 05118 $ig->setWidths( $params['widths'] ); 05119 } 05120 if ( isset( $params['heights'] ) ) { 05121 $ig->setHeights( $params['heights'] ); 05122 } 05123 $ig->setAdditionalOptions( $params ); 05124 05125 wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) ); 05126 05127 $lines = StringUtils::explode( "\n", $text ); 05128 foreach ( $lines as $line ) { 05129 # match lines like these: 05130 # Image:someimage.jpg|This is some image 05131 $matches = array(); 05132 preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); 05133 # Skip empty lines 05134 if ( count( $matches ) == 0 ) { 05135 continue; 05136 } 05137 05138 if ( strpos( $matches[0], '%' ) !== false ) { 05139 $matches[1] = rawurldecode( $matches[1] ); 05140 } 05141 $title = Title::newFromText( $matches[1], NS_FILE ); 05142 if ( is_null( $title ) ) { 05143 # Bogus title. Ignore these so we don't bomb out later. 05144 continue; 05145 } 05146 05147 # We need to get what handler the file uses, to figure out parameters. 05148 # Note, a hook can overide the file name, and chose an entirely different 05149 # file (which potentially could be of a different type and have different handler). 05150 $options = array(); 05151 $descQuery = false; 05152 wfRunHooks( 'BeforeParserFetchFileAndTitle', 05153 array( $this, $title, &$options, &$descQuery ) ); 05154 # Don't register it now, as ImageGallery does that later. 05155 $file = $this->fetchFileNoRegister( $title, $options ); 05156 $handler = $file ? $file->getHandler() : false; 05157 05158 wfProfileIn( __METHOD__ . '-getMagicWord' ); 05159 $paramMap = array( 05160 'img_alt' => 'gallery-internal-alt', 05161 'img_link' => 'gallery-internal-link', 05162 ); 05163 if ( $handler ) { 05164 $paramMap = $paramMap + $handler->getParamMap(); 05165 // We don't want people to specify per-image widths. 05166 // Additionally the width parameter would need special casing anyhow. 05167 unset( $paramMap['img_width'] ); 05168 } 05169 05170 $mwArray = new MagicWordArray( array_keys( $paramMap ) ); 05171 wfProfileOut( __METHOD__ . '-getMagicWord' ); 05172 05173 $label = ''; 05174 $alt = ''; 05175 $link = ''; 05176 $handlerOptions = array(); 05177 if ( isset( $matches[3] ) ) { 05178 // look for an |alt= definition while trying not to break existing 05179 // captions with multiple pipes (|) in it, until a more sensible grammar 05180 // is defined for images in galleries 05181 05182 // FIXME: Doing recursiveTagParse at this stage, and the trim before 05183 // splitting on '|' is a bit odd, and different from makeImage. 05184 $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) ); 05185 $parameterMatches = StringUtils::explode( '|', $matches[3] ); 05186 05187 foreach ( $parameterMatches as $parameterMatch ) { 05188 list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch ); 05189 if ( $magicName ) { 05190 $paramName = $paramMap[$magicName]; 05191 05192 switch ( $paramName ) { 05193 case 'gallery-internal-alt': 05194 $alt = $this->stripAltText( $match, false ); 05195 break; 05196 case 'gallery-internal-link': 05197 $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) ); 05198 $chars = self::EXT_LINK_URL_CLASS; 05199 $prots = $this->mUrlProtocols; 05200 //check to see if link matches an absolute url, if not then it must be a wiki link. 05201 if ( preg_match( "/^($prots)$chars+$/u", $linkValue ) ) { 05202 $link = $linkValue; 05203 } else { 05204 $localLinkTitle = Title::newFromText( $linkValue ); 05205 if ( $localLinkTitle !== null ) { 05206 $link = $localLinkTitle->getLocalURL(); 05207 } 05208 } 05209 break; 05210 default: 05211 // Must be a handler specific parameter. 05212 if ( $handler->validateParam( $paramName, $match ) ) { 05213 $handlerOptions[$paramName] = $match; 05214 } else { 05215 // Guess not. Append it to the caption. 05216 wfDebug( "$parameterMatch failed parameter validation" ); 05217 $label .= '|' . $parameterMatch; 05218 } 05219 } 05220 05221 } else { 05222 // concatenate all other pipes 05223 $label .= '|' . $parameterMatch; 05224 } 05225 } 05226 // remove the first pipe 05227 $label = substr( $label, 1 ); 05228 } 05229 05230 $ig->add( $title, $label, $alt, $link, $handlerOptions ); 05231 } 05232 $html = $ig->toHTML(); 05233 wfProfileOut( __METHOD__ ); 05234 return $html; 05235 } 05236 05241 function getImageParams( $handler ) { 05242 if ( $handler ) { 05243 $handlerClass = get_class( $handler ); 05244 } else { 05245 $handlerClass = ''; 05246 } 05247 if ( !isset( $this->mImageParams[$handlerClass] ) ) { 05248 # Initialise static lists 05249 static $internalParamNames = array( 05250 'horizAlign' => array( 'left', 'right', 'center', 'none' ), 05251 'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 05252 'bottom', 'text-bottom' ), 05253 'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless', 05254 'upright', 'border', 'link', 'alt', 'class' ), 05255 ); 05256 static $internalParamMap; 05257 if ( !$internalParamMap ) { 05258 $internalParamMap = array(); 05259 foreach ( $internalParamNames as $type => $names ) { 05260 foreach ( $names as $name ) { 05261 $magicName = str_replace( '-', '_', "img_$name" ); 05262 $internalParamMap[$magicName] = array( $type, $name ); 05263 } 05264 } 05265 } 05266 05267 # Add handler params 05268 $paramMap = $internalParamMap; 05269 if ( $handler ) { 05270 $handlerParamMap = $handler->getParamMap(); 05271 foreach ( $handlerParamMap as $magic => $paramName ) { 05272 $paramMap[$magic] = array( 'handler', $paramName ); 05273 } 05274 } 05275 $this->mImageParams[$handlerClass] = $paramMap; 05276 $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) ); 05277 } 05278 return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ); 05279 } 05280 05289 function makeImage( $title, $options, $holders = false ) { 05290 # Check if the options text is of the form "options|alt text" 05291 # Options are: 05292 # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang 05293 # * left no resizing, just left align. label is used for alt= only 05294 # * right same, but right aligned 05295 # * none same, but not aligned 05296 # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox 05297 # * center center the image 05298 # * frame Keep original image size, no magnify-button. 05299 # * framed Same as "frame" 05300 # * frameless like 'thumb' but without a frame. Keeps user preferences for width 05301 # * upright reduce width for upright images, rounded to full __0 px 05302 # * border draw a 1px border around the image 05303 # * alt Text for HTML alt attribute (defaults to empty) 05304 # * class Set a class for img node 05305 # * link Set the target of the image link. Can be external, interwiki, or local 05306 # vertical-align values (no % or length right now): 05307 # * baseline 05308 # * sub 05309 # * super 05310 # * top 05311 # * text-top 05312 # * middle 05313 # * bottom 05314 # * text-bottom 05315 05316 $parts = StringUtils::explode( "|", $options ); 05317 05318 # Give extensions a chance to select the file revision for us 05319 $options = array(); 05320 $descQuery = false; 05321 wfRunHooks( 'BeforeParserFetchFileAndTitle', 05322 array( $this, $title, &$options, &$descQuery ) ); 05323 # Fetch and register the file (file title may be different via hooks) 05324 list( $file, $title ) = $this->fetchFileAndTitle( $title, $options ); 05325 05326 # Get parameter map 05327 $handler = $file ? $file->getHandler() : false; 05328 05329 list( $paramMap, $mwArray ) = $this->getImageParams( $handler ); 05330 05331 if ( !$file ) { 05332 $this->addTrackingCategory( 'broken-file-category' ); 05333 } 05334 05335 # Process the input parameters 05336 $caption = ''; 05337 $params = array( 'frame' => array(), 'handler' => array(), 05338 'horizAlign' => array(), 'vertAlign' => array() ); 05339 foreach ( $parts as $part ) { 05340 $part = trim( $part ); 05341 list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); 05342 $validated = false; 05343 if ( isset( $paramMap[$magicName] ) ) { 05344 list( $type, $paramName ) = $paramMap[$magicName]; 05345 05346 # Special case; width and height come in one variable together 05347 if ( $type === 'handler' && $paramName === 'width' ) { 05348 $parsedWidthParam = $this->parseWidthParam( $value ); 05349 if ( isset( $parsedWidthParam['width'] ) ) { 05350 $width = $parsedWidthParam['width']; 05351 if ( $handler->validateParam( 'width', $width ) ) { 05352 $params[$type]['width'] = $width; 05353 $validated = true; 05354 } 05355 } 05356 if ( isset( $parsedWidthParam['height'] ) ) { 05357 $height = $parsedWidthParam['height']; 05358 if ( $handler->validateParam( 'height', $height ) ) { 05359 $params[$type]['height'] = $height; 05360 $validated = true; 05361 } 05362 } 05363 # else no validation -- bug 13436 05364 } else { 05365 if ( $type === 'handler' ) { 05366 # Validate handler parameter 05367 $validated = $handler->validateParam( $paramName, $value ); 05368 } else { 05369 # Validate internal parameters 05370 switch ( $paramName ) { 05371 case 'manualthumb': 05372 case 'alt': 05373 case 'class': 05374 # @todo FIXME: Possibly check validity here for 05375 # manualthumb? downstream behavior seems odd with 05376 # missing manual thumbs. 05377 $validated = true; 05378 $value = $this->stripAltText( $value, $holders ); 05379 break; 05380 case 'link': 05381 $chars = self::EXT_LINK_URL_CLASS; 05382 $prots = $this->mUrlProtocols; 05383 if ( $value === '' ) { 05384 $paramName = 'no-link'; 05385 $value = true; 05386 $validated = true; 05387 } elseif ( preg_match( "/^(?i)$prots/", $value ) ) { 05388 if ( preg_match( "/^((?i)$prots)$chars+$/u", $value, $m ) ) { 05389 $paramName = 'link-url'; 05390 $this->mOutput->addExternalLink( $value ); 05391 if ( $this->mOptions->getExternalLinkTarget() ) { 05392 $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget(); 05393 } 05394 $validated = true; 05395 } 05396 } else { 05397 $linkTitle = Title::newFromText( $value ); 05398 if ( $linkTitle ) { 05399 $paramName = 'link-title'; 05400 $value = $linkTitle; 05401 $this->mOutput->addLink( $linkTitle ); 05402 $validated = true; 05403 } 05404 } 05405 break; 05406 default: 05407 # Most other things appear to be empty or numeric... 05408 $validated = ( $value === false || is_numeric( trim( $value ) ) ); 05409 } 05410 } 05411 05412 if ( $validated ) { 05413 $params[$type][$paramName] = $value; 05414 } 05415 } 05416 } 05417 if ( !$validated ) { 05418 $caption = $part; 05419 } 05420 } 05421 05422 # Process alignment parameters 05423 if ( $params['horizAlign'] ) { 05424 $params['frame']['align'] = key( $params['horizAlign'] ); 05425 } 05426 if ( $params['vertAlign'] ) { 05427 $params['frame']['valign'] = key( $params['vertAlign'] ); 05428 } 05429 05430 $params['frame']['caption'] = $caption; 05431 05432 # Will the image be presented in a frame, with the caption below? 05433 $imageIsFramed = isset( $params['frame']['frame'] ) || 05434 isset( $params['frame']['framed'] ) || 05435 isset( $params['frame']['thumbnail'] ) || 05436 isset( $params['frame']['manualthumb'] ); 05437 05438 # In the old days, [[Image:Foo|text...]] would set alt text. Later it 05439 # came to also set the caption, ordinary text after the image -- which 05440 # makes no sense, because that just repeats the text multiple times in 05441 # screen readers. It *also* came to set the title attribute. 05442 # 05443 # Now that we have an alt attribute, we should not set the alt text to 05444 # equal the caption: that's worse than useless, it just repeats the 05445 # text. This is the framed/thumbnail case. If there's no caption, we 05446 # use the unnamed parameter for alt text as well, just for the time be- 05447 # ing, if the unnamed param is set and the alt param is not. 05448 # 05449 # For the future, we need to figure out if we want to tweak this more, 05450 # e.g., introducing a title= parameter for the title; ignoring the un- 05451 # named parameter entirely for images without a caption; adding an ex- 05452 # plicit caption= parameter and preserving the old magic unnamed para- 05453 # meter for BC; ... 05454 if ( $imageIsFramed ) { # Framed image 05455 if ( $caption === '' && !isset( $params['frame']['alt'] ) ) { 05456 # No caption or alt text, add the filename as the alt text so 05457 # that screen readers at least get some description of the image 05458 $params['frame']['alt'] = $title->getText(); 05459 } 05460 # Do not set $params['frame']['title'] because tooltips don't make sense 05461 # for framed images 05462 } else { # Inline image 05463 if ( !isset( $params['frame']['alt'] ) ) { 05464 # No alt text, use the "caption" for the alt text 05465 if ( $caption !== '' ) { 05466 $params['frame']['alt'] = $this->stripAltText( $caption, $holders ); 05467 } else { 05468 # No caption, fall back to using the filename for the 05469 # alt text 05470 $params['frame']['alt'] = $title->getText(); 05471 } 05472 } 05473 # Use the "caption" for the tooltip text 05474 $params['frame']['title'] = $this->stripAltText( $caption, $holders ); 05475 } 05476 05477 wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params, $this ) ); 05478 05479 # Linker does the rest 05480 $time = isset( $options['time'] ) ? $options['time'] : false; 05481 $ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'], 05482 $time, $descQuery, $this->mOptions->getThumbSize() ); 05483 05484 # Give the handler a chance to modify the parser object 05485 if ( $handler ) { 05486 $handler->parserTransformHook( $this, $file ); 05487 } 05488 05489 return $ret; 05490 } 05491 05497 protected function stripAltText( $caption, $holders ) { 05498 # Strip bad stuff out of the title (tooltip). We can't just use 05499 # replaceLinkHoldersText() here, because if this function is called 05500 # from replaceInternalLinks2(), mLinkHolders won't be up-to-date. 05501 if ( $holders ) { 05502 $tooltip = $holders->replaceText( $caption ); 05503 } else { 05504 $tooltip = $this->replaceLinkHoldersText( $caption ); 05505 } 05506 05507 # make sure there are no placeholders in thumbnail attributes 05508 # that are later expanded to html- so expand them now and 05509 # remove the tags 05510 $tooltip = $this->mStripState->unstripBoth( $tooltip ); 05511 $tooltip = Sanitizer::stripAllTags( $tooltip ); 05512 05513 return $tooltip; 05514 } 05515 05520 function disableCache() { 05521 wfDebug( "Parser output marked as uncacheable.\n" ); 05522 if ( !$this->mOutput ) { 05523 throw new MWException( __METHOD__ . 05524 " can only be called when actually parsing something" ); 05525 } 05526 $this->mOutput->setCacheTime( -1 ); // old style, for compatibility 05527 $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency 05528 } 05529 05538 function attributeStripCallback( &$text, $frame = false ) { 05539 $text = $this->replaceVariables( $text, $frame ); 05540 $text = $this->mStripState->unstripBoth( $text ); 05541 return $text; 05542 } 05543 05549 function getTags() { 05550 return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ), array_keys( $this->mFunctionTagHooks ) ); 05551 } 05552 05563 function replaceTransparentTags( $text ) { 05564 $matches = array(); 05565 $elements = array_keys( $this->mTransparentTagHooks ); 05566 $text = self::extractTagsAndParams( $elements, $text, $matches, $this->mUniqPrefix ); 05567 $replacements = array(); 05568 05569 foreach ( $matches as $marker => $data ) { 05570 list( $element, $content, $params, $tag ) = $data; 05571 $tagName = strtolower( $element ); 05572 if ( isset( $this->mTransparentTagHooks[$tagName] ) ) { 05573 $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) ); 05574 } else { 05575 $output = $tag; 05576 } 05577 $replacements[$marker] = $output; 05578 } 05579 return strtr( $text, $replacements ); 05580 } 05581 05611 private function extractSections( $text, $section, $mode, $newText = '' ) { 05612 global $wgTitle; # not generally used but removes an ugly failure mode 05613 $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true ); 05614 $outText = ''; 05615 $frame = $this->getPreprocessor()->newFrame(); 05616 05617 # Process section extraction flags 05618 $flags = 0; 05619 $sectionParts = explode( '-', $section ); 05620 $sectionIndex = array_pop( $sectionParts ); 05621 foreach ( $sectionParts as $part ) { 05622 if ( $part === 'T' ) { 05623 $flags |= self::PTD_FOR_INCLUSION; 05624 } 05625 } 05626 05627 # Check for empty input 05628 if ( strval( $text ) === '' ) { 05629 # Only sections 0 and T-0 exist in an empty document 05630 if ( $sectionIndex == 0 ) { 05631 if ( $mode === 'get' ) { 05632 return ''; 05633 } else { 05634 return $newText; 05635 } 05636 } else { 05637 if ( $mode === 'get' ) { 05638 return $newText; 05639 } else { 05640 return $text; 05641 } 05642 } 05643 } 05644 05645 # Preprocess the text 05646 $root = $this->preprocessToDom( $text, $flags ); 05647 05648 # <h> nodes indicate section breaks 05649 # They can only occur at the top level, so we can find them by iterating the root's children 05650 $node = $root->getFirstChild(); 05651 05652 # Find the target section 05653 if ( $sectionIndex == 0 ) { 05654 # Section zero doesn't nest, level=big 05655 $targetLevel = 1000; 05656 } else { 05657 while ( $node ) { 05658 if ( $node->getName() === 'h' ) { 05659 $bits = $node->splitHeading(); 05660 if ( $bits['i'] == $sectionIndex ) { 05661 $targetLevel = $bits['level']; 05662 break; 05663 } 05664 } 05665 if ( $mode === 'replace' ) { 05666 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); 05667 } 05668 $node = $node->getNextSibling(); 05669 } 05670 } 05671 05672 if ( !$node ) { 05673 # Not found 05674 if ( $mode === 'get' ) { 05675 return $newText; 05676 } else { 05677 return $text; 05678 } 05679 } 05680 05681 # Find the end of the section, including nested sections 05682 do { 05683 if ( $node->getName() === 'h' ) { 05684 $bits = $node->splitHeading(); 05685 $curLevel = $bits['level']; 05686 if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) { 05687 break; 05688 } 05689 } 05690 if ( $mode === 'get' ) { 05691 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); 05692 } 05693 $node = $node->getNextSibling(); 05694 } while ( $node ); 05695 05696 # Write out the remainder (in replace mode only) 05697 if ( $mode === 'replace' ) { 05698 # Output the replacement text 05699 # Add two newlines on -- trailing whitespace in $newText is conventionally 05700 # stripped by the editor, so we need both newlines to restore the paragraph gap 05701 # Only add trailing whitespace if there is newText 05702 if ( $newText != "" ) { 05703 $outText .= $newText . "\n\n"; 05704 } 05705 05706 while ( $node ) { 05707 $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); 05708 $node = $node->getNextSibling(); 05709 } 05710 } 05711 05712 if ( is_string( $outText ) ) { 05713 # Re-insert stripped tags 05714 $outText = rtrim( $this->mStripState->unstripBoth( $outText ) ); 05715 } 05716 05717 return $outText; 05718 } 05719 05732 public function getSection( $text, $section, $deftext = '' ) { 05733 return $this->extractSections( $text, $section, "get", $deftext ); 05734 } 05735 05746 public function replaceSection( $oldtext, $section, $text ) { 05747 return $this->extractSections( $oldtext, $section, "replace", $text ); 05748 } 05749 05755 function getRevisionId() { 05756 return $this->mRevisionId; 05757 } 05758 05764 protected function getRevisionObject() { 05765 if ( !is_null( $this->mRevisionObject ) ) { 05766 return $this->mRevisionObject; 05767 } 05768 if ( is_null( $this->mRevisionId ) ) { 05769 return null; 05770 } 05771 05772 $this->mRevisionObject = Revision::newFromId( $this->mRevisionId ); 05773 return $this->mRevisionObject; 05774 } 05775 05780 function getRevisionTimestamp() { 05781 if ( is_null( $this->mRevisionTimestamp ) ) { 05782 wfProfileIn( __METHOD__ ); 05783 05784 global $wgContLang; 05785 05786 $revObject = $this->getRevisionObject(); 05787 $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow(); 05788 05789 # The cryptic '' timezone parameter tells to use the site-default 05790 # timezone offset instead of the user settings. 05791 # 05792 # Since this value will be saved into the parser cache, served 05793 # to other users, and potentially even used inside links and such, 05794 # it needs to be consistent for all visitors. 05795 $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); 05796 05797 wfProfileOut( __METHOD__ ); 05798 } 05799 return $this->mRevisionTimestamp; 05800 } 05801 05807 function getRevisionUser() { 05808 if ( is_null( $this->mRevisionUser ) ) { 05809 $revObject = $this->getRevisionObject(); 05810 05811 # if this template is subst: the revision id will be blank, 05812 # so just use the current user's name 05813 if ( $revObject ) { 05814 $this->mRevisionUser = $revObject->getUserText(); 05815 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) { 05816 $this->mRevisionUser = $this->getUser()->getName(); 05817 } 05818 } 05819 return $this->mRevisionUser; 05820 } 05821 05827 function getRevisionSize() { 05828 if ( is_null( $this->mRevisionSize ) ) { 05829 $revObject = $this->getRevisionObject(); 05830 05831 # if this variable is subst: the revision id will be blank, 05832 # so just use the parser input size, because the own substituation 05833 # will change the size. 05834 if ( $revObject ) { 05835 $this->mRevisionSize = $revObject->getSize(); 05836 } elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) { 05837 $this->mRevisionSize = $this->mInputSize; 05838 } 05839 } 05840 return $this->mRevisionSize; 05841 } 05842 05848 public function setDefaultSort( $sort ) { 05849 $this->mDefaultSort = $sort; 05850 $this->mOutput->setProperty( 'defaultsort', $sort ); 05851 } 05852 05863 public function getDefaultSort() { 05864 if ( $this->mDefaultSort !== false ) { 05865 return $this->mDefaultSort; 05866 } else { 05867 return ''; 05868 } 05869 } 05870 05877 public function getCustomDefaultSort() { 05878 return $this->mDefaultSort; 05879 } 05880 05890 public function guessSectionNameFromWikiText( $text ) { 05891 # Strip out wikitext links(they break the anchor) 05892 $text = $this->stripSectionName( $text ); 05893 $text = Sanitizer::normalizeSectionNameWhitespace( $text ); 05894 return '#' . Sanitizer::escapeId( $text, 'noninitial' ); 05895 } 05896 05905 public function guessLegacySectionNameFromWikiText( $text ) { 05906 # Strip out wikitext links(they break the anchor) 05907 $text = $this->stripSectionName( $text ); 05908 $text = Sanitizer::normalizeSectionNameWhitespace( $text ); 05909 return '#' . Sanitizer::escapeId( $text, array( 'noninitial', 'legacy' ) ); 05910 } 05911 05926 public function stripSectionName( $text ) { 05927 # Strip internal link markup 05928 $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text ); 05929 $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text ); 05930 05931 # Strip external link markup 05932 # @todo FIXME: Not tolerant to blank link text 05933 # I.E. [http://www.mediawiki.org] will render as [1] or something depending 05934 # on how many empty links there are on the page - need to figure that out. 05935 $text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text ); 05936 05937 # Parse wikitext quotes (italics & bold) 05938 $text = $this->doQuotes( $text ); 05939 05940 # Strip HTML tags 05941 $text = StringUtils::delimiterReplace( '<', '>', '', $text ); 05942 return $text; 05943 } 05944 05955 function testSrvus( $text, Title $title, ParserOptions $options, $outputType = self::OT_HTML ) { 05956 $this->startParse( $title, $options, $outputType, true ); 05957 05958 $text = $this->replaceVariables( $text ); 05959 $text = $this->mStripState->unstripBoth( $text ); 05960 $text = Sanitizer::removeHTMLtags( $text ); 05961 return $text; 05962 } 05963 05970 function testPst( $text, Title $title, ParserOptions $options ) { 05971 return $this->preSaveTransform( $text, $title, $options->getUser(), $options ); 05972 } 05973 05980 function testPreprocess( $text, Title $title, ParserOptions $options ) { 05981 return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS ); 05982 } 05983 06000 function markerSkipCallback( $s, $callback ) { 06001 $i = 0; 06002 $out = ''; 06003 while ( $i < strlen( $s ) ) { 06004 $markerStart = strpos( $s, $this->mUniqPrefix, $i ); 06005 if ( $markerStart === false ) { 06006 $out .= call_user_func( $callback, substr( $s, $i ) ); 06007 break; 06008 } else { 06009 $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) ); 06010 $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart ); 06011 if ( $markerEnd === false ) { 06012 $out .= substr( $s, $markerStart ); 06013 break; 06014 } else { 06015 $markerEnd += strlen( self::MARKER_SUFFIX ); 06016 $out .= substr( $s, $markerStart, $markerEnd - $markerStart ); 06017 $i = $markerEnd; 06018 } 06019 } 06020 } 06021 return $out; 06022 } 06023 06030 function killMarkers( $text ) { 06031 return $this->mStripState->killMarkers( $text ); 06032 } 06033 06050 function serializeHalfParsedText( $text ) { 06051 wfProfileIn( __METHOD__ ); 06052 $data = array( 06053 'text' => $text, 06054 'version' => self::HALF_PARSED_VERSION, 06055 'stripState' => $this->mStripState->getSubState( $text ), 06056 'linkHolders' => $this->mLinkHolders->getSubArray( $text ) 06057 ); 06058 wfProfileOut( __METHOD__ ); 06059 return $data; 06060 } 06061 06077 function unserializeHalfParsedText( $data ) { 06078 if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) { 06079 throw new MWException( __METHOD__ . ': invalid version' ); 06080 } 06081 06082 # First, extract the strip state. 06083 $texts = array( $data['text'] ); 06084 $texts = $this->mStripState->merge( $data['stripState'], $texts ); 06085 06086 # Now renumber links 06087 $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts ); 06088 06089 # Should be good to go. 06090 return $texts[0]; 06091 } 06092 06102 function isValidHalfParsedText( $data ) { 06103 return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION; 06104 } 06105 06114 public function parseWidthParam( $value ) { 06115 $parsedWidthParam = array(); 06116 if ( $value === '' ) { 06117 return $parsedWidthParam; 06118 } 06119 $m = array(); 06120 # (bug 13500) In both cases (width/height and width only), 06121 # permit trailing "px" for backward compatibility. 06122 if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) { 06123 $width = intval( $m[1] ); 06124 $height = intval( $m[2] ); 06125 $parsedWidthParam['width'] = $width; 06126 $parsedWidthParam['height'] = $height; 06127 } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) { 06128 $width = intval( $value ); 06129 $parsedWidthParam['width'] = $width; 06130 } 06131 return $parsedWidthParam; 06132 } 06133 }