Completed
Branch master (939199)
by
unknown
39:35
created

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
161
	/**
162
	 * In which language to get this message. True, which is the default,
163
	 * means the current user language, false content language.
164
	 *
165
	 * @var bool
166
	 */
167
	protected $interface = true;
168
169
	/**
170
	 * In which language to get this message. Overrides the $interface setting.
171
	 *
172
	 * @var Language|bool Explicit language object, or false for user language
173
	 */
174
	protected $language = false;
175
176
	/**
177
	 * @var string The message key. If $keysToTry has more than one element,
178
	 * this may change to one of the keys to try when fetching the message text.
179
	 */
180
	protected $key;
181
182
	/**
183
	 * @var string[] List of keys to try when fetching the message.
184
	 */
185
	protected $keysToTry;
186
187
	/**
188
	 * @var array List of parameters which will be substituted into the message.
189
	 */
190
	protected $parameters = [];
191
192
	/**
193
	 * Format for the message.
194
	 * Supported formats are:
195
	 * * text (transform)
196
	 * * escaped (transform+htmlspecialchars)
197
	 * * block-parse
198
	 * * parse (default)
199
	 * * plain
200
	 *
201
	 * @var string
202
	 */
203
	protected $format = 'parse';
204
205
	/**
206
	 * @var bool Whether database can be used.
207
	 */
208
	protected $useDatabase = true;
209
210
	/**
211
	 * @var Title Title object to use as context.
212
	 */
213
	protected $title = null;
214
215
	/**
216
	 * @var Content Content object representing the message.
217
	 */
218
	protected $content = null;
219
220
	/**
221
	 * @var string
222
	 */
223
	protected $message;
224
225
	/**
226
	 * @since 1.17
227
	 * @param string|string[]|MessageSpecifier $key Message key, or array of
228
	 * message keys to try and use the first non-empty message for, or a
229
	 * MessageSpecifier to copy from.
230
	 * @param array $params Message parameters.
231
	 * @param Language $language [optional] Language to use (defaults to current user language).
232
	 * @throws InvalidArgumentException
233
	 */
234
	public function __construct( $key, $params = [], Language $language = null ) {
235
		if ( $key instanceof MessageSpecifier ) {
236
			if ( $params ) {
237
				throw new InvalidArgumentException(
238
					'$params must be empty if $key is a MessageSpecifier'
239
				);
240
			}
241
			$params = $key->getParams();
242
			$key = $key->getKey();
243
		}
244
245
		if ( !is_string( $key ) && !is_array( $key ) ) {
246
			throw new InvalidArgumentException( '$key must be a string or an array' );
247
		}
248
249
		$this->keysToTry = (array)$key;
250
251
		if ( empty( $this->keysToTry ) ) {
252
			throw new InvalidArgumentException( '$key must not be an empty list' );
253
		}
254
255
		$this->key = reset( $this->keysToTry );
256
257
		$this->parameters = array_values( $params );
258
		// User language is only resolved in getLanguage(). This helps preserve the
259
		// semantic intent of "user language" across serialize() and unserialize().
260
		$this->language = $language ?: false;
261
	}
262
263
	/**
264
	 * @see Serializable::serialize()
265
	 * @since 1.26
266
	 * @return string
267
	 */
268
	public function serialize() {
269
		return serialize( [
270
			'interface' => $this->interface,
271
			'language' => $this->language ? $this->language->getCode() : false,
272
			'key' => $this->key,
273
			'keysToTry' => $this->keysToTry,
274
			'parameters' => $this->parameters,
275
			'format' => $this->format,
276
			'useDatabase' => $this->useDatabase,
277
			'title' => $this->title,
278
		] );
279
	}
280
281
	/**
282
	 * @see Serializable::unserialize()
283
	 * @since 1.26
284
	 * @param string $serialized
285
	 */
286
	public function unserialize( $serialized ) {
287
		$data = unserialize( $serialized );
288
		$this->interface = $data['interface'];
289
		$this->key = $data['key'];
290
		$this->keysToTry = $data['keysToTry'];
291
		$this->parameters = $data['parameters'];
292
		$this->format = $data['format'];
293
		$this->useDatabase = $data['useDatabase'];
294
		$this->language = $data['language'] ? Language::factory( $data['language'] ) : false;
295
		$this->title = $data['title'];
296
	}
297
298
	/**
299
	 * @since 1.24
300
	 *
301
	 * @return bool True if this is a multi-key message, that is, if the key provided to the
302
	 * constructor was a fallback list of keys to try.
303
	 */
