Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/Message.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Fetching and processing of interface messages.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author Niklas Laxström
22
 */
23
24
/**
25
 * The Message class provides methods which fulfil two basic services:
26
 *  - fetching interface messages
27
 *  - processing messages into a variety of formats
28
 *
29
 * First implemented with MediaWiki 1.17, the Message class is intended to
30
 * replace the old wfMsg* functions that over time grew unusable.
31
 * @see https://www.mediawiki.org/wiki/Manual:Messages_API for equivalences
32
 * between old and new functions.
33
 *
34
 * You should use the wfMessage() global function which acts as a wrapper for
35
 * the Message class. The wrapper let you pass parameters as arguments.
36
 *
37
 * The most basic usage cases would be:
38
 *
39
 * @code
40
 *     // Initialize a Message object using the 'some_key' message key
41
 *     $message = wfMessage( 'some_key' );
42
 *
43
 *     // Using two parameters those values are strings 'value1' and 'value2':
44
 *     $message = wfMessage( 'some_key',
45
 *          'value1', 'value2'
46
 *     );
47
 * @endcode
48
 *
49
 * @section message_global_fn Global function wrapper:
50
 *
51
 * Since wfMessage() returns a Message instance, you can chain its call with
52
 * a method. Some of them return a Message instance too so you can chain them.
53
 * You will find below several examples of wfMessage() usage.
54
 *
55
 * Fetching a message text for interface message:
56
 *
57
 * @code
58
 *    $button = Xml::button(
59
 *         wfMessage( 'submit' )->text()
60
 *    );
61
 * @endcode
62
 *
63
 * A Message instance can be passed parameters after it has been constructed,
64
 * use the params() method to do so:
65
 *
66
 * @code
67
 *     wfMessage( 'welcome-to' )
68
 *         ->params( $wgSitename )
69
 *         ->text();
70
 * @endcode
71
 *
72
 * {{GRAMMAR}} and friends work correctly:
73
 *
74
 * @code
75
 *    wfMessage( 'are-friends',
76
 *        $user, $friend
77
 *    );
78
 *    wfMessage( 'bad-message' )
79
 *         ->rawParams( '<script>...</script>' )
80
 *         ->escaped();
81
 * @endcode
82
 *
83
 * @section message_language Changing language:
84
 *
85
 * Messages can be requested in a different language or in whatever current
86
 * content language is being used. The methods are:
87
 *     - Message->inContentLanguage()
88
 *     - Message->inLanguage()
89
 *
90
 * Sometimes the message text ends up in the database, so content language is
91
 * needed:
92
 *
93
 * @code
94
 *    wfMessage( 'file-log',
95
 *        $user, $filename
96
 *    )->inContentLanguage()->text();
97
 * @endcode
98
 *
99
 * Checking whether a message exists:
100
 *
101
 * @code
102
 *    wfMessage( 'mysterious-message' )->exists()
103
 *    // returns a boolean whether the 'mysterious-message' key exist.
104
 * @endcode
105
 *
106
 * If you want to use a different language:
107
 *
108
 * @code
109
 *    $userLanguage = $user->getOption( 'language' );
110
 *    wfMessage( 'email-header' )
111
 *         ->inLanguage( $userLanguage )
112
 *         ->plain();
113
 * @endcode
114
 *
115
 * @note You can parse the text only in the content or interface languages
116
 *
117
 * @section message_compare_old Comparison with old wfMsg* functions:
118
 *
119
 * Use full parsing:
120
 *
121
 * @code
122
 *     // old style:
123
 *     wfMsgExt( 'key', [ 'parseinline' ], 'apple' );
124
 *     // new style:
125
 *     wfMessage( 'key', 'apple' )->parse();
126
 * @endcode
127
 *
128
 * Parseinline is used because it is more useful when pre-building HTML.
129
 * In normal use it is better to use OutputPage::(add|wrap)WikiMsg.
130
 *
131
 * Places where HTML cannot be used. {{-transformation is done.
132
 * @code
133
 *     // old style:
134
 *     wfMsgExt( 'key', [ 'parsemag' ], 'apple', 'pear' );
135
 *     // new style:
136
 *     wfMessage( 'key', 'apple', 'pear' )->text();
137
 * @endcode
138
 *
139
 * Shortcut for escaping the message too, similar to wfMsgHTML(), but
140
 * parameters are not replaced after escaping by default.
141
 * @code
142
 *     $escaped = wfMessage( 'key' )
143
 *          ->rawParams( 'apple' )
144
 *          ->escaped();
145
 * @endcode
146
 *
147
 * @section message_appendix Appendix:
148
 *
149
 * @todo
150
 * - test, can we have tests?
151
 * - this documentation needs to be extended
152
 *
153
 * @see https://www.mediawiki.org/wiki/WfMessage()
154
 * @see https://www.mediawiki.org/wiki/New_messages_API
155
 * @see https://www.mediawiki.org/wiki/Localisation
156
 *
157
 * @since 1.17
158
 */
