Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.12% covered (warning)
88.12%
304 / 345
67.92% covered (warning)
67.92%
72 / 106
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserOptions
88.37% covered (warning)
88.37%
304 / 344
67.92% covered (warning)
67.92%
72 / 106
194.26
0.00% covered (danger)
0.00%
0 / 1
 getOption
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 lazyLoadOption
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 nullifyLazyOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLazyOptions
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getCacheVaryingOptionsHash
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setOption
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setOptionLegacy
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getInterwikiMagic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInterwikiMagic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowExternalImages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowExternalImagesFrom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEnableImageWhitelist
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowSpecialInclusion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAllowSpecialInclusion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInterfaceMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInterfaceMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTargetLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTargetLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxIncludeSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxIncludeSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxPPNodeCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxPPNodeCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaxPPExpandDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxTemplateDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxTemplateDepth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExpensiveParserFunctionLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExpensiveParserFunctionLimit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRemoveComments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRemoveComments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEnableLimitReport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enableLimitReport
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCleanSignatures
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCleanSignatures
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExternalLinkTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExternalLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisableContentConversion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 disableContentConversion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisableTitleConversion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 disableTitleConversion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setThumbSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIsPreview
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIsPreview
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIsSectionPreview
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIsSectionPreview
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIsPrintable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIsPrintable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPreSaveTransform
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPreSaveTransform
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUseParsoid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUseParsoid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDateFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initDateFormat
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDateFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserLangObj
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserLang
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUserLang
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMagicISBNLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMagicPMIDLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMagicRFCLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSuppressTOC
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSuppressTOC
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSuppressSectionEditLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSuppressSectionEditLinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCollapsibleSections
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCollapsibleSections
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowUnsafeRawHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAllowUnsafeRawHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWrapOutputClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setWrapOutputClass
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getCurrentRevisionRecordCallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentRevisionRecordCallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTemplateCallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTemplateCallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpeculativeRevId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSpeculativePageId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initSpeculativeRevId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initSpeculativePageId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setSpeculativeRevIdCallback
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setSpeculativePageIdCallback
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestamp
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRedirectTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRedirectTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addExtraKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserIdentity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 newFromAnon
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newFromUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromUserAndLang
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromContext
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 newCanonical
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 clearStaticCache
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getDefaults
100.00% covered (success)
100.00%
71 / 71
100.00% covered (success)
100.00%
1 / 1
2
 initialiseFromUser
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 matches
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
8
 matchesForCacheKey
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 registerWatcher
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 optionUsed
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 allCacheVaryingOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 optionToString
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 optionsHash
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
7
 isSafeToCache
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 setupFakeRevision
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
3
 getRenderReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRenderReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Options for the PHP parser
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Parser
22 */
23
24namespace MediaWiki\Parser;
25
26use InvalidArgumentException;
27use LogicException;
28use MediaWiki\Content\Content;
29use MediaWiki\Context\IContextSource;
30use MediaWiki\HookContainer\HookRunner;
31use MediaWiki\Language\Language;
32use MediaWiki\MainConfigNames;
33use MediaWiki\MediaWikiServices;
34use MediaWiki\Revision\MutableRevisionRecord;
35use MediaWiki\Revision\SlotRecord;
36use MediaWiki\StubObject\StubObject;
37use MediaWiki\Title\Title;
38use MediaWiki\User\UserIdentity;
39use MediaWiki\User\UserIdentityValue;
40use MediaWiki\Utils\MWTimestamp;
41use ReflectionClass;
42use Wikimedia\IPUtils;
43use Wikimedia\ScopedCallback;
44
45/**
46 * @brief Set options of the Parser
47 *
48 * How to add an option in core:
49 *  1. Add it to one of the arrays in ParserOptions::setDefaults()
50 *  2. If necessary, add an entry to ParserOptions::$inCacheKey
51 *  3. Add a getter and setter in the section for that.
52 *
53 * How to add an option in an extension:
54 *  1. Use the 'ParserOptionsRegister' hook to register it.
55 *  2. Where necessary, use $popt->getOption() and $popt->setOption()
56 *     to access it.
57 *
58 * @ingroup Parser
59 */
60class ParserOptions {
61
62    /**
63     * Default values for all options that are relevant for caching.
64     * @see self::getDefaults()
65     * @var array|null
66     */
67    private static $defaults = null;
68
69    /**
70     * Lazy-loaded options
71     * @var callable[]|null
72     */
73    private static $lazyOptions = null;
74
75    /**
76     * Initial lazy-loaded options (before hook)
77     * @var callable[]
78     */
79    private static $initialLazyOptions = [
80        'dateformat' => [ __CLASS__, 'initDateFormat' ],
81        'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
82        'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
83    ];
84
85    /**
86     * Specify options that are included in the cache key
87     * @var array|null
88     */
89    private static $cacheVaryingOptionsHash = null;
90
91    /**
92     * Initial inCacheKey options (before hook)
93     * @var array
94     */
95    private static $initialCacheVaryingOptionsHash = [
96        'dateformat' => true,
97        'thumbsize' => true,
98        'printable' => true,
99        'userlang' => true,
100        'useParsoid' => true,
101        'suppressSectionEditLinks' => true,
102        'collapsibleSections' => true,
103    ];
104
105    /**
106     * Specify pseudo-options that are actually callbacks.
107     * These must be ignored when checking for cacheability.
108     * @var array
109     */
110    private static $callbacks = [
111        'currentRevisionRecordCallback' => true,
112        'templateCallback' => true,
113        'speculativeRevIdCallback' => true,
114        'speculativePageIdCallback' => true,
115    ];
116
117    /**
118     * Current values for all options that are relevant for caching.
119     * @var array
120     */
121    private $options;
122
123    /**
124     * Timestamp used for {{CURRENTDAY}} etc.
125     * @var string|null
126     * @note Caching based on parse time is handled externally
127     */
128    private $mTimestamp;
129
130    /**
131     * Stored user object
132     * @var UserIdentity
133     * @todo Track this for caching somehow without fragmenting the cache
134     */
135    private $mUser;
136
137    /**
138     * Function to be called when an option is accessed.
139     * @var callable|null
140     * @note Used for collecting used options, does not affect caching
141     */
142    private $onAccessCallback = null;
143
144    /**
145     * If the page being parsed is a redirect, this should hold the redirect
146     * target.
147     * @var Title|null
148     * @todo Track this for caching somehow
149     */
150    private $redirectTarget = null;
151
152    /**
153     * Appended to the options hash
154     * @var string
155     */
156    private $mExtraKey = '';
157
158    /**
159     * The reason for rendering the content.
160     * @var string
161     */
162    private $renderReason = 'unknown';
163
164    /**
165     * Fetch an option and track that is was accessed
166     * @since 1.30
167     * @param string $name Option name
168     * @return mixed
169     */
170    public function getOption( $name ) {
171        if ( !array_key_exists( $name, $this->options ) ) {
172            throw new InvalidArgumentException( "Unknown parser option $name" );
173        }
174
175        $this->lazyLoadOption( $name );
176        $this->optionUsed( $name );
177        return $this->options[$name];
178    }
179
180    /**
181     * @param string $name Lazy load option without tracking usage
182     */
183    private function lazyLoadOption( $name ) {
184        $lazyOptions = self::getLazyOptions();
185        if ( isset( $lazyOptions[$name] ) && $this->options[$name] === null ) {
186            $this->options[$name] = call_user_func( $lazyOptions[$name], $this, $name );
187        }
188    }
189
190    /**
191     * Resets lazy loaded options to null in the provided $options array
192     * @param array $options
193     * @return array
194     */
195    private function nullifyLazyOption( array $options ): array {
196        return array_fill_keys( array_keys( self::getLazyOptions() ), null ) + $options;
197    }
198
199    /**
200     * Get lazy-loaded options.
201     *
202     * This array should be initialised by the constructor. The return type
203     * hint is used as an assertion to ensure this has happened and to coerce
204     * the type for static analysis.
205     *
206     * @internal Public for testing only
207     *
208     * @return array
209     */
210    public static function getLazyOptions(): array {
211        // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
212        // already been called.
213        if ( self::$lazyOptions === null ) {
214            self::getDefaults();
215        }
216        return self::$lazyOptions;
217    }
218
219    /**
220     * Get cache varying options, with the name of the option in the key, and a
221     * boolean in the value which indicates whether the cache is indeed varied.
222     *
223     * @see self::allCacheVaryingOptions()
224     *
225     * @return array
226     */
227    private static function getCacheVaryingOptionsHash(): array {
228        // Trigger a call to the 'ParserOptionsRegister' hook if it hasn't
229        // already been called.
230        if ( self::$cacheVaryingOptionsHash === null ) {
231            self::getDefaults();
232        }
233        return self::$cacheVaryingOptionsHash;
234    }
235
236    /**
237     * Set an option, generically
238     * @since 1.30
239     * @param string $name Option name
240     * @param mixed $value New value. Passing null will set null, unlike many
241     *  of the existing accessors which ignore null for historical reasons.
242     * @return mixed Old value
243     */
244    public function setOption( $name, $value ) {
245        if ( !array_key_exists( $name, $this->options ) ) {
246            throw new InvalidArgumentException( "Unknown parser option $name" );
247        }
248        $old = $this->options[$name];
249        $this->options[$name] = $value;
250        return $old;
251    }
252
253    /**
254     * Legacy implementation
255     * @since 1.30 For implementing legacy setters only. Don't use this in new code.
256     * @deprecated since 1.30
257     * @param string $name Option name
258     * @param mixed $value New value. Passing null does not set the value.
259     * @return mixed Old value
260     */
261    protected function setOptionLegacy( $name, $value ) {
262        if ( !array_key_exists( $name, $this->options ) ) {
263            throw new InvalidArgumentException( "Unknown parser option $name" );
264        }
265        return wfSetVar( $this->options[$name], $value );
266    }
267
268    /**
269     * Whether to extract interlanguage links
270     *
271     * When true, interlanguage links will be returned by
272     * ParserOutput::getLanguageLinks() instead of generating link HTML.
273     *
274     * @return bool
275     */
276    public function getInterwikiMagic() {
277        return $this->getOption( 'interwikiMagic' );
278    }
279
280    /**
281     * Specify whether to extract interlanguage links
282     * @param bool|null $x New value (null is no change)
283     * @return bool Old value
284     */
285    public function setInterwikiMagic( $x ) {
286        return $this->setOptionLegacy( 'interwikiMagic', $x );
287    }
288
289    /**
290     * Allow all external images inline?
291     * @return bool
292     */
293    public function getAllowExternalImages() {
294        return $this->getOption( 'allowExternalImages' );
295    }
296
297    /**
298     * External images to allow
299     *
300     * When self::getAllowExternalImages() is false
301     *
302     * @return string|string[] URLs to allow
303     */
304    public function getAllowExternalImagesFrom() {
305        return $this->getOption( 'allowExternalImagesFrom' );
306    }
307
308    /**
309     * Use the on-wiki external image whitelist?
310     * @return bool
311     */
312    public function getEnableImageWhitelist() {
313        return $this->getOption( 'enableImageWhitelist' );
314    }
315
316    /**
317     * Allow inclusion of special pages?
318     * @return bool
319     */
320    public function getAllowSpecialInclusion() {
321        return $this->getOption( 'allowSpecialInclusion' );
322    }
323
324    /**
325     * Allow inclusion of special pages?
326     * @param bool|null $x New value (null is no change)
327     * @return bool Old value
328     */
329    public function setAllowSpecialInclusion( $x ) {
330        return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
331    }
332
333    /**
334     * Parsing an interface message?
335     * @return bool
336     */
337    public function getInterfaceMessage() {
338        return $this->getOption( 'interfaceMessage' );
339    }
340
341    /**
342     * Parsing an interface message?
343     * @param bool|null $x New value (null is no change)
344     * @return bool Old value
345     */
346    public function setInterfaceMessage( $x ) {
347        return $this->setOptionLegacy( 'interfaceMessage', $x );
348    }
349
350    /**
351     * Target language for the parse
352     * @return Language|null
353     */
354    public function getTargetLanguage() {
355        return $this->getOption( 'targetLanguage' );
356    }
357
358    /**
359     * Target language for the parse
360     * @param Language|null $x New value
361     * @return Language|null Old value
362     */
363    public function setTargetLanguage( $x ) {
364        return $this->setOption( 'targetLanguage', $x );
365    }
366
367    /**
368     * Maximum size of template expansions, in bytes
369     * @return int
370     */
371    public function getMaxIncludeSize() {
372        return $this->getOption( 'maxIncludeSize' );
373    }
374
375    /**
376     * Maximum size of template expansions, in bytes
377     * @param int|null $x New value (null is no change)
378     * @return int Old value
379     */
380    public function setMaxIncludeSize( $x ) {
381        return $this->setOptionLegacy( 'maxIncludeSize', $x );
382    }
383
384    /**
385     * Maximum number of nodes touched by PPFrame::expand()
386     * @return int
387     */
388    public function getMaxPPNodeCount() {
389        return $this->getOption( 'maxPPNodeCount' );
390    }
391
392    /**
393     * Maximum number of nodes touched by PPFrame::expand()
394     * @param int|null $x New value (null is no change)
395     * @return int Old value
396     */
397    public function setMaxPPNodeCount( $x ) {
398        return $this->setOptionLegacy( 'maxPPNodeCount', $x );
399    }
400
401    /**
402     * Maximum recursion depth in PPFrame::expand()
403     * @return int
404     */
405    public function getMaxPPExpandDepth() {
406        return $this->getOption( 'maxPPExpandDepth' );
407    }
408
409    /**
410     * Maximum recursion depth for templates within templates
411     * @return int
412     * @internal Only used by Parser (T318826)
413     */
414    public function getMaxTemplateDepth() {
415        return $this->getOption( 'maxTemplateDepth' );
416    }
417
418    /**
419     * Maximum recursion depth for templates within templates
420     * @param int|null $x New value (null is no change)
421     * @return int Old value
422     * @internal Only used by ParserTestRunner (T318826)
423     */
424    public function setMaxTemplateDepth( $x ) {
425        return $this->setOptionLegacy( 'maxTemplateDepth', $x );
426    }
427
428    /**
429     * Maximum number of calls per parse to expensive parser functions
430     * @since 1.20
431     * @return int
432     */
433    public function getExpensiveParserFunctionLimit() {
434        return $this->getOption( 'expensiveParserFunctionLimit' );
435    }
436
437    /**
438     * Maximum number of calls per parse to expensive parser functions
439     * @since 1.20
440     * @param int|null $x New value (null is no change)
441     * @return int Old value
442     */
443    public function setExpensiveParserFunctionLimit( $x ) {
444        return $this->setOptionLegacy( 'expensiveParserFunctionLimit', $x );
445    }
446
447    /**
448     * Remove HTML comments
449     * @warning Only applies to preprocess operations
450     * @return bool
451     */
452    public function getRemoveComments() {
453        return $this->getOption( 'removeComments' );
454    }
455
456    /**
457     * Remove HTML comments
458     * @warning Only applies to preprocess operations
459     * @param bool|null $x New value (null is no change)
460     * @return bool Old value
461     */
462    public function setRemoveComments( $x ) {
463        return $this->setOptionLegacy( 'removeComments', $x );
464    }
465
466    /**
467     * @deprecated since 1.38. This does nothing now, to control limit reporting
468     * please provide 'includeDebugInfo' option to ParserOutput::getText.
469     *
470     * Enable limit report in an HTML comment on output
471     * @return bool
472     */
473    public function getEnableLimitReport() {
474        return false;
475    }
476
477    /**
478     * @deprecated since 1.38. This does nothing now, to control limit reporting
479     * please provide 'includeDebugInfo' option to ParserOutput::getText.
480     *
481     * Enable limit report in an HTML comment on output
482     * @param bool|null $x New value (null is no change)
483     * @return bool Old value
484     */
485    public function enableLimitReport( $x = true ) {
486        return false;
487    }
488
489    /**
490     * Clean up signature texts?
491     * @see Parser::cleanSig
492     * @return bool
493     */
494    public function getCleanSignatures() {
495        return $this->getOption( 'cleanSignatures' );
496    }
497
498    /**
499     * Clean up signature texts?
500     * @see Parser::cleanSig
501     * @param bool|null $x New value (null is no change)
502     * @return bool Old value
503     */
504    public function setCleanSignatures( $x ) {
505        return $this->setOptionLegacy( 'cleanSignatures', $x );
506    }
507
508    /**
509     * Target attribute for external links
510     * @return string|false
511     * @internal Only set by installer (T317647)
512     */
513    public function getExternalLinkTarget() {
514        return $this->getOption( 'externalLinkTarget' );
515    }
516
517    /**
518     * Target attribute for external links
519     * @param string|false|null $x New value (null is no change)
520     * @return string Old value
521     * @internal Only used by installer (T317647)
522     */
523    public function setExternalLinkTarget( $x ) {
524        return $this->setOptionLegacy( 'externalLinkTarget', $x );
525    }
526
527    /**
528     * Whether content conversion should be disabled
529     * @return bool
530     */
531    public function getDisableContentConversion() {
532        return $this->getOption( 'disableContentConversion' );
533    }
534
535    /**
536     * Whether content conversion should be disabled
537     * @param bool|null $x New value (null is no change)
538     * @return bool Old value
539     */
540    public function disableContentConversion( $x = true ) {
541        return $this->setOptionLegacy( 'disableContentConversion', $x );
542    }
543
544    /**
545     * Whether title conversion should be disabled
546     * @return bool
547     */
548    public function getDisableTitleConversion() {
549        return $this->getOption( 'disableTitleConversion' );
550    }
551
552    /**
553     * Whether title conversion should be disabled
554     * @param bool|null $x New value (null is no change)
555     * @return bool Old value
556     */
557    public function disableTitleConversion( $x = true ) {
558        return $this->setOptionLegacy( 'disableTitleConversion', $x );
559    }
560
561    /**
562     * Thumb size preferred by the user.
563     * @return int
564     */
565    public function getThumbSize() {
566        return $this->getOption( 'thumbsize' );
567    }
568
569    /**
570     * Thumb size preferred by the user.
571     * @param int|null $x New value (null is no change)
572     * @return int Old value
573     */
574    public function setThumbSize( $x ) {
575        return $this->setOptionLegacy( 'thumbsize', $x );
576    }
577
578    /**
579     * Parsing the page for a "preview" operation?
580     * @return bool
581     */
582    public function getIsPreview() {
583        return $this->getOption( 'isPreview' );
584    }
585
586    /**
587     * Parsing the page for a "preview" operation?
588     * @param bool|null $x New value (null is no change)
589     * @return bool Old value
590     */
591    public function setIsPreview( $x ) {
592        return $this->setOptionLegacy( 'isPreview', $x );
593    }
594
595    /**
596     * Parsing the page for a "preview" operation on a single section?
597     * @return bool
598     */
599    public function getIsSectionPreview() {
600        return $this->getOption( 'isSectionPreview' );
601    }
602
603    /**
604     * Parsing the page for a "preview" operation on a single section?
605     * @param bool|null $x New value (null is no change)
606     * @return bool Old value
607     */
608    public function setIsSectionPreview( $x ) {
609        return $this->setOptionLegacy( 'isSectionPreview', $x );
610    }
611
612    /**
613     * Parsing the printable version of the page?
614     * @return bool
615     */
616    public function getIsPrintable() {
617        return $this->getOption( 'printable' );
618    }
619
620    /**
621     * Parsing the printable version of the page?
622     * @param bool|null $x New value (null is no change)
623     * @return bool Old value
624     */
625    public function setIsPrintable( $x ) {
626        return $this->setOptionLegacy( 'printable', $x );
627    }
628
629    /**
630     * Transform wiki markup when saving the page?
631     * @return bool
632     */
633    public function getPreSaveTransform() {
634        return $this->getOption( 'preSaveTransform' );
635    }
636
637    /**
638     * Transform wiki markup when saving the page?
639     * @param bool|null $x New value (null is no change)
640     * @return bool Old value
641     */
642    public function setPreSaveTransform( $x ) {
643        return $this->setOptionLegacy( 'preSaveTransform', $x );
644    }
645
646    /**
647     * Parsoid-format HTML output, or legacy wikitext parser HTML?
648     * @see T300191
649     * @unstable
650     * @since 1.41
651     * @return bool
652     */
653    public function getUseParsoid(): bool {
654        return $this->getOption( 'useParsoid' );
655    }
656
657    /**
658     * Request Parsoid-format HTML output.
659     * @see T300191
660     * @unstable
661     * @since 1.41
662     */
663    public function setUseParsoid() {
664        $this->setOption( 'useParsoid', true );
665    }
666
667    /**
668     * Date format index
669     * @return string
670     */
671    public function getDateFormat() {
672        return $this->getOption( 'dateformat' );
673    }
674
675    /**
676     * Lazy initializer for dateFormat
677     * @param ParserOptions $popt
678     * @return string
679     */
680    private static function initDateFormat( ParserOptions $popt ) {
681        $userFactory = MediaWikiServices::getInstance()->getUserFactory();
682        return $userFactory->newFromUserIdentity( $popt->getUserIdentity() )->getDatePreference();
683    }
684
685    /**
686     * Date format index
687     * @param string|null $x New value (null is no change)
688     * @return string Old value
689     */
690    public function setDateFormat( $x ) {
691        return $this->setOptionLegacy( 'dateformat', $x );
692    }
693
694    /**
695     * Get the user language used by the parser for this page and split the parser cache.
696     *
697     * @warning Calling this causes the parser cache to be fragmented by user language!
698     * To avoid cache fragmentation, output should not depend on the user language.
699     * Use Parser::getTargetLanguage() instead!
700     *
701     * @note This function will trigger a cache fragmentation by recording the
702     * 'userlang' option, see optionUsed(). This is done to avoid cache pollution
703     * when the page is rendered based on the language of the user.
704     *
705     * @note When saving, this will return the default language instead of the user's.
706     * {{int: }} uses this which used to produce inconsistent link tables (T16404).
707     *
708     * @return Language
709     * @since 1.19
710     */
711    public function getUserLangObj() {
712        return $this->getOption( 'userlang' );
713    }
714
715    /**
716     * Same as getUserLangObj() but returns a string instead.
717     *
718     * @warning Calling this causes the parser cache to be fragmented by user language!
719     * To avoid cache fragmentation, output should not depend on the user language.
720     * Use Parser::getTargetLanguage() instead!
721     *
722     * @see getUserLangObj()
723     *
724     * @return string Language code
725     * @since 1.17
726     */
727    public function getUserLang() {
728        return $this->getUserLangObj()->getCode();
729    }
730
731    /**
732     * Set the user language used by the parser for this page and split the parser cache.
733     * @param string|Language $x New value
734     * @return Language Old value
735     */
736    public function setUserLang( $x ) {
737        if ( is_string( $x ) ) {
738            $x = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $x );
739        }
740
741        return $this->setOptionLegacy( 'userlang', $x );
742    }
743
744    /**
745     * Are magic ISBN links enabled?
746     * @since 1.28
747     * @return bool
748     */
749    public function getMagicISBNLinks() {
750        return $this->getOption( 'magicISBNLinks' );
751    }
752
753    /**
754     * Are magic PMID links enabled?
755     * @since 1.28
756     * @return bool
757     */
758    public function getMagicPMIDLinks() {
759        return $this->getOption( 'magicPMIDLinks' );
760    }
761
762    /**
763     * Are magic RFC links enabled?
764     * @since 1.28
765     * @return bool
766     */
767    public function getMagicRFCLinks() {
768        return $this->getOption( 'magicRFCLinks' );
769    }
770
771    /**
772     * Should the table of contents be suppressed?
773     * Used when parsing "code" pages (like JavaScript) as wikitext
774     * for backlink support and categories, but where we don't want
775     * other metadata generated (like the table of contents).
776     * @see T307691
777     * @since 1.39
778     * @return bool
779     */
780    public function getSuppressTOC() {
781        return $this->getOption( 'suppressTOC' );
782    }
783
784    /**
785     * Suppress generation of the table of contents.
786     * Used when parsing "code" pages (like JavaScript) as wikitext
787     * for backlink support and categories, but where we don't want
788     * other metadata generated (like the table of contents).
789     * @see T307691
790     * @since 1.39
791     * @deprecated since 1.42; just clear the metadata in the final
792     *  parser output
793     */
794    public function setSuppressTOC() {
795        wfDeprecated( __METHOD__, '1.42' );
796        $this->setOption( 'suppressTOC', true );
797    }
798
799    /**
800     * Should section edit links be suppressed?
801     * Used when parsing wikitext which will be presented in a
802     * non-interactive context: previews, UX text, etc.
803     * @since 1.42
804     * @return bool
805     */
806    public function getSuppressSectionEditLinks() {
807        return $this->getOption( 'suppressSectionEditLinks' );
808    }
809
810    /**
811     * Suppress section edit links in the output.
812     * Used when parsing wikitext which will be presented in a
813     * non-interactive context: previews, UX text, etc.
814     * @since 1.42
815     */
816    public function setSuppressSectionEditLinks() {
817        $this->setOption( 'suppressSectionEditLinks', true );
818    }
819
820    /**
821     * Should section contents be wrapped in <div> to make them
822     * collapsible?
823     * @since 1.42
824     */
825    public function getCollapsibleSections(): bool {
826        return $this->getOption( 'collapsibleSections' );
827    }
828
829    /**
830     * Wrap section contents in a <div> to allow client-side code
831     * to collapse them.
832     * @since 1.42
833     */
834    public function setCollapsibleSections(): void {
835        $this->setOption( 'collapsibleSections', true );
836    }
837
838    /**
839     * If the wiki is configured to allow raw html ($wgRawHtml = true)
840     * is it allowed in the specific case of parsing this page.
841     *
842     * This is meant to disable unsafe parser tags in cases where
843     * a malicious user may control the input to the parser.
844     *
845     * @note This is expected to be true for normal pages even if the
846     *  wiki has $wgRawHtml disabled in general. The setting only
847     *  signifies that raw html would be unsafe in the current context
848     *  provided that raw html is allowed at all.
849     * @since 1.29
850     * @return bool
851     */
852    public function getAllowUnsafeRawHtml() {
853        return $this->getOption( 'allowUnsafeRawHtml' );
854    }
855
856    /**
857     * If the wiki is configured to allow raw html ($wgRawHtml = true)
858     * is it allowed in the specific case of parsing this page.
859     * @see self::getAllowUnsafeRawHtml()
860     * @since 1.29
861     * @param bool|null $x Value to set or null to get current value
862     * @return bool Current value for allowUnsafeRawHtml
863     */
864    public function setAllowUnsafeRawHtml( $x ) {
865        return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
866    }
867
868    /**
869     * Class to use to wrap output from Parser::parse()
870     * @since 1.30
871     * @return string|false
872     */
873    public function getWrapOutputClass() {
874        return $this->getOption( 'wrapclass' );
875    }
876
877    /**
878     * CSS class to use to wrap output from Parser::parse()
879     * @since 1.30
880     * @param string $className Class name to use for wrapping.
881     *   Passing false to indicate "no wrapping" was deprecated in MediaWiki 1.31.
882     * @return string|false Current value
883     */
884    public function setWrapOutputClass( $className ) {
885        if ( $className === true ) { // DWIM, they probably want the default class name
886            $className = 'mw-parser-output';
887        }
888        if ( $className === false ) {
889            wfDeprecated( __METHOD__ . '( false )', '1.31' );
890        }
891        return $this->setOption( 'wrapclass', $className );
892    }
893
894    /**
895     * Callback for current revision fetching; first argument to call_user_func().
896     * @internal
897     * @since 1.35
898     * @return callable
899     */
900    public function getCurrentRevisionRecordCallback() {
901        return $this->getOption( 'currentRevisionRecordCallback' );
902    }
903
904    /**
905     * Callback for current revision fetching; first argument to call_user_func().
906     * @internal
907     * @since 1.35
908     * @param callable|null $x New value
909     * @return callable Old value
910     */
911    public function setCurrentRevisionRecordCallback( $x ) {
912        return $this->setOption( 'currentRevisionRecordCallback', $x );
913    }
914
915    /**
916     * Callback for template fetching; first argument to call_user_func().
917     * @return callable
918     */
919    public function getTemplateCallback() {
920        return $this->getOption( 'templateCallback' );
921    }
922
923    /**
924     * Callback for template fetching; first argument to call_user_func().
925     * @param callable|null $x New value (null is no change)
926     * @return callable Old value
927     */
928    public function setTemplateCallback( $x ) {
929        return $this->setOptionLegacy( 'templateCallback', $x );
930    }
931
932    /**
933     * A guess for {{REVISIONID}}, calculated using the callback provided via
934     * setSpeculativeRevIdCallback(). For consistency, the value will be calculated upon the
935     * first call of this method, and re-used for subsequent calls.
936     *
937     * If no callback was defined via setSpeculativeRevIdCallback(), this method will return false.
938     *
939     * @since 1.32
940     * @return int|false
941     */
942    public function getSpeculativeRevId() {
943        return $this->getOption( 'speculativeRevId' );
944    }
945
946    /**
947     * A guess for {{PAGEID}}, calculated using the callback provided via
948     * setSpeculativeRevPageCallback(). For consistency, the value will be calculated upon the
949     * first call of this method, and re-used for subsequent calls.
950     *
951     * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
952     *
953     * @since 1.34
954     * @return int|false
955     */
956    public function getSpeculativePageId() {
957        return $this->getOption( 'speculativePageId' );
958    }
959
960    /**
961     * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
962     *
963     * @param ParserOptions $popt
964     * @return int|false
965     */
966    private static function initSpeculativeRevId( ParserOptions $popt ) {
967        $cb = $popt->getOption( 'speculativeRevIdCallback' );
968        $id = $cb ? $cb() : null;
969
970        // returning null would result in this being re-called every access
971        return $id ?? false;
972    }
973
974    /**
975     * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
976     *
977     * @param ParserOptions $popt
978     * @return int|false
979     */
980    private static function initSpeculativePageId( ParserOptions $popt ) {
981        $cb = $popt->getOption( 'speculativePageIdCallback' );
982        $id = $cb ? $cb() : null;
983
984        // returning null would result in this being re-called every access
985        return $id ?? false;
986    }
987
988    /**
989     * Callback to generate a guess for {{REVISIONID}}
990     * @param callable|null $x New value
991     * @return callable|null Old value
992     * @since 1.28
993     */
994    public function setSpeculativeRevIdCallback( $x ) {
995        $this->setOption( 'speculativeRevId', null ); // reset
996        return $this->setOption( 'speculativeRevIdCallback', $x );
997    }
998
999    /**
1000     * Callback to generate a guess for {{PAGEID}}
1001     * @param callable|null $x New value
1002     * @return callable|null Old value
1003     * @since 1.34
1004     */
1005    public function setSpeculativePageIdCallback( $x ) {
1006        $this->setOption( 'speculativePageId', null ); // reset
1007        return $this->setOption( 'speculativePageIdCallback', $x );
1008    }
1009
1010    /**
1011     * Timestamp used for {{CURRENTDAY}} etc.
1012     * @return string TS_MW timestamp
1013     */
1014    public function getTimestamp() {
1015        if ( !isset( $this->mTimestamp ) ) {
1016            $this->mTimestamp = wfTimestampNow();
1017        }
1018        return $this->mTimestamp;
1019    }
1020
1021    /**
1022     * Timestamp used for {{CURRENTDAY}} etc.
1023     * @param string|null $x New value (null is no change)
1024     * @return string Old value
1025     */
1026    public function setTimestamp( $x ) {
1027        return wfSetVar( $this->mTimestamp, $x );
1028    }
1029
1030    /**
1031     * Note that setting or changing this does not *make* the page a redirect
1032     * or change its target, it merely records the information for reference
1033     * during the parse.
1034     *
1035     * @since 1.24
1036     * @param Title|null $title
1037     */
1038    public function setRedirectTarget( $title ) {
1039        $this->redirectTarget = $title;
1040    }
1041
1042    /**
1043     * Get the previously-set redirect target.
1044     *
1045     * @since 1.24
1046     * @return Title|null
1047     */
1048    public function getRedirectTarget() {
1049        return $this->redirectTarget;
1050    }
1051
1052    /**
1053     * Extra key that should be present in the parser cache key.
1054     * @warning Consider registering your additional options with the
1055     *  ParserOptionsRegister hook instead of using this method.
1056     * @param string $key
1057     */
1058    public function addExtraKey( $key ) {
1059        $this->mExtraKey .= '!' . $key;
1060    }
1061
1062    /**
1063     * Get the identity of the user for whom the parse is made.
1064     * @since 1.36
1065     * @return UserIdentity
1066     */
1067    public function getUserIdentity(): UserIdentity {
1068        return $this->mUser;
1069    }
1070
1071    /**
1072     * @param UserIdentity $user
1073     * @param Language|null $lang
1074     */
1075    public function __construct( UserIdentity $user, $lang = null ) {
1076        if ( $lang === null ) {
1077            global $wgLang;
1078            StubObject::unstub( $wgLang );
1079            $lang = $wgLang;
1080        }
1081        $this->initialiseFromUser( $user, $lang );
1082    }
1083
1084    /**
1085     * Get a ParserOptions object for an anonymous user
1086     * @since 1.27
1087     * @return ParserOptions
1088     */
1089    public static function newFromAnon() {
1090        return new ParserOptions( MediaWikiServices::getInstance()->getUserFactory()->newAnonymous(),
1091            MediaWikiServices::getInstance()->getContentLanguage() );
1092    }
1093
1094    /**
1095     * Get a ParserOptions object from a given user.
1096     * Language will be taken from $wgLang.
1097     *
1098     * @param UserIdentity $user
1099     * @return ParserOptions
1100     */
1101    public static function newFromUser( $user ) {
1102        return new ParserOptions( $user );
1103    }
1104
1105    /**
1106     * Get a ParserOptions object from a given user and language
1107     *
1108     * @param UserIdentity $user
1109     * @param Language $lang
1110     * @return ParserOptions
1111     */
1112    public static function newFromUserAndLang( UserIdentity $user, Language $lang ) {
1113        return new ParserOptions( $user, $lang );
1114    }
1115
1116    /**
1117     * Get a ParserOptions object from a IContextSource object
1118     *
1119     * @param IContextSource $context
1120     * @return ParserOptions
1121     */
1122    public static function newFromContext( IContextSource $context ) {
1123        $contextUser = $context->getUser();
1124
1125        // Use the stashed temporary account name instead of an IP address as the user for the ParserOptions
1126        // (if a stashed name is set). This is so that magic words like {{REVISIONUSER}} show the temporary account
1127        // name instead of IP address.
1128        $tempUserCreator = MediaWikiServices::getInstance()->getTempUserCreator();
1129        if ( $tempUserCreator->isEnabled() && IPUtils::isIPAddress( $contextUser->getName() ) ) {
1130            // We do not attempt to acquire a temporary account name if no name is stashed, as this may be called in
1131            // contexts (such as the parse API) where the user will not be performing an edit on their next action
1132            // and therefore would be increasing the rate limit unnecessarily.
1133            $tempName = $tempUserCreator->getStashedName( $context->getRequest()->getSession() );
1134            if ( $tempName !== null ) {
1135                $contextUser = UserIdentityValue::newAnonymous( $tempName );
1136            }
1137        }
1138
1139        return new ParserOptions( $contextUser, $context->getLanguage() );
1140    }
1141
1142    /**
1143     * Creates a "canonical" ParserOptions object
1144     *
1145     * For historical reasons, certain options have default values that are
1146     * different from the canonical values used for caching.
1147     *
1148     * @since 1.30
1149     * @since 1.32 Added string and IContextSource as options for the first parameter
1150     * @since 1.36 UserIdentity is also allowed
1151     * @deprecated since 1.38. Use ::newFromContext, ::newFromAnon or ::newFromUserAndLang instead.
1152     *   Canonical ParserOptions are now exactly the same as non-canonical.
1153     * @param IContextSource|string|UserIdentity $context
1154     *  - If an IContextSource, the options are initialized based on the source's UserIdentity and Language.
1155     *  - If the string 'canonical', the options are initialized with an anonymous user and
1156     *    the content language.
1157     *  - If a UserIdentity, the options are initialized for that UserIdentity
1158     *    'userlang' is taken from the $userLang parameter, defaulting to $wgLang if that is null.
1159     * @param Language|StubObject|null $userLang (see above)
1160     * @return ParserOptions
1161     */
1162    public static function newCanonical( $context, $userLang = null ) {
1163        if ( $context instanceof IContextSource ) {
1164            $ret = self::newFromContext( $context );
1165        } elseif ( $context === 'canonical' ) {
1166            $ret = self::newFromAnon();
1167        } elseif ( $context instanceof UserIdentity ) {
1168            $ret = new self( $context, $userLang );
1169        } else {
1170            throw new InvalidArgumentException(
1171                '$context must be an IContextSource, the string "canonical", or a UserIdentity'
1172            );
1173        }
1174        return $ret;
1175    }
1176
1177    /**
1178     * Reset static caches
1179     * @internal For testing
1180     */
1181    public static function clearStaticCache() {
1182        if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
1183            throw new LogicException( __METHOD__ . ' is just for testing' );
1184        }
1185        self::$defaults = null;
1186        self::$lazyOptions = null;
1187        self::$cacheVaryingOptionsHash = null;
1188    }
1189
1190    /**
1191     * Get default option values
1192     * @warning If you change the default for an existing option, all existing
1193     *  parser cache entries will be invalid. To avoid bugs, you'll need to handle
1194     *  that somehow (e.g. with the RejectParserCacheValue hook) because
1195     *  MediaWiki won't do it for you.
1196     * @return array
1197     */
1198    private static function getDefaults() {
1199        $services = MediaWikiServices::getInstance();
1200        $mainConfig = $services->getMainConfig();
1201        $interwikiMagic = $mainConfig->get( MainConfigNames::InterwikiMagic );
1202        $allowExternalImages = $mainConfig->get( MainConfigNames::AllowExternalImages );
1203        $allowExternalImagesFrom = $mainConfig->get( MainConfigNames::AllowExternalImagesFrom );
1204        $enableImageWhitelist = $mainConfig->get( MainConfigNames::EnableImageWhitelist );
1205        $allowSpecialInclusion = $mainConfig->get( MainConfigNames::AllowSpecialInclusion );
1206        $maxArticleSize = $mainConfig->get( MainConfigNames::MaxArticleSize );
1207        $maxPPNodeCount = $mainConfig->get( MainConfigNames::MaxPPNodeCount );
1208        $maxTemplateDepth = $mainConfig->get( MainConfigNames::MaxTemplateDepth );
1209        $maxPPExpandDepth = $mainConfig->get( MainConfigNames::MaxPPExpandDepth );
1210        $cleanSignatures = $mainConfig->get( MainConfigNames::CleanSignatures );
1211        $externalLinkTarget = $mainConfig->get( MainConfigNames::ExternalLinkTarget );
1212        $expensiveParserFunctionLimit = $mainConfig->get( MainConfigNames::ExpensiveParserFunctionLimit );
1213        $enableMagicLinks = $mainConfig->get( MainConfigNames::EnableMagicLinks );
1214        $languageConverterFactory = $services->getLanguageConverterFactory();
1215        $userOptionsLookup = $services->getUserOptionsLookup();
1216        $contentLanguage = $services->getContentLanguage();
1217
1218        if ( self::$defaults === null ) {
1219            // *UPDATE* ParserOptions::matches() if any of this changes as needed
1220            self::$defaults = [
1221                'dateformat' => null,
1222                'interfaceMessage' => false,
1223                'targetLanguage' => null,
1224                'removeComments' => true,
1225                'suppressTOC' => false,
1226                'suppressSectionEditLinks' => false,
1227                'collapsibleSections' => false,
1228                'enableLimitReport' => false,
1229                'preSaveTransform' => true,
1230                'isPreview' => false,
1231                'isSectionPreview' => false,
1232                'printable' => false,
1233                'allowUnsafeRawHtml' => true,
1234                'wrapclass' => 'mw-parser-output',
1235                'currentRevisionRecordCallback' => [ Parser::class, 'statelessFetchRevisionRecord' ],
1236                'templateCallback' => [ Parser::class, 'statelessFetchTemplate' ],
1237                'speculativeRevIdCallback' => null,
1238                'speculativeRevId' => null,
1239                'speculativePageIdCallback' => null,
1240                'speculativePageId' => null,
1241                'useParsoid' => false,
1242            ];
1243
1244            self::$cacheVaryingOptionsHash = self::$initialCacheVaryingOptionsHash;
1245            self::$lazyOptions = self::$initialLazyOptions;
1246
1247            ( new HookRunner( $services->getHookContainer() ) )->onParserOptionsRegister(
1248                self::$defaults,
1249                self::$cacheVaryingOptionsHash,
1250                self::$lazyOptions
1251            );
1252
1253            ksort( self::$cacheVaryingOptionsHash );
1254        }
1255
1256        // Unit tests depend on being able to modify the globals at will
1257        return self::$defaults + [
1258            'interwikiMagic' => $interwikiMagic,
1259            'allowExternalImages' => $allowExternalImages,
1260            'allowExternalImagesFrom' => $allowExternalImagesFrom,
1261            'enableImageWhitelist' => $enableImageWhitelist,
1262            'allowSpecialInclusion' => $allowSpecialInclusion,
1263            'maxIncludeSize' => $maxArticleSize * 1024,
1264            'maxPPNodeCount' => $maxPPNodeCount,
1265            'maxPPExpandDepth' => $maxPPExpandDepth,
1266            'maxTemplateDepth' => $maxTemplateDepth,
1267            'expensiveParserFunctionLimit' => $expensiveParserFunctionLimit,
1268            'externalLinkTarget' => $externalLinkTarget,
1269            'cleanSignatures' => $cleanSignatures,
1270            'disableContentConversion' => $languageConverterFactory->isConversionDisabled(),
1271            'disableTitleConversion' => $languageConverterFactory->isLinkConversionDisabled(),
1272            // FIXME: The fallback to false for enableMagicLinks is a band-aid to allow
1273            // the phpunit entrypoint patch (I82045c207738d152d5b0006f353637cfaa40bb66)
1274            // to be merged.
1275            // It is possible that a test somewhere is globally resetting $wgEnableMagicLinks
1276            // to null, or that ParserOptions is somehow similarly getting reset in such a way
1277            // that $enableMagicLinks ends up as null rather than an array. This workaround
1278            // seems harmless, but would be nice to eventually fix the underlying issue.
1279            'magicISBNLinks' => $enableMagicLinks['ISBN'] ?? false,
1280            'magicPMIDLinks' => $enableMagicLinks['PMID'] ?? false,
1281            'magicRFCLinks' => $enableMagicLinks['RFC'] ?? false,
1282            'thumbsize' => $userOptionsLookup->getDefaultOption( 'thumbsize' ),
1283            'userlang' => $contentLanguage,
1284        ];
1285    }
1286
1287    /**
1288     * Get user options
1289     *
1290     * @param UserIdentity $user
1291     * @param Language $lang
1292     */
1293    private function initialiseFromUser( UserIdentity $user, Language $lang ) {
1294        // Initially lazy loaded option defaults must not be taken into account,
1295        // otherwise lazy loading does not work. Setting a default for lazy option
1296        // is useful for matching with canonical options.
1297        $this->options = $this->nullifyLazyOption( self::getDefaults() );
1298
1299        $this->mUser = $user;
1300        $services = MediaWikiServices::getInstance();
1301        $optionsLookup = $services->getUserOptionsLookup();
1302        $this->options['thumbsize'] = $optionsLookup->getOption( $user, 'thumbsize' );
1303        $this->options['userlang'] = $lang;
1304    }
1305
1306    /**
1307     * Check if these options match that of another options set
1308     *
1309     * This ignores report limit settings that only affect HTML comments
1310     *
1311     * @param ParserOptions $other
1312     * @return bool
1313     * @since 1.25
1314     */
1315    public function matches( ParserOptions $other ) {
1316        // Compare most options
1317        $options = array_keys( $this->options );
1318        $options = array_diff( $options, [
1319            'enableLimitReport', // only affects HTML comments
1320            'tidy', // Has no effect since 1.35; removed in 1.36
1321        ] );
1322        foreach ( $options as $option ) {
1323            // Resolve any lazy options
1324            $this->lazyLoadOption( $option );
1325            $other->lazyLoadOption( $option );
1326
1327            $o1 = $this->optionToString( $this->options[$option] );
1328            $o2 = $this->optionToString( $other->options[$option] );
1329            if ( $o1 !== $o2 ) {
1330                return false;
1331            }
1332        }
1333
1334        // Compare most other fields
1335        foreach ( ( new ReflectionClass( $this ) )->getProperties() as $property ) {
1336            $field = $property->getName();
1337            if ( $property->isStatic() ) {
1338                continue;
1339            }
1340            if ( in_array( $field, [
1341                'options', // Already checked above
1342                'onAccessCallback', // only used for ParserOutput option tracking
1343            ] ) ) {
1344                continue;
1345            }
1346
1347            if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
1348                return false;
1349            }
1350        }
1351
1352        return true;
1353    }
1354
1355    /**
1356     * @param ParserOptions $other
1357     * @return bool Whether the cache key relevant options match those of $other
1358     * @since 1.33
1359     */
1360    public function matchesForCacheKey( ParserOptions $other ) {
1361        foreach ( self::allCacheVaryingOptions() as $option ) {
1362            // Populate any lazy options
1363            $this->lazyLoadOption( $option );
1364            $other->lazyLoadOption( $option );
1365
1366            $o1 = $this->optionToString( $this->options[$option] );
1367            $o2 = $this->optionToString( $other->options[$option] );
1368            if ( $o1 !== $o2 ) {
1369                return false;
1370            }
1371        }
1372
1373        return true;
1374    }
1375
1376    /**
1377     * Registers a callback for tracking which ParserOptions which are used.
1378     *
1379     * @since 1.16
1380     * @param callable|null $callback
1381     */
1382    public function registerWatcher( $callback ) {
1383        $this->onAccessCallback = $callback;
1384    }
1385
1386    /**
1387     * Record that an option was internally accessed.
1388     *
1389     * This calls the watcher set by ParserOptions::registerWatcher().
1390     * Typically, the watcher callback is ParserOutput::recordOption().
1391     * The information registered this way is consumed by ParserCache::save().
1392     *
1393     * @param string $optionName Name of the option
1394     */
1395    private function optionUsed( $optionName ) {
1396        if ( $this->onAccessCallback ) {
1397            call_user_func( $this->onAccessCallback, $optionName );
1398        }
1399    }
1400
1401    /**
1402     * Return all option keys that vary the options hash
1403     * @since 1.30
1404     * @return string[]
1405     */
1406    public static function allCacheVaryingOptions() {
1407        return array_keys( array_filter( self::getCacheVaryingOptionsHash() ) );
1408    }
1409
1410    /**
1411     * Convert an option to a string value
1412     * @param mixed $value
1413     * @return string
1414     */
1415    private function optionToString( $value ) {
1416        if ( $value === true ) {
1417            return '1';
1418        } elseif ( $value === false ) {
1419            return '0';
1420        } elseif ( $value === null ) {
1421            return '';
1422        } elseif ( $value instanceof Language ) {
1423            return $value->getCode();
1424        } elseif ( is_array( $value ) ) {
1425            return '[' . implode( ',', array_map( [ $this, 'optionToString' ], $value ) ) . ']';
1426        } else {
1427            return (string)$value;
1428        }
1429    }
1430
1431    /**
1432     * Generate a hash string with the values set on these ParserOptions
1433     * for the keys given in the array.
1434     * This will be used as part of the hash key for the parser cache,
1435     * so users sharing the options with vary for the same page share
1436     * the same cached data safely.
1437     *
1438     * @since 1.17
1439     * @param string[] $forOptions
1440     * @param Title|null $title Used to get the content language of the page (since r97636)
1441     * @return string Page rendering hash
1442     */
1443    public function optionsHash( $forOptions, $title = null ) {
1444        $renderHashAppend = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RenderHashAppend );
1445
1446        $inCacheKey = self::allCacheVaryingOptions();
1447
1448        // Resolve any lazy options
1449        $lazyOpts = array_intersect( $forOptions,
1450            $inCacheKey, array_keys( self::getLazyOptions() ) );
1451        foreach ( $lazyOpts as $k ) {
1452            $this->lazyLoadOption( $k );
1453        }
1454
1455        $options = $this->options;
1456        $defaults = self::getDefaults();
1457
1458        // We only include used options with non-canonical values in the key
1459        // so adding a new option doesn't invalidate the entire parser cache.
1460        // The drawback to this is that changing the default value of an option
1461        // requires manual invalidation of existing cache entries, as mentioned
1462        // in the docs on the relevant methods and hooks.
1463        $values = [];
1464        foreach ( array_intersect( $inCacheKey, $forOptions ) as $option ) {
1465            $v = $this->optionToString( $options[$option] );
1466            $d = $this->optionToString( $defaults[$option] );
1467            if ( $v !== $d ) {
1468                $values[] = "$option=$v";
1469            }
1470        }
1471
1472        $confstr = $values ? implode( '!', $values ) : 'canonical';
1473
1474        // add in language specific options, if any
1475        // @todo FIXME: This is just a way of retrieving the url/user preferred variant
1476        $services = MediaWikiServices::getInstance();
1477        $lang = $title ? $title->getPageLanguage() : $services->getContentLanguage();
1478        $converter = $services->getLanguageConverterFactory()->getLanguageConverter( $lang );
1479        $confstr .= $converter->getExtraHashOptions();
1480
1481        $confstr .= $renderHashAppend;
1482
1483        if ( $this->mExtraKey != '' ) {
1484            $confstr .= $this->mExtraKey;
1485        }
1486
1487        $user = $services->getUserFactory()->newFromUserIdentity( $this->getUserIdentity() );
1488        // Give a chance for extensions to modify the hash, if they have
1489        // extra options or other effects on the parser cache.
1490        ( new HookRunner( $services->getHookContainer() ) )->onPageRenderingHash(
1491            $confstr,
1492            $user,
1493            $forOptions
1494        );
1495
1496        // Make it a valid memcached key fragment
1497        $confstr = str_replace( ' ', '_', $confstr );
1498
1499        return $confstr;
1500    }
1501
1502    /**
1503     * Test whether these options are safe to cache
1504     * @param string[]|null $usedOptions the list of options actually used in the parse. Defaults to all options.
1505     * @return bool
1506     * @since 1.30
1507     */
1508    public function isSafeToCache( ?array $usedOptions = null ) {
1509        $defaults = self::getDefaults();
1510        $inCacheKey = self::getCacheVaryingOptionsHash();
1511        $usedOptions ??= array_keys( $this->options );
1512        foreach ( $usedOptions as $option ) {
1513            if ( empty( $inCacheKey[$option] ) && empty( self::$callbacks[$option] ) ) {
1514                $v = $this->optionToString( $this->options[$option] ?? null );
1515                $d = $this->optionToString( $defaults[$option] ?? null );
1516                if ( $v !== $d ) {
1517                    return false;
1518                }
1519            }
1520        }
1521        return true;
1522    }
1523
1524    /**
1525     * Sets a hook to force that a page exists, and sets a current revision callback to return
1526     * a revision with custom content when the current revision of the page is requested.
1527     *
1528     * @since 1.25
1529     * @param Title $title
1530     * @param Content $content
1531     * @param UserIdentity $user The user that the fake revision is attributed to
1532     * @return ScopedCallback to unset the hook
1533     */
1534    public function setupFakeRevision( $title, $content, $user ) {
1535        $oldCallback = $this->setCurrentRevisionRecordCallback(
1536            static function (
1537                $titleToCheck, $parser = null ) use ( $title, $content, $user, &$oldCallback
1538            ) {
1539                if ( $titleToCheck->equals( $title ) ) {
1540                    $revRecord = new MutableRevisionRecord( $title );
1541                    $revRecord->setContent( SlotRecord::MAIN, $content )
1542                        ->setUser( $user )
1543                        ->setTimestamp( MWTimestamp::now( TS_MW ) )
1544                        ->setPageId( $title->getArticleID() )
1545                        ->setParentId( $title->getLatestRevID() );
1546                    return $revRecord;
1547                } else {
1548                    return call_user_func( $oldCallback, $titleToCheck, $parser );
1549                }
1550            }
1551        );
1552
1553        $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1554        $hookScope = $hookContainer->scopedRegister(
1555            'TitleExists',
1556            static function ( $titleToCheck, &$exists ) use ( $title ) {
1557                if ( $titleToCheck->equals( $title ) ) {
1558                    $exists = true;
1559                }
1560            }
1561        );
1562
1563        $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1564        $linkCache->clearBadLink( $title->getPrefixedDBkey() );
1565
1566        return new ScopedCallback( function () use ( $title, $hookScope, $linkCache, $oldCallback ) {
1567            ScopedCallback::consume( $hookScope );
1568            $linkCache->clearLink( $title );
1569            $this->setCurrentRevisionRecordCallback( $oldCallback );
1570        } );
1571    }
1572
1573    /**
1574     * Returns reason for rendering the content. This human-readable, intended for logging and debugging only.
1575     * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1576     * @return string
1577     */
1578    public function getRenderReason(): string {
1579        return $this->renderReason;
1580    }
1581
1582    /**
1583     * Sets reason for rendering the content. This human-readable, intended for logging and debugging only.
1584     * Expected values include "edit", "view", "purge", "LinksUpdate", etc.
1585     * @param string $renderReason
1586     */
1587    public function setRenderReason( string $renderReason ): void {
1588        $this->renderReason = $renderReason;
1589    }
1590}
1591
1592/** @deprecated class alias since 1.43 */
1593class_alias( ParserOptions::class, 'ParserOptions' );
1594
1595/**
1596 * For really cool vim folding this needs to be at the end:
1597 * vim: foldmarker=@{,@} foldmethod=marker
1598 */