Update git submodules
[mediawiki.git] / includes / parser / ParserOptions.php
blobe1bd1404dc793e16412b0a786588e4316ae46ad9
1 <?php
2 /**
3 * Options for the PHP parser
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
20 * @file
21 * @ingroup Parser
24 use MediaWiki\HookContainer\HookRunner;
25 use MediaWiki\MainConfigNames;
26 use MediaWiki\MediaWikiServices;
27 use MediaWiki\Revision\MutableRevisionRecord;
28 use MediaWiki\Revision\SlotRecord;
29 use MediaWiki\StubObject\StubObject;
30 use MediaWiki\Title\Title;
31 use MediaWiki\User\User;
32 use MediaWiki\User\UserIdentity;
33 use MediaWiki\Utils\MWTimestamp;
34 use Wikimedia\ScopedCallback;
36 /**
37 * @brief Set options of the Parser
39 * How to add an option in core:
40 * 1. Add it to one of the arrays in ParserOptions::setDefaults()
41 * 2. If necessary, add an entry to ParserOptions::$inCacheKey
42 * 3. Add a getter and setter in the section for that.
44 * How to add an option in an extension:
45 * 1. Use the 'ParserOptionsRegister' hook to register it.
46 * 2. Where necessary, use $popt->getOption() and $popt->setOption()
47 * to access it.
49 * @ingroup Parser
51 class ParserOptions {
53 /**
54 * Default values for all options that are relevant for caching.
55 * @see self::getDefaults()
56 * @var array|null
58 private static $defaults = null;
60 /**
61 * Lazy-loaded options
62 * @var callable[]|null
64 private static $lazyOptions = null;
66 /**
67 * Initial lazy-loaded options (before hook)
68 * @var callable[]
70 private static $initialLazyOptions = [
71 'dateformat' => [ __CLASS__, 'initDateFormat' ],
72 'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
73 'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
76 /**
77 * Specify options that are included in the cache key
78 * @var array|null
80 private static $cacheVaryingOptionsHash = null;
82 /**
83 * Initial inCacheKey options (before hook)
84 * @var array
86 private static $initialCacheVaryingOptionsHash = [
87 'dateformat' => true,
88 'thumbsize' => true,
89 'printable' => true,
90 'userlang' => true,
91 'useParsoid' => true,
94 /**
95 * Specify pseudo-options that are actually callbacks.
96 * These must be ignored when checking for cacheability.
97 * @var array
99 private static $callbacks = [
100 'currentRevisionRecordCallback' => true,
101 'templateCallback' => true,
102 'speculativeRevIdCallback' => true,
103 'speculativePageIdCallback' => true,
107 * Current values for all options that are relevant for caching.
108 * @var array
110 private $options;
113 * Timestamp used for {{CURRENTDAY}} etc.
114 * @var string|null
115 * @note Caching based on parse time is handled externally
117 private $mTimestamp;
120 * Stored user object
121 * @var UserIdentity
122 * @todo Track this for caching somehow without fragmenting the cache
124 private $mUser;
127 * Function to be called when an option is accessed.
128 * @var callable|null
129 * @note Used for collecting used options, does not affect caching
131 private $onAccessCallback = null;
134 * If the page being parsed is a redirect, this should hold the redirect
135 * target.
136 * @var Title|null
137 * @todo Track this for caching somehow
139 private $redirectTarget = null;
142 * Appended to the options hash
144 private $mExtraKey = '';
147 * The reason for rendering the content.
148 * @var string
150 private $renderReason = 'unknown';
153 * Fetch an option and track that is was accessed
154 * @since 1.30
155 * @param string $name Option name
156 * @return mixed
158 public function getOption( $name ) {
159 if ( !array_key_exists( $name, $this->options ) ) {
160 throw new InvalidArgumentException( "Unknown parser option $name" );
163 $this->lazyLoadOption( $name );
164 $this->optionUsed( $name );
165 return $this->options[$name];
169 * @param string $name Lazy load option without tracking usage
171 private function lazyLoadOption( $name ) {
172 $lazyOptions = self::getLazyOptions();
173 if ( isset( $lazyOptions[$name] ) && $this->options[$name] === null ) {
174 $this->options[$name] = call_user_func( $lazyOptions[$name], $this, $name );
179 * Resets lazy loaded options to null in the provided $options array
180 * @param array $options
181 * @return array
183 private function nullifyLazyOption( array $options ): array {
184 return array_fill_keys( array_keys( self::getLazyOptions() ), null ) + $options;
188 * Get lazy-loaded options.
190 * This array should be initialised by the constructor. The return type
191 * hint is used as an assertion to ensure this has happened and to coerce
192 * the type for static analysis.
194 * @internal Public for testing only
196 * @return array
198 public static function getLazyOptions(): array {
199 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
200 // already been called.
201 if ( self::$lazyOptions === null ) {
202 self::getDefaults();
204 return self::$lazyOptions;
208 * Get cache varying options, with the name of the option in the key, and a
209 * boolean in the value which indicates whether the cache is indeed varied.
211 * @see self::allCacheVaryingOptions()
213 * @return array
215 private static function getCacheVaryingOptionsHash(): array {
216 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
217 // already been called.
218 if ( self::$cacheVaryingOptionsHash === null ) {
219 self::getDefaults();
221 return self::$cacheVaryingOptionsHash;
225 * Set an option, generically
226 * @since 1.30
227 * @param string $name Option name
228 * @param mixed $value New value. Passing null will set null, unlike many
229 * of the existing accessors which ignore null for historical reasons.
230 * @return mixed Old value
232 public function setOption( $name, $value ) {
233 if ( !array_key_exists( $name, $this->options ) ) {
234 throw new InvalidArgumentException( "Unknown parser option $name" );
236 $old = $this->options[$name];
237 $this->options[$name] = $value;
238 return $old;
242 * Legacy implementation
243 * @since 1.30 For implementing legacy setters only. Don't use this in new code.
244 * @deprecated since 1.30
245 * @param string $name Option name
246 * @param mixed $value New value. Passing null does not set the value.
247 * @return mixed Old value
249 protected function setOptionLegacy( $name, $value ) {
250 if ( !array_key_exists( $name, $this->options ) ) {
251 throw new InvalidArgumentException( "Unknown parser option $name" );
253 return wfSetVar( $this->options[$name], $value );
257 * Whether to extract interlanguage links
259 * When true, interlanguage links will be returned by
260 * ParserOutput::getLanguageLinks() instead of generating link HTML.
262 * @return bool
264 public function getInterwikiMagic() {
265 return $this->getOption( 'interwikiMagic' );
269 * Specify whether to extract interlanguage links
270 * @param bool|null $x New value (null is no change)
271 * @return bool Old value
273 public function setInterwikiMagic( $x ) {
274 return $this->setOptionLegacy( 'interwikiMagic', $x );
278 * Allow all external images inline?
279 * @return bool
281 public function getAllowExternalImages() {
282 return $this->getOption( 'allowExternalImages' );
286 * Allow all external images inline?
287 * @param bool|null $x New value (null is no change)
288 * @return bool Old value
289 * @deprecated since 1.35; per-parser configuration of image handling via
290 * parser options is deprecated. Use site configuration.
292 public function setAllowExternalImages( $x ) {
293 wfDeprecated( __METHOD__, '1.35' );
294 return $this->setOptionLegacy( 'allowExternalImages', $x );
298 * External images to allow
300 * When self::getAllowExternalImages() is false
302 * @return string|string[] URLs to allow
304 public function getAllowExternalImagesFrom() {
305 return $this->getOption( 'allowExternalImagesFrom' );
309 * External images to allow
311 * When self::getAllowExternalImages() is false
313 * @param string|string[]|null $x New value (null is no change)
314 * @return string|string[] Old value
315 * @deprecated since 1.35; per-parser configuration of image handling via
316 * parser options is deprecated. Use site configuration.
318 public function setAllowExternalImagesFrom( $x ) {
319 wfDeprecated( __METHOD__, '1.35' );
320 return $this->setOptionLegacy( 'allowExternalImagesFrom', $x );
324 * Use the on-wiki external image whitelist?
325 * @return bool
327 public function getEnableImageWhitelist() {
328 return $this->getOption( 'enableImageWhitelist' );
332 * Use the on-wiki external image whitelist?
333 * @param bool|null $x New value (null is no change)
334 * @return bool Old value
335 * @deprecated since 1.35; per-parser configuration of image handling via
336 * parser options is deprecated. Use site configuration.
338 public function setEnableImageWhitelist( $x ) {
339 wfDeprecated( __METHOD__, '1.35' );
340 return $this->setOptionLegacy( 'enableImageWhitelist', $x );
344 * Allow inclusion of special pages?
345 * @return bool
347 public function getAllowSpecialInclusion() {
348 return $this->getOption( 'allowSpecialInclusion' );
352 * Allow inclusion of special pages?
353 * @param bool|null $x New value (null is no change)
354 * @return bool Old value
356 public function setAllowSpecialInclusion( $x ) {
357 return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
361 * Parsing an interface message?
362 * @return bool
364 public function getInterfaceMessage() {
365 return $this->getOption( 'interfaceMessage' );
369 * Parsing an interface message?
370 * @param bool|null $x New value (null is no change)
371 * @return bool Old value
373 public function setInterfaceMessage( $x ) {
374 return $this->setOptionLegacy( 'interfaceMessage', $x );
378 * Target language for the parse
379 * @return Language|null
381 public function getTargetLanguage() {
382 return $this->getOption( 'targetLanguage' );
386 * Target language for the parse
387 * @param Language|null $x New value
388 * @return Language|null Old value
390 public function setTargetLanguage( $x ) {
391 return $this->setOption( 'targetLanguage', $x );
395 * Maximum size of template expansions, in bytes
396 * @return int
398 public function getMaxIncludeSize() {
399 return $this->getOption( 'maxIncludeSize' );
403 * Maximum size of template expansions, in bytes
404 * @param int|null $x New value (null is no change)
405 * @return int Old value
407 public function setMaxIncludeSize( $x ) {
408 return $this->setOptionLegacy( 'maxIncludeSize', $x );
412 * Maximum number of nodes touched by PPFrame::expand()
413 * @return int
415 public function getMaxPPNodeCount() {
416 return $this->getOption( 'maxPPNodeCount' );
420 * Maximum number of nodes touched by PPFrame::expand()
421 * @param int|null $x New value (null is no change)
422 * @return int Old value
424 public function setMaxPPNodeCount( $x ) {
425 return $this->setOptionLegacy( 'maxPPNodeCount', $x );
429 * Maximum recursion depth in PPFrame::expand()
430 * @return int
432 public function getMaxPPExpandDepth() {
433 return $this->getOption( 'maxPPExpandDepth' );
437 * Maximum recursion depth for templates within templates
438 * @return int
439 * @internal Only used by Parser (T318826)
441 public function getMaxTemplateDepth() {
442 return $this->getOption( 'maxTemplateDepth' );
446 * Maximum recursion depth for templates within templates
447 * @param int|null $x New value (null is no change)
448 * @return int Old value
449 * @internal Only used by ParserTestRunner (T318826)
451 public function setMaxTemplateDepth( $x ) {
452 return $this->setOptionLegacy( 'maxTemplateDepth', $x );
456 * Maximum number of calls per parse to expensive parser functions
457 * @since 1.20
458 * @return int
460 public function getExpensiveParserFunctionLimit() {
461 return $this->getOption( 'expensiveParserFunctionLimit' );
465 * Maximum number of calls per parse to expensive parser functions
466 * @since 1.20
467 * @param int|null $x New value (null is no change)
468 * @return int Old value
470 public function setExpensiveParserFunctionLimit( $x ) {
471 return $this->setOptionLegacy( 'expensiveParserFunctionLimit', $x );
475 * Remove HTML comments
476 * @warning Only applies to preprocess operations
477 * @return bool
479 public function getRemoveComments() {
480 return $this->getOption( 'removeComments' );
484 * Remove HTML comments
485 * @warning Only applies to preprocess operations
486 * @param bool|null $x New value (null is no change)
487 * @return bool Old value
489 public function setRemoveComments( $x ) {
490 return $this->setOptionLegacy( 'removeComments', $x );
494 * @deprecated since 1.38. This does nothing now, to control limit reporting
495 * please provide 'includeDebugInfo' option to ParserOutput::getText.
497 * Enable limit report in an HTML comment on output
498 * @return bool
500 public function getEnableLimitReport() {
501 return false;
505 * @deprecated since 1.38. This does nothing now, to control limit reporting
506 * please provide 'includeDebugInfo' option to ParserOutput::getText.
508 * Enable limit report in an HTML comment on output
509 * @param bool|null $x New value (null is no change)
510 * @return bool Old value
512 public function enableLimitReport( $x = true ) {
513 return false;
517 * Clean up signature texts?
518 * @see Parser::cleanSig
519 * @return bool
521 public function getCleanSignatures() {
522 return $this->getOption( 'cleanSignatures' );
526 * Clean up signature texts?
527 * @see Parser::cleanSig
528 * @param bool|null $x New value (null is no change)
529 * @return bool Old value
531 public function setCleanSignatures( $x ) {
532 return $this->setOptionLegacy( 'cleanSignatures', $x );
536 * Target attribute for external links
537 * @return string|false
538 * @internal Only set by installer (T317647)
540 public function getExternalLinkTarget() {
541 return $this->getOption( 'externalLinkTarget' );
545 * Target attribute for external links
546 * @param string|false|null $x New value (null is no change)
547 * @return string Old value
548 * @internal Only used by installer (T317647)
550 public function setExternalLinkTarget( $x ) {
551 return $this->setOptionLegacy( 'externalLinkTarget', $x );
555 * Whether content conversion should be disabled
556 * @return bool
558 public function getDisableContentConversion() {
559 return $this->getOption( 'disableContentConversion' );
563 * Whether content conversion should be disabled
564 * @param bool|null $x New value (null is no change)
565 * @return bool Old value
567 public function disableContentConversion( $x = true ) {
568 return $this->setOptionLegacy( 'disableContentConversion', $x );
572 * Whether title conversion should be disabled
573 * @return bool
575 public function getDisableTitleConversion() {
576 return $this->getOption( 'disableTitleConversion' );
580 * Whether title conversion should be disabled
581 * @param bool|null $x New value (null is no change)
582 * @return bool Old value
584 public function disableTitleConversion( $x = true ) {
585 return $this->setOptionLegacy( 'disableTitleConversion', $x );
589 * Thumb size preferred by the user.
590 * @return int
592 public function getThumbSize() {
593 return $this->getOption( 'thumbsize' );
597 * Thumb size preferred by the user.
598 * @param int|null $x New value (null is no change)
599 * @return int Old value
601 public function setThumbSize( $x ) {
602 return $this->setOptionLegacy( 'thumbsize', $x );
606 * Parsing the page for a "preview" operation?
607 * @return bool
609 public function getIsPreview() {
610 return $this->getOption( 'isPreview' );
614 * Parsing the page for a "preview" operation?
615 * @param bool|null $x New value (null is no change)
616 * @return bool Old value
618 public function setIsPreview( $x ) {
619 return $this->setOptionLegacy( 'isPreview', $x );
623 * Parsing the page for a "preview" operation on a single section?
624 * @return bool
626 public function getIsSectionPreview() {
627 return $this->getOption( 'isSectionPreview' );
631 * Parsing the page for a "preview" operation on a single section?
632 * @param bool|null $x New value (null is no change)
633 * @return bool Old value
635 public function setIsSectionPreview( $x ) {
636 return $this->setOptionLegacy( 'isSectionPreview', $x );
640 * Parsing the printable version of the page?
641 * @return bool
643 public function getIsPrintable() {
644 return $this->getOption( 'printable' );
648 * Parsing the printable version of the page?
649 * @param bool|null $x New value (null is no change)
650 * @return bool Old value
652 public function setIsPrintable( $x ) {
653 return $this->setOptionLegacy( 'printable', $x );
657 * Transform wiki markup when saving the page?
658 * @return bool
660 public function getPreSaveTransform() {
661 return $this->getOption( 'preSaveTransform' );
665 * Transform wiki markup when saving the page?
666 * @param bool|null $x New value (null is no change)
667 * @return bool Old value
669 public function setPreSaveTransform( $x ) {
670 return $this->setOptionLegacy( 'preSaveTransform', $x );
674 * Parsoid-format HTML output, or legacy wikitext parser HTML?
675 * @see T300191
676 * @unstable
677 * @since 1.41
678 * @return bool
680 public function getUseParsoid(): bool {
681 return $this->getOption( 'useParsoid' );
685 * Request Parsoid-format HTML output.
686 * @see T300191
687 * @unstable
688 * @since 1.41
690 public function setUseParsoid() {
691 $this->setOption( 'useParsoid', true );
695 * Date format index
696 * @return string
698 public function getDateFormat() {
699 return $this->getOption( 'dateformat' );
703 * Lazy initializer for dateFormat
704 * @param ParserOptions $popt
705 * @return string
707 private static function initDateFormat( ParserOptions $popt ) {
708 $userFactory = MediaWikiServices::getInstance()->getUserFactory();
709 return $userFactory->newFromUserIdentity( $popt->getUserIdentity() )->getDatePreference();
713 * Date format index
714 * @param string|null $x New value (null is no change)
715 * @return string Old value
717 public function setDateFormat( $x ) {
718 return $this->setOptionLegacy( 'dateformat', $x );
722 * Get the user language used by the parser for this page and split the parser cache.
724 * @warning Calling this causes the parser cache to be fragmented by user language!
725 * To avoid cache fragmentation, output should not depend on the user language.
726 * Use Parser::getTargetLanguage() instead!
728 * @note This function will trigger a cache fragmentation by recording the
729 * 'userlang' option, see optionUsed(). This is done to avoid cache pollution
730 * when the page is rendered based on the language of the user.
732 * @note When saving, this will return the default language instead of the user's.
733 * {{int: }} uses this which used to produce inconsistent link tables (T16404).
735 * @return Language
736 * @since 1.19
738 public function getUserLangObj() {
739 return $this->getOption( 'userlang' );
743 * Same as getUserLangObj() but returns a string instead.
745 * @warning Calling this causes the parser cache to be fragmented by user language!
746 * To avoid cache fragmentation, output should not depend on the user language.
747 * Use Parser::getTargetLanguage() instead!
749 * @see getUserLangObj()
751 * @return string Language code
752 * @since 1.17
754 public function getUserLang() {
755 return $this->getUserLangObj()->getCode();
759 * Set the user language used by the parser for this page and split the parser cache.
760 * @param string|Language $x New value
761 * @return Language Old value
763 public function setUserLang( $x ) {
764 if ( is_string( $x ) ) {
765 $x = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $x );
768 return $this->setOptionLegacy( 'userlang', $x );
772 * Are magic ISBN links enabled?
773 * @since 1.28
774 * @return bool
776 public function getMagicISBNLinks() {
777 return $this->getOption( 'magicISBNLinks' );
781 * Are magic PMID links enabled?
782 * @since 1.28
783 * @return bool
785 public function getMagicPMIDLinks() {
786 return $this->getOption( 'magicPMIDLinks' );
790 * Are magic RFC links enabled?
791 * @since 1.28
792 * @return bool
794 public function getMagicRFCLinks() {
795 return $this->getOption( 'magicRFCLinks' );
799 * Should the table of contents be suppressed?
800 * Used when parsing "code" pages (like JavaScript) as wikitext
801 * for backlink support and categories, but where we don't want
802 * other metadata generated (like the table of contents).
803 * @see T307691
804 * @since 1.39
805 * @return bool
807 public function getSuppressTOC() {
808 return $this->getOption( 'suppressTOC' );
812 * Suppress generation of the table of contents.
813 * Used when parsing "code" pages (like JavaScript) as wikitext
814 * for backlink support and categories, but where we don't want
815 * other metadata generated (like the table of contents).
816 * @see T307691
817 * @since 1.39
819 public function setSuppressTOC() {
820 $this->setOption( 'suppressTOC', true );
824 * If the wiki is configured to allow raw html ($wgRawHtml = true)
825 * is it allowed in the specific case of parsing this page.
827 * This is meant to disable unsafe parser tags in cases where
828 * a malicious user may control the input to the parser.
830 * @note This is expected to be true for normal pages even if the
831 * wiki has $wgRawHtml disabled in general. The setting only
832 * signifies that raw html would be unsafe in the current context
833 * provided that raw html is allowed at all.
834 * @since 1.29
835 * @return bool
837 public function getAllowUnsafeRawHtml() {
838 return $this->getOption( 'allowUnsafeRawHtml' );
842 * If the wiki is configured to allow raw html ($wgRawHtml = true)
843 * is it allowed in the specific case of parsing this page.
844 * @see self::getAllowUnsafeRawHtml()
845 * @since 1.29
846 * @param bool|null $x Value to set or null to get current value
847 * @return bool Current value for allowUnsafeRawHtml
849 public function setAllowUnsafeRawHtml( $x ) {
850 return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
854 * Class to use to wrap output from Parser::parse()
855 * @since 1.30
856 * @return string|false
858 public function getWrapOutputClass() {
859 return $this->getOption( 'wrapclass' );
863 * CSS class to use to wrap output from Parser::parse()
864 * @since 1.30
865 * @param string $className Class name to use for wrapping.
866 * Passing false to indicate "no wrapping" was deprecated in MediaWiki 1.31.
867 * @return string|false Current value
869 public function setWrapOutputClass( $className ) {
870 if ( $className === true ) { // DWIM, they probably want the default class name
871 $className = 'mw-parser-output';
873 if ( $className === false ) {
874 wfDeprecated( __METHOD__ . '( false )', '1.31' );
876 return $this->setOption( 'wrapclass', $className );
880 * Callback for current revision fetching; first argument to call_user_func().
881 * @internal
882 * @since 1.35
883 * @return callable
885 public function getCurrentRevisionRecordCallback() {
886 return $this->getOption( 'currentRevisionRecordCallback' );
890 * Callback for current revision fetching; first argument to call_user_func().
891 * @internal
892 * @since 1.35
893 * @param callable|null $x New value
894 * @return callable Old value
896 public function setCurrentRevisionRecordCallback( $x ) {
897 return $this->setOption( 'currentRevisionRecordCallback', $x );
901 * Callback for template fetching; first argument to call_user_func().
902 * @return callable
904 public function getTemplateCallback() {
905 return $this->getOption( 'templateCallback' );
909 * Callback for template fetching; first argument to call_user_func().
910 * @param callable|null $x New value (null is no change)
911 * @return callable Old value
913 public function setTemplateCallback( $x ) {
914 return $this->setOptionLegacy( 'templateCallback', $x );
918 * A guess for {{REVISIONID}}, calculated using the callback provided via
919 * setSpeculativeRevIdCallback(). For consistency, the value will be calculated upon the
920 * first call of this method, and re-used for subsequent calls.
922 * If no callback was defined via setSpeculativeRevIdCallback(), this method will return false.
924 * @since 1.32
925 * @return int|false
927 public function getSpeculativeRevId() {
928 return $this->getOption( 'speculativeRevId' );
932 * A guess for {{PAGEID}}, calculated using the callback provided via
933 * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the
934 * first call of this method, and re-used for subsequent calls.
936 * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
938 * @since 1.34
939 * @return int|false
941 public function getSpeculativePageId() {
942 return $this->getOption( 'speculativePageId' );
946 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
948 * @param ParserOptions $popt
949 * @return int|false
951 private static function initSpeculativeRevId( ParserOptions $popt ) {
952 $cb = $popt->getOption( 'speculativeRevIdCallback' );
953 $id = $cb ? $cb() : null;
955 // returning null would result in this being re-called every access
956 return $id ?? false;
960 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
962 * @param ParserOptions $popt
963 * @return int|false
965 private static function initSpeculativePageId( ParserOptions $popt ) {
966 $cb = $popt->getOption( 'speculativePageIdCallback' );
967 $id = $cb ? $cb() : null;
969 // returning null would result in this being re-called every access
970 return $id ?? false;
974 * Callback to generate a guess for {{REVISIONID}}
975 * @param callable|null $x New value
976 * @return callable|null Old value
977 * @since 1.28
979 public function setSpeculativeRevIdCallback( $x ) {
980 $this->setOption( 'speculativeRevId', null ); // reset
981 return $this->setOption( 'speculativeRevIdCallback', $x );
985 * Callback to generate a guess for {{PAGEID}}
986 * @param callable|null $x New value
987 * @return callable|null Old value
988 * @since 1.34
990 public function setSpeculativePageIdCallback( $x ) {
991 $this->setOption( 'speculativePageId', null ); // reset
992 return $this->setOption( 'speculativePageIdCallback', $x );
996 * Timestamp used for {{CURRENTDAY}} etc.
997 * @return string TS_MW timestamp
999 public function getTimestamp() {
1000 if ( !isset( $this->mTimestamp ) ) {
1001 $this->mTimestamp = wfTimestampNow();
1003 return $this->mTimestamp;
1007 * Timestamp used for {{CURRENTDAY}} etc.
1008 * @param string|null $x New value (null is no change)
1009 * @return string Old value
1011 public function setTimestamp( $x ) {
1012 return wfSetVar( $this->mTimestamp, $x );
1016 * Note that setting or changing this does not *make* the page a redirect
1017 * or change its target, it merely records the information for reference
1018 * during the parse.
1020 * @since 1.24
1021 * @param Title|null $title
1023 public function setRedirectTarget( $title ) {
1024 $this->redirectTarget = $title;
1028 * Get the previously-set redirect target.
1030 * @since 1.24
1031 * @return Title|null
1033 public function getRedirectTarget() {
1034 return $this->redirectTarget;
1038 * Extra key that should be present in the parser cache key.
1039 * @warning Consider registering your additional options with the
1040 * ParserOptionsRegister hook instead of using this method.
1041 * @param string $key
1043 public function addExtraKey( $key ) {
1044 $this->mExtraKey .= '!' . $key;
1048 * Get the identity of the user for whom the parse is made.
1049 * @since 1.36
1050 * @return UserIdentity
1052 public function getUserIdentity(): UserIdentity {
1053 return $this->mUser;
1057 * @param UserIdentity $user
1058 * @param Language|null $lang
1060 public function __construct( UserIdentity $user, $lang = null ) {
1061 if ( $lang === null ) {
1062 global $wgLang;
1063 StubObject::unstub( $wgLang );
1064 $lang = $wgLang;
1066 $this->initialiseFromUser( $user, $lang );
1070 * Get a ParserOptions object for an anonymous user
1071 * @since 1.27
1072 * @return ParserOptions
1074 public static function newFromAnon() {
1075 return new ParserOptions( new User,
1076 MediaWikiServices::getInstance()->getContentLanguage() );
1080 * Get a ParserOptions object from a given user.
1081 * Language will be taken from $wgLang.
1083 * @param UserIdentity $user
1084 * @return ParserOptions
1086 public static function newFromUser( $user ) {
1087 return new ParserOptions( $user );
1091 * Get a ParserOptions object from a given user and language
1093 * @param UserIdentity $user
1094 * @param Language $lang
1095 * @return ParserOptions
1097 public static function newFromUserAndLang( UserIdentity $user, Language $lang ) {
1098 return new ParserOptions( $user, $lang );
1102 * Get a ParserOptions object from a IContextSource object
1104 * @param IContextSource $context
1105 * @return ParserOptions
1107 public static function newFromContext( IContextSource $context ) {
1108 return new ParserOptions( $context->getUser(), $context->getLanguage() );
1112 * Creates a "canonical" ParserOptions object
1114 * For historical reasons, certain options have default values that are
1115 * different from the canonical values used for caching.
1117 * @since 1.30
1118 * @since 1.32 Added string and IContextSource as options for the first parameter
1119 * @since 1.36 UserIdentity is also allowed
1120 * @deprecated since 1.38. Use ::newFromContext, ::newFromAnon or ::newFromUserAndLang instead.
1121 * Canonical ParserOptions are now exactly the same as non-canonical.
1122 * @param IContextSource|string|UserIdentity $context
1123 * - If an IContextSource, the options are initialized based on the source's UserIdentity and Language.
1124 * - If the string 'canonical', the options are initialized with an anonymous user and
1125 * the content language.
1126 * - If a UserIdentity, the options are initialized for that UserIdentity
1127 * 'userlang' is taken from the $userLang parameter, defaulting to $wgLang if that is null.
1128 * @param Language|StubObject|null $userLang (see above)
1129 * @return ParserOptions
1131 public static function newCanonical( $context, $userLang = null ) {
1132 if ( $context instanceof IContextSource ) {
1133 $ret = self::newFromContext( $context );
1134 } elseif ( $context === 'canonical' ) {
1135 $ret = self::newFromAnon();
1136 } elseif ( $context instanceof UserIdentity ) {
1137 $ret = new self( $context, $userLang );
1138 } else {
1139 throw new InvalidArgumentException(
1140 '$context must be an IContextSource, the string "canonical", or a UserIdentity'
1143 return $ret;
1147 * Reset static caches
1148 * @internal For testing
1150 public static function clearStaticCache() {
1151 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
1152 throw new LogicException( __METHOD__ . ' is just for testing' );
1154 self::$defaults = null;
1155 self::$lazyOptions = null;
1156 self::$cacheVaryingOptionsHash = null;
1160 * Get default option values
1161 * @warning If you change the default for an existing option, all existing
1162 * parser cache entries will be invalid. To avoid bugs, you'll need to handle
1163 * that somehow (e.g. with the RejectParserCacheValue hook) because
1164 * MediaWiki won't do it for you.
1165 * @return array
1167 private static function getDefaults() {
1168 $services = MediaWikiServices::getInstance();
1169 $mainConfig = $services->getMainConfig();
1170 $interwikiMagic = $mainConfig->get( MainConfigNames::InterwikiMagic );
1171 $allowExternalImages = $mainConfig->get( MainConfigNames::AllowExternalImages );
1172 $allowExternalImagesFrom = $mainConfig->get( MainConfigNames::AllowExternalImagesFrom );
1173 $enableImageWhitelist = $mainConfig->get( MainConfigNames::EnableImageWhitelist );
1174 $allowSpecialInclusion = $mainConfig->get( MainConfigNames::AllowSpecialInclusion );
1175 $maxArticleSize = $mainConfig->get( MainConfigNames::MaxArticleSize );
1176 $maxPPNodeCount = $mainConfig->get( MainConfigNames::MaxPPNodeCount );
1177 $maxTemplateDepth = $mainConfig->get( MainConfigNames::MaxTemplateDepth );
1178 $maxPPExpandDepth = $mainConfig->get( MainConfigNames::MaxPPExpandDepth );
1179 $cleanSignatures = $mainConfig->get( MainConfigNames::CleanSignatures );
1180 $externalLinkTarget = $mainConfig->get( MainConfigNames::ExternalLinkTarget );
1181 $expensiveParserFunctionLimit = $mainConfig->get( MainConfigNames::ExpensiveParserFunctionLimit );
1182 $enableMagicLinks = $mainConfig->get( MainConfigNames::EnableMagicLinks );
1183 $languageConverterFactory = $services->getLanguageConverterFactory();
1184 $userOptionsLookup = $services->getUserOptionsLookup();
1185 $contentLanguage = $services->getContentLanguage();
1187 if ( self::$defaults === null ) {
1188 // *UPDATE* ParserOptions::matches() if any of this changes as needed
1189 self::$defaults = [
1190 'dateformat' => null,
1191 'interfaceMessage' => false,
1192 'targetLanguage' => null,
1193 'removeComments' => true,
1194 'suppressTOC' => false,
1195 'enableLimitReport' => false,
1196 'preSaveTransform' => true,
1197 'isPreview' => false,
1198 'isSectionPreview' => false,
1199 'printable' => false,
1200 'allowUnsafeRawHtml' => true,
1201 'wrapclass' => 'mw-parser-output',
1202 'currentRevisionRecordCallback' => [ Parser::class, 'statelessFetchRevisionRecord' ],
1203 'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
1204 'speculativeRevIdCallback' => null,
1205 'speculativeRevId' => null,
1206 'speculativePageIdCallback' => null,
1207 'speculativePageId' => null,
1208 'useParsoid' => false,
1211 self::$cacheVaryingOptionsHash = self::$initialCacheVaryingOptionsHash;
1212 self::$lazyOptions = self::$initialLazyOptions;
1214 ( new HookRunner( $services->getHookContainer() ) )->onParserOptionsRegister(
1215 self::$defaults,
1216 self::$cacheVaryingOptionsHash,
1217 self::$lazyOptions
1220 ksort( self::$cacheVaryingOptionsHash );
1223 // Unit tests depend on being able to modify the globals at will
1224 return self::$defaults + [
1225 'interwikiMagic' => $interwikiMagic,
1226 'allowExternalImages' => $allowExternalImages,
1227 'allowExternalImagesFrom' => $allowExternalImagesFrom,
1228 'enableImageWhitelist' => $enableImageWhitelist,
1229 'allowSpecialInclusion' => $allowSpecialInclusion,
1230 'maxIncludeSize' => $maxArticleSize * 1024,
1231 'maxPPNodeCount' => $maxPPNodeCount,
1232 'maxPPExpandDepth' => $maxPPExpandDepth,
1233 'maxTemplateDepth' => $maxTemplateDepth,
1234 'expensiveParserFunctionLimit' => $expensiveParserFunctionLimit,
1235 'externalLinkTarget' => $externalLinkTarget,
1236 'cleanSignatures' => $cleanSignatures,
1237 'disableContentConversion' => $languageConverterFactory->isConversionDisabled(),
1238 'disableTitleConversion' => $languageConverterFactory->isLinkConversionDisabled(),
1239 // FIXME: The fallback to false for enableMagicLinks is a band-aid to allow
1240 // the phpunit entrypoint patch (I82045c207738d152d5b0006f353637cfaa40bb66)
1241 // to be merged.
1242 // It is possible that a test somewhere is globally resetting $wgEnableMagicLinks
1243 // to null, or that ParserOptions is somehow similarly getting reset in such a way
1244 // that $enableMagicLinks ends up as null rather than an array. This workaround
1245 // seems harmless, but would be nice to eventually fix the underlying issue.
1246 'magicISBNLinks' => $enableMagicLinks['ISBN'] ?? false,
1247 'magicPMIDLinks' => $enableMagicLinks['PMID'] ?? false,
1248 'magicRFCLinks' => $enableMagicLinks['RFC'] ?? false,
1249 'thumbsize' => $userOptionsLookup->getDefaultOption( 'thumbsize' ),
1250 'userlang' => $contentLanguage,
1255 * Get user options
1257 * @param UserIdentity $user
1258 * @param Language $lang
1260 private function initialiseFromUser( UserIdentity $user, Language $lang ) {
1261 // Initially lazy loaded option defaults must not be taken into account,
1262 // otherwise lazy loading does not work. Setting a default for lazy option
1263 // is useful for matching with canonical options.
1264 $this->options = $this->nullifyLazyOption( self::getDefaults() );
1266 $this->mUser = $user;
1267 $services = MediaWikiServices::getInstance();
1268 $optionsLookup = $services->getUserOptionsLookup();
1269 $this->options['thumbsize'] = $optionsLookup->getOption( $user, 'thumbsize' );
1270 $this->options['userlang'] = $lang;
1274 * Check if these options match that of another options set
1276 * This ignores report limit settings that only affect HTML comments
1278 * @param ParserOptions $other
1279 * @return bool
1280 * @since 1.25
1282 public function matches( ParserOptions $other ) {
1283 // Compare most options
1284 $options = array_keys( $this->options );
1285 $options = array_diff( $options, [
1286 'enableLimitReport', // only affects HTML comments
1287 'tidy', // Has no effect since 1.35; removed in 1.36
1288 ] );
1289 foreach ( $options as $option ) {
1290 // Resolve any lazy options
1291 $this->lazyLoadOption( $option );
1292 $other->lazyLoadOption( $option );
1294 $o1 = $this->optionToString( $this->options[$option] );
1295 $o2 = $this->optionToString( $other->options[$option] );
1296 if ( $o1 !== $o2 ) {
1297 return false;
1301 // Compare most other fields
1302 foreach ( ( new ReflectionClass( $this ) )->getProperties() as $property ) {
1303 $field = $property->getName();
1304 if ( $property->isStatic() ) {
1305 continue;
1307 if ( in_array( $field, [
1308 'options', // Already checked above
1309 'onAccessCallback', // only used for ParserOutput option tracking
1310 ] ) ) {
1311 continue;
1314 if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
1315 return false;
1319 return true;
1323 * @param ParserOptions $other
1324 * @return bool Whether the cache key relevant options match those of $other
1325 * @since 1.33
1327 public function matchesForCacheKey( ParserOptions $other ) {
1328 foreach ( self::allCacheVaryingOptions() as $option ) {
1329 // Populate any lazy options
1330 $this->lazyLoadOption( $option );
1331 $other->lazyLoadOption( $option );
1333 $o1 = $this->optionToString( $this->options[$option] );
1334 $o2 = $this->optionToString( $other->options[$option] );
1335 if ( $o1 !== $o2 ) {
1336 return false;
1340 return true;
1344 * Registers a callback for tracking which ParserOptions which are used.
1346 * @since 1.16
1347 * @param callable|null $callback
1349 public function registerWatcher( $callback ) {
1350 $this->onAccessCallback = $callback;
1354 * Record that an option was internally accessed.
1356 * This calls the watcher set by ParserOptions::registerWatcher().
1357 * Typically, the watcher callback is ParserOutput::recordOption().
1358 * The information registered this way is consumed by ParserCache::save().
1360 * @param string $optionName Name of the option
1362 private function optionUsed( $optionName ) {
1363 if ( $this->onAccessCallback ) {
1364 call_user_func( $this->onAccessCallback, $optionName );
1369 * Return all option keys that vary the options hash
1370 * @since 1.30
1371 * @return string[]
1373 public static function allCacheVaryingOptions() {
1374 return array_keys( array_filter( self::getCacheVaryingOptionsHash() ) );
1378 * Convert an option to a string value
1379 * @param mixed $value
1380 * @return string
1382 private function optionToString( $value ) {
1383 if ( $value === true ) {
1384 return '1';
1385 } elseif ( $value === false ) {
1386 return '0';
1387 } elseif ( $value === null ) {
1388 return '';
1389 } elseif ( $value instanceof Language ) {
1390 return $value->getCode();
1391 } elseif ( is_array( $value ) ) {
1392 return '[' . implode( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
1393 } else {
1394 return (string)$value;
1399 * Generate a hash string with the values set on these ParserOptions
1400 * for the keys given in the array.
1401 * This will be used as part of the hash key for the parser cache,
1402 * so users sharing the options with vary for the same page share
1403 * the same cached data safely.
1405 * @since 1.17
1406 * @param string[] $forOptions
1407 * @param Title|null $title Used to get the content language of the page (since r97636)
1408 * @return string Page rendering hash
1410 public function optionsHash( $forOptions, $title = null ) {
1411 $renderHashAppend = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RenderHashAppend );
1413 $inCacheKey = self::allCacheVaryingOptions();
1415 // Resolve any lazy options
1416 $lazyOpts = array_intersect( $forOptions,
1417 $inCacheKey, array_keys( self::getLazyOptions() ) );
1418 foreach ( $lazyOpts as $k ) {
1419 $this->lazyLoadOption( $k );
1422 $options = $this->options;
1423 $defaults = self::getDefaults();
1425 // We only include used options with non-canonical values in the key
1426 // so adding a new option doesn't invalidate the entire parser cache.
1427 // The drawback to this is that changing the default value of an option
1428 // requires manual invalidation of existing cache entries, as mentioned
1429 // in the docs on the relevant methods and hooks.
1430 $values = [];
1431 foreach ( array_intersect( $inCacheKey, $forOptions ) as $option ) {
1432 $v = $this->optionToString( $options[$option] );
1433 $d = $this->optionToString( $defaults[$option] );
1434 if ( $v !== $d ) {
1435 $values[] = "$option=$v";
1439 $confstr = $values ? implode( '!', $values ) : 'canonical';
1441 // add in language specific options, if any
1442 // @todo FIXME: This is just a way of retrieving the url/user preferred variant
1443 $services = MediaWikiServices::getInstance();
1444 $lang = $title ? $title->getPageLanguage() : $services->getContentLanguage();
1445 $converter = $services->getLanguageConverterFactory()->getLanguageConverter( $lang );
1446 $confstr .= $converter->getExtraHashOptions();
1448 $confstr .= $renderHashAppend;
1450 if ( $this->mExtraKey != '' ) {
1451 $confstr .= $this->mExtraKey;
1454 $user = $services->getUserFactory()->newFromUserIdentity( $this->getUserIdentity() );
1455 // Give a chance for extensions to modify the hash, if they have
1456 // extra options or other effects on the parser cache.
1457 ( new HookRunner( $services->getHookContainer() ) )->onPageRenderingHash(
1458 $confstr,
1459 $user,
1460 $forOptions
1463 // Make it a valid memcached key fragment
1464 $confstr = str_replace( ' ', '_', $confstr );
1466 return $confstr;
1470 * Test whether these options are safe to cache
1471 * @param string[]|null $usedOptions the list of options actually used in the parse. Defaults to all options.
1472 * @return bool
1473 * @since 1.30
1475 public function isSafeToCache( array $usedOptions = null ) {
1476 $defaults = self::getDefaults();
1477 $inCacheKey = self::getCacheVaryingOptionsHash();
1478 $usedOptions ??= array_keys( $this->options );
1479 foreach ( $usedOptions as $option ) {
1480 if ( empty( $inCacheKey[$option] ) && empty( self::$callbacks[$option] ) ) {
1481 $v = $this->optionToString( $this->options[$option] ?? null );
1482 $d = $this->optionToString( $defaults[$option] ?? null );
1483 if ( $v !== $d ) {
1484 return false;
1488 return true;
1492 * Sets a hook to force that a page exists, and sets a current revision callback to return
1493 * a revision with custom content when the current revision of the page is requested.
1495 * @since 1.25
1496 * @param Title $title
1497 * @param Content $content
1498 * @param UserIdentity $user The user that the fake revision is attributed to
1499 * @return ScopedCallback to unset the hook
1501 public function setupFakeRevision( $title, $content, $user ) {
1502 $oldCallback = $this->setCurrentRevisionRecordCallback(
1503 static function (
1504 $titleToCheck, $parser = null ) use ( $title, $content, $user, &$oldCallback
1506 if ( $titleToCheck->equals( $title ) ) {
1507 $revRecord = new MutableRevisionRecord( $title );
1508 $revRecord->setContent( SlotRecord::MAIN, $content )
1509 ->setUser( $user )
1510 ->setTimestamp( MWTimestamp::now( TS_MW ) )
1511 ->setPageId( $title->getArticleID() )
1512 ->setParentId( $title->getLatestRevID() );
1513 return $revRecord;
1514 } else {
1515 return call_user_func( $oldCallback, $titleToCheck, $parser );
1520 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1521 $hookScope = $hookContainer->scopedRegister(
1522 'TitleExists',
1523 static function ( $titleToCheck, &$exists ) use ( $title ) {
1524 if ( $titleToCheck->equals( $title ) ) {
1525 $exists = true;
1530 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1531 $linkCache->clearBadLink( $title->getPrefixedDBkey() );
1533 return new ScopedCallback( function () use ( $title, $hookScope, $linkCache, $oldCallback ) {
1534 ScopedCallback::consume( $hookScope );
1535 $linkCache->clearLink( $title );
1536 $this->setCurrentRevisionRecordCallback( $oldCallback );
1537 } );
1541 * Returns reason for rendering the content. This human-readable, intended for logging and debugging only.
1542 * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1543 * @return string
1545 public function getRenderReason(): string {
1546 return $this->renderReason;
1550 * Sets reason for rendering the content. This human-readable, intended for logging and debugging only.
1551 * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1552 * @param string $renderReason
1554 public function setRenderReason( string $renderReason ): void {
1555 $this->renderReason = $renderReason;
1560 * For really cool vim folding this needs to be at the end:
1561 * vim: foldmarker=@{,@} foldmethod=marker