Merge "Use PHP_OS rather than php_uname, which may be disabled"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.js
1 /**
2 * Base library for MediaWiki.
3 *
4 * Exposed globally as `mediaWiki` with `mw` as shortcut.
5 *
6 * @class mw
7 * @alternateClassName mediaWiki
8 * @singleton
9 */
10 ( function ( $ ) {
11 'use strict';
12
13 var mw,
14 hasOwn = Object.prototype.hasOwnProperty,
15 slice = Array.prototype.slice,
16 trackCallbacks = $.Callbacks( 'memory' ),
17 trackQueue = [];
18
19 /**
20 * Create an object that can be read from or written to from methods that allow
21 * interaction both with single and multiple properties at once.
22 *
23 * @example
24 *
25 * var collection, query, results;
26 *
27 * // Create your address book
28 * collection = new mw.Map();
29 *
30 * // This data could be coming from an external source (eg. API/AJAX)
31 * collection.set( {
32 * 'John Doe': 'john@example.org',
33 * 'Jane Doe': 'jane@example.org',
34 * 'George van Halen': 'gvanhalen@example.org'
35 * } );
36 *
37 * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
38 *
39 * // You can detect missing keys first
40 * if ( !collection.exists( wanted ) ) {
41 * // One or more are missing (in this case: "Daniel Jackson")
42 * mw.log( 'One or more names were not found in your address book' );
43 * }
44 *
45 * // Or just let it give you what it can. Optionally fill in from a default.
46 * results = collection.get( wanted, 'nobody@example.com' );
47 * mw.log( results['Jane Doe'] ); // "jane@example.org"
48 * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
49 *
50 * @class mw.Map
51 *
52 * @constructor
53 * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
54 * empty object.
55 * For backwards-compatibility with mw.config, this can also be `true` in which case values
56 * are copied to the Window object as global variables (T72470). Values are copied in
57 * one direction only. Changes to globals are not reflected in the map.
58 */
59 function Map( values ) {
60 if ( values === true ) {
61 this.values = {};
62
63 // Override #set to also set the global variable
64 this.set = function ( selection, value ) {
65 var s;
66
67 if ( $.isPlainObject( selection ) ) {
68 for ( s in selection ) {
69 setGlobalMapValue( this, s, selection[s] );
70 }
71 return true;
72 }
73 if ( typeof selection === 'string' && arguments.length ) {
74 setGlobalMapValue( this, selection, value );
75 return true;
76 }
77 return false;
78 };
79
80 return;
81 }
82
83 this.values = values || {};
84 }
85
86 /**
87 * Alias property to the global object.
88 *
89 * @private
90 * @static
91 * @param {mw.Map} map
92 * @param {string} key
93 * @param {Mixed} value
94 */
95 function setGlobalMapValue( map, key, value ) {
96 map.values[key] = value;
97 mw.log.deprecate(
98 window,
99 key,
100 value,
101 // Deprecation notice for mw.config globals (T58550, T72470)
102 map === mw.config && 'Use mw.config instead.'
103 );
104 }
105
106 Map.prototype = {
107 /**
108 * Get the value of one or more keys.
109 *
110 * If called with no arguments, all values are returned.
111 *
112 * @param {string|Array} [selection] Key or array of keys to retrieve values for.
113 * @param {Mixed} [fallback=null] Value for keys that don't exist.
114 * @return {Mixed|Object| null} If selection was a string, returns the value,
115 * If selection was an array, returns an object of key/values.
116 * If no selection is passed, the 'values' container is returned. (Beware that,
117 * as is the default in JavaScript, the object is returned by reference.)
118 */
119 get: function ( selection, fallback ) {
120 var results, i;
121 // If we only do this in the `return` block, it'll fail for the
122 // call to get() from the mutli-selection block.
123 fallback = arguments.length > 1 ? fallback : null;
124
125 if ( $.isArray( selection ) ) {
126 selection = slice.call( selection );
127 results = {};
128 for ( i = 0; i < selection.length; i++ ) {
129 results[selection[i]] = this.get( selection[i], fallback );
130 }
131 return results;
132 }
133
134 if ( typeof selection === 'string' ) {
135 if ( !hasOwn.call( this.values, selection ) ) {
136 return fallback;
137 }
138 return this.values[selection];
139 }
140
141 if ( selection === undefined ) {
142 return this.values;
143 }
144
145 // Invalid selection key
146 return null;
147 },
148
149 /**
150 * Set one or more key/value pairs.
151 *
152 * @param {string|Object} selection Key to set value for, or object mapping keys to values
153 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
154 * @return {boolean} True on success, false on failure
155 */
156 set: function ( selection, value ) {
157 var s;
158
159 if ( $.isPlainObject( selection ) ) {
160 for ( s in selection ) {
161 this.values[s] = selection[s];
162 }
163 return true;
164 }
165 if ( typeof selection === 'string' && arguments.length > 1 ) {
166 this.values[selection] = value;
167 return true;
168 }
169 return false;
170 },
171
172 /**
173 * Check if one or more keys exist.
174 *
175 * @param {Mixed} selection Key or array of keys to check
176 * @return {boolean} True if the key(s) exist
177 */
178 exists: function ( selection ) {
179 var s;
180
181 if ( $.isArray( selection ) ) {
182 for ( s = 0; s < selection.length; s++ ) {
183 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
184 return false;
185 }
186 }
187 return true;
188 }
189 return typeof selection === 'string' && hasOwn.call( this.values, selection );
190 }
191 };
192
193 /**
194 * Object constructor for messages.
195 *
196 * Similar to the Message class in MediaWiki PHP.
197 *
198 * Format defaults to 'text'.
199 *
200 * @example
201 *
202 * var obj, str;
203 * mw.messages.set( {
204 * 'hello': 'Hello world',
205 * 'hello-user': 'Hello, $1!',
206 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
207 * } );
208 *
209 * obj = new mw.Message( mw.messages, 'hello' );
210 * mw.log( obj.text() );
211 * // Hello world
212 *
213 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
214 * mw.log( obj.text() );
215 * // Hello, John Doe!
216 *
217 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
218 * mw.log( obj.text() );
219 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
220 *
221 * // Using mw.message shortcut
222 * obj = mw.message( 'hello-user', 'John Doe' );
223 * mw.log( obj.text() );
224 * // Hello, John Doe!
225 *
226 * // Using mw.msg shortcut
227 * str = mw.msg( 'hello-user', 'John Doe' );
228 * mw.log( str );
229 * // Hello, John Doe!
230 *
231 * // Different formats
232 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
233 *
234 * obj.format = 'text';
235 * str = obj.toString();
236 * // Same as:
237 * str = obj.text();
238 *
239 * mw.log( str );
240 * // Hello, John "Wiki" <3 Doe!
241 *
242 * mw.log( obj.escaped() );
243 * // Hello, John &quot;Wiki&quot; &lt;3 Doe!
244 *
245 * @class mw.Message
246 *
247 * @constructor
248 * @param {mw.Map} map Message store
249 * @param {string} key
250 * @param {Array} [parameters]
251 */
252 function Message( map, key, parameters ) {
253 this.format = 'text';
254 this.map = map;
255 this.key = key;
256 this.parameters = parameters === undefined ? [] : slice.call( parameters );
257 return this;
258 }
259
260 Message.prototype = {
261 /**
262 * Get parsed contents of the message.
263 *
264 * The default parser does simple $N replacements and nothing else.
265 * This may be overridden to provide a more complex message parser.
266 * The primary override is in the mediawiki.jqueryMsg module.
267 *
268 * This function will not be called for nonexistent messages.
269 *
270 * @return {string} Parsed message
271 */
272 parser: function () {
273 return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
274 },
275
276 /**
277 * Add (does not replace) parameters for `N$` placeholder values.
278 *
279 * @param {Array} parameters
280 * @chainable
281 */
282 params: function ( parameters ) {
283 var i;
284 for ( i = 0; i < parameters.length; i += 1 ) {
285 this.parameters.push( parameters[i] );
286 }
287 return this;
288 },
289
290 /**
291 * Convert message object to its string form based on current format.
292 *
293 * @return {string} Message as a string in the current form, or `<key>` if key
294 * does not exist.
295 */
296 toString: function () {
297 var text;
298
299 if ( !this.exists() ) {
300 // Use <key> as text if key does not exist
301 if ( this.format === 'escaped' || this.format === 'parse' ) {
302 // format 'escaped' and 'parse' need to have the brackets and key html escaped
303 return mw.html.escape( '<' + this.key + '>' );
304 }
305 return '<' + this.key + '>';
306 }
307
308 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
309 text = this.parser();
310 }
311
312 if ( this.format === 'escaped' ) {
313 text = this.parser();
314 text = mw.html.escape( text );
315 }
316
317 return text;
318 },
319
320 /**
321 * Change format to 'parse' and convert message to string
322 *
323 * If jqueryMsg is loaded, this parses the message text from wikitext
324 * (where supported) to HTML
325 *
326 * Otherwise, it is equivalent to plain.
327 *
328 * @return {string} String form of parsed message
329 */
330 parse: function () {
331 this.format = 'parse';
332 return this.toString();
333 },
334
335 /**
336 * Change format to 'plain' and convert message to string
337 *
338 * This substitutes parameters, but otherwise does not change the
339 * message text.
340 *
341 * @return {string} String form of plain message
342 */
343 plain: function () {
344 this.format = 'plain';
345 return this.toString();
346 },
347
348 /**
349 * Change format to 'text' and convert message to string
350 *
351 * If jqueryMsg is loaded, {{-transformation is done where supported
352 * (such as {{plural:}}, {{gender:}}, {{int:}}).
353 *
354 * Otherwise, it is equivalent to plain
355 *
356 * @return {string} String form of text message
357 */
358 text: function () {
359 this.format = 'text';
360 return this.toString();
361 },
362
363 /**
364 * Change the format to 'escaped' and convert message to string
365 *
366 * This is equivalent to using the 'text' format (see #text), then
367 * HTML-escaping the output.
368 *
369 * @return {string} String form of html escaped message
370 */
371 escaped: function () {
372 this.format = 'escaped';
373 return this.toString();
374 },
375
376 /**
377 * Check if a message exists
378 *
379 * @see mw.Map#exists
380 * @return {boolean}
381 */
382 exists: function () {
383 return this.map.exists( this.key );
384 }
385 };
386
387 /**
388 * @class mw
389 */
390 mw = {
391
392 /**
393 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
394 *
395 * On browsers that implement the Navigation Timing API, this function will produce floating-point
396 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
397 * it will fall back to using `Date`.
398 *
399 * @return {number} Current time
400 */
401 now: ( function () {
402 var perf = window.performance,
403 navStart = perf && perf.timing && perf.timing.navigationStart;
404 return navStart && typeof perf.now === 'function' ?
405 function () { return navStart + perf.now(); } :
406 function () { return +new Date(); };
407 }() ),
408
409 /**
410 * Format a string. Replace $1, $2 ... $N with positional arguments.
411 *
412 * Used by Message#parser().
413 *
414 * @since 1.25
415 * @param {string} fmt Format string
416 * @param {Mixed...} parameters Values for $N replacements
417 * @return {string} Formatted string
418 */
419 format: function ( formatString ) {
420 var parameters = slice.call( arguments, 1 );
421 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
422 var index = parseInt( match, 10 ) - 1;
423 return parameters[index] !== undefined ? parameters[index] : '$' + match;
424 } );
425 },
426
427 /**
428 * Track an analytic event.
429 *
430 * This method provides a generic means for MediaWiki JavaScript code to capture state
431 * information for analysis. Each logged event specifies a string topic name that describes
432 * the kind of event that it is. Topic names consist of dot-separated path components,
433 * arranged from most general to most specific. Each path component should have a clear and
434 * well-defined purpose.
435 *
436 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
437 * events that match their subcription, including those that fired before the handler was
438 * bound.
439 *
440 * @param {string} topic Topic name
441 * @param {Object} [data] Data describing the event, encoded as an object
442 */
443 track: function ( topic, data ) {
444 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
445 trackCallbacks.fire( trackQueue );
446 },
447
448 /**
449 * Register a handler for subset of analytic events, specified by topic.
450 *
451 * Handlers will be called once for each tracked event, including any events that fired before the
452 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
453 * the exact time at which the event fired, a string 'topic' property naming the event, and a
454 * 'data' property which is an object of event-specific data. The event topic and event data are
455 * also passed to the callback as the first and second arguments, respectively.
456 *
457 * @param {string} topic Handle events whose name starts with this string prefix
458 * @param {Function} callback Handler to call for each matching tracked event
459 * @param {string} callback.topic
460 * @param {Object} [callback.data]
461 */
462 trackSubscribe: function ( topic, callback ) {
463 var seen = 0;
464
465 trackCallbacks.add( function ( trackQueue ) {
466 var event;
467 for ( ; seen < trackQueue.length; seen++ ) {
468 event = trackQueue[ seen ];
469 if ( event.topic.indexOf( topic ) === 0 ) {
470 callback.call( event, event.topic, event.data );
471 }
472 }
473 } );
474 },
475
476 // Expose Map constructor
477 Map: Map,
478
479 // Expose Message constructor
480 Message: Message,
481
482 /**
483 * Map of configuration values.
484 *
485 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
486 * on mediawiki.org.
487 *
488 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
489 * global `window` object.
490 *
491 * @property {mw.Map} config
492 */
493 // Dummy placeholder later assigned in ResourceLoaderStartUpModule
494 config: null,
495
496 /**
497 * Empty object for third-party libraries, for cases where you don't
498 * want to add a new global, or the global is bad and needs containment
499 * or wrapping.
500 *
501 * @property
502 */
503 libs: {},
504
505 /**
506 * Access container for deprecated functionality that can be moved from
507 * from their legacy location and attached to this object (e.g. a global
508 * function that is deprecated and as stop-gap can be exposed through here).
509 *
510 * This was reserved for future use but never ended up being used.
511 *
512 * @deprecated since 1.22 Let deprecated identifiers keep their original name
513 * and use mw.log#deprecate to create an access container for tracking.
514 * @property
515 */
516 legacy: {},
517
518 /**
519 * Store for messages.
520 *
521 * @property {mw.Map}
522 */
523 messages: new Map(),
524
525 /**
526 * Store for templates associated with a module.
527 *
528 * @property {mw.Map}
529 */
530 templates: new Map(),
531
532 /**
533 * Get a message object.
534 *
535 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
536 *
537 * @see mw.Message
538 * @param {string} key Key of message to get
539 * @param {Mixed...} parameters Values for $N replacements
540 * @return {mw.Message}
541 */
542 message: function ( key ) {
543 var parameters = slice.call( arguments, 1 );
544 return new Message( mw.messages, key, parameters );
545 },
546
547 /**
548 * Get a message string using the (default) 'text' format.
549 *
550 * Shortcut for `mw.message( key, parameters... ).text()`.
551 *
552 * @see mw.Message
553 * @param {string} key Key of message to get
554 * @param {Mixed...} parameters Values for $N replacements
555 * @return {string}
556 */
557 msg: function () {
558 return mw.message.apply( mw.message, arguments ).toString();
559 },
560
561 /**
562 * Dummy placeholder for {@link mw.log}
563 * @method
564 */
565 log: ( function () {
566 // Also update the restoration of methods in mediawiki.log.js
567 // when adding or removing methods here.
568 var log = function () {};
569
570 /**
571 * @class mw.log
572 * @singleton
573 */
574
575 /**
576 * Write a message the console's warning channel.
577 * Also logs a stacktrace for easier debugging.
578 * Actions not supported by the browser console are silently ignored.
579 *
580 * @param {string...} msg Messages to output to console
581 */
582 log.warn = function () {
583 var console = window.console;
584 if ( console && console.warn && console.warn.apply ) {
585 console.warn.apply( console, arguments );
586 if ( console.trace ) {
587 console.trace();
588 }
589 }
590 };
591
592 /**
593 * Create a property in a host object that, when accessed, will produce
594 * a deprecation warning in the console with backtrace.
595 *
596 * @param {Object} obj Host object of deprecated property
597 * @param {string} key Name of property to create in `obj`
598 * @param {Mixed} val The value this property should return when accessed
599 * @param {string} [msg] Optional text to include in the deprecation message
600 */
601 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
602 obj[key] = val;
603 } : function ( obj, key, val, msg ) {
604 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
605 // Support: IE8
606 // Can throw on Object.defineProperty.
607 try {
608 Object.defineProperty( obj, key, {
609 configurable: true,
610 enumerable: true,
611 get: function () {
612 mw.track( 'mw.deprecate', key );
613 mw.log.warn( msg );
614 return val;
615 },
616 set: function ( newVal ) {
617 mw.track( 'mw.deprecate', key );
618 mw.log.warn( msg );
619 val = newVal;
620 }
621 } );
622 } catch ( err ) {
623 // Fallback to creating a copy of the value to the object.
624 obj[key] = val;
625 }
626 };
627
628 return log;
629 }() ),
630
631 /**
632 * Client for ResourceLoader server end point.
633 *
634 * This client is in charge of maintaining the module registry and state
635 * machine, initiating network (batch) requests for loading modules, as
636 * well as dependency resolution and execution of source code.
637 *
638 * For more information, refer to
639 * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
640 *
641 * @class mw.loader
642 * @singleton
643 */
644 loader: ( function () {
645
646 /**
647 * Fired via mw.track on various resource loading errors.
648 *
649 * @event resourceloader_exception
650 * @param {Error|Mixed} e The error that was thrown. Almost always an Error
651 * object, but in theory module code could manually throw something else, and that
652 * might also end up here.
653 * @param {string} [module] Name of the module which caused the error. Omitted if the
654 * error is not module-related or the module cannot be easily identified due to
655 * batched handling.
656 * @param {string} source Source of the error. Possible values:
657 *
658 * - style: stylesheet error (only affects old IE where a special style loading method
659 * is used)
660 * - load-callback: exception thrown by user callback
661 * - module-execute: exception thrown by module code
662 * - store-eval: could not evaluate module code cached in localStorage
663 * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
664 * - store-localstorage-json: JSON conversion error in mw.loader.store.set
665 * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
666 */
667
668 /**
669 * Fired via mw.track on resource loading error conditions.
670 *
671 * @event resourceloader_assert
672 * @param {string} source Source of the error. Possible values:
673 *
674 * - bug-T59567: failed to cache script due to an Opera function -> string conversion
675 * bug; see <https://phabricator.wikimedia.org/T59567> for details
676 */
677
678 /**
679 * Mapping of registered modules.
680 *
681 * See #implement for exact details on support for script, style and messages.
682 *
683 * Format:
684 *
685 * {
686 * 'moduleName': {
687 * // From startup mdoule
688 * 'version': ############## (unix timestamp)
689 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
690 * 'group': 'somegroup', (or) null
691 * 'source': 'local', (or) 'anotherwiki'
692 * 'skip': 'return !!window.Example', (or) null
693 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
694 *
695 * // Added during implementation
696 * 'skipped': true
697 * 'script': ...
698 * 'style': ...
699 * 'messages': { 'key': 'value' }
700 * }
701 * }
702 *
703 * @property
704 * @private
705 */
706 var registry = {},
707 // Mapping of sources, keyed by source-id, values are strings.
708 //
709 // Format:
710 //
711 // {
712 // 'sourceId': 'http://example.org/w/load.php'
713 // }
714 //
715 sources = {},
716
717 // List of modules which will be loaded as when ready
718 batch = [],
719
720 // List of modules to be loaded
721 queue = [],
722
723 // List of callback functions waiting for modules to be ready to be called
724 jobs = [],
725
726 // Selector cache for the marker element. Use getMarker() to get/use the marker!
727 $marker = null,
728
729 // Buffer for #addEmbeddedCSS
730 cssBuffer = '',
731
732 // Callbacks for #addEmbeddedCSS
733 cssCallbacks = $.Callbacks();
734
735 function getMarker() {
736 if ( !$marker ) {
737 // Cache
738 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
739 if ( !$marker.length ) {
740 mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
741 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
742 }
743 }
744 return $marker;
745 }
746
747 /**
748 * Create a new style element and add it to the DOM.
749 *
750 * @private
751 * @param {string} text CSS text
752 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
753 * should be inserted before
754 * @return {HTMLElement} Reference to the created style element
755 */
756 function newStyleTag( text, nextnode ) {
757 var s = document.createElement( 'style' );
758 // Support: IE
759 // Must attach to document before setting cssText (bug 33305)
760 if ( nextnode ) {
761 $( nextnode ).before( s );
762 } else {
763 document.getElementsByTagName( 'head' )[0].appendChild( s );
764 }
765 if ( s.styleSheet ) {
766 // Support: IE6-10
767 // Old IE ignores appended text nodes, access stylesheet directly.
768 s.styleSheet.cssText = text;
769 } else {
770 // Standard behaviour
771 s.appendChild( document.createTextNode( text ) );
772 }
773 return s;
774 }
775
776 /**
777 * Add a bit of CSS text to the current browser page.
778 *
779 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
780 * or create a new one based on whether the given `cssText` is safe for extension.
781 *
782 * @param {string} [cssText=cssBuffer] If called without cssText,
783 * the internal buffer will be inserted instead.
784 * @param {Function} [callback]
785 */
786 function addEmbeddedCSS( cssText, callback ) {
787 var $style, styleEl;
788
789 if ( callback ) {
790 cssCallbacks.add( callback );
791 }
792
793 // Yield once before inserting the <style> tag. There are likely
794 // more calls coming up which we can combine this way.
795 // Appending a stylesheet and waiting for the browser to repaint
796 // is fairly expensive, this reduces that (bug 45810)
797 if ( cssText ) {
798 // Be careful not to extend the buffer with css that needs a new stylesheet.
799 // cssText containing `@import` rules needs to go at the start of a buffer,
800 // since those only work when placed at the start of a stylesheet; bug 35562.
801 if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
802 // Linebreak for somewhat distinguishable sections
803 // (the rl-cachekey comment separating each)
804 cssBuffer += '\n' + cssText;
805 // TODO: Use requestAnimationFrame in the future which will
806 // perform even better by not injecting styles while the browser
807 // is painting.
808 setTimeout( function () {
809 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
810 // (below version 13) has the non-standard behaviour of passing a
811 // numerical "lateness" value as first argument to this callback
812 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
813 addEmbeddedCSS();
814 } );
815 return;
816 }
817
818 // This is a delayed call and we got a buffer still
819 } else if ( cssBuffer ) {
820 cssText = cssBuffer;
821 cssBuffer = '';
822
823 } else {
824 // This is a delayed call, but buffer was already cleared by
825 // another delayed call.
826 return;
827 }
828
829 // By default, always create a new <style>. Appending text to a <style>
830 // tag is bad as it means the contents have to be re-parsed (bug 45810).
831 //
832 // Except, of course, in IE 9 and below. In there we default to re-using and
833 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
834 if ( 'documentMode' in document && document.documentMode <= 9 ) {
835
836 $style = getMarker().prev();
837 // Verify that the element before the marker actually is a
838 // <style> tag and one that came from ResourceLoader
839 // (not some other style tag or even a `<meta>` or `<script>`).
840 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
841 // There's already a dynamic <style> tag present and
842 // we are able to append more to it.
843 styleEl = $style.get( 0 );
844 // Support: IE6-10
845 if ( styleEl.styleSheet ) {
846 try {
847 styleEl.styleSheet.cssText += cssText;
848 } catch ( e ) {
849 mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } );
850 }
851 } else {
852 styleEl.appendChild( document.createTextNode( cssText ) );
853 }
854 cssCallbacks.fire().empty();
855 return;
856 }
857 }
858
859 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
860
861 cssCallbacks.fire().empty();
862 }
863
864 /**
865 * Zero-pad three numbers.
866 *
867 * @private
868 * @param {number} a
869 * @param {number} b
870 * @param {number} c
871 * @return {string}
872 */
873 function pad( a, b, c ) {
874 return (
875 ( a < 10 ? '0' : '' ) + a +
876 ( b < 10 ? '0' : '' ) + b +
877 ( c < 10 ? '0' : '' ) + c
878 );
879 }
880
881 /**
882 * Convert UNIX timestamp to ISO8601 format.
883 *
884 * @private
885 * @param {number} timestamp UNIX timestamp
886 */
887 function formatVersionNumber( timestamp ) {
888 var d = new Date();
889 d.setTime( timestamp * 1000 );
890 return [
891 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
892 'T',
893 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
894 'Z'
895 ].join( '' );
896 }
897
898 /**
899 * Resolve dependencies and detect circular references.
900 *
901 * @private
902 * @param {string} module Name of the top-level module whose dependencies shall be
903 * resolved and sorted.
904 * @param {Array} resolved Returns a topological sort of the given module and its
905 * dependencies, such that later modules depend on earlier modules. The array
906 * contains the module names. If the array contains already some module names,
907 * this function appends its result to the pre-existing array.
908 * @param {Object} [unresolved] Hash used to track the current dependency
909 * chain; used to report loops in the dependency graph.
910 * @throws {Error} If any unregistered module or a dependency loop is encountered
911 */
912 function sortDependencies( module, resolved, unresolved ) {
913 var n, deps, len, skip;
914
915 if ( !hasOwn.call( registry, module ) ) {
916 throw new Error( 'Unknown dependency: ' + module );
917 }
918
919 if ( registry[module].skip !== null ) {
920 /*jshint evil:true */
921 skip = new Function( registry[module].skip );
922 registry[module].skip = null;
923 if ( skip() ) {
924 registry[module].skipped = true;
925 registry[module].dependencies = [];
926 registry[module].state = 'ready';
927 handlePending( module );
928 return;
929 }
930 }
931
932 // Resolves dynamic loader function and replaces it with its own results
933 if ( $.isFunction( registry[module].dependencies ) ) {
934 registry[module].dependencies = registry[module].dependencies();
935 // Ensures the module's dependencies are always in an array
936 if ( typeof registry[module].dependencies !== 'object' ) {
937 registry[module].dependencies = [registry[module].dependencies];
938 }
939 }
940 if ( $.inArray( module, resolved ) !== -1 ) {
941 // Module already resolved; nothing to do
942 return;
943 }
944 // Create unresolved if not passed in
945 if ( !unresolved ) {
946 unresolved = {};
947 }
948 // Tracks down dependencies
949 deps = registry[module].dependencies;
950 len = deps.length;
951 for ( n = 0; n < len; n += 1 ) {
952 if ( $.inArray( deps[n], resolved ) === -1 ) {
953 if ( unresolved[deps[n]] ) {
954 throw new Error(
955 'Circular reference detected: ' + module +
956 ' -> ' + deps[n]
957 );
958 }
959
960 // Add to unresolved
961 unresolved[module] = true;
962 sortDependencies( deps[n], resolved, unresolved );
963 delete unresolved[module];
964 }
965 }
966 resolved[resolved.length] = module;
967 }
968
969 /**
970 * Get a list of module names that a module depends on in their proper dependency
971 * order.
972 *
973 * @private
974 * @param {string[]} module Array of string module names
975 * @return {Array} List of dependencies, including 'module'.
976 */
977 function resolve( modules ) {
978 var resolved = [];
979 $.each( modules, function ( idx, module ) {
980 sortDependencies( module, resolved );
981 } );
982 return resolved;
983 }
984
985 /**
986 * Determine whether all dependencies are in state 'ready', which means we may
987 * execute the module or job now.
988 *
989 * @private
990 * @param {Array} module Names of modules to be checked
991 * @return {boolean} True if all modules are in state 'ready', false otherwise
992 */
993 function allReady( modules ) {
994 var i;
995 for ( i = 0; i < modules.length; i++ ) {
996 if ( mw.loader.getState( modules[i] ) !== 'ready' ) {
997 return false;
998 }
999 }
1000 return true;
1001 }
1002
1003 /**
1004 * Determine whether all dependencies are in state 'ready', which means we may
1005 * execute the module or job now.
1006 *
1007 * @private
1008 * @param {Array} modules Names of modules to be checked
1009 * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
1010 */
1011 function anyFailed( modules ) {
1012 var i, state;
1013 for ( i = 0; i < modules.length; i++ ) {
1014 state = mw.loader.getState( modules[i] );
1015 if ( state === 'error' || state === 'missing' ) {
1016 return true;
1017 }
1018 }
1019 return false;
1020 }
1021
1022 /**
1023 * A module has entered state 'ready', 'error', or 'missing'. Automatically update
1024 * pending jobs and modules that depend upon this module. If the given module failed,
1025 * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
1026 * all jobs/modules now having their dependencies satisfied.
1027 *
1028 * Jobs that depend on a failed module, will have their error callback ran (if any).
1029 *
1030 * @private
1031 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
1032 */
1033 function handlePending( module ) {
1034 var j, job, hasErrors, m, stateChange;
1035
1036 if ( registry[module].state === 'error' || registry[module].state === 'missing' ) {
1037 // If the current module failed, mark all dependent modules also as failed.
1038 // Iterate until steady-state to propagate the error state upwards in the
1039 // dependency tree.
1040 do {
1041 stateChange = false;
1042 for ( m in registry ) {
1043 if ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) {
1044 if ( anyFailed( registry[m].dependencies ) ) {
1045 registry[m].state = 'error';
1046 stateChange = true;
1047 }
1048 }
1049 }
1050 } while ( stateChange );
1051 }
1052
1053 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1054 for ( j = 0; j < jobs.length; j += 1 ) {
1055 hasErrors = anyFailed( jobs[j].dependencies );
1056 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
1057 // All dependencies satisfied, or some have errors
1058 job = jobs[j];
1059 jobs.splice( j, 1 );
1060 j -= 1;
1061 try {
1062 if ( hasErrors ) {
1063 if ( $.isFunction( job.error ) ) {
1064 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
1065 }
1066 } else {
1067 if ( $.isFunction( job.ready ) ) {
1068 job.ready();
1069 }
1070 }
1071 } catch ( e ) {
1072 // A user-defined callback raised an exception.
1073 // Swallow it to protect our state machine!
1074 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
1075 }
1076 }
1077 }
1078
1079 if ( registry[module].state === 'ready' ) {
1080 // The current module became 'ready'. Set it in the module store, and recursively execute all
1081 // dependent modules that are loaded and now have all dependencies satisfied.
1082 mw.loader.store.set( module, registry[module] );
1083 for ( m in registry ) {
1084 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
1085 execute( m );
1086 }
1087 }
1088 }
1089 }
1090
1091 /**
1092 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
1093 * depending on whether document-ready has occurred yet and whether we are in async mode.
1094 *
1095 * @private
1096 * @param {string} src URL to script, will be used as the src attribute in the script tag
1097 * @param {Function} [callback] Callback which will be run when the script is done
1098 * @param {boolean} [async=false] Whether to load modules asynchronously.
1099 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1100 */
1101 function addScript( src, callback, async ) {
1102 // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
1103 if ( $.isReady || async ) {
1104 $.ajax( {
1105 url: src,
1106 dataType: 'script',
1107 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1108 // XHR for a same domain request instead of <script>, which changes the request
1109 // headers (potentially missing a cache hit), and reduces caching in general
1110 // since browsers cache XHR much less (if at all). And XHR means we retreive
1111 // text, so we'd need to $.globalEval, which then messes up line numbers.
1112 crossDomain: true,
1113 cache: true,
1114 async: true
1115 } ).always( callback );
1116 } else {
1117 /*jshint evil:true */
1118 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1119 if ( callback ) {
1120 // Document.write is synchronous, so this is called when it's done.
1121 // FIXME: That's a lie. doc.write isn't actually synchronous.
1122 callback();
1123 }
1124 }
1125 }
1126
1127 /**
1128 * Executes a loaded module, making it ready to use
1129 *
1130 * @private
1131 * @param {string} module Module name to execute
1132 */
1133 function execute( module ) {
1134 var key, value, media, i, urls, cssHandle, checkCssHandles,
1135 cssHandlesRegistered = false;
1136
1137 if ( !hasOwn.call( registry, module ) ) {
1138 throw new Error( 'Module has not been registered yet: ' + module );
1139 } else if ( registry[module].state === 'registered' ) {
1140 throw new Error( 'Module has not been requested from the server yet: ' + module );
1141 } else if ( registry[module].state === 'loading' ) {
1142 throw new Error( 'Module has not completed loading yet: ' + module );
1143 } else if ( registry[module].state === 'ready' ) {
1144 throw new Error( 'Module has already been executed: ' + module );
1145 }
1146
1147 /**
1148 * Define loop-function here for efficiency
1149 * and to avoid re-using badly scoped variables.
1150 * @ignore
1151 */
1152 function addLink( media, url ) {
1153 var el = document.createElement( 'link' );
1154 // Support: IE
1155 // Insert in document *before* setting href
1156 getMarker().before( el );
1157 el.rel = 'stylesheet';
1158 if ( media && media !== 'all' ) {
1159 el.media = media;
1160 }
1161 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1162 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1163 el.href = url;
1164 }
1165
1166 function runScript() {
1167 var script, markModuleReady, nestedAddScript;
1168 try {
1169 script = registry[module].script;
1170 markModuleReady = function () {
1171 registry[module].state = 'ready';
1172 handlePending( module );
1173 };
1174 nestedAddScript = function ( arr, callback, async, i ) {
1175 // Recursively call addScript() in its own callback
1176 // for each element of arr.
1177 if ( i >= arr.length ) {
1178 // We're at the end of the array
1179 callback();
1180 return;
1181 }
1182
1183 addScript( arr[i], function () {
1184 nestedAddScript( arr, callback, async, i + 1 );
1185 }, async );
1186 };
1187
1188 if ( $.isArray( script ) ) {
1189 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1190 } else if ( $.isFunction( script ) ) {
1191 registry[module].state = 'ready';
1192 // Pass jQuery twice so that the signature of the closure which wraps
1193 // the script can bind both '$' and 'jQuery'.
1194 script( $, $ );
1195 handlePending( module );
1196 }
1197 } catch ( e ) {
1198 // This needs to NOT use mw.log because these errors are common in production mode
1199 // and not in debug mode, such as when a symbol that should be global isn't exported
1200 registry[module].state = 'error';
1201 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
1202 handlePending( module );
1203 }
1204 }
1205
1206 // This used to be inside runScript, but since that is now fired asychronously
1207 // (after CSS is loaded) we need to set it here right away. It is crucial that
1208 // when execute() is called this is set synchronously, otherwise modules will get
1209 // executed multiple times as the registry will state that it isn't loading yet.
1210 registry[module].state = 'loading';
1211
1212 // Add localizations to message system
1213 if ( $.isPlainObject( registry[module].messages ) ) {
1214 mw.messages.set( registry[module].messages );
1215 }
1216
1217 // Initialise templates
1218 if ( registry[module].templates ) {
1219 mw.templates.set( module, registry[module].templates );
1220 }
1221
1222 if ( $.isReady || registry[module].async ) {
1223 // Make sure we don't run the scripts until all (potentially asynchronous)
1224 // stylesheet insertions have completed.
1225 ( function () {
1226 var pending = 0;
1227 checkCssHandles = function () {
1228 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1229 // one of the cssHandles is fired while we're still creating more handles.
1230 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1231 runScript();
1232 runScript = undefined; // Revoke
1233 }
1234 };
1235 cssHandle = function () {
1236 var check = checkCssHandles;
1237 pending++;
1238 return function () {
1239 if ( check ) {
1240 pending--;
1241 check();
1242 check = undefined; // Revoke
1243 }
1244 };
1245 };
1246 }() );
1247 } else {
1248 // We are in blocking mode, and so we can't afford to wait for CSS
1249 cssHandle = function () {};
1250 // Run immediately
1251 checkCssHandles = runScript;
1252 }
1253
1254 // Process styles (see also mw.loader.implement)
1255 // * back-compat: { <media>: css }
1256 // * back-compat: { <media>: [url, ..] }
1257 // * { "css": [css, ..] }
1258 // * { "url": { <media>: [url, ..] } }
1259 if ( $.isPlainObject( registry[module].style ) ) {
1260 for ( key in registry[module].style ) {
1261 value = registry[module].style[key];
1262 media = undefined;
1263
1264 if ( key !== 'url' && key !== 'css' ) {
1265 // Backwards compatibility, key is a media-type
1266 if ( typeof value === 'string' ) {
1267 // back-compat: { <media>: css }
1268 // Ignore 'media' because it isn't supported (nor was it used).
1269 // Strings are pre-wrapped in "@media". The media-type was just ""
1270 // (because it had to be set to something).
1271 // This is one of the reasons why this format is no longer used.
1272 addEmbeddedCSS( value, cssHandle() );
1273 } else {
1274 // back-compat: { <media>: [url, ..] }
1275 media = key;
1276 key = 'bc-url';
1277 }
1278 }
1279
1280 // Array of css strings in key 'css',
1281 // or back-compat array of urls from media-type
1282 if ( $.isArray( value ) ) {
1283 for ( i = 0; i < value.length; i += 1 ) {
1284 if ( key === 'bc-url' ) {
1285 // back-compat: { <media>: [url, ..] }
1286 addLink( media, value[i] );
1287 } else if ( key === 'css' ) {
1288 // { "css": [css, ..] }
1289 addEmbeddedCSS( value[i], cssHandle() );
1290 }
1291 }
1292 // Not an array, but a regular object
1293 // Array of urls inside media-type key
1294 } else if ( typeof value === 'object' ) {
1295 // { "url": { <media>: [url, ..] } }
1296 for ( media in value ) {
1297 urls = value[media];
1298 for ( i = 0; i < urls.length; i += 1 ) {
1299 addLink( media, urls[i] );
1300 }
1301 }
1302 }
1303 }
1304 }
1305
1306 // Kick off.
1307 cssHandlesRegistered = true;
1308 checkCssHandles();
1309 }
1310
1311 /**
1312 * Adds a dependencies to the queue with optional callbacks to be run
1313 * when the dependencies are ready or fail
1314 *
1315 * @private
1316 * @param {string|string[]} dependencies Module name or array of string module names
1317 * @param {Function} [ready] Callback to execute when all dependencies are ready
1318 * @param {Function} [error] Callback to execute when any dependency fails
1319 * @param {boolean} [async=false] Whether to load modules asynchronously.
1320 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1321 */
1322 function request( dependencies, ready, error, async ) {
1323 // Allow calling by single module name
1324 if ( typeof dependencies === 'string' ) {
1325 dependencies = [dependencies];
1326 }
1327
1328 // Add ready and error callbacks if they were given
1329 if ( ready !== undefined || error !== undefined ) {
1330 jobs[jobs.length] = {
1331 dependencies: $.grep( dependencies, function ( module ) {
1332 var state = mw.loader.getState( module );
1333 return state === 'registered' || state === 'loaded' || state === 'loading';
1334 } ),
1335 ready: ready,
1336 error: error
1337 };
1338 }
1339
1340 $.each( dependencies, function ( idx, module ) {
1341 var state = mw.loader.getState( module );
1342 if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
1343 queue.push( module );
1344 if ( async ) {
1345 registry[module].async = true;
1346 }
1347 }
1348 } );
1349
1350 mw.loader.work();
1351 }
1352
1353 function sortQuery( o ) {
1354 var key,
1355 sorted = {},
1356 a = [];
1357
1358 for ( key in o ) {
1359 if ( hasOwn.call( o, key ) ) {
1360 a.push( key );
1361 }
1362 }
1363 a.sort();
1364 for ( key = 0; key < a.length; key += 1 ) {
1365 sorted[a[key]] = o[a[key]];
1366 }
1367 return sorted;
1368 }
1369
1370 /**
1371 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1372 * to a query string of the form foo.bar,baz|bar.baz,quux
1373 * @private
1374 */
1375 function buildModulesString( moduleMap ) {
1376 var p, prefix,
1377 arr = [];
1378
1379 for ( prefix in moduleMap ) {
1380 p = prefix === '' ? '' : prefix + '.';
1381 arr.push( p + moduleMap[prefix].join( ',' ) );
1382 }
1383 return arr.join( '|' );
1384 }
1385
1386 /**
1387 * Asynchronously append a script tag to the end of the body
1388 * that invokes load.php
1389 * @private
1390 * @param {Object} moduleMap Module map, see #buildModulesString
1391 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1392 * @param {string} sourceLoadScript URL of load.php
1393 * @param {boolean} async Whether to load modules asynchronously.
1394 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1395 */
1396 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1397 var request = $.extend(
1398 { modules: buildModulesString( moduleMap ) },
1399 currReqBase
1400 );
1401 request = sortQuery( request );
1402 // Support: IE6
1403 // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
1404 // isn't actually used in IE6, but MediaWiki enforces it in general.
1405 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1406 }
1407
1408 /**
1409 * Resolve indexed dependencies.
1410 *
1411 * ResourceLoader uses an optimization to save space which replaces module names in
1412 * dependency lists with the index of that module within the array of module
1413 * registration data if it exists. The benefit is a significant reduction in the data
1414 * size of the startup module. This function changes those dependency lists back to
1415 * arrays of strings.
1416 *
1417 * @param {Array} modules Modules array
1418 */
1419 function resolveIndexedDependencies( modules ) {
1420 $.each( modules, function ( idx, module ) {
1421 if ( module[2] ) {
1422 module[2] = $.map( module[2], function ( dep ) {
1423 return typeof dep === 'number' ? modules[dep][0] : dep;
1424 } );
1425 }
1426 } );
1427 }
1428
1429 /* Public Members */
1430 return {
1431 /**
1432 * The module registry is exposed as an aid for debugging and inspecting page
1433 * state; it is not a public interface for modifying the registry.
1434 *
1435 * @see #registry
1436 * @property
1437 * @private
1438 */
1439 moduleRegistry: registry,
1440
1441 /**
1442 * @inheritdoc #newStyleTag
1443 * @method
1444 */
1445 addStyleTag: newStyleTag,
1446
1447 /**
1448 * Batch-request queued dependencies from the server.
1449 */
1450 work: function () {
1451 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1452 source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
1453 currReqBase, currReqBaseLength, moduleMap, l,
1454 lastDotIndex, prefix, suffix, bytesAdded, async;
1455
1456 // Build a list of request parameters common to all requests.
1457 reqBase = {
1458 skin: mw.config.get( 'skin' ),
1459 lang: mw.config.get( 'wgUserLanguage' ),
1460 debug: mw.config.get( 'debug' )
1461 };
1462 // Split module batch by source and by group.
1463 splits = {};
1464 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
1465
1466 // Appends a list of modules from the queue to the batch
1467 for ( q = 0; q < queue.length; q += 1 ) {
1468 // Only request modules which are registered
1469 if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
1470 // Prevent duplicate entries
1471 if ( $.inArray( queue[q], batch ) === -1 ) {
1472 batch[batch.length] = queue[q];
1473 // Mark registered modules as loading
1474 registry[queue[q]].state = 'loading';
1475 }
1476 }
1477 }
1478
1479 mw.loader.store.init();
1480 if ( mw.loader.store.enabled ) {
1481 concatSource = [];
1482 origBatch = batch;
1483 batch = $.grep( batch, function ( module ) {
1484 var source = mw.loader.store.get( module );
1485 if ( source ) {
1486 concatSource.push( source );
1487 return false;
1488 }
1489 return true;
1490 } );
1491 try {
1492 $.globalEval( concatSource.join( ';' ) );
1493 } catch ( err ) {
1494 // Not good, the cached mw.loader.implement calls failed! This should
1495 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1496 // Depending on how corrupt the string is, it is likely that some
1497 // modules' implement() succeeded while the ones after the error will
1498 // never run and leave their modules in the 'loading' state forever.
1499
1500 // Since this is an error not caused by an individual module but by
1501 // something that infected the implement call itself, don't take any
1502 // risks and clear everything in this cache.
1503 mw.loader.store.clear();
1504 // Re-add the ones still pending back to the batch and let the server
1505 // repopulate these modules to the cache.
1506 // This means that at most one module will be useless (the one that had
1507 // the error) instead of all of them.
1508 mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
1509 origBatch = $.grep( origBatch, function ( module ) {
1510 return registry[module].state === 'loading';
1511 } );
1512 batch = batch.concat( origBatch );
1513 }
1514 }
1515
1516 // Early exit if there's nothing to load...
1517 if ( !batch.length ) {
1518 return;
1519 }
1520
1521 // The queue has been processed into the batch, clear up the queue.
1522 queue = [];
1523
1524 // Always order modules alphabetically to help reduce cache
1525 // misses for otherwise identical content.
1526 batch.sort();
1527
1528 // Split batch by source and by group.
1529 for ( b = 0; b < batch.length; b += 1 ) {
1530 bSource = registry[batch[b]].source;
1531 bGroup = registry[batch[b]].group;
1532 if ( !hasOwn.call( splits, bSource ) ) {
1533 splits[bSource] = {};
1534 }
1535 if ( !hasOwn.call( splits[bSource], bGroup ) ) {
1536 splits[bSource][bGroup] = [];
1537 }
1538 bSourceGroup = splits[bSource][bGroup];
1539 bSourceGroup[bSourceGroup.length] = batch[b];
1540 }
1541
1542 // Clear the batch - this MUST happen before we append any
1543 // script elements to the body or it's possible that a script
1544 // will be locally cached, instantly load, and work the batch
1545 // again, all before we've cleared it causing each request to
1546 // include modules which are already loaded.
1547 batch = [];
1548
1549 for ( source in splits ) {
1550
1551 sourceLoadScript = sources[source];
1552
1553 for ( group in splits[source] ) {
1554
1555 // Cache access to currently selected list of
1556 // modules for this group from this source.
1557 modules = splits[source][group];
1558
1559 // Calculate the highest timestamp
1560 maxVersion = 0;
1561 for ( g = 0; g < modules.length; g += 1 ) {
1562 if ( registry[modules[g]].version > maxVersion ) {
1563 maxVersion = registry[modules[g]].version;
1564 }
1565 }
1566
1567 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1568 // For user modules append a user name to the request.
1569 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1570 currReqBase.user = mw.config.get( 'wgUserName' );
1571 }
1572 currReqBaseLength = $.param( currReqBase ).length;
1573 async = true;
1574 // We may need to split up the request to honor the query string length limit,
1575 // so build it piece by piece.
1576 l = currReqBaseLength + 9; // '&modules='.length == 9
1577
1578 moduleMap = {}; // { prefix: [ suffixes ] }
1579
1580 for ( i = 0; i < modules.length; i += 1 ) {
1581 // Determine how many bytes this module would add to the query string
1582 lastDotIndex = modules[i].lastIndexOf( '.' );
1583
1584 // If lastDotIndex is -1, substr() returns an empty string
1585 prefix = modules[i].substr( 0, lastDotIndex );
1586 suffix = modules[i].slice( lastDotIndex + 1 );
1587
1588 bytesAdded = hasOwn.call( moduleMap, prefix )
1589 ? suffix.length + 3 // '%2C'.length == 3
1590 : modules[i].length + 3; // '%7C'.length == 3
1591
1592 // If the request would become too long, create a new one,
1593 // but don't create empty requests
1594 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1595 // This request would become too long, create a new one
1596 // and fire off the old one
1597 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1598 moduleMap = {};
1599 async = true;
1600 l = currReqBaseLength + 9;
1601 mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
1602 }
1603 if ( !hasOwn.call( moduleMap, prefix ) ) {
1604 moduleMap[prefix] = [];
1605 }
1606 moduleMap[prefix].push( suffix );
1607 if ( !registry[modules[i]].async ) {
1608 // If this module is blocking, make the entire request blocking
1609 // This is slightly suboptimal, but in practice mixing of blocking
1610 // and async modules will only occur in debug mode.
1611 async = false;
1612 }
1613 l += bytesAdded;
1614 }
1615 // If there's anything left in moduleMap, request that too
1616 if ( !$.isEmptyObject( moduleMap ) ) {
1617 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1618 }
1619 }
1620 }
1621 },
1622
1623 /**
1624 * Register a source.
1625 *
1626 * The #work method will use this information to split up requests by source.
1627 *
1628 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1629 *
1630 * @param {string} id Short string representing a source wiki, used internally for
1631 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1632 * @param {Object|string} loadUrl load.php url, may be an object for backwards-compatibility
1633 * @return {boolean}
1634 */
1635 addSource: function ( id, loadUrl ) {
1636 var source;
1637 // Allow multiple additions
1638 if ( typeof id === 'object' ) {
1639 for ( source in id ) {
1640 mw.loader.addSource( source, id[source] );
1641 }
1642 return true;
1643 }
1644
1645 if ( hasOwn.call( sources, id ) ) {
1646 throw new Error( 'source already registered: ' + id );
1647 }
1648
1649 if ( typeof loadUrl === 'object' ) {
1650 loadUrl = loadUrl.loadScript;
1651 }
1652
1653 sources[id] = loadUrl;
1654
1655 return true;
1656 },
1657
1658 /**
1659 * Register a module, letting the system know about it and its
1660 * properties. Startup modules contain calls to this function.
1661 *
1662 * When using multiple module registration by passing an array, dependencies that
1663 * are specified as references to modules within the array will be resolved before
1664 * the modules are registered.
1665 *
1666 * @param {string|Array} module Module name or array of arrays, each containing
1667 * a list of arguments compatible with this method
1668 * @param {number} version Module version number as a timestamp (falls backs to 0)
1669 * @param {string|Array|Function} dependencies One string or array of strings of module
1670 * names on which this module depends, or a function that returns that array.
1671 * @param {string} [group=null] Group which the module is in
1672 * @param {string} [source='local'] Name of the source
1673 * @param {string} [skip=null] Script body of the skip function
1674 */
1675 register: function ( module, version, dependencies, group, source, skip ) {
1676 var i, len;
1677 // Allow multiple registration
1678 if ( typeof module === 'object' ) {
1679 resolveIndexedDependencies( module );
1680 for ( i = 0, len = module.length; i < len; i++ ) {
1681 // module is an array of module names
1682 if ( typeof module[i] === 'string' ) {
1683 mw.loader.register( module[i] );
1684 // module is an array of arrays
1685 } else if ( typeof module[i] === 'object' ) {
1686 mw.loader.register.apply( mw.loader, module[i] );
1687 }
1688 }
1689 return;
1690 }
1691 // Validate input
1692 if ( typeof module !== 'string' ) {
1693 throw new Error( 'module must be a string, not a ' + typeof module );
1694 }
1695 if ( hasOwn.call( registry, module ) ) {
1696 throw new Error( 'module already registered: ' + module );
1697 }
1698 // List the module as registered
1699 registry[module] = {
1700 version: version !== undefined ? parseInt( version, 10 ) : 0,
1701 dependencies: [],
1702 group: typeof group === 'string' ? group : null,
1703 source: typeof source === 'string' ? source : 'local',
1704 state: 'registered',
1705 skip: typeof skip === 'string' ? skip : null
1706 };
1707 if ( typeof dependencies === 'string' ) {
1708 // Allow dependencies to be given as a single module name
1709 registry[module].dependencies = [ dependencies ];
1710 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1711 // Allow dependencies to be given as an array of module names
1712 // or a function which returns an array
1713 registry[module].dependencies = dependencies;
1714 }
1715 },
1716
1717 /**
1718 * Implement a module given the components that make up the module.
1719 *
1720 * When #load or #using requests one or more modules, the server
1721 * response contain calls to this function.
1722 *
1723 * All arguments are required.
1724 *
1725 * @param {string} module Name of module
1726 * @param {Function|Array} script Function with module code or Array of URLs to
1727 * be used as the src attribute of a new `<script>` tag.
1728 * @param {Object} [style] Should follow one of the following patterns:
1729 *
1730 * { "css": [css, ..] }
1731 * { "url": { <media>: [url, ..] } }
1732 *
1733 * And for backwards compatibility (needs to be supported forever due to caching):
1734 *
1735 * { <media>: css }
1736 * { <media>: [url, ..] }
1737 *
1738 * The reason css strings are not concatenated anymore is bug 31676. We now check
1739 * whether it's safe to extend the stylesheet.
1740 *
1741 * @param {Object} [msgs] List of key/value pairs to be added to mw#messages.
1742 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1743 */
1744 implement: function ( module, script, style, msgs, templates ) {
1745 // Validate input
1746 if ( typeof module !== 'string' ) {
1747 throw new Error( 'module must be of type string, not ' + typeof module );
1748 }
1749 if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
1750 throw new Error( 'script must be of type function or array, not ' + typeof script );
1751 }
1752 if ( style && !$.isPlainObject( style ) ) {
1753 throw new Error( 'style must be of type object, not ' + typeof style );
1754 }
1755 if ( msgs && !$.isPlainObject( msgs ) ) {
1756 throw new Error( 'msgs must be of type object, not a ' + typeof msgs );
1757 }
1758 if ( templates && !$.isPlainObject( templates ) ) {
1759 throw new Error( 'templates must be of type object, not a ' + typeof templates );
1760 }
1761 // Automatically register module
1762 if ( !hasOwn.call( registry, module ) ) {
1763 mw.loader.register( module );
1764 }
1765 // Check for duplicate implementation
1766 if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
1767 throw new Error( 'module already implemented: ' + module );
1768 }
1769 // Attach components
1770 registry[module].script = script || [];
1771 registry[module].style = style || {};
1772 registry[module].messages = msgs || {};
1773 registry[module].templates = templates || {};
1774 // The module may already have been marked as erroneous
1775 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1776 registry[module].state = 'loaded';
1777 if ( allReady( registry[module].dependencies ) ) {
1778 execute( module );
1779 }
1780 }
1781 },
1782
1783 /**
1784 * Execute a function as soon as one or more required modules are ready.
1785 *
1786 * Example of inline dependency on OOjs:
1787 *
1788 * mw.loader.using( 'oojs', function () {
1789 * OO.compare( [ 1 ], [ 1 ] );
1790 * } );
1791 *
1792 * @param {string|Array} dependencies Module name or array of modules names the callback
1793 * dependends on to be ready before executing
1794 * @param {Function} [ready] Callback to execute when all dependencies are ready
1795 * @param {Function} [error] Callback to execute if one or more dependencies failed
1796 * @return {jQuery.Promise}
1797 * @since 1.23 this returns a promise
1798 */
1799 using: function ( dependencies, ready, error ) {
1800 var deferred = $.Deferred();
1801
1802 // Allow calling with a single dependency as a string
1803 if ( typeof dependencies === 'string' ) {
1804 dependencies = [ dependencies ];
1805 } else if ( !$.isArray( dependencies ) ) {
1806 // Invalid input
1807 throw new Error( 'Dependencies must be a string or an array' );
1808 }
1809
1810 if ( ready ) {
1811 deferred.done( ready );
1812 }
1813 if ( error ) {
1814 deferred.fail( error );
1815 }
1816
1817 // Resolve entire dependency map
1818 dependencies = resolve( dependencies );
1819 if ( allReady( dependencies ) ) {
1820 // Run ready immediately
1821 deferred.resolve();
1822 } else if ( anyFailed( dependencies ) ) {
1823 // Execute error immediately if any dependencies have errors
1824 deferred.reject(
1825 new Error( 'One or more dependencies failed to load' ),
1826 dependencies
1827 );
1828 } else {
1829 // Not all dependencies are ready: queue up a request
1830 request( dependencies, deferred.resolve, deferred.reject );
1831 }
1832
1833 return deferred.promise();
1834 },
1835
1836 /**
1837 * Load an external script or one or more modules.
1838 *
1839 * @param {string|Array} modules Either the name of a module, array of modules,
1840 * or a URL of an external script or style
1841 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1842 * external script or style; acceptable values are "text/css" and
1843 * "text/javascript"; if no type is provided, text/javascript is assumed.
1844 * @param {boolean} [async] Whether to load modules asynchronously.
1845 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1846 * Defaults to `true` if loading a URL, `false` otherwise.
1847 */
1848 load: function ( modules, type, async ) {
1849 var filtered, l;
1850
1851 // Validate input
1852 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1853 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1854 }
1855 // Allow calling with an external url or single dependency as a string
1856 if ( typeof modules === 'string' ) {
1857 if ( /^(https?:)?\/\//.test( modules ) ) {
1858 if ( async === undefined ) {
1859 // Assume async for bug 34542
1860 async = true;
1861 }
1862 if ( type === 'text/css' ) {
1863 // Support: IE 7-8
1864 // Use properties instead of attributes as IE throws security
1865 // warnings when inserting a <link> tag with a protocol-relative
1866 // URL set though attributes - when on HTTPS. See bug 41331.
1867 l = document.createElement( 'link' );
1868 l.rel = 'stylesheet';
1869 l.href = modules;
1870 $( 'head' ).append( l );
1871 return;
1872 }
1873 if ( type === 'text/javascript' || type === undefined ) {
1874 addScript( modules, null, async );
1875 return;
1876 }
1877 // Unknown type
1878 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1879 }
1880 // Called with single module
1881 modules = [ modules ];
1882 }
1883
1884 // Filter out undefined modules, otherwise resolve() will throw
1885 // an exception for trying to load an undefined module.
1886 // Undefined modules are acceptable here in load(), because load() takes
1887 // an array of unrelated modules, whereas the modules passed to
1888 // using() are related and must all be loaded.
1889 filtered = $.grep( modules, function ( module ) {
1890 var state = mw.loader.getState( module );
1891 return state !== null && state !== 'error' && state !== 'missing';
1892 } );
1893
1894 if ( filtered.length === 0 ) {
1895 return;
1896 }
1897 // Resolve entire dependency map
1898 filtered = resolve( filtered );
1899 // If all modules are ready, or if any modules have errors, nothing to be done.
1900 if ( allReady( filtered ) || anyFailed( filtered ) ) {
1901 return;
1902 }
1903 // Since some modules are not yet ready, queue up a request.
1904 request( filtered, undefined, undefined, async );
1905 },
1906
1907 /**
1908 * Change the state of one or more modules.
1909 *
1910 * @param {string|Object} module Module name or object of module name/state pairs
1911 * @param {string} state State name
1912 */
1913 state: function ( module, state ) {
1914 var m;
1915
1916 if ( typeof module === 'object' ) {
1917 for ( m in module ) {
1918 mw.loader.state( m, module[m] );
1919 }
1920 return;
1921 }
1922 if ( !hasOwn.call( registry, module ) ) {
1923 mw.loader.register( module );
1924 }
1925 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1926 && registry[module].state !== state ) {
1927 // Make sure pending modules depending on this one get executed if their
1928 // dependencies are now fulfilled!
1929 registry[module].state = state;
1930 handlePending( module );
1931 } else {
1932 registry[module].state = state;
1933 }
1934 },
1935
1936 /**
1937 * Get the version of a module.
1938 *
1939 * @param {string} module Name of module
1940 * @return {string|null} The version, or null if the module (or its version) is not
1941 * in the registry.
1942 */
1943 getVersion: function ( module ) {
1944 if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
1945 return null;
1946 }
1947 return formatVersionNumber( registry[module].version );
1948 },
1949
1950 /**
1951 * Get the state of a module.
1952 *
1953 * @param {string} module Name of module
1954 * @return {string|null} The state, or null if the module (or its state) is not
1955 * in the registry.
1956 */
1957 getState: function ( module ) {
1958 if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
1959 return null;
1960 }
1961 return registry[module].state;
1962 },
1963
1964 /**
1965 * Get the names of all registered modules.
1966 *
1967 * @return {Array}
1968 */
1969 getModuleNames: function () {
1970 return $.map( registry, function ( i, key ) {
1971 return key;
1972 } );
1973 },
1974
1975 /**
1976 * @inheritdoc mw.inspect#runReports
1977 * @method
1978 */
1979 inspect: function () {
1980 var args = slice.call( arguments );
1981 mw.loader.using( 'mediawiki.inspect', function () {
1982 mw.inspect.runReports.apply( mw.inspect, args );
1983 } );
1984 },
1985
1986 /**
1987 * On browsers that implement the localStorage API, the module store serves as a
1988 * smart complement to the browser cache. Unlike the browser cache, the module store
1989 * can slice a concatenated response from ResourceLoader into its constituent
1990 * modules and cache each of them separately, using each module's versioning scheme
1991 * to determine when the cache should be invalidated.
1992 *
1993 * @singleton
1994 * @class mw.loader.store
1995 */
1996 store: {
1997 // Whether the store is in use on this page.
1998 enabled: null,
1999
2000 // The contents of the store, mapping '[module name]@[version]' keys
2001 // to module implementations.
2002 items: {},
2003
2004 // Cache hit stats
2005 stats: { hits: 0, misses: 0, expired: 0 },
2006
2007 /**
2008 * Construct a JSON-serializable object representing the content of the store.
2009 * @return {Object} Module store contents.
2010 */
2011 toJSON: function () {
2012 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2013 },
2014
2015 /**
2016 * Get the localStorage key for the entire module store. The key references
2017 * $wgDBname to prevent clashes between wikis which share a common host.
2018 *
2019 * @return {string} localStorage item key
2020 */
2021 getStoreKey: function () {
2022 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2023 },
2024
2025 /**
2026 * Get a key on which to vary the module cache.
2027 * @return {string} String of concatenated vary conditions.
2028 */
2029 getVary: function () {
2030 return [
2031 mw.config.get( 'skin' ),
2032 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2033 mw.config.get( 'wgUserLanguage' )
2034 ].join( ':' );
2035 },
2036
2037 /**
2038 * Get a key for a specific module. The key format is '[name]@[version]'.
2039 *
2040 * @param {string} module Module name
2041 * @return {string|null} Module key or null if module does not exist
2042 */
2043 getModuleKey: function ( module ) {
2044 return hasOwn.call( registry, module ) ?
2045 ( module + '@' + registry[module].version ) : null;
2046 },
2047
2048 /**
2049 * Initialize the store.
2050 *
2051 * Retrieves store from localStorage and (if successfully retrieved) decoding
2052 * the stored JSON value to a plain object.
2053 *
2054 * The try / catch block is used for JSON & localStorage feature detection.
2055 * See the in-line documentation for Modernizr's localStorage feature detection
2056 * code for a full account of why we need a try / catch:
2057 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2058 */
2059 init: function () {
2060 var raw, data;
2061
2062 if ( mw.loader.store.enabled !== null ) {
2063 // Init already ran
2064 return;
2065 }
2066
2067 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
2068 // Disabled by configuration.
2069 // Clear any previous store to free up space. (T66721)
2070 mw.loader.store.clear();
2071 mw.loader.store.enabled = false;
2072 return;
2073 }
2074 if ( mw.config.get( 'debug' ) ) {
2075 // Disable module store in debug mode
2076 mw.loader.store.enabled = false;
2077 return;
2078 }
2079
2080 try {
2081 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2082 // If we get here, localStorage is available; mark enabled
2083 mw.loader.store.enabled = true;
2084 data = JSON.parse( raw );
2085 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2086 mw.loader.store.items = data.items;
2087 return;
2088 }
2089 } catch ( e ) {
2090 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
2091 }
2092
2093 if ( raw === undefined ) {
2094 // localStorage failed; disable store
2095 mw.loader.store.enabled = false;
2096 } else {
2097 mw.loader.store.update();
2098 }
2099 },
2100
2101 /**
2102 * Retrieve a module from the store and update cache hit stats.
2103 *
2104 * @param {string} module Module name
2105 * @return {string|boolean} Module implementation or false if unavailable
2106 */
2107 get: function ( module ) {
2108 var key;
2109
2110 if ( !mw.loader.store.enabled ) {
2111 return false;
2112 }
2113
2114 key = mw.loader.store.getModuleKey( module );
2115 if ( key in mw.loader.store.items ) {
2116 mw.loader.store.stats.hits++;
2117 return mw.loader.store.items[key];
2118 }
2119 mw.loader.store.stats.misses++;
2120 return false;
2121 },
2122
2123 /**
2124 * Stringify a module and queue it for storage.
2125 *
2126 * @param {string} module Module name
2127 * @param {Object} descriptor The module's descriptor as set in the registry
2128 */
2129 set: function ( module, descriptor ) {
2130 var args, key;
2131
2132 if ( !mw.loader.store.enabled ) {
2133 return false;
2134 }
2135
2136 key = mw.loader.store.getModuleKey( module );
2137
2138 if (
2139 // Already stored a copy of this exact version
2140 key in mw.loader.store.items ||
2141 // Module failed to load
2142 descriptor.state !== 'ready' ||
2143 // Unversioned, private, or site-/user-specific
2144 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2145 // Partial descriptor
2146 $.inArray( undefined, [ descriptor.script, descriptor.style,
2147 descriptor.messages, descriptor.templates ] ) !== -1
2148 ) {
2149 // Decline to store
2150 return false;
2151 }
2152
2153 try {
2154 args = [
2155 JSON.stringify( module ),
2156 typeof descriptor.script === 'function' ?
2157 String( descriptor.script ) :
2158 JSON.stringify( descriptor.script ),
2159 JSON.stringify( descriptor.style ),
2160 JSON.stringify( descriptor.messages ),
2161 JSON.stringify( descriptor.templates )
2162 ];
2163 // Attempted workaround for a possible Opera bug (bug T59567).
2164 // This regex should never match under sane conditions.
2165 if ( /^\s*\(/.test( args[1] ) ) {
2166 args[1] = 'function' + args[1];
2167 mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
2168 }
2169 } catch ( e ) {
2170 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
2171 return;
2172 }
2173
2174 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2175 mw.loader.store.update();
2176 },
2177
2178 /**
2179 * Iterate through the module store, removing any item that does not correspond
2180 * (in name and version) to an item in the module registry.
2181 */
2182 prune: function () {
2183 var key, module;
2184
2185 if ( !mw.loader.store.enabled ) {
2186 return false;
2187 }
2188
2189 for ( key in mw.loader.store.items ) {
2190 module = key.slice( 0, key.indexOf( '@' ) );
2191 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2192 mw.loader.store.stats.expired++;
2193 delete mw.loader.store.items[key];
2194 }
2195 }
2196 },
2197
2198 /**
2199 * Clear the entire module store right now.
2200 */
2201 clear: function () {
2202 mw.loader.store.items = {};
2203 localStorage.removeItem( mw.loader.store.getStoreKey() );
2204 },
2205
2206 /**
2207 * Sync modules to localStorage.
2208 *
2209 * This function debounces localStorage updates. When called multiple times in
2210 * quick succession, the calls are coalesced into a single update operation.
2211 * This allows us to call #update without having to consider the module load
2212 * queue; the call to localStorage.setItem will be naturally deferred until the
2213 * page is quiescent.
2214 *
2215 * Because localStorage is shared by all pages with the same origin, if multiple
2216 * pages are loaded with different module sets, the possibility exists that
2217 * modules saved by one page will be clobbered by another. But the impact would
2218 * be minor and the problem would be corrected by subsequent page views.
2219 *
2220 * @method
2221 */
2222 update: ( function () {
2223 var timer;
2224
2225 function flush() {
2226 var data,
2227 key = mw.loader.store.getStoreKey();
2228
2229 if ( !mw.loader.store.enabled ) {
2230 return false;
2231 }
2232 mw.loader.store.prune();
2233 try {
2234 // Replacing the content of the module store might fail if the new
2235 // contents would exceed the browser's localStorage size limit. To
2236 // avoid clogging the browser with stale data, always remove the old
2237 // value before attempting to set the new one.
2238 localStorage.removeItem( key );
2239 data = JSON.stringify( mw.loader.store );
2240 localStorage.setItem( key, data );
2241 } catch ( e ) {
2242 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
2243 }
2244 }
2245
2246 return function () {
2247 clearTimeout( timer );
2248 timer = setTimeout( flush, 2000 );
2249 };
2250 }() )
2251 }
2252 };
2253 }() ),
2254
2255 /**
2256 * HTML construction helper functions
2257 *
2258 * @example
2259 *
2260 * var Html, output;
2261 *
2262 * Html = mw.html;
2263 * output = Html.element( 'div', {}, new Html.Raw(
2264 * Html.element( 'img', { src: '<' } )
2265 * ) );
2266 * mw.log( output ); // <div><img src="&lt;"/></div>
2267 *
2268 * @class mw.html
2269 * @singleton
2270 */
2271 html: ( function () {
2272 function escapeCallback( s ) {
2273 switch ( s ) {
2274 case '\'':
2275 return '&#039;';
2276 case '"':
2277 return '&quot;';
2278 case '<':
2279 return '&lt;';
2280 case '>':
2281 return '&gt;';
2282 case '&':
2283 return '&amp;';
2284 }
2285 }
2286
2287 return {
2288 /**
2289 * Escape a string for HTML.
2290 *
2291 * Converts special characters to HTML entities.
2292 *
2293 * mw.html.escape( '< > \' & "' );
2294 * // Returns &lt; &gt; &#039; &amp; &quot;
2295 *
2296 * @param {string} s The string to escape
2297 * @return {string} HTML
2298 */
2299 escape: function ( s ) {
2300 return s.replace( /['"<>&]/g, escapeCallback );
2301 },
2302
2303 /**
2304 * Create an HTML element string, with safe escaping.
2305 *
2306 * @param {string} name The tag name.
2307 * @param {Object} attrs An object with members mapping element names to values
2308 * @param {Mixed} contents The contents of the element. May be either:
2309 *
2310 * - string: The string is escaped.
2311 * - null or undefined: The short closing form is used, e.g. `<br/>`.
2312 * - this.Raw: The value attribute is included without escaping.
2313 * - this.Cdata: The value attribute is included, and an exception is
2314 * thrown if it contains an illegal ETAGO delimiter.
2315 * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
2316 * @return {string} HTML
2317 */
2318 element: function ( name, attrs, contents ) {
2319 var v, attrName, s = '<' + name;
2320
2321 for ( attrName in attrs ) {
2322 v = attrs[attrName];
2323 // Convert name=true, to name=name
2324 if ( v === true ) {
2325 v = attrName;
2326 // Skip name=false
2327 } else if ( v === false ) {
2328 continue;
2329 }
2330 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2331 }
2332 if ( contents === undefined || contents === null ) {
2333 // Self close tag
2334 s += '/>';
2335 return s;
2336 }
2337 // Regular open tag
2338 s += '>';
2339 switch ( typeof contents ) {
2340 case 'string':
2341 // Escaped
2342 s += this.escape( contents );
2343 break;
2344 case 'number':
2345 case 'boolean':
2346 // Convert to string
2347 s += String( contents );
2348 break;
2349 default:
2350 if ( contents instanceof this.Raw ) {
2351 // Raw HTML inclusion
2352 s += contents.value;
2353 } else if ( contents instanceof this.Cdata ) {
2354 // CDATA
2355 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2356 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2357 }
2358 s += contents.value;
2359 } else {
2360 throw new Error( 'mw.html.element: Invalid type of contents' );
2361 }
2362 }
2363 s += '</' + name + '>';
2364 return s;
2365 },
2366
2367 /**
2368 * Wrapper object for raw HTML passed to mw.html.element().
2369 * @class mw.html.Raw
2370 */
2371 Raw: function ( value ) {
2372 this.value = value;
2373 },
2374
2375 /**
2376 * Wrapper object for CDATA element contents passed to mw.html.element()
2377 * @class mw.html.Cdata
2378 */
2379 Cdata: function ( value ) {
2380 this.value = value;
2381 }
2382 };
2383 }() ),
2384
2385 // Skeleton user object. mediawiki.user.js extends this
2386 user: {
2387 options: new Map(),
2388 tokens: new Map()
2389 },
2390
2391 /**
2392 * Registry and firing of events.
2393 *
2394 * MediaWiki has various interface components that are extended, enhanced
2395 * or manipulated in some other way by extensions, gadgets and even
2396 * in core itself.
2397 *
2398 * This framework helps streamlining the timing of when these other
2399 * code paths fire their plugins (instead of using document-ready,
2400 * which can and should be limited to firing only once).
2401 *
2402 * Features like navigating to other wiki pages, previewing an edit
2403 * and editing itself – without a refresh – can then retrigger these
2404 * hooks accordingly to ensure everything still works as expected.
2405 *
2406 * Example usage:
2407 *
2408 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2409 * mw.hook( 'wikipage.content' ).fire( $content );
2410 *
2411 * Handlers can be added and fired for arbitrary event names at any time. The same
2412 * event can be fired multiple times. The last run of an event is memorized
2413 * (similar to `$(document).ready` and `$.Deferred().done`).
2414 * This means if an event is fired, and a handler added afterwards, the added
2415 * function will be fired right away with the last given event data.
2416 *
2417 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2418 * Thus allowing flexible use and optimal maintainability and authority control.
2419 * You can pass around the `add` and/or `fire` method to another piece of code
2420 * without it having to know the event name (or `mw.hook` for that matter).
2421 *
2422 * var h = mw.hook( 'bar.ready' );
2423 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2424 *
2425 * Note: Events are documented with an underscore instead of a dot in the event
2426 * name due to jsduck not supporting dots in that position.
2427 *
2428 * @class mw.hook
2429 */
2430 hook: ( function () {
2431 var lists = {};
2432
2433 /**
2434 * Create an instance of mw.hook.
2435 *
2436 * @method hook
2437 * @member mw
2438 * @param {string} name Name of hook.
2439 * @return {mw.hook}
2440 */
2441 return function ( name ) {
2442 var list = hasOwn.call( lists, name ) ?
2443 lists[name] :
2444 lists[name] = $.Callbacks( 'memory' );
2445
2446 return {
2447 /**
2448 * Register a hook handler
2449 * @param {Function...} handler Function to bind.
2450 * @chainable
2451 */
2452 add: list.add,
2453
2454 /**
2455 * Unregister a hook handler
2456 * @param {Function...} handler Function to unbind.
2457 * @chainable
2458 */
2459 remove: list.remove,
2460
2461 /**
2462 * Run a hook.
2463 * @param {Mixed...} data
2464 * @chainable
2465 */
2466 fire: function () {
2467 return list.fireWith.call( this, null, slice.call( arguments ) );
2468 }
2469 };
2470 };
2471 }() )
2472 };
2473
2474 // Alias $j to jQuery for backwards compatibility
2475 // @deprecated since 1.23 Use $ or jQuery instead
2476 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2477
2478 /**
2479 * Log a message to window.console, if possible.
2480 *
2481 * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
2482 * also in production mode). Gets console references in each invocation instead of caching the
2483 * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
2484 *
2485 * @private
2486 * @method log_
2487 * @param {string} topic Stream name passed by mw.track
2488 * @param {Object} data Data passed by mw.track
2489 * @param {Error} [data.exception]
2490 * @param {string} data.source Error source
2491 * @param {string} [data.module] Name of module which caused the error
2492 */
2493 function log( topic, data ) {
2494 var msg,
2495 e = data.exception,
2496 source = data.source,
2497 module = data.module,
2498 console = window.console;
2499
2500 if ( console && console.log ) {
2501 msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
2502 if ( module ) {
2503 msg += ' in module ' + module;
2504 }
2505 msg += ( e ? ':' : '.' );
2506 console.log( msg );
2507
2508 // If we have an exception object, log it to the error channel to trigger a
2509 // proper stacktraces in browsers that support it. No fallback as we have no browsers
2510 // that don't support error(), but do support log().
2511 if ( e && console.error ) {
2512 console.error( String( e ), e );
2513 }
2514 }
2515 }
2516
2517 // subscribe to error streams
2518 mw.trackSubscribe( 'resourceloader.exception', log );
2519 mw.trackSubscribe( 'resourceloader.assert', log );
2520
2521 // Attach to window and globally alias
2522 window.mw = window.mediaWiki = mw;
2523 }( jQuery ) );