Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / resources / src / mediawiki.base / mediawiki.base.js
1 /*!
2 * This file is currently loaded as part of the 'mediawiki' module and therefore
3 * concatenated to mediawiki.js and executed at the same time. This file exists
4 * to help prepare for splitting up the 'mediawiki' module.
5 * This effort is tracked at https://phabricator.wikimedia.org/T192623
6 *
7 * In short:
8 *
9 * - mediawiki.js will be reduced to the minimum needed to define mw.loader and
10 * mw.config, and then moved to its own private "mediawiki.loader" module that
11 * can be embedded within the StartupModule response.
12 *
13 * - mediawiki.base.js and other files in this directory will remain part of the
14 * "mediawiki" module, and will remain a default/implicit dependency for all
15 * regular modules, just like jquery and wikibits already are.
16 */
17 ( function () {
18 'use strict';
19
20 var slice = Array.prototype.slice,
21 mwLoaderTrack = mw.track,
22 trackCallbacks = $.Callbacks( 'memory' ),
23 trackHandlers = [],
24 queue;
25
26 /**
27 * Object constructor for messages.
28 *
29 * Similar to the Message class in MediaWiki PHP.
30 *
31 * Format defaults to 'text'.
32 *
33 * @example
34 *
35 * var obj, str;
36 * mw.messages.set( {
37 * 'hello': 'Hello world',
38 * 'hello-user': 'Hello, $1!',
39 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
40 * } );
41 *
42 * obj = new mw.Message( mw.messages, 'hello' );
43 * mw.log( obj.text() );
44 * // Hello world
45 *
46 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
47 * mw.log( obj.text() );
48 * // Hello, John Doe!
49 *
50 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
51 * mw.log( obj.text() );
52 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
53 *
54 * // Using mw.message shortcut
55 * obj = mw.message( 'hello-user', 'John Doe' );
56 * mw.log( obj.text() );
57 * // Hello, John Doe!
58 *
59 * // Using mw.msg shortcut
60 * str = mw.msg( 'hello-user', 'John Doe' );
61 * mw.log( str );
62 * // Hello, John Doe!
63 *
64 * // Different formats
65 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
66 *
67 * obj.format = 'text';
68 * str = obj.toString();
69 * // Same as:
70 * str = obj.text();
71 *
72 * mw.log( str );
73 * // Hello, John "Wiki" <3 Doe!
74 *
75 * mw.log( obj.escaped() );
76 * // Hello, John &quot;Wiki&quot; &lt;3 Doe!
77 *
78 * @class mw.Message
79 *
80 * @constructor
81 * @param {mw.Map} map Message store
82 * @param {string} key
83 * @param {Array} [parameters]
84 */
85 function Message( map, key, parameters ) {
86 this.format = 'text';
87 this.map = map;
88 this.key = key;
89 this.parameters = parameters === undefined ? [] : slice.call( parameters );
90 return this;
91 }
92
93 Message.prototype = {
94 /**
95 * Get parsed contents of the message.
96 *
97 * The default parser does simple $N replacements and nothing else.
98 * This may be overridden to provide a more complex message parser.
99 * The primary override is in the mediawiki.jqueryMsg module.
100 *
101 * This function will not be called for nonexistent messages.
102 *
103 * @return {string} Parsed message
104 */
105 parser: function () {
106 var text = this.map.get( this.key );
107 if (
108 mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
109 ( !text || text === '(' + this.key + ')' )
110 ) {
111 text = '(' + this.key + '$*)';
112 }
113 return mw.format.apply( null, [ text ].concat( this.parameters ) );
114 },
115
116 /**
117 * Add (does not replace) parameters for `$N` placeholder values.
118 *
119 * @param {Array} parameters
120 * @return {mw.Message}
121 * @chainable
122 */
123 params: function ( parameters ) {
124 var i;
125 for ( i = 0; i < parameters.length; i++ ) {
126 this.parameters.push( parameters[ i ] );
127 }
128 return this;
129 },
130
131 /**
132 * Convert message object to its string form based on current format.
133 *
134 * @return {string} Message as a string in the current form, or `<key>` if key
135 * does not exist.
136 */
137 toString: function () {
138 var text;
139
140 if ( !this.exists() ) {
141 // Use ⧼key⧽ as text if key does not exist
142 // Err on the side of safety, ensure that the output
143 // is always html safe in the event the message key is
144 // missing, since in that case its highly likely the
145 // message key is user-controlled.
146 // '⧼' is used instead of '<' to side-step any
147 // double-escaping issues.
148 // (Keep synchronised with Message::toString() in PHP.)
149 return '⧼' + mw.html.escape( this.key ) + '⧽';
150 }
151
152 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
153 text = this.parser();
154 }
155
156 if ( this.format === 'escaped' ) {
157 text = this.parser();
158 text = mw.html.escape( text );
159 }
160
161 return text;
162 },
163
164 /**
165 * Change format to 'parse' and convert message to string
166 *
167 * If jqueryMsg is loaded, this parses the message text from wikitext
168 * (where supported) to HTML
169 *
170 * Otherwise, it is equivalent to plain.
171 *
172 * @return {string} String form of parsed message
173 */
174 parse: function () {
175 this.format = 'parse';
176 return this.toString();
177 },
178
179 /**
180 * Change format to 'plain' and convert message to string
181 *
182 * This substitutes parameters, but otherwise does not change the
183 * message text.
184 *
185 * @return {string} String form of plain message
186 */
187 plain: function () {
188 this.format = 'plain';
189 return this.toString();
190 },
191
192 /**
193 * Change format to 'text' and convert message to string
194 *
195 * If jqueryMsg is loaded, {{-transformation is done where supported
196 * (such as {{plural:}}, {{gender:}}, {{int:}}).
197 *
198 * Otherwise, it is equivalent to plain
199 *
200 * @return {string} String form of text message
201 */
202 text: function () {
203 this.format = 'text';
204 return this.toString();
205 },
206
207 /**
208 * Change the format to 'escaped' and convert message to string
209 *
210 * This is equivalent to using the 'text' format (see #text), then
211 * HTML-escaping the output.
212 *
213 * @return {string} String form of html escaped message
214 */
215 escaped: function () {
216 this.format = 'escaped';
217 return this.toString();
218 },
219
220 /**
221 * Check if a message exists
222 *
223 * @see mw.Map#exists
224 * @return {boolean}
225 */
226 exists: function () {
227 if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
228 return true;
229 }
230 return this.map.exists( this.key );
231 }
232 };
233
234 /**
235 * @class mw
236 * @singleton
237 */
238
239 /**
240 * @inheritdoc mw.inspect#runReports
241 * @method
242 */
243 mw.inspect = function () {
244 var args = arguments;
245 // Lazy-load
246 mw.loader.using( 'mediawiki.inspect', function () {
247 mw.inspect.runReports.apply( mw.inspect, args );
248 } );
249 };
250
251 /**
252 * Replace $* with a list of parameters for &uselang=qqx.
253 *
254 * @private
255 * @since 1.33
256 * @param {string} formatString Format string
257 * @param {Array} parameters Values for $N replacements
258 * @return {string} Transformed format string
259 */
260 mw.internalDoTransformFormatForQqx = function ( formatString, parameters ) {
261 var parametersString;
262 if ( formatString.indexOf( '$*' ) !== -1 ) {
263 parametersString = '';
264 if ( parameters.length ) {
265 parametersString = ': ' + parameters.map( function ( _, i ) {
266 return '$' + ( i + 1 );
267 } ).join( ', ' );
268 }
269 return formatString.replace( '$*', parametersString );
270 }
271 return formatString;
272 };
273
274 /**
275 * Format a string. Replace $1, $2 ... $N with positional arguments.
276 *
277 * Used by Message#parser().
278 *
279 * @since 1.25
280 * @param {string} formatString Format string
281 * @param {...Mixed} parameters Values for $N replacements
282 * @return {string} Formatted string
283 */
284 mw.format = function ( formatString ) {
285 var parameters = slice.call( arguments, 1 );
286 formatString = mw.internalDoTransformFormatForQqx( formatString, parameters );
287 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
288 var index = parseInt( match, 10 ) - 1;
289 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
290 } );
291 };
292
293 // Expose Message constructor
294 mw.Message = Message;
295
296 /**
297 * Get a message object.
298 *
299 * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
300 *
301 * @see mw.Message
302 * @param {string} key Key of message to get
303 * @param {...Mixed} parameters Values for $N replacements
304 * @return {mw.Message}
305 */
306 mw.message = function ( key ) {
307 var parameters = slice.call( arguments, 1 );
308 return new Message( mw.messages, key, parameters );
309 };
310
311 /**
312 * Get a message string using the (default) 'text' format.
313 *
314 * Shortcut for `mw.message( key, parameters... ).text()`.
315 *
316 * @see mw.Message
317 * @param {string} key Key of message to get
318 * @param {...Mixed} parameters Values for $N replacements
319 * @return {string}
320 */
321 mw.msg = function () {
322 return mw.message.apply( mw.message, arguments ).toString();
323 };
324
325 /**
326 * Track an analytic event.
327 *
328 * This method provides a generic means for MediaWiki JavaScript code to capture state
329 * information for analysis. Each logged event specifies a string topic name that describes
330 * the kind of event that it is. Topic names consist of dot-separated path components,
331 * arranged from most general to most specific. Each path component should have a clear and
332 * well-defined purpose.
333 *
334 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
335 * events that match their subcription, including those that fired before the handler was
336 * bound.
337 *
338 * @param {string} topic Topic name
339 * @param {Object} [data] Data describing the event, encoded as an object
340 */
341 mw.track = function ( topic, data ) {
342 mwLoaderTrack( topic, data );
343 trackCallbacks.fire( mw.trackQueue );
344 };
345
346 /**
347 * Register a handler for subset of analytic events, specified by topic.
348 *
349 * Handlers will be called once for each tracked event, including any events that fired before the
350 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
351 * the exact time at which the event fired, a string 'topic' property naming the event, and a
352 * 'data' property which is an object of event-specific data. The event topic and event data are
353 * also passed to the callback as the first and second arguments, respectively.
354 *
355 * @param {string} topic Handle events whose name starts with this string prefix
356 * @param {Function} callback Handler to call for each matching tracked event
357 * @param {string} callback.topic
358 * @param {Object} [callback.data]
359 */
360 mw.trackSubscribe = function ( topic, callback ) {
361 var seen = 0;
362 function handler( trackQueue ) {
363 var event;
364 for ( ; seen < trackQueue.length; seen++ ) {
365 event = trackQueue[ seen ];
366 if ( event.topic.indexOf( topic ) === 0 ) {
367 callback.call( event, event.topic, event.data );
368 }
369 }
370 }
371
372 trackHandlers.push( [ handler, callback ] );
373
374 trackCallbacks.add( handler );
375 };
376
377 /**
378 * Stop handling events for a particular handler
379 *
380 * @param {Function} callback
381 */
382 mw.trackUnsubscribe = function ( callback ) {
383 trackHandlers = trackHandlers.filter( function ( fns ) {
384 if ( fns[ 1 ] === callback ) {
385 trackCallbacks.remove( fns[ 0 ] );
386 // Ensure the tuple is removed to avoid holding on to closures
387 return false;
388 }
389 return true;
390 } );
391 };
392
393 // Fire events from before track() triggered fire()
394 trackCallbacks.fire( mw.trackQueue );
395
396 /**
397 * Registry and firing of events.
398 *
399 * MediaWiki has various interface components that are extended, enhanced
400 * or manipulated in some other way by extensions, gadgets and even
401 * in core itself.
402 *
403 * This framework helps streamlining the timing of when these other
404 * code paths fire their plugins (instead of using document-ready,
405 * which can and should be limited to firing only once).
406 *
407 * Features like navigating to other wiki pages, previewing an edit
408 * and editing itself – without a refresh – can then retrigger these
409 * hooks accordingly to ensure everything still works as expected.
410 *
411 * Example usage:
412 *
413 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
414 * mw.hook( 'wikipage.content' ).fire( $content );
415 *
416 * Handlers can be added and fired for arbitrary event names at any time. The same
417 * event can be fired multiple times. The last run of an event is memorized
418 * (similar to `$(document).ready` and `$.Deferred().done`).
419 * This means if an event is fired, and a handler added afterwards, the added
420 * function will be fired right away with the last given event data.
421 *
422 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
423 * Thus allowing flexible use and optimal maintainability and authority control.
424 * You can pass around the `add` and/or `fire` method to another piece of code
425 * without it having to know the event name (or `mw.hook` for that matter).
426 *
427 * var h = mw.hook( 'bar.ready' );
428 * new mw.Foo( .. ).fetch( { callback: h.fire } );
429 *
430 * Note: Events are documented with an underscore instead of a dot in the event
431 * name due to jsduck not supporting dots in that position.
432 *
433 * @class mw.hook
434 */
435 mw.hook = ( function () {
436 var lists = Object.create( null );
437
438 /**
439 * Create an instance of mw.hook.
440 *
441 * @method hook
442 * @member mw
443 * @param {string} name Name of hook.
444 * @return {mw.hook}
445 */
446 return function ( name ) {
447 var list = lists[ name ] || ( lists[ name ] = $.Callbacks( 'memory' ) );
448
449 return {
450 /**
451 * Register a hook handler
452 *
453 * @param {...Function} handler Function to bind.
454 * @chainable
455 */
456 add: list.add,
457
458 /**
459 * Unregister a hook handler
460 *
461 * @param {...Function} handler Function to unbind.
462 * @chainable
463 */
464 remove: list.remove,
465
466 /**
467 * Run a hook.
468 *
469 * @param {...Mixed} data
470 * @return {mw.hook}
471 * @chainable
472 */
473 fire: function () {
474 return list.fireWith.call( this, null, slice.call( arguments ) );
475 }
476 };
477 };
478 }() );
479
480 /**
481 * HTML construction helper functions
482 *
483 * @example
484 *
485 * var Html, output;
486 *
487 * Html = mw.html;
488 * output = Html.element( 'div', {}, new Html.Raw(
489 * Html.element( 'img', { src: '<' } )
490 * ) );
491 * mw.log( output ); // <div><img src="&lt;"/></div>
492 *
493 * @class mw.html
494 * @singleton
495 */
496 mw.html = ( function () {
497 function escapeCallback( s ) {
498 switch ( s ) {
499 case '\'':
500 return '&#039;';
501 case '"':
502 return '&quot;';
503 case '<':
504 return '&lt;';
505 case '>':
506 return '&gt;';
507 case '&':
508 return '&amp;';
509 }
510 }
511
512 return {
513 /**
514 * Escape a string for HTML.
515 *
516 * Converts special characters to HTML entities.
517 *
518 * mw.html.escape( '< > \' & "' );
519 * // Returns &lt; &gt; &#039; &amp; &quot;
520 *
521 * @param {string} s The string to escape
522 * @return {string} HTML
523 */
524 escape: function ( s ) {
525 return s.replace( /['"<>&]/g, escapeCallback );
526 },
527
528 /**
529 * Create an HTML element string, with safe escaping.
530 *
531 * @param {string} name The tag name.
532 * @param {Object} [attrs] An object with members mapping element names to values
533 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
534 *
535 * - string: Text to be escaped.
536 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
537 * - this.Raw: The raw value is directly included.
538 * - this.Cdata: The raw value is directly included. An exception is
539 * thrown if it contains any illegal ETAGO delimiter.
540 * See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
541 * @return {string} HTML
542 */
543 element: function ( name, attrs, contents ) {
544 var v, attrName, s = '<' + name;
545
546 if ( attrs ) {
547 for ( attrName in attrs ) {
548 v = attrs[ attrName ];
549 // Convert name=true, to name=name
550 if ( v === true ) {
551 v = attrName;
552 // Skip name=false
553 } else if ( v === false ) {
554 continue;
555 }
556 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
557 }
558 }
559 if ( contents === undefined || contents === null ) {
560 // Self close tag
561 s += '/>';
562 return s;
563 }
564 // Regular open tag
565 s += '>';
566 switch ( typeof contents ) {
567 case 'string':
568 // Escaped
569 s += this.escape( contents );
570 break;
571 case 'number':
572 case 'boolean':
573 // Convert to string
574 s += String( contents );
575 break;
576 default:
577 if ( contents instanceof this.Raw ) {
578 // Raw HTML inclusion
579 s += contents.value;
580 } else if ( contents instanceof this.Cdata ) {
581 // CDATA
582 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
583 throw new Error( 'Illegal end tag found in CDATA' );
584 }
585 s += contents.value;
586 } else {
587 throw new Error( 'Invalid type of contents' );
588 }
589 }
590 s += '</' + name + '>';
591 return s;
592 },
593
594 /**
595 * Wrapper object for raw HTML passed to mw.html.element().
596 *
597 * @class mw.html.Raw
598 * @constructor
599 * @param {string} value
600 */
601 Raw: function ( value ) {
602 this.value = value;
603 },
604
605 /**
606 * Wrapper object for CDATA element contents passed to mw.html.element()
607 *
608 * @class mw.html.Cdata
609 * @constructor
610 * @param {string} value
611 */
612 Cdata: function ( value ) {
613 this.value = value;
614 }
615 };
616 }() );
617
618 /**
619 * Execute a function as soon as one or more required modules are ready.
620 *
621 * Example of inline dependency on OOjs:
622 *
623 * mw.loader.using( 'oojs', function () {
624 * OO.compare( [ 1 ], [ 1 ] );
625 * } );
626 *
627 * Example of inline dependency obtained via `require()`:
628 *
629 * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
630 * var util = require( 'mediawiki.util' );
631 * } );
632 *
633 * Since MediaWiki 1.23 this also returns a promise.
634 *
635 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
636 *
637 * @member mw.loader
638 * @param {string|Array} dependencies Module name or array of modules names the
639 * callback depends on to be ready before executing
640 * @param {Function} [ready] Callback to execute when all dependencies are ready
641 * @param {Function} [error] Callback to execute if one or more dependencies failed
642 * @return {jQuery.Promise} With a `require` function
643 */
644 mw.loader.using = function ( dependencies, ready, error ) {
645 var deferred = $.Deferred();
646
647 // Allow calling with a single dependency as a string
648 if ( !Array.isArray( dependencies ) ) {
649 dependencies = [ dependencies ];
650 }
651
652 if ( ready ) {
653 deferred.done( ready );
654 }
655 if ( error ) {
656 deferred.fail( error );
657 }
658
659 try {
660 // Resolve entire dependency map
661 dependencies = mw.loader.resolve( dependencies );
662 } catch ( e ) {
663 return deferred.reject( e ).promise();
664 }
665
666 mw.loader.enqueue(
667 dependencies,
668 function () { deferred.resolve( mw.loader.require ); },
669 deferred.reject
670 );
671
672 return deferred.promise();
673 };
674
675 /**
676 * Load a script by URL.
677 *
678 * Example:
679 *
680 * mw.loader.getScript(
681 * 'https://example.org/x-1.0.0.js'
682 * )
683 * .then( function () {
684 * // Script succeeded. You can use X now.
685 * }, function ( e ) {
686 * // Script failed. X is not avaiable
687 * mw.log.error( e.message ); // => "Failed to load script"
688 * } );
689 * } );
690 *
691 * @member mw.loader
692 * @param {string} url Script URL
693 * @return {jQuery.Promise} Resolved when the script is loaded
694 */
695 mw.loader.getScript = function ( url ) {
696 return $.ajax( url, { dataType: 'script', cache: true } )
697 .catch( function () {
698 throw new Error( 'Failed to load script' );
699 } );
700 };
701
702 // Alias $j to jQuery for backwards compatibility
703 // @deprecated since 1.23 Use $ or jQuery instead
704 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
705
706 // Process callbacks for Grade A that require modules.
707 queue = window.RLQ;
708 // Replace temporary RLQ implementation from startup.js with the
709 // final implementation that also processes callbacks that can
710 // require modules. It must also support late arrivals of
711 // plain callbacks. (T208093)
712 window.RLQ = {
713 push: function ( entry ) {
714 if ( typeof entry === 'function' ) {
715 entry();
716 } else {
717 mw.loader.using( entry[ 0 ], entry[ 1 ] );
718 }
719 }
720 };
721 while ( queue[ 0 ] ) {
722 window.RLQ.push( queue.shift() );
723 }
724 }() );