Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.15% covered (warning)
88.15%
320 / 363
70.75% covered (warning)
70.75%
75 / 106
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserOptions
88.40% covered (warning)
88.40%
320 / 362
70.75% covered (warning)
70.75%
75 / 106
201.48
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
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 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
85.42% covered (warning)
85.42%
41 / 48
0.00% covered (danger)
0.00%
0 / 1
8.20
 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\Page\PageIdentity;
35use MediaWiki\Page\PageIdentityValue;
36use MediaWiki\Page\PageRecord;
37use MediaWiki\Revision\MutableRevisionRecord;
38use MediaWiki\Revision\SlotRecord;
39use MediaWiki\StubObject\StubObject;
40use MediaWiki\Title\Title;
41use MediaWiki\User\UserIdentity;
42use MediaWiki\User\UserIdentityValue;
43use MediaWiki\Utils\MWTimestamp;
44use ReflectionClass;
45use Wikimedia\IPUtils;
46use Wikimedia\ScopedCallback;
47
48/**
49 * @brief Set options of the Parser
50 *
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.
55 *
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.
60 *
61 * @ingroup Parser
62 */
63class ParserOptions {
64
65    /**
66     * Default values for all options that are relevant for caching.
67     * @see self::getDefaults()
68     * @var array|null
69     */
70    private static $defaults = null;
71
72    /**
73     * Lazy-loaded options
74     * @var callable[]|null
75     */
76    private static $lazyOptions = null;
77
78    /**
79     * Initial lazy-loaded options (before hook)
80     * @var callable[]
81     */
82    private static $initialLazyOptions = [
83        'dateformat' => [ __CLASS__, 'initDateFormat' ],
84        'speculativeRevId' => [ __CLASS__, 'initSpeculativeRevId' ],
85        'speculativePageId' => [ __CLASS__, 'initSpeculativePageId' ],
86    ];
87
88    /**
89     * Specify options that are included in the cache key
90     * @var array|null
91     */
92    private static $cacheVaryingOptionsHash = null;
93
94    /**
95     * Initial inCacheKey options (before hook)
96     * @var array
97     */
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,
106    ];
107
108    /**
109     * Specify pseudo-options that are actually callbacks.
110     * These must be ignored when checking for cacheability.
111     * @var array
112     */
113    private static $callbacks = [
114        'currentRevisionRecordCallback' => true,
115        'templateCallback' => true,
116        'speculativeRevIdCallback' => true,
117        'speculativePageIdCallback' => true,
118    ];
119
120    /**
121     * Current values for all options that are relevant for caching.
122     * @var array
123     */
124    private $options;
125
126    /**
127     * Timestamp used for {{CURRENTDAY}} etc.
128     * @var string|null
129     * @note Caching based on parse time is handled externally
130     */
131    private $mTimestamp;
132
133    /**
134     * Stored user object
135     * @var UserIdentity
136     * @todo Track this for caching somehow without fragmenting the cache
137     */
138    private $mUser;
139
140    /**
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
144     */
145    private $onAccessCallback = null;
146
147    /**
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
152     */
153    private $redirectTarget = null;
154
155    /**
156     * Appended to the options hash
157     * @var string
158     */
159    private $mExtraKey = '';
160
161    /**
162     * The reason for rendering the content.
163     * @var string
164     */
165    private $renderReason = 'unknown';
166
167    /**
168     * Fetch an option and track that is was accessed
169     * @since 1.30
170     * @param string $name Option name
171     * @return mixed
172     */
173    public function getOption( $name ) {
174        if ( !array_key_exists( $name, $this->options ) ) {
175            throw new InvalidArgumentException( "Unknown parser option $name" );
176        }
177
178        $this->lazyLoadOption( $name );
179        $this->optionUsed( $name );
180        return $this->options[$name];
181    }
182
183    /**
184     * @param string $name Lazy load option without tracking usage
185     */
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 );
190        }
191    }
192
193    /**
194     * Resets lazy loaded options to null in the provided $options array
195     * @param array $options
196     * @return array
197     */
198    private function nullifyLazyOption( array $options ): array {
199        return array_fill_keys( array_keys( self::getLazyOptions() ), null ) + $options;
200    }
201
202    /**
203     * Get lazy-loaded options.
204     *
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.
208     *
209     * @internal Public for testing only
210     *
211     * @return array
212     */
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();
218        }
219        return self::$lazyOptions;
220    }
221
222    /**
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.
225     *
226     * @see self::allCacheVaryingOptions()
227     *
228     * @return array
229     */
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();
235        }
236        return self::$cacheVaryingOptionsHash;
237    }
238
239    /**
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
246     */
247    public function setOption( $name, $value ) {
248        if ( !array_key_exists( $name, $this->options ) ) {
249            throw new InvalidArgumentException( "Unknown parser option $name" );
250        }
251        $old = $this->options[$name];
252        $this->options[$name] = $value;
253        return $old;
254    }
255
256    /**
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
263     */
264    protected function setOptionLegacy( $name, $value ) {
265        if ( !array_key_exists( $name, $this->options ) ) {
266            throw new InvalidArgumentException( "Unknown parser option $name" );
267        }
268        return wfSetVar( $this->options[$name], $value );
269    }
270
271    /**
272     * Whether to extract interlanguage links
273     *
274     * When true, interlanguage links will be returned by
275     * ParserOutput::getLanguageLinks() instead of generating link HTML.
276     *
277     * @return bool
278     */
279    public function getInterwikiMagic() {
280        return $this->getOption( 'interwikiMagic' );
281    }
282
283    /**
284     * Specify whether to extract interlanguage links
285     * @param bool|null $x New value (null is no change)
286     * @return bool Old value
287     */
288    public function setInterwikiMagic( $x ) {
289        return $this->setOptionLegacy( 'interwikiMagic', $x );
290    }
291
292    /**
293     * Allow all external images inline?
294     * @return bool
295     */
296    public function getAllowExternalImages() {
297        return $this->getOption( 'allowExternalImages' );
298    }
299
300    /**
301     * External images to allow
302     *
303     * When self::getAllowExternalImages() is false
304     *
305     * @return string|string[] URLs to allow
306     */
307    public function getAllowExternalImagesFrom() {
308        return $this->getOption( 'allowExternalImagesFrom' );
309    }
310
311    /**
312     * Use the on-wiki external image whitelist?
313     * @return bool
314     */
315    public function getEnableImageWhitelist() {
316        return $this->getOption( 'enableImageWhitelist' );
317    }
318
319    /**
320     * Allow inclusion of special pages?
321     * @return bool
322     */
323    public function getAllowSpecialInclusion() {
324        return $this->getOption( 'allowSpecialInclusion' );
325    }
326
327    /**
328     * Allow inclusion of special pages?
329     * @param bool|null $x New value (null is no change)
330     * @return bool Old value
331     */
332    public function setAllowSpecialInclusion( $x ) {
333        return $this->setOptionLegacy( 'allowSpecialInclusion', $x );
334    }
335
336    /**
337     * Parsing an interface message?
338     * @return bool
339     */
340    public function getInterfaceMessage() {
341        return $this->getOption( 'interfaceMessage' );
342    }
343
344    /**
345     * Parsing an interface message?
346     * @param bool|null $x New value (null is no change)
347     * @return bool Old value
348     */
349    public function setInterfaceMessage( $x ) {
350        return $this->setOptionLegacy( 'interfaceMessage', $x );
351    }
352
353    /**
354     * Target language for the parse
355     * @return Language|null
356     */
357    public function getTargetLanguage() {
358        return $this->getOption( 'targetLanguage' );
359    }
360
361    /**
362     * Target language for the parse
363     * @param Language|null $x New value
364     * @return Language|null Old value
365     */
366    public function setTargetLanguage( $x ) {
367        return $this->setOption( 'targetLanguage', $x );
368    }
369
370    /**
371     * Maximum size of template expansions, in bytes
372     * @return int
373     */
374    public function getMaxIncludeSize() {
375        return $this->getOption( 'maxIncludeSize' );
376    }
377
378    /**
379     * Maximum size of template expansions, in bytes
380     * @param int|null $x New value (null is no change)
381     * @return int Old value
382     */
383    public function setMaxIncludeSize( $x ) {
384        return $this->setOptionLegacy( 'maxIncludeSize', $x );
385    }
386
387    /**
388     * Maximum number of nodes touched by PPFrame::expand()
389     * @return int
390     */
391    public function getMaxPPNodeCount() {
392        return $this->getOption( 'maxPPNodeCount' );
393    }
394
395    /**
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
399     */
400    public function setMaxPPNodeCount( $x ) {
401        return $this->setOptionLegacy( 'maxPPNodeCount', $x );
402    }
403
404    /**
405     * Maximum recursion depth in PPFrame::expand()
406     * @return int
407     */
408    public function getMaxPPExpandDepth() {
409        return $this->getOption( 'maxPPExpandDepth' );
410    }
411
412    /**
413     * Maximum recursion depth for templates within templates
414     * @return int
415     * @internal Only used by Parser (T318826)
416     */
417    public function getMaxTemplateDepth() {
418        return $this->getOption( 'maxTemplateDepth' );
419    }
420
421    /**
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)
426     */
427    public function setMaxTemplateDepth( $x ) {
428        return $this->setOptionLegacy( 'maxTemplateDepth', $x );
429    }
430
431    /**
432     * Maximum number of calls per parse to expensive parser functions
433     * @since 1.20
434     * @return int
435     */
436    public function getExpensiveParserFunctionLimit() {
437        return $this->getOption( 'expensiveParserFunctionLimit' );
438    }
439
440    /**
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
445     */
446    public function setExpensiveParserFunctionLimit( $x ) {
447        return $this->setOptionLegacy( 'expensiveParserFunctionLimit', $x );
448    }
449
450    /**
451     * Remove HTML comments
452     * @warning Only applies to preprocess operations
453     * @return bool
454     */
455    public function getRemoveComments() {
456        return $this->getOption( 'removeComments' );
457    }
458
459    /**
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
464     */
465    public function setRemoveComments( $x ) {
466        return $this->setOptionLegacy( 'removeComments', $x );
467    }
468
469    /**
470     * @deprecated since 1.38. This does nothing now, to control limit reporting
471     * please provide 'includeDebugInfo' option to ParserOutput::getText.
472     *
473     * Enable limit report in an HTML comment on output
474     * @return bool
475     */
476    public function getEnableLimitReport() {
477        return false;
478    }
479
480    /**
481     * @deprecated since 1.38. This does nothing now, to control limit reporting
482     * please provide 'includeDebugInfo' option to ParserOutput::getText.
483     *
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
487     */
488    public function enableLimitReport( $x = true ) {
489        return false;
490    }
491
492    /**
493     * Clean up signature texts?
494     * @see Parser::cleanSig
495     * @return bool
496     */
497    public function getCleanSignatures() {
498        return $this->getOption( 'cleanSignatures' );
499    }
500
501    /**
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
506     */
507    public function setCleanSignatures( $x ) {
508        return $this->setOptionLegacy( 'cleanSignatures', $x );
509    }
510
511    /**
512     * Target attribute for external links
513     * @return string|false
514     * @internal Only set by installer (T317647)
515     */
516    public function getExternalLinkTarget() {
517        return $this->getOption( 'externalLinkTarget' );
518    }
519
520    /**
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)
525     */
526    public function setExternalLinkTarget( $x ) {
527        return $this->setOptionLegacy( 'externalLinkTarget', $x );
528    }
529
530    /**
531     * Whether content conversion should be disabled
532     * @return bool
533     */
534    public function getDisableContentConversion() {
535        return $this->getOption( 'disableContentConversion' );
536    }
537
538    /**
539     * Whether content conversion should be disabled
540     * @param bool|null $x New value (null is no change)
541     * @return bool Old value
542     */
543    public function disableContentConversion( $x = true ) {
544        return $this->setOptionLegacy( 'disableContentConversion', $x );
545    }
546
547    /**
548     * Whether title conversion should be disabled
549     * @return bool
550     */
551    public function getDisableTitleConversion() {
552        return $this->getOption( 'disableTitleConversion' );
553    }
554
555    /**
556     * Whether title conversion should be disabled
557     * @param bool|null $x New value (null is no change)
558     * @return bool Old value
559     */
560    public function disableTitleConversion( $x = true ) {
561        return $this->setOptionLegacy( 'disableTitleConversion', $x );
562    }
563
564    /**
565     * Thumb size preferred by the user.
566     * @return int
567     */
568    public function getThumbSize() {
569        return $this->getOption( 'thumbsize' );
570    }
571
572    /**
573     * Thumb size preferred by the user.
574     * @param int|null $x New value (null is no change)
575     * @return int Old value
576     */
577    public function setThumbSize( $x ) {
578        return $this->setOptionLegacy( 'thumbsize', $x );
579    }
580
581    /**
582     * Parsing the page for a "preview" operation?
583     * @return bool
584     */
585    public function getIsPreview() {
586        return $this->getOption( 'isPreview' );
587    }
588
589    /**
590     * Parsing the page for a "preview" operation?
591     * @param bool|null $x New value (null is no change)
592     * @return bool Old value
593     */
594    public function setIsPreview( $x ) {
595        return $this->setOptionLegacy( 'isPreview', $x );
596    }
597
598    /**
599     * Parsing the page for a "preview" operation on a single section?
600     * @return bool
601     */
602    public function getIsSectionPreview() {
603        return $this->getOption( 'isSectionPreview' );
604    }
605
606    /**
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
610     */
611    public function setIsSectionPreview( $x ) {
612        return $this->setOptionLegacy( 'isSectionPreview', $x );
613    }
614
615    /**
616     * Parsing the printable version of the page?
617     * @return bool
618     */
619    public function getIsPrintable() {
620        return $this->getOption( 'printable' );
621    }
622
623    /**
624     * Parsing the printable version of the page?
625     * @param bool|null $x New value (null is no change)
626     * @return bool Old value
627     */
628    public function setIsPrintable( $x ) {
629        return $this->setOptionLegacy( 'printable', $x );
630    }
631
632    /**
633     * Transform wiki markup when saving the page?
634     * @return bool
635     */
636    public function getPreSaveTransform() {
637        return $this->getOption( 'preSaveTransform' );
638    }
639
640    /**
641     * Transform wiki markup when saving the page?
642     * @param bool|null $x New value (null is no change)
643     * @return bool Old value
644     */
645    public function setPreSaveTransform( $x ) {
646        return $this->setOptionLegacy( 'preSaveTransform', $x );
647    }
648
649    /**
650     * Parsoid-format HTML output, or legacy wikitext parser HTML?
651     * @see T300191
652     * @unstable
653     * @since 1.41
654     * @return bool
655     */
656    public function getUseParsoid(): bool {
657        return $this->getOption( 'useParsoid' );
658    }
659
660    /**
661     * Request Parsoid-format HTML output.
662     * @see T300191
663     * @unstable
664     * @since 1.41
665     */
666    public function setUseParsoid() {
667        $this->setOption( 'useParsoid', true );
668    }
669
670    /**
671     * Date format index
672     * @return string
673     */
674    public function getDateFormat() {
675        return $this->getOption( 'dateformat' );
676    }
677
678    /**
679     * Lazy initializer for dateFormat
680     * @param ParserOptions $popt
681     * @return string
682     */
683    private static function initDateFormat( ParserOptions $popt ) {
684        $userFactory = MediaWikiServices::getInstance()->getUserFactory();
685        return $userFactory->newFromUserIdentity( $popt->getUserIdentity() )->getDatePreference();
686    }
687
688    /**
689     * Date format index
690     * @param string|null $x New value (null is no change)
691     * @return string Old value
692     */
693    public function setDateFormat( $x ) {
694        return $this->setOptionLegacy( 'dateformat', $x );
695    }
696
697    /**
698     * Get the user language used by the parser for this page and split the parser cache.
699     *
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!
703     *
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.
707     *
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).
710     *
711     * @return Language
712     * @since 1.19
713     */
714    public function getUserLangObj() {
715        return $this->getOption( 'userlang' );
716    }
717
718    /**
719     * Same as getUserLangObj() but returns a string instead.
720     *
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!
724     *
725     * @see getUserLangObj()
726     *
727     * @return string Language code
728     * @since 1.17
729     */
730    public function getUserLang() {
731        return $this->getUserLangObj()->getCode();
732    }
733
734    /**
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
738     */
739    public function setUserLang( $x ) {
740        if ( is_string( $x ) ) {
741            $x = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $x );
742        }
743
744        return $this->setOptionLegacy( 'userlang', $x );
745    }
746
747    /**
748     * Are magic ISBN links enabled?
749     * @since 1.28
750     * @return bool
751     */
752    public function getMagicISBNLinks() {
753        return $this->getOption( 'magicISBNLinks' );
754    }
755
756    /**
757     * Are magic PMID links enabled?
758     * @since 1.28
759     * @return bool
760     */
761    public function getMagicPMIDLinks() {
762        return $this->getOption( 'magicPMIDLinks' );
763    }
764
765    /**
766     * Are magic RFC links enabled?
767     * @since 1.28
768     * @return bool
769     */
770    public function getMagicRFCLinks() {
771        return $this->getOption( 'magicRFCLinks' );
772    }
773
774    /**
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
782     */
783    public function getSuppressTOC() {
784        return $this->getOption( 'suppressTOC' );
785    }
786
787    /**
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
796     */
797    public function setSuppressTOC() {
798        wfDeprecated( __METHOD__, '1.42' );
799        $this->setOption( 'suppressTOC', true );
800    }
801
802    /**
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
808     */
809    public function getSuppressSectionEditLinks() {
810        return $this->getOption( 'suppressSectionEditLinks' );
811    }
812
813    /**
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
818     */
819    public function setSuppressSectionEditLinks() {
820        $this->setOption( 'suppressSectionEditLinks', true );
821    }
822
823    /**
824     * Should section contents be wrapped in <div> to make them
825     * collapsible?
826     * @since 1.42
827     */
828    public function getCollapsibleSections(): bool {
829        return $this->getOption( 'collapsibleSections' );
830    }
831
832    /**
833     * Wrap section contents in a <div> to allow client-side code
834     * to collapse them.
835     * @since 1.42
836     */
837    public function setCollapsibleSections(): void {
838        $this->setOption( 'collapsibleSections', true );
839    }
840
841    /**
842     * If the wiki is configured to allow raw html ($wgRawHtml = true)
843     * is it allowed in the specific case of parsing this page.
844     *
845     * This is meant to disable unsafe parser tags in cases where
846     * a malicious user may control the input to the parser.
847     *
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
854     */
855    public function getAllowUnsafeRawHtml() {
856        return $this->getOption( 'allowUnsafeRawHtml' );
857    }
858
859    /**
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
866     */
867    public function setAllowUnsafeRawHtml( $x ) {
868        return $this->setOptionLegacy( 'allowUnsafeRawHtml', $x );
869    }
870
871    /**
872     * Class to use to wrap output from Parser::parse()
873     * @since 1.30
874     * @return string|false
875     */
876    public function getWrapOutputClass() {
877        return $this->getOption( 'wrapclass' );
878    }
879
880    /**
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
886     */
887    public function setWrapOutputClass( $className ) {
888        if ( $className === true ) { // DWIM, they probably want the default class name
889            $className = 'mw-parser-output';
890        }
891        if ( $className === false ) {
892            wfDeprecated( __METHOD__ . '( false )', '1.31' );
893        }
894        return $this->setOption( 'wrapclass', $className );
895    }
896
897    /**
898     * Callback for current revision fetching; first argument to call_user_func().
899     * @internal
900     * @since 1.35
901     * @return callable
902     */
903    public function getCurrentRevisionRecordCallback() {
904        return $this->getOption( 'currentRevisionRecordCallback' );
905    }
906
907    /**
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
913     */
914    public function setCurrentRevisionRecordCallback( $x ) {
915        return $this->setOption( 'currentRevisionRecordCallback', $x );
916    }
917
918    /**
919     * Callback for template fetching; first argument to call_user_func().
920     * @return callable
921     */
922    public function getTemplateCallback() {
923        return $this->getOption( 'templateCallback' );
924    }
925
926    /**
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
930     */
931    public function setTemplateCallback( $x ) {
932        return $this->setOptionLegacy( 'templateCallback', $x );
933    }
934
935    /**
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.
939     *
940     * If no callback was defined via setSpeculativeRevIdCallback(), this method will return false.
941     *
942     * @since 1.32
943     * @return int|false
944     */
945    public function getSpeculativeRevId() {
946        return $this->getOption( 'speculativeRevId' );
947    }
948
949    /**
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.
953     *
954     * If no callback was defined via setSpeculativePageIdCallback(), this method will return false.
955     *
956     * @since 1.34
957     * @return int|false
958     */
959    public function getSpeculativePageId() {
960        return $this->getOption( 'speculativePageId' );
961    }
962
963    /**
964     * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativeRevId().
965     *
966     * @param ParserOptions $popt
967     * @return int|false
968     */
969    private static function initSpeculativeRevId( ParserOptions $popt ) {
970        $cb = $popt->getOption( 'speculativeRevIdCallback' );
971        $id = $cb ? $cb() : null;
972
973        // returning null would result in this being re-called every access
974        return $id ?? false;
975    }
976
977    /**
978     * Callback registered with ParserOptions::$lazyOptions, triggered by getSpeculativePageId().
979     *
980     * @param ParserOptions $popt
981     * @return int|false
982     */
983    private static function initSpeculativePageId( ParserOptions $popt ) {
984        $cb = $popt->getOption( 'speculativePageIdCallback' );
985        $id = $cb ? $cb() : null;
986
987        // returning null would result in this being re-called every access
988        return $id ?? false;
989    }
990
991    /**
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
996     */
997    public function setSpeculativeRevIdCallback( $x ) {
998        $this->setOption( 'speculativeRevId', null ); // reset
999        return $this->setOption( 'speculativeRevIdCallback', $x );
1000    }
1001
1002    /**
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
1007     */
1008    public function setSpeculativePageIdCallback( $x ) {
1009        $this->setOption( 'speculativePageId', null ); // reset
1010        return $this->setOption( 'speculativePageIdCallback', $x );
1011    }
1012
1013    /**
1014     * Timestamp used for {{CURRENTDAY}} etc.
1015     * @return string TS_MW timestamp
1016     */
1017    public function getTimestamp() {
1018        if ( $this->mTimestamp === null ) {
1019            $this->mTimestamp = wfTimestampNow();
1020        }
1021        return $this->mTimestamp;
1022    }
1023
1024    /**
1025     * Timestamp used for {{CURRENTDAY}} etc.
1026     * @param string|null $x New value (null is no change)
1027     * @return string Old value
1028     */
1029    public function setTimestamp( $x ) {
1030        return wfSetVar( $this->mTimestamp, $x );
1031    }
1032
1033    /**
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.
1037     *
1038     * @since 1.24
1039     * @param Title|null $title
1040     */
1041    public function setRedirectTarget( $title ) {
1042        $this->redirectTarget = $title;
1043    }
1044
1045    /**
1046     * Get the previously-set redirect target.
1047     *
1048     * @since 1.24
1049     * @return Title|null
1050     */
1051    public function getRedirectTarget() {
1052        return $this->redirectTarget;
1053    }
1054
1055    /**
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
1060     */
1061    public function addExtraKey( $key ) {
1062        $this->mExtraKey .= '!' . $key;
1063    }
1064
1065    /**
1066     * Get the identity of the user for whom the parse is made.
1067     * @since 1.36
1068     * @return UserIdentity
1069     */
1070    public function getUserIdentity(): UserIdentity {
1071        return $this->mUser;
1072    }
1073
1074    /**
1075     * @param UserIdentity $user
1076     * @param Language|null $lang
1077     */
1078    public function __construct( UserIdentity $user, $lang = null ) {
1079        if ( $lang === null ) {
1080            global $wgLang;
1081            StubObject::unstub( $wgLang );
1082            $lang = $wgLang;
1083        }
1084        $this->initialiseFromUser( $user, $lang );
1085    }
1086
1087    /**
1088     * Get a ParserOptions object for an anonymous user
1089     * @since 1.27
1090     * @return ParserOptions
1091     */
1092    public static function newFromAnon() {
1093        return new ParserOptions( MediaWikiServices::getInstance()->getUserFactory()->newAnonymous(),
1094            MediaWikiServices::getInstance()->getContentLanguage() );
1095    }
1096
1097    /**
1098     * Get a ParserOptions object from a given user.
1099     * Language will be taken from $wgLang.
1100     *
1101     * @param UserIdentity $user
1102     * @return ParserOptions
1103     */
1104    public static function newFromUser( $user ) {
1105        return new ParserOptions( $user );
1106    }
1107
1108    /**
1109     * Get a ParserOptions object from a given user and language
1110     *
1111     * @param UserIdentity $user
1112     * @param Language $lang
1113     * @return ParserOptions
1114     */
1115    public static function newFromUserAndLang( UserIdentity $user, Language $lang ) {
1116        return new ParserOptions( $user, $lang );
1117    }
1118
1119    /**
1120     * Get a ParserOptions object from a IContextSource object
1121     *
1122     * @param IContextSource $context
1123     * @return ParserOptions
1124     */
1125    public static function newFromContext( IContextSource $context ) {
1126        $contextUser = $context->getUser();
1127
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 );
1139            }
1140        }
1141
1142        return new ParserOptions( $contextUser, $context->getLanguage() );
1143    }
1144
1145    /**
1146     * Creates a "canonical" ParserOptions object
1147     *
1148     * For historical reasons, certain options have default values that are
1149     * different from the canonical values used for caching.
1150     *
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
1164     */
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'
1175            );
1176        }
1177        return $ret;
1178    }
1179
1180    /**
1181     * Reset static caches
1182     * @internal For testing
1183     */
1184    public static function clearStaticCache() {
1185        if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
1186            throw new LogicException( __METHOD__ . ' is just for testing' );
1187        }
1188        self::$defaults = null;
1189        self::$lazyOptions = null;
1190        self::$cacheVaryingOptionsHash = null;
1191    }
1192
1193    /**
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
1200     */
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();
1220
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,
1245            ];
1246
1247            self::$cacheVaryingOptionsHash = self::$initialCacheVaryingOptionsHash;
1248            self::$lazyOptions = self::$initialLazyOptions;
1249
1250            ( new HookRunner( $services->getHookContainer() ) )->onParserOptionsRegister(
1251                self::$defaults,
1252                self::$cacheVaryingOptionsHash,
1253                self::$lazyOptions
1254            );
1255
1256            ksort( self::$cacheVaryingOptionsHash );
1257        }
1258
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,
1287        ];
1288    }
1289
1290    /**
1291     * Get user options
1292     *
1293     * @param UserIdentity $user
1294     * @param Language $lang
1295     */
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() );
1301
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;
1307    }
1308
1309    /**
1310     * Check if these options match that of another options set
1311     *
1312     * This ignores report limit settings that only affect HTML comments
1313     *
1314     * @param ParserOptions $other
1315     * @return bool
1316     * @since 1.25
1317     */
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 );
1329
1330            $o1 = $this->optionToString( $this->options[$option] );
1331            $o2 = $this->optionToString( $other->options[$option] );
1332            if ( $o1 !== $o2 ) {
1333                return false;
1334            }
1335        }
1336
1337        // Compare most other fields
1338        foreach ( ( new ReflectionClass( $this ) )->getProperties() as $property ) {
1339            $field = $property->getName();
1340            if ( $property->isStatic() ) {
1341                continue;
1342            }
1343            if ( in_array( $field, [
1344                'options', // Already checked above
1345                'onAccessCallback', // only used for ParserOutput option tracking
1346            ] ) ) {
1347                continue;
1348            }
1349
1350            if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) {
1351                return false;
1352            }
1353        }
1354
1355        return true;
1356    }
1357
1358    /**
1359     * @param ParserOptions $other
1360     * @return bool Whether the cache key relevant options match those of $other
1361     * @since 1.33
1362     */
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 );
1368
1369            $o1 = $this->optionToString( $this->options[$option] );
1370            $o2 = $this->optionToString( $other->options[$option] );
1371            if ( $o1 !== $o2 ) {
1372                return false;
1373            }
1374        }
1375
1376        return true;
1377    }
1378
1379    /**
1380     * Registers a callback for tracking which ParserOptions which are used.
1381     *
1382     * @since 1.16
1383     * @param callable|null $callback
1384     */
1385    public function registerWatcher( $callback ) {
1386        $this->onAccessCallback = $callback;
1387    }
1388
1389    /**
1390     * Record that an option was internally accessed.
1391     *
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().
1395     *
1396     * @param string $optionName Name of the option
1397     */
1398    private function optionUsed( $optionName ) {
1399        if ( $this->onAccessCallback ) {
1400            call_user_func( $this->onAccessCallback, $optionName );
1401        }
1402    }
1403
1404    /**
1405     * Return all option keys that vary the options hash
1406     * @since 1.30
1407     * @return string[]
1408     */
1409    public static function allCacheVaryingOptions() {
1410        return array_keys( array_filter( self::getCacheVaryingOptionsHash() ) );
1411    }
1412
1413    /**
1414     * Convert an option to a string value
1415     * @param mixed $value
1416     * @return string
1417     */
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;
1431        }
1432    }
1433
1434    /**
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.
1440     *
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
1445     */
1446    public function optionsHash( $forOptions, $title = null ) {
1447        $renderHashAppend = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::RenderHashAppend );
1448
1449        $inCacheKey = self::allCacheVaryingOptions();
1450
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 );
1456        }
1457
1458        $options = $this->options;
1459        $defaults = self::getDefaults();
1460
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";
1472            }
1473        }
1474
1475        $confstr = $values ? implode( '!', $values ) : 'canonical';
1476
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();
1483
1484        $confstr .= $renderHashAppend;
1485
1486        if ( $this->mExtraKey != '' ) {
1487            $confstr .= $this->mExtraKey;
1488        }
1489
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
1497        );
1498
1499        // Make it a valid memcached key fragment
1500        $confstr = str_replace( ' ', '_', $confstr );
1501
1502        return $confstr;
1503    }
1504
1505    /**
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
1510     */
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;
1521                }
1522            }
1523        }
1524        return true;
1525    }
1526
1527    /**
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.
1530     *
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
1535     *
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.
1540     *
1541     * @since 1.25
1542     */
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 )
1547            {
1548                if ( $titleToCheck->isSamePageAs( $page ) ) {
1549                    if ( $page->exists() ) {
1550                        $pageId = $page->getId();
1551
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;
1560                        }
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()
1569                        );
1570                    }
1571
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 );
1582                }
1583            }
1584        );
1585
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;
1592                }
1593            }
1594        );
1595
1596        $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1597        $linkCache->clearBadLink( $page );
1598
1599        return new ScopedCallback( function () use ( $page, $hookScope, $linkCache, $oldCallback ) {
1600            ScopedCallback::consume( $hookScope );
1601            $linkCache->clearLink( $page );
1602            $this->setCurrentRevisionRecordCallback( $oldCallback );
1603        } );
1604    }
1605
1606    /**
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
1610     */
1611    public function getRenderReason(): string {
1612        return $this->renderReason;
1613    }
1614
1615    /**
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
1619     */
1620    public function setRenderReason( string $renderReason ): void {
1621        $this->renderReason = $renderReason;
1622    }
1623}
1624
1625/** @deprecated class alias since 1.43 */
1626class_alias( ParserOptions::class, 'ParserOptions' );
1627
1628/**
1629 * For really cool vim folding this needs to be at the end:
1630 * vim: foldmarker=@{,@} foldmethod=marker
1631 */