Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.00% covered (warning)
80.00%
336 / 420
55.07% covered (warning)
55.07%
38 / 69
CRAP
0.00% covered (danger)
0.00%
0 / 1
Message
80.19% covered (warning)
80.19%
336 / 419
55.07% covered (warning)
55.07%
38 / 69
531.66
0.00% covered (danger)
0.00%
0 / 1
 __construct
75.00% covered (warning)
75.00%
15 / 20
0.00% covered (danger)
0.00%
0 / 1
9.00
 serialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __serialize
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 unserialize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unserialize
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
7.02
 isMultiKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getKeysToTry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getLanguageCode
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getFetchedLanguage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 newFromKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromSpecifier
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
9.10
 newFallbackSequence
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 getTitle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 params
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 rawParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 numParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 durationParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 expiryParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 dateTimeParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 dateParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 userGroupParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 timeParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 timeperiodParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 sizeParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 bitrateParams
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 plaintextParams
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setContext
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 inLanguage
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
10.02
 inUserLanguage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 inContentLanguage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setInterfaceMessageFlag
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 useDatabase
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 title
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 page
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 format
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
14
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 text
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 plain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseAsBlock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 escaped
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBlank
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isDisabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 rawParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 numParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 durationParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expiryParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dateTimeParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dateParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 timeParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 userGroupParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 timeperiodParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sizeParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 bitrateParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 plaintextParam
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 listParam
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 replaceParameters
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 extractParam
83.33% covered (warning)
83.33%
45 / 54
0.00% covered (danger)
0.00%
0 / 1
8.30
 parseText
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 transformText
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 fetchMessage
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
9.02
 formatPlaintext
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
7
 listTypeMap
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 formatListParam
64.29% covered (warning)
64.29%
18 / 28
0.00% covered (danger)
0.00%
0 / 1
10.92
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @author Niklas Laxström
6 */
7
8namespace MediaWiki\Message;
9
10use InvalidArgumentException;
11use MediaWiki\Content\Content;
12use MediaWiki\Context\IContextSource;
13use MediaWiki\Context\RequestContext;
14use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
15use MediaWiki\Language\Language;
16use MediaWiki\Language\MessageInfo;
17use MediaWiki\Language\RawMessage;
18use MediaWiki\Logger\LoggerFactory;
19use MediaWiki\MainConfigNames;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Page\PageReference;
22use MediaWiki\Page\PageReferenceValue;
23use MediaWiki\Parser\Parser;
24use MediaWiki\Parser\ParserOutput;
25use MediaWiki\Parser\Sanitizer;
26use MediaWiki\StubObject\StubUserLang;
27use MediaWiki\Title\Title;
28use RuntimeException;
29use Serializable;
30use Stringable;
31use Wikimedia\Assert\Assert;
32use Wikimedia\Bcp47Code\Bcp47Code;
33use Wikimedia\Message\ListParam;
34use Wikimedia\Message\ListType;
35use Wikimedia\Message\MessageParam;
36use Wikimedia\Message\MessageSpecifier;
37use Wikimedia\Message\ParamType;
38use Wikimedia\Message\ScalarParam;
39
40/**
41 * The Message class deals with fetching and processing of interface message
42 * into a variety of formats.
43 *
44 * First implemented with MediaWiki 1.17, the Message class is intended to
45 * replace the old wfMsg* functions that over time grew unusable.
46 * @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences
47 * between old and new functions.
48 *
49 * The preferred way to create Message objects is via the msg() method of
50 * of an available RequestContext and ResourceLoader Context object; this will
51 * ensure that the message uses the correct language. When that is not
52 * possible, the wfMessage() global function can be used, which will cause
53 * Message to get the language from the global RequestContext object. In
54 * rare circumstances when sessions are not available or not initialized,
55 * that can lead to errors.
56 *
57 * The most basic usage cases would be:
58 *
59 * @code
60 *     // Initialize a Message object using the 'some_key' message key
61 *     $message = $context->msg( 'some_key' );
62 *
63 *     // Using two parameters those values are strings 'value1' and 'value2':
64 *     $message = $context->msg( 'some_key',
65 *          'value1', 'value2'
66 *     );
67 * @endcode
68 *
69 * @section message_global_fn Global function wrapper:
70 *
71 * Since msg() returns a Message instance, you can chain its call with a method.
72 * Some of them return a Message instance too so you can chain them.
73 * You will find below several examples of msg() usage.
74 *
75 * Fetching a message text for interface message:
76 *
77 * @code
78 *    $button = Html::submitButton(
79 *         $context->msg( 'submit' )->text()
80 *    );
81 * @endcode
82 *
83 * A Message instance can be passed parameters after it has been constructed,
84 * use the params() method to do so:
85 *
86 * @code
87 *     $context->msg( 'welcome-to' )
88 *         ->params( $wgSitename )
89 *         ->text();
90 * @endcode
91 *
92 * {{GRAMMAR}} and friends work correctly:
93 *
94 * @code
95 *    $context->msg( 'are-friends',
96 *        $user, $friend
97 *    );
98 *    $context->msg( 'bad-message' )
99 *         ->rawParams( '<script>...</script>' )
100 *         ->escaped();
101 * @endcode
102 *
103 * @section message_language Changing language:
104 *
105 * Messages can be requested in a different language or in whatever current
106 * content language is being used. The methods are:
107 *     - Message->inContentLanguage()
108 *     - Message->inLanguage()
109 *
110 * Sometimes the message text ends up in the database, so content language is
111 * needed:
112 *
113 * @code
114 *    wfMessage( 'file-log',
115 *        $user, $filename
116 *    )->inContentLanguage()->text();
117 * @endcode
118 *
119 * Checking whether a message exists:
120 *
121 * @code
122 *    $context->msg( 'mysterious-message' )->exists()
123 *    // returns a boolean whether the 'mysterious-message' key exist.
124 * @endcode
125 *
126 * If you want to use a different language:
127 *
128 * @code
129 *    $userLanguage = $user->getOption( 'language' );
130 *    wfMessage( 'email-header' )
131 *         ->inLanguage( $userLanguage )
132 *         ->plain();
133 * @endcode
134 *
135 * @note You can parse the text only in the content or interface languages
136 *
137 * @see https://www.mediawiki.org/wiki/Manual:Messages_API
138 * @see https://www.mediawiki.org/wiki/Localisation
139 *
140 * @since 1.17
141 * @newable
142 * @ingroup Language
143 */
144class Message implements Stringable, MessageSpecifier, Serializable {
145    use ProtectedHookAccessorTrait;
146
147    /** Use message text as-is */
148    public const FORMAT_PLAIN = 'plain';
149    /** Use normal wikitext -> HTML parsing (the result will be wrapped in a block-level HTML tag) */
150    public const FORMAT_BLOCK_PARSE = 'block-parse';
151    /** Use normal wikitext -> HTML parsing but strip the block-level wrapper */
152    public const FORMAT_PARSE = 'parse';
153    /** Transform {{..}} constructs but don't transform to HTML */
154    public const FORMAT_TEXT = 'text';
155    /** Transform {{..}} constructs, HTML-escape the result */
156    public const FORMAT_ESCAPED = 'escaped';
157
158    /**
159     * In which language to get this message. True, which is the default,
160     * means the current user language, false content language.
161     *
162     * @var bool
163     */
164    protected $isInterface = true;
165
166    /**
167     * In which language to get this message. Overrides the $interface setting.
168     *
169     * @var Language|string|null Explicit language object, a MediaWiki internal language code as a string,
170     *   or null for user language
171     */
172    protected $language = null;
173
174    /**
175     * @var callable|null A callable which returns the current user language,
176     *   or null to get it from global state.
177     */
178    protected $userLangCallback;
179
180    /**
181     * If the message was fetched via a fallback sequence from a language other
182     * than the requested one, this will be the final language code.
183     *
184     * @var string|null
185     */
186    protected $fetchedLangCode;
187
188    /**
189     * @var string The message key. If $keysToTry has more than one element,
190     * this may change to one of the keys to try when fetching the message text.
191     */
192    protected $key;
193
194    /**
195     * @var string[] List of keys to try when fetching the message.
196     * @phan-var non-empty-list<string>
197     */
198    protected $keysToTry;
199
200    /**
201     * @var ?string The message key that the message was fetched from, if different from
202     *   all of the requested $keysToTry (the requested key may be overridden by hooks).
203     */
204    protected $overriddenKey = null;
205
206    /**
207     * @var (MessageParam|Message|string|int|float)[] List of parameters which will be substituted
208     *   into the message.
209     */
210    protected $parameters = [];
211
212    /**
213     * @var bool If messages in the local MediaWiki namespace should be loaded; false to use only
214     *  the compiled LocalisationCache
215     */
216    protected $useDatabase = true;
217
218    /**
219     * @var ?PageReference page object to use as context.
220     */
221    protected $contextPage = null;
222
223    /**
224     * @var Content|null Content object representing the message.
225     */
226    protected $content = null;
227
228    /**
229     * @var string|null|false
230     */
231    protected $message;
232
233    /**
234     * @stable to call
235     * @since 1.17
236     * @param string|MessageSpecifier|string[] $key Message key, MessageSpecifier object to copy,
237     * or array of fallback message keys where we use the first message that exists and is non-empty.
238     * @param array $params Message parameters
239     * @param Language|null $language [optional] Language to use (defaults to current user language).
240     */
241    public function __construct( $key, $params = [], ?Language $language = null ) {
242        if ( $key instanceof MessageSpecifier ) {
243            if ( $params ) {
244                throw new InvalidArgumentException(
245                    'Cannot set $params when $key is a MessageSpecifier'
246                );
247            }
248            $params = $key->getParams();
249            $key = $key->getKey();
250        }
251
252        if ( is_string( $key ) ) {
253            $this->keysToTry = [ $key ];
254            $this->key = $key;
255        } elseif ( is_array( $key ) && $key ) {
256            $this->keysToTry = $key;
257            foreach ( $this->keysToTry as $key ) {
258                if ( !is_string( $key ) ) {
259                    throw new InvalidArgumentException( 'Message keys must be strings. ' .
260                        'Did you accidentally pass message key and parameters in one array?' );
261                }
262            }
263            $this->key = reset( $this->keysToTry );
264        } else {
265            throw new InvalidArgumentException( '$key must be a string or non-empty array' );
266        }
267
268        $this->params( ...$params );
269        // User language is only resolved in getLanguage(). This helps preserve the
270        // semantic intent of "user language" across serialize() and unserialize().
271        $this->language = $language;
272    }
273
274    /**
275     * @see Serializable::serialize()
276     * @since 1.26
277     * @return string
278     */
279    public function serialize(): string {
280        return serialize( $this->__serialize() );
281    }
282
283    /**
284     * @see Serializable::serialize()
285     * @since 1.38
286     * @return array
287     */
288    public function __serialize() {
289        return [
290            'interface' => $this->isInterface,
291            'language' => $this->language instanceof Language ? $this->language->getCode() : $this->language,
292            'key' => $this->key,
293            'keysToTry' => $this->keysToTry,
294            'parameters' => $this->parameters,
295            'useDatabase' => $this->useDatabase,
296            // Optimisation: Avoid cost of TitleFormatter on serialize,
297            // and especially cost of TitleParser on retrieval.
298            'titlevalue' => $this->contextPage
299                ? [ $this->contextPage->getNamespace(), $this->contextPage->getDBkey() ]
300                : null,
301        ];
302    }
303
304    /**
305     * @see Serializable::unserialize()
306     * @since 1.38
307     * @param string $serialized
308     */
309    public function unserialize( $serialized ): void {
310        $this->__unserialize( unserialize( $serialized ) );
311    }
312
313    /**
314     * @see Serializable::unserialize()
315     * @since 1.26
316     * @param array $data
317     */
318    public function __unserialize( $data ) {
319        if ( !is_array( $data ) ) {
320            throw new InvalidArgumentException( __METHOD__ . ': Invalid serialized data' );
321        }
322        $this->isInterface = $data['interface'];
323        $this->key = $data['key'];
324        $this->keysToTry = $data['keysToTry'];
325        // Accept old serialization format for compatibility with pre-MessageParam stored values
326        $this->parameters = array_map( static function ( $param ) {
327            if ( is_array( $param ) ) {
328                if ( isset( $param['type'] ) ) {
329                    return ListParam::newFromJsonArray( $param );
330                } else {
331                    return ScalarParam::newFromJsonArray( $param );
332                }
333            } else {
334                return $param;
335            }
336        }, $data['parameters'] );
337        $this->useDatabase = $data['useDatabase'];
338        // Until MW 1.39, we used 'false' instead of 'null' to indicate the user language (Idbe21afcea)
339        $this->language = $data['language'] ?: null;
340
341        // Since 1.35, the key 'titlevalue' is set, instead of 'titlestr'.
342        if ( isset( $data['titlevalue'] ) ) {
343            $this->contextPage = PageReferenceValue::localReference(
344                $data['titlevalue'][0],
345                $data['titlevalue'][1]
346            );
347        } elseif ( isset( $data['titlestr'] ) ) {
348            $titleParser = MediaWikiServices::getInstance()->getTitleParser();
349            $title = $titleParser->parseTitle( $data['titlestr'] );
350            // The title should not have any fragment or interwiki parts
351            $this->contextPage = PageReferenceValue::localReference(
352                $title->getNamespace(),
353                $title->getDBkey()
354            );
355        } else {
356            $this->contextPage = null;
357        }
358    }
359
360    /**
361     * @since 1.24
362     *
363     * @return bool True if this is a multi-key message, that is, if the key provided to the
364     * constructor was a fallback list of keys to try.
365     */
366    public function isMultiKey() {
367        return count( $this->keysToTry ) > 1;
368    }
369
370    /**
371     * @since 1.24
372     *
373     * @return string[] The list of keys to try when fetching the message text,
374     * in order of preference.
375     */
376    public function getKeysToTry() {
377        return $this->keysToTry;
378    }
379
380    /**
381     * Returns the message key.
382     *
383     * If a list of multiple possible keys was supplied to the constructor, this method may
384     * return any of these keys. After the message has been fetched, this method will return
385     * the key that was actually used to fetch the message.
386     *
387     * @since 1.21
388     *
389     * @return string
390     */
391    public function getKey(): string {
392        return $this->key;
393    }
394
395    /**
396     * Returns the message parameters.
397     *
398     * @since 1.21
399     *
400     * @return (MessageParam|Message|string|int|float)[]
401     */
402    public function getParams(): array {
403        return $this->parameters;
404    }
405
406    /**
407     * Returns the Language of the Message.
408     *
409     * @since 1.23
410     *
411     * @return Language
412     */
413    public function getLanguage(): Language {
414        // Defaults to null which means current user language
415        if ( $this->language !== null ) {
416            return MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $this->language );
417        } elseif ( $this->userLangCallback ) {
418            return ( $this->userLangCallback )();
419        } else {
420            return RequestContext::getMain()->getLanguage();
421        }
422    }
423
424    /**
425     * Get the language code of the Message
426     * @return string
427     * @since 1.44
428     */
429    public function getLanguageCode(): string {
430        if ( $this->language instanceof Language ) {
431            return $this->language->getCode();
432        } elseif ( is_string( $this->language ) ) {
433            return $this->language;
434        } elseif ( $this->userLangCallback ) {
435            return ( $this->userLangCallback )()->getCode();
436        } else {
437            return RequestContext::getMain()->getLanguage()->getCode();
438        }
439    }
440
441    /**
442     * Get the language in which the message was fetched, or the requested
443     * language if it is not available. This allows us to transform messages
444     * in the localisation of the source. (T268492)
445     * @return Language
446     */
447    protected function getFetchedLanguage(): Language {
448        if ( $this->fetchedLangCode ) {
449            return MediaWikiServices::getInstance()->getLanguageFactory()
450                ->getLanguage( $this->fetchedLangCode );
451        } else {
452            return $this->getLanguage();
453        }
454    }
455
456    /**
457     * Factory function that is just wrapper for the real constructor. It is
458     * intended to be used instead of the real constructor, because it allows
459     * chaining method calls, while new objects don't.
460     *
461     * @since 1.17
462     *
463     * @param string|string[]|MessageSpecifier $key
464     * @phpcs:ignore Generic.Files.LineLength
465     * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
466     *   See Message::params()
467     *
468     * @return self
469     */
470    public static function newFromKey( $key, ...$params ) {
471        return new self( $key, $params );
472    }
473
474    /**
475     * Transform a MessageSpecifier or a primitive value used interchangeably with
476     * specifiers (a message key string, or a key + params array) into a proper Message.
477     *
478     * Also accepts a MessageSpecifier inside an array: that's not considered a valid format
479     * but is an easy error to make due to how StatusValue stores messages internally.
480     * Providing further array elements in that case causes an exception to be thrown.
481     *
482     * When the MessageSpecifier object is an instance of Message, a clone of the object is returned.
483     * This is unlike the `new Message( â€¦ )` constructor, which returns a new object constructed from
484     * scratch with the same key. This difference is mostly relevant when the passed object is an
485     * instance of a subclass like RawMessage or ApiMessage.
486     *
487     * @param string|array|MessageSpecifier $value
488     * @param-taint $value tainted
489     * @return self
490     * @since 1.27
491     */
492    public static function newFromSpecifier( $value ) {
493        $params = [];
494        if ( is_array( $value ) ) {
495            $params = $value;
496            $value = array_shift( $params );
497        }
498
499        if ( $value instanceof Message ) { // Message, RawMessage, ApiMessage, etc
500            if ( $params ) {
501                throw new InvalidArgumentException(
502                    'Cannot have parameters when the key is already a Message instance'
503                );
504            }
505            $message = clone $value;
506        } elseif ( $value instanceof MessageSpecifier ) {
507            if ( $params ) {
508                throw new InvalidArgumentException(
509                    'Cannot have parameters when the key is already a MessageSpecifier instance'
510                );
511            }
512            $message = new Message( $value );
513        } elseif ( is_string( $value ) ) {
514            $message = new Message( $value, $params );
515        } else {
516            throw new InvalidArgumentException( 'Invalid argument type ' . get_debug_type( $value ) );
517        }
518
519        return $message;
520    }
521
522    /**
523     * Factory function accepting multiple message keys and returning a message instance
524     * for the first message which is non-empty. If all messages are empty then an
525     * instance of the last message key is returned.
526     *
527     * @since 1.18
528     *
529     * @param string|string[] ...$keys Message keys, or first argument as an array of all the
530     * message keys.
531     * @param-taint ...$keys tainted
532     *
533     * @return self
534     */
535    public static function newFallbackSequence( ...$keys ) {
536        if ( func_num_args() == 1 ) {
537            if ( is_array( $keys[0] ) ) {
538                // Allow an array to be passed as the first argument instead
539                $keys = array_values( $keys[0] );
540            } else {
541                // Optimize a single string to not need special fallback handling
542                $keys = $keys[0];
543            }
544        }
545        return new self( $keys );
546    }
547
548    /**
549     * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace.
550     * The title will be for the current language, if the message key is in
551     * $wgForceUIMsgAsContentMsg it will be append with the language code (except content
552     * language), because Message::inContentLanguage will also return in user language.
553     *
554     * @see $wgForceUIMsgAsContentMsg
555     * @return Title
556     * @since 1.26
557     */
558    public function getTitle() {
559        $forceUIMsgAsContentMsg = MediaWikiServices::getInstance()->getMainConfig()->get(
560            MainConfigNames::ForceUIMsgAsContentMsg );
561
562        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
563        $langCode = $this->getLanguageCode();
564        $title = $this->key;
565        if (
566            $langCode !== $contLang->getCode()
567            && in_array( $this->key, (array)$forceUIMsgAsContentMsg )
568        ) {
569            $title .= '/' . $langCode;
570        }
571
572        // Don't use $contLang->ucfirst() here. See T362654
573        return Title::makeTitle( NS_MEDIAWIKI, ucfirst( $title ) );
574    }
575
576    /**
577     * Adds parameters to the parameter list of this message.
578     *
579     * @since 1.17
580     *
581     * @phpcs:ignore Generic.Files.LineLength
582     * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
583     *   Parameters as strings or MessageParam values (from Message::numParam() and the like),
584     *   may also be passed as a single array instead of variadic parameters.
585     *
586     * @return $this
587     */
588    public function params( ...$params ) {
589        if ( count( $params ) === 1 && isset( $params[0] ) && is_array( $params[0] ) ) {
590            $params = $params[0];
591        }
592        foreach ( $params as $param ) {
593            if ( $param instanceof ScalarParam && $param->getType() === ParamType::TEXT ) {
594                // Unwrap for compatibility with legacy code that inspects the parameters
595                $param = $param->getValue();
596            }
597            if ( $param instanceof MessageSpecifier ) {
598                $param = static::newFromSpecifier( $param );
599            }
600            $this->parameters[] = $param;
601        }
602        return $this;
603    }
604
605    /**
606     * Add parameters that are substituted after parsing or escaping.
607     * In other words the parsing process cannot access the contents
608     * of this type of parameter, and you need to make sure it is
609     * sanitized beforehand.  The parser will see "$n", instead.
610     *
611     * @since 1.17
612     *
613     * @param string|int|float|MessageSpecifier|array<string|int|float|MessageSpecifier> ...$params
614     * Raw parameters as strings, or a single argument that is an array of raw parameters.
615     * @param-taint ...$params html,exec_html
616     *
617     * @return $this
618     */
619    public function rawParams( ...$params ) {
620        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
621            $params = $params[0];
622        }
623        foreach ( $params as $param ) {
624            $this->parameters[] = self::rawParam( $param );
625        }
626        return $this;
627    }
628
629    /**
630     * Add parameters that are numeric and will be passed through
631     * Language::formatNum before substitution
632     *
633     * @since 1.18
634     *
635     * @param string|int|float|list<string|int|float> ...$params Numeric parameters, or a single argument that is
636     * an array of numeric parameters.
637     *
638     * @return $this
639     */
640    public function numParams( ...$params ) {
641        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
642            $params = $params[0];
643        }
644        foreach ( $params as $param ) {
645            $this->parameters[] = self::numParam( $param );
646        }
647        return $this;
648    }
649
650    /**
651     * Add parameters that are durations of time and will be passed through
652     * Language::formatDuration before substitution
653     *
654     * @since 1.22
655     *
656     * @param int|int[] ...$params Duration parameters, or a single argument that is
657     * an array of duration parameters.
658     *
659     * @return $this
660     */
661    public function durationParams( ...$params ) {
662        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
663            $params = $params[0];
664        }
665        foreach ( $params as $param ) {
666            $this->parameters[] = self::durationParam( $param );
667        }
668        return $this;
669    }
670
671    /**
672     * Add parameters that are expiration times and will be passed through
673     * Language::formatExpiry before substitution
674     *
675     * @since 1.22
676     *
677     * @param string|string[] ...$params Expiry parameters, or a single argument that is
678     * an array of expiry parameters.
679     *
680     * @return $this
681     */
682    public function expiryParams( ...$params ) {
683        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
684            $params = $params[0];
685        }
686        foreach ( $params as $param ) {
687            $this->parameters[] = self::expiryParam( $param );
688        }
689        return $this;
690    }
691
692    /**
693     * Add parameters that are date-times and will be passed through
694     * Language::timeanddate before substitution
695     *
696     * @since 1.36
697     *
698     * @param string|string[] ...$params Date-time parameters, or a single argument that is
699     * an array of date-time parameters.
700     *
701     * @return $this
702     */
703    public function dateTimeParams( ...$params ) {
704        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
705            $params = $params[0];
706        }
707        foreach ( $params as $param ) {
708            $this->parameters[] = self::dateTimeParam( $param );
709        }
710        return $this;
711    }
712
713    /**
714     * Add parameters that are dates and will be passed through
715     * Language::date before substitution
716     *
717     * @since 1.36
718     *
719     * @param string|string[] ...$params Date parameters, or a single argument that is
720     * an array of date parameters.
721     *
722     * @return $this
723     */
724    public function dateParams( ...$params ) {
725        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
726            $params = $params[0];
727        }
728        foreach ( $params as $param ) {
729            $this->parameters[] = self::dateParam( $param );
730        }
731        return $this;
732    }
733
734    /**
735     * Add parameters that represent user groups
736     *
737     * @since 1.38
738     *
739     * @param string|string[] ...$params User Group parameters, or a single argument that is
740     * an array of user group parameters.
741     *
742     * @return $this
743     */
744    public function userGroupParams( ...$params ) {
745        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
746            $params = $params[0];
747        }
748        foreach ( $params as $param ) {
749            $this->parameters[] = self::userGroupParam( $param );
750        }
751        return $this;
752    }
753
754    /**
755     * Add parameters that are times and will be passed through
756     * Language::time before substitution
757     *
758     * @since 1.36
759     *
760     * @param string|string[] ...$params Time parameters, or a single argument that is
761     * an array of time parameters.
762     *
763     * @return $this
764     */
765    public function timeParams( ...$params ) {
766        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
767            $params = $params[0];
768        }
769        foreach ( $params as $param ) {
770            $this->parameters[] = self::timeParam( $param );
771        }
772        return $this;
773    }
774
775    /**
776     * Add parameters that are time periods and will be passed through
777     * Language::formatTimePeriod before substitution
778     *
779     * @since 1.22
780     *
781     * @param int|float|(int|float)[] ...$params Time period parameters, or a single argument that is
782     * an array of time period parameters.
783     *
784     * @return $this
785     */
786    public function timeperiodParams( ...$params ) {
787        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
788            $params = $params[0];
789        }
790        foreach ( $params as $param ) {
791            $this->parameters[] = self::timeperiodParam( $param );
792        }
793        return $this;
794    }
795
796    /**
797     * Add parameters that are file sizes and will be passed through
798     * Language::formatSize before substitution
799     *
800     * @since 1.22
801     *
802     * @param int|int[] ...$params Size parameters, or a single argument that is
803     * an array of size parameters.
804     *
805     * @return $this
806     */
807    public function sizeParams( ...$params ) {
808        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
809            $params = $params[0];
810        }
811        foreach ( $params as $param ) {
812            $this->parameters[] = self::sizeParam( $param );
813        }
814        return $this;
815    }
816
817    /**
818     * Add parameters that are bitrates and will be passed through
819     * Language::formatBitrate before substitution
820     *
821     * @since 1.22
822     *
823     * @param int|int[] ...$params Bit rate parameters, or a single argument that is
824     * an array of bit rate parameters.
825     *
826     * @return $this
827     */
828    public function bitrateParams( ...$params ) {
829        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
830            $params = $params[0];
831        }
832        foreach ( $params as $param ) {
833            $this->parameters[] = self::bitrateParam( $param );
834        }
835        return $this;
836    }
837
838    /**
839     * Add parameters that are plaintext and will be passed through without
840     * the content being evaluated.  Plaintext parameters are not valid as
841     * arguments to parser functions. This differs from self::rawParams in
842     * that the Message class handles escaping to match the output format.
843     *
844     * @since 1.25
845     *
846     * @param string|string[] ...$params plaintext parameters, or a single argument that is
847     * an array of plaintext parameters.
848     *
849     * @return $this
850     */
851    public function plaintextParams( ...$params ) {
852        if ( isset( $params[0] ) && is_array( $params[0] ) ) {
853            $params = $params[0];
854        }
855        foreach ( $params as $param ) {
856            $this->parameters[] = self::plaintextParam( $param );
857        }
858        return $this;
859    }
860
861    /**
862     * Set the language and the title from a context object
863     *
864     * @since 1.19
865     *
866     * @param IContextSource $context
867     *
868     * @return $this
869     */
870    public function setContext( IContextSource $context ) {
871        $this->userLangCallback = static function () use ( $context ) {
872            return $context->getLanguage();
873        };
874        $this->inUserLanguage();
875        $this->page( $context->getTitle() );
876
877        return $this;
878    }
879
880    /**
881     * Request the message in any language that is supported.
882     *
883     * As a side effect interface message status is unconditionally
884     * turned off.
885     *
886     * @since 1.17
887     * @param Bcp47Code|StubUserLang|string $lang Language code or language object.
888     * @return $this
889     */
890    public function inLanguage( $lang ) {
891        $previousLanguage = $this->language;
892
893        if ( $lang instanceof Language ) {
894            $this->language = $lang;
895        } elseif ( $lang instanceof StubUserLang ) {
896            $this->language = null;
897        } elseif ( $lang instanceof Bcp47Code ) {
898            if ( !( $this->language instanceof Language && $this->language->isSameCodeAs( $lang ) ) ) {
899                $this->language = MediaWikiServices::getInstance()->getLanguageFactory()
900                    ->getLanguage( $lang );
901            }
902        } elseif ( is_string( $lang ) ) {
903            if ( !( $this->language instanceof Language && $this->language->getCode() === $lang ) ) {
904                $this->language = MediaWikiServices::getInstance()->getLanguageFactory()
905                    ->getLanguageCode( $lang )->toString();
906            }
907        } else {
908            // Always throws. Moved here as an optimization.
909            Assert::parameterType( [ Bcp47Code::class, StubUserLang::class, 'string' ], $lang, '$lang' );
910        }
911
912        if ( $this->language !== $previousLanguage ) {
913            // The language has changed. Clear the message cache.
914            $this->message = null;
915        }
916        $this->isInterface = false;
917        return $this;
918    }
919
920    /**
921     * Request the message in the user's current language, overriding any
922     * explicit language that was previously set. Set the interface flag to
923     * true.
924     *
925     * @since 1.42
926     * @return $this
927     */
928    public function inUserLanguage(): self {
929        if ( $this->language ) {
930            // The language has changed. Clear the message cache.
931            $this->message = null;
932        }
933        $this->language = null;
934        $this->isInterface = true;
935        return $this;
936    }
937
938    /**
939     * Request the message in the wiki's content language,
940     * unless it is disabled for this message.
941     *
942     * @since 1.17
943     * @see $wgForceUIMsgAsContentMsg
944     *
945     * @return $this
946     */
947    public function inContentLanguage(): self {
948        $forceUIMsgAsContentMsg = MediaWikiServices::getInstance()->getMainConfig()->get(
949            MainConfigNames::ForceUIMsgAsContentMsg );
950        if ( in_array( $this->key, (array)$forceUIMsgAsContentMsg ) ) {
951            return $this;
952        }
953
954        $this->inLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
955        return $this;
956    }
957
958    /**
959     * Allows manipulating the interface message flag directly.
960     * Can be used to restore the flag after setting a language.
961     *
962     * @since 1.20
963     *
964     * @param bool $interface
965     *
966     * @return $this
967     */
968    public function setInterfaceMessageFlag( $interface ) {
969        $this->isInterface = (bool)$interface;
970        return $this;
971    }
972
973    /**
974     * @since 1.17
975     *
976     * @param bool $useDatabase If messages in the local MediaWiki namespace should be loaded; false
977     *  to use only the compiled LocalisationCache
978     *
979     * @return $this
980     */
981    public function useDatabase( $useDatabase ) {
982        $this->useDatabase = (bool)$useDatabase;
983        $this->message = null;
984        return $this;
985    }
986
987    /**
988     * Set the Title object to use as context when transforming the message
989     *
990     * @since 1.18
991     * @deprecated since 1.37. Use ::page instead
992     *
993     * @param Title $title
994     *
995     * @return $this
996     */
997    public function title( $title ) {
998        return $this->page( $title );
999    }
1000
1001    /**
1002     * Set the page object to use as context when transforming the message
1003     *
1004     * @since 1.37
1005     *
1006     * @param ?PageReference $page
1007     *
1008     * @return $this
1009     */
1010    public function page( ?PageReference $page ) {
1011        if ( $page ) {
1012            $page->assertWiki( PageReference::LOCAL );
1013        }
1014        $this->contextPage = $page;
1015        return $this;
1016    }
1017
1018    /**
1019     * Returns the message formatted a certain way.
1020     *
1021     * @since 1.17
1022     * @param string $format One of the FORMAT_* constants.
1023     * @return string Text or HTML
1024     */
1025    public function toString( string $format ): string {
1026        return $this->format( $format );
1027    }
1028
1029    /**
1030     * Returns the message formatted a certain way.
1031     *
1032     * @param string $format One of the FORMAT_* constants.
1033     * @return string Text or HTML
1034     * @suppress SecurityCheck-DoubleEscaped phan false positive
1035     */
1036    private function format( string $format ): string {
1037        $string = $this->fetchMessage();
1038
1039        if ( $string === false ) {
1040            // Err on the side of safety, ensure that the output
1041            // is always html safe in the event the message key is
1042            // missing, since in that case its highly likely the
1043            // message key is user-controlled.
1044            // 'â§¼' is used instead of '<' to side-step any
1045            // double-escaping issues.
1046            // (Keep synchronised with mw.Message#toString in JS.)
1047            return 'â§¼' . Sanitizer::escapeCombiningChar( htmlspecialchars( $this->key ) ) . 'â§½';
1048        }
1049
1050        if ( in_array( $this->getLanguageCode(), [ 'qqx', 'x-xss' ] ) ) {
1051            # Insert a list of alternative message keys for &uselang=qqx.
1052            if ( $string === '($*)' ) {
1053                $keylist = implode( ' / ', $this->keysToTry );
1054                if ( $this->overriddenKey !== null ) {
1055                    $keylist .= ' = ' . $this->overriddenKey;
1056                }
1057                $string = "($keylist$*)";
1058            }
1059            # Replace $* with a list of parameters for &uselang=qqx.
1060            if ( str_contains( $string, '$*' ) ) {
1061                $paramlist = '';
1062                if ( $this->parameters !== [] ) {
1063                    $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
1064                }
1065                $string = str_replace( '$*', $paramlist, $string );
1066            }
1067        }
1068
1069        # Replace parameters before text parsing
1070        $string = $this->replaceParameters( $string, 'before', $format );
1071
1072        # Maybe transform using the full parser
1073        if ( $format === self::FORMAT_PARSE ) {
1074            $po = $this->parseText( $string );
1075            $string = Parser::stripOuterParagraph( $po->getContentHolderText() );
1076        } elseif ( $format === self::FORMAT_BLOCK_PARSE ) {
1077            $po = $this->parseText( $string );
1078            $string = $po->getContentHolderText();
1079        } elseif ( $format === self::FORMAT_TEXT ) {
1080            $string = $this->transformText( $string );
1081        } elseif ( $format === self::FORMAT_ESCAPED ) {
1082            $string = $this->transformText( $string );
1083            $string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
1084            $string = Sanitizer::escapeCombiningChar( $string );
1085        }
1086
1087        # Raw parameter replacement
1088        $string = $this->replaceParameters( $string, 'after', $format );
1089
1090        // XXX: Sometimes messages are created prior to MediaWikiServices being
1091        // constructed which causes problems when we try to run the hook.
1092        // Skip the hook for very early calls.
1093        if ( MediaWikiServices::hasInstance() ) {
1094            if ( $format === self::FORMAT_TEXT || $format === self::FORMAT_PLAIN ) {
1095                $this->getHookRunner()->onMessagePostProcessText( $string, $format, $this->key );
1096            } else {
1097                $this->getHookRunner()->onMessagePostProcessHtml( $string, $format, $this->key );
1098            }
1099        }
1100        return $string;
1101    }
1102
1103    /**
1104     * Magic method implementation of the above, so we can do, eg:
1105     *     $foo = new Message( $key );
1106     *     $string = "<abbr>$foo</abbr>";
1107     *
1108     * @since 1.18
1109     *
1110     * @return string
1111     * @return-taint escaped
1112     */
1113    public function __toString() {
1114        return $this->format( self::FORMAT_PARSE );
1115    }
1116
1117    /**
1118     * Fully parse the text from wikitext to HTML.
1119     *
1120     * @since 1.17
1121     *
1122     * @return string Parsed HTML.
1123     * @return-taint escaped
1124     */
1125    public function parse() {
1126        return $this->format( self::FORMAT_PARSE );
1127    }
1128
1129    /**
1130     * Returns the message text. {{-transformation occurs (substituting the template
1131     * with its parsed result).
1132     *
1133     * @since 1.17
1134     *
1135     * @return string Unescaped message text.
1136     * @return-taint tainted
1137     */
1138    public function text() {
1139        return $this->format( self::FORMAT_TEXT );
1140    }
1141
1142    /**
1143     * Returns the message text as-is, only parameters are substituted.
1144     *
1145     * @since 1.17
1146     *
1147     * @return string Unescaped untransformed message text.
1148     * @return-taint tainted
1149     */
1150    public function plain() {
1151        return $this->format( self::FORMAT_PLAIN );
1152    }
1153
1154    /**
1155     * Returns the parsed message text which is always surrounded by a block element.
1156     *
1157     * @since 1.17
1158     *
1159     * @return string HTML
1160     * @return-taint escaped
1161     */
1162    public function parseAsBlock() {
1163        return $this->format( self::FORMAT_BLOCK_PARSE );
1164    }
1165
1166    /**
1167     * Returns the message text. {{-transformation (substituting the template with its
1168     * parsed result) is done and the result is HTML escaped excluding any raw parameters.
1169     *
1170     * @since 1.17
1171     *
1172     * @return string HTML escaped message text.
1173     * @return-taint escaped
1174     */
1175    public function escaped() {
1176        return $this->format( self::FORMAT_ESCAPED );
1177    }
1178
1179    /**
1180     * Check whether a message key has been defined currently.
1181     *
1182     * @since 1.17
1183     *
1184     * @return bool
1185     */
1186    public function exists() {
1187        return $this->fetchMessage() !== false;
1188    }
1189
1190    /**
1191     * Check whether a message does not exist, or is an empty string
1192     *
1193     * @since 1.18
1194     * @todo FIXME: Merge with isDisabled()?
1195     *
1196     * @return bool
1197     */
1198    public function isBlank() {
1199        $message = $this->fetchMessage();
1200        return $message === false || $message === '';
1201    }
1202
1203    /**
1204     * Check whether a message does not exist, is an empty string, or is "-".
1205     *
1206     * @since 1.18
1207     *
1208     * @return bool
1209     */
1210    public function isDisabled() {
1211        $message = $this->fetchMessage();
1212        return $message === false || $message === '' || $message === '-';
1213    }
1214
1215    /**
1216     * @since 1.17
1217     *
1218     * @param string|int|float|MessageSpecifier $raw
1219     * @param-taint $raw html,exec_html
1220     *
1221     * @return ScalarParam
1222     */
1223    public static function rawParam( $raw ): ScalarParam {
1224        return new ScalarParam( ParamType::RAW, $raw );
1225    }
1226
1227    /**
1228     * @since 1.18
1229     *
1230     * @param string|int|float $num
1231     *
1232     * @return ScalarParam
1233     */
1234    public static function numParam( $num ): ScalarParam {
1235        return new ScalarParam( ParamType::NUM, $num );
1236    }
1237
1238    /**
1239     * @since 1.22
1240     *
1241     * @param int $duration
1242     *
1243     * @return ScalarParam
1244     */
1245    public static function durationParam( $duration ): ScalarParam {
1246        return new ScalarParam( ParamType::DURATION_LONG, $duration );
1247    }
1248
1249    /**
1250     * @since 1.22
1251     *
1252     * @param string $expiry
1253     *
1254     * @return ScalarParam
1255     */
1256    public static function expiryParam( $expiry ): ScalarParam {
1257        return new ScalarParam( ParamType::EXPIRY, $expiry );
1258    }
1259
1260    /**
1261     * @since 1.36
1262     *
1263     * @param string $dateTime
1264     *
1265     * @return ScalarParam
1266     */
1267    public static function dateTimeParam( string $dateTime ): ScalarParam {
1268        return new ScalarParam( ParamType::DATETIME, $dateTime );
1269    }
1270
1271    /**
1272     * @since 1.36
1273     *
1274     * @param string $date
1275     *
1276     * @return ScalarParam
1277     */
1278    public static function dateParam( string $date ): ScalarParam {
1279        return new ScalarParam( ParamType::DATE, $date );
1280    }
1281
1282    /**
1283     * @since 1.36
1284     *
1285     * @param string $time
1286     *
1287     * @return ScalarParam
1288     */
1289    public static function timeParam( string $time ): ScalarParam {
1290        return new ScalarParam( ParamType::TIME, $time );
1291    }
1292
1293    /**
1294     * @since 1.38
1295     *
1296     * @param string $userGroup
1297     *
1298     * @return ScalarParam
1299     */
1300    public static function userGroupParam( string $userGroup ): ScalarParam {
1301        return new ScalarParam( ParamType::GROUP, $userGroup );
1302    }
1303
1304    /**
1305     * @since 1.22
1306     *
1307     * @param int|float $period
1308     *
1309     * @return ScalarParam
1310     */
1311    public static function timeperiodParam( $period ): ScalarParam {
1312        return new ScalarParam( ParamType::DURATION_SHORT, $period );
1313    }
1314
1315    /**
1316     * @since 1.22
1317     *
1318     * @param int $size
1319     *
1320     * @return ScalarParam
1321     */
1322    public static function sizeParam( $size ): ScalarParam {
1323        return new ScalarParam( ParamType::SIZE, $size );
1324    }
1325
1326    /**
1327     * @since 1.22
1328     *
1329     * @param int $bitrate
1330     *
1331     * @return ScalarParam
1332     */
1333    public static function bitrateParam( $bitrate ): ScalarParam {
1334        return new ScalarParam( ParamType::BITRATE, $bitrate );
1335    }
1336
1337    /**
1338     * @since 1.25
1339     *
1340     * @param string $plaintext
1341     *
1342     * @return ScalarParam
1343     */
1344    public static function plaintextParam( $plaintext ): ScalarParam {
1345        return new ScalarParam( ParamType::PLAINTEXT, $plaintext );
1346    }
1347
1348    /**
1349     * @since 1.29
1350     *
1351     * @param (MessageParam|MessageSpecifier|string|int|float)[] $list
1352     * @param ListType|string $type One of the ListType constants
1353     * @return ListParam
1354     */
1355    public static function listParam( array $list, $type = ListType::AND ): ListParam {
1356        if ( is_string( $type ) ) {
1357            wfDeprecated( __METHOD__ . ' with string type', '1.45' );
1358            $type = ListType::from( $type );
1359        }
1360        return new ListParam( $type, $list );
1361    }
1362
1363    /**
1364     * Substitutes any parameters into the message text.
1365     *
1366     * @since 1.17
1367     *
1368     * @param string $message The message text.
1369     * @param string $type Either "before" or "after".
1370     * @param string $format One of the FORMAT_* constants.
1371     *
1372     * @return string
1373     */
1374    protected function replaceParameters( $message, $type, $format ) {
1375        // A temporary marker for $1 parameters that is only valid
1376        // in non-attribute contexts. However if the entire message is escaped
1377        // then we don't want to use it because it will be mangled in all contexts
1378        // and its unnecessary as ->escaped() messages aren't html.
1379        $marker = $format === self::FORMAT_ESCAPED ? '$' : '$\'"';
1380        $replacementKeys = [];
1381        foreach ( $this->parameters as $n => $param ) {
1382            [ $paramType, $value ] = $this->extractParam( $param, $format );
1383            if ( $type === 'before' ) {
1384                if ( $paramType === 'before' ) {
1385                    $replacementKeys['$' . ( $n + 1 )] = $value;
1386                } else /* $paramType === 'after' */ {
1387                    // To protect against XSS from replacing parameters
1388                    // inside html attributes, we convert $1 to $'"1.
1389                    // In the event that one of the parameters ends up
1390                    // in an attribute, either the ' or the " will be
1391                    // escaped, breaking the replacement and avoiding XSS.
1392                    $replacementKeys['$' . ( $n + 1 )] = $marker . ( $n + 1 );
1393                }
1394            } elseif ( $paramType === 'after' ) {
1395                $replacementKeys[$marker . ( $n + 1 )] = $value;
1396            }
1397        }
1398        return strtr( $message, $replacementKeys );
1399    }
1400
1401    /**
1402     * Extracts the parameter type and preprocessed the value if needed.
1403     *
1404     * @since 1.18
1405     *
1406     * @param ScalarParam|ListParam|MessageSpecifier|string $param Parameter as defined in this class.
1407     * @param string $format One of the FORMAT_* constants.
1408     *
1409     * @return array Array with the parameter type (either "before" or "after") and the value.
1410     */
1411    protected function extractParam( $param, $format ) {
1412        if ( $param instanceof ScalarParam ) {
1413            return match ( $param->getType() ) {
1414                ParamType::RAW =>
1415                    [ 'after', $this->extractParam( $param->getValue(), self::FORMAT_PARSE )[1] ],
1416                ParamType::NUM =>
1417                    // Replace number params always in before step for now.
1418                    // No support for combined raw and num params
1419                    [ 'before', $this->getLanguage()->formatNum( $param->getValue() ) ],
1420                ParamType::DURATION_LONG =>
1421                    [ 'before', $this->getLanguage()->formatDuration( $param->getValue() ) ],
1422                ParamType::EXPIRY =>
1423                    [ 'before', $this->getLanguage()->formatExpiry( $param->getValue() ) ],
1424                ParamType::DATETIME =>
1425                    [ 'before', $this->getLanguage()->timeanddate( $param->getValue() ) ],
1426                ParamType::DATE =>
1427                    [ 'before', $this->getLanguage()->date( $param->getValue() ) ],
1428                ParamType::TIME =>
1429                    [ 'before', $this->getLanguage()->time( $param->getValue() ) ],
1430                ParamType::GROUP =>
1431                    [ 'before', $this->getLanguage()->getGroupName( $param->getValue() ) ],
1432                ParamType::DURATION_SHORT =>
1433                    [ 'before', $this->getLanguage()->formatTimePeriod( $param->getValue() ) ],
1434                ParamType::SIZE =>
1435                    [ 'before', $this->getLanguage()->formatSize( $param->getValue() ) ],
1436                ParamType::BITRATE =>
1437                    [ 'before', $this->getLanguage()->formatBitrate( $param->getValue() ) ],
1438                ParamType::PLAINTEXT =>
1439                    [ 'after', $this->formatPlaintext( $param->getValue(), $format ) ],
1440                // ParamType::TEXT is impossible because we unwrapped it in params()
1441                default =>
1442                    throw new \LogicException( "Invalid ScalarParam type: {$param->getType()->value}" ),
1443            };
1444        } elseif ( $param instanceof ListParam ) {
1445            return $this->formatListParam( $param->getValue(), $param->getListType(), $format );
1446        } elseif ( is_array( $param ) ) {
1447            LoggerFactory::getInstance( 'Bug58676' )->warning(
1448                'Invalid parameter for message "{msgkey}": {param}',
1449                [
1450                    'exception' => new RuntimeException,
1451                    'msgkey' => $this->key,
1452                    'param' => htmlspecialchars( serialize( $param ) ),
1453                ]
1454            );
1455            return [ 'before', '[INVALID]' ];
1456        } elseif ( $param instanceof MessageSpecifier ) {
1457            // Match language, flags, etc. to the current message.
1458            $msg = static::newFromSpecifier( $param );
1459            if ( $msg->language !== $this->language || $msg->useDatabase !== $this->useDatabase ) {
1460                // Cache depends on these parameters
1461                $msg->message = null;
1462            }
1463            $msg->isInterface = $this->isInterface;
1464            $msg->language = $this->language;
1465            $msg->userLangCallback = $this->userLangCallback;
1466            $msg->useDatabase = $this->useDatabase;
1467            $msg->contextPage = $this->contextPage;
1468
1469            // DWIM
1470            if ( $format === 'block-parse' ) {
1471                $format = 'parse';
1472            }
1473
1474            // Message objects should not be before parameters because
1475            // then they'll get double escaped. If the message needs to be
1476            // escaped, it'll happen right here when we call toString().
1477            // (Using the public toString() to allow mocking / subclassing.)
1478            return [ 'after', $msg->toString( $format ) ];
1479        } else {
1480            return [ 'before', $param ];
1481        }
1482    }
1483
1484    /**
1485     * Wrapper for what ever method we use to parse wikitext.
1486     *
1487     * @since 1.17
1488     *
1489     * @param string $string Wikitext message contents.
1490     *
1491     * @return ParserOutput Wikitext parsed into HTML.
1492     */
1493    protected function parseText( string $string ): ParserOutput {
1494        $out = MediaWikiServices::getInstance()->getMessageParser()->parse(
1495            $string,
1496            $this->contextPage ?? PageReferenceValue::localReference( NS_SPECIAL, 'Badtitle/Message' ),
1497            /*linestart*/ true,
1498            $this->isInterface,
1499            $this->getFetchedLanguage()
1500        );
1501
1502        return $out;
1503    }
1504
1505    /**
1506     * Wrapper for what ever method we use to {{-transform wikitext (substituting the
1507     * template with its parsed result).
1508     *
1509     * @since 1.17
1510     *
1511     * @param string $string Wikitext message contents.
1512     *
1513     * @return string Wikitext with {{-constructs substituted with its parsed result.
1514     */
1515    protected function transformText( $string ) {
1516        return MediaWikiServices::getInstance()->getMessageParser()->transform(
1517            $string,
1518            $this->isInterface,
1519            $this->getFetchedLanguage(),
1520            $this->contextPage
1521        );
1522    }
1523
1524    /**
1525     * Wrapper for whatever method we use to get message contents.
1526     *
1527     * @since 1.17
1528     *
1529     * @return string|false
1530     */
1531    protected function fetchMessage() {
1532        if ( $this->message === null ) {
1533            $cache = MediaWikiServices::getInstance()->getMessageCache();
1534
1535            $info = new MessageInfo;
1536            $langCode = $this->getLanguageCode();
1537            foreach ( $this->keysToTry as $key ) {
1538                $message = $cache->get( $key, $this->useDatabase, $this->getLanguageCode(), $info );
1539                if ( $message !== false && $message !== '' ) {
1540                    if ( $info->usedKey && $info->usedKey !== $key ) {
1541                        $this->overriddenKey = $info->usedKey;
1542                    }
1543                    if ( $info->langCode && $info->langCode !== $langCode ) {
1544                        $this->fetchedLangCode = $info->langCode;
1545                    }
1546                    break;
1547                }
1548            }
1549
1550            // NOTE: The constructor makes sure keysToTry isn't empty,
1551            //       so we know that $key and $message are initialized.
1552            // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable False positive
1553            // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty False positive
1554            $this->key = $key;
1555            // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
1556            $this->message = $message;
1557        }
1558        return $this->message;
1559    }
1560
1561    /**
1562     * Formats a message parameter wrapped with 'plaintext'. Ensures that
1563     * the entire string is displayed unchanged when displayed in the output
1564     * format.
1565     *
1566     * @since 1.25
1567     *
1568     * @param string $plaintext String to ensure plaintext output of
1569     * @param string $format One of the FORMAT_* constants.
1570     *
1571     * @return string Input plaintext encoded for output to $format
1572     */
1573    protected function formatPlaintext( $plaintext, $format ) {
1574        switch ( $format ) {
1575            case self::FORMAT_TEXT:
1576            case self::FORMAT_PLAIN:
1577                return $plaintext;
1578
1579            case self::FORMAT_PARSE:
1580            case self::FORMAT_BLOCK_PARSE:
1581            case self::FORMAT_ESCAPED:
1582            default:
1583                return Sanitizer::escapeCombiningChar( htmlspecialchars( $plaintext, ENT_QUOTES ) );
1584        }
1585    }
1586
1587    /**
1588     * Mapping from Message::listParam() types to Language methods.
1589     * @param Language $lang
1590     * @param ListType $listType
1591     * @return callable(array):string
1592     */
1593    protected static function listTypeMap( Language $lang, ListType $listType ): callable {
1594        return match ( $listType ) {
1595            ListType::COMMA => $lang->commaList( ... ),
1596            ListType::SEMICOLON => $lang->semicolonList( ... ),
1597            ListType::PIPE => $lang->pipeList( ... ),
1598            ListType::AND => $lang->listToText( ... ),
1599        };
1600    }
1601
1602    /**
1603     * Formats a list of parameters as a concatenated string.
1604     * @since 1.29
1605     * @param array $params
1606     * @param string|ListType $listType
1607     * @param string $format One of the FORMAT_* constants.
1608     * @return array Array with the parameter type (either "before" or "after") and the value.
1609     */
1610    protected function formatListParam( array $params, $listType, $format ) {
1611        if ( is_string( $listType ) ) {
1612            $originalListType = $listType;
1613            $listType = ListType::tryFrom( $listType );
1614            if ( $listType === null ) {
1615                $warning = 'Invalid list type for message "' . $this->key . '": '
1616                    . htmlspecialchars( $originalListType )
1617                    . ' (params are ' . htmlspecialchars( serialize( $params ) ) . ')';
1618                trigger_error( $warning, E_USER_WARNING );
1619                $e = new InvalidArgumentException;
1620                wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
1621                return [ 'before', '[INVALID]' ];
1622            }
1623        }
1624        $func = self::listTypeMap( $this->getLanguage(), $listType );
1625
1626        // Handle an empty list sensibly
1627        if ( !$params ) {
1628            return [ 'before', $func( [] ) ];
1629        }
1630
1631        // First, determine what kinds of list items we have
1632        $types = [];
1633        $vars = [];
1634        $list = [];
1635        foreach ( $params as $n => $p ) {
1636            if ( $p instanceof ScalarParam && $p->getType() === ParamType::TEXT ) {
1637                // Unwrap like in params()
1638                $p = $p->getValue();
1639            }
1640            [ $type, $value ] = $this->extractParam( $p, $format );
1641            $types[$type] = true;
1642            $list[] = $value;
1643            $vars[] = '$' . ( $n + 1 );
1644        }
1645
1646        // Easy case: all are 'before' or 'after', so just join the
1647        // values and use the same type.
1648        if ( count( $types ) === 1 ) {
1649            return [ key( $types ), $func( $list ) ];
1650        }
1651
1652        // Hard case: We need to process each value per its type, then
1653        // return the concatenated values as 'after'. We handle this by turning
1654        // the list into a RawMessage and processing that as a parameter.
1655        $vars = $func( $vars );
1656        return $this->extractParam( new RawMessage( $vars, $params ), $format );
1657    }
1658}
1659
1660/** @deprecated class alias since 1.42 */
1661class_alias( Message::class, 'Message' );