*/
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Special\SpecialPageFactory;
use Wikimedia\ScopedCallback;
/**
* - Parser::getPreloadText()
* removes <noinclude> sections and <includeonly> tags
*
- * Globals used:
- * object: $wgContLang
- *
* @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
*
* @par Settings:
const TOC_START = '<mw:toc>';
const TOC_END = '</mw:toc>';
+ /** @var int Assume that no output will later be saved this many seconds after parsing */
+ const MAX_TTS = 900;
+
# Persistent:
public $mTagHooks = [];
public $mTransparentTagHooks = [];
public $mImageParams = [];
public $mImageParamsMagicArray = [];
public $mMarkerIndex = 0;
+ /**
+ * @var bool Whether firstCallInit still needs to be called
+ */
public $mFirstCall = true;
# Initialised by initialiseVariables()
*/
protected $mLinkRenderer;
+ /** @var MagicWordFactory */
+ private $magicWordFactory;
+
+ /** @var Language */
+ private $contLang;
+
+ /** @var ParserFactory */
+ private $factory;
+
+ /** @var SpecialPageFactory */
+ private $specialPageFactory;
+
+ /** @var Config */
+ private $siteConfig;
+
/**
- * @param array $conf
+ * @param array $parserConf See $wgParserConf documentation
+ * @param MagicWordFactory|null $magicWordFactory
+ * @param Language|null $contLang Content language
+ * @param ParserFactory|null $factory
+ * @param string|null $urlProtocols As returned from wfUrlProtocols()
+ * @param SpecialPageFactory|null $spFactory
+ * @param Config|null $siteConfig
*/
- public function __construct( $conf = [] ) {
- $this->mConf = $conf;
- $this->mUrlProtocols = wfUrlProtocols();
+ public function __construct(
+ array $parserConf = [], MagicWordFactory $magicWordFactory = null,
+ Language $contLang = null, ParserFactory $factory = null, $urlProtocols = null,
+ SpecialPageFactory $spFactory = null, Config $siteConfig = null
+ ) {
+ $this->mConf = $parserConf;
+ $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
self::EXT_LINK_ADDR .
self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
- if ( isset( $conf['preprocessorClass'] ) ) {
- $this->mPreprocessorClass = $conf['preprocessorClass'];
- } elseif ( defined( 'HPHP_VERSION' ) ) {
- # Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop
+ if ( isset( $parserConf['preprocessorClass'] ) ) {
+ $this->mPreprocessorClass = $parserConf['preprocessorClass'];
+ } elseif ( wfIsHHVM() ) {
+ # Under HHVM Preprocessor_Hash is much faster than Preprocessor_DOM
$this->mPreprocessorClass = Preprocessor_Hash::class;
} elseif ( extension_loaded( 'domxml' ) ) {
# PECL extension that conflicts with the core DOM extension (T15770)
$this->mPreprocessorClass = Preprocessor_Hash::class;
}
wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
+
+ $services = MediaWikiServices::getInstance();
+ $this->magicWordFactory = $magicWordFactory ??
+ $services->getMagicWordFactory();
+
+ $this->contLang = $contLang ?? $services->getContentLanguage();
+
+ $this->factory = $factory ?? $services->getParserFactory();
+ $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
+ $this->siteConfig = $siteConfig ?? MediaWikiServices::getInstance()->getMainConfig();
}
/**
* @private
*/
public function clearState() {
- if ( $this->mFirstCall ) {
- $this->firstCallInit();
- }
+ $this->firstCallInit();
$this->mOutput = new ParserOutput;
$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
$this->mAutonumber = 0;
* Do not call this function recursively.
*
* @param string $text Text we want to parse
+ * @param-taint $text escapes_htmlnoent
* @param Title $title
* @param ParserOptions $options
* @param bool $linestart
* @param bool $clearState
- * @param int $revid Number to pass in {{REVISIONID}}
+ * @param int|null $revid Number to pass in {{REVISIONID}}
* @return ParserOutput A ParserOutput
+ * @return-taint escaped
*/
public function parse(
$text, Title $title, ParserOptions $options,
|| isset( $this->mDoubleUnderscores['notitleconvert'] )
|| $this->mOutput->getDisplayTitle() !== false )
) {
- $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
+ $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
if ( $convruletitle ) {
$this->mOutput->setTitleText( $convruletitle );
} else {
- $titleText = $this->getConverterLanguage()->convertTitle( $title );
+ $titleText = $this->getTargetLanguage()->convertTitle( $title );
$this->mOutput->setTitleText( $titleText );
}
}
# with CSS (T37247)
$class = $this->mOptions->getWrapOutputClass();
if ( $class !== false && !$this->mOptions->getInterfaceMessage() ) {
- $text = Html::rawElement( 'div', [ 'class' => $class ], $text );
+ $this->mOutput->addWrapperDivClass( $class );
}
$this->mOutput->setText( $text );
* @return string
*/
protected function makeLimitReport() {
- global $wgShowHostnames;
-
$maxIncludeSize = $this->mOptions->getMaxIncludeSize();
$cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
$limitReport = "NewPP limit report\n";
- if ( $wgShowHostnames ) {
+ if ( $this->siteConfig->get( 'ShowHostnames' ) ) {
$limitReport .= 'Parsed by ' . wfHostname() . "\n";
}
$limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
// Since we're not really outputting HTML, decode the entities and
// then re-encode the things that need hiding inside HTML comments.
$limitReport = htmlspecialchars_decode( $limitReport );
- // Run deprecated hook
- Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
// Sanitize for comment. Note '‐' in the replacement is U+2010,
// which looks much like the problematic '-'.
// Add on template profiling data in human/machine readable way
$dataByFunc = $this->mProfiler->getFunctionStats();
uasort( $dataByFunc, function ( $a, $b ) {
- return $a['real'] < $b['real']; // descending order
+ return $b['real'] <=> $a['real']; // descending order
} );
$profileReport = [];
foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
$this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
// Add other cache related metadata
- if ( $wgShowHostnames ) {
+ if ( $this->siteConfig->get( 'ShowHostnames' ) ) {
$this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
}
$this->mOutput->setLimitReportData( 'cachereport-timestamp',
* $text are not expanded
*
* @param string $text Text extension wants to have parsed
+ * @param-taint $text escapes_htmlnoent
* @param bool|PPFrame $frame The frame to use for expanding any template variables
* @return string UNSAFE half-parsed HTML
+ * @return-taint escaped
*/
public function recursiveTagParse( $text, $frame = false ) {
// Avoid PHP 7.1 warning from passing $this by reference
* @since 1.25
*
* @param string $text Text extension wants to have parsed
+ * @param-taint $text escapes_htmlnoent
* @param bool|PPFrame $frame The frame to use for expanding any template variables
* @return string Fully parsed HTML
+ * @return-taint escaped
*/
public function recursiveTagParseFully( $text, $frame = false ) {
$text = $this->recursiveTagParse( $text, $frame );
* Also removes comments.
* Do not call this function recursively.
* @param string $text
- * @param Title $title
+ * @param Title|null $title
* @param ParserOptions $options
* @param int|null $revid
* @param bool|PPFrame $frame
/**
* Accessor/mutator for the Title object
*
- * @param Title $x Title object or null to just get the current one
+ * @param Title|null $x Title object or null to just get the current one
* @return Title
*/
public function Title( $x = null ) {
/**
* Accessor/mutator for the ParserOptions object
*
- * @param ParserOptions $x New value or null to just get the current one
+ * @param ParserOptions|null $x New value or null to just get the current one
* @return ParserOptions Current ParserOptions object
*/
public function Options( $x = null ) {
/**
* Get the language object for language conversion
+ * @deprecated since 1.32, just use getTargetLanguage()
* @return Language|null
*/
public function getConverterLanguage() {
return $this->mLinkRenderer;
}
+ /**
+ * Get the MagicWordFactory that this Parser is using
+ *
+ * @since 1.32
+ * @return MagicWordFactory
+ */
+ public function getMagicWordFactory() {
+ return $this->magicWordFactory;
+ }
+
+ /**
+ * Get the content language that this Parser is using
+ *
+ * @since 1.32
+ * @return Language
+ */
+ public function getContentLanguage() {
+ return $this->contLang;
+ }
+
/**
* Replaces all occurrences of HTML-style comments and the given tags
* in the text with a random marker and returns the next text. The output
$line = "</{$last_tag}>{$line}";
}
array_pop( $tr_attributes );
- $outLine = $line . str_repeat( '</dd></dl>', $indent_level );
+ if ( $indent_level > 0 ) {
+ $outLine = rtrim( $line ) . str_repeat( '</dd></dl>', $indent_level );
+ } else {
+ $outLine = $line;
+ }
} elseif ( $first_two === '|-' ) {
# Now we have a table row
$line = preg_replace( '#^\|-+#', '', $line );
# be mistaken as delimiting cell parameters
# Bug T153140: Neither should language converter markup.
if ( preg_match( '/\[\[|-\{/', $cell_data[0] ) === 1 ) {
- $cell = "{$previous}<{$last_tag}>{$cell}";
+ $cell = "{$previous}<{$last_tag}>" . trim( $cell );
} elseif ( count( $cell_data ) == 1 ) {
- $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
+ // Whitespace in cells is trimmed
+ $cell = "{$previous}<{$last_tag}>" . trim( $cell_data[0] );
} else {
$attributes = $this->mStripState->unstripBoth( $cell_data[0] );
$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag );
- $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
+ // Whitespace in cells is trimmed
+ $cell = "{$previous}<{$last_tag}{$attributes}>" . trim( $cell_data[1] );
}
$outLine .= $cell;
* @private
*
* @param string $text The text to parse
+ * @param-taint $text escapes_html
* @param bool $isMain Whether this is being called from the main parse() function
* @param PPFrame|bool $frame A pre-processor frame
*
}
# Clean up special characters, only run once, next-to-last before doBlockLevels
- $fixtags = [
- # French spaces, last one Guillemet-left
- # only if there is something before the space
- '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 ',
- # french spaces, Guillemet-right
- '/(\\302\\253) /' => '\\1 ',
- '/ (!\s*important)/' => ' \\1', # Beware of CSS magic word !important, T13874.
- ];
- $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
+ $text = Sanitizer::armorFrenchSpaces( $text );
$text = $this->doBlockLevels( $text, $linestart );
# The position of the convert() call should not be changed. it
# assumes that the links are all replaced and the only thing left
# is the <nowiki> mark.
- $text = $this->getConverterLanguage()->convert( $text );
+ $text = $this->getTargetLanguage()->convert( $text );
}
}
} else {
# attempt to sanitize at least some nesting problems
# (T4702 and quite a few others)
+ # This code path is buggy and deprecated!
+ wfDeprecated( 'disabling tidy', '1.33' );
$tidyregs = [
# ''Something [http://www.cool.com cool''] -->
# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
/**
* @throws MWException
* @param array $m
- * @return HTML|string
+ * @return string HTML
*/
public function magicLinkCallback( $m ) {
if ( isset( $m[1] ) && $m[1] !== '' ) {
if ( $text === false ) {
# Not an image, make a link
$text = Linker::makeExternalLink( $url,
- $this->getConverterLanguage()->markNoConversion( $url, true ),
+ $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
true, 'free',
$this->getExternalLinkAttribs( $url ), $this->mTitle );
# Register it in the output object...
public function doHeadings( $text ) {
for ( $i = 6; $i >= 1; --$i ) {
$h = str_repeat( '=', $i );
- $text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text );
+ // Trim non-newline whitespace from headings
+ // Using \s* will break for: "==\n===\n" and parse as <h2>=</h2>
+ $text = preg_replace( "/^(?:$h)[ \\t]*(.+?)[ \\t]*(?:$h)\\s*$/m", "<h$i>\\1</h$i>", $text );
}
return $text;
}
$dtrail = '';
- # Set linktype for CSS - if URL==text, link is essentially free
- $linktype = ( $text === $url ) ? 'free' : 'text';
+ # Set linktype for CSS
+ $linktype = 'text';
# No link text, e.g. [http://domain.tld/some.link]
if ( $text == '' ) {
list( $dtrail, $trail ) = Linker::splitTrail( $trail );
}
- $text = $this->getConverterLanguage()->markNoConversion( $text );
+ // Excluding protocol-relative URLs may avoid many false positives.
+ if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
+ $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
+ }
$url = Sanitizer::cleanUrl( $url );
* @since 1.21
* @param string|bool $url Optional URL, to extract the domain from for rel =>
* nofollow if appropriate
- * @param Title $title Optional Title, for wgNoFollowNsExceptions lookups
+ * @param Title|null $title Optional Title, for wgNoFollowNsExceptions lookups
* @return string|null Rel attribute for $url
*/
public static function getExternalLinkRel( $url = false, $title = null ) {
* @return string
*/
public static function normalizeLinkUrl( $url ) {
- # First, make sure unsafe characters are encoded
+ # Test for RFC 3986 IPv6 syntax
+ $scheme = '[a-z][a-z0-9+.-]*:';
+ $userinfo = '(?:[a-z0-9\-._~!$&\'()*+,;=:]|%[0-9a-f]{2})*';
+ $ipv6Host = '\\[((?:[0-9a-f:]|%3[0-A]|%[46][1-6])+)\\]';
+ if ( preg_match( "<^(?:{$scheme})?//(?:{$userinfo}@)?{$ipv6Host}(?:[:/?#].*|)$>i", $url, $m ) &&
+ IP::isValid( rawurldecode( $m[1] ) )
+ ) {
+ $isIPv6 = rawurldecode( $m[1] );
+ } else {
+ $isIPv6 = false;
+ }
+
+ # Make sure unsafe characters are encoded
$url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
function ( $m ) {
return rawurlencode( $m[0] );
$ret = self::normalizeUrlComponent(
substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
+ # Fix IPv6 syntax
+ if ( $isIPv6 !== false ) {
+ $ipv6Host = "%5B({$isIPv6})%5D";
+ $ret = preg_replace(
+ "<^((?:{$scheme})?//(?:{$userinfo}@)?){$ipv6Host}(?=[:/?#]|$)>i",
+ "$1[$2]",
+ $ret
+ );
+ }
+
return $ret;
}
* @private
*/
public function replaceInternalLinks2( &$s ) {
- global $wgExtraInterlanguageLinkPrefixes;
-
static $tc = false, $e1, $e1_img;
# the % is needed to support urlencoded titles as well
if ( !$tc ) {
if ( $useLinkPrefixExtension ) {
# Match the end of a line for a word that's not followed by whitespace,
# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
- global $wgContLang;
- $charset = $wgContLang->linkPrefixCharset();
+ $charset = $this->contLang->linkPrefixCharset();
$e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
}
if (
$iw && $this->mOptions->getInterwikiMagic() && $nottalk && (
Language::fetchLanguageName( $iw, null, 'mw' ) ||
- in_array( $iw, $wgExtraInterlanguageLinkPrefixes )
+ in_array( $iw, $this->siteConfig->get( 'ExtraInterlanguageLinkPrefixes' ) )
)
) {
# T26502: filter duplicates
}
$sortkey = Sanitizer::decodeCharReferences( $sortkey );
$sortkey = str_replace( "\n", '', $sortkey );
- $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
+ $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
continue;
*
* @private
*
- * @param string $index Magic variable identifier as mapped in MagicWord::$mVariableIDs
+ * @param string $index Magic variable identifier as mapped in MagicWordFactory::$mVariableIDs
* @param bool|PPFrame $frame
*
* @throws MWException
* @return string
*/
public function getVariableValue( $index, $frame = false ) {
- global $wgContLang, $wgSitename, $wgServer, $wgServerName;
- global $wgArticlePath, $wgScriptPath, $wgStylePath;
-
if ( is_null( $this->mTitle ) ) {
// If no title set, bad things are going to happen
// later. Title should always be set since this
$this->mOutput->setFlag( 'vary-revision' );
wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
}
- $value = $pageid ? $pageid : null;
+ $value = $pageid ?: null;
break;
case 'revisionid':
# Let the edit saving system know we should parse the page
$this->mOutput->setFlag( 'vary-revision-id' );
wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
$value = $this->mRevisionId;
- if ( !$value && $this->mOptions->getSpeculativeRevIdCallback() ) {
- $value = call_user_func( $this->mOptions->getSpeculativeRevIdCallback() );
- $this->mOutput->setSpeculativeRevIdUsed( $value );
+
+ if ( !$value ) {
+ $rev = $this->getRevisionObject();
+ if ( $rev ) {
+ $value = $rev->getId();
+ }
+ }
+
+ if ( !$value ) {
+ $value = $this->mOptions->getSpeculativeRevId();
+ if ( $value ) {
+ $this->mOutput->setSpeculativeRevIdUsed( $value );
+ }
}
break;
case 'revisionday':
- # 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__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
- $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+ $value = (int)$this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
break;
case 'revisionday2':
- # 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__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
- $value = substr( $this->getRevisionTimestamp(), 6, 2 );
+ $value = $this->getRevisionTimestampSubstring( 6, 2, self::MAX_TTS, $index );
break;
case 'revisionmonth':
- # 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__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
- $value = substr( $this->getRevisionTimestamp(), 4, 2 );
+ $value = $this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
break;
case 'revisionmonth1':
- # 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__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
- $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+ $value = (int)$this->getRevisionTimestampSubstring( 4, 2, self::MAX_TTS, $index );
break;
case 'revisionyear':
- # 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__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
- $value = substr( $this->getRevisionTimestamp(), 0, 4 );
+ $value = $this->getRevisionTimestampSubstring( 0, 4, self::MAX_TTS, $index );
break;
case 'revisiontimestamp':
# Let the edit saving system know we should parse the page
$value = $this->getRevisionSize();
break;
case 'namespace':
- $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ $value = str_replace( '_', ' ',
+ $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
break;
case 'namespacee':
- $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
break;
case 'namespacenumber':
$value = $this->mTitle->getNamespace();
$value = SpecialVersion::getVersion();
break;
case 'articlepath':
- return $wgArticlePath;
+ return $this->siteConfig->get( 'ArticlePath' );
case 'sitename':
- return $wgSitename;
+ return $this->siteConfig->get( 'Sitename' );
case 'server':
- return $wgServer;
+ return $this->siteConfig->get( 'Server' );
case 'servername':
- return $wgServerName;
+ return $this->siteConfig->get( 'ServerName' );
case 'scriptpath':
- return $wgScriptPath;
+ return $this->siteConfig->get( 'ScriptPath' );
case 'stylepath':
- return $wgStylePath;
+ return $this->siteConfig->get( 'StylePath' );
case 'directionmark':
return $pageLang->getDirMark();
case 'contentlanguage':
- global $wgLanguageCode;
- return $wgLanguageCode;
+ return $this->siteConfig->get( 'LanguageCode' );
case 'pagelanguage':
$value = $pageLang->getCode();
break;
return $value;
}
+ /**
+ * @param int $start
+ * @param int $len
+ * @param int $mtts Max time-till-save; sets vary-revision if result might change by then
+ * @param string $variable Parser variable name
+ * @return string
+ */
+ private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
+ # Get the timezone-adjusted timestamp to be used for this revision
+ $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
+ # Possibly set vary-revision if there is not yet an associated revision
+ if ( !$this->getRevisionObject() ) {
+ # Get the timezone-adjusted timestamp $mtts seconds in the future
+ $resThen = substr(
+ $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
+ $start,
+ $len
+ );
+
+ if ( $resNow !== $resThen ) {
+ # 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__ . ": $variable used, setting vary-revision...\n" );
+ }
+ }
+
+ return $resNow;
+ }
+
/**
* initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
*
* @private
*/
public function initialiseVariables() {
- $variableIDs = MagicWord::getVariableIDs();
- $substIDs = MagicWord::getSubstIDs();
+ $variableIDs = $this->magicWordFactory->getVariableIDs();
+ $substIDs = $this->magicWordFactory->getSubstIDs();
- $this->mVariables = new MagicWordArray( $variableIDs );
- $this->mSubstWords = new MagicWordArray( $substIDs );
+ $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
+ $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
}
/**
* 'expansion-depth-exceeded-category')
* @param string|int|null $current Current value
* @param string|int|null $max Maximum allowed, when an explicit limit has been
- * exceeded, provide the values (optional)
+ * exceeded, provide the values (optional)
*/
public function limitationWarn( $limitationType, $current = '', $max = '' ) {
# does no harm if $current and $max are present but are unnecessary for the message
$id = $this->mVariables->matchStartToEnd( $part1 );
if ( $id !== false ) {
$text = $this->getVariableValue( $id, $frame );
- if ( MagicWord::getCacheTTL( $id ) > -1 ) {
- $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
+ if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
+ $this->mOutput->updateCacheExpiry(
+ $this->magicWordFactory->getCacheTTL( $id ) );
}
$found = true;
}
# MSG, MSGNW and RAW
if ( !$found ) {
# Check for MSGNW:
- $mwMsgnw = MagicWord::get( 'msgnw' );
+ $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
$nowiki = true;
} else {
# Remove obsolete MSG:
- $mwMsg = MagicWord::get( 'msg' );
+ $mwMsg = $this->magicWordFactory->get( 'msg' );
$mwMsg->matchStartAndRemove( $part1 );
}
# Check for RAW:
- $mwRaw = MagicWord::get( 'raw' );
+ $mwRaw = $this->magicWordFactory->get( 'raw' );
if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
$forceRawInterwiki = true;
}
for ( $i = 0; $i < $argsLength; $i++ ) {
$funcArgs[] = $args->item( $i );
}
- try {
- $result = $this->callParserFunction( $frame, $func, $funcArgs );
- } catch ( Exception $ex ) {
- throw $ex;
- }
+
+ $result = $this->callParserFunction( $frame, $func, $funcArgs );
// Extract any forwarded flags
if ( isset( $result['title'] ) ) {
if ( $title ) {
$titleText = $title->getPrefixedText();
# Check for language variants if the template is not found
- if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
- $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
+ if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
+ $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
}
# Do recursion depth check
$limit = $this->mOptions->getMaxTemplateDepth();
&& $this->mOptions->getAllowSpecialInclusion()
&& $this->ot['html']
) {
- $specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
+ $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
// Pass the template arguments as URL parameters.
// "uselang" will have no effect since the Language object
// is forced to the one defined in ParserOptions.
$context->setUser( User::newFromName( '127.0.0.1', false ) );
}
$context->setLanguage( $this->mOptions->getUserLangObj() );
- $ret = SpecialPageFactory::capturePath(
- $title, $context, $this->getLinkRenderer() );
+ $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
if ( $ret ) {
$text = $context->getOutput()->getHTML();
$this->mOutput->addOutputPageMetadata( $context->getOutput() );
* @return array
*/
public function callParserFunction( $frame, $function, array $args = [] ) {
- global $wgContLang;
-
# Case sensitive functions
if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
$function = $this->mFunctionSynonyms[1][$function];
} else {
# Case insensitive functions
- $function = $wgContLang->lc( $function );
+ $function = $this->contLang->lc( $function );
if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
$function = $this->mFunctionSynonyms[0][$function];
} else {
}
}
- $result = call_user_func_array( $callback, $allArgs );
+ $result = $callback( ...$allArgs );
# The interface for function hooks allows them to return a wikitext
# string or an array containing the string and any flags. This mungs
if ( is_string( $stuff['text'] ) ) {
$text = strtr( $text, "\x7f", "?" );
}
- $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
+ $finalTitle = $stuff['finalTitle'] ?? $title;
if ( isset( $stuff['deps'] ) ) {
foreach ( $stuff['deps'] as $dep ) {
$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
$rev_id = $rev ? $rev->getId() : 0;
# If there is no current revision, there is no page
if ( $id === false && !$rev ) {
- $linkCache = LinkCache::singleton();
+ $linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addBadLinkObj( $title );
}
break;
}
} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
- global $wgContLang;
- $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
+ $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
+ lcfirst( $title->getText() ) )->inContentLanguage();
if ( !$message->exists() ) {
$text = false;
break;
* @param Title $title
* @param array $options Array of options to RepoGroup::findFile
* @return File|bool
+ * @deprecated since 1.32, use fetchFileAndTitle instead
*/
public function fetchFile( $title, $options = [] ) {
+ wfDeprecated( __METHOD__, '1.32' );
return $this->fetchFileAndTitle( $title, $options )[0];
}
* Transclude an interwiki link.
*
* @param Title $title
- * @param string $action
+ * @param string $action Usually one of (raw, render)
*
* @return string
*/
public function interwikiTransclude( $title, $action ) {
- global $wgEnableScaryTranscluding;
-
- if ( !$wgEnableScaryTranscluding ) {
+ if ( !$this->siteConfig->get( 'EnableScaryTranscluding' ) ) {
return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
}
$url = $title->getFullURL( [ 'action' => $action ] );
-
- if ( strlen( $url ) > 255 ) {
+ if ( strlen( $url ) > 1024 ) {
return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
}
- return $this->fetchScaryTemplateMaybeFromCache( $url );
- }
- /**
- * @param string $url
- * @return mixed|string
- */
- public function fetchScaryTemplateMaybeFromCache( $url ) {
- global $wgTranscludeCacheExpiry;
- $dbr = wfGetDB( DB_REPLICA );
- $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
- $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
- [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
- if ( $obj ) {
- return $obj->tc_contents;
- }
-
- $req = MWHttpRequest::factory( $url, [], __METHOD__ );
- $status = $req->execute(); // Status object
- if ( $status->isOK() ) {
- $text = $req->getContent();
- } elseif ( $req->getStatus() != 200 ) {
+ $wikiId = $title->getTransWikiID(); // remote wiki ID or false
+
+ $fname = __METHOD__;
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
+ $data = $cache->getWithSetCallback(
+ $cache->makeGlobalKey(
+ 'interwiki-transclude',
+ ( $wikiId !== false ) ? $wikiId : 'external',
+ sha1( $url )
+ ),
+ $this->siteConfig->get( 'TranscludeCacheExpiry' ),
+ function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
+ $req = MWHttpRequest::factory( $url, [], $fname );
+
+ $status = $req->execute(); // Status object
+ if ( !$status->isOK() ) {
+ $ttl = $cache::TTL_UNCACHEABLE;
+ } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
+ $ttl = min( $cache::TTL_LAGGED, $ttl );
+ }
+
+ return [
+ 'text' => $status->isOK() ? $req->getContent() : null,
+ 'code' => $req->getStatus()
+ ];
+ },
+ [
+ 'checkKeys' => ( $wikiId !== false )
+ ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
+ : [],
+ 'pcGroup' => 'interwiki-transclude:5',
+ 'pcTTL' => $cache::TTL_PROC_LONG
+ ]
+ );
+
+ if ( is_string( $data['text'] ) ) {
+ $text = $data['text'];
+ } elseif ( $data['code'] != 200 ) {
// Though we failed to fetch the content, this status is useless.
- return wfMessage( 'scarytranscludefailed-httpstatus' )
- ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
+ $text = wfMessage( 'scarytranscludefailed-httpstatus' )
+ ->params( $url, $data['code'] )->inContentLanguage()->text();
} else {
- return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
+ $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
}
- $dbw = wfGetDB( DB_MASTER );
- $dbw->replace( 'transcache', [ 'tc_url' ], [
- 'tc_url' => $url,
- 'tc_time' => $dbw->timestamp( time() ),
- 'tc_contents' => $text
- ] );
return $text;
}
*/
public function doDoubleUnderscore( $text ) {
# The position of __TOC__ needs to be recorded
- $mw = MagicWord::get( 'toc' );
+ $mw = $this->magicWordFactory->get( 'toc' );
if ( $mw->match( $text ) ) {
$this->mShowToc = true;
$this->mForceTocPosition = true;
}
# Now match and remove the rest of them
- $mwa = MagicWord::getDoubleUnderscoreArray();
+ $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
$this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
* @private
*/
public function formatHeadings( $text, $origText, $isMain = true ) {
- global $wgMaxTocLevel;
-
# Inhibit editsection links if requested in the page
if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
$maybeShowEditLink = false;
# Get all headlines for numbering them and adding funky stuff like [edit]
# links - this is for later, but we need the number of headlines right now
+ # NOTE: white space in headings have been trimmed in doHeadings. They shouldn't
+ # be trimmed here since whitespace in HTML headings is significant.
$matches = [];
$numMatches = preg_match_all(
- '/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i',
+ '/<H(?P<level>[1-6])(?P<attrib>.*?>)(?P<header>[\s\S]*?)<\/H[1-6] *>/i',
$text,
$matches
);
$headlines = $numMatches !== false ? $matches[3] : [];
+ $maxTocLevel = $this->siteConfig->get( 'MaxTocLevel' );
foreach ( $headlines as $headline ) {
$isTemplate = false;
$titleText = false;
# Increase TOC level
$toclevel++;
$sublevelCount[$toclevel] = 0;
- if ( $toclevel < $wgMaxTocLevel ) {
+ if ( $toclevel < $maxTocLevel ) {
$prevtoclevel = $toclevel;
$toc .= Linker::tocIndent();
$numVisible++;
if ( $i == 0 ) {
$toclevel = 1;
}
- if ( $toclevel < $wgMaxTocLevel ) {
- if ( $prevtoclevel < $wgMaxTocLevel ) {
+ if ( $toclevel < $maxTocLevel ) {
+ if ( $prevtoclevel < $maxTocLevel ) {
# Unindent only if the previous toc level was shown :p
$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel );
$prevtoclevel = $toclevel;
}
} else {
# No change in level, end TOC line
- if ( $toclevel < $wgMaxTocLevel ) {
+ if ( $toclevel < $maxTocLevel ) {
$toc .= Linker::tocLineEnd();
}
}
# Avoid insertion of weird stuff like <math> by expanding the relevant sections
$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
+ # Remove any <style> or <script> tags (T198618)
+ $safeHeadline = preg_replace(
+ '#<(style|script)(?: [^>]*[^>/])?>.*?</\1>#is',
+ '',
+ $safeHeadline
+ );
+
# Strip out HTML (first regex removes any tag not allowed)
# Allowed tags are:
# * <sup> and <sub> (T10393)
) . ' ' . $headline;
}
- if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
+ if ( $enoughToc && ( !isset( $maxTocLevel ) || $toclevel < $maxTocLevel ) ) {
$toc .= Linker::tocLine( $linkAnchor, $tocline,
$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
}
}
if ( $enoughToc ) {
- if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
+ if ( $prevtoclevel > 0 && $prevtoclevel < $maxTocLevel ) {
$toc .= Linker::tocUnindent( $prevtoclevel - 1 );
}
$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() );
* @return string
*/
private function pstPass2( $text, $user ) {
- global $wgContLang;
-
- # Note: This is the timestamp saved as hardcoded wikitext to
- # the database, we use $wgContLang here in order to give
- # everyone the same signature and use the default one rather
- # than the one selected in each user's preferences.
- # (see also T14815)
+ # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
+ # $this->contLang here in order to give everyone the same signature and use the default one
+ # rather than the one selected in each user's preferences. (see also T14815)
$ts = $this->mOptions->getTimestamp();
$timestamp = MWTimestamp::getLocalInstance( $ts );
$ts = $timestamp->format( 'YmdHis' );
$tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
- $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
+ $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
# Variable replacement
# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
* @return string
*/
public function getUserSig( &$user, $nickname = false, $fancySig = null ) {
- global $wgMaxSigChars;
-
$username = $user->getName();
# If not given, retrieve from the user object.
$nickname = $nickname == null ? $username : $nickname;
- if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
+ if ( mb_strlen( $nickname ) > $this->siteConfig->get( 'MaxSigChars' ) ) {
$nickname = $username;
wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
} elseif ( $fancySig !== false ) {
# @todo FIXME: Regex doesn't respect extension tags or nowiki
# => Move this logic to braceSubstitution()
- $substWord = MagicWord::get( 'subst' );
+ $substWord = $this->magicWordFactory->get( 'subst' );
$substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
$substText = '{{' . $substWord->getSynonym( 0 );
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;
+ $oldVal = $this->mTagHooks[$tag] ?? null;
$this->mTagHooks[$tag] = $callback;
if ( !in_array( $tag, $this->mStripList ) ) {
$this->mStripList[] = $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;
+ $oldVal = $this->mTransparentTagHooks[$tag] ?? null;
$this->mTransparentTagHooks[$tag] = $callback;
return $oldVal;
* @return string|callable The old callback function for this name, if any
*/
public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
- global $wgContLang;
-
$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
$this->mFunctionHooks[$id] = [ $callback, $flags ];
# Add to function cache
- $mw = MagicWord::get( $id );
+ $mw = $this->magicWordFactory->get( $id );
if ( !$mw ) {
throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
}
foreach ( $synonyms as $syn ) {
# Case
if ( !$sensitive ) {
- $syn = $wgContLang->lc( $syn );
+ $syn = $this->contLang->lc( $syn );
}
# Add leading hash
if ( !( $flags & self::SFH_NO_HASH ) ) {
* @return array
*/
public function getFunctionHooks() {
+ $this->firstCallInit();
return array_keys( $this->mFunctionHooks );
}
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;
+ $old = $this->mFunctionTagHooks[$tag] ?? null;
$this->mFunctionTagHooks[$tag] = [ $callback, $flags ];
if ( !in_array( $tag, $this->mStripList ) ) {
unset( $paramMap['img_width'] );
}
- $mwArray = new MagicWordArray( array_keys( $paramMap ) );
+ $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
$label = '';
$alt = '';
$alt = $this->stripAltText( $match, false );
break;
case 'gallery-internal-link':
- $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
- $chars = self::EXT_LINK_URL_CLASS;
- $addr = self::EXT_LINK_ADDR;
- $prots = $this->mUrlProtocols;
- // check to see if link matches an absolute url, if not then it must be a wiki link.
+ $linkValue = $this->stripAltText( $match, false );
if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
// Result of LanguageConverter::markNoConversion
// invoked on an external link.
$linkValue = substr( $linkValue, 4, -2 );
}
- if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
- $link = $linkValue;
- $this->mOutput->addExternalLink( $link );
- } else {
- $localLinkTitle = Title::newFromText( $linkValue );
- if ( $localLinkTitle !== null ) {
- $this->mOutput->addLink( $localLinkTitle );
- $link = $localLinkTitle->getLinkURL();
- }
+ list( $type, $target ) = $this->parseLinkParameter( $linkValue );
+ if ( $type === 'link-url' ) {
+ $link = $target;
+ $this->mOutput->addExternalLink( $target );
+ } elseif ( $type === 'link-title' ) {
+ $link = $target->getLinkURL();
+ $this->mOutput->addLink( $target );
}
break;
default:
} else {
// Guess not, consider it as caption.
wfDebug( "$parameterMatch failed parameter validation\n" );
- $label = '|' . $parameterMatch;
+ $label = $parameterMatch;
}
}
} else {
// Last pipe wins.
- $label = '|' . $parameterMatch;
+ $label = $parameterMatch;
}
}
- // Remove the pipe.
- $label = substr( $label, 1 );
}
$ig->add( $title, $label, $alt, $link, $handlerOptions );
}
}
$this->mImageParams[$handlerClass] = $paramMap;
- $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
+ $this->mImageParamsMagicArray[$handlerClass] =
+ $this->magicWordFactory->newArray( array_keys( $paramMap ) );
}
return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
}
# * bottom
# * text-bottom
+ global $wgMediaInTargetLanguage;
+
# Protect LanguageConverter markup when splitting into parts
$parts = StringUtils::delimiterExplode(
'-{', '}-', '|', $options, true /* allow nesting */
$value = $this->stripAltText( $value, $holders );
break;
case 'link':
- $chars = self::EXT_LINK_URL_CLASS;
- $addr = self::EXT_LINK_ADDR;
- $prots = $this->mUrlProtocols;
- if ( $value === '' ) {
- $paramName = 'no-link';
- $value = true;
+ list( $paramName, $value ) =
+ $this->parseLinkParameter(
+ $this->stripAltText( $value, $holders )
+ );
+ if ( $paramName ) {
$validated = true;
- } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
- if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
- $paramName = 'link-url';
- $this->mOutput->addExternalLink( $value );
+ if ( $paramName === 'no-link' ) {
+ $value = true;
+ }
+ if ( $paramName === 'link-url' ) {
if ( $this->mOptions->getExternalLinkTarget() ) {
$params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
}
- $validated = true;
- }
- } else {
- $linkTitle = Title::newFromText( $value );
- if ( $linkTitle ) {
- $paramName = 'link-title';
- $value = $linkTitle;
- $this->mOutput->addLink( $linkTitle );
- $validated = true;
}
}
break;
# Use the "caption" for the tooltip text
$params['frame']['title'] = $this->stripAltText( $caption, $holders );
}
+ if ( $wgMediaInTargetLanguage ) {
+ $params['handler']['targetlang'] = $this->getTargetLanguage()->getCode();
+ }
Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] );
# Linker does the rest
- $time = isset( $options['time'] ) ? $options['time'] : false;
+ $time = $options['time'] ?? false;
$ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'],
$time, $descQuery, $this->mOptions->getThumbSize() );
return $ret;
}
+ /**
+ * Parse the value of 'link' parameter in image syntax (`[[File:Foo.jpg|link=<value>]]`).
+ *
+ * Adds an entry to appropriate link tables.
+ *
+ * @since 1.32
+ * @return array of `[ type, target ]`, where:
+ * - `type` is one of:
+ * - `null`: Given value is not a valid link target, use default
+ * - `'no-link'`: Given value is empty, do not generate a link
+ * - `'link-url'`: Given value is a valid external link
+ * - `'link-title'`: Given value is a valid internal link
+ * - `target` is:
+ * - When `type` is `null` or `'no-link'`: `false`
+ * - When `type` is `'link-url'`: URL string corresponding to given value
+ * - When `type` is `'link-title'`: Title object corresponding to given value
+ */
+ public function parseLinkParameter( $value ) {
+ $chars = self::EXT_LINK_URL_CLASS;
+ $addr = self::EXT_LINK_ADDR;
+ $prots = $this->mUrlProtocols;
+ $type = null;
+ $target = false;
+ if ( $value === '' ) {
+ $type = 'no-link';
+ } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
+ if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
+ $this->mOutput->addExternalLink( $value );
+ $type = 'link-url';
+ $target = $value;
+ }
+ } else {
+ $linkTitle = Title::newFromText( $value );
+ if ( $linkTitle ) {
+ $this->mOutput->addLink( $linkTitle );
+ $type = 'link-title';
+ $target = $linkTitle;
+ }
+ }
+ return [ $type, $target ];
+ }
+
/**
* @param string $caption
* @param LinkHolderArray|bool $holders
# that are later expanded to html- so expand them now and
# remove the tags
$tooltip = $this->mStripState->unstripBoth( $tooltip );
+ # Compatibility hack! In HTML certain entity references not terminated
+ # by a semicolon are decoded (but not if we're in an attribute; that's
+ # how link URLs get away without properly escaping & in queries).
+ # But wikitext has always required semicolon-termination of entities,
+ # so encode & where needed to avoid decode of semicolon-less entities.
+ # See T209236 and
+ # https://www.w3.org/TR/html5/syntax.html#named-character-references
+ # T210437 discusses moving this workaround to Sanitizer::stripAllTags.
+ $tooltip = preg_replace( "/
+ & # 1. entity prefix
+ (?= # 2. followed by:
+ (?: # a. one of the legacy semicolon-less named entities
+ A(?:Elig|MP|acute|circ|grave|ring|tilde|uml)|
+ C(?:OPY|cedil)|E(?:TH|acute|circ|grave|uml)|
+ GT|I(?:acute|circ|grave|uml)|LT|Ntilde|
+ O(?:acute|circ|grave|slash|tilde|uml)|QUOT|REG|THORN|
+ U(?:acute|circ|grave|uml)|Yacute|
+ a(?:acute|c(?:irc|ute)|elig|grave|mp|ring|tilde|uml)|brvbar|
+ c(?:cedil|edil|urren)|cent(?!erdot;)|copy(?!sr;)|deg|
+ divide(?!ontimes;)|e(?:acute|circ|grave|th|uml)|
+ frac(?:1(?:2|4)|34)|
+ gt(?!c(?:c|ir)|dot|lPar|quest|r(?:a(?:pprox|rr)|dot|eq(?:less|qless)|less|sim);)|
+ i(?:acute|circ|excl|grave|quest|uml)|laquo|
+ lt(?!c(?:c|ir)|dot|hree|imes|larr|quest|r(?:Par|i(?:e|f|));)|
+ m(?:acr|i(?:cro|ddot))|n(?:bsp|tilde)|
+ not(?!in(?:E|dot|v(?:a|b|c)|)|ni(?:v(?:a|b|c)|);)|
+ o(?:acute|circ|grave|rd(?:f|m)|slash|tilde|uml)|
+ p(?:lusmn|ound)|para(?!llel;)|quot|r(?:aquo|eg)|
+ s(?:ect|hy|up(?:1|2|3)|zlig)|thorn|times(?!b(?:ar|)|d;)|
+ u(?:acute|circ|grave|ml|uml)|y(?:acute|en|uml)
+ )
+ (?:[^;]|$)) # b. and not followed by a semicolon
+ # S = study, for efficiency
+ /Sx", '&', $tooltip );
$tooltip = Sanitizer::stripAllTags( $tooltip );
return $tooltip;
* @return array
*/
public function getTags() {
+ $this->firstCallInit();
return array_merge(
array_keys( $this->mTransparentTagHooks ),
array_keys( $this->mTagHooks ),
);
}
+ /**
+ * @since 1.32
+ * @return array
+ */
+ public function getFunctionSynonyms() {
+ $this->firstCallInit();
+ return $this->mFunctionSynonyms;
+ }
+
+ /**
+ * @since 1.32
+ * @return string
+ */
+ public function getUrlProtocols() {
+ return $this->mUrlProtocols;
+ }
+
/**
* Replace transparent tags in $text with the values given by the callbacks.
*
if ( !is_null( $this->mRevisionObject ) ) {
return $this->mRevisionObject;
}
- if ( is_null( $this->mRevisionId ) ) {
- return null;
- }
+ // NOTE: try to get the RevisionObject even if mRevisionId is null.
+ // This is useful when parsing revision that has not yet been saved.
+ // However, if we get back a saved revision even though we are in
+ // preview mode, we'll have to ignore it, see below.
+ // NOTE: This callback may be used to inject an OLD revision that was
+ // already loaded, so "current" is a bit of a misnomer. We can't just
+ // skip it if mRevisionId is set.
$rev = call_user_func(
$this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this
);
- # If the parse is for a new revision, then the callback should have
- # already been set to force the object and should match mRevisionId.
- # If not, try to fetch by mRevisionId for sanity.
- if ( $rev && $rev->getId() != $this->mRevisionId ) {
+ if ( $this->mRevisionId === null && $rev && $rev->getId() ) {
+ // We are in preview mode (mRevisionId is null), and the current revision callback
+ // returned an existing revision. Ignore it and return null, it's probably the page's
+ // current revision, which is not what we want here. Note that we do want to call the
+ // callback to allow the unsaved revision to be injected here, e.g. for
+ // self-transclusion previews.
+ return null;
+ }
+
+ // If the parse is for a new revision, then the callback should have
+ // already been set to force the object and should match mRevisionId.
+ // If not, try to fetch by mRevisionId for sanity.
+ if ( $this->mRevisionId && $rev && $rev->getId() != $this->mRevisionId ) {
$rev = Revision::newFromId( $this->mRevisionId );
}
*/
public function getRevisionTimestamp() {
if ( is_null( $this->mRevisionTimestamp ) ) {
- global $wgContLang;
-
$revObject = $this->getRevisionObject();
$timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
# Since this value will be saved into the parser cache, served
# to other users, and potentially even used inside links and such,
# it needs to be consistent for all visitors.
- $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
-
+ $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
}
return $this->mRevisionTimestamp;
}
return '#' . Sanitizer::escapeIdForLink( $sectionName );
}
- private static function makeLegacyAnchor( $sectionName ) {
- global $wgFragmentMode;
- if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
+ private function makeLegacyAnchor( $sectionName ) {
+ $fragmentMode = $this->siteConfig->get( 'FragmentMode' );
+ if ( isset( $fragmentMode[1] ) && $fragmentMode[1] === 'legacy' ) {
// ForAttribute() and ForLink() are the same for legacy encoding
$id = Sanitizer::escapeIdForAttribute( $sectionName, Sanitizer::ID_FALLBACK );
} else {
# Strip out wikitext links(they break the anchor)
$text = $this->stripSectionName( $text );
$sectionName = self::getSectionNameFromStrippedText( $text );
- return self::makeLegacyAnchor( $sectionName );
+ return $this->makeLegacyAnchor( $sectionName );
}
/**
* @return Parser A parser object that is not parsing anything
*/
public function getFreshParser() {
- global $wgParserConf;
if ( $this->mInParse ) {
- return new $wgParserConf['class']( $wgParserConf );
+ return $this->factory->create();
} else {
return $this;
}