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