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