var $mTplExpandCache; // empty-frame expansion cache
var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
var $mExpensiveFunctionCount; // number of expensive parser function calls
+ var $mFileCache;
# Temporary
# These are variables reset at least once per parse regardless of $clearState
$this->mVarCache = array();
if ( isset( $conf['preprocessorClass'] ) ) {
$this->mPreprocessorClass = $conf['preprocessorClass'];
+ } elseif ( extension_loaded( 'domxml' ) ) {
+ // PECL extension that conflicts with the core DOM extension (bug 13770)
+ wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
+ $this->mPreprocessorClass = 'Preprocessor_Hash';
} elseif ( extension_loaded( 'dom' ) ) {
$this->mPreprocessorClass = 'Preprocessor_DOM';
} else {
$this->mHeadings = array();
$this->mDoubleUnderscores = array();
$this->mExpensiveFunctionCount = 0;
+ $this->mFileCache = array();
# Fix cloning
if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
$text = Sanitizer::normalizeCharReferences( $text );
- if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
- $text = self::tidy($text);
+ if ( ( $wgUseTidy && $this->mOptions->mTidy ) || $wgAlwaysUseTidy ) {
+ $text = MWTidy::tidy( $text );
} else {
# attempt to sanitize at least some nesting problems
# (bug #2702 and quite a few others)
$this->mStripState->general->setPair( $rnd, $text );
return $rnd;
}
-
+
/**
- * Interface with html tidy, used if $wgUseTidy = true.
- * If tidy isn't able to correct the markup, the original will be
- * returned in all its glory with a warning comment appended.
- *
- * Either the external tidy program or the in-process tidy extension
- * will be used depending on availability. Override the default
- * $wgTidyInternal setting to disable the internal if it's not working.
- *
- * @param string $text Hideous HTML input
- * @return string Corrected HTML output
- * @public
- * @static
+ * Interface with html tidy
+ * @deprecated Use MWTidy::tidy()
*/
- function tidy( $text ) {
- global $wgTidyInternal;
- $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
-' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
-'<head><title>test</title></head><body>'.$text.'</body></html>';
- if( $wgTidyInternal ) {
- $correctedtext = self::internalTidy( $wrappedtext );
- } else {
- $correctedtext = self::externalTidy( $wrappedtext );
- }
- if( is_null( $correctedtext ) ) {
- wfDebug( "Tidy error detected!\n" );
- return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
- }
- return $correctedtext;
- }
-
- /**
- * Spawn an external HTML tidy process and get corrected markup back from it.
- *
- * @private
- * @static
- */
- function externalTidy( $text ) {
- global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
- wfProfileIn( __METHOD__ );
-
- $cleansource = '';
- $opts = ' -utf8';
-
- $descriptorspec = array(
- 0 => array('pipe', 'r'),
- 1 => array('pipe', 'w'),
- 2 => array('file', wfGetNull(), 'a')
- );
- $pipes = array();
- if( function_exists('proc_open') ) {
- $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
- if (is_resource($process)) {
- // Theoretically, this style of communication could cause a deadlock
- // here. If the stdout buffer fills up, then writes to stdin could
- // block. This doesn't appear to happen with tidy, because tidy only
- // writes to stdout after it's finished reading from stdin. Search
- // for tidyParseStdin and tidySaveStdout in console/tidy.c
- fwrite($pipes[0], $text);
- fclose($pipes[0]);
- while (!feof($pipes[1])) {
- $cleansource .= fgets($pipes[1], 1024);
- }
- fclose($pipes[1]);
- proc_close($process);
- }
- }
-
- wfProfileOut( __METHOD__ );
-
- if( $cleansource == '' && $text != '') {
- // Some kind of error happened, so we couldn't get the corrected text.
- // Just give up; we'll use the source text and append a warning.
- return null;
- } else {
- return $cleansource;
- }
- }
-
- /**
- * Use the HTML tidy PECL extension to use the tidy library in-process,
- * saving the overhead of spawning a new process.
- *
- * 'pear install tidy' should be able to compile the extension module.
- *
- * @private
- * @static
- */
- function internalTidy( $text ) {
- global $wgTidyConf, $IP, $wgDebugTidy;
- wfProfileIn( __METHOD__ );
-
- $tidy = new tidy;
- $tidy->parseString( $text, $wgTidyConf, 'utf8' );
- $tidy->cleanRepair();
- if( $tidy->getStatus() == 2 ) {
- // 2 is magic number for fatal error
- // http://www.php.net/manual/en/function.tidy-get-status.php
- $cleansource = null;
- } else {
- $cleansource = tidy_get_output( $tidy );
- }
- if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
- $cleansource .= "<!--\nTidy reports:\n" .
- str_replace( '-->', '-->', $tidy->errorBuffer ) .
- "\n-->";
- }
-
- wfProfileOut( __METHOD__ );
- return $cleansource;
+ public static function tidy( $text ) {
+ wfDeprecated( __METHOD__ );
+ return MWTidy::tidy( $text );
}
/**
$text = $this->doDoubleUnderscore( $text );
$text = $this->doHeadings( $text );
- if($this->mOptions->getUseDynamicDates()) {
+ if( $this->mOptions->getUseDynamicDates() ) {
$df = DateFormatter::getInstance();
$text = $df->reformat( $this->mOptions->getDateFormat(), $text );
}
# replaceInternalLinks may sometimes leave behind
# absolute URLs, which have to be masked to hide them from replaceExternalLinks
- $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
+ $text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
$text = $this->doMagicLinks( $text );
$text = $this->formatHeadings( $text, $isMain );
}
function magicLinkCallback( $m ) {
- if ( isset( $m[1] ) && strval( $m[1] ) !== '' ) {
+ if ( isset( $m[1] ) && $m[1] !== '' ) {
# Skip anchor
return $m[0];
- } elseif ( isset( $m[2] ) && strval( $m[2] ) !== '' ) {
+ } elseif ( isset( $m[2] ) && $m[2] !== '' ) {
# Skip HTML element
return $m[0];
- } elseif ( isset( $m[3] ) && strval( $m[3] ) !== '' ) {
+ } elseif ( isset( $m[3] ) && $m[3] !== '' ) {
# Free external link
return $this->makeFreeExternalLink( $m[0] );
- } elseif ( isset( $m[4] ) && strval( $m[4] ) !== '' ) {
+ } elseif ( isset( $m[4] ) && $m[4] !== '' ) {
# RFC or PMID
if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
$keyword = 'RFC';
$sk = $this->mOptions->getSkin();
$la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
- } elseif ( isset( $m[5] ) && strval( $m[5] ) !== '' ) {
+ } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
# ISBN
$isbn = $m[5];
$num = strtr( $isbn, array(
$text = $this->maybeMakeExternalImage( $url );
if ( $text === false ) {
# Not an image, make a link
- $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+ $text = $sk->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
$pasteurized = self::replaceUnusualEscapes( $url );
if ( $text == '' ) {
# Autonumber if allowed. See bug #5918
if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
- $text = '[' . ++$this->mAutonumber . ']';
+ $langObj = $this->getFunctionLang();
+ $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
$linktype = 'autonumber';
} else {
# Otherwise just use the URL
# 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, $this->mTitle->getNamespace() ) . $dtrail . $trail;
+ $s .= $sk->makeExternalLink( $url, $text, false, $linktype,
+ $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
# Register link in the output object.
# Replace unnecessary URL escape codes with the referenced character
return $s;
}
+ /**
+ * Get an associative array of additional HTML attributes appropriate for a
+ * particular external link. This currently may include rel => nofollow
+ * (depending on configuration, namespace, and the URL's domain) and/or a
+ * target attribute (depending on configuration).
+ *
+ * @param string $url Optional URL, to extract the domain from for rel =>
+ * nofollow if appropriate
+ * @return array Associative array of HTML attributes
+ */
+ function getExternalLinkAttribs( $url = false ) {
+ $attribs = array();
+ global $wgNoFollowLinks, $wgNoFollowNsExceptions;
+ $ns = $this->mTitle->getNamespace();
+ if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) {
+ $attribs['rel'] = 'nofollow';
+
+ global $wgNoFollowDomainExceptions;
+ if ( $wgNoFollowDomainExceptions ) {
+ $bits = wfParseUrl( $url );
+ if ( is_array( $bits ) && isset( $bits['host'] ) ) {
+ foreach ( $wgNoFollowDomainExceptions as $domain ) {
+ if( substr( $bits['host'], -strlen( $domain ) )
+ == $domain ) {
+ unset( $attribs['rel'] );
+ break;
+ }
+ }
+ }
+ }
+ }
+ if ( $this->mOptions->getExternalLinkTarget() ) {
+ $attribs['target'] = $this->mOptions->getExternalLinkTarget();
+ }
+ return $attribs;
+ }
+
+
/**
* Replace unusual URL escape codes with their equivalent characters
* @param string
/**
* make an image if it's allowed, either through the global
- * option or through the exception
+ * option, through the exception, or through the on-wiki whitelist
* @private
*/
function maybeMakeExternalImage( $url ) {
$imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
$imagesexception = !empty($imagesfrom);
$text = false;
+ # $imagesfrom could be either a single string or an array of strings, parse out the latter
+ if( $imagesexception && is_array( $imagesfrom ) ) {
+ $imagematch = false;
+ foreach( $imagesfrom as $match ) {
+ if( strpos( $url, $match ) === 0 ) {
+ $imagematch = true;
+ break;
+ }
+ }
+ } elseif( $imagesexception ) {
+ $imagematch = (strpos( $url, $imagesfrom ) === 0);
+ } else {
+ $imagematch = false;
+ }
if ( $this->mOptions->getAllowExternalImages()
- || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
+ || ( $imagesexception && $imagematch ) ) {
if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
# Image found
$text = $sk->makeExternalImage( $url );
}
}
+ if( !$text && $this->mOptions->getEnableImageWhitelist()
+ && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
+ $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
+ foreach( $whitelist as $entry ) {
+ # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
+ if( strpos( $entry, '#' ) === 0 || $entry === '' )
+ continue;
+ if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
+ # Image matches a whitelist entry
+ $text = $sk->makeExternalImage( $url );
+ break;
+ }
+ }
+ }
return $text;
}
wfProfileOut( __METHOD__."-misc" );
wfProfileIn( __METHOD__."-title" );
$nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
- if( !$nt ) {
+ if( $nt === NULL ) {
$s .= $prefix . '[[' . $line;
wfProfileOut( __METHOD__."-title" );
continue;
if ($might_be_img) { # if this is actually an invalid link
wfProfileIn( __METHOD__."-might_be_img" );
- if ($ns == NS_IMAGE && $noforce) { #but might be an image
+ if ($ns == NS_FILE && $noforce) { #but might be an image
$found = false;
while ( true ) {
#look at the next 'line' to see if we can close it there
}
wfProfileOut( __METHOD__."-interwiki" );
- if ( $ns == NS_IMAGE ) {
+ if ( $ns == NS_FILE ) {
wfProfileIn( __METHOD__."-image" );
if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
# recursively parse links inside the image caption
}
# Self-link checking
- if( $nt->getFragment() === '' && $nt->getNamespace() != NS_SPECIAL ) {
+ if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
$s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
continue;
}
}
- # Special and Media are pseudo-namespaces; no pages actually exist in them
+ # NS_MEDIA is a pseudo-namespace for linking directly to a file
+ # FIXME: Should do batch file existence checks, see comment below
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 ) );
# Cloak with NOPARSE to avoid replacement in replaceExternalLinks
$s .= $prefix . $this->armorLinks( $link ) . $trail;
$this->mOutput->addImage( $nt->getDBkey() );
+ wfProfileOut( __METHOD__."-media" );
continue;
- } elseif( $ns == NS_SPECIAL ) {
- if( SpecialPage::exists( $nt->getDBkey() ) ) {
- $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
- } else {
- $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
- }
- continue;
- } elseif( $ns == NS_IMAGE ) {
- $img = wfFindFile( $nt );
- if( $img ) {
- // Force a blue link if the file exists; may be a remote
- // upload on the shared repository, and we want to see its
- // auto-generated page.
- $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
- $this->mOutput->addLink( $nt );
- continue;
- }
}
- $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
+
+ wfProfileIn( __METHOD__."-always_known" );
+ # Some titles, such as valid special pages or files in foreign repos, should
+ # be shown as bluelinks even though they're not included in the page table
+ #
+ # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
+ # 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 );
+ } else {
+ # Links will be added to the output link list after checking
+ $s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
+ }
+ wfProfileOut( __METHOD__."-always_known" );
}
wfProfileOut( __METHOD__ );
return $holders;
$inBlockElem = true;
}
} else if ( !$inBlockElem && !$this->mInPre ) {
- if ( ' ' == $t{0} and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
+ if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
// pre
if ($this->mLastSection !== 'pre') {
$paragraphStack = false;
$this->mOutput->setFlag( 'vary-revision' );
wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
return $this->getRevisionTimestamp();
+ case 'revisionuser':
+ // Let the edit saving system know we should parse the page
+ // *after* a revision ID has been assigned. This is for null edits.
+ $this->mOutput->setFlag( 'vary-revision' );
+ wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
+ return $this->getRevisionUser();
case 'namespace':
return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
case 'namespacee':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
case 'numberofusers':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+ case 'numberofactiveusers':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::activeUsers() );
case 'numberofpages':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
case 'numberofadmins':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
case 'numberofedits':
return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+ case 'numberofviews':
+ return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() );
case 'currenttimestamp':
return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
case 'localtimestamp':
* @private
*/
function replaceVariables( $text, $frame = false, $argsOnly = false ) {
- # Prevent too big inclusions
- if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
+ # Is there any text? Also, Prevent too big inclusions!
+ if ( strlen( $text ) < 1 || strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
return $text;
}
-
wfProfileIn( __METHOD__ );
if ( $frame === false ) {
* @private
*/
function braceSubstitution( $piece, $frame ) {
- global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
+ global $wgContLang, $wgNonincludableNamespaces;
wfProfileIn( __METHOD__ );
wfProfileIn( __METHOD__.'-setup' );
# Workaround for PHP bug 35229 and similar
if ( !is_callable( $callback ) ) {
- throw new MWException( "Tag hook for $name is not callable\n" );
+ wfProfileOut( __METHOD__ . '-pfunc' );
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Tag hook for $function is not callable\n" );
}
$result = call_user_func_array( $callback, $allArgs );
$found = true;
if($wgContLang->hasVariants() && $title->getArticleID() == 0){
$wgContLang->findVariantLink( $part1, $title, true );
}
- # Do infinite loop check
- if ( !$frame->loopCheck( $title ) ) {
- $found = true;
- $text = "<span class=\"error\">Template loop detected: [[$titleText]]</span>";
- wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
- }
# Do recursion depth check
$limit = $this->mOptions->getMaxTemplateDepth();
if ( $frame->depth >= $limit ) {
$found = true;
- $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>";
+ $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>';
}
}
}
}
$found = true;
}
+
+ # Do infinite loop check
+ # This has to be done after redirect resolution to avoid infinite loops via redirects
+ if ( !$frame->loopCheck( $title ) ) {
+ $found = true;
+ $text = '<span class="error">' . wfMsgForContent( 'parser-template-loop-warning', $titleText ) . '</span>';
+ wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
+ }
wfProfileOut( __METHOD__ . '-loadtpl' );
}
if( $rev ) {
$text = $rev->getText();
} elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
- global $wgLang;
- $message = $wgLang->lcfirst( $title->getText() );
+ global $wgContLang;
+ $message = $wgContLang->lcfirst( $title->getText() );
$text = wfMsgForContentNoTrans( $message );
if( wfEmptyMsg( $message, $text ) ) {
$text = false;
throw new MWException( '<html> extension tag encountered unexpectedly' );
}
case 'nowiki':
+ $content = strtr($content, array('-{' => '-{', '}-' => '}-'));
$output = Xml::escapeTagsOnly( $content );
break;
case 'math':
* Fills $this->mDoubleUnderscores, returns the modified text
*/
function doDoubleUnderscore( $text ) {
+ wfProfileIn( __METHOD__ );
// The position of __TOC__ needs to be recorded
$mw = MagicWord::get( 'toc' );
if( $mw->match( $text ) ) {
} elseif( isset( $this->mDoubleUnderscores['index'] ) ) {
$this->mOutput->setIndexPolicy( 'index' );
}
-
+ wfProfileOut( __METHOD__ );
return $text;
}
* @private
*/
function formatHeadings( $text, $isMain=true ) {
- global $wgMaxTocLevel, $wgContLang;
+ global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds, $wgSectionContainers;
$doNumberHeadings = $this->mOptions->getNumberHeadings();
- if( !$this->mTitle->quickUserCan( 'edit' ) ) {
+ $showEditLink = $this->mOptions->getEditSection();
+
+ // Do not call quickUserCan unless necessary
+ if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) {
$showEditLink = 0;
- } else {
- $showEditLink = $this->mOptions->getEditSection();
}
# Inhibit editsection links if requested in the page
- if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
+ if ( isset( $this->mDoubleUnderscores['noeditsection'] ) || $this->mOptions->getIsPrintable() ) {
$showEditLink = 0;
}
$this->mOutput->setNewSection( true );
}
+ # Allow user to remove the "new section"
+ # link via __NONEWSECTIONLINK__
+ if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) {
+ $this->mOutput->hideNewSection( true );
+ }
+
# if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
# override above conditions and always show TOC above first header
if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
# Save headline for section edit hint before it's escaped
$headlineHint = $safeHeadline;
- $safeHeadline = Sanitizer::escapeId( $safeHeadline );
- # HTML names must be case-insensitively unique (bug 10721)
+
+ if ( $wgEnforceHtmlIds ) {
+ $legacyHeadline = false;
+ $safeHeadline = Sanitizer::escapeId( $safeHeadline,
+ 'noninitial' );
+ } else {
+ # For reverse compatibility, provide an id that's
+ # HTML4-compatible, like we used to.
+ #
+ # It may be worth noting, academically, that it's possible for
+ # the legacy anchor to conflict with a non-legacy headline
+ # anchor on the page. In this case likely the "correct" thing
+ # would be to either drop the legacy anchors or make sure
+ # they're numbered first. However, this would require people
+ # to type in section names like "abc_.D7.93.D7.90.D7.A4"
+ # manually, so let's not bother worrying about it.
+ $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
+ 'noninitial' );
+ $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' );
+
+ if ( $legacyHeadline == $safeHeadline ) {
+ # No reason to have both (in fact, we can't)
+ $legacyHeadline = false;
+ } elseif ( $legacyHeadline != Sanitizer::escapeId(
+ $legacyHeadline, 'xml' ) ) {
+ # The legacy id is invalid XML. We used to allow this, but
+ # there's no reason to do so anymore. Backward
+ # compatibility will fail slightly in this case, but it's
+ # no big deal.
+ $legacyHeadline = false;
+ }
+ }
+
+ # HTML names must be case-insensitively unique (bug 10721). FIXME:
+ # Does this apply to Unicode characters? Because we aren't
+ # handling those here.
$arrayKey = strtolower( $safeHeadline );
+ if ( $legacyHeadline === false ) {
+ $legacyArrayKey = false;
+ } else {
+ $legacyArrayKey = strtolower( $legacyHeadline );
+ }
# count how many in assoc. array so we can track dupes in anchors
- isset( $refers[$arrayKey] ) ? $refers[$arrayKey]++ : $refers[$arrayKey] = 1;
- $refcount[$headlineCount] = $refers[$arrayKey];
+ if ( isset( $refers[$arrayKey] ) ) {
+ $refers[$arrayKey]++;
+ } else {
+ $refers[$arrayKey] = 1;
+ }
+ if ( isset( $refers[$legacyArrayKey] ) ) {
+ $refers[$legacyArrayKey]++;
+ } else {
+ $refers[$legacyArrayKey] = 1;
+ }
# Don't number the heading if it is the only one (looks silly)
if( $doNumberHeadings && count( $matches[3] ) > 1) {
# Create the anchor for linking from the TOC to the section
$anchor = $safeHeadline;
- if($refcount[$headlineCount] > 1 ) {
- $anchor .= '_' . $refcount[$headlineCount];
+ $legacyAnchor = $legacyHeadline;
+ if ( $refers[$arrayKey] > 1 ) {
+ $anchor .= '_' . $refers[$arrayKey];
+ }
+ if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
+ $legacyAnchor .= '_' . $refers[$legacyArrayKey];
}
if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
$toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
} else {
$editlink = '';
}
- $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
+ $head[$headlineCount] = $sk->makeHeadline( $level,
+ $matches['attrib'][$headlineCount], $anchor, $headline,
+ $editlink, $legacyAnchor );
$headlineCount++;
}
$blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
$i = 0;
+ if ( $wgSectionContainers ) {
+ $openDivs = array();
+ }
+
foreach( $blocks as $block ) {
if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) {
# This is the [edit] link that appears for the top block of text when
# Top anchor now in skin
$full = $full.$toc;
}
+
+ # wrap each section in a div if $wgSectionContainers is set to true
+ if ( $wgSectionContainers ) {
+ if( !empty( $head[$i] ) ) { # if there's no next header, then don't try to close out any existing sections here
+ # get the level of the next header section
+ preg_match('/<H([0-6])/i', $head[$i], $hLevelMatches);
+
+ if ( count($hLevelMatches) > 0 ) {
+ $hLevel = $hLevelMatches[1];
+ if ( $i != 0 ) { # we don't have an open div for section 0, so don't try to close it
+ # close any open divs for sections with headers that are <= to the next header level
+ $this->closeSectionContainers( $hLevel, &$currentHLevel, &$full, &$openDivs);
+ }
+ $currentHLevel = $hLevel;
+ }
+ }
+
+ # open the div for the next header, if there is one
+ if ( isset($currentHLevel) && !empty( $head[$i] ) ) {
+ $full .= '<div id="section_' . $i . '_container">';
+ array_push($openDivs, array($currentHLevel, $i));
+ }
+
+ # if we've outputed the last section of the article, close any open divs that are remaining
+ if ( $i == ( count($blocks) - 1) && isset($currentHLevel) ) {
+ $this->closeSectionContainers( $hLevel, &$currentHLevel, &$full, &$openDivs);
+ }
+ }
if( !empty( $head[$i] ) ) {
$full .= $head[$i];
}
}
+ /**
+ * Analyze the header level of the current and next section being parsed to
+ * determine if any already parsed sections need to be closed
+ *
+ * @param string $hLevel the level of the next header to be parsed
+ * @param string $currentHLevel the level of the last parsed header
+ * @param string $full a reference to the string that stores the output of the parser
+ * @param array $openDivs a reference to the array that stores a list of open section containers
+ * @return true
+ */
+ function closeSectionContainers( $hLevel, &$currentHLevel, &$full, &$openDivs) {
+ while ( $hLevel <= $currentHLevel ) {
+ $full .= '</div>';
+ $popped = array_pop($openDivs);
+ if ( count($openDivs) ) {
+ $currentHLevel = $openDivs[count($openDivs) - 1][0];
+ } else {
+ break;
+ }
+ }
+ return true;
+ }
+
/**
* Transform wiki markup when saving a page by doing \r\n -> \n
* conversion, substitting signatures, {{subst:}} templates, etc.
*
* @param string $text the text to transform
* @param Title &$title the Title object for the current article
- * @param User &$user the User object describing the current user
+ * @param User $user the User object describing the current user
* @param ParserOptions $options parsing options
* @param bool $clearState whether to clear the parser state first
* @return string the altered wiki markup
* @public
*/
- function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
+ function preSaveTransform( $text, Title $title, $user, $options, $clearState = true ) {
$this->mOptions = $options;
$this->setTitle( $title );
$this->setOutputType( self::OT_WIKI );
putenv( 'TZ='.$wgLocaltimezone );
$ts = date( 'YmdHis', $unixts );
$tz = date( 'T', $unixts ); # might vary on DST changeover!
+
+ /* Allow translation of timezones trough wiki. date() can return
+ * whatever crap the system uses, localised or not, so we cannot
+ * ship premade translations.
+ */
+ $key = 'timezone-' . strtolower( trim( $tz ) );
+ $value = wfMsgForContent( $key );
+ if ( !wfEmptyMsg( $key, $value ) ) $tz = $value;
+
putenv( 'TZ='.$oldtz );
}
* @return mixed An expanded string, or false if invalid.
*/
function validateSig( $text ) {
- return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+ return( Xml::isWellFormedXmlFragment( $text ) ? $text : false );
}
/**
$content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
$attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
- return wfOpenElement( 'pre', $attribs ) .
+ return Xml::openElement( 'pre', $attribs ) .
Xml::escapeTagsOnly( $content ) .
'</pre>';
}
if ( strpos( $matches[0], '%' ) !== false )
$matches[1] = urldecode( $matches[1] );
- $tp = Title::newFromText( $matches[1]/*, NS_IMAGE*/ );
+ $tp = Title::newFromText( $matches[1]/*, NS_FILE*/ );
$nt =& $tp;
if( is_null( $nt ) ) {
# Bogus title. Ignore these so we don't bomb out later.
$ig->add( $nt, $html );
# Only add real images (bug #5586)
- if ( $nt->getNamespace() == NS_IMAGE ) {
+ if ( $nt->getNamespace() == NS_FILE ) {
$this->mOutput->addImage( $nt->getDBkey() );
}
}
'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
'bottom', 'text-bottom' ),
'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
- 'upright', 'border' ),
+ 'upright', 'border', 'link', 'alt' ),
);
static $internalParamMap;
if ( !$internalParamMap ) {
function makeImage( $title, $options, $holders = false ) {
# Check if the options text is of the form "options|alt text"
# Options are:
- # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
- # * left no resizing, just left align. label is used for alt= only
- # * right same, but right aligned
- # * none same, but not aligned
- # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
- # * center center the image
- # * framed Keep original image size, no magnify-button.
- # * frameless like 'thumb' but without a frame. Keeps user preferences for width
- # * upright reduce width for upright images, rounded to full __0 px
- # * border draw a 1px border around the image
+ # * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang
+ # * left no resizing, just left align. label is used for alt= only
+ # * right same, but right aligned
+ # * none same, but not aligned
+ # * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox
+ # * center center the image
+ # * framed Keep original image size, no magnify-button.
+ # * frameless like 'thumb' but without a frame. Keeps user preferences for width
+ # * upright reduce width for upright images, rounded to full __0 px
+ # * border draw a 1px border around the image
+ # * alt Text for HTML alt attribute (defaults to empty)
# vertical-align values (no % or length right now):
# * baseline
# * sub
return $sk->link( $title );
}
+ # Get the file
+ $imagename = $title->getDBkey();
+ if ( isset( $this->mFileCache[$imagename][$time] ) ) {
+ $file = $this->mFileCache[$imagename][$time];
+ } else {
+ $file = wfFindFile( $title, $time );
+ if ( count( $this->mFileCache ) > 1000 ) {
+ $this->mFileCache = array();
+ }
+ $this->mFileCache[$imagename][$time] = $file;
+ }
# Get parameter map
- $file = wfFindFile( $title, $time );
$handler = $file ? $file->getHandler() : false;
list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
} else {
# Validate internal parameters
switch( $paramName ) {
- case "manualthumb":
- /// @fixme - possibly check validity here?
- /// downstream behavior seems odd with missing manual thumbs.
+ case 'manualthumb':
+ case 'alt':
+ // @fixme - possibly check validity here for
+ // manualthumb? downstream behavior seems odd with
+ // missing manual thumbs.
$validated = true;
+ $value = $this->stripAltText( $value, $holders );
+ break;
+ case 'link':
+ $chars = self::EXT_LINK_URL_CLASS;
+ $prots = $this->mUrlProtocols;
+ if ( $value === '' ) {
+ $paramName = 'no-link';
+ $value = true;
+ $validated = true;
+ } elseif ( preg_match( "/^$prots/", $value ) ) {
+ if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
+ $paramName = 'link-url';
+ $this->mOutput->addExternalLink( $value );
+ $validated = true;
+ }
+ } else {
+ $linkTitle = Title::newFromText( $value );
+ if ( $linkTitle ) {
+ $paramName = 'link-title';
+ $value = $linkTitle;
+ $this->mOutput->addLink( $linkTitle );
+ $validated = true;
+ }
+ }
break;
default:
// Most other things appear to be empty or numeric...
$params['frame']['valign'] = key( $params['vertAlign'] );
}
- # Strip bad stuff out of the alt text
- # We can't just use replaceLinkHoldersText() here, because if this function
- # is called from replaceInternalLinks2(), mLinkHolders won't be up to date.
- if ( $holders ) {
- $alt = $holders->replaceText( $caption );
- } else {
- $alt = $this->replaceLinkHoldersText( $caption );
- }
+ $params['frame']['caption'] = $caption;
- # make sure there are no placeholders in thumbnail attributes
- # that are later expanded to html- so expand them now and
- # remove the tags
- $alt = $this->mStripState->unstripBoth( $alt );
- $alt = Sanitizer::stripAllTags( $alt );
+ $params['frame']['title'] = $this->stripAltText( $caption, $holders );
- $params['frame']['alt'] = $alt;
- $params['frame']['caption'] = $caption;
+ # In the old days, [[Image:Foo|text...]] would set alt text. Later it
+ # came to also set the caption, ordinary text after the image -- which
+ # makes no sense, because that just repeats the text multiple times in
+ # screen readers. It *also* came to set the title attribute.
+ #
+ # Now that we have an alt attribute, we should not set the alt text to
+ # equal the caption: that's worse than useless, it just repeats the
+ # text. This is the framed/thumbnail case. If there's no caption, we
+ # use the unnamed parameter for alt text as well, just for the time be-
+ # ing, if the unnamed param is set and the alt param is not.
+ #
+ # For the future, we need to figure out if we want to tweak this more,
+ # e.g., introducing a title= parameter for the title; ignoring the un-
+ # named parameter entirely for images without a caption; adding an ex-
+ # plicit caption= parameter and preserving the old magic unnamed para-
+ # meter for BC; ...
+ if( $caption !== '' && !isset( $params['frame']['alt'] )
+ && !isset( $params['frame']['framed'] )
+ && !isset( $params['frame']['thumbnail'] )
+ && !isset( $params['frame']['manualthumb'] ) ) {
+ $params['frame']['alt'] = $params['frame']['title'];
+ }
wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
return $ret;
}
+
+ protected function stripAltText( $caption, $holders ) {
+ # Strip bad stuff out of the title (tooltip). We can't just use
+ # replaceLinkHoldersText() here, because if this function is called
+ # from replaceInternalLinks2(), mLinkHolders won't be up-to-date.
+ if ( $holders ) {
+ $tooltip = $holders->replaceText( $caption );
+ } else {
+ $tooltip = $this->replaceLinkHoldersText( $caption );
+ }
+
+ # make sure there are no placeholders in thumbnail attributes
+ # that are later expanded to html- so expand them now and
+ # remove the tags
+ $tooltip = $this->mStripState->unstripBoth( $tooltip );
+ $tooltip = Sanitizer::stripAllTags( $tooltip );
+
+ return $tooltip;
+ }
/**
* Set a flag in the output object indicating that the content is dynamic and
// Output the replacement text
// Add two newlines on -- trailing whitespace in $newText is conventionally
// stripped by the editor, so we need both newlines to restore the paragraph gap
- $outText .= $newText . "\n\n";
+ // Only add trailing whitespace if there is newText
+ if($newText != "") {
+ $outText .= $newText . "\n\n";
+ }
+
while ( $node ) {
$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
$node = $node->getNextSibling();
return $this->mRevisionTimestamp;
}
+ /**
+ * Get the name of the user that edited the last revision
+ */
+ function getRevisionUser() {
+ // if this template is subst: the revision id will be blank,
+ // so just use the current user's name
+ if( $this->mRevisionId ) {
+ $revision = Revision::newFromId( $this->mRevisionId );
+ $revuser = $revision->getUserText();
+ } else {
+ global $wgUser;
+ $revuser = $wgUser->getName();
+ }
+ return $revuser;
+ }
+
/**
* Mutator for $mDefaultSort
*
}
}
+ /**
+ * Accessor for $mDefaultSort
+ * Unlike getDefaultSort(), will return false if none is set
+ *
+ * @return string or false
+ */
+ public function getCustomDefaultSort() {
+ return $this->mDefaultSort;
+ }
+
/**
* Try to guess the section anchor name based on a wikitext fragment
* presumably extracted from a heading, for example "Header" from
}
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;
+
+ return $data;
+ }
+
+ function unserialiseHalfParsedText( $data, $intPrefix = null /* Unique identifying prefix */ ) {
+ if (!$intPrefix)
+ $intPrefix = $this->getRandomString();
+
+ // 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'];
+
+ // 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 );
+ }
+
+ // Should be good to go.
+ return $text;
+ }
}
/**