159
class Message implements MessageSpecifier, Serializable {
160
	/** Use message text as-is */
161
	const FORMAT_PLAIN = 'plain';
162
	/** Use normal wikitext -> HTML parsing (the result will be wrapped in a block-level HTML tag) */
163
	const FORMAT_BLOCK_PARSE = 'block-parse';
164
	/** Use normal wikitext -> HTML parsing but strip the block-level wrapper */
165
	const FORMAT_PARSE = 'parse';
166
	/** Transform {{..}} constructs but don't transform to HTML */
167
	const FORMAT_TEXT = 'text';
168
	/** Transform {{..}} constructs, HTML-escape the result */
169
	const FORMAT_ESCAPED = 'escaped';
170
171
	/**
172
	 * In which language to get this message. True, which is the default,
173
	 * means the current user language, false content language.
174
	 *
175
	 * @var bool
176
	 */
177
	protected $interface = true;
178
179
	/**
180
	 * In which language to get this message. Overrides the $interface setting.
181
	 *
182
	 * @var Language|bool Explicit language object, or false for user language
183
	 */
184
	protected $language = false;
185
186
	/**
187
	 * @var string The message key. If $keysToTry has more than one element,
188
	 * this may change to one of the keys to try when fetching the message text.
189
	 */
190
	protected $key;
191
192
	/**
193
	 * @var string[] List of keys to try when fetching the message.
194
	 */
195
	protected $keysToTry;
196
197
	/**
198
	 * @var array List of parameters which will be substituted into the message.
199
	 */
200
	protected $parameters = [];
201
202
	/**
203
	 * @var string
204
	 * @deprecated
205
	 */
206
	protected $format = 'parse';
207
208
	/**
209
	 * @var bool Whether database can be used.
210
	 */
211
	protected $useDatabase = true;
212
213
	/**
214
	 * @var Title Title object to use as context.
215
	 */
216
	protected $title = null;
217
218
	/**
219
	 * @var Content Content object representing the message.
220
	 */
221
	protected $content = null;
222
223
	/**
224
	 * @var string
225
	 */
226
	protected $message;
227
228
	/**
229
	 * @since 1.17
230
	 * @param string|string[]|MessageSpecifier $key Message key, or array of
231
	 * message keys to try and use the first non-empty message for, or a
232
	 * MessageSpecifier to copy from.
233
	 * @param array $params Message parameters.
234
	 * @param Language $language [optional] Language to use (defaults to current user language).
235
	 * @throws InvalidArgumentException
236
	 */
237
	public function __construct( $key, $params = [], Language $language = null ) {
238
		if ( $key instanceof MessageSpecifier ) {
239
			if ( $params ) {
240
				throw new InvalidArgumentException(
241
					'$params must be empty if $key is a MessageSpecifier'
242
				);
243
			}
244
			$params = $key->getParams();
245
			$key = $key->getKey();
246
		}
247
248
		if ( !is_string( $key ) && !is_array( $key ) ) {
249
			throw new InvalidArgumentException( '$key must be a string or an array' );
250
		}
251
252
		$this->keysToTry = (array)$key;
253
254
		if ( empty( $this->keysToTry ) ) {
255
			throw new InvalidArgumentException( '$key must not be an empty list' );
256
		}
257
258
		$this->key = reset( $this->keysToTry );
259
260
		$this->parameters = array_values( $params );
261
		// User language is only resolved in getLanguage(). This helps preserve the
262
		// semantic intent of "user language" across serialize() and unserialize().
263
		$this->language = $language ?: false;
264
	}
265
266
	/**
267
	 * @see Serializable::serialize()
268
	 * @since 1.26
269
	 * @return string
270
	 */
271
	public function serialize() {
272
		return serialize( [
273
			'interface' => $this->interface,
274
			'language' => $this->language ? $this->language->getCode() : false,
275
			'key' => $this->key,
276
			'keysToTry' => $this->keysToTry,
277
			'parameters' => $this->parameters,
278
			'format' => $this->format,
279
			'useDatabase' => $this->useDatabase,
280
			'title' => $this->title,
281
		] );
282
	}
283
284
	/**
285
	 * @see Serializable::unserialize()
286
	 * @since 1.26
287
	 * @param string $serialized
288
	 */
289
	public function unserialize( $serialized ) {
290
		$data = unserialize( $serialized );
291
		$this->interface = $data['interface'];
292
		$this->key = $data['key'];
293
		$this->keysToTry = $data['keysToTry'];
294
		$this->parameters = $data['parameters'];
295
		$this->format = $data['format'];
296
		$this->useDatabase = $data['useDatabase'];
297
		$this->language = $data['language'] ? Language::factory( $data['language'] ) : false;
298
		$this->title = $data['title'];
299
	}
300
301
	/**
302
	 * @since 1.24
303
	 *
304
	 * @return bool True if this is a multi-key message, that is, if the key provided to the
305
	 * constructor was a fallback list of keys to try.
306
	 */
307
	public function isMultiKey() {
308
		return count( $this->keysToTry ) > 1;
309
	}
310
311
	/**
312
	 * @since 1.24
313
	 *
314
	 * @return string[] The list of keys to try when fetching the message text,
315
	 * in order of preference.
316
	 */
317
	public function getKeysToTry() {
318
		return $this->keysToTry;
319
	}
320
321
	/**
322
	 * Returns the message key.
323
	 *
324
	 * If a list of multiple possible keys was supplied to the constructor, this method may
325
	 * return any of these keys. After the message has been fetched, this method will return
326
	 * the key that was actually used to fetch the message.
327
	 *
328
	 * @since 1.21
329
	 *
330
	 * @return string
331
	 */
332
	public function getKey() {
333
		return $this->key;
334
	}
335
336
	/**
337
	 * Returns the message parameters.
338
	 *
339
	 * @since 1.21
340
	 *
341
	 * @return array
342
	 */
343
	public function getParams() {
344
		return $this->parameters;
345
	}
346
347
	/**
348
	 * Returns the message format.
349
	 *
350
	 * @since 1.21
351
	 *
352
	 * @return string
353
	 * @deprecated since 1.29 formatting is not stateful
354
	 */
355
	public function getFormat() {
356
		wfDeprecated( __METHOD__, '1.29' );
357
		return $this->format;
358
	}
359
360
	/**
361
	 * Returns the Language of the Message.
362
	 *
363
	 * @since 1.23
364
	 *
365
	 * @return Language
366
	 */
367
	public function getLanguage() {
368
		// Defaults to false which means current user language
369
		return $this->language ?: RequestContext::getMain()->getLanguage();
370
	}
371
372
	/**
373
	 * Factory function that is just wrapper for the real constructor. It is
374
	 * intended to be used instead of the real constructor, because it allows
375
	 * chaining method calls, while new objects don't.
376
	 *
377
	 * @since 1.17
378
	 *
379
	 * @param string|string[]|MessageSpecifier $key
380
	 * @param mixed $param,... Parameters as strings.
381
	 *
382
	 * @return Message
383
	 */
384
	public static function newFromKey( $key /*...*/ ) {
385
		$params = func_get_args();
386
		array_shift( $params );
387
		return new self( $key, $params );
388
	}
389
390
	/**
391
	 * Transform a MessageSpecifier or a primitive value used interchangeably with
392
	 * specifiers (a message key string, or a key + params array) into a proper Message.
393
	 *
394
	 * Also accepts a MessageSpecifier inside an array: that's not considered a valid format
395
	 * but is an easy error to make due to how StatusValue stores messages internally.
396
	 * Further array elements are ignored in that case.
397
	 *
398
	 * @param string|array|MessageSpecifier $value
399
	 * @return Message
400
	 * @throws InvalidArgumentException
401
	 * @since 1.27
402
	 */
403
	public static function newFromSpecifier( $value ) {
404
		$params = [];
405
		if ( is_array( $value ) ) {
406
			$params = $value;
407
			$value = array_shift( $params );
408
		}
409
410
		if ( $value instanceof Message ) { // Message, RawMessage, ApiMessage, etc
411
			$message = clone( $value );
412
		} elseif ( $value instanceof MessageSpecifier ) {
413
			$message = new Message( $value );
414
		} elseif ( is_string( $value ) ) {
415
			$message = new Message( $value, $params );
416
		} else {
417
			throw new InvalidArgumentException( __METHOD__ . ': invalid argument type '
418
				. gettype( $value ) );
419
		}
420
421
		return $message;
422
	}
423
424
	/**
425
	 * Factory function accepting multiple message keys and returning a message instance
426
	 * for the first message which is non-empty. If all messages are empty then an
427
	 * instance of the first message key is returned.
428
	 *
429
	 * @since 1.18
430
	 *
431
	 * @param string|string[] $keys,... Message keys, or first argument as an array of all the
432
	 * message keys.
433
	 *
434
	 * @return Message
435
	 */
436
	public static function newFallbackSequence( /*...*/ ) {
437
		$keys = func_get_args();
438
		if ( func_num_args() == 1 ) {
439
			if ( is_array( $keys[0] ) ) {
440
				// Allow an array to be passed as the first argument instead
441
				$keys = array_values( $keys[0] );
442
			} else {
443
				// Optimize a single string to not need special fallback handling
444
				$keys = $keys[0];
445
			}
446
		}
447
		return new self( $keys );
448
	}
449
450
	/**
451
	 * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace.
452
	 * The title will be for the current language, if the message key is in
453
	 * $wgForceUIMsgAsContentMsg it will be append with the language code (except content
454
	 * language), because Message::inContentLanguage will also return in user language.
455
	 *
456
	 * @see $wgForceUIMsgAsContentMsg
457
	 * @return Title
458
	 * @since 1.26
459
	 */
460
	public function getTitle() {
461
		global $wgContLang, $wgForceUIMsgAsContentMsg;
462
463
		$title = $this->key;
464
		if (
465
			!$this->language->equals( $wgContLang )
466
			&& in_array( $this->key, (array)$wgForceUIMsgAsContentMsg )
467
		) {
468
			$code = $this->language->getCode();
469
			$title .= '/' . $code;
470
		}
471
472
		return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( strtr( $title, ' ', '_' ) ) );
473
	}
