X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/parser/ParserOptions.php diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index 1bda0792..c7146a13 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -2,101 +2,856 @@ /** * 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 + */