304
	public function isMultiKey() {
305
		return count( $this->keysToTry ) > 1;
306
	}
307
308
	/**
309
	 * @since 1.24
310
	 *
311
	 * @return string[] The list of keys to try when fetching the message text,
312
	 * in order of preference.
313
	 */
314
	public function getKeysToTry() {
315
		return $this->keysToTry;
316
	}
317
318
	/**
319
	 * Returns the message key.
320
	 *
321
	 * If a list of multiple possible keys was supplied to the constructor, this method may
322
	 * return any of these keys. After the message has been fetched, this method will return
323
	 * the key that was actually used to fetch the message.
324
	 *
325
	 * @since 1.21
326
	 *
327
	 * @return string
328
	 */
329
	public function getKey() {
330
		return $this->key;
331
	}
332
333
	/**
334
	 * Returns the message parameters.
335
	 *
336
	 * @since 1.21
337
	 *
338
	 * @return array
339
	 */
340
	public function getParams() {
341
		return $this->parameters;
342
	}
343
344
	/**
345
	 * Returns the message format.
346
	 *
347
	 * @since 1.21
348
	 *
349
	 * @return string
350
	 */
351
	public function getFormat() {
352
		return $this->format;
353
	}
354
355
	/**
356
	 * Returns the Language of the Message.
357
	 *
358
	 * @since 1.23
359
	 *
360
	 * @return Language
361
	 */
362
	public function getLanguage() {
363
		// Defaults to false which means current user language
364
		return $this->language ?: RequestContext::getMain()->getLanguage();
365
	}
366
367
	/**
368
	 * Factory function that is just wrapper for the real constructor. It is
369
	 * intended to be used instead of the real constructor, because it allows
370
	 * chaining method calls, while new objects don't.
371
	 *
372
	 * @since 1.17
373
	 *
374
	 * @param string|string[]|MessageSpecifier $key
375
	 * @param mixed $param,... Parameters as strings.
376
	 *
377
	 * @return Message
378
	 */
379
	public static function newFromKey( $key /*...*/ ) {
380
		$params = func_get_args();
381
		array_shift( $params );
382
		return new self( $key, $params );
383
	}
384
385
	/**
386
	 * Transform a MessageSpecifier or a primitive value used interchangeably with
387
	 * specifiers (a message key string, or a key + params array) into a proper Message.
388
	 *
389
	 * Also accepts a MessageSpecifier inside an array: that's not considered a valid format
390
	 * but is an easy error to make due to how StatusValue stores messages internally.
391
	 * Further array elements are ignored in that case.
392
	 *
393
	 * @param string|array|MessageSpecifier $value
394
	 * @return Message
395
	 * @throws InvalidArgumentException
396
	 * @since 1.27
397
	 */
398
	public static function newFromSpecifier( $value ) {
399
		$params = [];
400
		if ( is_array( $value ) ) {
401
			$params = $value;
402
			$value = array_shift( $params );
403
		}
404
405
		if ( $value instanceof Message ) { // Message, RawMessage, ApiMessage, etc
406
			$message = clone( $value );
407
		} elseif ( $value instanceof MessageSpecifier ) {
408
			$message = new Message( $value );
409
		} elseif ( is_string( $value ) ) {
410
			$message = new Message( $value, $params );
411
		} else {
412
			throw new InvalidArgumentException( __METHOD__ . ': invalid argument type '
413
				. gettype( $value ) );
414
		}
415
416
		return $message;
417
	}
418
419
	/**
420
	 * Factory function accepting multiple message keys and returning a message instance
421
	 * for the first message which is non-empty. If all messages are empty then an
422
	 * instance of the first message key is returned.
423
	 *
424
	 * @since 1.18
425
	 *
426
	 * @param string|string[] $keys,... Message keys, or first argument as an array of all the
427
	 * message keys.
428
	 *
429
	 * @return Message
430
	 */
431
	public static function newFallbackSequence( /*...*/ ) {
432
		$keys = func_get_args();
433
		if ( func_num_args() == 1 ) {
434
			if ( is_array( $keys[0] ) ) {
435
				// Allow an array to be passed as the first argument instead
436
				$keys = array_values( $keys[0] );
437
			} else {
438
				// Optimize a single string to not need special fallback handling
439
				$keys = $keys[0];
440
			}
441
		}
442
		return new self( $keys );
443
	}
