jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/354504 )
Change subject: Try harder to avoid parser cache pollution
......................................................................
Try harder to avoid parser cache pollution
* ParserOptions is reorganized so it knows all the options and their
defaults, and can report whether the non-key options are at their
defaults.
* Definition of the "canonical" ParserOptions (which is unfortunately
different from the "default" ParserOptions) is moved from
ContentHandler to ParserOptions.
* WikiPage uses this to throw an exception if it's asked to cache
with options that aren't used in the cache key.
* ParserCache gets some temporary code to try to avoid a massive cache
stampede on upgrade.
Bug: T110269
Change-Id: I7fb9ffca96e6bd04db44d2d5f2509ec96ad9371f
Depends-On: I4070a8f51927121f690469716625db4a1064dea5
---
M RELEASE-NOTES-1.30
M docs/hooks.txt
M includes/content/ContentHandler.php
M includes/page/WikiPage.php
M includes/parser/ParserCache.php
M includes/parser/ParserOptions.php
M tests/phpunit/includes/deferred/LinksUpdateTest.php
M tests/phpunit/includes/parser/ParserOptionsTest.php
8 files changed, 1,111 insertions(+), 481 deletions(-)
Approvals:
Tim Starling: Looks good to me, approved
jenkins-bot: Verified
diff --git a/RELEASE-NOTES-1.30 b/RELEASE-NOTES-1.30
index 22fed0c..64fe822 100644
--- a/RELEASE-NOTES-1.30
+++ b/RELEASE-NOTES-1.30
@@ -23,6 +23,10 @@
* $wgExceptionHooks has been removed.
* $wgShellLocale is now applied for all requests. wfInitShellLocale() is
deprecated and a no-op, as it is no longer needed.
+* WikiPage::getParserOutput() will now throw an exception if passed
+ ParserOptions would pollute the parser cache. Callers should use
+ WikiPage::makeParserOptions() to create the ParserOptions object and only
+ change options that affect the parser cache key.
=== New features in 1.30 ===
* (T37247) Output from Parser::parse() will now be wrapped in a div with
@@ -33,6 +37,8 @@
* File storage backends that supports headers (eg. Swift) now store an
X-Content-Dimensions header for originals that contain the media's dimensions
as page ranges keyed by dimensions.
+* Added a 'ParserOptionsRegister' hook to allow extensions to register
+ additional parser options.
=== Languages updated in 1.30 ===
diff --git a/docs/hooks.txt b/docs/hooks.txt
index 62b22e1..0e8b508 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -2417,7 +2417,8 @@
&$pager: the pager
&$queryInfo: the query parameters
-'PageRenderingHash': Alter the parser cache option hash key. A parser extension
+'PageRenderingHash': NOTE: Consider using ParserOptionsRegister instead.
+Alter the parser cache option hash key. A parser extension
which depends on user options should install this hook and append its values to
the key.
&$confstr: reference to a hash key string which can be modified
@@ -2541,6 +2542,16 @@
&$params: 2-D array of parameters
$parser: Parser object that called the hook
+'ParserOptionsRegister': Register additional parser options. Note that if you
+change the default value for an option, all existing parser cache entries will
+be invalid. To avoid bugs, you'll need to handle that somehow (e.g. with the
+RejectParserCacheValue hook) because MediaWiki won't do it for you.
+&$defaults: Set the default value for your option here.
+&$inCacheKey: To fragment the parser cache on your option, set a truthy value
here.
+&$lazyLoad: To lazy-initialize your option, set it null in $defaults and set a
+ callable here. The callable is passed the ParserOptions object and the option
+ name.
+
'ParserSectionCreate': Called each time the parser creates a document section
from wikitext. Use this to apply per-section modifications to HTML (like
wrapping the section in a DIV). Caveat: DIVs are valid wikitext, and a DIV
diff --git a/includes/content/ContentHandler.php
b/includes/content/ContentHandler.php
index bccb147..85894ed 100644
--- a/includes/content/ContentHandler.php
+++ b/includes/content/ContentHandler.php
@@ -1007,22 +1007,22 @@
* @return ParserOptions
*/
public function makeParserOptions( $context ) {
- global $wgContLang, $wgEnableParserLimitReporting;
+ global $wgContLang;
if ( $context instanceof IContextSource ) {
- $options = ParserOptions::newFromContext( $context );
+ $user = $context->getUser();
+ $lang = $context->getLanguage();
} elseif ( $context instanceof User ) { // settings per user
(even anons)
- $options = ParserOptions::newFromUser( $context );
+ $user = $context;
+ $lang = null;
} elseif ( $context === 'canonical' ) { // canonical settings
- $options = ParserOptions::newFromUserAndLang( new User,
$wgContLang );
+ $user = new User;
+ $lang = $wgContLang;
} else {
throw new MWException( "Bad context for parser options:
$context" );
}
- $options->enableLimitReport( $wgEnableParserLimitReporting );
// show inclusion/loop reports
- $options->setTidy( true ); // fix bad HTML
-
- return $options;
+ return ParserOptions::newCanonical( $user, $lang );
}
/**
diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php
index 2adc5fb..0e23a88 100644
--- a/includes/page/WikiPage.php
+++ b/includes/page/WikiPage.php
@@ -1055,6 +1055,13 @@
) {
$useParserCache =
( !$forceParse ) && $this->shouldCheckParserCache(
$parserOptions, $oldid );
+
+ if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
+ throw new InvalidArgumentException(
+ 'The supplied ParserOptions are not safe to
cache. Fix the options or set $forceParse = true.'
+ );
+ }
+
wfDebug( __METHOD__ .
': using parser cache: ' . ( $useParserCache ? 'yes' :
'no' ) . "\n" );
if ( $parserOptions->getStubThreshold() ) {
diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php
index 76a7e1e..9c6cf93 100644
--- a/includes/parser/ParserCache.php
+++ b/includes/parser/ParserCache.php
@@ -138,6 +138,20 @@
* @return bool|mixed|string
*/
public function getKey( $article, $popts, $useOutdated = true ) {
+ $dummy = null;
+ return $this->getKeyReal( $article, $popts, $useOutdated,
$dummy );
+ }
+
+ /**
+ * Temporary internal function to allow accessing $usedOptions
+ * @todo Merge this back to self::getKey() when
ParserOptions::optionsHashPre30() is removed
+ * @param WikiPage $article
+ * @param ParserOptions $popts
+ * @param bool $useOutdated (default true)
+ * @param array &$usedOptions Don't use this, it will go away soon
+ * @return bool|mixed|string
+ */
+ private function getKeyReal( $article, $popts, $useOutdated,
&$usedOptions ) {
global $wgCacheEpoch;
if ( $popts instanceof User ) {
@@ -204,7 +218,8 @@
$touched = $article->getTouched();
- $parserOutputKey = $this->getKey( $article, $popts,
$useOutdated );
+ $usedOptions = null;
+ $parserOutputKey = $this->getKeyReal( $article, $popts,
$useOutdated, $usedOptions );
if ( $parserOutputKey === false ) {
wfIncrStats( 'pcache.miss.absent' );
return false;
@@ -214,6 +229,13 @@
/** @var ParserOutput $value */
$value = $this->mMemc->get( $parserOutputKey, $casToken,
BagOStuff::READ_VERIFIED );
if ( !$value ) {
+ $parserOutputKey = $this->getParserOutputKey(
+ $article,
+ $popts->optionsHashPre30( $usedOptions,
$article->getTitle() )
+ );
+ $value = $this->mMemc->get( $parserOutputKey,
$casToken, BagOStuff::READ_VERIFIED );
+ }
+ if ( !$value ) {
wfDebug( "ParserOutput cache miss.\n" );
wfIncrStats( "pcache.miss.absent" );
return false;
diff --git a/includes/parser/ParserOptions.php
b/includes/parser/ParserOptions.php
index d097414..f8ed63f 100644
--- a/includes/parser/ParserOptions.php
+++ b/includes/parser/ParserOptions.php
@@ -25,214 +25,74 @@
/**
* @brief Set options of the Parser
*
- * All member variables are supposed to be private in theory, although in
- * practice this is not the case.
+ * How to add an option in core:
+ * 1. Add it to one of the arrays in ParserOptions::setDefaults()
+ * 2. If necessary, add an entry to ParserOptions::$inCacheKey
+ * 3. Add a getter and setter in the section for that.
+ *
+ * How to add an option in an extension:
+ * 1. Use the 'ParserOptionsRegister' hook to register it.
+ * 2. Where necessary, use $popt->getOption() and $popt->setOption()
+ * to access it.
*
* @ingroup Parser
*/
class ParserOptions {
/**
- * Interlanguage links are removed and returned in an array
+ * Default values for all options that are relevant for caching.
+ * @see self::getDefaults()
+ * @var array|null
*/
- private $mInterwikiMagic;
+ private static $defaults = null;
/**
- * Allow external images inline?
+ * Lazy-loaded options
+ * @var callback[]
*/
- private $mAllowExternalImages;
+ private static $lazyOptions = [
+ 'dateformat' => [ __CLASS__, 'initDateFormat' ],
+ ];
/**
- * If not, any exception?
+ * Specify options that are included in the cache key
+ * @var array
*/
- private $mAllowExternalImagesFrom;
+ private static $inCacheKey = [
+ 'dateformat' => true,
+ 'editsection' => true,
+ 'numberheadings' => true,
+ 'thumbsize' => true,
+ 'stubthreshold' => true,
+ 'printable' => true,
+ 'userlang' => true,
+ 'wrapclass' => true,
+ ];
/**
- * If not or it doesn't match, should we check an on-wiki whitelist?
+ * Current values for all options that are relevant for caching.
+ * @var array
*/
- private $mEnableImageWhitelist;
-
- /**
- * Date format index
- */
- private $mDateFormat = null;
-
- /**
- * Create "edit section" links?
- */
- private $mEditSection = true;
-
- /**
- * Allow inclusion of special pages?
- */
- private $mAllowSpecialInclusion;
-
- /**
- * Use tidy to cleanup output HTML?
- */
- private $mTidy = false;
-
- /**
- * Which lang to call for PLURAL and GRAMMAR
- */
- private $mInterfaceMessage = false;
-
- /**
- * Overrides $mInterfaceMessage with arbitrary language
- */
- private $mTargetLanguage = null;
-
- /**
- * Maximum size of template expansions, in bytes
- */
- private $mMaxIncludeSize;
-
- /**
- * Maximum number of nodes touched by PPFrame::expand()
- */
- private $mMaxPPNodeCount;
-
- /**
- * Maximum number of nodes generated by Preprocessor::preprocessToObj()
- */
- private $mMaxGeneratedPPNodeCount;
-
- /**
- * Maximum recursion depth in PPFrame::expand()
- */
- private $mMaxPPExpandDepth;
-
- /**
- * Maximum recursion depth for templates within templates
- */
- private $mMaxTemplateDepth;
-
- /**
- * Maximum number of calls per parse to expensive parser functions
- */
- private $mExpensiveParserFunctionLimit;
-
- /**
- * Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
- */
- private $mRemoveComments = true;
-
- /**
- * @var callable Callback for current revision fetching; first argument
to call_user_func().
- */
- private $mCurrentRevisionCallback =
- [ 'Parser', 'statelessFetchRevision' ];
-
- /**
- * @var callable Callback for template fetching; first argument to
call_user_func().
- */
- private $mTemplateCallback =
- [ 'Parser', 'statelessFetchTemplate' ];
-
- /**
- * @var callable|null Callback to generate a guess for {{REVISIONID}}
- */
- private $mSpeculativeRevIdCallback;
-
- /**
- * Enable limit report in an HTML comment on output
- */
- private $mEnableLimitReport = false;
+ private $options;
/**
* Timestamp used for {{CURRENTDAY}} etc.
+ * @var string|null
+ * @note Caching based on parse time is handled externally
*/
private $mTimestamp;
/**
- * Target attribute for external links
- */
- private $mExternalLinkTarget;
-
- /**
- * Clean up signature texts?
- * @see Parser::cleanSig
- */
- private $mCleanSignatures;
-
- /**
- * Transform wiki markup when saving the page?
- */
- private $mPreSaveTransform = true;
-
- /**
- * Whether content conversion should be disabled
- */
- private $mDisableContentConversion;
-
- /**
- * Whether title conversion should be disabled
- */
- private $mDisableTitleConversion;
-
- /**
- * Automatically number headings?
- */
- private $mNumberHeadings;
-
- /**
- * Thumb size preferred by the user.
- */
- private $mThumbSize;
-
- /**
- * Maximum article size of an article to be marked as "stub"
- */
- private $mStubThreshold;
-
- /**
- * Language object of the User language.
- */
- private $mUserLang;
-
- /**
- * @var User
* Stored user object
+ * @var User
+ * @todo Track this for caching somehow without fragmenting the cache
insanely
*/
private $mUser;
/**
- * Parsing the page for a "preview" operation?
- */
- private $mIsPreview = false;
-
- /**
- * Parsing the page for a "preview" operation on a single section?
- */
- private $mIsSectionPreview = false;
-
- /**
- * Parsing the printable version of the page?
- */
- private $mIsPrintable = false;
-
- /**
- * Extra key that should be present in the caching key.
- */
- private $mExtraKey = '';
-
- /**
- * Are magic ISBN links enabled?
- */
- private $mMagicISBNLinks = true;
-
- /**
- * Are magic PMID links enabled?
- */
- private $mMagicPMIDLinks = true;
-
- /**
- * Are magic RFC links enabled?
- */
- private $mMagicRFCLinks = true;
-
- /**
* Function to be called when an option is accessed.
+ * @var callable|null
+ * @note Used for collecting used options, does not affect caching
*/
private $onAccessCallback = null;
@@ -240,182 +100,577 @@
* If the page being parsed is a redirect, this should hold the redirect
* target.
* @var Title|null
+ * @todo Track this for caching somehow
*/
private $redirectTarget = null;
/**
- * If the wiki is configured to allow raw html ($wgRawHtml = true)
- * is it allowed in the specific case of parsing this page.
- *
- * This is meant to disable unsafe parser tags in cases where
- * a malicious user may control the input to the parser.
- *
- * @note This is expected to be true for normal pages even if the
- * wiki has $wgRawHtml disabled in general. The setting only
- * signifies that raw html would be unsafe in the current context
- * provided that raw html is allowed at all.
- * @var boolean
+ * Appended to the options hash
*/
- private $allowUnsafeRawHtml = true;
+ private $mExtraKey = '';
/**
- * CSS class to use to wrap output from Parser::parse().
- * @var string|false
+ * @name Option accessors
+ * @{
*/
- private $wrapOutputClass = 'mw-parser-output';
+ /**
+ * Fetch an option, generically
+ * @since 1.30
+ * @param string $name Option name
+ * @return mixed
+ */
+ public function getOption( $name ) {
+ if ( !array_key_exists( $name, $this->options ) ) {
+ throw new InvalidArgumentException( "Unknown parser
option $name" );
+ }
+
+ if ( isset( self::$lazyOptions[$name] ) &&
$this->options[$name] === null ) {
+ $this->options[$name] = call_user_func(
self::$lazyOptions[$name], $this, $name );
+ }
+ if ( !empty( self::$inCacheKey[$name] ) ) {
+ $this->optionUsed( $name );
+ }
+ return $this->options[$name];
+ }
+
+ /**
+ * Set an option, generically
+ * @since 1.30
+ * @param string $name Option name
+ * @param mixed $value New value. Passing null will set null, unlike
many
+ * of the existing accessors which ignore null for historical reasons.
+ * @return mixed Old value
+ */
+ public function setOption( $name, $value ) {
+ if ( !array_key_exists( $name, $this->options ) ) {
+ throw new InvalidArgumentException( "Unknown parser
option $name" );
+ }
+ $old = $this->options[$name];
+ $this->options[$name] = $value;
+ return $old;
+ }
+
+ /**
+ * Legacy implementation
+ * @since 1.30 For implementing legacy setters only. Don't use this in
new code.
+ * @deprecated since 1.30
+ * @param string $name Option name
+ * @param mixed $value New value. Passing null does not set the value.
+ * @return mixed Old value
+ */
+ protected function setOptionLegacy( $name, $value ) {
+ if ( !array_key_exists( $name, $this->options ) ) {
+ throw new InvalidArgumentException( "Unknown parser
option $name" );
+ }
+ return wfSetVar( $this->options[$name], $value );
+ }
+
+ /**
+ * Whether to extract interlanguage links
+ *
+ * When true, interlanguage links will be returned by
+ * ParserOutput::getLanguageLinks() instead of generating link HTML.
+ *
+ * @return bool
+ */
public function getInterwikiMagic() {
- return $this->mInterwikiMagic;
+ return $this->getOption( 'interwikiMagic' );
}
+ /**
+ * Specify whether to extract interlanguage links
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setInterwikiMagic( $x ) {
+ return $this->setOptionLegacy( 'interwikiMagic', $x );
+ }
+
+ /**
+ * Allow all external images inline?
+ * @return bool
+ */
public function getAllowExternalImages() {
- return $this->mAllowExternalImages;
+ return $this->getOption( 'allowExternalImages' );
}
+ /**
+ * Allow all external images inline?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setAllowExternalImages( $x ) {
+ return $this->setOptionLegacy( 'allowExternalImages', $x );
+ }
+
+ /**
+ * External images to allow
+ *
+ * When self::getAllowExternalImages() is false
+ *
+ * @return string|string[] URLs to allow
+ */
public function getAllowExternalImagesFrom() {
- return $this->mAllowExternalImagesFrom;
+ return $this->getOption( 'allowExternalImagesFrom' );
}
+ /**
+ * External images to allow
+ *
+ * When self::getAllowExternalImages() is false
+ *
+ * @param string|string[]|null $x New value (null is no change)
+ * @return string|string[] Old value
+ */
+ public function setAllowExternalImagesFrom( $x ) {
+ return $this->setOptionLegacy( 'allowExternalImagesFrom', $x );
+ }
+
+ /**
+ * Use the on-wiki external image whitelist?
+ * @return bool
+ */
public function getEnableImageWhitelist() {
- return $this->mEnableImageWhitelist;
+ return $this->getOption( 'enableImageWhitelist' );
}
+ /**
+ * Use the on-wiki external image whitelist?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setEnableImageWhitelist( $x ) {
+ return $this->setOptionLegacy( 'enableImageWhitelist', $x );
+ }
+
+ /**
+ * Create "edit section" links?
+ * @return bool
+ */
public function getEditSection() {
- return $this->mEditSection;
+ return $this->getOption( 'editsection' );
}
+ /**
+ * Create "edit section" links?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setEditSection( $x ) {
+ return $this->setOptionLegacy( 'editsection', $x );
+ }
+
+ /**
+ * Automatically number headings?
+ * @return bool
+ */
public function getNumberHeadings() {
- $this->optionUsed( 'numberheadings' );
-
- return $this->mNumberHeadings;
+ return $this->getOption( 'numberheadings' );
}
+ /**
+ * Automatically number headings?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setNumberHeadings( $x ) {
+ return $this->setOptionLegacy( 'numberheadings', $x );
+ }
+
+ /**
+ * Allow inclusion of special pages?
+ * @return bool
+ */
public function getAllowSpecialInclusion() {
- return $this->mAllowSpecialInclusion;
+ return $this->getOption( 'allowSpecialInclusion' );
}
+ /**
+ * Allow inclusion of special pages?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setAllowSpecialInclusion( $x ) {
+ return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
+ }
+
+ /**
+ * Use tidy to cleanup output HTML?
+ * @return bool
+ */
public function getTidy() {
- return $this->mTidy;
+ return $this->getOption( 'tidy' );
}
+ /**
+ * Use tidy to cleanup output HTML?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setTidy( $x ) {
+ return $this->setOptionLegacy( 'tidy', $x );
+ }
+
+ /**
+ * Parsing an interface message?
+ * @return bool
+ */
public function getInterfaceMessage() {
- return $this->mInterfaceMessage;
+ return $this->getOption( 'interfaceMessage' );
}
+ /**
+ * Parsing an interface message?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setInterfaceMessage( $x ) {
+ return $this->setOptionLegacy( 'interfaceMessage', $x );
+ }
+
+ /**
+ * Target language for the parse
+ * @return Language|null
+ */
public function getTargetLanguage() {
- return $this->mTargetLanguage;
+ return $this->getOption( 'targetLanguage' );
}
+ /**
+ * Target language for the parse
+ * @param Language|null $x New value
+ * @return Language|null Old value
+ */
+ public function setTargetLanguage( $x ) {
+ return $this->setOption( 'targetLanguage', $x );
+ }
+
+ /**
+ * Maximum size of template expansions, in bytes
+ * @return int
+ */
public function getMaxIncludeSize() {
- return $this->mMaxIncludeSize;
+ return $this->getOption( 'maxIncludeSize' );
}
+ /**
+ * Maximum size of template expansions, in bytes
+ * @param int|null $x New value (null is no change)
+ * @return int Old value
+ */
+ public function setMaxIncludeSize( $x ) {
+ return $this->setOptionLegacy( 'maxIncludeSize', $x );
+ }
+
+ /**
+ * Maximum number of nodes touched by PPFrame::expand()
+ * @return int
+ */
public function getMaxPPNodeCount() {
- return $this->mMaxPPNodeCount;
+ return $this->getOption( 'maxPPNodeCount' );
}
+ /**
+ * Maximum number of nodes touched by PPFrame::expand()
+ * @param int|null $x New value (null is no change)
+ * @return int Old value
+ */
+ public function setMaxPPNodeCount( $x ) {
+ return $this->setOptionLegacy( 'maxPPNodeCount', $x );
+ }
+
+ /**
+ * Maximum number of nodes generated by Preprocessor::preprocessToObj()
+ * @return int
+ */
public function getMaxGeneratedPPNodeCount() {
- return $this->mMaxGeneratedPPNodeCount;
+ return $this->getOption( 'maxGeneratedPPNodeCount' );
}
+ /**
+ * Maximum number of nodes generated by Preprocessor::preprocessToObj()
+ * @param int|null $x New value (null is no change)
+ * @return int
+ */
+ public function setMaxGeneratedPPNodeCount( $x ) {
+ return $this->setOptionLegacy( 'maxGeneratedPPNodeCount', $x );
+ }
+
+ /**
+ * Maximum recursion depth in PPFrame::expand()
+ * @return int
+ */
public function getMaxPPExpandDepth() {
- return $this->mMaxPPExpandDepth;
+ return $this->getOption( 'maxPPExpandDepth' );
}
+ /**
+ * Maximum recursion depth for templates within templates
+ * @return int
+ */
public function getMaxTemplateDepth() {
- return $this->mMaxTemplateDepth;
+ return $this->getOption( 'maxTemplateDepth' );
}
- /* @since 1.20 */
+ /**
+ * Maximum recursion depth for templates within templates
+ * @param int|null $x New value (null is no change)
+ * @return int Old value
+ */
+ public function setMaxTemplateDepth( $x ) {
+ return $this->setOptionLegacy( 'maxTemplateDepth', $x );
+ }
+
+ /**
+ * Maximum number of calls per parse to expensive parser functions
+ * @since 1.20
+ * @return int
+ */
public function getExpensiveParserFunctionLimit() {
- return $this->mExpensiveParserFunctionLimit;
+ return $this->getOption( 'expensiveParserFunctionLimit' );
}
+ /**
+ * Maximum number of calls per parse to expensive parser functions
+ * @since 1.20
+ * @param int|null $x New value (null is no change)
+ * @return int Old value
+ */
+ public function setExpensiveParserFunctionLimit( $x ) {
+ return $this->setOptionLegacy( 'expensiveParserFunctionLimit',
$x );
+ }
+
+ /**
+ * Remove HTML comments
+ * @warning Only applies to preprocess operations
+ * @return bool
+ */
public function getRemoveComments() {
- return $this->mRemoveComments;
+ return $this->getOption( 'removeComments' );
}
- /* @since 1.24 */
- public function getCurrentRevisionCallback() {
- return $this->mCurrentRevisionCallback;
+ /**
+ * Remove HTML comments
+ * @warning Only applies to preprocess operations
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setRemoveComments( $x ) {
+ return $this->setOptionLegacy( 'removeComments', $x );
}
- public function getTemplateCallback() {
- return $this->mTemplateCallback;
- }
-
- /** @since 1.28 */
- public function getSpeculativeRevIdCallback() {
- return $this->mSpeculativeRevIdCallback;
- }
-
+ /**
+ * Enable limit report in an HTML comment on output
+ * @return bool
+ */
public function getEnableLimitReport() {
- return $this->mEnableLimitReport;
+ return $this->getOption( 'enableLimitReport' );
}
+ /**
+ * Enable limit report in an HTML comment on output
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function enableLimitReport( $x = true ) {
+ return $this->setOptionLegacy( 'enableLimitReport', $x );
+ }
+
+ /**
+ * Clean up signature texts?
+ * @see Parser::cleanSig
+ * @return bool
+ */
public function getCleanSignatures() {
- return $this->mCleanSignatures;
+ return $this->getOption( 'cleanSignatures' );
}
+ /**
+ * Clean up signature texts?
+ * @see Parser::cleanSig
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setCleanSignatures( $x ) {
+ return $this->setOptionLegacy( 'cleanSignatures', $x );
+ }
+
+ /**
+ * Target attribute for external links
+ * @return string
+ */
public function getExternalLinkTarget() {
- return $this->mExternalLinkTarget;
+ return $this->getOption( 'externalLinkTarget' );
}
+ /**
+ * Target attribute for external links
+ * @param string|null $x New value (null is no change)
+ * @return string Old value
+ */
+ public function setExternalLinkTarget( $x ) {
+ return $this->setOptionLegacy( 'externalLinkTarget', $x );
+ }
+
+ /**
+ * Whether content conversion should be disabled
+ * @return bool
+ */
public function getDisableContentConversion() {
- return $this->mDisableContentConversion;
+ return $this->getOption( 'disableContentConversion' );
}
+ /**
+ * Whether content conversion should be disabled
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function disableContentConversion( $x = true ) {
+ return $this->setOptionLegacy( 'disableContentConversion', $x );
+ }
+
+ /**
+ * Whether title conversion should be disabled
+ * @return bool
+ */
public function getDisableTitleConversion() {
- return $this->mDisableTitleConversion;
+ return $this->getOption( 'disableTitleConversion' );
}
+ /**
+ * Whether title conversion should be disabled
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function disableTitleConversion( $x = true ) {
+ return $this->setOptionLegacy( 'disableTitleConversion', $x );
+ }
+
+ /**
+ * Thumb size preferred by the user.
+ * @return int
+ */
public function getThumbSize() {
- $this->optionUsed( 'thumbsize' );
-
- return $this->mThumbSize;
+ return $this->getOption( 'thumbsize' );
}
+ /**
+ * Thumb size preferred by the user.
+ * @param int|null $x New value (null is no change)
+ * @return int Old value
+ */
+ public function setThumbSize( $x ) {
+ return $this->setOptionLegacy( 'thumbsize', $x );
+ }
+
+ /**
+ * Thumb size preferred by the user.
+ * @return int
+ */
public function getStubThreshold() {
- $this->optionUsed( 'stubthreshold' );
-
- return $this->mStubThreshold;
+ return $this->getOption( 'stubthreshold' );
}
+ /**
+ * Thumb size preferred by the user.
+ * @param int|null $x New value (null is no change)
+ * @return int Old value
+ */
+ public function setStubThreshold( $x ) {
+ return $this->setOptionLegacy( 'stubthreshold', $x );
+ }
+
+ /**
+ * Parsing the page for a "preview" operation?
+ * @return bool
+ */
public function getIsPreview() {
- return $this->mIsPreview;
+ return $this->getOption( 'isPreview' );
}
+ /**
+ * Parsing the page for a "preview" operation?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setIsPreview( $x ) {
+ return $this->setOptionLegacy( 'isPreview', $x );
+ }
+
+ /**
+ * Parsing the page for a "preview" operation on a single section?
+ * @return bool
+ */
public function getIsSectionPreview() {
- return $this->mIsSectionPreview;
+ return $this->getOption( 'isSectionPreview' );
}
+ /**
+ * Parsing the page for a "preview" operation on a single section?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setIsSectionPreview( $x ) {
+ return $this->setOptionLegacy( 'isSectionPreview', $x );
+ }
+
+ /**
+ * Parsing the printable version of the page?
+ * @return bool
+ */
public function getIsPrintable() {
- $this->optionUsed( 'printable' );
-
- return $this->mIsPrintable;
+ return $this->getOption( 'printable' );
}
- public function getUser() {
- return $this->mUser;
+ /**
+ * Parsing the printable version of the page?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setIsPrintable( $x ) {
+ return $this->setOptionLegacy( 'printable', $x );
}
+ /**
+ * Transform wiki markup when saving the page?
+ * @return bool
+ */
public function getPreSaveTransform() {
- return $this->mPreSaveTransform;
+ return $this->getOption( 'preSaveTransform' );
}
+ /**
+ * Transform wiki markup when saving the page?
+ * @param bool|null $x New value (null is no change)
+ * @return bool Old value
+ */
+ public function setPreSaveTransform( $x ) {
+ return $this->setOptionLegacy( 'preSaveTransform', $x );
+ }
+
+ /**
+ * Date format index
+ * @return string
+ */
public function getDateFormat() {
- $this->optionUsed( 'dateformat' );
- if ( !isset( $this->mDateFormat ) ) {
- $this->mDateFormat = $this->mUser->getDatePreference();
- }
- return $this->mDateFormat;
+ return $this->getOption( 'dateformat' );
}
- public function getTimestamp() {
- if ( !isset( $this->mTimestamp ) ) {
- $this->mTimestamp = wfTimestampNow();
- }
- return $this->mTimestamp;
+ /**
+ * Lazy initializer for dateFormat
+ */
+ private static function initDateFormat( $popt ) {
+ return $popt->mUser->getDatePreference();
+ }
+
+ /**
+ * Date format index
+ * @param string|null $x New value (null is no change)
+ * @return string Old value
+ */
+ public function setDateFormat( $x ) {
+ return $this->setOptionLegacy( 'dateformat', $x );
}
/**
@@ -436,8 +691,7 @@
* @since 1.19
*/
public function getUserLangObj() {
- $this->optionUsed( 'userlang' );
- return $this->mUserLang;
+ return $this->getOption( 'userlang' );
}
/**
@@ -457,34 +711,72 @@
}
/**
+ * Set the user language used by the parser for this page and split the
parser cache.
+ * @param string|Language $x New value
+ * @return Language Old value
+ */
+ public function setUserLang( $x ) {
+ if ( is_string( $x ) ) {
+ $x = Language::factory( $x );
+ }
+
+ return $this->setOptionLegacy( 'userlang', $x );
+ }
+
+ /**
+ * Are magic ISBN links enabled?
* @since 1.28
* @return bool
*/
public function getMagicISBNLinks() {
- return $this->mMagicISBNLinks;
+ return $this->getOption( 'magicISBNLinks' );
}
/**
+ * Are magic PMID links enabled?
* @since 1.28
* @return bool
*/
public function getMagicPMIDLinks() {
- return $this->mMagicPMIDLinks;
+ return $this->getOption( 'magicPMIDLinks' );
}
/**
+ * Are magic RFC links enabled?
* @since 1.28
* @return bool
*/
public function getMagicRFCLinks() {
- return $this->mMagicRFCLinks;
+ return $this->getOption( 'magicRFCLinks' );
}
/**
+ * If the wiki is configured to allow raw html ($wgRawHtml = true)
+ * is it allowed in the specific case of parsing this page.
+ *
+ * This is meant to disable unsafe parser tags in cases where
+ * a malicious user may control the input to the parser.
+ *
+ * @note This is expected to be true for normal pages even if the
+ * wiki has $wgRawHtml disabled in general. The setting only
+ * signifies that raw html would be unsafe in the current context
+ * provided that raw html is allowed at all.
* @since 1.29
* @return bool
*/
public function getAllowUnsafeRawHtml() {
- return $this->allowUnsafeRawHtml;
+ return $this->getOption( 'allowUnsafeRawHtml' );
+ }
+
+ /**
+ * If the wiki is configured to allow raw html ($wgRawHtml = true)
+ * is it allowed in the specific case of parsing this page.
+ * @see self::getAllowUnsafeRawHtml()
+ * @since 1.29
+ * @param bool|null Value to set or null to get current value
+ * @return bool Current value for allowUnsafeRawHtml
+ */
+ public function setAllowUnsafeRawHtml( $x ) {
+ return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
}
/**
@@ -493,156 +785,7 @@
* @return string|bool
*/
public function getWrapOutputClass() {
- $this->optionUsed( 'wrapclass' );
- return $this->wrapOutputClass;
- }
-
- public function setInterwikiMagic( $x ) {
- return wfSetVar( $this->mInterwikiMagic, $x );
- }
-
- public function setAllowExternalImages( $x ) {
- return wfSetVar( $this->mAllowExternalImages, $x );
- }
-
- public function setAllowExternalImagesFrom( $x ) {
- return wfSetVar( $this->mAllowExternalImagesFrom, $x );
- }
-
- public function setEnableImageWhitelist( $x ) {
- return wfSetVar( $this->mEnableImageWhitelist, $x );
- }
-
- public function setDateFormat( $x ) {
- return wfSetVar( $this->mDateFormat, $x );
- }
-
- public function setEditSection( $x ) {
- return wfSetVar( $this->mEditSection, $x );
- }
-
- public function setNumberHeadings( $x ) {
- return wfSetVar( $this->mNumberHeadings, $x );
- }
-
- public function setAllowSpecialInclusion( $x ) {
- return wfSetVar( $this->mAllowSpecialInclusion, $x );
- }
-
- public function setTidy( $x ) {
- return wfSetVar( $this->mTidy, $x );
- }
-
- public function setInterfaceMessage( $x ) {
- return wfSetVar( $this->mInterfaceMessage, $x );
- }
-
- public function setTargetLanguage( $x ) {
- return wfSetVar( $this->mTargetLanguage, $x, true );
- }
-
- public function setMaxIncludeSize( $x ) {
- return wfSetVar( $this->mMaxIncludeSize, $x );
- }
-
- public function setMaxPPNodeCount( $x ) {
- return wfSetVar( $this->mMaxPPNodeCount, $x );
- }
-
- public function setMaxGeneratedPPNodeCount( $x ) {
- return wfSetVar( $this->mMaxGeneratedPPNodeCount, $x );
- }
-
- public function setMaxTemplateDepth( $x ) {
- return wfSetVar( $this->mMaxTemplateDepth, $x );
- }
-
- /* @since 1.20 */
- public function setExpensiveParserFunctionLimit( $x ) {
- return wfSetVar( $this->mExpensiveParserFunctionLimit, $x );
- }
-
- public function setRemoveComments( $x ) {
- return wfSetVar( $this->mRemoveComments, $x );
- }
-
- /* @since 1.24 */
- public function setCurrentRevisionCallback( $x ) {
- return wfSetVar( $this->mCurrentRevisionCallback, $x );
- }
-
- /** @since 1.28 */
- public function setSpeculativeRevIdCallback( $x ) {
- return wfSetVar( $this->mSpeculativeRevIdCallback, $x );
- }
-
- public function setTemplateCallback( $x ) {
- return wfSetVar( $this->mTemplateCallback, $x );
- }
-
- public function enableLimitReport( $x = true ) {
- return wfSetVar( $this->mEnableLimitReport, $x );
- }
-
- public function setTimestamp( $x ) {
- return wfSetVar( $this->mTimestamp, $x );
- }
-
- public function setCleanSignatures( $x ) {
- return wfSetVar( $this->mCleanSignatures, $x );
- }
-
- public function setExternalLinkTarget( $x ) {
- return wfSetVar( $this->mExternalLinkTarget, $x );
- }
-
- public function disableContentConversion( $x = true ) {
- return wfSetVar( $this->mDisableContentConversion, $x );
- }
-
- public function disableTitleConversion( $x = true ) {
- return wfSetVar( $this->mDisableTitleConversion, $x );
- }
-
- public function setUserLang( $x ) {
- if ( is_string( $x ) ) {
- $x = Language::factory( $x );
- }
-
- return wfSetVar( $this->mUserLang, $x );
- }
-
- public function setThumbSize( $x ) {
- return wfSetVar( $this->mThumbSize, $x );
- }
-
- public function setStubThreshold( $x ) {
- return wfSetVar( $this->mStubThreshold, $x );
- }
-
- public function setPreSaveTransform( $x ) {
- return wfSetVar( $this->mPreSaveTransform, $x );
- }
-
- public function setIsPreview( $x ) {
- return wfSetVar( $this->mIsPreview, $x );
- }
-
- public function setIsSectionPreview( $x ) {
- return wfSetVar( $this->mIsSectionPreview, $x );
- }
-
- public function setIsPrintable( $x ) {
- return wfSetVar( $this->mIsPrintable, $x );
- }
-
- /**
- * @param bool|null Value to set or null to get current value
- * @return bool Current value for allowUnsafeRawHtml
- * @since 1.29
- */
- public function setAllowUnsafeRawHtml( $x ) {
- return wfSetVar( $this->allowUnsafeRawHtml, $x );
+ return $this->getOption( 'wrapclass' );
}
/**
@@ -655,7 +798,84 @@
if ( $className === true ) { // DWIM, they probably want the
default class name
$className = 'mw-parser-output';
}
- return wfSetVar( $this->wrapOutputClass, $className );
+ return $this->setOption( 'wrapclass', $className );
+ }
+
+ /**
+ * Callback for current revision fetching; first argument to
call_user_func().
+ * @since 1.24
+ * @return callable
+ */
+ public function getCurrentRevisionCallback() {
+ return $this->getOption( 'currentRevisionCallback' );
+ }
+
+ /**
+ * Callback for current revision fetching; first argument to
call_user_func().
+ * @since 1.24
+ * @param callable|null $x New value (null is no change)
+ * @return callable Old value
+ */
+ public function setCurrentRevisionCallback( $x ) {
+ return $this->setOptionLegacy( 'currentRevisionCallback', $x );
+ }
+
+ /**
+ * Callback for template fetching; first argument to call_user_func().
+ * @return callable
+ */
+ public function getTemplateCallback() {
+ return $this->getOption( 'templateCallback' );
+ }
+
+ /**
+ * Callback for template fetching; first argument to call_user_func().
+ * @param callable|null $x New value (null is no change)
+ * @return callable Old value
+ */
+ public function setTemplateCallback( $x ) {
+ return $this->setOptionLegacy( 'templateCallback', $x );
+ }
+
+ /**
+ * Callback to generate a guess for {{REVISIONID}}
+ * @since 1.28
+ * @return callable|null
+ */
+ public function getSpeculativeRevIdCallback() {
+ return $this->getOption( 'speculativeRevIdCallback' );
+ }
+
+ /**
+ * Callback to generate a guess for {{REVISIONID}}
+ * @since 1.28
+ * @param callable|null $x New value (null is no change)
+ * @return callable|null Old value
+ */
+ public function setSpeculativeRevIdCallback( $x ) {
+ return $this->setOptionLegacy( 'speculativeRevIdCallback', $x );
+ }
+
+ /**@}*/
+
+ /**
+ * Timestamp used for {{CURRENTDAY}} etc.
+ * @return string
+ */
+ public function getTimestamp() {
+ if ( !isset( $this->mTimestamp ) ) {
+ $this->mTimestamp = wfTimestampNow();
+ }
+ return $this->mTimestamp;
+ }
+
+ /**
+ * Timestamp used for {{CURRENTDAY}} etc.
+ * @param string|null $x New value (null is no change)
+ * @return string Old value
+ */
+ public function setTimestamp( $x ) {
+ return wfSetVar( $this->mTimestamp, $x );
}
/**
@@ -684,6 +904,8 @@
/**
* Extra key that should be present in the parser cache key.
+ * @warning Consider registering your additional options with the
+ * ParserOptionsRegister hook instead of using this method.
* @param string $key
*/
public function addExtraKey( $key ) {
@@ -691,7 +913,18 @@
}
/**
+ * Current user
+ * @return User
+ */
+ public function getUser() {
+ return $this->mUser;
+ }
+
+ /**
* Constructor
+ * @warning For interaction with the parser cache, use
+ * WikiPage::makeParserOptions(), ContentHandler::makeParserOptions(),
or
+ * ParserOptions::newCanonical() instead.
* @param User $user
* @param Language $lang
*/
@@ -716,6 +949,9 @@
/**
* Get a ParserOptions object for an anonymous user
+ * @warning For interaction with the parser cache, use
+ * WikiPage::makeParserOptions(), ContentHandler::makeParserOptions(),
or
+ * ParserOptions::newCanonical() instead.
* @since 1.27
* @return ParserOptions
*/
@@ -728,6 +964,9 @@
* Get a ParserOptions object from a given user.
* Language will be taken from $wgLang.
*
+ * @warning For interaction with the parser cache, use
+ * WikiPage::makeParserOptions(), ContentHandler::makeParserOptions(),
or
+ * ParserOptions::newCanonical() instead.
* @param User $user
* @return ParserOptions
*/
@@ -738,6 +977,9 @@
/**
* Get a ParserOptions object from a given user and language
*
+ * @warning For interaction with the parser cache, use
+ * WikiPage::makeParserOptions(), ContentHandler::makeParserOptions(),
or
+ * ParserOptions::newCanonical() instead.
* @param User $user
* @param Language $lang
* @return ParserOptions
@@ -749,11 +991,125 @@
/**
* Get a ParserOptions object from a IContextSource object
*
+ * @warning For interaction with the parser cache, use
+ * WikiPage::makeParserOptions(), ContentHandler::makeParserOptions(),
or
+ * ParserOptions::newCanonical() instead.
* @param IContextSource $context
* @return ParserOptions
*/
public static function newFromContext( IContextSource $context ) {
return new ParserOptions( $context->getUser(),
$context->getLanguage() );
+ }
+
+ /**
+ * Creates a "canonical" ParserOptions object
+ *
+ * For historical reasons, certain options have default values that are
+ * different from the canonical values used for caching.
+ *
+ * @since 1.30
+ * @param User|null $user
+ * @param Language|StubObject|null $lang
+ * @return ParserOptions
+ */
+ public static function newCanonical( User $user = null, $lang = null ) {
+ $ret = new ParserOptions( $user, $lang );
+ foreach ( self::getCanonicalOverrides() as $k => $v ) {
+ $ret->setOption( $k, $v );
+ }
+ return $ret;
+ }
+
+ /**
+ * Get default option values
+ * @warning If you change the default for an existing option (unless
it's
+ * being overridden by self::getCanonicalOverrides()), all existing
parser
+ * cache entries will be invalid. To avoid bugs, you'll need to handle
+ * that somehow (e.g. with the RejectParserCacheValue hook) because
+ * MediaWiki won't do it for you.
+ * @return array
+ */
+ private static function getDefaults() {
+ global $wgInterwikiMagic, $wgAllowExternalImages,
+ $wgAllowExternalImagesFrom, $wgEnableImageWhitelist,
$wgAllowSpecialInclusion,
+ $wgMaxArticleSize, $wgMaxPPNodeCount,
$wgMaxTemplateDepth, $wgMaxPPExpandDepth,
+ $wgCleanSignatures, $wgExternalLinkTarget,
$wgExpensiveParserFunctionLimit,
+ $wgMaxGeneratedPPNodeCount, $wgDisableLangConversion,
$wgDisableTitleConversion,
+ $wgEnableMagicLinks, $wgContLang;
+
+ if ( self::$defaults === null ) {
+ // *UPDATE* ParserOptions::matches() if any of this
changes as needed
+ self::$defaults = [
+ 'dateformat' => null,
+ 'editsection' => true,
+ 'tidy' => false,
+ 'interfaceMessage' => false,
+ 'targetLanguage' => null,
+ 'removeComments' => true,
+ 'enableLimitReport' => false,
+ 'preSaveTransform' => true,
+ 'isPreview' => false,
+ 'isSectionPreview' => false,
+ 'printable' => false,
+ 'allowUnsafeRawHtml' => true,
+ 'wrapclass' => 'mw-parser-output',
+ 'currentRevisionCallback' => [ 'Parser',
'statelessFetchRevision' ],
+ 'templateCallback' => [ 'Parser',
'statelessFetchTemplate' ],
+ 'speculativeRevIdCallback' => null,
+ ];
+
+ Hooks::run( 'ParserOptionsRegister', [
+ &self::$defaults,
+ &self::$inCacheKey,
+ &self::$lazyOptions,
+ ] );
+
+ ksort( self::$inCacheKey );
+ }
+
+ // Unit tests depend on being able to modify the globals at will
+ return self::$defaults + [
+ 'interwikiMagic' => $wgInterwikiMagic,
+ 'allowExternalImages' => $wgAllowExternalImages,
+ 'allowExternalImagesFrom' => $wgAllowExternalImagesFrom,
+ 'enableImageWhitelist' => $wgEnableImageWhitelist,
+ 'allowSpecialInclusion' => $wgAllowSpecialInclusion,
+ 'maxIncludeSize' => $wgMaxArticleSize * 1024,
+ 'maxPPNodeCount' => $wgMaxPPNodeCount,
+ 'maxGeneratedPPNodeCount' => $wgMaxGeneratedPPNodeCount,
+ 'maxPPExpandDepth' => $wgMaxPPExpandDepth,
+ 'maxTemplateDepth' => $wgMaxTemplateDepth,
+ 'expensiveParserFunctionLimit' =>
$wgExpensiveParserFunctionLimit,
+ 'externalLinkTarget' => $wgExternalLinkTarget,
+ 'cleanSignatures' => $wgCleanSignatures,
+ 'disableContentConversion' => $wgDisableLangConversion,
+ 'disableTitleConversion' => $wgDisableLangConversion ||
$wgDisableTitleConversion,
+ 'magicISBNLinks' => $wgEnableMagicLinks['ISBN'],
+ 'magicPMIDLinks' => $wgEnableMagicLinks['PMID'],
+ 'magicRFCLinks' => $wgEnableMagicLinks['RFC'],
+ 'numberheadings' => User::getDefaultOption(
'numberheadings' ),
+ 'thumbsize' => User::getDefaultOption( 'thumbsize' ),
+ 'stubthreshold' => 0,
+ 'userlang' => $wgContLang,
+ ];
+ }
+
+ /**
+ * Get "canonical" non-default option values
+ * @see self::newCanonical
+ * @warning If you change the override for an existing option, all
existing
+ * parser cache entries will be invalid. To avoid bugs, you'll need to
+ * handle that somehow (e.g. with the RejectParserCacheValue hook)
because
+ * MediaWiki won't do it for you.
+ * @return array
+ */
+ private static function getCanonicalOverrides() {
+ global $wgEnableParserLimitReporting;
+
+ return [
+ 'tidy' => true,
+ 'enableLimitReport' => $wgEnableParserLimitReporting,
+ ];
}
/**
@@ -763,38 +1119,13 @@
* @param Language $lang
*/
private function initialiseFromUser( $user, $lang ) {
- global $wgInterwikiMagic, $wgAllowExternalImages,
- $wgAllowExternalImagesFrom, $wgEnableImageWhitelist,
$wgAllowSpecialInclusion,
- $wgMaxArticleSize, $wgMaxPPNodeCount,
$wgMaxTemplateDepth, $wgMaxPPExpandDepth,
- $wgCleanSignatures, $wgExternalLinkTarget,
$wgExpensiveParserFunctionLimit,
- $wgMaxGeneratedPPNodeCount, $wgDisableLangConversion,
$wgDisableTitleConversion,
- $wgEnableMagicLinks;
-
- // *UPDATE* ParserOptions::matches() if any of this changes as
needed
- $this->mInterwikiMagic = $wgInterwikiMagic;
- $this->mAllowExternalImages = $wgAllowExternalImages;
- $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
- $this->mEnableImageWhitelist = $wgEnableImageWhitelist;
- $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
- $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
- $this->mMaxPPNodeCount = $wgMaxPPNodeCount;
- $this->mMaxGeneratedPPNodeCount = $wgMaxGeneratedPPNodeCount;
- $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
- $this->mMaxTemplateDepth = $wgMaxTemplateDepth;
- $this->mExpensiveParserFunctionLimit =
$wgExpensiveParserFunctionLimit;
- $this->mCleanSignatures = $wgCleanSignatures;
- $this->mExternalLinkTarget = $wgExternalLinkTarget;
- $this->mDisableContentConversion = $wgDisableLangConversion;
- $this->mDisableTitleConversion = $wgDisableLangConversion ||
$wgDisableTitleConversion;
- $this->mMagicISBNLinks = $wgEnableMagicLinks['ISBN'];
- $this->mMagicPMIDLinks = $wgEnableMagicLinks['PMID'];
- $this->mMagicRFCLinks = $wgEnableMagicLinks['RFC'];
+ $this->options = self::getDefaults();
$this->mUser = $user;
- $this->mNumberHeadings = $user->getOption( 'numberheadings' );
- $this->mThumbSize = $user->getOption( 'thumbsize' );
- $this->mStubThreshold = $user->getStubThreshold();
- $this->mUserLang = $lang;
+ $this->options['numberheadings'] = $user->getOption(
'numberheadings' );
+ $this->options['thumbsize'] = $user->getOption( 'thumbsize' );
+ $this->options['stubthreshold'] = $user->getStubThreshold();
+ $this->options['userlang'] = $lang;
}
/**
@@ -807,9 +1138,36 @@
* @since 1.25
*/
public function matches( ParserOptions $other ) {
+ // Populate lazy options
+ foreach ( self::$lazyOptions as $name => $callback ) {
+ if ( $this->options[$name] === null ) {
+ $this->options[$name] = call_user_func(
$callback, $this, $name );
+ }
+ if ( $other->options[$name] === null ) {
+ $other->options[$name] = call_user_func(
$callback, $other, $name );
+ }
+ }
+
+ // Compare most options
+ $options = array_keys( $this->options );
+ $options = array_diff( $options, [
+ 'enableLimitReport', // only affects HTML comments
+ ] );
+ foreach ( $options as $option ) {
+ $o1 = $this->optionToString( $this->options[$option] );
+ $o2 = $this->optionToString( $other->options[$option] );
+ if ( $o1 !== $o2 ) {
+ return false;
+ }
+ }
+
+ // Compare most other fields
$fields = array_keys( get_class_vars( __CLASS__ ) );
$fields = array_diff( $fields, [
- 'mEnableLimitReport', // only effects HTML comments
+ 'defaults', // static
+ 'lazyOptions', // static
+ 'inCacheKey', // static
+ 'options', // Already checked above
'onAccessCallback', // only used for ParserOutput
option tracking
] );
foreach ( $fields as $field ) {
@@ -817,11 +1175,8 @@
return false;
}
}
- // Check the object and lazy-loaded options
- return (
- $this->mUserLang->equals( $other->mUserLang ) &&
- $this->getDateFormat() === $other->getDateFormat()
- );
+
+ return true;
}
/**
@@ -851,6 +1206,7 @@
* Returns the full array of options that would have been used by
* in 1.16.
* Used to get the old parser cache entries when available.
+ * @todo 1.16 was years ago, can we remove this?
* @return array
*/
public static function legacyOptions() {
@@ -865,15 +1221,32 @@
}
/**
+ * Convert an option to a string value
+ * @param mixed $value
+ * @return string
+ */
+ private function optionToString( $value ) {
+ if ( $value === true ) {
+ return '1';
+ } elseif ( $value === false ) {
+ return '0';
+ } elseif ( $value === null ) {
+ return '';
+ } elseif ( $value instanceof Language ) {
+ return $value->getCode();
+ } elseif ( is_array( $value ) ) {
+ return '[' . join( ',', array_map( [ $this,
'optionToString' ], $value ) ) . ']';
+ } else {
+ return (string)$value;
+ }
+ }
+
+ /**
* Generate a hash string with the values set on these ParserOptions
* for the keys given in the array.
* This will be used as part of the hash key for the parser cache,
* so users sharing the options with vary for the same page share
* the same cached data safely.
- *
- * Extensions which require it should install 'PageRenderingHash' hook,
- * which will give them a chance to modify this key based on their own
- * settings.
*
* @since 1.17
* @param array $forOptions
@@ -881,6 +1254,61 @@
* @return string Page rendering hash
*/
public function optionsHash( $forOptions, $title = null ) {
+ global $wgRenderHashAppend;
+
+ // We only include used options with non-canonical values in
the key
+ // so adding a new option doesn't invalidate the entire parser
cache.
+ // The drawback to this is that changing the default value of
an option
+ // requires manual invalidation of existing cache entries, as
mentioned
+ // in the docs on the relevant methods and hooks.
+ $defaults = self::getCanonicalOverrides() + self::getDefaults();
+ $values = [];
+ foreach ( self::$inCacheKey as $option => $include ) {
+ if ( $include && in_array( $option, $forOptions, true )
) {
+ $v = $this->optionToString(
$this->options[$option] );
+ $d = $this->optionToString( $defaults[$option]
);
+ if ( $v !== $d ) {
+ $values[] = "$option=$v";
+ }
+ }
+ }
+
+ $confstr = $values ? join( '!', $values ) : 'canonical';
+
+ // add in language specific options, if any
+ // @todo FIXME: This is just a way of retrieving the url/user
preferred variant
+ if ( !is_null( $title ) ) {
+ $confstr .=
$title->getPageLanguage()->getExtraHashOptions();
+ } else {
+ global $wgContLang;
+ $confstr .= $wgContLang->getExtraHashOptions();
+ }
+
+ $confstr .= $wgRenderHashAppend;
+
+ if ( $this->mExtraKey != '' ) {
+ $confstr .= $this->mExtraKey;
+ }
+
+ // Give a chance for extensions to modify the hash, if they have
+ // extra options or other effects on the parser cache.
+ Hooks::run( 'PageRenderingHash', [ &$confstr, $this->getUser(),
&$forOptions ] );
+
+ // Make it a valid memcached key fragment
+ $confstr = str_replace( ' ', '_', $confstr );
+
+ return $confstr;
+ }
+
+ /**
+ * Generate the hash used before MediaWiki 1.30
+ * @since 1.30
+ * @deprecated since 1.30. Do not use this unless you're ParserCache.
+ * @param array $forOptions
+ * @param Title $title Used to get the content language of the page
(since r97636)
+ * @return string Page rendering hash
+ */
+ public function optionsHashPre30( $forOptions, $title = null ) {
global $wgRenderHashAppend;
// FIXME: Once the cache key is reorganized this argument
@@ -892,7 +1320,7 @@
// since it disables the parser cache, its value will always
// be 0 when this function is called by parsercache.
if ( in_array( 'stubthreshold', $forOptions ) ) {
- $confstr .= '!' . $this->mStubThreshold;
+ $confstr .= '!' . $this->options['stubthreshold'];
} else {
$confstr .= '!*';
}
@@ -902,19 +1330,19 @@
}
if ( in_array( 'numberheadings', $forOptions ) ) {
- $confstr .= '!' . ( $this->mNumberHeadings ? '1' : '' );
+ $confstr .= '!' . ( $this->options['numberheadings'] ?
'1' : '' );
} else {
$confstr .= '!*';
}
if ( in_array( 'userlang', $forOptions ) ) {
- $confstr .= '!' . $this->mUserLang->getCode();
+ $confstr .= '!' . $this->options['userlang']->getCode();
} else {
$confstr .= '!*';
}
if ( in_array( 'thumbsize', $forOptions ) ) {
- $confstr .= '!' . $this->mThumbSize;
+ $confstr .= '!' . $this->options['thumbsize'];
} else {
$confstr .= '!*';
}
@@ -936,16 +1364,18 @@
// directly. At least Wikibase does at this point in time.
if ( !in_array( 'editsection', $forOptions ) ) {
$confstr .= '!*';
- } elseif ( !$this->mEditSection ) {
+ } elseif ( !$this->options['editsection'] ) {
$confstr .= '!edit=0';
}
- if ( $this->mIsPrintable && in_array( 'printable', $forOptions
) ) {
+ if ( $this->options['printable'] && in_array( 'printable',
$forOptions ) ) {
$confstr .= '!printable=1';
}
- if ( $this->wrapOutputClass !== 'mw-parser-output' && in_array(
'wrapclass', $forOptions ) ) {
- $confstr .= '!wrapclass=' . $this->wrapOutputClass;
+ if ( $this->options['wrapclass'] !== 'mw-parser-output' &&
+ in_array( 'wrapclass', $forOptions )
+ ) {
+ $confstr .= '!wrapclass=' . $this->options['wrapclass'];
}
if ( $this->mExtraKey != '' ) {
@@ -960,6 +1390,25 @@
$confstr = str_replace( ' ', '_', $confstr );
return $confstr;
+ }
+
+ /**
+ * Test whether these options are safe to cache
+ * @since 1.30
+ * @return bool
+ */
+ public function isSafeToCache() {
+ $defaults = self::getCanonicalOverrides() + self::getDefaults();
+ foreach ( $this->options as $option => $value ) {
+ if ( empty( self::$inCacheKey[$option] ) ) {
+ $v = $this->optionToString( $value );
+ $d = $this->optionToString( $defaults[$option]
);
+ if ( $v !== $d ) {
+ return false;
+ }
+ }
+ }
+ return true;
}
/**
@@ -1009,3 +1458,8 @@
} );
}
}
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=@{,@} foldmethod=marker
+ */
diff --git a/tests/phpunit/includes/deferred/LinksUpdateTest.php
b/tests/phpunit/includes/deferred/LinksUpdateTest.php
index 9cc3ffd..639c323 100644
--- a/tests/phpunit/includes/deferred/LinksUpdateTest.php
+++ b/tests/phpunit/includes/deferred/LinksUpdateTest.php
@@ -167,7 +167,7 @@
$this->assertRecentChangeByCategorization(
$title,
- $wikiPage->getParserOutput( new ParserOptions() ),
+ $wikiPage->getParserOutput(
ParserOptions::newCanonical() ),
Title::newFromText( 'Category:Foo' ),
[ [ 'Foo', '[[:Testing]] added to category' ] ]
);
@@ -177,7 +177,7 @@
$this->assertRecentChangeByCategorization(
$title,
- $wikiPage->getParserOutput( new ParserOptions() ),
+ $wikiPage->getParserOutput(
ParserOptions::newCanonical() ),
Title::newFromText( 'Category:Foo' ),
[
[ 'Foo', '[[:Testing]] added to category' ],
@@ -187,7 +187,7 @@
$this->assertRecentChangeByCategorization(
$title,
- $wikiPage->getParserOutput( new ParserOptions() ),
+ $wikiPage->getParserOutput(
ParserOptions::newCanonical() ),
Title::newFromText( 'Category:Bar' ),
[
[ 'Bar', '[[:Testing]] added to category' ],
@@ -211,7 +211,7 @@
$this->assertRecentChangeByCategorization(
$templateTitle,
- $templatePage->getParserOutput( new ParserOptions() ),
+ $templatePage->getParserOutput(
ParserOptions::newCanonical() ),
Title::newFromText( 'Baz' ),
[]
);
@@ -221,7 +221,7 @@
$this->assertRecentChangeByCategorization(
$templateTitle,
- $templatePage->getParserOutput( new ParserOptions() ),
+ $templatePage->getParserOutput(
ParserOptions::newCanonical() ),
Title::newFromText( 'Baz' ),
[ [
'Baz',
diff --git a/tests/phpunit/includes/parser/ParserOptionsTest.php
b/tests/phpunit/includes/parser/ParserOptionsTest.php
index aacdb1a..81f0564 100644
--- a/tests/phpunit/includes/parser/ParserOptionsTest.php
+++ b/tests/phpunit/includes/parser/ParserOptionsTest.php
@@ -6,13 +6,46 @@
class ParserOptionsTest extends MediaWikiTestCase {
/**
- * @dataProvider provideOptionsHash
+ * @dataProvider provideIsSafeToCache
+ * @param bool $expect Expected value
+ * @param array $options Options to set
+ */
+ public function testIsSafeToCache( $expect, $options ) {
+ $popt = ParserOptions::newCanonical();
+ foreach ( $options as $name => $value ) {
+ $popt->setOption( $name, $value );
+ }
+ $this->assertSame( $expect, $popt->isSafeToCache() );
+ }
+
+ public static function provideIsSafeToCache() {
+ return [
+ 'No overrides' => [ true, [] ],
+ 'In-key options are ok' => [ true, [
+ 'editsection' => false,
+ 'thumbsize' => 1e100,
+ 'wrapclass' => false,
+ ] ],
+ 'Non-in-key options are not ok' => [ false, [
+ 'removeComments' => false,
+ ] ],
+ 'Canonical override, not default (1)' => [ true, [
+ 'tidy' => true,
+ ] ],
+ 'Canonical override, not default (2)' => [ false, [
+ 'tidy' => false,
+ ] ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideOptionsHashPre30
* @param array $usedOptions Used options
* @param string $expect Expected value
* @param array $options Options to set
* @param array $globals Globals to set
*/
- public function testOptionsHash( $usedOptions, $expect, $options,
$globals = [] ) {
+ public function testOptionsHashPre30( $usedOptions, $expect, $options,
$globals = [] ) {
global $wgHooks;
$globals += [
@@ -28,10 +61,10 @@
foreach ( $options as $setter => $value ) {
$popt->$setter( $value );
}
- $this->assertSame( $expect, $popt->optionsHash( $usedOptions )
);
+ $this->assertSame( $expect, $popt->optionsHashPre30(
$usedOptions ) );
}
- public static function provideOptionsHash() {
+ public static function provideOptionsHashPre30() {
$used = [ 'wrapclass', 'editsection', 'printable' ];
return [
@@ -57,13 +90,99 @@
];
}
+ /**
+ * @dataProvider provideOptionsHash
+ * @param array $usedOptions Used options
+ * @param string $expect Expected value
+ * @param array $options Options to set
+ * @param array $globals Globals to set
+ */
+ public function testOptionsHash( $usedOptions, $expect, $options,
$globals = [] ) {
+ global $wgHooks;
+
+ $globals += [
+ 'wgRenderHashAppend' => '',
+ 'wgHooks' => [],
+ ];
+ $globals['wgHooks'] += [
+ 'PageRenderingHash' => [],
+ ] + $wgHooks;
+ $this->setMwGlobals( $globals );
+
+ $popt = ParserOptions::newCanonical();
+ foreach ( $options as $name => $value ) {
+ $popt->setOption( $name, $value );
+ }
+ $this->assertSame( $expect, $popt->optionsHash( $usedOptions )
);
+ }
+
+ public static function provideOptionsHash() {
+ $used = [ 'wrapclass', 'editsection', 'printable' ];
+
+ $classWrapper = TestingAccessWrapper::newFromClass(
ParserOptions::class );
+ $classWrapper->getDefaults();
+ $allUsableOptions = array_diff(
+ array_keys( $classWrapper->inCacheKey ),
+ array_keys( $classWrapper->lazyOptions )
+ );
+
+ return [
+ 'Canonical options, nothing used' => [ [], 'canonical',
[] ],
+ 'Canonical options, used some options' => [ $used,
'canonical', [] ],
+ 'Used some options, non-default values' => [
+ $used,
+ 'printable=1!wrapclass=foobar',
+ [
+ 'wrapclass' => 'foobar',
+ 'printable' => true,
+ ]
+ ],
+ 'Canonical options, used all non-lazy options' => [
$allUsableOptions, 'canonical', [] ],
+ 'Canonical options, nothing used, but with hooks and
$wgRenderHashAppend' => [
+ [],
+
'canonical!wgRenderHashAppend!onPageRenderingHash',
+ [],
+ [
+ 'wgRenderHashAppend' =>
'!wgRenderHashAppend',
+ 'wgHooks' => [ 'PageRenderingHash' => [
[ __CLASS__ . '::onPageRenderingHash' ] ] ],
+ ]
+ ],
+ ];
+ }
+
public static function onPageRenderingHash( &$confstr ) {
$confstr .= '!onPageRenderingHash';
}
+ /**
+ * @expectedException InvalidArgumentException
+ * @expectedExceptionMessage Unknown parser option bogus
+ */
+ public function testGetInvalidOption() {
+ $popt = ParserOptions::newCanonical();
+ $popt->getOption( 'bogus' );
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ * @expectedExceptionMessage Unknown parser option bogus
+ */
+ public function testSetInvalidOption() {
+ $popt = ParserOptions::newCanonical();
+ $popt->setOption( 'bogus', true );
+ }
+
public function testMatches() {
- $popt1 = new ParserOptions();
- $popt2 = new ParserOptions();
+ $classWrapper = TestingAccessWrapper::newFromClass(
ParserOptions::class );
+ $oldDefaults = $classWrapper->defaults;
+ $oldLazy = $classWrapper->lazyOptions;
+ $reset = new ScopedCallback( function () use ( $classWrapper,
$oldDefaults, $oldLazy ) {
+ $classWrapper->defaults = $oldDefaults;
+ $classWrapper->lazyOptions = $oldLazy;
+ } );
+
+ $popt1 = ParserOptions::newCanonical();
+ $popt2 = ParserOptions::newCanonical();
$this->assertTrue( $popt1->matches( $popt2 ) );
$popt1->enableLimitReport( true );
@@ -72,6 +191,17 @@
$popt2->setTidy( !$popt2->getTidy() );
$this->assertFalse( $popt1->matches( $popt2 ) );
+
+ $ctr = 0;
+ $classWrapper->defaults += [ __METHOD__ => null ];
+ $classWrapper->lazyOptions += [ __METHOD__ => function () use (
&$ctr ) {
+ return ++$ctr;
+ } ];
+ $popt1 = ParserOptions::newCanonical();
+ $popt2 = ParserOptions::newCanonical();
+ $this->assertFalse( $popt1->matches( $popt2 ) );
+
+ ScopedCallback::consume( $reset );
}
}
--
To view, visit https://gerrit.wikimedia.org/r/354504
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I7fb9ffca96e6bd04db44d2d5f2509ec96ad9371f
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Anomie <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: C. Scott Ananian <[email protected]>
Gerrit-Reviewer: Daniel Kinzler <[email protected]>
Gerrit-Reviewer: Jackmcbarn <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Tim Starling <[email protected]>
Gerrit-Reviewer: Zppix <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits