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