474
475
	/**
476
	 * Adds parameters to the parameter list of this message.
477
	 *
478
	 * @since 1.17
479
	 *
480
	 * @param mixed ... Parameters as strings, or a single argument that is
481
	 * an array of strings.
482
	 *
483
	 * @return Message $this
484
	 */
485
	public function params( /*...*/ ) {
486
		$args = func_get_args();
487 View Code Duplication
		if ( isset( $args[0] ) && is_array( $args[0] ) ) {
488
			$args = $args[0];
489
		}
490
		$args_values = array_values( $args );
491
		$this->parameters = array_merge( $this->parameters, $args_values );
492
		return $this;
493
	}
494
495
	/**
496
	 * Add parameters that are substituted after parsing or escaping.
497
	 * In other words the parsing process cannot access the contents
498
	 * of this type of parameter, and you need to make sure it is
499
	 * sanitized beforehand.  The parser will see "$n", instead.
500
	 *
501
	 * @since 1.17
502
	 *
503
	 * @param mixed $params,... Raw parameters as strings, or a single argument that is
504
	 * an array of raw parameters.
505
	 *
506
	 * @return Message $this
507
	 */
508 View Code Duplication
	public function rawParams( /*...*/ ) {
509
		$params = func_get_args();
510
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
511
			$params = $params[0];
512
		}
