]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - includes/parser/ParserOptions.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / parser / ParserOptions.php
index 1bda0792fa1bb785fec242f5b159f2aa612532ce..c7146a13063faa1f2a3e5093d90475e9e95ae69c 100644 (file)
 /**
  * Options for the PHP parser
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Parser
  */
+use Wikimedia\ScopedCallback;
+
 /**
- * Set options of the Parser
- * @todo document
+ * @brief Set options of the Parser
+ *
+ * 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 {
-       # All variables are supposed to be private in theory, although in practise this is not the case.
-       var $mUseDynamicDates;           # Use DateFormatter to format dates
-       var $mInterwikiMagic;            # Interlanguage links are removed and returned in an array
-       var $mAllowExternalImages;       # Allow external images inline
-       var $mAllowExternalImagesFrom;   # If not, any exception?
-       var $mEnableImageWhitelist;      # If not or it doesn't match, should we check an on-wiki whitelist?
-       var $mSkin;                      # Reference to the preferred skin
-       var $mDateFormat;                # Date format index
-       var $mEditSection;               # Create "edit section" links
-       var $mNumberHeadings;            # Automatically number headings
-       var $mAllowSpecialInclusion;     # Allow inclusion of special pages
-       var $mTidy;                      # Ask for tidy cleanup
-       var $mInterfaceMessage;          # Which lang to call for PLURAL and GRAMMAR
-       var $mTargetLanguage;            # Overrides above setting with arbitrary language
-       var $mMaxIncludeSize;            # Maximum size of template expansions, in bytes
-       var $mMaxPPNodeCount;            # Maximum number of nodes touched by PPFrame::expand()
-       var $mMaxPPExpandDepth;          # Maximum recursion depth in PPFrame::expand()
-       var $mMaxTemplateDepth;          # Maximum recursion depth for templates within templates
-       var $mRemoveComments;            # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
-       var $mTemplateCallback;          # Callback for template fetching
-       var $mEnableLimitReport;         # Enable limit report in an HTML comment on output
-       var $mTimestamp;                 # Timestamp used for {{CURRENTDAY}} etc.
-       var $mExternalLinkTarget;        # Target attribute for external links
-       var $mMath;                      # User math preference (as integer)
-       var $mUserLang;                  # Language code of the User language.
-       var $mThumbSize;                 # Thumb size preferred by the user.
-       var $mCleanSignatures;           #
-
-       var $mUser;                      # Stored user object, just used to initialise the skin
-       var $mIsPreview;                 # Parsing the page for a "preview" operation
-       var $mIsSectionPreview;          # Parsing the page for a "preview" operation on a single section
-       var $mIsPrintable;               # Parsing the printable version of the page
-       
-       var $mExtraKey = '';             # Extra key that should be present in the caching key.
-       
-       protected $onAccessCallback = null;
-       
-       function getUseDynamicDates()               { return $this->mUseDynamicDates; }
-       function getInterwikiMagic()                { return $this->mInterwikiMagic; }
-       function getAllowExternalImages()           { return $this->mAllowExternalImages; }
-       function getAllowExternalImagesFrom()       { return $this->mAllowExternalImagesFrom; }
-       function getEnableImageWhitelist()          { return $this->mEnableImageWhitelist; }
-       function getEditSection()                   { $this->optionUsed('editsection');
-                                                     return $this->mEditSection; }
-       function getNumberHeadings()                { $this->optionUsed('numberheadings');
-                                                     return $this->mNumberHeadings; }
-       function getAllowSpecialInclusion()         { return $this->mAllowSpecialInclusion; }
-       function getTidy()                          { return $this->mTidy; }
-       function getInterfaceMessage()              { return $this->mInterfaceMessage; }
-       function getTargetLanguage()                { return $this->mTargetLanguage; }
-       function getMaxIncludeSize()                { return $this->mMaxIncludeSize; }
-       function getMaxPPNodeCount()                { return $this->mMaxPPNodeCount; }
-       function getMaxPPExpandDepth()              { return $this->mMaxPPExpandDepth; }
-       function getMaxTemplateDepth()              { return $this->mMaxTemplateDepth; }
-       function getRemoveComments()                { return $this->mRemoveComments; }
-       function getTemplateCallback()              { return $this->mTemplateCallback; }
-       function getEnableLimitReport()             { return $this->mEnableLimitReport; }
-       function getCleanSignatures()               { return $this->mCleanSignatures; }
-       function getExternalLinkTarget()            { return $this->mExternalLinkTarget; }
-       function getMath()                          { $this->optionUsed('math');
-                                                     return $this->mMath; }
-       function getThumbSize()                     { $this->optionUsed('thumbsize');
-                                                     return $this->mThumbSize; }
-       
-       function getIsPreview()                     { return $this->mIsPreview; }
-       function getIsSectionPreview()              { return $this->mIsSectionPreview; }
-       function getIsPrintable()                   { $this->optionUsed('printable');
-                                                     return $this->mIsPrintable; }
-
-       function getSkin( $title = null ) {
-               if ( !isset( $this->mSkin ) ) {
-                       $this->mSkin = $this->mUser->getSkin( $title );
+
+       /**
+        * Default values for all options that are relevant for caching.
+        * @see self::getDefaults()
+        * @var array|null
+        */
+       private static $defaults = null;
+
+       /**
+        * Lazy-loaded options
+        * @var callback[]
+        */
+       private static $lazyOptions = [
+               'dateformat' => [ __CLASS__, 'initDateFormat' ],
+       ];
+
+       /**
+        * Specify options that are included in the cache key
+        * @var array
+        */
+       private static $inCacheKey = [
+               'dateformat' => true,
+               'numberheadings' => true,
+               'thumbsize' => true,
+               'stubthreshold' => true,
+               'printable' => true,
+               'userlang' => true,
+               'wrapclass' => true,
+       ];
+
+       /**
+        * Current values for all options that are relevant for caching.
+        * @var array
+        */
+       private $options;
+
+       /**
+        * Timestamp used for {{CURRENTDAY}} etc.
+        * @var string|null
+        * @note Caching based on parse time is handled externally
+        */
+       private $mTimestamp;
+
+       /**
+        * The edit section flag is in ParserOptions for historical reasons, but
+        * doesn't actually affect the parser output since Feb 2015.
+        * @var bool
+        */
+       private $mEditSection = true;
+
+       /**
+        * Stored user object
+        * @var User
+        * @todo Track this for caching somehow without fragmenting the cache insanely
+        */
+       private $mUser;
+
+       /**
+        * 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;
+
+       /**
+        * 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;
+
+       /**
+        * Appended to the options hash
+        */
+       private $mExtraKey = '';
+
+       /**
+        * @name Option accessors
+        * @{
+        */
+
+       /**
+        * 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->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->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->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->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 );
+       }
+
+       /**
+        * Automatically number headings?
+        * @return bool
+        */
+       public function getNumberHeadings() {
+               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->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->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->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->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->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->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->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->getOption( 'maxPPExpandDepth' );
+       }
+
+       /**
+        * Maximum recursion depth for templates within templates
+        * @return int
+        */
+       public function getMaxTemplateDepth() {
+               return $this->getOption( 'maxTemplateDepth' );
+       }
+
+       /**
+        * 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->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->getOption( 'removeComments' );
+       }
+
+       /**
+        * 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 );
+       }
+
+       /**
+        * Enable limit report in an HTML comment on output
+        * @return bool
+        */
+       public function getEnableLimitReport() {
+               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->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->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->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->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() {
+               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() {
+               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->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->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() {
+               return $this->getOption( 'printable' );
+       }
+
+       /**
+        * 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->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() {
+               return $this->getOption( 'dateformat' );
+       }
+
+       /**
+        * 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 );
+       }
+
+       /**
+        * Get the user language used by the parser for this page and split the parser cache.
+        *
+        * @warning: Calling this causes the parser cache to be fragmented by user language!
+        * To avoid cache fragmentation, output should not depend on the user language.
+        * Use Parser::getFunctionLang() or Parser::getTargetLanguage() instead!
+        *
+        * @note This function will trigger a cache fragmentation by recording the
+        * 'userlang' option, see optionUsed(). This is done to avoid cache pollution
+        * when the page is rendered based on the language of the user.
+        *
+        * @note When saving, this will return the default language instead of the user's.
+        * {{int: }} uses this which used to produce inconsistent link tables (T16404).
+        *
+        * @return Language
+        * @since 1.19
+        */
+       public function getUserLangObj() {
+               return $this->getOption( 'userlang' );
+       }
+
+       /**
+        * Same as getUserLangObj() but returns a string instead.
+        *
+        * @warning: Calling this causes the parser cache to be fragmented by user language!
+        * To avoid cache fragmentation, output should not depend on the user language.
+        * Use Parser::getFunctionLang() or Parser::getTargetLanguage() instead!
+        *
+        * @see getUserLangObj()
+        *
+        * @return string Language code
+        * @since 1.17
+        */
+       public function getUserLang() {
+               return $this->getUserLangObj()->getCode();
+       }
+
+       /**
+        * 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->mSkin;
+
+               return $this->setOptionLegacy( 'userlang', $x );
+       }
+
+       /**
+        * Are magic ISBN links enabled?
+        * @since 1.28
+        * @return bool
+        */
+       public function getMagicISBNLinks() {
+               return $this->getOption( 'magicISBNLinks' );
        }
 
-       function getDateFormat() {
-               $this->optionUsed('dateformat');
-               if ( !isset( $this->mDateFormat ) ) {
-                       $this->mDateFormat = $this->mUser->getDatePreference();
+       /**
+        * Are magic PMID links enabled?
+        * @since 1.28
+        * @return bool
+        */
+       public function getMagicPMIDLinks() {
+               return $this->getOption( 'magicPMIDLinks' );
+       }
+       /**
+        * Are magic RFC links enabled?
+        * @since 1.28
+        * @return bool
+        */
+       public function getMagicRFCLinks() {
+               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->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 $x Value to set or null to get current value
+        * @return bool Current value for allowUnsafeRawHtml
+        */
+       public function setAllowUnsafeRawHtml( $x ) {
+               return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
+       }
+
+       /**
+        * Class to use to wrap output from Parser::parse()
+        * @since 1.30
+        * @return string|bool
+        */
+       public function getWrapOutputClass() {
+               return $this->getOption( 'wrapclass' );
+       }
+
+       /**
+        * CSS class to use to wrap output from Parser::parse()
+        * @since 1.30
+        * @param string|bool $className Set false to disable wrapping.
+        * @return string|bool Current value
+        */
+       public function setWrapOutputClass( $className ) {
+               if ( $className === true ) { // DWIM, they probably want the default class name
+                       $className = 'mw-parser-output';
                }
-               return $this->mDateFormat;
+               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 );
        }
 
-       function getTimestamp() {
+       /**
+        * 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();
                }
@@ -104,235 +859,551 @@ class ParserOptions {
        }
 
        /**
-        * You shouldn't use this. Really. $parser->getFunctionLang() is all you need.
-        * Using this fragments the cache and is discouraged. Yes, {{int: }} uses this,
-        * producing inconsistent tables (Bug 14404).
-        */
-       function getUserLang() {
-               $this->optionUsed('userlang');
-               return $this->mUserLang;
-       }
-
-       function setUseDynamicDates( $x )           { return wfSetVar( $this->mUseDynamicDates, $x ); }
-       function setInterwikiMagic( $x )            { return wfSetVar( $this->mInterwikiMagic, $x ); }
-       function setAllowExternalImages( $x )       { return wfSetVar( $this->mAllowExternalImages, $x ); }
-       function setAllowExternalImagesFrom( $x )   { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
-       function setEnableImageWhitelist( $x )      { return wfSetVar( $this->mEnableImageWhitelist, $x ); }
-       function setDateFormat( $x )                { return wfSetVar( $this->mDateFormat, $x ); }
-       function setEditSection( $x )               { return wfSetVar( $this->mEditSection, $x ); }
-       function setNumberHeadings( $x )            { return wfSetVar( $this->mNumberHeadings, $x ); }
-       function setAllowSpecialInclusion( $x )     { return wfSetVar( $this->mAllowSpecialInclusion, $x ); }
-       function setTidy( $x )                      { return wfSetVar( $this->mTidy, $x); }
-       function setSkin( $x )                      { $this->mSkin = $x; }
-       function setInterfaceMessage( $x )          { return wfSetVar( $this->mInterfaceMessage, $x); }
-       function setTargetLanguage( $x )            { return wfSetVar( $this->mTargetLanguage, $x, true ); }
-       function setMaxIncludeSize( $x )            { return wfSetVar( $this->mMaxIncludeSize, $x ); }
-       function setMaxPPNodeCount( $x )            { return wfSetVar( $this->mMaxPPNodeCount, $x ); }
-       function setMaxTemplateDepth( $x )          { return wfSetVar( $this->mMaxTemplateDepth, $x ); }
-       function setRemoveComments( $x )            { return wfSetVar( $this->mRemoveComments, $x ); }
-       function setTemplateCallback( $x )          { return wfSetVar( $this->mTemplateCallback, $x ); }
-       function enableLimitReport( $x = true )     { return wfSetVar( $this->mEnableLimitReport, $x ); }
-       function setTimestamp( $x )                 { return wfSetVar( $this->mTimestamp, $x ); }
-       function setCleanSignatures( $x )           { return wfSetVar( $this->mCleanSignatures, $x ); }
-       function setExternalLinkTarget( $x )        { return wfSetVar( $this->mExternalLinkTarget, $x ); }
-       function setMath( $x )                      { return wfSetVar( $this->mMath, $x ); }
-       function setUserLang( $x )                  { return wfSetVar( $this->mUserLang, $x ); }
-       function setThumbSize( $x )                 { return wfSetVar( $this->mThumbSize, $x ); }
-       
-       function setIsPreview( $x )                 { return wfSetVar( $this->mIsPreview, $x ); }
-       function setIsSectionPreview( $x )          { return wfSetVar( $this->mIsSectionPreview, $x ); }
-       function setIsPrintable( $x )               { return wfSetVar( $this->mIsPrintable, $x ); }
+        * 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 );
+       }
 
        /**
-        * Extra key that should be present in the parser cache key.
+        * Create "edit section" links?
+        * @return bool
         */
-       function addExtraKey( $key ) {
-               $this->mExtraKey .= '!' . $key;
+       public function getEditSection() {
+               return $this->mEditSection;
        }
 
-       function __construct( $user = null ) {
-               $this->initialiseFromUser( $user );
+       /**
+        * Create "edit section" links?
+        * @param bool|null $x New value (null is no change)
+        * @return bool Old value
+        */
+       public function setEditSection( $x ) {
+               return wfSetVar( $this->mEditSection, $x );
        }
 
        /**
-        * Get parser options
+        * Set the redirect target.
+        *
+        * Note that setting or changing this does not *make* the page a redirect
+        * or change its target, it merely records the information for reference
+        * during the parse.
         *
-        * @param $user User object
-        * @return ParserOptions object
+        * @since 1.24
+        * @param Title|null $title
         */
-       static function newFromUser( $user ) {
-               return new ParserOptions( $user );
+       function setRedirectTarget( $title ) {
+               $this->redirectTarget = $title;
        }
 
-       /** Get user options */
-       function initialiseFromUser( $userInput ) {
-               global $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
-               global $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion, $wgMaxArticleSize;
-               global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth, $wgCleanSignatures;
-               global $wgExternalLinkTarget, $wgLang;
+       /**
+        * Get the previously-set redirect target.
+        *
+        * @since 1.24
+        * @return Title|null
+        */
+       function getRedirectTarget() {
+               return $this->redirectTarget;
+       }
 
-               wfProfileIn( __METHOD__ );
+       /**
+        * 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 ) {
+               $this->mExtraKey .= '!' . $key;
+       }
+
+       /**
+        * Current user
+        * @return User
+        */
+       public function getUser() {
+               return $this->mUser;
+       }
 
-               if ( !$userInput ) {
+       /**
+        * @warning For interaction with the parser cache, use
+        *  WikiPage::makeParserOptions(), ContentHandler::makeParserOptions(), or
+        *  ParserOptions::newCanonical() instead.
+        * @param User $user
+        * @param Language $lang
+        */
+       public function __construct( $user = null, $lang = null ) {
+               if ( $user === null ) {
                        global $wgUser;
-                       if ( isset( $wgUser ) ) {
-                               $user = $wgUser;
-                       } else {
+                       if ( $wgUser === null ) {
                                $user = new User;
+                       } else {
+                               $user = $wgUser;
                        }
-               } else {
-                       $user =& $userInput;
                }
+               if ( $lang === null ) {
+                       global $wgLang;
+                       if ( !StubObject::isRealObject( $wgLang ) ) {
+                               $wgLang->_unstub();
+                       }
+                       $lang = $wgLang;
+               }
+               $this->initialiseFromUser( $user, $lang );
+       }
+
+       /**
+        * 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
+        */
+       public static function newFromAnon() {
+               global $wgContLang;
+               return new ParserOptions( new User, $wgContLang );
+       }
+
+       /**
+        * 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
+        */
+       public static function newFromUser( $user ) {
+               return new ParserOptions( $user );
+       }
+
+       /**
+        * 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
+        */
+       public static function newFromUserAndLang( User $user, Language $lang ) {
+               return new ParserOptions( $user, $lang );
+       }
+
+       /**
+        * 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,
+                               '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,
+                       ];
+
+                       // @codingStandardsIgnoreStart Squiz.WhiteSpace.OperatorSpacing.NoSpaceAfterAmp
+                       Hooks::run( 'ParserOptionsRegister', [
+                               &self::$defaults,
+                               &self::$inCacheKey,
+                               &self::$lazyOptions,
+                       ] );
+                       // @codingStandardsIgnoreEnd
+
+                       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,
+               ];
+       }
+
+       /**
+        * Get user options
+        *
+        * @param User $user
+        * @param Language $lang
+        */
+       private function initialiseFromUser( $user, $lang ) {
+               $this->options = self::getDefaults();
 
                $this->mUser = $user;
+               $this->options['numberheadings'] = $user->getOption( 'numberheadings' );
+               $this->options['thumbsize'] = $user->getOption( 'thumbsize' );
+               $this->options['stubthreshold'] = $user->getStubThreshold();
+               $this->options['userlang'] = $lang;
+       }
+
+       /**
+        * Check if these options match that of another options set
+        *
+        * This ignores report limit settings that only affect HTML comments
+        *
+        * @param ParserOptions $other
+        * @return bool
+        * @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;
+                       }
+               }
 
-               $this->mUseDynamicDates = $wgUseDynamicDates;
-               $this->mInterwikiMagic = $wgInterwikiMagic;
-               $this->mAllowExternalImages = $wgAllowExternalImages;
-               $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
-               $this->mEnableImageWhitelist = $wgEnableImageWhitelist;
-               $this->mSkin = null; # Deferred
-               $this->mDateFormat = null; # Deferred
-               $this->mEditSection = true;
-               $this->mNumberHeadings = $user->getOption( 'numberheadings' );
-               $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
-               $this->mTidy = false;
-               $this->mInterfaceMessage = false;
-               $this->mTargetLanguage = null; // default depends on InterfaceMessage setting
-               $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
-               $this->mMaxPPNodeCount = $wgMaxPPNodeCount;
-               $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
-               $this->mMaxTemplateDepth = $wgMaxTemplateDepth;
-               $this->mRemoveComments = true;
-               $this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' );
-               $this->mEnableLimitReport = false;
-               $this->mCleanSignatures = $wgCleanSignatures;
-               $this->mExternalLinkTarget = $wgExternalLinkTarget;
-               $this->mMath = $user->getOption( 'math' );
-               $this->mUserLang = $wgLang->getCode();
-               $this->mThumbSize = $user->getOption( 'thumbsize' );
-               
-               $this->mIsPreview = false;
-               $this->mIsSectionPreview = false;
-               $this->mIsPrintable = false;
-
-               wfProfileOut( __METHOD__ );
+               // Compare most other fields
+               $fields = array_keys( get_class_vars( __CLASS__ ) );
+               $fields = array_diff( $fields, [
+                       'defaults', // static
+                       'lazyOptions', // static
+                       'inCacheKey', // static
+                       'options', // Already checked above
+                       'onAccessCallback', // only used for ParserOutput option tracking
+               ] );
+               foreach ( $fields as $field ) {
+                       if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
+                               return false;
+                       }
+               }
+
+               return true;
        }
 
        /**
         * Registers a callback for tracking which ParserOptions which are used.
         * This is a private API with the parser.
+        * @param callable $callback
         */
-       function registerWatcher( $callback ) {
+       public function registerWatcher( $callback ) {
                $this->onAccessCallback = $callback;
        }
-       
+
        /**
         * Called when an option is accessed.
+        * Calls the watcher that was set using registerWatcher().
+        * Typically, the watcher callback is ParserOutput::registerOption().
+        * The information registered that way will be used by ParserCache::save().
+        *
+        * @param string $optionName Name of the option
         */
-       protected function optionUsed( $optionName ) {
+       public function optionUsed( $optionName ) {
                if ( $this->onAccessCallback ) {
                        call_user_func( $this->onAccessCallback, $optionName );
                }
        }
-       
+
        /**
-        * Returns the full array of options that would have been used by 
+        * 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.
+        * @deprecated since 1.30. You probably want self::allCacheVaryingOptions() instead.
+        * @return array
         */
        public static function legacyOptions() {
-               global $wgUseDynamicDates;
-               $legacyOpts = array( 'math', 'stubthreshold', 'numberheadings', 'userlang', 'thumbsize', 'editsection', 'printable' );
-               if ( $wgUseDynamicDates ) {
-                       $legacyOpts[] = 'dateformat';
+               wfDeprecated( __METHOD__, '1.30' );
+               return [
+                       'stubthreshold',
+                       'numberheadings',
+                       'userlang',
+                       'thumbsize',
+                       'editsection',
+                       'printable'
+               ];
+       }
+
+       /**
+        * Return all option keys that vary the options hash
+        * @since 1.30
+        * @return string[]
+        */
+       public static function allCacheVaryingOptions() {
+               // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
+               // already been called.
+               if ( self::$defaults === null ) {
+                       self::getDefaults();
                }
-               return $legacyOpts;
+               return array_keys( array_filter( self::$inCacheKey ) );
        }
-       
+
+       /**
+        * 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 sharign the options with vary for the same page share 
+        * so users sharing the options with vary for the same page share
         * the same cached data safely.
-        * 
-        * Replaces User::getPageRenderingHash()
-        *
-        * 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
-        * @return \string Page rendering hash
-        */
-       public function optionsHash( $forOptions ) {
-               global $wgContLang, $wgRenderHashAppend;
-
-               $confstr = '';
-               
-               if ( in_array( 'math', $forOptions ) )
-                       $confstr .= $this->mMath;
-               else
-                       $confstr .= '*';
-                       
-
-               // Space assigned for the stubthreshold but unused
-               // since it disables the parser cache, its value will always 
-               // be 0 when this function is called by parsercache.
-               // The conditional is here to avoid a confusing 0
-               if ( true || in_array( 'stubthreshold', $forOptions ) )
-                       $confstr .= '!0' ;
-               else
-                       $confstr .= '!*' ;
-
-               if ( in_array( 'dateformat', $forOptions ) )
-                       $confstr .= '!' . $this->getDateFormat();
-               
-               if ( in_array( 'numberheadings', $forOptions ) )
-                       $confstr .= '!' . ( $this->mNumberHeadings ? '1' : '' );
-               else
-                       $confstr .= '!*';
-               
-               if ( in_array( 'userlang', $forOptions ) )
-                       $confstr .= '!' . $this->mUserLang;
-               else
-                       $confstr .= '!*';
-
-               if ( in_array( 'thumbsize', $forOptions ) )
-                       $confstr .= '!' . $this->mThumbSize;
-               else
-                       $confstr .= '!*';
+        * @param array $forOptions
+        * @param Title $title Used to get the content language of the page (since r97636)
+        * @return string Page rendering hash
+        */
+       public function optionsHash( $forOptions, $title = null ) {
+               global $wgRenderHashAppend;
+
+               $options = $this->options;
+               $defaults = self::getCanonicalOverrides() + self::getDefaults();
+               $inCacheKey = self::$inCacheKey;
+
+               // Historical hack: 'editsection' hasn't been a true parser option since
+               // Feb 2015 (instead the parser outputs a constant placeholder and post-parse
+               // processing handles the option). But Wikibase forces it in $forOptions
+               // and expects the cache key to still vary on it for T85252.
+               // @deprecated since 1.30, Wikibase should use addExtraKey() or something instead.
+               if ( in_array( 'editsection', $forOptions, true ) ) {
+                       $options['editsection'] = $this->mEditSection;
+                       $defaults['editsection'] = true;
+                       $inCacheKey['editsection'] = true;
+                       ksort( $inCacheKey );
+               }
+
+               // 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.
+               $values = [];
+               foreach ( $inCacheKey as $option => $include ) {
+                       if ( $include && in_array( $option, $forOptions, true ) ) {
+                               $v = $this->optionToString( $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
-               // FIXME: This is just a way of retrieving the url/user preferred variant
-               $confstr .= $wgContLang->getExtraHashOptions();
+               // @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();
+               }
 
-               // Since the skin could be overloading link(), it should be
-               // included here but in practice, none of our skins do that.
-               // $confstr .= "!" . $this->mSkin->getSkinName();
-               
                $confstr .= $wgRenderHashAppend;
 
-               if ( !in_array( 'editsection', $forOptions ) ) {
-                       $confstr .= '!*';
-               } elseif ( !$this->mEditSection ) {
-                       $confstr .= '!edit=0';
-               }
-               
-               if (  $this->mIsPrintable && in_array( 'printable', $forOptions ) )
-                       $confstr .= '!printable=1';
-               
-               if ( $this->mExtraKey != '' )
+               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.
-               wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
+               Hooks::run( 'PageRenderingHash', [ &$confstr, $this->getUser(), &$forOptions ] );
 
                // Make it a valid memcached key fragment
                $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;
+       }
+
+       /**
+        * Sets a hook to force that a page exists, and sets a current revision callback to return
+        * a revision with custom content when the current revision of the page is requested.
+        *
+        * @since 1.25
+        * @param Title $title
+        * @param Content $content
+        * @param User $user The user that the fake revision is attributed to
+        * @return ScopedCallback to unset the hook
+        */
+       public function setupFakeRevision( $title, $content, $user ) {
+               $oldCallback = $this->setCurrentRevisionCallback(
+                       function (
+                               $titleToCheck, $parser = false ) use ( $title, $content, $user, &$oldCallback
+                       ) {
+                               if ( $titleToCheck->equals( $title ) ) {
+                                       return new Revision( [
+                                               'page' => $title->getArticleID(),
+                                               'user_text' => $user->getName(),
+                                               'user' => $user->getId(),
+                                               'parent_id' => $title->getLatestRevID(),
+                                               'title' => $title,
+                                               'content' => $content
+                                       ] );
+                               } else {
+                                       return call_user_func( $oldCallback, $titleToCheck, $parser );
+                               }
+                       }
+               );
+
+               global $wgHooks;
+               $wgHooks['TitleExists'][] =
+                       function ( $titleToCheck, &$exists ) use ( $title ) {
+                               if ( $titleToCheck->equals( $title ) ) {
+                                       $exists = true;
+                               }
+                       };
+               end( $wgHooks['TitleExists'] );
+               $key = key( $wgHooks['TitleExists'] );
+               LinkCache::singleton()->clearBadLink( $title->getPrefixedDBkey() );
+               return new ScopedCallback( function () use ( $title, $key ) {
+                       global $wgHooks;
+                       unset( $wgHooks['TitleExists'][$key] );
+                       LinkCache::singleton()->clearLink( $title );
+               } );
+       }
 }
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=@{,@} foldmethod=marker
+ */