444
445
	/**
446
	 * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace.
447
	 * The title will be for the current language, if the message key is in
448
	 * $wgForceUIMsgAsContentMsg it will be append with the language code (except content
449
	 * language), because Message::inContentLanguage will also return in user language.
450
	 *
451
	 * @see $wgForceUIMsgAsContentMsg
452
	 * @return Title
453
	 * @since 1.26
454
	 */
455
	public function getTitle() {
456
		global $wgContLang, $wgForceUIMsgAsContentMsg;
457
458
		$title = $this->key;
459
		if (
460
			!$this->language->equals( $wgContLang )
461
			&& in_array( $this->key, (array)$wgForceUIMsgAsContentMsg )
462
		) {
463
			$code = $this->language->getCode();
464
			$title .= '/' . $code;
465
		}
466
467
		return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( strtr( $title, ' ', '_' ) ) );
468
	}
469
470
	/**
471
	 * Adds parameters to the parameter list of this message.
472
	 *
473
	 * @since 1.17
474
	 *
475
	 * @param mixed ... Parameters as strings, or a single argument that is
476
	 * an array of strings.
477
	 *
478
	 * @return Message $this
479
	 */
480
	public function params( /*...*/ ) {
481
		$args = func_get_args();
482 View Code Duplication
		if ( isset( $args[0] ) && is_array( $args[0] ) ) {
483
			$args = $args[0];
484
		}
485
		$args_values = array_values( $args );
486
		$this->parameters = array_merge( $this->parameters, $args_values );
487
		return $this;
488
	}
489
490
	/**
491
	 * Add parameters that are substituted after parsing or escaping.
492
	 * In other words the parsing process cannot access the contents
493
	 * of this type of parameter, and you need to make sure it is
494
	 * sanitized beforehand.  The parser will see "$n", instead.
495
	 *
496
	 * @since 1.17
497
	 *
498
	 * @param mixed $params,... Raw parameters as strings, or a single argument that is
499
	 * an array of raw parameters.
500
	 *
501
	 * @return Message $this
502
	 */
503 View Code Duplication
	public function rawParams( /*...*/ ) {
504
		$params = func_get_args();
505
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
506
			$params = $params[0];
507
		}
508
		foreach ( $params as $param ) {
509
			$this->parameters[] = self::rawParam( $param );
510
		}
511
		return $this;
512
	}
513
514
	/**
515
	 * Add parameters that are numeric and will be passed through
516
	 * Language::formatNum before substitution
517
	 *
518
	 * @since 1.18
519
	 *
520
	 * @param mixed $param,... Numeric parameters, or a single argument that is
521
	 * an array of numeric parameters.
522
	 *
523
	 * @return Message $this
524
	 */
525 View Code Duplication
	public function numParams( /*...*/ ) {
526
		$params = func_get_args();
527
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
528
			$params = $params[0];
529
		}
530
		foreach ( $params as $param ) {
531
			$this->parameters[] = self::numParam( $param );
532
		}
533
		return $this;
534
	}
535
536
	/**
537
	 * Add parameters that are durations of time and will be passed through
538
	 * Language::formatDuration before substitution
539
	 *
540
	 * @since 1.22
541
	 *
542
	 * @param int|int[] $param,... Duration parameters, or a single argument that is
543
	 * an array of duration parameters.
544
	 *
545
	 * @return Message $this
546
	 */
547 View Code Duplication
	public function durationParams( /*...*/ ) {
548
		$params = func_get_args();
549
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
550
			$params = $params[0];
551
		}
552
		foreach ( $params as $param ) {
553
			$this->parameters[] = self::durationParam( $param );
554
		}
555
		return $this;
556
	}
557
558
	/**
559
	 * Add parameters that are expiration times and will be passed through
560
	 * Language::formatExpiry before substitution
561
	 *
562
	 * @since 1.22
563
	 *
564
	 * @param string|string[] $param,... Expiry parameters, or a single argument that is
565
	 * an array of expiry parameters.
566
	 *
567
	 * @return Message $this
568
	 */
569 View Code Duplication
	public function expiryParams( /*...*/ ) {
570
		$params = func_get_args();
571
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
572
			$params = $params[0];
573
		}
574
		foreach ( $params as $param ) {
575
			$this->parameters[] = self::expiryParam( $param );
576
		}
577
		return $this;
578
	}
