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