513
		foreach ( $params as $param ) {
514
			$this->parameters[] = self::rawParam( $param );
515
		}
516
		return $this;
517
	}
518
519
	/**
520
	 * Add parameters that are numeric and will be passed through
521
	 * Language::formatNum before substitution
522
	 *
523
	 * @since 1.18
524
	 *
525
	 * @param mixed $param,... Numeric parameters, or a single argument that is
526
	 * an array of numeric parameters.
527
	 *
528
	 * @return Message $this
529
	 */
530 View Code Duplication
	public function numParams( /*...*/ ) {
531
		$params = func_get_args();
532
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
533
			$params = $params[0];
534
		}
535
		foreach ( $params as $param ) {
536
			$this->parameters[] = self::numParam( $param );
537
		}
538
		return $this;
539
	}
540
541
	/**
542
	 * Add parameters that are durations of time and will be passed through
543
	 * Language::formatDuration before substitution
544
	 *
545
	 * @since 1.22
546
	 *
547
	 * @param int|int[] $param,... Duration parameters, or a single argument that is
548
	 * an array of duration parameters.
549
	 *
550
	 * @return Message $this
551
	 */
552 View Code Duplication
	public function durationParams( /*...*/ ) {
553
		$params = func_get_args();
554
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
555
			$params = $params[0];
556
		}
557
		foreach ( $params as $param ) {
558
			$this->parameters[] = self::durationParam( $param );
559
		}
560
		return $this;
561
	}
562
563
	/**
564
	 * Add parameters that are expiration times and will be passed through
565
	 * Language::formatExpiry before substitution
566
	 *
567
	 * @since 1.22
568
	 *
569
	 * @param string|string[] $param,... Expiry parameters, or a single argument that is
570
	 * an array of expiry parameters.
571
	 *
572
	 * @return Message $this
573
	 */
574 View Code Duplication
	public function expiryParams( /*...*/ ) {
575
		$params = func_get_args();
576
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
577
			$params = $params[0];
578
		}
579
		foreach ( $params as $param ) {
580
			$this->parameters[] = self::expiryParam( $param );
581
		}
582
		return $this;
583
	}
584
585
	/**
586
	 * Add parameters that are time periods and will be passed through
587
	 * Language::formatTimePeriod before substitution
588
	 *
589
	 * @since 1.22
590
	 *
591
	 * @param int|int[] $param,... Time period parameters, or a single argument that is
592
	 * an array of time period parameters.
593
	 *
594
	 * @return Message $this
595
	 */
596 View Code Duplication
	public function timeperiodParams( /*...*/ ) {
597
		$params = func_get_args();
598
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
599
			$params = $params[0];
600
		}
601
		foreach ( $params as $param ) {
602
			$this->parameters[] = self::timeperiodParam( $param );
603
		}
604
		return $this;
605
	}
606
607
	/**
608
	 * Add parameters that are file sizes and will be passed through
609
	 * Language::formatSize before substitution
610
	 *
611
	 * @since 1.22
612
	 *
613
	 * @param int|int[] $param,... Size parameters, or a single argument that is
614
	 * an array of size parameters.
615
	 *
616
	 * @return Message $this
617
	 */
618 View Code Duplication
	public function sizeParams( /*...*/ ) {
619
		$params = func_get_args();
620
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
621
			$params = $params[0];
622
		}
623
		foreach ( $params as $param ) {
624
			$this->parameters[] = self::sizeParam( $param );
625
		}
626
		return $this;
627
	}
628
629
	/**
630
	 * Add parameters that are bitrates and will be passed through
631
	 * Language::formatBitrate before substitution
632
	 *
633
	 * @since 1.22
634
	 *
635
	 * @param int|int[] $param,... Bit rate parameters, or a single argument that is
636
	 * an array of bit rate parameters.
637
	 *
638
	 * @return Message $this
639
	 */
640 View Code Duplication
	public function bitrateParams( /*...*/ ) {
641
		$params = func_get_args();
642
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
643
			$params = $params[0];
644
		}
645
		foreach ( $params as $param ) {
646
			$this->parameters[] = self::bitrateParam( $param );
647
		}
648
		return $this;
649
	}