579
580
	/**
581
	 * Add parameters that are time periods and will be passed through
582
	 * Language::formatTimePeriod before substitution
583
	 *
584
	 * @since 1.22
585
	 *
586
	 * @param int|int[] $param,... Time period parameters, or a single argument that is
587
	 * an array of time period parameters.
588
	 *
589
	 * @return Message $this
590
	 */
591 View Code Duplication
	public function timeperiodParams( /*...*/ ) {
592
		$params = func_get_args();
593
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
594
			$params = $params[0];
595
		}
596
		foreach ( $params as $param ) {
597
			$this->parameters[] = self::timeperiodParam( $param );
598
		}
599
		return $this;
600
	}
601
602
	/**
603
	 * Add parameters that are file sizes and will be passed through
604
	 * Language::formatSize before substitution
605
	 *
606
	 * @since 1.22
607
	 *
608
	 * @param int|int[] $param,... Size parameters, or a single argument that is
609
	 * an array of size parameters.
610
	 *
611
	 * @return Message $this
612
	 */
613 View Code Duplication
	public function sizeParams( /*...*/ ) {
614
		$params = func_get_args();
615
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
616
			$params = $params[0];
617
		}
618
		foreach ( $params as $param ) {
619
			$this->parameters[] = self::sizeParam( $param );
620
		}
621
		return $this;
622
	}
623
624
	/**
625
	 * Add parameters that are bitrates and will be passed through
626
	 * Language::formatBitrate before substitution
627
	 *
628
	 * @since 1.22
629
	 *
630
	 * @param int|int[] $param,... Bit rate parameters, or a single argument that is
631
	 * an array of bit rate parameters.
632
	 *
633
	 * @return Message $this
634
	 */
635 View Code Duplication
	public function bitrateParams( /*...*/ ) {
636
		$params = func_get_args();
637
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
638
			$params = $params[0];
639
		}
640
		foreach ( $params as $param ) {
641
			$this->parameters[] = self::bitrateParam( $param );
642
		}
643
		return $this;
644
	}
645
646
	/**
647
	 * Add parameters that are plaintext and will be passed through without
648
	 * the content being evaluated.  Plaintext parameters are not valid as
649
	 * arguments to parser functions. This differs from self::rawParams in
650
	 * that the Message class handles escaping to match the output format.
651
	 *
652
	 * @since 1.25
653
	 *
654
	 * @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...
655
	 * an array of plaintext parameters.
656
	 *
657
	 * @return Message $this
658
	 */
659 View Code Duplication
	public function plaintextParams( /*...*/ ) {
660
		$params = func_get_args();
661
		if ( isset( $params[0] ) && is_array( $params[0] ) ) {
662
			$params = $params[0];
663
		}
664
		foreach ( $params as $param ) {
665
			$this->parameters[] = self::plaintextParam( $param );
666
		}
667
		return $this;
668
	}
669
670
	/**
671
	 * Set the language and the title from a context object
672
	 *
673
	 * @since 1.19
674
	 *
675
	 * @param IContextSource $context
676
	 *
677
	 * @return Message $this
678
	 */
679
	public function setContext( IContextSource $context ) {
680
		$this->inLanguage( $context->getLanguage() );
681
		$this->title( $context->getTitle() );
682
		$this->interface = true;
683
684
		return $this;
685
	}
686
687
	/**
688
	 * Request the message in any language that is supported.
689
	 *
690
	 * As a side effect interface message status is unconditionally
691
	 * turned off.
692
	 *
693
	 * @since 1.17
694
	 * @param Language|string $lang Language code or Language object.
695
	 * @return Message $this
696
	 * @throws MWException
697
	 */
698
	public function inLanguage( $lang ) {
699
		if ( $lang instanceof Language ) {
700
			$this->language = $lang;
701
		} elseif ( is_string( $lang ) ) {
702
			if ( !$this->language instanceof Language || $this->language->getCode() != $lang ) {
703
				$this->language = Language::factory( $lang );
704
			}
705
		} elseif ( $lang instanceof StubUserLang ) {
706
			$this->language = false;
707
		} else {
708
			$type = gettype( $lang );
709
			throw new MWException( __METHOD__ . " must be "
710
				. "passed a String or Language object; $type given"
711
			);
712
		}
713
		$this->message = null;
714
		$this->interface = false;
715
		return $this;
716
	}
717
718
	/**
719
	 * Request the message in the wiki's content language,
720
	 * unless it is disabled for this message.
721
	 *
722
	 * @since 1.17
723
	 * @see $wgForceUIMsgAsContentMsg
724
	 *
725
	 * @return Message $this
726
	 */
