Mark ParserOptions suppressSectionEditLinks as safe to cache
[mediawiki.git] / includes / parser / ParserOptions.php
blobffea02fc086a8417580ec5e56c87ce54bf38a932
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\Context\IContextSource;
25 use MediaWiki\HookContainer\HookRunner;
26 use MediaWiki\MainConfigNames;
27 use MediaWiki\MediaWikiServices;
28 use MediaWiki\Parser\Parser;
29 use MediaWiki\Revision\MutableRevisionRecord;
30 use MediaWiki\Revision\SlotRecord;
31 use MediaWiki\StubObject\StubObject;
32 use MediaWiki\Title\Title;
33 use MediaWiki\User\UserIdentity;
34 use MediaWiki\Utils\MWTimestamp;
35 use Wikimedia\ScopedCallback;
37 /**
38 * @brief Set options of the Parser
40 * How to add an option in core:
41 * 1. Add it to one of the arrays in ParserOptions::setDefaults()
42 * 2. If necessary, add an entry to ParserOptions::$inCacheKey
43 * 3. Add a getter and setter in the section for that.
45 * How to add an option in an extension:
46 * 1. Use the 'ParserOptionsRegister' hook to register it.
47 * 2. Where necessary, use $popt->getOption() and $popt->setOption()
48 * to access it.
50 * @ingroup Parser
52 class ParserOptions {
54 /**
55 * Default values for all options that are relevant for caching.
56 * @see self::getDefaults()
57 * @var array|null
59 private static $defaults = null;
61 /**
62 * Lazy-loaded options
63 * @var callable[]|null
65 private static $lazyOptions = null;
67 /**
68 * Initial lazy-loaded options (before hook)
69 * @var callable[]
71 private static $initialLazyOptions = [
72 'dateformat' => [ __CLASS__, 'initDateFormat' ],
73 'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
74 'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
77 /**
78 * Specify options that are included in the cache key
79 * @var array|null
81 private static $cacheVaryingOptionsHash = null;
83 /**
84 * Initial inCacheKey options (before hook)
85 * @var array
87 private static $initialCacheVaryingOptionsHash = [
88 'dateformat' => true,
89 'thumbsize' => true,
90 'printable' => true,
91 'userlang' => true,
92 'useParsoid' => true,
93 'suppressSectionEditLinks' => true,
96 /**
97 * Specify pseudo-options that are actually callbacks.
98 * These must be ignored when checking for cacheability.
99 * @var array
101 private static $callbacks = [
102 'currentRevisionRecordCallback' => true,
103 'templateCallback' => true,
104 'speculativeRevIdCallback' => true,
105 'speculativePageIdCallback' => true,
109 * Current values for all options that are relevant for caching.
110 * @var array
112 private $options;
115 * Timestamp used for {{CURRENTDAY}} etc.
116 * @var string|null
117 * @note Caching based on parse time is handled externally
119 private $mTimestamp;
122 * Stored user object
123 * @var UserIdentity
124 * @todo Track this for caching somehow without fragmenting the cache
126 private $mUser;
129 * Function to be called when an option is accessed.
130 * @var callable|null
131 * @note Used for collecting used options, does not affect caching
133 private $onAccessCallback = null;
136 * If the page being parsed is a redirect, this should hold the redirect
137 * target.
138 * @var Title|null
139 * @todo Track this for caching somehow
141 private $redirectTarget = null;
144 * Appended to the options hash
146 private $mExtraKey = '';
149 * The reason for rendering the content.
150 * @var string
152 private $renderReason = 'unknown';
155 * Fetch an option and track that is was accessed
156 * @since 1.30
157 * @param string $name Option name
158 * @return mixed
160 public function getOption( $name ) {
161 if ( !array_key_exists( $name, $this->options ) ) {
162 throw new InvalidArgumentException( "Unknown parser option $name" );
165 $this->lazyLoadOption( $name );
166 $this->optionUsed( $name );
167 return $this->options[$name];
171 * @param string $name Lazy load option without tracking usage
173 private function lazyLoadOption( $name ) {
174 $lazyOptions = self::getLazyOptions();
175 if ( isset( $lazyOptions[$name] ) && $this->options[$name] === null ) {
176 $this->options[$name] = call_user_func( $lazyOptions[$name], $this, $name );
181 * Resets lazy loaded options to null in the provided $options array
182 * @param array $options
183 * @return array
185 private function nullifyLazyOption( array $options ): array {
186 return array_fill_keys( array_keys( self::getLazyOptions() ), null ) + $options;
190 * Get lazy-loaded options.
192 * This array should be initialised by the constructor. The return type
193 * hint is used as an assertion to ensure this has happened and to coerce
194 * the type for static analysis.
196 * @internal Public for testing only
198 * @return array
200 public static function getLazyOptions(): array {
201 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
202 // already been called.
203 if ( self::$lazyOptions === null ) {
204 self::getDefaults();
206 return self::$lazyOptions;
210 * Get cache varying options, with the name of the option in the key, and a
211 * boolean in the value which indicates whether the cache is indeed varied.
213 * @see self::allCacheVaryingOptions()
215 * @return array
217 private static function getCacheVaryingOptionsHash(): array {
218 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
219 // already been called.
220 if ( self::$cacheVaryingOptionsHash === null ) {
221 self::getDefaults();
223 return self::$cacheVaryingOptionsHash;
227 * Set an option, generically
228 * @since 1.30
229 * @param string $name Option name
230 * @param mixed $value New value. Passing null will set null, unlike many
231 * of the existing accessors which ignore null for historical reasons.
232 * @return mixed Old value
234 public function setOption( $name, $value ) {
235 if ( !array_key_exists( $name, $this->options ) ) {
236 throw new InvalidArgumentException( "Unknown parser option $name" );
238 $old = $this->options[$name];
239 $this->options[$name] = $value;
240 return $old;
244 * Legacy implementation
245 * @since 1.30 For implementing legacy setters only. Don't use this in new code.
246 * @deprecated since 1.30
247 * @param string $name Option name
248 * @param mixed $value New value. Passing null does not set the value.
249 * @return mixed Old value
251 protected function setOptionLegacy( $name, $value ) {
252 if ( !array_key_exists( $name, $this->options ) ) {
253 throw new InvalidArgumentException( "Unknown parser option $name" );
255 return wfSetVar( $this->options[$name], $value );
259 * Whether to extract interlanguage links
261 * When true, interlanguage links will be returned by
262 * ParserOutput::getLanguageLinks() instead of generating link HTML.
264 * @return bool
266 public function getInterwikiMagic() {
267 return $this->getOption( 'interwikiMagic' );
271 * Specify whether to extract interlanguage links
272 * @param bool|null $x New value (null is no change)
273 * @return bool Old value
275 public function setInterwikiMagic( $x ) {
276 return $this->setOptionLegacy( 'interwikiMagic', $x );
280 * Allow all external images inline?
281 * @return bool
283 public function getAllowExternalImages() {
284 return $this->getOption( 'allowExternalImages' );
288 * Allow all external images inline?
289 * @param bool|null $x New value (null is no change)
290 * @return bool Old value
291 * @deprecated since 1.35; per-parser configuration of image handling via
292 * parser options is deprecated. Use site configuration.
294 public function setAllowExternalImages( $x ) {
295 wfDeprecated( __METHOD__, '1.35' );
296 return $this->setOptionLegacy( 'allowExternalImages', $x );
300 * External images to allow
302 * When self::getAllowExternalImages() is false
304 * @return string|string[] URLs to allow
306 public function getAllowExternalImagesFrom() {
307 return $this->getOption( 'allowExternalImagesFrom' );
311 * External images to allow
313 * When self::getAllowExternalImages() is false
315 * @param string|string[]|null $x New value (null is no change)
316 * @return string|string[] Old value
317 * @deprecated since 1.35; per-parser configuration of image handling via
318 * parser options is deprecated. Use site configuration.
320 public function setAllowExternalImagesFrom( $x ) {
321 wfDeprecated( __METHOD__, '1.35' );
322 return $this->setOptionLegacy( 'allowExternalImagesFrom', $x );
326 * Use the on-wiki external image whitelist?
327 * @return bool
329 public function getEnableImageWhitelist() {
330 return $this->getOption( 'enableImageWhitelist' );
334 * Use the on-wiki external image whitelist?
335 * @param bool|null $x New value (null is no change)
336 * @return bool Old value
337 * @deprecated since 1.35; per-parser configuration of image handling via
338 * parser options is deprecated. Use site configuration.
340 public function setEnableImageWhitelist( $x ) {
341 wfDeprecated( __METHOD__, '1.35' );
342 return $this->setOptionLegacy( 'enableImageWhitelist', $x );
346 * Allow inclusion of special pages?
347 * @return bool
349 public function getAllowSpecialInclusion() {
350 return $this->getOption( 'allowSpecialInclusion' );
354 * Allow inclusion of special pages?
355 * @param bool|null $x New value (null is no change)
356 * @return bool Old value
358 public function setAllowSpecialInclusion( $x ) {
359 return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
363 * Parsing an interface message?
364 * @return bool
366 public function getInterfaceMessage() {
367 return $this->getOption( 'interfaceMessage' );
371 * Parsing an interface message?
372 * @param bool|null $x New value (null is no change)
373 * @return bool Old value
375 public function setInterfaceMessage( $x ) {
376 return $this->setOptionLegacy( 'interfaceMessage', $x );
380 * Target language for the parse
381 * @return Language|null
383 public function getTargetLanguage() {
384 return $this->getOption( 'targetLanguage' );
388 * Target language for the parse
389 * @param Language|null $x New value
390 * @return Language|null Old value
392 public function setTargetLanguage( $x ) {
393 return $this->setOption( 'targetLanguage', $x );
397 * Maximum size of template expansions, in bytes
398 * @return int
400 public function getMaxIncludeSize() {
401 return $this->getOption( 'maxIncludeSize' );
405 * Maximum size of template expansions, in bytes
406 * @param int|null $x New value (null is no change)
407 * @return int Old value
409 public function setMaxIncludeSize( $x ) {
410 return $this->setOptionLegacy( 'maxIncludeSize', $x );
414 * Maximum number of nodes touched by PPFrame::expand()
415 * @return int
417 public function getMaxPPNodeCount() {
418 return $this->getOption( 'maxPPNodeCount' );
422 * Maximum number of nodes touched by PPFrame::expand()
423 * @param int|null $x New value (null is no change)
424 * @return int Old value
426 public function setMaxPPNodeCount( $x ) {
427 return $this->setOptionLegacy( 'maxPPNodeCount', $x );
431 * Maximum recursion depth in PPFrame::expand()
432 * @return int
434 public function getMaxPPExpandDepth() {
435 return $this->getOption( 'maxPPExpandDepth' );
439 * Maximum recursion depth for templates within templates
440 * @return int
441 * @internal Only used by Parser (T318826)
443 public function getMaxTemplateDepth() {
444 return $this->getOption( 'maxTemplateDepth' );
448 * Maximum recursion depth for templates within templates
449 * @param int|null $x New value (null is no change)
450 * @return int Old value
451 * @internal Only used by ParserTestRunner (T318826)
453 public function setMaxTemplateDepth( $x ) {
454 return $this->setOptionLegacy( 'maxTemplateDepth', $x );
458 * Maximum number of calls per parse to expensive parser functions
459 * @since 1.20
460 * @return int
462 public function getExpensiveParserFunctionLimit() {
463 return $this->getOption( 'expensiveParserFunctionLimit' );
467 * Maximum number of calls per parse to expensive parser functions
468 * @since 1.20
469 * @param int|null $x New value (null is no change)
470 * @return int Old value
472 public function setExpensiveParserFunctionLimit( $x ) {
473 return $this->setOptionLegacy( 'expensiveParserFunctionLimit', $x );
477 * Remove HTML comments
478 * @warning Only applies to preprocess operations
479 * @return bool
481 public function getRemoveComments() {
482 return $this->getOption( 'removeComments' );
486 * Remove HTML comments
487 * @warning Only applies to preprocess operations
488 * @param bool|null $x New value (null is no change)
489 * @return bool Old value
491 public function setRemoveComments( $x ) {
492 return $this->setOptionLegacy( 'removeComments', $x );
496 * @deprecated since 1.38. This does nothing now, to control limit reporting
497 * please provide 'includeDebugInfo' option to ParserOutput::getText.
499 * Enable limit report in an HTML comment on output
500 * @return bool
502 public function getEnableLimitReport() {
503 return false;
507 * @deprecated since 1.38. This does nothing now, to control limit reporting
508 * please provide 'includeDebugInfo' option to ParserOutput::getText.
510 * Enable limit report in an HTML comment on output
511 * @param bool|null $x New value (null is no change)
512 * @return bool Old value
514 public function enableLimitReport( $x = true ) {
515 return false;
519 * Clean up signature texts?
520 * @see Parser::cleanSig
521 * @return bool
523 public function getCleanSignatures() {
524 return $this->getOption( 'cleanSignatures' );
528 * Clean up signature texts?
529 * @see Parser::cleanSig
530 * @param bool|null $x New value (null is no change)
531 * @return bool Old value
533 public function setCleanSignatures( $x ) {
534 return $this->setOptionLegacy( 'cleanSignatures', $x );
538 * Target attribute for external links
539 * @return string|false
540 * @internal Only set by installer (T317647)
542 public function getExternalLinkTarget() {
543 return $this->getOption( 'externalLinkTarget' );
547 * Target attribute for external links
548 * @param string|false|null $x New value (null is no change)
549 * @return string Old value
550 * @internal Only used by installer (T317647)
552 public function setExternalLinkTarget( $x ) {
553 return $this->setOptionLegacy( 'externalLinkTarget', $x );
557 * Whether content conversion should be disabled
558 * @return bool
560 public function getDisableContentConversion() {
561 return $this->getOption( 'disableContentConversion' );
565 * Whether content conversion should be disabled
566 * @param bool|null $x New value (null is no change)
567 * @return bool Old value
569 public function disableContentConversion( $x = true ) {
570 return $this->setOptionLegacy( 'disableContentConversion', $x );
574 * Whether title conversion should be disabled
575 * @return bool
577 public function getDisableTitleConversion() {
578 return $this->getOption( 'disableTitleConversion' );
582 * Whether title conversion should be disabled
583 * @param bool|null $x New value (null is no change)
584 * @return bool Old value
586 public function disableTitleConversion( $x = true ) {
587 return $this->setOptionLegacy( 'disableTitleConversion', $x );
591 * Thumb size preferred by the user.
592 * @return int
594 public function getThumbSize() {
595 return $this->getOption( 'thumbsize' );
599 * Thumb size preferred by the user.
600 * @param int|null $x New value (null is no change)
601 * @return int Old value
603 public function setThumbSize( $x ) {
604 return $this->setOptionLegacy( 'thumbsize', $x );
608 * Parsing the page for a "preview" operation?
609 * @return bool
611 public function getIsPreview() {
612 return $this->getOption( 'isPreview' );
616 * Parsing the page for a "preview" operation?
617 * @param bool|null $x New value (null is no change)
618 * @return bool Old value
620 public function setIsPreview( $x ) {
621 return $this->setOptionLegacy( 'isPreview', $x );
625 * Parsing the page for a "preview" operation on a single section?
626 * @return bool
628 public function getIsSectionPreview() {
629 return $this->getOption( 'isSectionPreview' );
633 * Parsing the page for a "preview" operation on a single section?
634 * @param bool|null $x New value (null is no change)
635 * @return bool Old value
637 public function setIsSectionPreview( $x ) {
638 return $this->setOptionLegacy( 'isSectionPreview', $x );
642 * Parsing the printable version of the page?
643 * @return bool
645 public function getIsPrintable() {
646 return $this->getOption( 'printable' );
650 * Parsing the printable version of the page?
651 * @param bool|null $x New value (null is no change)
652 * @return bool Old value
654 public function setIsPrintable( $x ) {
655 return $this->setOptionLegacy( 'printable', $x );
659 * Transform wiki markup when saving the page?
660 * @return bool
662 public function getPreSaveTransform() {
663 return $this->getOption( 'preSaveTransform' );
667 * Transform wiki markup when saving the page?
668 * @param bool|null $x New value (null is no change)
669 * @return bool Old value
671 public function setPreSaveTransform( $x ) {
672 return $this->setOptionLegacy( 'preSaveTransform', $x );
676 * Parsoid-format HTML output, or legacy wikitext parser HTML?
677 * @see T300191
678 * @unstable
679 * @since 1.41
680 * @return bool
682 public function getUseParsoid(): bool {
683 return $this->getOption( 'useParsoid' );
687 * Request Parsoid-format HTML output.
688 * @see T300191
689 * @unstable
690 * @since 1.41
692 public function setUseParsoid() {
693 $this->setOption( 'useParsoid', true );
697 * Date format index
698 * @return string
700 public function getDateFormat() {
701 return $this->getOption( 'dateformat' );
705 * Lazy initializer for dateFormat
706 * @param ParserOptions $popt
707 * @return string
709 private static function initDateFormat( ParserOptions $popt ) {
710 $userFactory = MediaWikiServices::getInstance()->getUserFactory();
711 return $userFactory->newFromUserIdentity( $popt->getUserIdentity() )->getDatePreference();
715 * Date format index
716 * @param string|null $x New value (null is no change)
717 * @return string Old value
719 public function setDateFormat( $x ) {
720 return $this->setOptionLegacy( 'dateformat', $x );
724 * Get the user language used by the parser for this page and split the parser cache.
726 * @warning Calling this causes the parser cache to be fragmented by user language!
727 * To avoid cache fragmentation, output should not depend on the user language.
728 * Use Parser::getTargetLanguage() instead!
730 * @note This function will trigger a cache fragmentation by recording the
731 * 'userlang' option, see optionUsed(). This is done to avoid cache pollution
732 * when the page is rendered based on the language of the user.
734 * @note When saving, this will return the default language instead of the user's.
735 * {{int: }} uses this which used to produce inconsistent link tables (T16404).
737 * @return Language
738 * @since 1.19
740 public function getUserLangObj() {
741 return $this->getOption( 'userlang' );
745 * Same as getUserLangObj() but returns a string instead.
747 * @warning Calling this causes the parser cache to be fragmented by user language!
748 * To avoid cache fragmentation, output should not depend on the user language.
749 * Use Parser::getTargetLanguage() instead!
751 * @see getUserLangObj()
753 * @return string Language code
754 * @since 1.17
756 public function getUserLang() {
757 return $this->getUserLangObj()->getCode();
761 * Set the user language used by the parser for this page and split the parser cache.
762 * @param string|Language $x New value
763 * @return Language Old value
765 public function setUserLang( $x ) {
766 if ( is_string( $x ) ) {
767 $x = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $x );
770 return $this->setOptionLegacy( 'userlang', $x );
774 * Are magic ISBN links enabled?
775 * @since 1.28
776 * @return bool
778 public function getMagicISBNLinks() {
779 return $this->getOption( 'magicISBNLinks' );
783 * Are magic PMID links enabled?
784 * @since 1.28
785 * @return bool
787 public function getMagicPMIDLinks() {
788 return $this->getOption( 'magicPMIDLinks' );
792 * Are magic RFC links enabled?
793 * @since 1.28
794 * @return bool
796 public function getMagicRFCLinks() {
797 return $this->getOption( 'magicRFCLinks' );
801 * Should the table of contents be suppressed?
802 * Used when parsing "code" pages (like JavaScript) as wikitext
803 * for backlink support and categories, but where we don't want
804 * other metadata generated (like the table of contents).
805 * @see T307691
806 * @since 1.39
807 * @return bool
809 public function getSuppressTOC() {
810 return $this->getOption( 'suppressTOC' );
814 * Suppress generation of the table of contents.
815 * Used when parsing "code" pages (like JavaScript) as wikitext
816 * for backlink support and categories, but where we don't want
817 * other metadata generated (like the table of contents).
818 * @see T307691
819 * @since 1.39
820 * @deprecated since 1.42; just clear the metadata in the final
821 * parser output
823 public function setSuppressTOC() {
824 wfDeprecated( __METHOD__, '1.42' );
825 $this->setOption( 'suppressTOC', true );
829 * Should section edit links be suppressed?
830 * Used when parsing wikitext which will be presented in a
831 * non-interactive context: previews, UX text, etc.
832 * @since 1.42
833 * @return bool
835 public function getSuppressSectionEditLinks() {
836 return $this->getOption( 'suppressSectionEditLinks' );
840 * Suppress section edit links in the output.
841 * Used when parsing wikitext which will be presented in a
842 * non-interactive context: previews, UX text, etc.
843 * @since 1.42
845 public function setSuppressSectionEditLinks() {
846 $this->setOption( 'suppressSectionEditLinks', true );
850 * If the wiki is configured to allow raw html ($wgRawHtml = true)
851 * is it allowed in the specific case of parsing this page.
853 * This is meant to disable unsafe parser tags in cases where
854 * a malicious user may control the input to the parser.
856 * @note This is expected to be true for normal pages even if the
857 * wiki has $wgRawHtml disabled in general. The setting only
858 * signifies that raw html would be unsafe in the current context
859 * provided that raw html is allowed at all.
860 * @since 1.29
861 * @return bool
863 public function getAllowUnsafeRawHtml() {
864 return $this->getOption( 'allowUnsafeRawHtml' );
868 * If the wiki is configured to allow raw html ($wgRawHtml = true)
869 * is it allowed in the specific case of parsing this page.
870 * @see self::getAllowUnsafeRawHtml()
871 * @since 1.29
872 * @param bool|null $x Value to set or null to get current value
873 * @return bool Current value for allowUnsafeRawHtml
875 public function setAllowUnsafeRawHtml( $x ) {
876 return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
880 * Class to use to wrap output from Parser::parse()
881 * @since 1.30
882 * @return string|false
884 public function getWrapOutputClass() {
885 return $this->getOption( 'wrapclass' );
889 * CSS class to use to wrap output from Parser::parse()
890 * @since 1.30
891 * @param string $className Class name to use for wrapping.
892 * Passing false to indicate "no wrapping" was deprecated in MediaWiki 1.31.
893 * @return string|false Current value
895 public function setWrapOutputClass( $className ) {
896 if ( $className === true ) { // DWIM, they probably want the default class name
897 $className = 'mw-parser-output';
899 if ( $className === false ) {
900 wfDeprecated( __METHOD__ . '( false )', '1.31' );
902 return $this->setOption( 'wrapclass', $className );
906 * Callback for current revision fetching; first argument to call_user_func().
907 * @internal
908 * @since 1.35
909 * @return callable
911 public function getCurrentRevisionRecordCallback() {
912 return $this->getOption( 'currentRevisionRecordCallback' );
916 * Callback for current revision fetching; first argument to call_user_func().
917 * @internal
918 * @since 1.35
919 * @param callable|null $x New value
920 * @return callable Old value
922 public function setCurrentRevisionRecordCallback( $x ) {
923 return $this->setOption( 'currentRevisionRecordCallback', $x );
927 * Callback for template fetching; first argument to call_user_func().
928 * @return callable
930 public function getTemplateCallback() {
931 return $this->getOption( 'templateCallback' );
935 * Callback for template fetching; first argument to call_user_func().
936 * @param callable|null $x New value (null is no change)
937 * @return callable Old value
939 public function setTemplateCallback( $x ) {
940 return $this->setOptionLegacy( 'templateCallback', $x );
944 * A guess for {{REVISIONID}}, calculated using the callback provided via
945 * setSpeculativeRevIdCallback(). For consistency, the value will be calculated upon the
946 * first call of this method, and re-used for subsequent calls.
948 * If no callback was defined via setSpeculativeRevIdCallback(), this method will return false.
950 * @since 1.32
951 * @return int|false
953 public function getSpeculativeRevId() {
954 return $this->getOption( 'speculativeRevId' );
958 * A guess for {{PAGEID}}, calculated using the callback provided via
959 * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the
960 * first call of this method, and re-used for subsequent calls.
962 * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
964 * @since 1.34
965 * @return int|false
967 public function getSpeculativePageId() {
968 return $this->getOption( 'speculativePageId' );
972 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
974 * @param ParserOptions $popt
975 * @return int|false
977 private static function initSpeculativeRevId( ParserOptions $popt ) {
978 $cb = $popt->getOption( 'speculativeRevIdCallback' );
979 $id = $cb ? $cb() : null;
981 // returning null would result in this being re-called every access
982 return $id ?? false;
986 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
988 * @param ParserOptions $popt
989 * @return int|false
991 private static function initSpeculativePageId( ParserOptions $popt ) {
992 $cb = $popt->getOption( 'speculativePageIdCallback' );
993 $id = $cb ? $cb() : null;
995 // returning null would result in this being re-called every access
996 return $id ?? false;
1000 * Callback to generate a guess for {{REVISIONID}}
1001 * @param callable|null $x New value
1002 * @return callable|null Old value
1003 * @since 1.28
1005 public function setSpeculativeRevIdCallback( $x ) {
1006 $this->setOption( 'speculativeRevId', null ); // reset
1007 return $this->setOption( 'speculativeRevIdCallback', $x );
1011 * Callback to generate a guess for {{PAGEID}}
1012 * @param callable|null $x New value
1013 * @return callable|null Old value
1014 * @since 1.34
1016 public function setSpeculativePageIdCallback( $x ) {
1017 $this->setOption( 'speculativePageId', null ); // reset
1018 return $this->setOption( 'speculativePageIdCallback', $x );
1022 * Timestamp used for {{CURRENTDAY}} etc.
1023 * @return string TS_MW timestamp
1025 public function getTimestamp() {
1026 if ( !isset( $this->mTimestamp ) ) {
1027 $this->mTimestamp = wfTimestampNow();
1029 return $this->mTimestamp;
1033 * Timestamp used for {{CURRENTDAY}} etc.
1034 * @param string|null $x New value (null is no change)
1035 * @return string Old value
1037 public function setTimestamp( $x ) {
1038 return wfSetVar( $this->mTimestamp, $x );
1042 * Note that setting or changing this does not *make* the page a redirect
1043 * or change its target, it merely records the information for reference
1044 * during the parse.
1046 * @since 1.24
1047 * @param Title|null $title
1049 public function setRedirectTarget( $title ) {
1050 $this->redirectTarget = $title;
1054 * Get the previously-set redirect target.
1056 * @since 1.24
1057 * @return Title|null
1059 public function getRedirectTarget() {
1060 return $this->redirectTarget;
1064 * Extra key that should be present in the parser cache key.
1065 * @warning Consider registering your additional options with the
1066 * ParserOptionsRegister hook instead of using this method.
1067 * @param string $key
1069 public function addExtraKey( $key ) {
1070 $this->mExtraKey .= '!' . $key;
1074 * Get the identity of the user for whom the parse is made.
1075 * @since 1.36
1076 * @return UserIdentity
1078 public function getUserIdentity(): UserIdentity {
1079 return $this->mUser;
1083 * @param UserIdentity $user
1084 * @param Language|null $lang
1086 public function __construct( UserIdentity $user, $lang = null ) {
1087 if ( $lang === null ) {
1088 global $wgLang;
1089 StubObject::unstub( $wgLang );
1090 $lang = $wgLang;
1092 $this->initialiseFromUser( $user, $lang );
1096 * Get a ParserOptions object for an anonymous user
1097 * @since 1.27
1098 * @return ParserOptions
1100 public static function newFromAnon() {
1101 return new ParserOptions( MediaWikiServices::getInstance()->getUserFactory()->newAnonymous(),
1102 MediaWikiServices::getInstance()->getContentLanguage() );
1106 * Get a ParserOptions object from a given user.
1107 * Language will be taken from $wgLang.
1109 * @param UserIdentity $user
1110 * @return ParserOptions
1112 public static function newFromUser( $user ) {
1113 return new ParserOptions( $user );
1117 * Get a ParserOptions object from a given user and language
1119 * @param UserIdentity $user
1120 * @param Language $lang
1121 * @return ParserOptions
1123 public static function newFromUserAndLang( UserIdentity $user, Language $lang ) {
1124 return new ParserOptions( $user, $lang );
1128 * Get a ParserOptions object from a IContextSource object
1130 * @param IContextSource $context
1131 * @return ParserOptions
1133 public static function newFromContext( IContextSource $context ) {
1134 return new ParserOptions( $context->getUser(), $context->getLanguage() );
1138 * Creates a "canonical" ParserOptions object
1140 * For historical reasons, certain options have default values that are
1141 * different from the canonical values used for caching.
1143 * @since 1.30
1144 * @since 1.32 Added string and IContextSource as options for the first parameter
1145 * @since 1.36 UserIdentity is also allowed
1146 * @deprecated since 1.38. Use ::newFromContext, ::newFromAnon or ::newFromUserAndLang instead.
1147 * Canonical ParserOptions are now exactly the same as non-canonical.
1148 * @param IContextSource|string|UserIdentity $context
1149 * - If an IContextSource, the options are initialized based on the source's UserIdentity and Language.
1150 * - If the string 'canonical', the options are initialized with an anonymous user and
1151 * the content language.
1152 * - If a UserIdentity, the options are initialized for that UserIdentity
1153 * 'userlang' is taken from the $userLang parameter, defaulting to $wgLang if that is null.
1154 * @param Language|StubObject|null $userLang (see above)
1155 * @return ParserOptions
1157 public static function newCanonical( $context, $userLang = null ) {
1158 if ( $context instanceof IContextSource ) {
1159 $ret = self::newFromContext( $context );
1160 } elseif ( $context === 'canonical' ) {
1161 $ret = self::newFromAnon();
1162 } elseif ( $context instanceof UserIdentity ) {
1163 $ret = new self( $context, $userLang );
1164 } else {
1165 throw new InvalidArgumentException(
1166 '$context must be an IContextSource, the string "canonical", or a UserIdentity'
1169 return $ret;
1173 * Reset static caches
1174 * @internal For testing
1176 public static function clearStaticCache() {
1177 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
1178 throw new LogicException( __METHOD__ . ' is just for testing' );
1180 self::$defaults = null;
1181 self::$lazyOptions = null;
1182 self::$cacheVaryingOptionsHash = null;
1186 * Get default option values
1187 * @warning If you change the default for an existing option, all existing
1188 * parser cache entries will be invalid. To avoid bugs, you'll need to handle
1189 * that somehow (e.g. with the RejectParserCacheValue hook) because
1190 * MediaWiki won't do it for you.
1191 * @return array
1193 private static function getDefaults() {
1194 $services = MediaWikiServices::getInstance();
1195 $mainConfig = $services->getMainConfig();
1196 $interwikiMagic = $mainConfig->get( MainConfigNames::InterwikiMagic );
1197 $allowExternalImages = $mainConfig->get( MainConfigNames::AllowExternalImages );
1198 $allowExternalImagesFrom = $mainConfig->get( MainConfigNames::AllowExternalImagesFrom );
1199 $enableImageWhitelist = $mainConfig->get( MainConfigNames::EnableImageWhitelist );
1200 $allowSpecialInclusion = $mainConfig->get( MainConfigNames::AllowSpecialInclusion );
1201 $maxArticleSize = $mainConfig->get( MainConfigNames::MaxArticleSize );
1202 $maxPPNodeCount = $mainConfig->get( MainConfigNames::MaxPPNodeCount );
1203 $maxTemplateDepth = $mainConfig->get( MainConfigNames::MaxTemplateDepth );
1204 $maxPPExpandDepth = $mainConfig->get( MainConfigNames::MaxPPExpandDepth );
1205 $cleanSignatures = $mainConfig->get( MainConfigNames::CleanSignatures );
1206 $externalLinkTarget = $mainConfig->get( MainConfigNames::ExternalLinkTarget );
1207 $expensiveParserFunctionLimit = $mainConfig->get( MainConfigNames::ExpensiveParserFunctionLimit );
1208 $enableMagicLinks = $mainConfig->get( MainConfigNames::EnableMagicLinks );
1209 $languageConverterFactory = $services->getLanguageConverterFactory();
1210 $userOptionsLookup = $services->getUserOptionsLookup();
1211 $contentLanguage = $services->getContentLanguage();
1213 if ( self::$defaults === null ) {
1214 // *UPDATE* ParserOptions::matches() if any of this changes as needed
1215 self::$defaults = [
1216 'dateformat' => null,
1217 'interfaceMessage' => false,
1218 'targetLanguage' => null,
1219 'removeComments' => true,
1220 'suppressTOC' => false,
1221 'suppressSectionEditLinks' => false,
1222 'enableLimitReport' => false,
1223 'preSaveTransform' => true,
1224 'isPreview' => false,
1225 'isSectionPreview' => false,
1226 'printable' => false,
1227 'allowUnsafeRawHtml' => true,
1228 'wrapclass' => 'mw-parser-output',
1229 'currentRevisionRecordCallback' => [ Parser::class, 'statelessFetchRevisionRecord' ],
1230 'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
1231 'speculativeRevIdCallback' => null,
1232 'speculativeRevId' => null,
1233 'speculativePageIdCallback' => null,
1234 'speculativePageId' => null,
1235 'useParsoid' => false,
1238 self::$cacheVaryingOptionsHash = self::$initialCacheVaryingOptionsHash;
1239 self::$lazyOptions = self::$initialLazyOptions;
1241 ( new HookRunner( $services->getHookContainer() ) )->onParserOptionsRegister(
1242 self::$defaults,
1243 self::$cacheVaryingOptionsHash,
1244 self::$lazyOptions
1247 ksort( self::$cacheVaryingOptionsHash );
1250 // Unit tests depend on being able to modify the globals at will
1251 return self::$defaults + [
1252 'interwikiMagic' => $interwikiMagic,
1253 'allowExternalImages' => $allowExternalImages,
1254 'allowExternalImagesFrom' => $allowExternalImagesFrom,
1255 'enableImageWhitelist' => $enableImageWhitelist,
1256 'allowSpecialInclusion' => $allowSpecialInclusion,
1257 'maxIncludeSize' => $maxArticleSize * 1024,
1258 'maxPPNodeCount' => $maxPPNodeCount,
1259 'maxPPExpandDepth' => $maxPPExpandDepth,
1260 'maxTemplateDepth' => $maxTemplateDepth,
1261 'expensiveParserFunctionLimit' => $expensiveParserFunctionLimit,
1262 'externalLinkTarget' => $externalLinkTarget,
1263 'cleanSignatures' => $cleanSignatures,
1264 'disableContentConversion' => $languageConverterFactory->isConversionDisabled(),
1265 'disableTitleConversion' => $languageConverterFactory->isLinkConversionDisabled(),
1266 // FIXME: The fallback to false for enableMagicLinks is a band-aid to allow
1267 // the phpunit entrypoint patch (I82045c207738d152d5b0006f353637cfaa40bb66)
1268 // to be merged.
1269 // It is possible that a test somewhere is globally resetting $wgEnableMagicLinks
1270 // to null, or that ParserOptions is somehow similarly getting reset in such a way
1271 // that $enableMagicLinks ends up as null rather than an array. This workaround
1272 // seems harmless, but would be nice to eventually fix the underlying issue.
1273 'magicISBNLinks' => $enableMagicLinks['ISBN'] ?? false,
1274 'magicPMIDLinks' => $enableMagicLinks['PMID'] ?? false,
1275 'magicRFCLinks' => $enableMagicLinks['RFC'] ?? false,
1276 'thumbsize' => $userOptionsLookup->getDefaultOption( 'thumbsize' ),
1277 'userlang' => $contentLanguage,
1282 * Get user options
1284 * @param UserIdentity $user
1285 * @param Language $lang
1287 private function initialiseFromUser( UserIdentity $user, Language $lang ) {
1288 // Initially lazy loaded option defaults must not be taken into account,
1289 // otherwise lazy loading does not work. Setting a default for lazy option
1290 // is useful for matching with canonical options.
1291 $this->options = $this->nullifyLazyOption( self::getDefaults() );
1293 $this->mUser = $user;
1294 $services = MediaWikiServices::getInstance();
1295 $optionsLookup = $services->getUserOptionsLookup();
1296 $this->options['thumbsize'] = $optionsLookup->getOption( $user, 'thumbsize' );
1297 $this->options['userlang'] = $lang;
1301 * Check if these options match that of another options set
1303 * This ignores report limit settings that only affect HTML comments
1305 * @param ParserOptions $other
1306 * @return bool
1307 * @since 1.25
1309 public function matches( ParserOptions $other ) {
1310 // Compare most options
1311 $options = array_keys( $this->options );
1312 $options = array_diff( $options, [
1313 'enableLimitReport', // only affects HTML comments
1314 'tidy', // Has no effect since 1.35; removed in 1.36
1315 ] );
1316 foreach ( $options as $option ) {
1317 // Resolve any lazy options
1318 $this->lazyLoadOption( $option );
1319 $other->lazyLoadOption( $option );
1321 $o1 = $this->optionToString( $this->options[$option] );
1322 $o2 = $this->optionToString( $other->options[$option] );
1323 if ( $o1 !== $o2 ) {
1324 return false;
1328 // Compare most other fields
1329 foreach ( ( new ReflectionClass( $this ) )->getProperties() as $property ) {
1330 $field = $property->getName();
1331 if ( $property->isStatic() ) {
1332 continue;
1334 if ( in_array( $field, [
1335 'options', // Already checked above
1336 'onAccessCallback', // only used for ParserOutput option tracking
1337 ] ) ) {
1338 continue;
1341 if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
1342 return false;
1346 return true;
1350 * @param ParserOptions $other
1351 * @return bool Whether the cache key relevant options match those of $other
1352 * @since 1.33
1354 public function matchesForCacheKey( ParserOptions $other ) {
1355 foreach ( self::allCacheVaryingOptions() as $option ) {
1356 // Populate any lazy options
1357 $this->lazyLoadOption( $option );
1358 $other->lazyLoadOption( $option );
1360 $o1 = $this->optionToString( $this->options[$option] );
1361 $o2 = $this->optionToString( $other->options[$option] );
1362 if ( $o1 !== $o2 ) {
1363 return false;
1367 return true;
1371 * Registers a callback for tracking which ParserOptions which are used.
1373 * @since 1.16
1374 * @param callable|null $callback
1376 public function registerWatcher( $callback ) {
1377 $this->onAccessCallback = $callback;
1381 * Record that an option was internally accessed.
1383 * This calls the watcher set by ParserOptions::registerWatcher().
1384 * Typically, the watcher callback is ParserOutput::recordOption().
1385 * The information registered this way is consumed by ParserCache::save().
1387 * @param string $optionName Name of the option
1389 private function optionUsed( $optionName ) {
1390 if ( $this->onAccessCallback ) {
1391 call_user_func( $this->onAccessCallback, $optionName );
1396 * Return all option keys that vary the options hash
1397 * @since 1.30
1398 * @return string[]
1400 public static function allCacheVaryingOptions() {
1401 return array_keys( array_filter( self::getCacheVaryingOptionsHash() ) );
1405 * Convert an option to a string value
1406 * @param mixed $value
1407 * @return string
1409 private function optionToString( $value ) {
1410 if ( $value === true ) {
1411 return '1';
1412 } elseif ( $value === false ) {
1413 return '0';
1414 } elseif ( $value === null ) {
1415 return '';
1416 } elseif ( $value instanceof Language ) {
1417 return $value->getCode();
1418 } elseif ( is_array( $value ) ) {
1419 return '[' . implode( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
1420 } else {
1421 return (string)$value;
1426 * Generate a hash string with the values set on these ParserOptions
1427 * for the keys given in the array.
1428 * This will be used as part of the hash key for the parser cache,
1429 * so users sharing the options with vary for the same page share
1430 * the same cached data safely.
1432 * @since 1.17
1433 * @param string[] $forOptions
1434 * @param Title|null $title Used to get the content language of the page (since r97636)
1435 * @return string Page rendering hash
1437 public function optionsHash( $forOptions, $title = null ) {
1438 $renderHashAppend = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RenderHashAppend );
1440 $inCacheKey = self::allCacheVaryingOptions();
1442 // Resolve any lazy options
1443 $lazyOpts = array_intersect( $forOptions,
1444 $inCacheKey, array_keys( self::getLazyOptions() ) );
1445 foreach ( $lazyOpts as $k ) {
1446 $this->lazyLoadOption( $k );
1449 $options = $this->options;
1450 $defaults = self::getDefaults();
1452 // We only include used options with non-canonical values in the key
1453 // so adding a new option doesn't invalidate the entire parser cache.
1454 // The drawback to this is that changing the default value of an option
1455 // requires manual invalidation of existing cache entries, as mentioned
1456 // in the docs on the relevant methods and hooks.
1457 $values = [];
1458 foreach ( array_intersect( $inCacheKey, $forOptions ) as $option ) {
1459 $v = $this->optionToString( $options[$option] );
1460 $d = $this->optionToString( $defaults[$option] );
1461 if ( $v !== $d ) {
1462 $values[] = "$option=$v";
1466 $confstr = $values ? implode( '!', $values ) : 'canonical';
1468 // add in language specific options, if any
1469 // @todo FIXME: This is just a way of retrieving the url/user preferred variant
1470 $services = MediaWikiServices::getInstance();
1471 $lang = $title ? $title->getPageLanguage() : $services->getContentLanguage();
1472 $converter = $services->getLanguageConverterFactory()->getLanguageConverter( $lang );
1473 $confstr .= $converter->getExtraHashOptions();
1475 $confstr .= $renderHashAppend;
1477 if ( $this->mExtraKey != '' ) {
1478 $confstr .= $this->mExtraKey;
1481 $user = $services->getUserFactory()->newFromUserIdentity( $this->getUserIdentity() );
1482 // Give a chance for extensions to modify the hash, if they have
1483 // extra options or other effects on the parser cache.
1484 ( new HookRunner( $services->getHookContainer() ) )->onPageRenderingHash(
1485 $confstr,
1486 $user,
1487 $forOptions
1490 // Make it a valid memcached key fragment
1491 $confstr = str_replace( ' ', '_', $confstr );
1493 return $confstr;
1497 * Test whether these options are safe to cache
1498 * @param string[]|null $usedOptions the list of options actually used in the parse. Defaults to all options.
1499 * @return bool
1500 * @since 1.30
1502 public function isSafeToCache( array $usedOptions = null ) {
1503 $defaults = self::getDefaults();
1504 $inCacheKey = self::getCacheVaryingOptionsHash();
1505 $usedOptions ??= array_keys( $this->options );
1506 foreach ( $usedOptions as $option ) {
1507 if ( empty( $inCacheKey[$option] ) && empty( self::$callbacks[$option] ) ) {
1508 $v = $this->optionToString( $this->options[$option] ?? null );
1509 $d = $this->optionToString( $defaults[$option] ?? null );
1510 if ( $v !== $d ) {
1511 return false;
1515 return true;
1519 * Sets a hook to force that a page exists, and sets a current revision callback to return
1520 * a revision with custom content when the current revision of the page is requested.
1522 * @since 1.25
1523 * @param Title $title
1524 * @param Content $content
1525 * @param UserIdentity $user The user that the fake revision is attributed to
1526 * @return ScopedCallback to unset the hook
1528 public function setupFakeRevision( $title, $content, $user ) {
1529 $oldCallback = $this->setCurrentRevisionRecordCallback(
1530 static function (
1531 $titleToCheck, $parser = null ) use ( $title, $content, $user, &$oldCallback
1533 if ( $titleToCheck->equals( $title ) ) {
1534 $revRecord = new MutableRevisionRecord( $title );
1535 $revRecord->setContent( SlotRecord::MAIN, $content )
1536 ->setUser( $user )
1537 ->setTimestamp( MWTimestamp::now( TS_MW ) )
1538 ->setPageId( $title->getArticleID() )
1539 ->setParentId( $title->getLatestRevID() );
1540 return $revRecord;
1541 } else {
1542 return call_user_func( $oldCallback, $titleToCheck, $parser );
1547 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1548 $hookScope = $hookContainer->scopedRegister(
1549 'TitleExists',
1550 static function ( $titleToCheck, &$exists ) use ( $title ) {
1551 if ( $titleToCheck->equals( $title ) ) {
1552 $exists = true;
1557 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1558 $linkCache->clearBadLink( $title->getPrefixedDBkey() );
1560 return new ScopedCallback( function () use ( $title, $hookScope, $linkCache, $oldCallback ) {
1561 ScopedCallback::consume( $hookScope );
1562 $linkCache->clearLink( $title );
1563 $this->setCurrentRevisionRecordCallback( $oldCallback );
1564 } );
1568 * Returns reason for rendering the content. This human-readable, intended for logging and debugging only.
1569 * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1570 * @return string
1572 public function getRenderReason(): string {
1573 return $this->renderReason;
1577 * Sets reason for rendering the content. This human-readable, intended for logging and debugging only.
1578 * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1579 * @param string $renderReason
1581 public function setRenderReason( string $renderReason ): void {
1582 $this->renderReason = $renderReason;
1587 * For really cool vim folding this needs to be at the end:
1588 * vim: foldmarker=@{,@} foldmethod=marker