* changes in an incompatible way, so the parser cache
* can automatically discard old data.
*/
- const VERSION = '1.6.4';
+ const VERSION = '1.6.5';
+
+ /**
+ * Update this version number when the output of serialiseHalfParsedText()
+ * changes in an incompatible way
+ */
+ const HALF_PARSED_VERSION = 2;
# Flags for Parser::setFunctionHook
# Also available as global constants from Defines.php
# Constants needed for external link processing
# Everything except bracket, space, or control characters
- const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
+ const EXT_LINK_URL_CLASS = '(?:[^\]\[<>"\\x00-\\x20\\x7F]|(?:\[\]))';
const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
var $mVariables, $mSubstWords; # Initialised by initialiseVariables()
var $mConf, $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols; # Initialised in constructor
-
# Cleared with clearState():
- var $mOutput, $mAutonumber, $mDTopen, $mStripState;
+ var $mOutput, $mAutonumber, $mDTopen;
+
+ /**
+ * @var StripState
+ */
+ var $mStripState;
+
var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
var $mLinkHolders, $mLinkID;
var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
var $mTplExpandCache; # empty-frame expansion cache
var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
var $mExpensiveFunctionCount; # number of expensive parser function calls
+
+ /**
+ * @var User
+ */
var $mUser; # User object; only used when doing pre-save transform
# Temporary
# These are variables reset at least once per parse regardless of $clearState
- var $mOptions; # ParserOptions object
+
+ /**
+ * @var ParserOptions
+ */
+ var $mOptions;
+
+ /**
+ * @var Title
+ */
var $mTitle; # Title context, used for self-link rendering and similar things
var $mOutputType; # Output type, one of the OT_xxx constants
var $ot; # Shortcut alias, see setOutputType()
/**
* Constructor
- *
- * @public
*/
- function __construct( $conf = array() ) {
+ public function __construct( $conf = array() ) {
$this->mConf = $conf;
$this->mUrlProtocols = wfUrlProtocols();
$this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
- '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/S';
+ '(?:[^\]\[<>"\x00-\x20\x7F]|\[\])+) *([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/S';
if ( isset( $conf['preprocessorClass'] ) ) {
$this->mPreprocessorClass = $conf['preprocessorClass'];
} elseif ( extension_loaded( 'domxml' ) ) {
$this->mLastSection = '';
$this->mDTopen = false;
$this->mIncludeCount = array();
- $this->mStripState = new StripState;
$this->mArgStack = false;
$this->mInPre = false;
$this->mLinkHolders = new LinkHolderArray( $this );
# $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
# Changed to \x7f to allow XML double-parsing -- TS
$this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
+ $this->mStripState = new StripState( $this->mUniqPrefix );
# Clear these on every parse, bug 4549
wfProfileIn( __METHOD__ );
wfProfileIn( $fname );
- $this->mOptions = $options;
- if ( $clearState ) {
- $this->clearState();
- }
-
- $this->setTitle( $title ); # Page title has to be set for the pre-processor
+ $this->startParse( $title, $options, self::OT_HTML, $clearState );
$oldRevisionId = $this->mRevisionId;
$oldRevisionObject = $this->mRevisionObject;
$this->mRevisionTimestamp = null;
$this->mRevisionUser = null;
}
- $this->setOutputType( self::OT_HTML );
+
wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
# No more strip!
wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
}
/**
- * A page get its title converted except:
- * a) Language conversion is globally disabled
- * b) Title convert is globally disabled
- * c) The page is a redirect page
- * d) User request with a "linkconvert" set to "no"
- * e) A "nocontentconvert" magic word has been set
- * f) A "notitleconvert" magic word has been set
- * g) User sets "noconvertlink" in his/her preference
- *
- * Note that if a user tries to set a title in a conversion
- * rule but content conversion was not done, then the parser
- * won't pick it up. This is probably expected behavior.
+ * A converted title will be provided in the output object if title and
+ * content conversion are enabled, the article text does not contain
+ * a conversion-suppressing double-underscore tag, and no
+ * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
+ * automatic link conversion.
*/
if ( !( $wgDisableLangConversion
|| $wgDisableTitleConversion
wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
-//!JF Move to its own function
-
- $uniq_prefix = $this->mUniqPrefix;
- $matches = array();
- $elements = array_keys( $this->mTransparentTagHooks );
- $text = $this->extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-
- foreach ( $matches as $marker => $data ) {
- list( $element, $content, $params, $tag ) = $data;
- $tagName = strtolower( $element );
- if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
- $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) );
- } else {
- $output = $tag;
- }
- $this->mStripState->general->setPair( $marker, $output );
- }
+ $text = $this->replaceTransparentTags( $text );
$text = $this->mStripState->unstripGeneral( $text );
$text = Sanitizer::normalizeCharReferences( $text );
* Expand templates and variables in the text, producing valid, static wikitext.
* Also removes comments.
*/
- function preprocess( $text, $title, $options, $revid = null ) {
+ function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) {
wfProfileIn( __METHOD__ );
- $this->mOptions = $options;
- $this->clearState();
- $this->setOutputType( self::OT_PREPROCESS );
- $this->setTitle( $title );
+ $this->startParse( $title, $options, self::OT_PREPROCESS, true );
if ( $revid !== null ) {
$this->mRevisionId = $revid;
}
* <noinclude>, <includeonly> etc. are parsed as for template transclusion,
* comments, templates, arguments, tags hooks and parser functions are untouched.
*/
- public function getPreloadText( $text, $title, $options ) {
+ public function getPreloadText( $text, Title $title, ParserOptions $options ) {
# Parser (re)initialisation
- $this->mOptions = $options;
- $this->clearState();
- $this->setOutputType( self::OT_PLAIN );
- $this->setTitle( $title );
+ $this->startParse( $title, $options, self::OT_PLAIN, true );
$flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
- return $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
+ $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
+ $text = $this->mStripState->unstripBoth( $text );
+ return $text;
}
/**
* Get a random string
*
- * @private
* @static
*/
- static private function getRandomString() {
+ static public function getRandomString() {
return dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
}
return $this->mLinkID++;
}
+ function setLinkID( $id ) {
+ $this->mLinkID = $id;
+ }
+
+ /**
+ * @return Language
+ */
function getFunctionLang() {
global $wgLang, $wgContLang;
function insertStripItem( $text ) {
$rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
$this->mMarkerIndex++;
- $this->mStripState->general->setPair( $rnd, $text );
+ $this->mStripState->addGeneral( $rnd, $text );
return $rnd;
}
throw new MWException( __METHOD__.': unrecognised match type "' .
substr( $m[0], 0, 20 ) . '"' );
}
- $url = wfMsgForContent( $urlmsg, $id);
- $sk = $this->mOptions->getSkin( $this->mTitle );
- $la = $sk->getExternalLinkAttributes( "external $CssClass" );
- return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+ $url = wfMsgForContent( $urlmsg, $id );
+ return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $CssClass );
} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
# ISBN
$isbn = $m[5];
global $wgContLang;
wfProfileIn( __METHOD__ );
- $sk = $this->mOptions->getSkin( $this->mTitle );
$trail = '';
# The characters '<' and '>' (which were escaped by
$text = $this->maybeMakeExternalImage( $url );
if ( $text === false ) {
# Not an image, make a link
- $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
+ $text = Linker::makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
$this->getExternalLinkAttribs( $url ) );
# Register it in the output object...
# Replace unnecessary URL escape codes with their equivalent characters
global $wgContLang;
wfProfileIn( __METHOD__ );
- $sk = $this->mOptions->getSkin( $this->mTitle );
-
$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
$s = array_shift( $bits );
# This means that users can paste URLs directly into the text
# Funny characters like รถ aren't valid in URLs anyway
# This was changed in August 2004
- $s .= $sk->makeExternalLink( $url, $text, false, $linktype,
+ $s .= Linker::makeExternalLink( $url, $text, false, $linktype,
$this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
# Register link in the output object.
* @private
*/
function maybeMakeExternalImage( $url ) {
- $sk = $this->mOptions->getSkin( $this->mTitle );
$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
$imagesexception = !empty( $imagesfrom );
$text = false;
|| ( $imagesexception && $imagematch ) ) {
if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
# Image found
- $text = $sk->makeExternalImage( $url );
+ $text = Linker::makeExternalImage( $url );
}
}
if ( !$text && $this->mOptions->getEnableImageWhitelist()
}
if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
# Image matches a whitelist entry
- $text = $sk->makeExternalImage( $url );
+ $text = Linker::makeExternalImage( $url );
break;
}
}
$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
}
- $sk = $this->mOptions->getSkin( $this->mTitle );
$holders = new LinkHolderArray( $this );
# split the entire text string on occurences of [[
$holders->merge( $this->replaceInternalLinks2( $text ) );
}
# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
- $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
+ $s .= $prefix . $this->armorLinks(
+ $this->makeImage( $nt, $text, $holders ) ) . $trail;
} else {
$s .= $prefix . $trail;
}
- $this->mOutput->addImage( $nt->getDBkey() );
wfProfileOut( __METHOD__."-image" );
continue;
-
}
if ( $ns == NS_CATEGORY ) {
# Self-link checking
if ( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
if ( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
- $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
+ $s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail );
continue;
}
}
if ( $ns == NS_MEDIA ) {
wfProfileIn( __METHOD__."-media" );
# Give extensions a chance to select the file revision for us
- $skip = $time = false;
- wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
- if ( $skip ) {
- $link = $sk->link( $nt );
- } else {
- $link = $sk->makeMediaLinkObj( $nt, $text, $time );
- }
+ $time = $sha1 = $descQuery = false;
+ wfRunHooks( 'BeforeParserFetchFileAndTitle',
+ array( $this, &$nt, &$time, &$sha1, &$descQuery ) );
+ # Fetch and register the file (file title may be different via hooks)
+ list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $time, $sha1 );
# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
- $s .= $prefix . $this->armorLinks( $link ) . $trail;
- $this->mOutput->addImage( $nt->getDBkey() );
+ $s .= $prefix . $this->armorLinks(
+ Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail;
wfProfileOut( __METHOD__."-media" );
continue;
}
# batch file existence checks for NS_FILE and NS_MEDIA
if ( $iw == '' && $nt->isAlwaysKnown() ) {
$this->mOutput->addLink( $nt );
- $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+ $s .= $this->makeKnownLinkHolder( $nt, $text, array(), $trail, $prefix );
} else {
# Links will be added to the output link list after checking
- $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
+ $s .= $holders->makeHolder( $nt, $text, array(), $trail, $prefix );
}
wfProfileOut( __METHOD__."-always_known" );
}
*
* @deprecated
*/
- function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ function makeLinkHolder( &$nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix );
}
*
* @param $nt Title
* @param $text String
- * @param $query String
+ * @param $query Array or String
* @param $trail String
* @param $prefix String
* @return String: HTML-wikitext mix oh yuck
*/
- function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ function makeKnownLinkHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = '' ) {
list( $inside, $trail ) = Linker::splitTrail( $trail );
- $sk = $this->mOptions->getSkin( $this->mTitle );
- # FIXME: use link() instead of deprecated makeKnownLinkObj()
- $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
+
+ if ( is_string( $query ) ) {
+ $query = wfCgiToArray( $query );
+ }
+ if ( $text == '' ) {
+ $text = htmlspecialchars( $nt->getPrefixedText() );
+ }
+
+ $link = Linker::linkKnown( $nt, "$prefix$text$inside", array(), $query );
+
return $this->armorLinks( $link ) . $trail;
}
}
if ( $stack > 0 ) {
wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
+ wfProfileOut( __METHOD__ );
return false;
}
wfProfileOut( __METHOD__ );
$value = wfEscapeWikiText( $this->mTitle->getText() );
break;
case 'pagenamee':
- $value = $this->mTitle->getPartialURL();
+ $value = wfEscapeWikiText( $this->mTitle->getPartialURL() );
break;
case 'fullpagename':
$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
break;
case 'fullpagenamee':
- $value = $this->mTitle->getPrefixedURL();
+ $value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() );
break;
case 'subpagename':
$value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
break;
case 'subpagenamee':
- $value = $this->mTitle->getSubpageUrlForm();
+ $value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() );
break;
case 'basepagename':
$value = wfEscapeWikiText( $this->mTitle->getBaseText() );
break;
case 'basepagenamee':
- $value = wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+ $value = wfEscapeWikiText( wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) ) );
break;
case 'talkpagename':
if ( $this->mTitle->canTalk() ) {
case 'talkpagenamee':
if ( $this->mTitle->canTalk() ) {
$talkPage = $this->mTitle->getTalkPage();
- $value = $talkPage->getPrefixedUrl();
+ $value = wfEscapeWikiText( $talkPage->getPrefixedUrl() );
} else {
$value = '';
}
break;
case 'subjectpagenamee':
$subjPage = $this->mTitle->getSubjectPage();
- $value = $subjPage->getPrefixedUrl();
+ $value = wfEscapeWikiText( $subjPage->getPrefixedUrl() );
break;
case 'revisionid':
# Let the edit saving system know we should parse the page
/**
* Fetch the unparsed text of a template and register a reference to it.
+ * @param Title $title
+ * @return Array ( string or false, Title )
*/
function fetchTemplateAndTitle( $title ) {
$templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate()
return array( $text, $finalTitle );
}
+ /**
+ * Fetch the unparsed text of a template and register a reference to it.
+ * @param Title $title
+ * @return mixed string or false
+ */
function fetchTemplate( $title ) {
$rv = $this->fetchTemplateAndTitle( $title );
return $rv[0];
for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
# Give extensions a chance to select the revision instead
$id = false; # Assume current
- wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
+ wfRunHooks( 'BeforeParserFetchTemplateAndtitle',
+ array( $parser, &$title, &$skip, &$id ) );
if ( $skip ) {
$text = false;
$deps[] = array(
- 'title' => $title,
- 'page_id' => $title->getArticleID(),
- 'rev_id' => null );
+ 'title' => $title,
+ 'page_id' => $title->getArticleID(),
+ 'rev_id' => null
+ );
break;
}
- $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
+ # Get the revision
+ $rev = $id
+ ? Revision::newFromId( $id )
+ : Revision::newFromTitle( $title );
$rev_id = $rev ? $rev->getId() : 0;
# If there is no current revision, there is no page
if ( $id === false && !$rev ) {
}
$deps[] = array(
- 'title' => $title,
- 'page_id' => $title->getArticleID(),
- 'rev_id' => $rev_id );
+ 'title' => $title,
+ 'page_id' => $title->getArticleID(),
+ 'rev_id' => $rev_id );
+ if ( $rev && !$title->equals( $rev->getTitle() ) ) {
+ # We fetched a rev from a different title; register it too...
+ $deps[] = array(
+ 'title' => $rev->getTitle(),
+ 'page_id' => $rev->getPage(),
+ 'rev_id' => $rev_id );
+ }
if ( $rev ) {
$text = $rev->getText();
} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
global $wgContLang;
- $message = $wgContLang->lcfirst( $title->getText() );
- $text = wfMsgForContentNoTrans( $message );
- if ( wfEmptyMsg( $message, $text ) ) {
+ $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
+ if ( !$message->exists() ) {
$text = false;
break;
}
+ $text = $message->plain();
} else {
break;
}
'deps' => $deps );
}
+ /**
+ * Fetch a file and its title and register a reference to it.
+ * @param Title $title
+ * @param string $time MW timestamp
+ * @param string $sha1 base 36 SHA-1
+ * @return mixed File or false
+ */
+ function fetchFile( $title, $time = false, $sha1 = false ) {
+ $res = $this->fetchFileAndTitle( $title, $time, $sha1 );
+ return $res[0];
+ }
+
+ /**
+ * Fetch a file and its title and register a reference to it.
+ * @param Title $title
+ * @param string $time MW timestamp
+ * @param string $sha1 base 36 SHA-1
+ * @return Array ( File or false, Title of file )
+ */
+ function fetchFileAndTitle( $title, $time = false, $sha1 = false ) {
+ if ( $time === '0' ) {
+ $file = false; // broken thumbnail forced by hook
+ } elseif ( $sha1 ) { // get by (sha1,timestamp)
+ $file = RepoGroup::singleton()->findFileFromKey( $sha1, array( 'time' => $time ) );
+ } else { // get by (name,timestamp)
+ $file = wfFindFile( $title, array( 'time' => $time ) );
+ }
+ $time = $file ? $file->getTimestamp() : null;
+ $sha1 = $file ? $file->getSha1() : null;
+ # Register the file as a dependency...
+ $this->mOutput->addImage( $title->getDBkey(), $time, $sha1 );
+ if ( $file && !$title->equals( $file->getTitle() ) ) {
+ # We fetched a rev from a different title; register it too...
+ $this->mOutput->addImage( $file->getTitle()->getDBkey(), $time, $sha1 );
+ # Update fetched file title
+ $title = $file->getTitle();
+ }
+ return array( $file, $title );
+ }
+
/**
* Transclude an interwiki link.
*/
if ( $markerType === 'none' ) {
return $output;
} elseif ( $markerType === 'nowiki' ) {
- $this->mStripState->nowiki->setPair( $marker, $output );
+ $this->mStripState->addNoWiki( $marker, $output );
} elseif ( $markerType === 'general' ) {
- $this->mStripState->general->setPair( $marker, $output );
+ $this->mStripState->addGeneral( $marker, $output );
} else {
throw new MWException( __METHOD__.': invalid marker type' );
}
function formatHeadings( $text, $origText, $isMain=true ) {
global $wgMaxTocLevel, $wgContLang, $wgHtml5, $wgExperimentalHtmlIds;
- $doNumberHeadings = $this->mOptions->getNumberHeadings();
-
# Inhibit editsection links if requested in the page
if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
$showEditLink = 0;
$enoughToc = true;
}
- # We need this to perform operations on the HTML
- $sk = $this->mOptions->getSkin( $this->mTitle );
-
# headline counter
$headlineCount = 0;
$numVisible = 0;
$sublevelCount[$toclevel] = 0;
if ( $toclevel<$wgMaxTocLevel ) {
$prevtoclevel = $toclevel;
- $toc .= $sk->tocIndent();
+ $toc .= Linker::tocIndent();
$numVisible++;
}
} elseif ( $level < $prevlevel && $toclevel > 1 ) {
if ( $toclevel<$wgMaxTocLevel ) {
if ( $prevtoclevel < $wgMaxTocLevel ) {
# Unindent only if the previous toc level was shown :p
- $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+ $toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
$prevtoclevel = $toclevel;
} else {
- $toc .= $sk->tocLineEnd();
+ $toc .= Linker::tocLineEnd();
}
}
} else {
# No change in level, end TOC line
if ( $toclevel<$wgMaxTocLevel ) {
- $toc .= $sk->tocLineEnd();
+ $toc .= Linker::tocLineEnd();
}
}
}
# Don't number the heading if it is the only one (looks silly)
- if ( $doNumberHeadings && count( $matches[3] ) > 1) {
+ if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
# the two are different if the line contains a link
$headline = $numbering . ' ' . $headline;
}
$legacyAnchor .= '_' . $refers[$legacyArrayKey];
}
if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
- $toc .= $sk->tocLine( $anchor, $tocline,
+ $toc .= Linker::tocLine( $anchor, $tocline,
$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
}
// We use a page and section attribute to stop the language converter from converting these important bits
// of data, but put the headline hint inside a content block because the language converter is supposed to
// be able to convert that piece of data.
- $editlink = '<editsection page="' . htmlspecialchars($editlinkArgs[0]);
+ $editlink = '<mw:editsection page="' . htmlspecialchars($editlinkArgs[0]);
$editlink .= '" section="' . htmlspecialchars($editlinkArgs[1]) .'"';
if ( isset($editlinkArgs[2]) ) {
- $editlink .= '>' . $editlinkArgs[2] . '</editsection>';
+ $editlink .= '>' . $editlinkArgs[2] . '</mw:editsection>';
} else {
$editlink .= '/>';
}
} else {
$editlink = '';
}
- $head[$headlineCount] = $sk->makeHeadline( $level,
+ $head[$headlineCount] = Linker::makeHeadline( $level,
$matches['attrib'][$headlineCount], $anchor, $headline,
$editlink, $legacyAnchor );
if ( $enoughToc ) {
if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
- $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
+ $toc .= Linker::tocUnindent( $prevtoclevel - 1 );
}
- $toc = $sk->tocList( $toc );
+ $toc = Linker::tocList( $toc, $this->mOptions->getUserLang() );
$this->mOutput->setTOCHTML( $toc );
}
* @return String: the altered wiki markup
*/
public function preSaveTransform( $text, Title $title, User $user, ParserOptions $options, $clearState = true ) {
- $this->mOptions = $options;
- $this->setTitle( $title );
+ $this->startParse( $title, $options, self::OT_WIKI, $clearState );
$this->setUser( $user );
- $this->setOutputType( self::OT_WIKI );
-
- if ( $clearState ) {
- $this->clearState();
- }
$pairs = array(
"\r\n" => "\n",
);
$text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
- $text = $this->pstPass2( $text, $user );
+ if( $options->getPreSaveTransform() ) {
+ $text = $this->pstPass2( $text, $user );
+ }
$text = $this->mStripState->unstripBoth( $text );
$this->setUser( null ); #Reset
# whatever crap the system uses, localised or not, so we cannot
# ship premade translations.
$key = 'timezone-' . strtolower( trim( $tzMsg ) );
- $value = wfMsgForContent( $key );
- if ( !wfEmptyMsg( $key, $value ) ) {
- $tzMsg = $value;
+ $msg = wfMessage( $key )->inContentLanguage();
+ if ( $msg->exists() ) {
+ $tzMsg = $msg->text();
}
date_default_timezone_set( $oldtz );
# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
$text = $this->replaceVariables( $text );
+ # This works almost by chance, as the replaceVariables are done before the getUserSig(),
+ # which may corrupt this parser instance via its wfMsgExt( parsemag ) call-
+
# Signatures
$sigText = $this->getUserSig( $user );
$text = strtr( $text, array(
* validated, ready-to-insert wikitext.
* If you have pre-fetched the nickname or the fancySig option, you can
* specify them here to save a database query.
+ * Do not reuse this parser instance after calling getUserSig(),
+ * as it may have changed if it's the $wgParser.
*
* @param $user User
* @param $nickname String: nickname to use or false to use user's default nickname
* Set up some variables which are usually set up in parse()
* so that an external function can call some class members with confidence
*/
- public function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
+ public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
+ $this->startParse( $title, $options, $outputType, $clearState );
+ }
+
+ private function startParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
$this->setTitle( $title );
- $options->resetUsage();
$this->mOptions = $options;
$this->setOutputType( $outputType );
if ( $clearState ) {
*
* @param $text String: the text to preprocess
* @param $options ParserOptions: options
+ * @param $title Title object or null to use $wgTitle
* @return String
*/
- public function transformMsg( $text, $options ) {
- global $wgTitle;
+ public function transformMsg( $text, $options, $title = null ) {
static $executing = false;
# Guard against infinite recursion
$executing = true;
wfProfileIn( __METHOD__ );
- $text = $this->preprocess( $text, $wgTitle, $options );
+ if ( !$title ) {
+ global $wgTitle;
+ $title = $wgTitle;
+ }
+ if ( !$title ) {
+ # It's not uncommon having a null $wgTitle in scripts. See r80898
+ # Create a ghost title in such case
+ $title = Title::newFromText( 'Dwimmerlaik' );
+ }
+ $text = $this->preprocess( $text, $title, $options );
$executing = false;
wfProfileOut( __METHOD__ );
*/
public function setHook( $tag, $callback ) {
$tag = strtolower( $tag );
+ if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" );
$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
$this->mTagHooks[$tag] = $callback;
if ( !in_array( $tag, $this->mStripList ) ) {
function setTransparentTagHook( $tag, $callback ) {
$tag = strtolower( $tag );
+ if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" );
$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
$this->mTransparentTagHooks[$tag] = $callback;
*/
function setFunctionTagHook( $tag, $callback, $flags ) {
$tag = strtolower( $tag );
+ if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" );
$old = isset( $this->mFunctionTagHooks[$tag] ) ?
$this->mFunctionTagHooks[$tag] : null;
$this->mFunctionTagHooks[$tag] = array( $callback, $flags );
$ig->setParser( $this );
$ig->setHideBadImages();
$ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
- $ig->useSkin( $this->mOptions->getSkin( $this->mTitle ) );
- $ig->mRevisionId = $this->mRevisionId;
if ( isset( $params['showfilename'] ) ) {
$ig->setShowFilename( true );
if ( strpos( $matches[0], '%' ) !== false ) {
$matches[1] = rawurldecode( $matches[1] );
}
- $tp = Title::newFromText( $matches[1] );
+ $tp = Title::newFromText( $matches[1], NS_FILE );
$nt =& $tp;
if ( is_null( $nt ) ) {
# Bogus title. Ignore these so we don't bomb out later.
$html = $this->recursiveTagParse( trim( $label ) );
$ig->add( $nt, $html );
-
- # Only add real images (bug #5586)
- if ( $nt->getNamespace() == NS_FILE ) {
- $this->mOutput->addImage( $nt->getDBkey() );
- }
}
return $ig->toHTML();
}
* @param $title Title
* @param $options String
* @param $holders LinkHolderArray
+ * @return string HTML
*/
function makeImage( $title, $options, $holders = false ) {
# Check if the options text is of the form "options|alt text"
# * text-bottom
$parts = StringUtils::explode( "|", $options );
- $sk = $this->mOptions->getSkin( $this->mTitle );
# Give extensions a chance to select the file revision for us
- $skip = $time = $descQuery = false;
- wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
-
- if ( $skip ) {
- return $sk->link( $title );
- }
+ $time = $sha1 = $descQuery = false;
+ wfRunHooks( 'BeforeParserFetchFileAndTitle',
+ array( $this, &$title, &$time, &$sha1, &$descQuery ) );
+ # Fetch and register the file (file title may be different via hooks)
+ list( $file, $title ) = $this->fetchFileAndTitle( $title, $time, $sha1 );
- # Get the file
- $file = wfFindFile( $title, array( 'time' => $time ) );
# Get parameter map
$handler = $file ? $file->getHandler() : false;
wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
# Linker does the rest
- $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery, $this->mOptions->getThumbSize() );
+ $ret = Linker::makeImageLink2( $title, $file, $params['frame'], $params['handler'],
+ $time, $descQuery, $this->mOptions->getThumbSize() );
# Give the handler a chance to modify the parser object
if ( $handler ) {
return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ) );
}
+ /**
+ * Replace transparent tags in $text with the values given by the callbacks.
+ *
+ * Transparent tag hooks are like regular XML-style tag hooks, except they
+ * operate late in the transformation sequence, on HTML instead of wikitext.
+ */
+ function replaceTransparentTags( $text ) {
+ $matches = array();
+ $elements = array_keys( $this->mTransparentTagHooks );
+ $text = $this->extractTagsAndParams( $elements, $text, $matches, $this->mUniqPrefix );
+
+ foreach ( $matches as $marker => $data ) {
+ list( $element, $content, $params, $tag ) = $data;
+ $tagName = strtolower( $element );
+ if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+ $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) );
+ } else {
+ $output = $tag;
+ }
+ $this->mStripState->addGeneral( $marker, $output );
+ }
+ return $text;
+ }
+
/**
* Break wikitext input into sections, and either pull or replace
* some particular section's text.
* for "replace", the whole page with the section replaced.
*/
private function extractSections( $text, $section, $mode, $newText='' ) {
- global $wgTitle;
- $this->mOptions = new ParserOptions;
- $this->clearState();
- $this->setTitle( $wgTitle ); # not generally used but removes an ugly failure mode
- $this->setOutputType( self::OT_PLAIN );
+ global $wgTitle; # not generally used but removes an ugly failure mode
+ $this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true );
$outText = '';
$frame = $this->getPreprocessor()->newFrame();
if ( $sectionIndex == 0 ) {
# Section zero doesn't nest, level=big
$targetLevel = 1000;
+ if ( !$node ) {
+ # The page definitely exists - we checked that earlier - so it must be blank: see bug #14005
+ return $text;
+ }
} else {
while ( $node ) {
if ( $node->getName() === 'h' ) {
}
/**
- * This function returns $oldtext after the content of the section
+ * This function returns $oldtext after the content of the section
* specified by $section has been replaced with $text.
- *
+ *
* @param $text String: former text of the article
* @param $section Numeric: section identifier
* @param $text String: replacing text
/**
* Accessor for $mDefaultSort
- * Will use the title/prefixed title if none is set
+ * Will use the empty string if none is set.
+ *
+ * This value is treated as a prefix, so the
+ * empty string is equivalent to sorting by
+ * page name.
*
* @return string
*/
if ( $this->mDefaultSort !== false ) {
return $this->mDefaultSort;
} else {
- return $this->mTitle->getCategorySortkey();
+ return '';
}
}
/**
* strip/replaceVariables/unstrip for preprocessor regression testing
*/
- function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
- $this->mOptions = $options;
- $this->clearState();
+ function testSrvus( $text, $title, ParserOptions $options, $outputType = self::OT_HTML ) {
if ( !$title instanceof Title ) {
$title = Title::newFromText( $title );
}
- $this->mTitle = $title;
- $options->resetUsage();
- $this->setOutputType( $outputType );
+ $this->startParse( $title, $options, $outputType, true );
+
$text = $this->replaceVariables( $text );
$text = $this->mStripState->unstripBoth( $text );
$text = Sanitizer::removeHTMLtags( $text );
return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
}
+ /**
+ * Call a callback function on all regions of the given text that are not
+ * inside strip markers, and replace those regions with the return value
+ * of the callback. For example, with input:
+ *
+ * aaa<MARKER>bbb
+ *
+ * This will call the callback function twice, with 'aaa' and 'bbb'. Those
+ * two strings will be replaced with the value returned by the callback in
+ * each case.
+ */
function markerSkipCallback( $s, $callback ) {
$i = 0;
$out = '';
return $out;
}
- function serialiseHalfParsedText( $text ) {
- $data = array();
- $data['text'] = $text;
-
- # First, find all strip markers, and store their
- # data in an array.
- $stripState = new StripState;
- $pos = 0;
- while ( ( $start_pos = strpos( $text, $this->mUniqPrefix, $pos ) )
- && ( $end_pos = strpos( $text, self::MARKER_SUFFIX, $pos ) ) )
- {
- $end_pos += strlen( self::MARKER_SUFFIX );
- $marker = substr( $text, $start_pos, $end_pos-$start_pos );
-
- if ( !empty( $this->mStripState->general->data[$marker] ) ) {
- $replaceArray = $stripState->general;
- $stripText = $this->mStripState->general->data[$marker];
- } elseif ( !empty( $this->mStripState->nowiki->data[$marker] ) ) {
- $replaceArray = $stripState->nowiki;
- $stripText = $this->mStripState->nowiki->data[$marker];
- } else {
- throw new MWException( "Hanging strip marker: '$marker'." );
- }
-
- $replaceArray->setPair( $marker, $stripText );
- $pos = $end_pos;
- }
- $data['stripstate'] = $stripState;
-
- # Now, find all of our links, and store THEIR
- # data in an array! :)
- $links = array( 'internal' => array(), 'interwiki' => array() );
- $pos = 0;
-
- # Internal links
- while ( ( $start_pos = strpos( $text, '<!--LINK ', $pos ) ) ) {
- list( $ns, $trail ) = explode( ':', substr( $text, $start_pos + strlen( '<!--LINK ' ) ), 2 );
-
- $ns = trim( $ns );
- if ( empty( $links['internal'][$ns] ) ) {
- $links['internal'][$ns] = array();
- }
-
- $key = trim( substr( $trail, 0, strpos( $trail, '-->' ) ) );
- $links['internal'][$ns][] = $this->mLinkHolders->internals[$ns][$key];
- $pos = $start_pos + strlen( "<!--LINK $ns:$key-->" );
- }
-
- $pos = 0;
-
- # Interwiki links
- while ( ( $start_pos = strpos( $text, '<!--IWLINK ', $pos ) ) ) {
- $data = substr( $text, $start_pos );
- $key = trim( substr( $data, 0, strpos( $data, '-->' ) ) );
- $links['interwiki'][] = $this->mLinkHolders->interwiki[$key];
- $pos = $start_pos + strlen( "<!--IWLINK $key-->" );
- }
-
- $data['linkholder'] = $links;
-
+ /**
+ * Save the parser state required to convert the given half-parsed text to
+ * HTML. "Half-parsed" in this context means the output of
+ * recursiveTagParse() or internalParse(). This output has strip markers
+ * from replaceVariables (extensionSubstitution() etc.), and link
+ * placeholders from replaceLinkHolders().
+ *
+ * Returns an array which can be serialized and stored persistently. This
+ * array can later be loaded into another parser instance with
+ * unserializeHalfParsedText(). The text can then be safely incorporated into
+ * the return value of a parser hook.
+ */
+ function serializeHalfParsedText( $text ) {
+ wfProfileIn( __METHOD__ );
+ $data = array(
+ 'text' => $text,
+ 'version' => self::HALF_PARSED_VERSION,
+ 'stripState' => $this->mStripState->getSubState( $text ),
+ 'linkHolders' => $this->mLinkHolders->getSubArray( $text )
+ );
+ wfProfileOut( __METHOD__ );
return $data;
}
/**
- * TODO: document
- * @param $data Array
- * @param $intPrefix String unique identifying prefix
+ * Load the parser state given in the $data array, which is assumed to
+ * have been generated by serializeHalfParsedText(). The text contents is
+ * extracted from the array, and its markers are transformed into markers
+ * appropriate for the current Parser instance. This transformed text is
+ * returned, and can be safely included in the return value of a parser
+ * hook.
+ *
+ * If the $data array has been stored persistently, the caller should first
+ * check whether it is still valid, by calling isValidHalfParsedText().
+ *
+ * @param $data Serialized data
* @return String
*/
- function unserialiseHalfParsedText( $data, $intPrefix = null ) {
- if ( !$intPrefix ) {
- $intPrefix = self::getRandomString();
+ function unserializeHalfParsedText( $data ) {
+ if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) {
+ throw new MWException( __METHOD__.': invalid version' );
}
# First, extract the strip state.
- $stripState = $data['stripstate'];
- $this->mStripState->general->merge( $stripState->general );
- $this->mStripState->nowiki->merge( $stripState->nowiki );
-
- # Now, extract the text, and renumber links
- $text = $data['text'];
- $links = $data['linkholder'];
+ $texts = array( $data['text'] );
+ $texts = $this->mStripState->merge( $data['stripState'], $texts );
- # Internal...
- foreach ( $links['internal'] as $ns => $nsLinks ) {
- foreach ( $nsLinks as $key => $entry ) {
- $newKey = $intPrefix . '-' . $key;
- $this->mLinkHolders->internals[$ns][$newKey] = $entry;
-
- $text = str_replace( "<!--LINK $ns:$key-->", "<!--LINK $ns:$newKey-->", $text );
- }
- }
-
- # Interwiki...
- foreach ( $links['interwiki'] as $key => $entry ) {
- $newKey = "$intPrefix-$key";
- $this->mLinkHolders->interwikis[$newKey] = $entry;
-
- $text = str_replace( "<!--IWLINK $key-->", "<!--IWLINK $newKey-->", $text );
- }
+ # Now renumber links
+ $texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts );
# Should be good to go.
- return $text;
- }
-}
-
-/**
- * @todo document, briefly.
- * @ingroup Parser
- */
-class StripState {
- var $general, $nowiki;
-
- function __construct() {
- $this->general = new ReplacementArray;
- $this->nowiki = new ReplacementArray;
- }
-
- function unstripGeneral( $text ) {
- wfProfileIn( __METHOD__ );
- do {
- $oldText = $text;
- $text = $this->general->replace( $text );
- } while ( $text !== $oldText );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- function unstripNoWiki( $text ) {
- wfProfileIn( __METHOD__ );
- do {
- $oldText = $text;
- $text = $this->nowiki->replace( $text );
- } while ( $text !== $oldText );
- wfProfileOut( __METHOD__ );
- return $text;
- }
-
- function unstripBoth( $text ) {
- wfProfileIn( __METHOD__ );
- do {
- $oldText = $text;
- $text = $this->general->replace( $text );
- $text = $this->nowiki->replace( $text );
- } while ( $text !== $oldText );
- wfProfileOut( __METHOD__ );
- return $text;
+ return $texts[0];
}
-}
-
-/**
- * @todo document, briefly.
- * @ingroup Parser
- */
-class OnlyIncludeReplacer {
- var $output = '';
- function replace( $matches ) {
- if ( substr( $matches[1], -1 ) === "\n" ) {
- $this->output .= substr( $matches[1], 0, -1 );
- } else {
- $this->output .= $matches[1];
- }
+ /**
+ * Returns true if the given array, presumed to be generated by
+ * serializeHalfParsedText(), is compatible with the current version of the
+ * parser.
+ *
+ * @param $data Array.
+ */
+ function isValidHalfParsedText( $data ) {
+ return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION;
}
}