727
	public function inContentLanguage() {
728
		global $wgForceUIMsgAsContentMsg;
729
		if ( in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) ) {
730
			return $this;
731
		}
732
733
		global $wgContLang;
734
		$this->inLanguage( $wgContLang );
735
		return $this;
736
	}
737
738
	/**
739
	 * Allows manipulating the interface message flag directly.
740
	 * Can be used to restore the flag after setting a language.
741
	 *
742
	 * @since 1.20
743
	 *
744
	 * @param bool $interface
745
	 *
746
	 * @return Message $this
747
	 */
748
	public function setInterfaceMessageFlag( $interface ) {
749
		$this->interface = (bool)$interface;
750
		return $this;
751
	}
752
753
	/**
754
	 * Enable or disable database use.
755
	 *
756
	 * @since 1.17
757
	 *
758
	 * @param bool $useDatabase
759
	 *
760
	 * @return Message $this
761
	 */
762
	public function useDatabase( $useDatabase ) {
763
		$this->useDatabase = (bool)$useDatabase;
764
		return $this;
765
	}
766
767
	/**
768
	 * Set the Title object to use as context when transforming the message
769
	 *
770
	 * @since 1.18
771
	 *
772
	 * @param Title $title
773
	 *
774
	 * @return Message $this
775
	 */
776
	public function title( $title ) {
777
		$this->title = $title;
778
		return $this;
779
	}
780
781
	/**
782
	 * Returns the message as a Content object.
783
	 *
784
	 * @return Content
785
	 */
786
	public function content() {
787
		if ( !$this->content ) {
788
			$this->content = new MessageContent( $this );
789
		}
790
791
		return $this->content;
792
	}
793
794
	/**
795
	 * Returns the message parsed from wikitext to HTML.
796
	 *
797
	 * @since 1.17
798
	 *
799
	 * @return string HTML
800
	 */
801
	public function toString() {
802
		$string = $this->fetchMessage();
803
804
		if ( $string === false ) {
805
			// Err on the side of safety, ensure that the output
806
			// is always html safe in the event the message key is
807
			// missing, since in that case its highly likely the
808
			// message key is user-controlled.
809
			// '⧼' is used instead of '<' to side-step any
810
			// double-escaping issues.
811
			return '⧼' . htmlspecialchars( $this->key ) . '⧽';
812
		}
813
814
		# Replace $* with a list of parameters for &uselang=qqx.
815
		if ( strpos( $string, '$*' ) !== false ) {
816
			$paramlist = '';
817
			if ( $this->parameters !== [] ) {
818
				$paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
819
			}
820
			$string = str_replace( '$*', $paramlist, $string );
821
		}
822
823
		# Replace parameters before text parsing
824
		$string = $this->replaceParameters( $string, 'before' );
825
826
		# Maybe transform using the full parser
827
		if ( $this->format === 'parse' ) {
828
			$string = $this->parseText( $string );
829
			$string = Parser::stripOuterParagraph( $string );
830
		} elseif ( $this->format === 'block-parse' ) {
831
			$string = $this->parseText( $string );
832
		} elseif ( $this->format === 'text' ) {
833
			$string = $this->transformText( $string );
834
		} elseif ( $this->format === 'escaped' ) {
835
			$string = $this->transformText( $string );
836
			$string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
837
		}
838
839
		# Raw parameter replacement
840
		$string = $this->replaceParameters( $string, 'after' );
841
842
		return $string;
843
	}
844
845
	/**
846
	 * Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
847
	 *     $foo = new Message( $key );
848
	 *     $string = "<abbr>$foo</abbr>";
849
	 *
850
	 * @since 1.18
851
	 *
852
	 * @return string
853
	 */
854
	public function __toString() {
855
		if ( $this->format !== 'parse' ) {
856
			$ex = new LogicException( __METHOD__ . ' using implicit format: ' . $this->format );
857
			\MediaWiki\Logger\LoggerFactory::getInstance( 'message-format' )->warning(
858
				$ex->getMessage(), [ 'exception' => $ex, 'format' => $this->format, 'key' => $this->key ] );
859
		}
860
861
		// PHP doesn't allow __toString to throw exceptions and will
862
		// trigger a fatal error if it does. So, catch any exceptions.
863
864
		try {
865
			return $this->toString();
866
		} catch ( Exception $ex ) {
867
			try {
868
				trigger_error( "Exception caught in " . __METHOD__ . " (message " . $this->key . "): "
869
					. $ex, E_USER_WARNING );
870
			} catch ( Exception $ex ) {
871
				// Doh! Cause a fatal error after all?
872
			}
873
874
			if ( $this->format === 'plain' || $this->format === 'text' ) {
875
				return '<' . $this->key . '>';
876
			}
877
			return '&lt;' . htmlspecialchars( $this->key ) . '&gt;';
878
		}
879
	}
