Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.86% covered (warning)
79.86%
333 / 417
55.07% covered (warning)
55.07%
38 / 69
CRAP
0.00% covered (danger)
0.00%
0 / 1
Message
80.05% covered (warning)
80.05%
333 / 416
55.07% covered (warning)
55.07%
38 / 69
534.53
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%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 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.02% covered (warning)
83.02%
44 / 53
0.00% covered (danger)
0.00%
0 / 1
8.31
 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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $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 self $this
1009     */
1010    public function page( ?PageReference $page ) {
1011        $this->contextPage = $page;
1012        return $this;
1013    }
1014
1015    /**
1016     * Returns the message formatted a certain way.
1017     *
1018     * @since 1.17
1019     * @param string $format One of the FORMAT_* constants.
1020     * @return string Text or HTML
1021     */
1022    public function toString( string $format ): string {
1023        return $this->format( $format );
1024    }
1025
1026    /**
1027     * Returns the message formatted a certain way.
1028     *
1029     * @param string $format One of the FORMAT_* constants.
1030     * @return string Text or HTML
1031     * @suppress SecurityCheck-DoubleEscaped phan false positive
1032     */
1033    private function format( string $format ): string {
1034        $string = $this->fetchMessage();
1035
1036        if ( $string === false ) {
1037            // Err on the side of safety, ensure that the output
1038            // is always html safe in the event the message key is
1039            // missing, since in that case its highly likely the
1040            // message key is user-controlled.
1041            // 'â§¼' is used instead of '<' to side-step any
1042            // double-escaping issues.
1043            // (Keep synchronised with mw.Message#toString in JS.)
1044            return 'â§¼' . Sanitizer::escapeCombiningChar( htmlspecialchars( $this->key ) ) . 'â§½';
1045        }
1046
1047        if ( in_array( $this->getLanguageCode(), [ 'qqx', 'x-xss' ] ) ) {
1048            # Insert a list of alternative message keys for &uselang=qqx.
1049            if ( $string === '($*)' ) {
1050                $keylist = implode( ' / ', $this->keysToTry );
1051                if ( $this->overriddenKey !== null ) {
1052                    $keylist .= ' = ' . $this->overriddenKey;
1053                }
1054                $string = "($keylist$*)";
1055            }
1056            # Replace $* with a list of parameters for &uselang=qqx.
1057            if ( str_contains( $string, '$*' ) ) {
1058                $paramlist = '';
1059                if ( $this->parameters !== [] ) {
1060                    $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
1061                }
1062                $string = str_replace( '$*', $paramlist, $string );
1063            }
1064        }
1065
1066        # Replace parameters before text parsing
1067        $string = $this->replaceParameters( $string, 'before', $format );
1068
1069        # Maybe transform using the full parser
1070        if ( $format === self::FORMAT_PARSE ) {
1071            $po = $this->parseText( $string );
1072            $string = Parser::stripOuterParagraph( $po->getContentHolderText() );
1073        } elseif ( $format === self::FORMAT_BLOCK_PARSE ) {
1074            $po = $this->parseText( $string );
1075            $string = $po->getContentHolderText();
1076        } elseif ( $format === self::FORMAT_TEXT ) {
1077            $string = $this->transformText( $string );
1078        } elseif ( $format === self::FORMAT_ESCAPED ) {
1079            $string = $this->transformText( $string );
1080            $string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
1081            $string = Sanitizer::escapeCombiningChar( $string );
1082        }
1083
1084        # Raw parameter replacement
1085        $string = $this->replaceParameters( $string, 'after', $format );
1086
1087        // XXX: Sometimes messages are created prior to MediaWikiServices being
1088        // constructed which causes problems when we try to run the hook.
1089        // Skip the hook for very early calls.
1090        if ( MediaWikiServices::hasInstance() ) {
1091            if ( $format === self::FORMAT_TEXT || $format === self::FORMAT_PLAIN ) {
1092                $this->getHookRunner()->onMessagePostProcessText( $string, $format, $this->key );
1093            } else {
1094                $this->getHookRunner()->onMessagePostProcessHtml( $string, $format, $this->key );
1095            }
1096        }
1097        return $string;
1098    }
1099
1100    /**
1101     * Magic method implementation of the above, so we can do, eg:
1102     *     $foo = new Message( $key );
1103     *     $string = "<abbr>$foo</abbr>";
1104     *
1105     * @since 1.18
1106     *
1107     * @return string
1108     * @return-taint escaped
1109     */
1110    public function __toString() {
1111        return $this->format( self::FORMAT_PARSE );
1112    }
1113
1114    /**
1115     * Fully parse the text from wikitext to HTML.
1116     *
1117     * @since 1.17
1118     *
1119     * @return string Parsed HTML.
1120     * @return-taint escaped
1121     */
1122    public function parse() {
1123        return $this->format( self::FORMAT_PARSE );
1124    }
1125
1126    /**
1127     * Returns the message text. {{-transformation occurs (substituting the template
1128     * with its parsed result).
1129     *
1130     * @since 1.17
1131     *
1132     * @return string Unescaped message text.
1133     * @return-taint tainted
1134     */
1135    public function text() {
1136        return $this->format( self::FORMAT_TEXT );
1137    }
1138
1139    /**
1140     * Returns the message text as-is, only parameters are substituted.
1141     *
1142     * @since 1.17
1143     *
1144     * @return string Unescaped untransformed message text.
1145     * @return-taint tainted
1146     */
1147    public function plain() {
1148        return $this->format( self::FORMAT_PLAIN );
1149    }
1150
1151    /**
1152     * Returns the parsed message text which is always surrounded by a block element.
1153     *
1154     * @since 1.17
1155     *
1156     * @return string HTML
1157     * @return-taint escaped
1158     */
1159    public function parseAsBlock() {
1160        return $this->format( self::FORMAT_BLOCK_PARSE );
1161    }
1162
1163    /**
1164     * Returns the message text. {{-transformation (substituting the template with its
1165     * parsed result) is done and the result is HTML escaped excluding any raw parameters.
1166     *
1167     * @since 1.17
1168     *
1169     * @return string HTML escaped message text.
1170     * @return-taint escaped
1171     */
1172    public function escaped() {
1173        return $this->format( self::FORMAT_ESCAPED );
1174    }
1175
1176    /**
1177     * Check whether a message key has been defined currently.
1178     *
1179     * @since 1.17
1180     *
1181     * @return bool
1182     */
1183    public function exists() {
1184        return $this->fetchMessage() !== false;
1185    }
1186
1187    /**
1188     * Check whether a message does not exist, or is an empty string
1189     *
1190     * @since 1.18
1191     * @todo FIXME: Merge with isDisabled()?
1192     *
1193     * @return bool
1194     */
1195    public function isBlank() {
1196        $message = $this->fetchMessage();
1197        return $message === false || $message === '';
1198    }
1199
1200    /**
1201     * Check whether a message does not exist, is an empty string, or is "-".
1202     *
1203     * @since 1.18
1204     *
1205     * @return bool
1206     */
1207    public function isDisabled() {
1208        $message = $this->fetchMessage();
1209        return $message === false || $message === '' || $message === '-';
1210    }
1211
1212    /**
1213     * @since 1.17
1214     *
1215     * @param string|int|float|MessageSpecifier $raw
1216     * @param-taint $raw html,exec_html
1217     *
1218     * @return ScalarParam
1219     */
1220    public static function rawParam( $raw ): ScalarParam {
1221        return new ScalarParam( ParamType::RAW, $raw );
1222    }
1223
1224    /**
1225     * @since 1.18
1226     *
1227     * @param string|int|float $num
1228     *
1229     * @return ScalarParam
1230     */
1231    public static function numParam( $num ): ScalarParam {
1232        return new ScalarParam( ParamType::NUM, $num );
1233    }
1234
1235    /**
1236     * @since 1.22
1237     *
1238     * @param int $duration
1239     *
1240     * @return ScalarParam
1241     */
1242    public static function durationParam( $duration ): ScalarParam {
1243        return new ScalarParam( ParamType::DURATION_LONG, $duration );
1244    }
1245
1246    /**
1247     * @since 1.22
1248     *
1249     * @param string $expiry
1250     *
1251     * @return ScalarParam
1252     */
1253    public static function expiryParam( $expiry ): ScalarParam {
1254        return new ScalarParam( ParamType::EXPIRY, $expiry );
1255    }
1256
1257    /**
1258     * @since 1.36
1259     *
1260     * @param string $dateTime
1261     *
1262     * @return ScalarParam
1263     */
1264    public static function dateTimeParam( string $dateTime ): ScalarParam {
1265        return new ScalarParam( ParamType::DATETIME, $dateTime );
1266    }
1267
1268    /**
1269     * @since 1.36
1270     *
1271     * @param string $date
1272     *
1273     * @return ScalarParam
1274     */
1275    public static function dateParam( string $date ): ScalarParam {
1276        return new ScalarParam( ParamType::DATE, $date );
1277    }
1278
1279    /**
1280     * @since 1.36
1281     *
1282     * @param string $time
1283     *
1284     * @return ScalarParam
1285     */
1286    public static function timeParam( string $time ): ScalarParam {
1287        return new ScalarParam( ParamType::TIME, $time );
1288    }
1289
1290    /**
1291     * @since 1.38
1292     *
1293     * @param string $userGroup
1294     *
1295     * @return ScalarParam
1296     */
1297    public static function userGroupParam( string $userGroup ): ScalarParam {
1298        return new ScalarParam( ParamType::GROUP, $userGroup );
1299    }
1300
1301    /**
1302     * @since 1.22
1303     *
1304     * @param int|float $period
1305     *
1306     * @return ScalarParam
1307     */
1308    public static function timeperiodParam( $period ): ScalarParam {
1309        return new ScalarParam( ParamType::DURATION_SHORT, $period );
1310    }
1311
1312    /**
1313     * @since 1.22
1314     *
1315     * @param int $size
1316     *
1317     * @return ScalarParam
1318     */
1319    public static function sizeParam( $size ): ScalarParam {
1320        return new ScalarParam( ParamType::SIZE, $size );
1321    }
1322
1323    /**
1324     * @since 1.22
1325     *
1326     * @param int $bitrate
1327     *
1328     * @return ScalarParam
1329     */
1330    public static function bitrateParam( $bitrate ): ScalarParam {
1331        return new ScalarParam( ParamType::BITRATE, $bitrate );
1332    }
1333
1334    /**
1335     * @since 1.25
1336     *
1337     * @param string $plaintext
1338     *
1339     * @return ScalarParam
1340     */
1341    public static function plaintextParam( $plaintext ): ScalarParam {
1342        return new ScalarParam( ParamType::PLAINTEXT, $plaintext );
1343    }
1344
1345    /**
1346     * @since 1.29
1347     *
1348     * @param (MessageParam|MessageSpecifier|string|int|float)[] $list
1349     * @param ListType|string $type One of the ListType constants
1350     * @return ListParam
1351     */
1352    public static function listParam( array $list, $type = ListType::AND ): ListParam {
1353        if ( is_string( $type ) ) {
1354            wfDeprecated( __METHOD__ . ' with string type', '1.45' );
1355            $type = ListType::from( $type );
1356        }
1357        return new ListParam( $type, $list );
1358    }
1359
1360    /**
1361     * Substitutes any parameters into the message text.
1362     *
1363     * @since 1.17
1364     *
1365     * @param string $message The message text.
1366     * @param string $type Either "before" or "after".
1367     * @param string $format One of the FORMAT_* constants.
1368     *
1369     * @return string
1370     */
1371    protected function replaceParameters( $message, $type, $format ) {
1372        // A temporary marker for $1 parameters that is only valid
1373        // in non-attribute contexts. However if the entire message is escaped
1374        // then we don't want to use it because it will be mangled in all contexts
1375        // and its unnecessary as ->escaped() messages aren't html.
1376        $marker = $format === self::FORMAT_ESCAPED ? '$' : '$\'"';
1377        $replacementKeys = [];
1378        foreach ( $this->parameters as $n => $param ) {
1379            [ $paramType, $value ] = $this->extractParam( $param, $format );
1380            if ( $type === 'before' ) {
1381                if ( $paramType === 'before' ) {
1382                    $replacementKeys['$' . ( $n + 1 )] = $value;
1383                } else /* $paramType === 'after' */ {
1384                    // To protect against XSS from replacing parameters
1385                    // inside html attributes, we convert $1 to $'"1.
1386                    // In the event that one of the parameters ends up
1387                    // in an attribute, either the ' or the " will be
1388                    // escaped, breaking the replacement and avoiding XSS.
1389                    $replacementKeys['$' . ( $n + 1 )] = $marker . ( $n + 1 );
1390                }
1391            } elseif ( $paramType === 'after' ) {
1392                $replacementKeys[$marker . ( $n + 1 )] = $value;
1393            }
1394        }
1395        return strtr( $message, $replacementKeys );
1396    }
1397
1398    /**
1399     * Extracts the parameter type and preprocessed the value if needed.
1400     *
1401     * @since 1.18
1402     *
1403     * @param ScalarParam|ListParam|MessageSpecifier|string $param Parameter as defined in this class.
1404     * @param string $format One of the FORMAT_* constants.
1405     *
1406     * @return array Array with the parameter type (either "before" or "after") and the value.
1407     */
1408    protected function extractParam( $param, $format ) {
1409        if ( $param instanceof ScalarParam ) {
1410            return match ( $param->getType() ) {
1411                ParamType::RAW =>
1412                    [ 'after', $this->extractParam( $param->getValue(), self::FORMAT_PARSE )[1] ],
1413                ParamType::NUM =>
1414                    // Replace number params always in before step for now.
1415                    // No support for combined raw and num params
1416                    [ 'before', $this->getLanguage()->formatNum( $param->getValue() ) ],
1417                ParamType::DURATION_LONG =>
1418                    [ 'before', $this->getLanguage()->formatDuration( $param->getValue() ) ],
1419                ParamType::EXPIRY =>
1420                    [ 'before', $this->getLanguage()->formatExpiry( $param->getValue() ) ],
1421                ParamType::DATETIME =>
1422                    [ 'before', $this->getLanguage()->timeanddate( $param->getValue() ) ],
1423                ParamType::DATE =>
1424                    [ 'before', $this->getLanguage()->date( $param->getValue() ) ],
1425                ParamType::TIME =>
1426                    [ 'before', $this->getLanguage()->time( $param->getValue() ) ],
1427                ParamType::GROUP =>
1428                    [ 'before', $this->getLanguage()->getGroupName( $param->getValue() ) ],
1429                ParamType::DURATION_SHORT =>
1430                    [ 'before', $this->getLanguage()->formatTimePeriod( $param->getValue() ) ],
1431                ParamType::SIZE =>
1432                    [ 'before', $this->getLanguage()->formatSize( $param->getValue() ) ],
1433                ParamType::BITRATE =>
1434                    [ 'before', $this->getLanguage()->formatBitrate( $param->getValue() ) ],
1435                ParamType::PLAINTEXT =>
1436                    [ 'after', $this->formatPlaintext( $param->getValue(), $format ) ],
1437                // ParamType::TEXT is impossible because we unwrapped it in params()
1438                default =>
1439                    throw new \LogicException( "Invalid ScalarParam type: {$param->getType()->value}" ),
1440            };
1441        } elseif ( $param instanceof ListParam ) {
1442            return $this->formatListParam( $param->getValue(), $param->getListType(), $format );
1443        } elseif ( is_array( $param ) ) {
1444            LoggerFactory::getInstance( 'Bug58676' )->warning(
1445                'Invalid parameter for message "{msgkey}": {param}',
1446                [
1447                    'exception' => new RuntimeException,
1448                    'msgkey' => $this->key,
1449                    'param' => htmlspecialchars( serialize( $param ) ),
1450                ]
1451            );
1452            return [ 'before', '[INVALID]' ];
1453        } elseif ( $param instanceof MessageSpecifier ) {
1454            // Match language, flags, etc. to the current message.
1455            $msg = static::newFromSpecifier( $param );
1456            if ( $msg->language !== $this->language || $msg->useDatabase !== $this->useDatabase ) {
1457                // Cache depends on these parameters
1458                $msg->message = null;
1459            }
1460            $msg->isInterface = $this->isInterface;
1461            $msg->language = $this->language;
1462            $msg->useDatabase = $this->useDatabase;
1463            $msg->contextPage = $this->contextPage;
1464
1465            // DWIM
1466            if ( $format === 'block-parse' ) {
1467                $format = 'parse';
1468            }
1469
1470            // Message objects should not be before parameters because
1471            // then they'll get double escaped. If the message needs to be
1472            // escaped, it'll happen right here when we call toString().
1473            // (Using the public toString() to allow mocking / subclassing.)
1474            return [ 'after', $msg->toString( $format ) ];
1475        } else {
1476            return [ 'before', $param ];
1477        }
1478    }
1479
1480    /**
1481     * Wrapper for what ever method we use to parse wikitext.
1482     *
1483     * @since 1.17
1484     *
1485     * @param string $string Wikitext message contents.
1486     *
1487     * @return ParserOutput Wikitext parsed into HTML.
1488     */
1489    protected function parseText( string $string ): ParserOutput {
1490        $out = MediaWikiServices::getInstance()->getMessageParser()->parse(
1491            $string,
1492            $this->contextPage ?? PageReferenceValue::localReference( NS_SPECIAL, 'Badtitle/Message' ),
1493            /*linestart*/ true,
1494            $this->isInterface,
1495            $this->getFetchedLanguage()
1496        );
1497
1498        return $out;
1499    }
1500
1501    /**
1502     * Wrapper for what ever method we use to {{-transform wikitext (substituting the
1503     * template with its parsed result).
1504     *
1505     * @since 1.17
1506     *
1507     * @param string $string Wikitext message contents.
1508     *
1509     * @return string Wikitext with {{-constructs substituted with its parsed result.
1510     */
1511    protected function transformText( $string ) {
1512        return MediaWikiServices::getInstance()->getMessageParser()->transform(
1513            $string,
1514            $this->isInterface,
1515            $this->getFetchedLanguage(),
1516            $this->contextPage
1517        );
1518    }
1519
1520    /**
1521     * Wrapper for whatever method we use to get message contents.
1522     *
1523     * @since 1.17
1524     *
1525     * @return string|false
1526     */
1527    protected function fetchMessage() {
1528        if ( $this->message === null ) {
1529            $cache = MediaWikiServices::getInstance()->getMessageCache();
1530
1531            $info = new MessageInfo;
1532            $langCode = $this->getLanguageCode();
1533            foreach ( $this->keysToTry as $key ) {
1534                $message = $cache->get( $key, $this->useDatabase, $this->getLanguageCode(), $info );
1535                if ( $message !== false && $message !== '' ) {
1536                    if ( $info->usedKey && $info->usedKey !== $key ) {
1537                        $this->overriddenKey = $info->usedKey;
1538                    }
1539                    if ( $info->langCode && $info->langCode !== $langCode ) {
1540                        $this->fetchedLangCode = $info->langCode;
1541                    }
1542                    break;
1543                }
1544            }
1545
1546            // NOTE: The constructor makes sure keysToTry isn't empty,
1547            //       so we know that $key and $message are initialized.
1548            // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable False positive
1549            // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty False positive
1550            $this->key = $key;
1551            // @phan-suppress-next-line PhanPossiblyUndeclaredVariable False positive
1552            $this->message = $message;
1553        }
1554        return $this->message;
1555    }
1556
1557    /**
1558     * Formats a message parameter wrapped with 'plaintext'. Ensures that
1559     * the entire string is displayed unchanged when displayed in the output
1560     * format.
1561     *
1562     * @since 1.25
1563     *
1564     * @param string $plaintext String to ensure plaintext output of
1565     * @param string $format One of the FORMAT_* constants.
1566     *
1567     * @return string Input plaintext encoded for output to $format
1568     */
1569    protected function formatPlaintext( $plaintext, $format ) {
1570        switch ( $format ) {
1571            case self::FORMAT_TEXT:
1572            case self::FORMAT_PLAIN:
1573                return $plaintext;
1574
1575            case self::FORMAT_PARSE:
1576            case self::FORMAT_BLOCK_PARSE:
1577            case self::FORMAT_ESCAPED:
1578            default:
1579                return Sanitizer::escapeCombiningChar( htmlspecialchars( $plaintext, ENT_QUOTES ) );
1580        }
1581    }
1582
1583    /**
1584     * Mapping from Message::listParam() types to Language methods.
1585     * @param Language $lang
1586     * @param ListType $listType
1587     * @return callable(array):string
1588     */
1589    protected static function listTypeMap( Language $lang, ListType $listType ): callable {
1590        return match ( $listType ) {
1591            ListType::COMMA => $lang->commaList( ... ),
1592            ListType::SEMICOLON => $lang->semicolonList( ... ),
1593            ListType::PIPE => $lang->pipeList( ... ),
1594            ListType::AND => $lang->listToText( ... ),
1595        };
1596    }
1597
1598    /**
1599     * Formats a list of parameters as a concatenated string.
1600     * @since 1.29
1601     * @param array $params
1602     * @param string|ListType $listType
1603     * @param string $format One of the FORMAT_* constants.
1604     * @return array Array with the parameter type (either "before" or "after") and the value.
1605     */
1606    protected function formatListParam( array $params, $listType, $format ) {
1607        if ( is_string( $listType ) ) {
1608            $originalListType = $listType;
1609            $listType = ListType::tryFrom( $listType );
1610            if ( $listType === null ) {
1611                $warning = 'Invalid list type for message "' . $this->key . '": '
1612                    . htmlspecialchars( $originalListType )
1613                    . ' (params are ' . htmlspecialchars( serialize( $params ) ) . ')';
1614                trigger_error( $warning, E_USER_WARNING );
1615                $e = new InvalidArgumentException;
1616                wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
1617                return [ 'before', '[INVALID]' ];
1618            }
1619        }
1620        $func = self::listTypeMap( $this->getLanguage(), $listType );
1621
1622        // Handle an empty list sensibly
1623        if ( !$params ) {
1624            return [ 'before', $func( [] ) ];
1625        }
1626
1627        // First, determine what kinds of list items we have
1628        $types = [];
1629        $vars = [];
1630        $list = [];
1631        foreach ( $params as $n => $p ) {
1632            if ( $p instanceof ScalarParam && $p->getType() === ParamType::TEXT ) {
1633                // Unwrap like in params()
1634                $p = $p->getValue();
1635            }
1636            [ $type, $value ] = $this->extractParam( $p, $format );
1637            $types[$type] = true;
1638            $list[] = $value;
1639            $vars[] = '$' . ( $n + 1 );
1640        }
1641
1642        // Easy case: all are 'before' or 'after', so just join the
1643        // values and use the same type.
1644        if ( count( $types ) === 1 ) {
1645            return [ key( $types ), $func( $list ) ];
1646        }
1647
1648        // Hard case: We need to process each value per its type, then
1649        // return the concatenated values as 'after'. We handle this by turning
1650        // the list into a RawMessage and processing that as a parameter.
1651        $vars = $func( $vars );
1652        return $this->extractParam( new RawMessage( $vars, $params ), $format );
1653    }
1654}
1655
1656/** @deprecated class alias since 1.42 */
1657class_alias( Message::class, 'Message' );