Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / parser / ParserOptions.php
blobb720a7529c1327dc1275e5fdcf156eecd1c8f1b4
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 namespace MediaWiki\Parser;
26 use InvalidArgumentException;
27 use LogicException;
28 use MediaWiki\Content\Content;
29 use MediaWiki\Context\IContextSource;
30 use MediaWiki\HookContainer\HookRunner;
31 use MediaWiki\Language\Language;
32 use MediaWiki\MainConfigNames;
33 use MediaWiki\MediaWikiServices;
34 use MediaWiki\Page\PageIdentity;
35 use MediaWiki\Page\PageIdentityValue;
36 use MediaWiki\Page\PageRecord;
37 use MediaWiki\Revision\MutableRevisionRecord;
38 use MediaWiki\Revision\SlotRecord;
39 use MediaWiki\StubObject\StubObject;
40 use MediaWiki\Title\Title;
41 use MediaWiki\User\UserIdentity;
42 use MediaWiki\User\UserIdentityValue;
43 use MediaWiki\Utils\MWTimestamp;
44 use ReflectionClass;
45 use Wikimedia\IPUtils;
46 use Wikimedia\ScopedCallback;
48 /**
49 * @brief Set options of the Parser
51 * How to add an option in core:
52 * 1. Add it to one of the arrays in ParserOptions::setDefaults()
53 * 2. If necessary, add an entry to ParserOptions::$inCacheKey
54 * 3. Add a getter and setter in the section for that.
56 * How to add an option in an extension:
57 * 1. Use the 'ParserOptionsRegister' hook to register it.
58 * 2. Where necessary, use $popt->getOption() and $popt->setOption()
59 * to access it.
61 * @ingroup Parser
63 class ParserOptions {
65 /**
66 * Default values for all options that are relevant for caching.
67 * @see self::getDefaults()
68 * @var array|null
70 private static $defaults = null;
72 /**
73 * Lazy-loaded options
74 * @var callable[]|null
76 private static $lazyOptions = null;
78 /**
79 * Initial lazy-loaded options (before hook)
80 * @var callable[]
82 private static $initialLazyOptions = [
83 'dateformat' => [ __CLASS__, 'initDateFormat' ],
84 'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
85 'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
88 /**
89 * Specify options that are included in the cache key
90 * @var array|null
92 private static $cacheVaryingOptionsHash = null;
94 /**
95 * Initial inCacheKey options (before hook)
96 * @var array
98 private static $initialCacheVaryingOptionsHash = [
99 'dateformat' => true,
100 'thumbsize' => true,
101 'printable' => true,
102 'userlang' => true,
103 'useParsoid' => true,
104 'suppressSectionEditLinks' => true,
105 'collapsibleSections' => true,
109 * Specify pseudo-options that are actually callbacks.
110 * These must be ignored when checking for cacheability.
111 * @var array
113 private static $callbacks = [
114 'currentRevisionRecordCallback' => true,
115 'templateCallback' => true,
116 'speculativeRevIdCallback' => true,
117 'speculativePageIdCallback' => true,
121 * Current values for all options that are relevant for caching.
122 * @var array
124 private $options;
127 * Timestamp used for {{CURRENTDAY}} etc.
128 * @var string|null
129 * @note Caching based on parse time is handled externally
131 private $mTimestamp;
134 * Stored user object
135 * @var UserIdentity
136 * @todo Track this for caching somehow without fragmenting the cache
138 private $mUser;
141 * Function to be called when an option is accessed.
142 * @var callable|null
143 * @note Used for collecting used options, does not affect caching
145 private $onAccessCallback = null;
148 * If the page being parsed is a redirect, this should hold the redirect
149 * target.
150 * @var Title|null
151 * @todo Track this for caching somehow
153 private $redirectTarget = null;
156 * Appended to the options hash
157 * @var string
159 private $mExtraKey = '';
162 * The reason for rendering the content.
163 * @var string
165 private $renderReason = 'unknown';
168 * Fetch an option and track that is was accessed
169 * @since 1.30
170 * @param string $name Option name
171 * @return mixed
173 public function getOption( $name ) {
174 if ( !array_key_exists( $name, $this->options ) ) {
175 throw new InvalidArgumentException( "Unknown parser option $name" );
178 $this->lazyLoadOption( $name );
179 $this->optionUsed( $name );
180 return $this->options[$name];
184 * @param string $name Lazy load option without tracking usage
186 private function lazyLoadOption( $name ) {
187 $lazyOptions = self::getLazyOptions();
188 if ( isset( $lazyOptions[$name] ) && $this->options[$name] === null ) {
189 $this->options[$name] = call_user_func( $lazyOptions[$name], $this, $name );
194 * Resets lazy loaded options to null in the provided $options array
195 * @param array $options
196 * @return array
198 private function nullifyLazyOption( array $options ): array {
199 return array_fill_keys( array_keys( self::getLazyOptions() ), null ) + $options;
203 * Get lazy-loaded options.
205 * This array should be initialised by the constructor. The return type
206 * hint is used as an assertion to ensure this has happened and to coerce
207 * the type for static analysis.
209 * @internal Public for testing only
211 * @return array
213 public static function getLazyOptions(): array {
214 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
215 // already been called.
216 if ( self::$lazyOptions === null ) {
217 self::getDefaults();
219 return self::$lazyOptions;
223 * Get cache varying options, with the name of the option in the key, and a
224 * boolean in the value which indicates whether the cache is indeed varied.
226 * @see self::allCacheVaryingOptions()
228 * @return array
230 private static function getCacheVaryingOptionsHash(): array {
231 // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
232 // already been called.
233 if ( self::$cacheVaryingOptionsHash === null ) {
234 self::getDefaults();
236 return self::$cacheVaryingOptionsHash;
240 * Set an option, generically
241 * @since 1.30
242 * @param string $name Option name
243 * @param mixed $value New value. Passing null will set null, unlike many
244 * of the existing accessors which ignore null for historical reasons.
245 * @return mixed Old value
247 public function setOption( $name, $value ) {
248 if ( !array_key_exists( $name, $this->options ) ) {
249 throw new InvalidArgumentException( "Unknown parser option $name" );
251 $old = $this->options[$name];
252 $this->options[$name] = $value;
253 return $old;
257 * Legacy implementation
258 * @since 1.30 For implementing legacy setters only. Don't use this in new code.
259 * @deprecated since 1.30
260 * @param string $name Option name
261 * @param mixed $value New value. Passing null does not set the value.
262 * @return mixed Old value
264 protected function setOptionLegacy( $name, $value ) {
265 if ( !array_key_exists( $name, $this->options ) ) {
266 throw new InvalidArgumentException( "Unknown parser option $name" );
268 return wfSetVar( $this->options[$name], $value );
272 * Whether to extract interlanguage links
274 * When true, interlanguage links will be returned by
275 * ParserOutput::getLanguageLinks() instead of generating link HTML.
277 * @return bool
279 public function getInterwikiMagic() {
280 return $this->getOption( 'interwikiMagic' );
284 * Specify whether to extract interlanguage links
285 * @param bool|null $x New value (null is no change)
286 * @return bool Old value
288 public function setInterwikiMagic( $x ) {
289 return $this->setOptionLegacy( 'interwikiMagic', $x );
293 * Allow all external images inline?
294 * @return bool
296 public function getAllowExternalImages() {
297 return $this->getOption( 'allowExternalImages' );
301 * External images to allow
303 * When self::getAllowExternalImages() is false
305 * @return string|string[] URLs to allow
307 public function getAllowExternalImagesFrom() {
308 return $this->getOption( 'allowExternalImagesFrom' );
312 * Use the on-wiki external image whitelist?
313 * @return bool
315 public function getEnableImageWhitelist() {
316 return $this->getOption( 'enableImageWhitelist' );
320 * Allow inclusion of special pages?
321 * @return bool
323 public function getAllowSpecialInclusion() {
324 return $this->getOption( 'allowSpecialInclusion' );
328 * Allow inclusion of special pages?
329 * @param bool|null $x New value (null is no change)
330 * @return bool Old value
332 public function setAllowSpecialInclusion( $x ) {
333 return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
337 * Parsing an interface message?
338 * @return bool
340 public function getInterfaceMessage() {
341 return $this->getOption( 'interfaceMessage' );
345 * Parsing an interface message?
346 * @param bool|null $x New value (null is no change)
347 * @return bool Old value
349 public function setInterfaceMessage( $x ) {
350 return $this->setOptionLegacy( 'interfaceMessage', $x );
354 * Target language for the parse
355 * @return Language|null
357 public function getTargetLanguage() {
358 return $this->getOption( 'targetLanguage' );
362 * Target language for the parse
363 * @param Language|null $x New value
364 * @return Language|null Old value
366 public function setTargetLanguage( $x ) {
367 return $this->setOption( 'targetLanguage', $x );
371 * Maximum size of template expansions, in bytes
372 * @return int
374 public function getMaxIncludeSize() {
375 return $this->getOption( 'maxIncludeSize' );
379 * Maximum size of template expansions, in bytes
380 * @param int|null $x New value (null is no change)
381 * @return int Old value
383 public function setMaxIncludeSize( $x ) {
384 return $this->setOptionLegacy( 'maxIncludeSize', $x );
388 * Maximum number of nodes touched by PPFrame::expand()
389 * @return int
391 public function getMaxPPNodeCount() {
392 return $this->getOption( 'maxPPNodeCount' );
396 * Maximum number of nodes touched by PPFrame::expand()
397 * @param int|null $x New value (null is no change)
398 * @return int Old value
400 public function setMaxPPNodeCount( $x ) {
401 return $this->setOptionLegacy( 'maxPPNodeCount', $x );
405 * Maximum recursion depth in PPFrame::expand()
406 * @return int
408 public function getMaxPPExpandDepth() {
409 return $this->getOption( 'maxPPExpandDepth' );
413 * Maximum recursion depth for templates within templates
414 * @return int
415 * @internal Only used by Parser (T318826)
417 public function getMaxTemplateDepth() {
418 return $this->getOption( 'maxTemplateDepth' );
422 * Maximum recursion depth for templates within templates
423 * @param int|null $x New value (null is no change)
424 * @return int Old value
425 * @internal Only used by ParserTestRunner (T318826)
427 public function setMaxTemplateDepth( $x ) {
428 return $this->setOptionLegacy( 'maxTemplateDepth', $x );
432 * Maximum number of calls per parse to expensive parser functions
433 * @since 1.20
434 * @return int
436 public function getExpensiveParserFunctionLimit() {
437 return $this->getOption( 'expensiveParserFunctionLimit' );
441 * Maximum number of calls per parse to expensive parser functions
442 * @since 1.20
443 * @param int|null $x New value (null is no change)
444 * @return int Old value
446 public function setExpensiveParserFunctionLimit( $x ) {
447 return $this->setOptionLegacy( 'expensiveParserFunctionLimit', $x );
451 * Remove HTML comments
452 * @warning Only applies to preprocess operations
453 * @return bool
455 public function getRemoveComments() {
456 return $this->getOption( 'removeComments' );
460 * Remove HTML comments
461 * @warning Only applies to preprocess operations
462 * @param bool|null $x New value (null is no change)
463 * @return bool Old value
465 public function setRemoveComments( $x ) {
466 return $this->setOptionLegacy( 'removeComments', $x );
470 * @deprecated since 1.38. This does nothing now, to control limit reporting
471 * please provide 'includeDebugInfo' option to ParserOutput::getText.
473 * Enable limit report in an HTML comment on output
474 * @return bool
476 public function getEnableLimitReport() {
477 return false;
481 * @deprecated since 1.38. This does nothing now, to control limit reporting
482 * please provide 'includeDebugInfo' option to ParserOutput::getText.
484 * Enable limit report in an HTML comment on output
485 * @param bool|null $x New value (null is no change)
486 * @return bool Old value
488 public function enableLimitReport( $x = true ) {
489 return false;
493 * Clean up signature texts?
494 * @see Parser::cleanSig
495 * @return bool
497 public function getCleanSignatures() {
498 return $this->getOption( 'cleanSignatures' );
502 * Clean up signature texts?
503 * @see Parser::cleanSig
504 * @param bool|null $x New value (null is no change)
505 * @return bool Old value
507 public function setCleanSignatures( $x ) {
508 return $this->setOptionLegacy( 'cleanSignatures', $x );
512 * Target attribute for external links
513 * @return string|false
514 * @internal Only set by installer (T317647)
516 public function getExternalLinkTarget() {
517 return $this->getOption( 'externalLinkTarget' );
521 * Target attribute for external links
522 * @param string|false|null $x New value (null is no change)
523 * @return string Old value
524 * @internal Only used by installer (T317647)
526 public function setExternalLinkTarget( $x ) {
527 return $this->setOptionLegacy( 'externalLinkTarget', $x );
531 * Whether content conversion should be disabled
532 * @return bool
534 public function getDisableContentConversion() {
535 return $this->getOption( 'disableContentConversion' );
539 * Whether content conversion should be disabled
540 * @param bool|null $x New value (null is no change)
541 * @return bool Old value
543 public function disableContentConversion( $x = true ) {
544 return $this->setOptionLegacy( 'disableContentConversion', $x );
548 * Whether title conversion should be disabled
549 * @return bool
551 public function getDisableTitleConversion() {
552 return $this->getOption( 'disableTitleConversion' );
556 * Whether title conversion should be disabled
557 * @param bool|null $x New value (null is no change)
558 * @return bool Old value
560 public function disableTitleConversion( $x = true ) {
561 return $this->setOptionLegacy( 'disableTitleConversion', $x );
565 * Thumb size preferred by the user.
566 * @return int
568 public function getThumbSize() {
569 return $this->getOption( 'thumbsize' );
573 * Thumb size preferred by the user.
574 * @param int|null $x New value (null is no change)
575 * @return int Old value
577 public function setThumbSize( $x ) {
578 return $this->setOptionLegacy( 'thumbsize', $x );
582 * Parsing the page for a "preview" operation?
583 * @return bool
585 public function getIsPreview() {
586 return $this->getOption( 'isPreview' );
590 * Parsing the page for a "preview" operation?
591 * @param bool|null $x New value (null is no change)
592 * @return bool Old value
594 public function setIsPreview( $x ) {
595 return $this->setOptionLegacy( 'isPreview', $x );
599 * Parsing the page for a "preview" operation on a single section?
600 * @return bool
602 public function getIsSectionPreview() {
603 return $this->getOption( 'isSectionPreview' );
607 * Parsing the page for a "preview" operation on a single section?
608 * @param bool|null $x New value (null is no change)
609 * @return bool Old value
611 public function setIsSectionPreview( $x ) {
612 return $this->setOptionLegacy( 'isSectionPreview', $x );
616 * Parsing the printable version of the page?
617 * @return bool
619 public function getIsPrintable() {
620 return $this->getOption( 'printable' );
624 * Parsing the printable version of the page?
625 * @param bool|null $x New value (null is no change)
626 * @return bool Old value
628 public function setIsPrintable( $x ) {
629 return $this->setOptionLegacy( 'printable', $x );
633 * Transform wiki markup when saving the page?
634 * @return bool
636 public function getPreSaveTransform() {
637 return $this->getOption( 'preSaveTransform' );
641 * Transform wiki markup when saving the page?
642 * @param bool|null $x New value (null is no change)
643 * @return bool Old value
645 public function setPreSaveTransform( $x ) {
646 return $this->setOptionLegacy( 'preSaveTransform', $x );
650 * Parsoid-format HTML output, or legacy wikitext parser HTML?
651 * @see T300191
652 * @unstable
653 * @since 1.41
654 * @return bool
656 public function getUseParsoid(): bool {
657 return $this->getOption( 'useParsoid' );
661 * Request Parsoid-format HTML output.
662 * @see T300191
663 * @unstable
664 * @since 1.41
666 public function setUseParsoid() {
667 $this->setOption( 'useParsoid', true );
671 * Date format index
672 * @return string
674 public function getDateFormat() {
675 return $this->getOption( 'dateformat' );
679 * Lazy initializer for dateFormat
680 * @param ParserOptions $popt
681 * @return string
683 private static function initDateFormat( ParserOptions $popt ) {
684 $userFactory = MediaWikiServices::getInstance()->getUserFactory();
685 return $userFactory->newFromUserIdentity( $popt->getUserIdentity() )->getDatePreference();
689 * Date format index
690 * @param string|null $x New value (null is no change)
691 * @return string Old value
693 public function setDateFormat( $x ) {
694 return $this->setOptionLegacy( 'dateformat', $x );
698 * Get the user language used by the parser for this page and split the parser cache.
700 * @warning Calling this causes the parser cache to be fragmented by user language!
701 * To avoid cache fragmentation, output should not depend on the user language.
702 * Use Parser::getTargetLanguage() instead!
704 * @note This function will trigger a cache fragmentation by recording the
705 * 'userlang' option, see optionUsed(). This is done to avoid cache pollution
706 * when the page is rendered based on the language of the user.
708 * @note When saving, this will return the default language instead of the user's.
709 * {{int: }} uses this which used to produce inconsistent link tables (T16404).
711 * @return Language
712 * @since 1.19
714 public function getUserLangObj() {
715 return $this->getOption( 'userlang' );
719 * Same as getUserLangObj() but returns a string instead.
721 * @warning Calling this causes the parser cache to be fragmented by user language!
722 * To avoid cache fragmentation, output should not depend on the user language.
723 * Use Parser::getTargetLanguage() instead!
725 * @see getUserLangObj()
727 * @return string Language code
728 * @since 1.17
730 public function getUserLang() {
731 return $this->getUserLangObj()->getCode();
735 * Set the user language used by the parser for this page and split the parser cache.
736 * @param string|Language $x New value
737 * @return Language Old value
739 public function setUserLang( $x ) {
740 if ( is_string( $x ) ) {
741 $x = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $x );
744 return $this->setOptionLegacy( 'userlang', $x );
748 * Are magic ISBN links enabled?
749 * @since 1.28
750 * @return bool
752 public function getMagicISBNLinks() {
753 return $this->getOption( 'magicISBNLinks' );
757 * Are magic PMID links enabled?
758 * @since 1.28
759 * @return bool
761 public function getMagicPMIDLinks() {
762 return $this->getOption( 'magicPMIDLinks' );
766 * Are magic RFC links enabled?
767 * @since 1.28
768 * @return bool
770 public function getMagicRFCLinks() {
771 return $this->getOption( 'magicRFCLinks' );
775 * Should the table of contents be suppressed?
776 * Used when parsing "code" pages (like JavaScript) as wikitext
777 * for backlink support and categories, but where we don't want
778 * other metadata generated (like the table of contents).
779 * @see T307691
780 * @since 1.39
781 * @return bool
783 public function getSuppressTOC() {
784 return $this->getOption( 'suppressTOC' );
788 * Suppress generation of the table of contents.
789 * Used when parsing "code" pages (like JavaScript) as wikitext
790 * for backlink support and categories, but where we don't want
791 * other metadata generated (like the table of contents).
792 * @see T307691
793 * @since 1.39
794 * @deprecated since 1.42; just clear the metadata in the final
795 * parser output
797 public function setSuppressTOC() {
798 wfDeprecated( __METHOD__, '1.42' );
799 $this->setOption( 'suppressTOC', true );
803 * Should section edit links be suppressed?
804 * Used when parsing wikitext which will be presented in a
805 * non-interactive context: previews, UX text, etc.
806 * @since 1.42
807 * @return bool
809 public function getSuppressSectionEditLinks() {
810 return $this->getOption( 'suppressSectionEditLinks' );
814 * Suppress section edit links in the output.
815 * Used when parsing wikitext which will be presented in a
816 * non-interactive context: previews, UX text, etc.
817 * @since 1.42
819 public function setSuppressSectionEditLinks() {
820 $this->setOption( 'suppressSectionEditLinks', true );
824 * Should section contents be wrapped in <div> to make them
825 * collapsible?
826 * @since 1.42
828 public function getCollapsibleSections(): bool {
829 return $this->getOption( 'collapsibleSections' );
833 * Wrap section contents in a <div> to allow client-side code
834 * to collapse them.
835 * @since 1.42
837 public function setCollapsibleSections(): void {
838 $this->setOption( 'collapsibleSections', true );
842 * If the wiki is configured to allow raw html ($wgRawHtml = true)
843 * is it allowed in the specific case of parsing this page.
845 * This is meant to disable unsafe parser tags in cases where
846 * a malicious user may control the input to the parser.
848 * @note This is expected to be true for normal pages even if the
849 * wiki has $wgRawHtml disabled in general. The setting only
850 * signifies that raw html would be unsafe in the current context
851 * provided that raw html is allowed at all.
852 * @since 1.29
853 * @return bool
855 public function getAllowUnsafeRawHtml() {
856 return $this->getOption( 'allowUnsafeRawHtml' );
860 * If the wiki is configured to allow raw html ($wgRawHtml = true)
861 * is it allowed in the specific case of parsing this page.
862 * @see self::getAllowUnsafeRawHtml()
863 * @since 1.29
864 * @param bool|null $x Value to set or null to get current value
865 * @return bool Current value for allowUnsafeRawHtml
867 public function setAllowUnsafeRawHtml( $x ) {
868 return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
872 * Class to use to wrap output from Parser::parse()
873 * @since 1.30
874 * @return string|false
876 public function getWrapOutputClass() {
877 return $this->getOption( 'wrapclass' );
881 * CSS class to use to wrap output from Parser::parse()
882 * @since 1.30
883 * @param string $className Class name to use for wrapping.
884 * Passing false to indicate "no wrapping" was deprecated in MediaWiki 1.31.
885 * @return string|false Current value
887 public function setWrapOutputClass( $className ) {
888 if ( $className === true ) { // DWIM, they probably want the default class name
889 $className = 'mw-parser-output';
891 if ( $className === false ) {
892 wfDeprecated( __METHOD__ . '( false )', '1.31' );
894 return $this->setOption( 'wrapclass', $className );
898 * Callback for current revision fetching; first argument to call_user_func().
899 * @internal
900 * @since 1.35
901 * @return callable
903 public function getCurrentRevisionRecordCallback() {
904 return $this->getOption( 'currentRevisionRecordCallback' );
908 * Callback for current revision fetching; first argument to call_user_func().
909 * @internal
910 * @since 1.35
911 * @param callable|null $x New value
912 * @return callable Old value
914 public function setCurrentRevisionRecordCallback( $x ) {
915 return $this->setOption( 'currentRevisionRecordCallback', $x );
919 * Callback for template fetching; first argument to call_user_func().
920 * @return callable
922 public function getTemplateCallback() {
923 return $this->getOption( 'templateCallback' );
927 * Callback for template fetching; first argument to call_user_func().
928 * @param callable|null $x New value (null is no change)
929 * @return callable Old value
931 public function setTemplateCallback( $x ) {
932 return $this->setOptionLegacy( 'templateCallback', $x );
936 * A guess for {{REVISIONID}}, calculated using the callback provided via
937 * setSpeculativeRevIdCallback(). For consistency, the value will be calculated upon the
938 * first call of this method, and re-used for subsequent calls.
940 * If no callback was defined via setSpeculativeRevIdCallback(), this method will return false.
942 * @since 1.32
943 * @return int|false
945 public function getSpeculativeRevId() {
946 return $this->getOption( 'speculativeRevId' );
950 * A guess for {{PAGEID}}, calculated using the callback provided via
951 * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the
952 * first call of this method, and re-used for subsequent calls.
954 * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
956 * @since 1.34
957 * @return int|false
959 public function getSpeculativePageId() {
960 return $this->getOption( 'speculativePageId' );
964 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
966 * @param ParserOptions $popt
967 * @return int|false
969 private static function initSpeculativeRevId( ParserOptions $popt ) {
970 $cb = $popt->getOption( 'speculativeRevIdCallback' );
971 $id = $cb ? $cb() : null;
973 // returning null would result in this being re-called every access
974 return $id ?? false;
978 * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
980 * @param ParserOptions $popt
981 * @return int|false
983 private static function initSpeculativePageId( ParserOptions $popt ) {
984 $cb = $popt->getOption( 'speculativePageIdCallback' );
985 $id = $cb ? $cb() : null;
987 // returning null would result in this being re-called every access
988 return $id ?? false;
992 * Callback to generate a guess for {{REVISIONID}}
993 * @param callable|null $x New value
994 * @return callable|null Old value
995 * @since 1.28
997 public function setSpeculativeRevIdCallback( $x ) {
998 $this->setOption( 'speculativeRevId', null ); // reset
999 return $this->setOption( 'speculativeRevIdCallback', $x );
1003 * Callback to generate a guess for {{PAGEID}}
1004 * @param callable|null $x New value
1005 * @return callable|null Old value
1006 * @since 1.34
1008 public function setSpeculativePageIdCallback( $x ) {
1009 $this->setOption( 'speculativePageId', null ); // reset
1010 return $this->setOption( 'speculativePageIdCallback', $x );
1014 * Timestamp used for {{CURRENTDAY}} etc.
1015 * @return string TS_MW timestamp
1017 public function getTimestamp() {
1018 if ( $this->mTimestamp === null ) {
1019 $this->mTimestamp = wfTimestampNow();
1021 return $this->mTimestamp;
1025 * Timestamp used for {{CURRENTDAY}} etc.
1026 * @param string|null $x New value (null is no change)
1027 * @return string Old value
1029 public function setTimestamp( $x ) {
1030 return wfSetVar( $this->mTimestamp, $x );
1034 * Note that setting or changing this does not *make* the page a redirect
1035 * or change its target, it merely records the information for reference
1036 * during the parse.
1038 * @since 1.24
1039 * @param Title|null $title
1041 public function setRedirectTarget( $title ) {
1042 $this->redirectTarget = $title;
1046 * Get the previously-set redirect target.
1048 * @since 1.24
1049 * @return Title|null
1051 public function getRedirectTarget() {
1052 return $this->redirectTarget;
1056 * Extra key that should be present in the parser cache key.
1057 * @warning Consider registering your additional options with the
1058 * ParserOptionsRegister hook instead of using this method.
1059 * @param string $key
1061 public function addExtraKey( $key ) {
1062 $this->mExtraKey .= '!' . $key;
1066 * Get the identity of the user for whom the parse is made.
1067 * @since 1.36
1068 * @return UserIdentity
1070 public function getUserIdentity(): UserIdentity {
1071 return $this->mUser;
1075 * @param UserIdentity $user
1076 * @param Language|null $lang
1078 public function __construct( UserIdentity $user, $lang = null ) {
1079 if ( $lang === null ) {
1080 global $wgLang;
1081 StubObject::unstub( $wgLang );
1082 $lang = $wgLang;
1084 $this->initialiseFromUser( $user, $lang );
1088 * Get a ParserOptions object for an anonymous user
1089 * @since 1.27
1090 * @return ParserOptions
1092 public static function newFromAnon() {
1093 return new ParserOptions( MediaWikiServices::getInstance()->getUserFactory()->newAnonymous(),
1094 MediaWikiServices::getInstance()->getContentLanguage() );
1098 * Get a ParserOptions object from a given user.
1099 * Language will be taken from $wgLang.
1101 * @param UserIdentity $user
1102 * @return ParserOptions
1104 public static function newFromUser( $user ) {
1105 return new ParserOptions( $user );
1109 * Get a ParserOptions object from a given user and language
1111 * @param UserIdentity $user
1112 * @param Language $lang
1113 * @return ParserOptions
1115 public static function newFromUserAndLang( UserIdentity $user, Language $lang ) {
1116 return new ParserOptions( $user, $lang );
1120 * Get a ParserOptions object from a IContextSource object
1122 * @param IContextSource $context
1123 * @return ParserOptions
1125 public static function newFromContext( IContextSource $context ) {
1126 $contextUser = $context->getUser();
1128 // Use the stashed temporary account name instead of an IP address as the user for the ParserOptions
1129 // (if a stashed name is set). This is so that magic words like {{REVISIONUSER}} show the temporary account
1130 // name instead of IP address.
1131 $tempUserCreator = MediaWikiServices::getInstance()->getTempUserCreator();
1132 if ( $tempUserCreator->isEnabled() && IPUtils::isIPAddress( $contextUser->getName() ) ) {
1133 // We do not attempt to acquire a temporary account name if no name is stashed, as this may be called in
1134 // contexts (such as the parse API) where the user will not be performing an edit on their next action
1135 // and therefore would be increasing the rate limit unnecessarily.
1136 $tempName = $tempUserCreator->getStashedName( $context->getRequest()->getSession() );
1137 if ( $tempName !== null ) {
1138 $contextUser = UserIdentityValue::newAnonymous( $tempName );
1142 return new ParserOptions( $contextUser, $context->getLanguage() );
1146 * Creates a "canonical" ParserOptions object
1148 * For historical reasons, certain options have default values that are
1149 * different from the canonical values used for caching.
1151 * @since 1.30
1152 * @since 1.32 Added string and IContextSource as options for the first parameter
1153 * @since 1.36 UserIdentity is also allowed
1154 * @deprecated since 1.38. Use ::newFromContext, ::newFromAnon or ::newFromUserAndLang instead.
1155 * Canonical ParserOptions are now exactly the same as non-canonical.
1156 * @param IContextSource|string|UserIdentity $context
1157 * - If an IContextSource, the options are initialized based on the source's UserIdentity and Language.
1158 * - If the string 'canonical', the options are initialized with an anonymous user and
1159 * the content language.
1160 * - If a UserIdentity, the options are initialized for that UserIdentity
1161 * 'userlang' is taken from the $userLang parameter, defaulting to $wgLang if that is null.
1162 * @param Language|StubObject|null $userLang (see above)
1163 * @return ParserOptions
1165 public static function newCanonical( $context, $userLang = null ) {
1166 if ( $context instanceof IContextSource ) {
1167 $ret = self::newFromContext( $context );
1168 } elseif ( $context === 'canonical' ) {
1169 $ret = self::newFromAnon();
1170 } elseif ( $context instanceof UserIdentity ) {
1171 $ret = new self( $context, $userLang );
1172 } else {
1173 throw new InvalidArgumentException(
1174 '$context must be an IContextSource, the string "canonical", or a UserIdentity'
1177 return $ret;
1181 * Reset static caches
1182 * @internal For testing
1184 public static function clearStaticCache() {
1185 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
1186 throw new LogicException( __METHOD__ . ' is just for testing' );
1188 self::$defaults = null;
1189 self::$lazyOptions = null;
1190 self::$cacheVaryingOptionsHash = null;
1194 * Get default option values
1195 * @warning If you change the default for an existing option, all existing
1196 * parser cache entries will be invalid. To avoid bugs, you'll need to handle
1197 * that somehow (e.g. with the RejectParserCacheValue hook) because
1198 * MediaWiki won't do it for you.
1199 * @return array
1201 private static function getDefaults() {
1202 $services = MediaWikiServices::getInstance();
1203 $mainConfig = $services->getMainConfig();
1204 $interwikiMagic = $mainConfig->get( MainConfigNames::InterwikiMagic );
1205 $allowExternalImages = $mainConfig->get( MainConfigNames::AllowExternalImages );
1206 $allowExternalImagesFrom = $mainConfig->get( MainConfigNames::AllowExternalImagesFrom );
1207 $enableImageWhitelist = $mainConfig->get( MainConfigNames::EnableImageWhitelist );
1208 $allowSpecialInclusion = $mainConfig->get( MainConfigNames::AllowSpecialInclusion );
1209 $maxArticleSize = $mainConfig->get( MainConfigNames::MaxArticleSize );
1210 $maxPPNodeCount = $mainConfig->get( MainConfigNames::MaxPPNodeCount );
1211 $maxTemplateDepth = $mainConfig->get( MainConfigNames::MaxTemplateDepth );
1212 $maxPPExpandDepth = $mainConfig->get( MainConfigNames::MaxPPExpandDepth );
1213 $cleanSignatures = $mainConfig->get( MainConfigNames::CleanSignatures );
1214 $externalLinkTarget = $mainConfig->get( MainConfigNames::ExternalLinkTarget );
1215 $expensiveParserFunctionLimit = $mainConfig->get( MainConfigNames::ExpensiveParserFunctionLimit );
1216 $enableMagicLinks = $mainConfig->get( MainConfigNames::EnableMagicLinks );
1217 $languageConverterFactory = $services->getLanguageConverterFactory();
1218 $userOptionsLookup = $services->getUserOptionsLookup();
1219 $contentLanguage = $services->getContentLanguage();
1221 if ( self::$defaults === null ) {
1222 // *UPDATE* ParserOptions::matches() if any of this changes as needed
1223 self::$defaults = [
1224 'dateformat' => null,
1225 'interfaceMessage' => false,
1226 'targetLanguage' => null,
1227 'removeComments' => true,
1228 'suppressTOC' => false,
1229 'suppressSectionEditLinks' => false,
1230 'collapsibleSections' => false,
1231 'enableLimitReport' => false,
1232 'preSaveTransform' => true,
1233 'isPreview' => false,
1234 'isSectionPreview' => false,
1235 'printable' => false,
1236 'allowUnsafeRawHtml' => true,
1237 'wrapclass' => 'mw-parser-output',
1238 'currentRevisionRecordCallback' => [ Parser::class, 'statelessFetchRevisionRecord' ],
1239 'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
1240 'speculativeRevIdCallback' => null,
1241 'speculativeRevId' => null,
1242 'speculativePageIdCallback' => null,
1243 'speculativePageId' => null,
1244 'useParsoid' => false,
1247 self::$cacheVaryingOptionsHash = self::$initialCacheVaryingOptionsHash;
1248 self::$lazyOptions = self::$initialLazyOptions;
1250 ( new HookRunner( $services->getHookContainer() ) )->onParserOptionsRegister(
1251 self::$defaults,
1252 self::$cacheVaryingOptionsHash,
1253 self::$lazyOptions
1256 ksort( self::$cacheVaryingOptionsHash );
1259 // Unit tests depend on being able to modify the globals at will
1260 return self::$defaults + [
1261 'interwikiMagic' => $interwikiMagic,
1262 'allowExternalImages' => $allowExternalImages,
1263 'allowExternalImagesFrom' => $allowExternalImagesFrom,
1264 'enableImageWhitelist' => $enableImageWhitelist,
1265 'allowSpecialInclusion' => $allowSpecialInclusion,
1266 'maxIncludeSize' => $maxArticleSize * 1024,
1267 'maxPPNodeCount' => $maxPPNodeCount,
1268 'maxPPExpandDepth' => $maxPPExpandDepth,
1269 'maxTemplateDepth' => $maxTemplateDepth,
1270 'expensiveParserFunctionLimit' => $expensiveParserFunctionLimit,
1271 'externalLinkTarget' => $externalLinkTarget,
1272 'cleanSignatures' => $cleanSignatures,
1273 'disableContentConversion' => $languageConverterFactory->isConversionDisabled(),
1274 'disableTitleConversion' => $languageConverterFactory->isLinkConversionDisabled(),
1275 // FIXME: The fallback to false for enableMagicLinks is a band-aid to allow
1276 // the phpunit entrypoint patch (I82045c207738d152d5b0006f353637cfaa40bb66)
1277 // to be merged.
1278 // It is possible that a test somewhere is globally resetting $wgEnableMagicLinks
1279 // to null, or that ParserOptions is somehow similarly getting reset in such a way
1280 // that $enableMagicLinks ends up as null rather than an array. This workaround
1281 // seems harmless, but would be nice to eventually fix the underlying issue.
1282 'magicISBNLinks' => $enableMagicLinks['ISBN'] ?? false,
1283 'magicPMIDLinks' => $enableMagicLinks['PMID'] ?? false,
1284 'magicRFCLinks' => $enableMagicLinks['RFC'] ?? false,
1285 'thumbsize' => $userOptionsLookup->getDefaultOption( 'thumbsize' ),
1286 'userlang' => $contentLanguage,
1291 * Get user options
1293 * @param UserIdentity $user
1294 * @param Language $lang
1296 private function initialiseFromUser( UserIdentity $user, Language $lang ) {
1297 // Initially lazy loaded option defaults must not be taken into account,
1298 // otherwise lazy loading does not work. Setting a default for lazy option
1299 // is useful for matching with canonical options.
1300 $this->options = $this->nullifyLazyOption( self::getDefaults() );
1302 $this->mUser = $user;
1303 $services = MediaWikiServices::getInstance();
1304 $optionsLookup = $services->getUserOptionsLookup();
1305 $this->options['thumbsize'] = $optionsLookup->getOption( $user, 'thumbsize' );
1306 $this->options['userlang'] = $lang;
1310 * Check if these options match that of another options set
1312 * This ignores report limit settings that only affect HTML comments
1314 * @param ParserOptions $other
1315 * @return bool
1316 * @since 1.25
1318 public function matches( ParserOptions $other ) {
1319 // Compare most options
1320 $options = array_keys( $this->options );
1321 $options = array_diff( $options, [
1322 'enableLimitReport', // only affects HTML comments
1323 'tidy', // Has no effect since 1.35; removed in 1.36
1324 ] );
1325 foreach ( $options as $option ) {
1326 // Resolve any lazy options
1327 $this->lazyLoadOption( $option );
1328 $other->lazyLoadOption( $option );
1330 $o1 = $this->optionToString( $this->options[$option] );
1331 $o2 = $this->optionToString( $other->options[$option] );
1332 if ( $o1 !== $o2 ) {
1333 return false;
1337 // Compare most other fields
1338 foreach ( ( new ReflectionClass( $this ) )->getProperties() as $property ) {
1339 $field = $property->getName();
1340 if ( $property->isStatic() ) {
1341 continue;
1343 if ( in_array( $field, [
1344 'options', // Already checked above
1345 'onAccessCallback', // only used for ParserOutput option tracking
1346 ] ) ) {
1347 continue;
1350 if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
1351 return false;
1355 return true;
1359 * @param ParserOptions $other
1360 * @return bool Whether the cache key relevant options match those of $other
1361 * @since 1.33
1363 public function matchesForCacheKey( ParserOptions $other ) {
1364 foreach ( self::allCacheVaryingOptions() as $option ) {
1365 // Populate any lazy options
1366 $this->lazyLoadOption( $option );
1367 $other->lazyLoadOption( $option );
1369 $o1 = $this->optionToString( $this->options[$option] );
1370 $o2 = $this->optionToString( $other->options[$option] );
1371 if ( $o1 !== $o2 ) {
1372 return false;
1376 return true;
1380 * Registers a callback for tracking which ParserOptions which are used.
1382 * @since 1.16
1383 * @param callable|null $callback
1385 public function registerWatcher( $callback ) {
1386 $this->onAccessCallback = $callback;
1390 * Record that an option was internally accessed.
1392 * This calls the watcher set by ParserOptions::registerWatcher().
1393 * Typically, the watcher callback is ParserOutput::recordOption().
1394 * The information registered this way is consumed by ParserCache::save().
1396 * @param string $optionName Name of the option
1398 private function optionUsed( $optionName ) {
1399 if ( $this->onAccessCallback ) {
1400 call_user_func( $this->onAccessCallback, $optionName );
1405 * Return all option keys that vary the options hash
1406 * @since 1.30
1407 * @return string[]
1409 public static function allCacheVaryingOptions() {
1410 return array_keys( array_filter( self::getCacheVaryingOptionsHash() ) );
1414 * Convert an option to a string value
1415 * @param mixed $value
1416 * @return string
1418 private function optionToString( $value ) {
1419 if ( $value === true ) {
1420 return '1';
1421 } elseif ( $value === false ) {
1422 return '0';
1423 } elseif ( $value === null ) {
1424 return '';
1425 } elseif ( $value instanceof Language ) {
1426 return $value->getCode();
1427 } elseif ( is_array( $value ) ) {
1428 return '[' . implode( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
1429 } else {
1430 return (string)$value;
1435 * Generate a hash string with the values set on these ParserOptions
1436 * for the keys given in the array.
1437 * This will be used as part of the hash key for the parser cache,
1438 * so users sharing the options with vary for the same page share
1439 * the same cached data safely.
1441 * @since 1.17
1442 * @param string[] $forOptions
1443 * @param Title|null $title Used to get the content language of the page (since r97636)
1444 * @return string Page rendering hash
1446 public function optionsHash( $forOptions, $title = null ) {
1447 $renderHashAppend = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RenderHashAppend );
1449 $inCacheKey = self::allCacheVaryingOptions();
1451 // Resolve any lazy options
1452 $lazyOpts = array_intersect( $forOptions,
1453 $inCacheKey, array_keys( self::getLazyOptions() ) );
1454 foreach ( $lazyOpts as $k ) {
1455 $this->lazyLoadOption( $k );
1458 $options = $this->options;
1459 $defaults = self::getDefaults();
1461 // We only include used options with non-canonical values in the key
1462 // so adding a new option doesn't invalidate the entire parser cache.
1463 // The drawback to this is that changing the default value of an option
1464 // requires manual invalidation of existing cache entries, as mentioned
1465 // in the docs on the relevant methods and hooks.
1466 $values = [];
1467 foreach ( array_intersect( $inCacheKey, $forOptions ) as $option ) {
1468 $v = $this->optionToString( $options[$option] );
1469 $d = $this->optionToString( $defaults[$option] );
1470 if ( $v !== $d ) {
1471 $values[] = "$option=$v";
1475 $confstr = $values ? implode( '!', $values ) : 'canonical';
1477 // add in language specific options, if any
1478 // @todo FIXME: This is just a way of retrieving the url/user preferred variant
1479 $services = MediaWikiServices::getInstance();
1480 $lang = $title ? $title->getPageLanguage() : $services->getContentLanguage();
1481 $converter = $services->getLanguageConverterFactory()->getLanguageConverter( $lang );
1482 $confstr .= $converter->getExtraHashOptions();
1484 $confstr .= $renderHashAppend;
1486 if ( $this->mExtraKey != '' ) {
1487 $confstr .= $this->mExtraKey;
1490 $user = $services->getUserFactory()->newFromUserIdentity( $this->getUserIdentity() );
1491 // Give a chance for extensions to modify the hash, if they have
1492 // extra options or other effects on the parser cache.
1493 ( new HookRunner( $services->getHookContainer() ) )->onPageRenderingHash(
1494 $confstr,
1495 $user,
1496 $forOptions
1499 // Make it a valid memcached key fragment
1500 $confstr = str_replace( ' ', '_', $confstr );
1502 return $confstr;
1506 * Test whether these options are safe to cache
1507 * @param string[]|null $usedOptions the list of options actually used in the parse. Defaults to all options.
1508 * @return bool
1509 * @since 1.30
1511 public function isSafeToCache( ?array $usedOptions = null ) {
1512 $defaults = self::getDefaults();
1513 $inCacheKey = self::getCacheVaryingOptionsHash();
1514 $usedOptions ??= array_keys( $this->options );
1515 foreach ( $usedOptions as $option ) {
1516 if ( empty( $inCacheKey[$option] ) && empty( self::$callbacks[$option] ) ) {
1517 $v = $this->optionToString( $this->options[$option] ?? null );
1518 $d = $this->optionToString( $defaults[$option] ?? null );
1519 if ( $v !== $d ) {
1520 return false;
1524 return true;
1528 * Sets a hook to force that a page exists, and sets a current revision callback to return
1529 * a revision with custom content when the current revision of the page is requested.
1531 * @param PageIdentity $page
1532 * @param Content $content
1533 * @param UserIdentity $user The user that the fake revision is attributed to
1534 * @param int $currentRevId
1536 * @return ScopedCallback to unset the hook
1537 * @internal since 1.44, this method is no longer considered safe to call
1538 * by extensions. It may be removed or changed in a backwards incompatible
1539 * way in 1.45 or later.
1541 * @since 1.25
1543 public function setupFakeRevision( $page, $content, $user, $currentRevId = 0 ) {
1544 $oldCallback = $this->setCurrentRevisionRecordCallback(
1545 function ( $titleToCheck, $parser = null )
1546 use ( $page, $content, $user, $currentRevId, &$oldCallback )
1548 if ( $titleToCheck->isSamePageAs( $page ) ) {
1549 if ( $page->exists() ) {
1550 $pageId = $page->getId();
1552 if ( $currentRevId ) {
1553 $parentRevision = $currentRevId;
1554 } elseif ( $page instanceof Title ) {
1555 $parentRevision = $page->getLatestRevID();
1556 } elseif ( $page instanceof PageRecord ) {
1557 $parentRevision = $page->getLatest();
1558 } else {
1559 $parentRevision = 0;
1561 } else {
1562 $pageId = $this->getSpeculativePageId() ?: 0;
1563 $parentRevision = 0;
1564 $page = new PageIdentityValue(
1565 $pageId,
1566 $page->getNamespace(),
1567 $page->getDBkey(),
1568 $page->getWikiId()
1572 $revRecord = new MutableRevisionRecord( $page );
1573 $revRecord->setContent( SlotRecord::MAIN, $content )
1574 ->setUser( $user )
1575 ->setTimestamp( MWTimestamp::now( TS_MW ) )
1576 ->setId( 0 )
1577 ->setPageId( $pageId )
1578 ->setParentId( $parentRevision );
1579 return $revRecord;
1580 } else {
1581 return call_user_func( $oldCallback, $titleToCheck, $parser );
1586 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1587 $hookScope = $hookContainer->scopedRegister(
1588 'TitleExists',
1589 static function ( Title $titleToCheck, &$exists ) use ( $page ) {
1590 if ( $titleToCheck->isSamePageAs( $page ) ) {
1591 $exists = true;
1596 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1597 $linkCache->clearBadLink( $page );
1599 return new ScopedCallback( function () use ( $page, $hookScope, $linkCache, $oldCallback ) {
1600 ScopedCallback::consume( $hookScope );
1601 $linkCache->clearLink( $page );
1602 $this->setCurrentRevisionRecordCallback( $oldCallback );
1603 } );
1607 * Returns reason for rendering the content. This human-readable, intended for logging and debugging only.
1608 * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1609 * @return string
1611 public function getRenderReason(): string {
1612 return $this->renderReason;
1616 * Sets reason for rendering the content. This human-readable, intended for logging and debugging only.
1617 * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1618 * @param string $renderReason
1620 public function setRenderReason( string $renderReason ): void {
1621 $this->renderReason = $renderReason;
1625 /** @deprecated class alias since 1.43 */
1626 class_alias( ParserOptions::class, 'ParserOptions' );
1629 * For really cool vim folding this needs to be at the end:
1630 * vim: foldmarker=@{,@} foldmethod=marker