880
881
	/**
882
	 * Fully parse the text from wikitext to HTML.
883
	 *
884
	 * @since 1.17
885
	 *
886
	 * @return string Parsed HTML.
887
	 */
888
	public function parse() {
889
		$this->format = 'parse';
890
		return $this->toString();
891
	}
892
893
	/**
894
	 * Returns the message text. {{-transformation is done.
895
	 *
896
	 * @since 1.17
897
	 *
898
	 * @return string Unescaped message text.
899
	 */
900
	public function text() {
901
		$this->format = 'text';
902
		return $this->toString();
903
	}
904
905
	/**
906
	 * Returns the message text as-is, only parameters are substituted.
907
	 *
908
	 * @since 1.17
909
	 *
910
	 * @return string Unescaped untransformed message text.
911
	 */
912
	public function plain() {
913
		$this->format = 'plain';
914
		return $this->toString();
915
	}
916
917
	/**
918
	 * Returns the parsed message text which is always surrounded by a block element.
919
	 *
920
	 * @since 1.17
921
	 *
922
	 * @return string HTML
923
	 */
924
	public function parseAsBlock() {
925
		$this->format = 'block-parse';
926
		return $this->toString();
927
	}
928
929
	/**
930
	 * Returns the message text. {{-transformation is done and the result
931
	 * is escaped excluding any raw parameters.
932
	 *
933
	 * @since 1.17
934
	 *
935
	 * @return string Escaped message text.
936
	 */
937
	public function escaped() {
938
		$this->format = 'escaped';
939
		return $this->toString();
940
	}
941
942
	/**
943
	 * Check whether a message key has been defined currently.
944
	 *
945
	 * @since 1.17
946
	 *
947
	 * @return bool
948
	 */
949
	public function exists() {
950
		return $this->fetchMessage() !== false;
951
	}
952
953
	/**
954
	 * Check whether a message does not exist, or is an empty string
955
	 *
956
	 * @since 1.18
957
	 * @todo FIXME: Merge with isDisabled()?
958
	 *
959
	 * @return bool
960
	 */
961
	public function isBlank() {
962
		$message = $this->fetchMessage();
963
		return $message === false || $message === '';
964
	}
965
966
	/**
967
	 * Check whether a message does not exist, is an empty string, or is "-".
968
	 *
969
	 * @since 1.18
970
	 *
971
	 * @return bool
972
	 */
973
	public function isDisabled() {
974
		$message = $this->fetchMessage();
975
		return $message === false || $message === '' || $message === '-';
976
	}
977
978
	/**
979
	 * @since 1.17
980
	 *
981
	 * @param mixed $raw
982
	 *
983
	 * @return array Array with a single "raw" key.
984
	 */
985
	public static function rawParam( $raw ) {
986
		return [ 'raw' => $raw ];
987
	}
988
989
	/**
990
	 * @since 1.18
991
	 *
992
	 * @param mixed $num
993
	 *
994
	 * @return array Array with a single "num" key.
995
	 */
996
	public static function numParam( $num ) {
997
		return [ 'num' => $num ];
998
	}
999
1000
	/**
1001
	 * @since 1.22
1002
	 *
1003
	 * @param int $duration
1004
	 *
1005
	 * @return int[] Array with a single "duration" key.
1006
	 */
1007
	public static function durationParam( $duration ) {
1008
		return [ 'duration' => $duration ];
1009
	}
1010
1011
	/**
1012
	 * @since 1.22
1013
	 *
1014
	 * @param string $expiry
1015
	 *
1016
	 * @return string[] Array with a single "expiry" key.
1017
	 */
1018
	public static function expiryParam( $expiry ) {
1019
		return [ 'expiry' => $expiry ];
1020
	}
1021
1022
	/**
1023
	 * @since 1.22
1024
	 *
1025
	 * @param number $period
1026
	 *
1027
	 * @return number[] Array with a single "period" key.
1028
	 */
1029
	public static function timeperiodParam( $period ) {
1030
		return [ 'period' => $period ];
1031
	}