650
651
	/**
652
	 * Add parameters that are plaintext and will be passed through without
653
	 * the content being evaluated.  Plaintext parameters are not valid as
654
	 * arguments to parser functions. This differs from self::rawParams in
655
	 * that the Message class handles escaping to match the output format.
656
	 *
657
	 * @since 1.25
658
	 *
659
	 * @param string|string[] $param,... plaintext parameters, or a single argument that is
0 ignored issues
show
There is no parameter named $param,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
660
	 * an array of plaintext parameters.
661
	 *
662
	 * @return Message $this
663
	 */
664 View Code Duplication
	public function plaintextParams( /*...*/ ) {
665
		$params = func_get_args();
666
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
667
			$params = $params[0];
668
		}
669
		foreach ( $params as $param ) {
670
			$this->parameters[] = self::plaintextParam( $param );
671
		}
672
		return $this;
673
	}
674
675
	/**
676
	 * Set the language and the title from a context object
677
	 *
678
	 * @since 1.19
679
	 *
680
	 * @param IContextSource $context
681
	 *
682
	 * @return Message $this
683
	 */
684
	public function setContext( IContextSource $context ) {
685
		$this->inLanguage( $context->getLanguage() );
686
		$this->title( $context->getTitle() );
687
		$this->interface = true;
688
689
		return $this;
690
	}
691
692
	/**
693
	 * Request the message in any language that is supported.
694
	 *
695
	 * As a side effect interface message status is unconditionally
696
	 * turned off.
697
	 *
698
	 * @since 1.17
699
	 * @param Language|string $lang Language code or Language object.
700
	 * @return Message $this
701
	 * @throws MWException
702
	 */
703
	public function inLanguage( $lang ) {
704
		if ( $lang instanceof Language ) {
705
			$this->language = $lang;
706
		} elseif ( is_string( $lang ) ) {
707
			if ( !$this->language instanceof Language || $this->language->getCode() != $lang ) {
708
				$this->language = Language::factory( $lang );
709
			}
710
		} elseif ( $lang instanceof StubUserLang ) {
711
			$this->language = false;
712
		} else {
713
			$type = gettype( $lang );
714
			throw new MWException( __METHOD__ . " must be "
715
				. "passed a String or Language object; $type given"
716
			);
717
		}
718
		$this->message = null;
719
		$this->interface = false;
720
		return $this;
721
	}
722
723
	/**
724
	 * Request the message in the wiki's content language,
725
	 * unless it is disabled for this message.
726
	 *
727
	 * @since 1.17
728
	 * @see $wgForceUIMsgAsContentMsg
729
	 *
730
	 * @return Message $this
731
	 */
732
	public function inContentLanguage() {
733
		global $wgForceUIMsgAsContentMsg;
734
		if ( in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) ) {
735
			return $this;
736
		}
737
738
		global $wgContLang;
739
		$this->inLanguage( $wgContLang );
740
		return $this;
741
	}
742
743
	/**
744
	 * Allows manipulating the interface message flag directly.
745
	 * Can be used to restore the flag after setting a language.
746
	 *
747
	 * @since 1.20
748
	 *
749
	 * @param bool $interface
750
	 *
751
	 * @return Message $this
752
	 */
753
	public function setInterfaceMessageFlag( $interface ) {
754
		$this->interface = (bool)$interface;
755
		return $this;
756
	}
757
758
	/**
759
	 * Enable or disable database use.
760
	 *
761
	 * @since 1.17
762
	 *
763
	 * @param bool $useDatabase
764
	 *
765
	 * @return Message $this
766
	 */
767
	public function useDatabase( $useDatabase ) {
768
		$this->useDatabase = (bool)$useDatabase;
769
		return $this;
770
	}
771
772
	/**
773
	 * Set the Title object to use as context when transforming the message
774
	 *
775
	 * @since 1.18
776
	 *
777
	 * @param Title $title
778
	 *
779
	 * @return Message $this
780
	 */
781
	public function title( $title ) {
782
		$this->title = $title;
783
		return $this;
784
	}
785
786
	/**
787
	 * Returns the message as a Content object.
788
	 *
789
	 * @return Content
790
	 */
791
	public function content() {
792
		if ( !$this->content ) {
793
			$this->content = new MessageContent( $this );
794
		}
795
796
		return $this->content;
797
	}
798
799
	/**
800
	 * Returns the message parsed from wikitext to HTML.
801
	 *
802
	 * @since 1.17
803
	 *
804
	 * @param string|null $format One of the FORMAT_* constants. Null means use whatever was used
805
	 *   the last time (this is for B/C and should be avoided).
806
	 *
807
	 * @return string HTML
808
	 */
