Fix r110054, \ -> @
[lhc/web/wiklou.git] / includes / Message.php
1 <?php
2 /**
3 * This class provides methods for fetching interface messages and
4 * processing them into variety of formats that are needed in MediaWiki.
5 *
6 * It is intented to replace the old wfMsg* functions that over time grew
7 * unusable.
8 * @see https://www.mediawiki.org/wiki/New_messages_API for
9 * equivalence between old and new functions.
10 *
11 * Below, you will find several examples of wfMessage() usage.
12 *
13 *
14 * Fetching a message text for interface message
15 * @code
16 * $button = Xml::button( wfMessage( 'submit' )->text() );
17 * @endcode
18 *
19 * Messages can have parameters:
20 *
21 * @code
22 * wfMessage( 'welcome-to' )->params( $wgSitename )->text();
23 * @endcode
24 *
25 * {{GRAMMAR}} and friends work correctly
26 * @code
27 * wfMessage( 'are-friends', $user, $friend );
28 * wfMessage( 'bad-message' )->rawParams( '<script>...</script>' )->escaped();
29 * @endcode
30 *
31 * Sometimes the message text ends up in the database, so content language is needed.
32 * @code
33 * wfMessage( 'file-log', $user, $filename )->inContentLanguage()->text()
34 * </pre>
35 * @endcode
36 *
37 * Checking whether a message exists:
38 * @code
39 * wfMessage( 'mysterious-message' )->exists()
40 * @endcode
41 *
42 * If you want to use a different language:
43 * @code
44 * wfMessage( 'email-header' )->inLanguage( $user->getOption( 'language' ) )->plain()
45 * @endcode
46 *
47 * @note You cannot parse the text except in the content or interface
48 * @note languages
49 *
50 * Comparison with old wfMsg* functions:
51 *
52 * Use full parsing.
53 *
54 * @code
55 * wfMsgExt( 'key', array( 'parseinline' ), 'apple' );
56 * === wfMessage( 'key', 'apple' )->parse();
57 * @endcode
58 *
59 * Parseinline is used because it is more useful when pre-building html.
60 * In normal use it is better to use OutputPage::(add|wrap)WikiMsg.
61 *
62 * Places where html cannot be used. {{-transformation is done.
63 * @code
64 * wfMsgExt( 'key', array( 'parsemag' ), 'apple', 'pear' );
65 * === wfMessage( 'key', 'apple', 'pear' )->text();
66 * @endcode
67 *
68 * Shortcut for escaping the message too, similar to wfMsgHTML, but
69 * parameters are not replaced after escaping by default.
70 * @code
71 * $escaped = wfMessage( 'key' )->rawParams( 'apple' )->escaped();
72 * @endcode
73 *
74 * @todo
75 * - test, can we have tests?
76 *
77 * @see https://www.mediawiki.org/wiki/WfMessage()
78 * @see https://www.mediawiki.org/wiki/New_messages_API
79 * @see https://www.mediawiki.org/wiki/Localisation
80 *
81 * @since 1.17
82 * @author Niklas Laxström
83 */
84 class Message {
85 /**
86 * In which language to get this message. True, which is the default,
87 * means the current interface language, false content language.
88 */
89 protected $interface = true;
90
91 /**
92 * In which language to get this message. Overrides the $interface
93 * variable.
94 *
95 * @var Language
96 */
97 protected $language = null;
98
99 /**
100 * The message key.
101 */
102 protected $key;
103
104 /**
105 * List of parameters which will be substituted into the message.
106 */
107 protected $parameters = array();
108
109 /**
110 * Format for the message.
111 * Supported formats are:
112 * * text (transform)
113 * * escaped (transform+htmlspecialchars)
114 * * block-parse
115 * * parse (default)
116 * * plain
117 */
118 protected $format = 'parse';
119
120 /**
121 * Whether database can be used.
122 */
123 protected $useDatabase = true;
124
125 /**
126 * Title object to use as context
127 */
128 protected $title = null;
129
130 /**
131 * @var string
132 */
133 protected $message;
134
135 /**
136 * Constructor.
137 * @param $key: message key, or array of message keys to try and use the first non-empty message for
138 * @param $params Array message parameters
139 * @return Message: $this
140 */
141 public function __construct( $key, $params = array() ) {
142 global $wgLang;
143 $this->key = $key;
144 $this->parameters = array_values( $params );
145 $this->language = $wgLang;
146 }
147
148 /**
149 * Factory function that is just wrapper for the real constructor. It is
150 * intented to be used instead of the real constructor, because it allows
151 * chaining method calls, while new objects don't.
152 * @param $key String: message key
153 * @param Varargs: parameters as Strings
154 * @return Message: $this
155 */
156 public static function newFromKey( $key /*...*/ ) {
157 $params = func_get_args();
158 array_shift( $params );
159 return new self( $key, $params );
160 }
161
162 /**
163 * Factory function accepting multiple message keys and returning a message instance
164 * for the first message which is non-empty. If all messages are empty then an
165 * instance of the first message key is returned.
166 * @param Varargs: message keys (or first arg as an array of all the message keys)
167 * @return Message: $this
168 */
169 public static function newFallbackSequence( /*...*/ ) {
170 $keys = func_get_args();
171 if ( func_num_args() == 1 ) {
172 if ( is_array($keys[0]) ) {
173 // Allow an array to be passed as the first argument instead
174 $keys = array_values($keys[0]);
175 } else {
176 // Optimize a single string to not need special fallback handling
177 $keys = $keys[0];
178 }
179 }
180 return new self( $keys );
181 }
182
183 /**
184 * Adds parameters to the parameter list of this message.
185 * @param Varargs: parameters as Strings, or a single argument that is an array of Strings
186 * @return Message: $this
187 */
188 public function params( /*...*/ ) {
189 $args = func_get_args();
190 if ( isset( $args[0] ) && is_array( $args[0] ) ) {
191 $args = $args[0];
192 }
193 $args_values = array_values( $args );
194 $this->parameters = array_merge( $this->parameters, $args_values );
195 return $this;
196 }
197
198 /**
199 * Add parameters that are substituted after parsing or escaping.
200 * In other words the parsing process cannot access the contents
201 * of this type of parameter, and you need to make sure it is
202 * sanitized beforehand. The parser will see "$n", instead.
203 * @param Varargs: raw parameters as Strings (or single argument that is an array of raw parameters)
204 * @return Message: $this
205 */
206 public function rawParams( /*...*/ ) {
207 $params = func_get_args();
208 if ( isset( $params[0] ) && is_array( $params[0] ) ) {
209 $params = $params[0];
210 }
211 foreach( $params as $param ) {
212 $this->parameters[] = self::rawParam( $param );
213 }
214 return $this;
215 }
216
217 /**
218 * Add parameters that are numeric and will be passed through
219 * Language::formatNum before substitution
220 * @param Varargs: numeric parameters (or single argument that is array of numeric parameters)
221 * @return Message: $this
222 */
223 public function numParams( /*...*/ ) {
224 $params = func_get_args();
225 if ( isset( $params[0] ) && is_array( $params[0] ) ) {
226 $params = $params[0];
227 }
228 foreach( $params as $param ) {
229 $this->parameters[] = self::numParam( $param );
230 }
231 return $this;
232 }
233
234 /**
235 * Set the language and the title from a context object
236 *
237 * @param $context IContextSource
238 * @return Message: $this
239 */
240 public function setContext( IContextSource $context ) {
241 $this->inLanguage( $context->getLanguage() );
242 $this->title( $context->getTitle() );
243
244 return $this;
245 }
246
247 /**
248 * Request the message in any language that is supported.
249 * As a side effect interface message status is unconditionally
250 * turned off.
251 * @param $lang Mixed: language code or Language object.
252 * @return Message: $this
253 */
254 public function inLanguage( $lang ) {
255 if ( $lang instanceof Language || $lang instanceof StubUserLang ) {
256 $this->language = $lang;
257 } elseif ( is_string( $lang ) ) {
258 if( $this->language->getCode() != $lang ) {
259 $this->language = Language::factory( $lang );
260 }
261 } else {
262 $type = gettype( $lang );
263 throw new MWException( __METHOD__ . " must be "
264 . "passed a String or Language object; $type given"
265 );
266 }
267 $this->interface = false;
268 return $this;
269 }
270
271 /**
272 * Request the message in the wiki's content language,
273 * unless it is disabled for this message.
274 * @see $wgForceUIMsgAsContentMsg
275 * @return Message: $this
276 */
277 public function inContentLanguage() {
278 global $wgForceUIMsgAsContentMsg;
279 if ( in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) ) {
280 return $this;
281 }
282
283 global $wgContLang;
284 $this->interface = false;
285 $this->language = $wgContLang;
286 return $this;
287 }
288
289 /**
290 * Enable or disable database use.
291 * @param $value Boolean
292 * @return Message: $this
293 */
294 public function useDatabase( $value ) {
295 $this->useDatabase = (bool) $value;
296 return $this;
297 }
298
299 /**
300 * Set the Title object to use as context when transforming the message
301 *
302 * @param $title Title object
303 * @return Message: $this
304 */
305 public function title( $title ) {
306 $this->title = $title;
307 return $this;
308 }
309
310 /**
311 * Returns the message parsed from wikitext to HTML.
312 * @return String: HTML
313 */
314 public function toString() {
315 $string = $this->getMessageText();
316
317 # Replace parameters before text parsing
318 $string = $this->replaceParameters( $string, 'before' );
319
320 # Maybe transform using the full parser
321 if( $this->format === 'parse' ) {
322 $string = $this->parseText( $string );
323 $m = array();
324 if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
325 $string = $m[1];
326 }
327 } elseif( $this->format === 'block-parse' ){
328 $string = $this->parseText( $string );
329 } elseif( $this->format === 'text' ){
330 $string = $this->transformText( $string );
331 } elseif( $this->format === 'escaped' ){
332 $string = $this->transformText( $string );
333 $string = htmlspecialchars( $string, ENT_QUOTES, 'UTF-8', false );
334 }
335
336 # Raw parameter replacement
337 $string = $this->replaceParameters( $string, 'after' );
338
339 return $string;
340 }
341
342 /**
343 * Magic method implementation of the above (for PHP >= 5.2.0), so we can do, eg:
344 * $foo = Message::get($key);
345 * $string = "<abbr>$foo</abbr>";
346 * @return String
347 */
348 public function __toString() {
349 return $this->toString();
350 }
351
352 /**
353 * Fully parse the text from wikitext to HTML
354 * @return String parsed HTML
355 */
356 public function parse() {
357 $this->format = 'parse';
358 return $this->toString();
359 }
360
361 /**
362 * Returns the message text. {{-transformation is done.
363 * @return String: Unescaped message text.
364 */
365 public function text() {
366 $this->format = 'text';
367 return $this->toString();
368 }
369
370 /**
371 * Returns the message text as-is, only parameters are subsituted.
372 * @return String: Unescaped untransformed message text.
373 */
374 public function plain() {
375 $this->format = 'plain';
376 return $this->toString();
377 }
378
379 /**
380 * Returns the parsed message text which is always surrounded by a block element.
381 * @return String: HTML
382 */
383 public function parseAsBlock() {
384 $this->format = 'block-parse';
385 return $this->toString();
386 }
387
388 /**
389 * Returns the message text. {{-transformation is done and the result
390 * is escaped excluding any raw parameters.
391 * @return String: Escaped message text.
392 */
393 public function escaped() {
394 $this->format = 'escaped';
395 return $this->toString();
396 }
397
398 /**
399 * Check whether a message key has been defined currently.
400 * @return Bool: true if it is and false if not.
401 */
402 public function exists() {
403 return $this->fetchMessage() !== false;
404 }
405
406 /**
407 * Check whether a message does not exist, or is an empty string
408 * @return Bool: true if is is and false if not
409 * @todo FIXME: Merge with isDisabled()?
410 */
411 public function isBlank() {
412 $message = $this->fetchMessage();
413 return $message === false || $message === '';
414 }
415
416 /**
417 * Check whether a message does not exist, is an empty string, or is "-"
418 * @return Bool: true if is is and false if not
419 */
420 public function isDisabled() {
421 $message = $this->fetchMessage();
422 return $message === false || $message === '' || $message === '-';
423 }
424
425 /**
426 * @param $value
427 * @return array
428 */
429 public static function rawParam( $value ) {
430 return array( 'raw' => $value );
431 }
432
433 /**
434 * @param $value
435 * @return array
436 */
437 public static function numParam( $value ) {
438 return array( 'num' => $value );
439 }
440
441 /**
442 * Substitutes any paramaters into the message text.
443 * @param $message String: the message text
444 * @param $type String: either before or after
445 * @return String
446 */
447 protected function replaceParameters( $message, $type = 'before' ) {
448 $replacementKeys = array();
449 foreach( $this->parameters as $n => $param ) {
450 list( $paramType, $value ) = $this->extractParam( $param );
451 if ( $type === $paramType ) {
452 $replacementKeys['$' . ($n + 1)] = $value;
453 }
454 }
455 $message = strtr( $message, $replacementKeys );
456 return $message;
457 }
458
459 /**
460 * Extracts the parameter type and preprocessed the value if needed.
461 * @param $param String|Array: Parameter as defined in this class.
462 * @return Tuple(type, value)
463 * @throws MWException
464 */
465 protected function extractParam( $param ) {
466 if ( is_array( $param ) && isset( $param['raw'] ) ) {
467 return array( 'after', $param['raw'] );
468 } elseif ( is_array( $param ) && isset( $param['num'] ) ) {
469 // Replace number params always in before step for now.
470 // No support for combined raw and num params
471 return array( 'before', $this->language->formatNum( $param['num'] ) );
472 } elseif ( !is_array( $param ) ) {
473 return array( 'before', $param );
474 } else {
475 throw new MWException( "Invalid message parameter" );
476 }
477 }
478
479 /**
480 * Wrapper for what ever method we use to parse wikitext.
481 * @param $string String: Wikitext message contents
482 * @return string Wikitext parsed into HTML
483 */
484 protected function parseText( $string ) {
485 return MessageCache::singleton()->parse( $string, $this->title, /*linestart*/true, $this->interface, $this->language )->getText();
486 }
487
488 /**
489 * Wrapper for what ever method we use to {{-transform wikitext.
490 * @param $string String: Wikitext message contents
491 * @return string Wikitext with {{-constructs replaced with their values.
492 */
493 protected function transformText( $string ) {
494 return MessageCache::singleton()->transform( $string, $this->interface, $this->language, $this->title );
495 }
496
497 /**
498 * Returns the textual value for the message.
499 * @return Message contents or placeholder
500 */
501 protected function getMessageText() {
502 $message = $this->fetchMessage();
503 if ( $message === false ) {
504 return '&lt;' . htmlspecialchars( is_array($this->key) ? $this->key[0] : $this->key ) . '&gt;';
505 } else {
506 return $message;
507 }
508 }
509
510 /**
511 * Wrapper for what ever method we use to get message contents
512 *
513 * @return string
514 */
515 protected function fetchMessage() {
516 if ( !isset( $this->message ) ) {
517 $cache = MessageCache::singleton();
518 if ( is_array($this->key) ) {
519 foreach ( $this->key as $key ) {
520 $message = $cache->get( $key, $this->useDatabase, $this->language );
521 if ( $message !== false && $message !== '' ) {
522 break;
523 }
524 }
525 $this->message = $message;
526 } else {
527 $this->message = $cache->get( $this->key, $this->useDatabase, $this->language );
528 }
529 }
530 return $this->message;
531 }
532
533 }