1032
1033
	/**
1034
	 * @since 1.22
1035
	 *
1036
	 * @param int $size
1037
	 *
1038
	 * @return int[] Array with a single "size" key.
1039
	 */
1040
	public static function sizeParam( $size ) {
1041
		return [ 'size' => $size ];
1042
	}
1043
1044
	/**
1045
	 * @since 1.22
1046
	 *
1047
	 * @param int $bitrate
1048
	 *
1049
	 * @return int[] Array with a single "bitrate" key.
1050
	 */
1051
	public static function bitrateParam( $bitrate ) {
1052
		return [ 'bitrate' => $bitrate ];
1053
	}
1054
1055
	/**
1056
	 * @since 1.25
1057
	 *
1058
	 * @param string $plaintext
1059
	 *
1060
	 * @return string[] Array with a single "plaintext" key.
1061
	 */
1062
	public static function plaintextParam( $plaintext ) {
1063
		return [ 'plaintext' => $plaintext ];
1064
	}
1065
1066
	/**
1067
	 * Substitutes any parameters into the message text.
1068
	 *
1069
	 * @since 1.17
1070
	 *
1071
	 * @param string $message The message text.
1072
	 * @param string $type Either "before" or "after".
1073
	 *
1074
	 * @return string
1075
	 */
1076
	protected function replaceParameters( $message, $type = 'before' ) {
1077
		$replacementKeys = [];
1078
		foreach ( $this->parameters as $n => $param ) {
1079
			list( $paramType, $value ) = $this->extractParam( $param );
1080
			if ( $type === $paramType ) {
1081
				$replacementKeys['$' . ( $n + 1 )] = $value;
1082
			}
1083
		}
1084
		$message = strtr( $message, $replacementKeys );
1085
		return $message;
1086
	}
1087
1088
	/**
1089
	 * Extracts the parameter type and preprocessed the value if needed.
1090
	 *
1091
	 * @since 1.18
1092
	 *
1093
	 * @param mixed $param Parameter as defined in this class.
1094
	 *
1095
	 * @return array Array with the parameter type (either "before" or "after") and the value.
1096
	 */
1097
	protected function extractParam( $param ) {
1098
		if ( is_array( $param ) ) {
1099
			if ( isset( $param['raw'] ) ) {
1100
				return [ 'after', $param['raw'] ];
1101 View Code Duplication
			} elseif ( isset( $param['num'] ) ) {
1102
				// Replace number params always in before step for now.
1103
				// No support for combined raw and num params
1104
				return [ 'before', $this->getLanguage()->formatNum( $param['num'] ) ];
1105
			} elseif ( isset( $param['duration'] ) ) {
1106
				return [ 'before', $this->getLanguage()->formatDuration( $param['duration'] ) ];
1107
			} elseif ( isset( $param['expiry'] ) ) {
1108
				return [ 'before', $this->getLanguage()->formatExpiry( $param['expiry'] ) ];
1109
			} elseif ( isset( $param['period'] ) ) {
1110
				return [ 'before', $this->getLanguage()->formatTimePeriod( $param['period'] ) ];
1111 View Code Duplication
			} elseif ( isset( $param['size'] ) ) {
1112
				return [ 'before', $this->getLanguage()->formatSize( $param['size'] ) ];
1113
			} elseif ( isset( $param['bitrate'] ) ) {
1114
				return [ 'before', $this->getLanguage()->formatBitrate( $param['bitrate'] ) ];
1115
			} elseif ( isset( $param['plaintext'] ) ) {
1116
				return [ 'after', $this->formatPlaintext( $param['plaintext'] ) ];
1117
			} else {
1118
				$warning = 'Invalid parameter for message "' . $this->getKey() . '": ' .
1119
					htmlspecialchars( serialize( $param ) );
1120
				trigger_error( $warning, E_USER_WARNING );
1121
				$e = new Exception;
1122
				wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
1123
1124
				return [ 'before', '[INVALID]' ];
1125
			}
1126
		} elseif ( $param instanceof Message ) {
1127
			// Message objects should not be before parameters because
1128
			// then they'll get double escaped. If the message needs to be
1129
			// escaped, it'll happen right here when we call toString().
1130
			return [ 'after', $param->toString() ];
1131
		} else {
1132
			return [ 'before', $param ];
1133
		}
1134
	}
