Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.39% |
312 / 393 |
|
53.62% |
37 / 69 |
CRAP | |
0.00% |
0 / 1 |
Message | |
79.59% |
312 / 392 |
|
53.62% |
37 / 69 |
539.99 | |
0.00% |
0 / 1 |
__construct | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
6.17 | |||
serialize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__serialize | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
unserialize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__unserialize | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
5.03 | |||
isMultiKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getKeysToTry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguage | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
newFromKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromSpecifier | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
5.09 | |||
newFallbackSequence | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
4.94 | |||
getTitle | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
params | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
7 | |||
rawParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
numParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
durationParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
expiryParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
dateTimeParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
dateParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
userGroupParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
objectParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
timeParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
timeperiodParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
sizeParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
bitrateParams | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
plaintextParams | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
setContext | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
inLanguage | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
10.02 | |||
inUserLanguage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
inContentLanguage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
setInterfaceMessageFlag | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
useDatabase | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
title | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
page | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
content | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
toString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
format | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
10 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parse | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
text | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
plain | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseAsBlock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
escaped | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
exists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isBlank | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isDisabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
rawParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
numParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
durationParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
expiryParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
dateTimeParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
dateParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
timeParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
userGroupParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
objectParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
timeperiodParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sizeParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
bitrateParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
plaintextParam | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
listParam | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
2.86 | |||
replaceParameters | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
extractParam | |
82.14% |
46 / 56 |
|
0.00% |
0 / 1 |
23.51 | |||
parseText | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
transformText | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
fetchMessage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
formatPlaintext | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
7 | |||
formatListParam | |
69.57% |
16 / 23 |
|
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 | |
22 | namespace MediaWiki\Message; |
23 | |
24 | use Content; |
25 | use InvalidArgumentException; |
26 | use Language; |
27 | use MediaWiki\Context\IContextSource; |
28 | use MediaWiki\Context\RequestContext; |
29 | use MediaWiki\Language\RawMessage; |
30 | use MediaWiki\Logger\LoggerFactory; |
31 | use MediaWiki\MainConfigNames; |
32 | use MediaWiki\MediaWikiServices; |
33 | use MediaWiki\Page\PageReference; |
34 | use MediaWiki\Page\PageReferenceValue; |
35 | use MediaWiki\Parser\Parser; |
36 | use MediaWiki\Parser\ParserOutput; |
37 | use MediaWiki\StubObject\StubUserLang; |
38 | use MediaWiki\Title\Title; |
39 | use MessageContent; |
40 | use MessageSpecifier; |
41 | use RuntimeException; |
42 | use Serializable; |
43 | use Stringable; |
44 | use Wikimedia\Assert\Assert; |
45 | use 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 | */ |
158 | class 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 */ |
1631 | class_alias( Message::class, 'Message' ); |