809
	public function toString( $format = null ) {
810
		if ( $format === null ) {
811
			$ex = new LogicException( __METHOD__ . ' using implicit format: ' . $this->format );
812
			\MediaWiki\Logger\LoggerFactory::getInstance( 'message-format' )->warning(
813
				$ex->getMessage(), [ 'exception' => $ex, 'format' => $this->format, 'key' => $this->key ] );
814
			$format = $this->format;
815
		}
816
		$string = $this->fetchMessage();
817
818
		if ( $string === false ) {
819
			// Err on the side of safety, ensure that the output
820
			// is always html safe in the event the message key is
821
			// missing, since in that case its highly likely the
822
			// message key is user-controlled.
823
			// 'â§¼' is used instead of '<' to side-step any
824
			// double-escaping issues.
825
			return 'â§¼' . htmlspecialchars( $this->key ) . 'â§½';
826
		}
827
828
		# Replace $* with a list of parameters for &uselang=qqx.
829
		if ( strpos( $string, '$*' ) !== false ) {
830
			$paramlist = '';
831
			if ( $this->parameters !== [] ) {
832
				$paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
833
			}
834
			$string = str_replace( '$*', $paramlist, $string );
835
		}
836
837
		# Replace parameters before text parsing
838
		$string = $this->replaceParameters( $string, 'before', $format );
839
840
		# Maybe transform using the full parser
841
		if ( $format === self::FORMAT_PARSE ) {
842
			$string = $this->parseText( $string );
843
			$string = Parser::stripOuterParagraph( $string );
844
		} elseif ( $format === self::FORMAT_BLOCK_PARSE ) {
845
			$string = $this->parseText( $string );
846
		} elseif ( $format === self::FORMAT_TEXT ) {
847
			$string = $this->transformText( $string );
848
		} elseif ( $format === self::FORMAT_ESCAPED ) {
849
			$string = $this->transformText( $string );
850
			$string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
851
		}
852
853
		# Raw parameter replacement
854
		$string = $this->replaceParameters( $string, 'after', $format );
855
856
		return $string;
857
	}
858
859
	/**
860
	 * Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
861
	 *     $foo = new Message( $key );
862
	 *     $string = "<abbr>$foo</abbr>";
863
	 *
864
	 * @since 1.18
865
	 *
866
	 * @return string
867
	 */
868
	public function __toString() {
869
		// PHP doesn't allow __toString to throw exceptions and will
870
		// trigger a fatal error if it does. So, catch any exceptions.
871
872
		try {
873
			return $this->toString( self::FORMAT_PARSE );
874
		} catch ( Exception $ex ) {
875
			try {
876
				trigger_error( "Exception caught in " . __METHOD__ . " (message " . $this->key . "): "
877
					. $ex, E_USER_WARNING );
878
			} catch ( Exception $ex ) {
879
				// Doh! Cause a fatal error after all?
880
			}
881
882
			return 'â§¼' . htmlspecialchars( $this->key ) . 'â§½';
883
		}
884
	}
885
886
	/**
887
	 * Fully parse the text from wikitext to HTML.
888
	 *
889
	 * @since 1.17
890
	 *
891
	 * @return string Parsed HTML.
892
	 */
893
	public function parse() {
894
		$this->format = self::FORMAT_PARSE;
895
		return $this->toString( self::FORMAT_PARSE );
896
	}
897
898
	/**
899
	 * Returns the message text. {{-transformation is done.
900
	 *
901
	 * @since 1.17
902
	 *
903
	 * @return string Unescaped message text.
904
	 */
905
	public function text() {
906
		$this->format = self::FORMAT_TEXT;
907
		return $this->toString( self::FORMAT_TEXT );
908
	}
909
910
	/**
911
	 * Returns the message text as-is, only parameters are substituted.
912
	 *
913
	 * @since 1.17
914
	 *
915
	 * @return string Unescaped untransformed message text.
916
	 */
917
	public function plain() {
918
		$this->format = self::FORMAT_PLAIN;
919
		return $this->toString( self::FORMAT_PLAIN );
920
	}
921
922
	/**
923
	 * Returns the parsed message text which is always surrounded by a block element.
924
	 *
925
	 * @since 1.17
926
	 *
927
	 * @return string HTML
928
	 */
929
	public function parseAsBlock() {
930
		$this->format = self::FORMAT_BLOCK_PARSE;
931
		return $this->toString( self::FORMAT_BLOCK_PARSE );
932
	}
933
934
	/**
935
	 * Returns the message text. {{-transformation is done and the result
936
	 * is escaped excluding any raw parameters.
937
	 *
938
	 * @since 1.17
939
	 *
940
	 * @return string Escaped message text.
941
	 */
942
	public function escaped() {
943
		$this->format = self::FORMAT_ESCAPED;
944
		return $this->toString( self::FORMAT_ESCAPED );
945
	}
946
947
	/**
948
	 * Check whether a message key has been defined currently.
949
	 *
950
	 * @since 1.17
951
	 *
952
	 * @return bool
953
	 */
954
	public function exists() {
955
		return $this->fetchMessage() !== false;
956
	}
957
958
	/**
959
	 * Check whether a message does not exist, or is an empty string
960
	 *
961
	 * @since 1.18
962
	 * @todo FIXME: Merge with isDisabled()?
963
	 *
964
	 * @return bool
965
	 */
966
	public function isBlank() {
967
		$message = $this->fetchMessage();
968
		return $message === false || $message === '';
969
	}
970
971
	/**
972
	 * Check whether a message does not exist, is an empty string, or is "-".
973
	 *
974
	 * @since 1.18
975
	 *
976
	 * @return bool
977
	 */
978
	public function isDisabled() {
979
		$message = $this->fetchMessage();
980
		return $message === false || $message === '' || $message === '-';
981
	}
982
983
	/**
984
	 * @since 1.17
985
	 *
986
	 * @param mixed $raw
987
	 *
988
	 * @return array Array with a single "raw" key.
989
	 */
