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