1135
1136
	/**
1137
	 * Wrapper for what ever method we use to parse wikitext.
1138
	 *
1139
	 * @since 1.17
1140
	 *
1141
	 * @param string $string Wikitext message contents.
1142
	 *
1143
	 * @return string Wikitext parsed into HTML.
1144
	 */
1145
	protected function parseText( $string ) {
1146
		$out = MessageCache::singleton()->parse(
1147
			$string,
1148
			$this->title,
1149
			/*linestart*/true,
1150
			$this->interface,
1151
			$this->getLanguage()
1152
		);
1153
1154
		return $out instanceof ParserOutput ? $out->getText() : $out;
1155
	}
1156
1157
	/**
1158
	 * Wrapper for what ever method we use to {{-transform wikitext.
1159
	 *
1160
	 * @since 1.17
1161
	 *
1162
	 * @param string $string Wikitext message contents.
1163
	 *
1164
	 * @return string Wikitext with {{-constructs replaced with their values.
1165
	 */
1166
	protected function transformText( $string ) {
1167
		return MessageCache::singleton()->transform(
1168
			$string,
1169
			$this->interface,
1170
			$this->getLanguage(),
1171
			$this->title
1172
		);
1173
	}
1174
1175
	/**
1176
	 * Wrapper for what ever method we use to get message contents.
1177
	 *
1178
	 * @since 1.17
1179
	 *
1180
	 * @return string
1181
	 * @throws MWException If message key array is empty.
1182
	 */
1183
	protected function fetchMessage() {
1184
		if ( $this->message === null ) {
1185
			$cache = MessageCache::singleton();
1186
1187
			foreach ( $this->keysToTry as $key ) {
1188
				$message = $cache->get( $key, $this->useDatabase, $this->getLanguage() );
1189
				if ( $message !== false && $message !== '' ) {
1190
					break;
1191
				}
1192
			}
1193
1194
			// NOTE: The constructor makes sure keysToTry isn't empty,
1195
			//       so we know that $key and $message are initialized.
1196
			$this->key = $key;
1197
			$this->message = $message;
1198
		}
1199
		return $this->message;
1200
	}
1201
1202
	/**
1203
	 * Formats a message parameter wrapped with 'plaintext'. Ensures that
1204
	 * the entire string is displayed unchanged when displayed in the output
1205
	 * format.
1206
	 *
1207
	 * @since 1.25
1208
	 *
1209
	 * @param string $plaintext String to ensure plaintext output of
1210
	 *
1211
	 * @return string Input plaintext encoded for output to $this->format
1212
	 */
1213
	protected function formatPlaintext( $plaintext ) {
1214
		switch ( $this->format ) {
1215
		case 'text':
1216
		case 'plain':
1217
			return $plaintext;
1218
1219
		case 'parse':
1220
		case 'block-parse':
1221
		case 'escaped':
1222
		default:
1223
			return htmlspecialchars( $plaintext, ENT_QUOTES );
1224
1225
		}
1226
	}
1227
}
1228
1229
/**
1230
 * Variant of the Message class.
1231
 *
1232
 * Rather than treating the message key as a lookup
1233
 * value (which is passed to the MessageCache and
1234
 * translated as necessary), a RawMessage key is
1235
 * treated as the actual message.
1236
 *
1237
 * All other functionality (parsing, escaping, etc.)
1238
 * is preserved.
1239
 *
1240
 * @since 1.21
1241
 */
1242
class RawMessage extends Message {
1243
1244
	/**
1245
	 * Call the parent constructor, then store the key as
1246
	 * the message.
1247
	 *
1248
	 * @see Message::__construct
1249
	 *
1250
	 * @param string $text Message to use.
1251
	 * @param array $params Parameters for the message.
1252
	 *
1253
	 * @throws InvalidArgumentException
1254
	 */
1255
	public function __construct( $text, $params = [] ) {
1256
		if ( !is_string( $text ) ) {
1257
			throw new InvalidArgumentException( '$text must be a string' );
1258
		}
1259
1260
		parent::__construct( $text, $params );
1261
1262
		// The key is the message.
1263
		$this->message = $text;
1264
	}
1265
1266
	/**
1267
	 * Fetch the message (in this case, the key).
1268
	 *
1269
	 * @return string
1270
	 */
1271
	public function fetchMessage() {
1272
		// Just in case the message is unset somewhere.
1273
		if ( $this->message === null ) {
1274
			$this->message = $this->key;
1275
		}
1276
1277
		return $this->message;
1278
	}
1279
1280
}
1281