990
	public static function rawParam( $raw ) {
991
		return [ 'raw' => $raw ];
992
	}
993
994
	/**
995
	 * @since 1.18
996
	 *
997
	 * @param mixed $num
998
	 *
999
	 * @return array Array with a single "num" key.
1000
	 */
1001
	public static function numParam( $num ) {
1002
		return [ 'num' => $num ];
1003
	}
1004
1005
	/**
1006
	 * @since 1.22
1007
	 *
1008
	 * @param int $duration
1009
	 *
1010
	 * @return int[] Array with a single "duration" key.
1011
	 */
1012
	public static function durationParam( $duration ) {
1013
		return [ 'duration' => $duration ];
1014
	}
1015
1016
	/**
1017
	 * @since 1.22
1018
	 *
1019
	 * @param string $expiry
1020
	 *
1021
	 * @return string[] Array with a single "expiry" key.
1022
	 */
1023
	public static function expiryParam( $expiry ) {
1024
		return [ 'expiry' => $expiry ];
1025
	}
1026
1027
	/**
1028
	 * @since 1.22
1029
	 *
1030
	 * @param number $period
1031
	 *
1032
	 * @return number[] Array with a single "period" key.
1033
	 */
1034
	public static function timeperiodParam( $period ) {
1035
		return [ 'period' => $period ];
1036
	}
1037
1038
	/**
1039
	 * @since 1.22
1040
	 *
1041
	 * @param int $size
1042
	 *
1043
	 * @return int[] Array with a single "size" key.
1044
	 */
1045
	public static function sizeParam( $size ) {
1046
		return [ 'size' => $size ];
1047
	}
1048
1049
	/**
1050
	 * @since 1.22
1051
	 *
1052
	 * @param int $bitrate
1053
	 *
1054
	 * @return int[] Array with a single "bitrate" key.
1055
	 */
1056
	public static function bitrateParam( $bitrate ) {
1057
		return [ 'bitrate' => $bitrate ];
1058
	}
1059
1060
	/**
1061
	 * @since 1.25
1062
	 *
1063
	 * @param string $plaintext
1064
	 *
1065
	 * @return string[] Array with a single "plaintext" key.
1066
	 */
1067
	public static function plaintextParam( $plaintext ) {
1068
		return [ 'plaintext' => $plaintext ];
1069
	}
1070
1071
	/**
1072
	 * Substitutes any parameters into the message text.
1073
	 *
1074
	 * @since 1.17
1075
	 *
1076
	 * @param string $message The message text.
1077
	 * @param string $type Either "before" or "after".
1078
	 * @param string $format One of the FORMAT_* constants.
1079
	 *
1080
	 * @return string
1081
	 */
1082
	protected function replaceParameters( $message, $type = 'before', $format ) {
1083
		$replacementKeys = [];
1084
		foreach ( $this->parameters as $n => $param ) {
1085
			list( $paramType, $value ) = $this->extractParam( $param, $format );
1086
			if ( $type === $paramType ) {
1087
				$replacementKeys['$' . ( $n + 1 )] = $value;
1088
			}
1089
		}
1090
		$message = strtr( $message, $replacementKeys );
1091
		return $message;
1092
	}
1093
1094
	/**
1095
	 * Extracts the parameter type and preprocessed the value if needed.
1096
	 *
1097
	 * @since 1.18
1098
	 *
1099
	 * @param mixed $param Parameter as defined in this class.
1100
	 * @param string $format One of the FORMAT_* constants.
1101
	 *
1102
	 * @return array Array with the parameter type (either "before" or "after") and the value.
1103
	 */
1104
	protected function extractParam( $param, $format ) {
1105
		if ( is_array( $param ) ) {
1106
			if ( isset( $param['raw'] ) ) {
1107
				return [ 'after', $param['raw'] ];
1108 View Code Duplication
			} elseif ( isset( $param['num'] ) ) {
1109
				// Replace number params always in before step for now.
1110
				// No support for combined raw and num params
1111
				return [ 'before', $this->getLanguage()->formatNum( $param['num'] ) ];
1112
			} elseif ( isset( $param['duration'] ) ) {
1113
				return [ 'before', $this->getLanguage()->formatDuration( $param['duration'] ) ];
1114
			} elseif ( isset( $param['expiry'] ) ) {
1115
				return [ 'before', $this->getLanguage()->formatExpiry( $param['expiry'] ) ];
1116
			} elseif ( isset( $param['period'] ) ) {
1117
				return [ 'before', $this->getLanguage()->formatTimePeriod( $param['period'] ) ];
1118 View Code Duplication
			} elseif ( isset( $param['size'] ) ) {
1119
				return [ 'before', $this->getLanguage()->formatSize( $param['size'] ) ];
1120
			} elseif ( isset( $param['bitrate'] ) ) {
1121
				return [ 'before', $this->getLanguage()->formatBitrate( $param['bitrate'] ) ];
1122
			} elseif ( isset( $param['plaintext'] ) ) {
1123
				return [ 'after', $this->formatPlaintext( $param['plaintext'], $format ) ];
1124
			} else {
1125
				$warning = 'Invalid parameter for message "' . $this->getKey() . '": ' .
1126
					htmlspecialchars( serialize( $param ) );
1127
				trigger_error( $warning, E_USER_WARNING );
1128
				$e = new Exception;
1129
				wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
1130
1131
				return [ 'before', '[INVALID]' ];
1132
			}
1133
		} elseif ( $param instanceof Message ) {
1134
			// Message objects should not be before parameters because
1135
			// then they'll get double escaped. If the message needs to be
1136
			// escaped, it'll happen right here when we call toString().
1137
			return [ 'after', $param->toString( $format ) ];
1138
		} else {
1139
			return [ 'before', $param ];
1140
		}
1141
	}
1142
1143
	/**
1144
	 * Wrapper for what ever method we use to parse wikitext.
1145
	 *
1146
	 * @since 1.17
1147
	 *
1148
	 * @param string $string Wikitext message contents.
1149
	 *
1150
	 * @return string Wikitext parsed into HTML.
1151
	 */
1152
	protected function parseText( $string ) {
1153
		$out = MessageCache::singleton()->parse(
1154
			$string,
1155
			$this->title,
1156
			/*linestart*/true,
1157
			$this->interface,
1158
			$this->getLanguage()
1159
		);
1160
1161
		return $out instanceof ParserOutput ? $out->getText() : $out;
1162
	}
1163
1164
	/**
1165
	 * Wrapper for what ever method we use to {{-transform wikitext.
1166
	 *
1167
	 * @since 1.17
1168
	 *
1169
	 * @param string $string Wikitext message contents.
1170
	 *
1171
	 * @return string Wikitext with {{-constructs replaced with their values.
1172
	 */
1173
	protected function transformText( $string ) {
1174
		return MessageCache::singleton()->transform(
1175
			$string,
1176
			$this->interface,
1177
			$this->getLanguage(),
1178
			$this->title
1179
		);
1180
	}
1181
1182
	/**
1183
	 * Wrapper for what ever method we use to get message contents.
1184
	 *
1185
	 * @since 1.17
1186
	 *
1187
	 * @return string
1188
	 * @throws MWException If message key array is empty.
1189
	 */
1190
	protected function fetchMessage() {
1191
		if ( $this->message === null ) {
1192
			$cache = MessageCache::singleton();
1193
1194
			foreach ( $this->keysToTry as $key ) {
1195
				$message = $cache->get( $key, $this->useDatabase, $this->getLanguage() );
1196
				if ( $message !== false && $message !== '' ) {
1197
					break;
1198
				}
1199
			}
1200
1201
			// NOTE: The constructor makes sure keysToTry isn't empty,
1202
			//       so we know that $key and $message are initialized.
1203
			$this->key = $key;
1204
			$this->message = $message;
1205
		}
1206
		return $this->message;
1207
	}
1208
1209
	/**
1210
	 * Formats a message parameter wrapped with 'plaintext'. Ensures that
1211
	 * the entire string is displayed unchanged when displayed in the output
1212
	 * format.
1213
	 *
1214
	 * @since 1.25
1215
	 *
1216
	 * @param string $plaintext String to ensure plaintext output of
1217
	 * @param string $format One of the FORMAT_* constants.
1218
	 *
1219
	 * @return string Input plaintext encoded for output to $format
1220
	 */
1221
	protected function formatPlaintext( $plaintext, $format ) {
1222
		switch ( $format ) {
1223
		case self::FORMAT_TEXT:
1224
		case self::FORMAT_PLAIN:
1225
			return $plaintext;
1226
1227
		case self::FORMAT_PARSE:
1228
		case self::FORMAT_BLOCK_PARSE:
1229
		case self::FORMAT_ESCAPED:
1230
		default:
1231
			return htmlspecialchars( $plaintext, ENT_QUOTES );
1232
1233
		}
1234
	}
1235
}
1236
1237
/**
1238
 * Variant of the Message class.
1239
 *
1240
 * Rather than treating the message key as a lookup
1241
 * value (which is passed to the MessageCache and
1242
 * translated as necessary), a RawMessage key is
1243
 * treated as the actual message.
1244
 *
1245
 * All other functionality (parsing, escaping, etc.)
1246
 * is preserved.
1247
 *
1248
 * @since 1.21
1249
 */
1250
class RawMessage extends Message {
1251
1252
	/**
1253
	 * Call the parent constructor, then store the key as
1254
	 * the message.
1255
	 *
1256
	 * @see Message::__construct
1257
	 *
1258
	 * @param string $text Message to use.
1259
	 * @param array $params Parameters for the message.
1260
	 *
1261
	 * @throws InvalidArgumentException
1262
	 */
1263
	public function __construct( $text, $params = [] ) {
1264
		if ( !is_string( $text ) ) {
1265
			throw new InvalidArgumentException( '$text must be a string' );
1266
		}
1267
1268
		parent::__construct( $text, $params );
1269
1270
		// The key is the message.
1271
		$this->message = $text;
1272
	}
1273
1274
	/**
1275
	 * Fetch the message (in this case, the key).
1276
	 *
1277
	 * @return string
1278
	 */
1279
	public function fetchMessage() {
1280
		// Just in case the message is unset somewhere.
1281
		if ( $this->message === null ) {
1282
			$this->message = $this->key;
1283
		}
1284
1285
		return $this->message;
1286
	}
1287
1288
}
1289