mw.loader: Replace log() calls with mw.track events
[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 * Check whether given styles are safe to to a stylesheet.
778 *
779 * @private
780 * @param {string} cssText
781 * @return {boolean} False if a new one must be created.
782 */
783 function canExpandStylesheetWith( cssText ) {
784 // Makes sure that cssText containing `@import`
785 // rules will end up in a new stylesheet (as those only work when
786 // placed at the start of a stylesheet; bug 35562).
787 return cssText.slice( 0, '@import'.length ) !== '@import';
788 }
789
790 /**
791 * Add a bit of CSS text to the current browser page.
792 *
793 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
794 * or create a new one based on whether the given `cssText` is safe for extension.
795 *
796 * @param {string} [cssText=cssBuffer] If called without cssText,
797 * the internal buffer will be inserted instead.
798 * @param {Function} [callback]
799 */
800 function addEmbeddedCSS( cssText, callback ) {
801 var $style, styleEl;
802
803 if ( callback ) {
804 cssCallbacks.add( callback );
805 }
806
807 // Yield once before inserting the <style> tag. There are likely
808 // more calls coming up which we can combine this way.
809 // Appending a stylesheet and waiting for the browser to repaint
810 // is fairly expensive, this reduces that (bug 45810)
811 if ( cssText ) {
812 // Be careful not to extend the buffer with css that needs a new stylesheet
813 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
814 // Linebreak for somewhat distinguishable sections
815 // (the rl-cachekey comment separating each)
816 cssBuffer += '\n' + cssText;
817 // TODO: Use requestAnimationFrame in the future which will
818 // perform even better by not injecting styles while the browser
819 // is painting.
820 setTimeout( function () {
821 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
822 // (below version 13) has the non-standard behaviour of passing a
823 // numerical "lateness" value as first argument to this callback
824 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
825 addEmbeddedCSS();
826 } );
827 return;
828 }
829
830 // This is a delayed call and we got a buffer still
831 } else if ( cssBuffer ) {
832 cssText = cssBuffer;
833 cssBuffer = '';
834
835 } else {
836 // This is a delayed call, but buffer was already cleared by
837 // another delayed call.
838 return;
839 }
840
841 // By default, always create a new <style>. Appending text to a <style>
842 // tag is bad as it means the contents have to be re-parsed (bug 45810).
843 //
844 // Except, of course, in IE 9 and below. In there we default to re-using and
845 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
846 if ( 'documentMode' in document && document.documentMode <= 9 ) {
847
848 $style = getMarker().prev();
849 // Verify that the element before the marker actually is a
850 // <style> tag and one that came from ResourceLoader
851 // (not some other style tag or even a `<meta>` or `<script>`).
852 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
853 // There's already a dynamic <style> tag present and
854 // canExpandStylesheetWith() gave a green light to append more to it.
855 styleEl = $style.get( 0 );
856 // Support: IE6-10
857 if ( styleEl.styleSheet ) {
858 try {
859 styleEl.styleSheet.cssText += cssText;
860 } catch ( e ) {
861 mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } );
862 }
863 } else {
864 styleEl.appendChild( document.createTextNode( cssText ) );
865 }
866 cssCallbacks.fire().empty();
867 return;
868 }
869 }
870
871 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
872
873 cssCallbacks.fire().empty();
874 }
875
876 /**
877 * Zero-pad three numbers.
878 *
879 * @private
880 * @param {number} a
881 * @param {number} b
882 * @param {number} c
883 * @return {string}
884 */
885 function pad( a, b, c ) {
886 return [
887 a < 10 ? '0' + a : a,
888 b < 10 ? '0' + b : b,
889 c < 10 ? '0' + c : c
890 ].join( '' );
891 }
892
893 /**
894 * Convert UNIX timestamp to ISO8601 format.
895 *
896 * @private
897 * @param {number} timestamp UNIX timestamp
898 */
899 function formatVersionNumber( timestamp ) {
900 var d = new Date();
901 d.setTime( timestamp * 1000 );
902 return [
903 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
904 'T',
905 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
906 'Z'
907 ].join( '' );
908 }
909
910 /**
911 * Resolve dependencies and detect circular references.
912 *
913 * @private
914 * @param {string} module Name of the top-level module whose dependencies shall be
915 * resolved and sorted.
916 * @param {Array} resolved Returns a topological sort of the given module and its
917 * dependencies, such that later modules depend on earlier modules. The array
918 * contains the module names. If the array contains already some module names,
919 * this function appends its result to the pre-existing array.
920 * @param {Object} [unresolved] Hash used to track the current dependency
921 * chain; used to report loops in the dependency graph.
922 * @throws {Error} If any unregistered module or a dependency loop is encountered
923 */
924 function sortDependencies( module, resolved, unresolved ) {
925 var n, deps, len, skip;
926
927 if ( !hasOwn.call( registry, module ) ) {
928 throw new Error( 'Unknown dependency: ' + module );
929 }
930
931 if ( registry[module].skip !== null ) {
932 /*jshint evil:true */
933 skip = new Function( registry[module].skip );
934 registry[module].skip = null;
935 if ( skip() ) {
936 registry[module].skipped = true;
937 registry[module].dependencies = [];
938 registry[module].state = 'ready';
939 handlePending( module );
940 return;
941 }
942 }
943
944 // Resolves dynamic loader function and replaces it with its own results
945 if ( $.isFunction( registry[module].dependencies ) ) {
946 registry[module].dependencies = registry[module].dependencies();
947 // Ensures the module's dependencies are always in an array
948 if ( typeof registry[module].dependencies !== 'object' ) {
949 registry[module].dependencies = [registry[module].dependencies];
950 }
951 }
952 if ( $.inArray( module, resolved ) !== -1 ) {
953 // Module already resolved; nothing to do
954 return;
955 }
956 // Create unresolved if not passed in
957 if ( !unresolved ) {
958 unresolved = {};
959 }
960 // Tracks down dependencies
961 deps = registry[module].dependencies;
962 len = deps.length;
963 for ( n = 0; n < len; n += 1 ) {
964 if ( $.inArray( deps[n], resolved ) === -1 ) {
965 if ( unresolved[deps[n]] ) {
966 throw new Error(
967 'Circular reference detected: ' + module +
968 ' -> ' + deps[n]
969 );
970 }
971
972 // Add to unresolved
973 unresolved[module] = true;
974 sortDependencies( deps[n], resolved, unresolved );
975 delete unresolved[module];
976 }
977 }
978 resolved[resolved.length] = module;
979 }
980
981 /**
982 * Get a list of module names that a module depends on in their proper dependency
983 * order.
984 *
985 * @private
986 * @param {string} module Module name or array of string module names
987 * @return {Array} List of dependencies, including 'module'.
988 * @throws {Error} If circular reference is detected
989 */
990 function resolve( module ) {
991 var m, resolved;
992
993 // Allow calling with an array of module names
994 if ( $.isArray( module ) ) {
995 resolved = [];
996 for ( m = 0; m < module.length; m += 1 ) {
997 sortDependencies( module[m], resolved );
998 }
999 return resolved;
1000 }
1001
1002 if ( typeof module === 'string' ) {
1003 resolved = [];
1004 sortDependencies( module, resolved );
1005 return resolved;
1006 }
1007
1008 throw new Error( 'Invalid module argument: ' + module );
1009 }
1010
1011 /**
1012 * Narrow down a list of module names to those matching a specific
1013 * state (see #registry for a list of valid states).
1014 *
1015 * One can also filter for 'unregistered', which will return the
1016 * modules names that don't have a registry entry.
1017 *
1018 * @private
1019 * @param {string|string[]} states Module states to filter by
1020 * @param {Array} [modules] List of module names to filter (optional, by default the
1021 * entire registry is used)
1022 * @return {Array} List of filtered module names
1023 */
1024 function filter( states, modules ) {
1025 var list, module, s, m;
1026
1027 // Allow states to be given as a string
1028 if ( typeof states === 'string' ) {
1029 states = [states];
1030 }
1031 // If called without a list of modules, build and use a list of all modules
1032 list = [];
1033 if ( modules === undefined ) {
1034 modules = [];
1035 for ( module in registry ) {
1036 modules[modules.length] = module;
1037 }
1038 }
1039 // Build a list of modules which are in one of the specified states
1040 for ( s = 0; s < states.length; s += 1 ) {
1041 for ( m = 0; m < modules.length; m += 1 ) {
1042 if ( !hasOwn.call( registry, modules[m] ) ) {
1043 // Module does not exist
1044 if ( states[s] === 'unregistered' ) {
1045 // OK, undefined
1046 list[list.length] = modules[m];
1047 }
1048 } else {
1049 // Module exists, check state
1050 if ( registry[modules[m]].state === states[s] ) {
1051 // OK, correct state
1052 list[list.length] = modules[m];
1053 }
1054 }
1055 }
1056 }
1057 return list;
1058 }
1059
1060 /**
1061 * Determine whether all dependencies are in state 'ready', which means we may
1062 * execute the module or job now.
1063 *
1064 * @private
1065 * @param {Array} dependencies Dependencies (module names) to be checked.
1066 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
1067 */
1068 function allReady( dependencies ) {
1069 return filter( 'ready', dependencies ).length === dependencies.length;
1070 }
1071
1072 /**
1073 * A module has entered state 'ready', 'error', or 'missing'. Automatically update
1074 * pending jobs and modules that depend upon this module. If the given module failed,
1075 * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
1076 * all jobs/modules now having their dependencies satisfied.
1077 *
1078 * Jobs that depend on a failed module, will have their error callback ran (if any).
1079 *
1080 * @private
1081 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
1082 */
1083 function handlePending( module ) {
1084 var j, job, hasErrors, m, stateChange;
1085
1086 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
1087 // If the current module failed, mark all dependent modules also as failed.
1088 // Iterate until steady-state to propagate the error state upwards in the
1089 // dependency tree.
1090 do {
1091 stateChange = false;
1092 for ( m in registry ) {
1093 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
1094 if ( filter( ['error', 'missing'], registry[m].dependencies ).length ) {
1095 registry[m].state = 'error';
1096 stateChange = true;
1097 }
1098 }
1099 }
1100 } while ( stateChange );
1101 }
1102
1103 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1104 for ( j = 0; j < jobs.length; j += 1 ) {
1105 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
1106 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
1107 // All dependencies satisfied, or some have errors
1108 job = jobs[j];
1109 jobs.splice( j, 1 );
1110 j -= 1;
1111 try {
1112 if ( hasErrors ) {
1113 if ( $.isFunction( job.error ) ) {
1114 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
1115 }
1116 } else {
1117 if ( $.isFunction( job.ready ) ) {
1118 job.ready();
1119 }
1120 }
1121 } catch ( e ) {
1122 // A user-defined callback raised an exception.
1123 // Swallow it to protect our state machine!
1124 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
1125 }
1126 }
1127 }
1128
1129 if ( registry[module].state === 'ready' ) {
1130 // The current module became 'ready'. Set it in the module store, and recursively execute all
1131 // dependent modules that are loaded and now have all dependencies satisfied.
1132 mw.loader.store.set( module, registry[module] );
1133 for ( m in registry ) {
1134 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
1135 execute( m );
1136 }
1137 }
1138 }
1139 }
1140
1141 /**
1142 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
1143 * depending on whether document-ready has occurred yet and whether we are in async mode.
1144 *
1145 * @private
1146 * @param {string} src URL to script, will be used as the src attribute in the script tag
1147 * @param {Function} [callback] Callback which will be run when the script is done
1148 * @param {boolean} [async=false] Whether to load modules asynchronously.
1149 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1150 */
1151 function addScript( src, callback, async ) {
1152 // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
1153 if ( $.isReady || async ) {
1154 $.ajax( {
1155 url: src,
1156 dataType: 'script',
1157 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1158 // XHR for a same domain request instead of <script>, which changes the request
1159 // headers (potentially missing a cache hit), and reduces caching in general
1160 // since browsers cache XHR much less (if at all). And XHR means we retreive
1161 // text, so we'd need to $.globalEval, which then messes up line numbers.
1162 crossDomain: true,
1163 cache: true,
1164 async: true
1165 } ).always( callback );
1166 } else {
1167 /*jshint evil:true */
1168 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1169 if ( callback ) {
1170 // Document.write is synchronous, so this is called when it's done.
1171 // FIXME: That's a lie. doc.write isn't actually synchronous.
1172 callback();
1173 }
1174 }
1175 }
1176
1177 /**
1178 * Executes a loaded module, making it ready to use
1179 *
1180 * @private
1181 * @param {string} module Module name to execute
1182 */
1183 function execute( module ) {
1184 var key, value, media, i, urls, cssHandle, checkCssHandles,
1185 cssHandlesRegistered = false;
1186
1187 if ( !hasOwn.call( registry, module ) ) {
1188 throw new Error( 'Module has not been registered yet: ' + module );
1189 } else if ( registry[module].state === 'registered' ) {
1190 throw new Error( 'Module has not been requested from the server yet: ' + module );
1191 } else if ( registry[module].state === 'loading' ) {
1192 throw new Error( 'Module has not completed loading yet: ' + module );
1193 } else if ( registry[module].state === 'ready' ) {
1194 throw new Error( 'Module has already been executed: ' + module );
1195 }
1196
1197 /**
1198 * Define loop-function here for efficiency
1199 * and to avoid re-using badly scoped variables.
1200 * @ignore
1201 */
1202 function addLink( media, url ) {
1203 var el = document.createElement( 'link' );
1204 // Support: IE
1205 // Insert in document *before* setting href
1206 getMarker().before( el );
1207 el.rel = 'stylesheet';
1208 if ( media && media !== 'all' ) {
1209 el.media = media;
1210 }
1211 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1212 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1213 el.href = url;
1214 }
1215
1216 function runScript() {
1217 var script, markModuleReady, nestedAddScript;
1218 try {
1219 script = registry[module].script;
1220 markModuleReady = function () {
1221 registry[module].state = 'ready';
1222 handlePending( module );
1223 };
1224 nestedAddScript = function ( arr, callback, async, i ) {
1225 // Recursively call addScript() in its own callback
1226 // for each element of arr.
1227 if ( i >= arr.length ) {
1228 // We're at the end of the array
1229 callback();
1230 return;
1231 }
1232
1233 addScript( arr[i], function () {
1234 nestedAddScript( arr, callback, async, i + 1 );
1235 }, async );
1236 };
1237
1238 if ( $.isArray( script ) ) {
1239 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1240 } else if ( $.isFunction( script ) ) {
1241 registry[module].state = 'ready';
1242 // Pass jQuery twice so that the signature of the closure which wraps
1243 // the script can bind both '$' and 'jQuery'.
1244 script( $, $ );
1245 handlePending( module );
1246 }
1247 } catch ( e ) {
1248 // This needs to NOT use mw.log because these errors are common in production mode
1249 // and not in debug mode, such as when a symbol that should be global isn't exported
1250 registry[module].state = 'error';
1251 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
1252 handlePending( module );
1253 }
1254 }
1255
1256 // This used to be inside runScript, but since that is now fired asychronously
1257 // (after CSS is loaded) we need to set it here right away. It is crucial that
1258 // when execute() is called this is set synchronously, otherwise modules will get
1259 // executed multiple times as the registry will state that it isn't loading yet.
1260 registry[module].state = 'loading';
1261
1262 // Add localizations to message system
1263 if ( $.isPlainObject( registry[module].messages ) ) {
1264 mw.messages.set( registry[module].messages );
1265 }
1266
1267 // Initialise templates
1268 if ( registry[module].templates ) {
1269 mw.templates.set( module, registry[module].templates );
1270 }
1271
1272 if ( $.isReady || registry[module].async ) {
1273 // Make sure we don't run the scripts until all (potentially asynchronous)
1274 // stylesheet insertions have completed.
1275 ( function () {
1276 var pending = 0;
1277 checkCssHandles = function () {
1278 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1279 // one of the cssHandles is fired while we're still creating more handles.
1280 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1281 runScript();
1282 runScript = undefined; // Revoke
1283 }
1284 };
1285 cssHandle = function () {
1286 var check = checkCssHandles;
1287 pending++;
1288 return function () {
1289 if ( check ) {
1290 pending--;
1291 check();
1292 check = undefined; // Revoke
1293 }
1294 };
1295 };
1296 }() );
1297 } else {
1298 // We are in blocking mode, and so we can't afford to wait for CSS
1299 cssHandle = function () {};
1300 // Run immediately
1301 checkCssHandles = runScript;
1302 }
1303
1304 // Process styles (see also mw.loader.implement)
1305 // * back-compat: { <media>: css }
1306 // * back-compat: { <media>: [url, ..] }
1307 // * { "css": [css, ..] }
1308 // * { "url": { <media>: [url, ..] } }
1309 if ( $.isPlainObject( registry[module].style ) ) {
1310 for ( key in registry[module].style ) {
1311 value = registry[module].style[key];
1312 media = undefined;
1313
1314 if ( key !== 'url' && key !== 'css' ) {
1315 // Backwards compatibility, key is a media-type
1316 if ( typeof value === 'string' ) {
1317 // back-compat: { <media>: css }
1318 // Ignore 'media' because it isn't supported (nor was it used).
1319 // Strings are pre-wrapped in "@media". The media-type was just ""
1320 // (because it had to be set to something).
1321 // This is one of the reasons why this format is no longer used.
1322 addEmbeddedCSS( value, cssHandle() );
1323 } else {
1324 // back-compat: { <media>: [url, ..] }
1325 media = key;
1326 key = 'bc-url';
1327 }
1328 }
1329
1330 // Array of css strings in key 'css',
1331 // or back-compat array of urls from media-type
1332 if ( $.isArray( value ) ) {
1333 for ( i = 0; i < value.length; i += 1 ) {
1334 if ( key === 'bc-url' ) {
1335 // back-compat: { <media>: [url, ..] }
1336 addLink( media, value[i] );
1337 } else if ( key === 'css' ) {
1338 // { "css": [css, ..] }
1339 addEmbeddedCSS( value[i], cssHandle() );
1340 }
1341 }
1342 // Not an array, but a regular object
1343 // Array of urls inside media-type key
1344 } else if ( typeof value === 'object' ) {
1345 // { "url": { <media>: [url, ..] } }
1346 for ( media in value ) {
1347 urls = value[media];
1348 for ( i = 0; i < urls.length; i += 1 ) {
1349 addLink( media, urls[i] );
1350 }
1351 }
1352 }
1353 }
1354 }
1355
1356 // Kick off.
1357 cssHandlesRegistered = true;
1358 checkCssHandles();
1359 }
1360
1361 /**
1362 * Adds a dependencies to the queue with optional callbacks to be run
1363 * when the dependencies are ready or fail
1364 *
1365 * @private
1366 * @param {string|string[]} dependencies Module name or array of string module names
1367 * @param {Function} [ready] Callback to execute when all dependencies are ready
1368 * @param {Function} [error] Callback to execute when any dependency fails
1369 * @param {boolean} [async=false] Whether to load modules asynchronously.
1370 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1371 */
1372 function request( dependencies, ready, error, async ) {
1373 var n;
1374
1375 // Allow calling by single module name
1376 if ( typeof dependencies === 'string' ) {
1377 dependencies = [dependencies];
1378 }
1379
1380 // Add ready and error callbacks if they were given
1381 if ( ready !== undefined || error !== undefined ) {
1382 jobs[jobs.length] = {
1383 'dependencies': filter(
1384 ['registered', 'loading', 'loaded'],
1385 dependencies
1386 ),
1387 'ready': ready,
1388 'error': error
1389 };
1390 }
1391
1392 // Queue up any dependencies that are registered
1393 dependencies = filter( ['registered'], dependencies );
1394 for ( n = 0; n < dependencies.length; n += 1 ) {
1395 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1396 queue[queue.length] = dependencies[n];
1397 if ( async ) {
1398 // Mark this module as async in the registry
1399 registry[dependencies[n]].async = true;
1400 }
1401 }
1402 }
1403
1404 // Work the queue
1405 mw.loader.work();
1406 }
1407
1408 function sortQuery( o ) {
1409 var key,
1410 sorted = {},
1411 a = [];
1412
1413 for ( key in o ) {
1414 if ( hasOwn.call( o, key ) ) {
1415 a.push( key );
1416 }
1417 }
1418 a.sort();
1419 for ( key = 0; key < a.length; key += 1 ) {
1420 sorted[a[key]] = o[a[key]];
1421 }
1422 return sorted;
1423 }
1424
1425 /**
1426 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1427 * to a query string of the form foo.bar,baz|bar.baz,quux
1428 * @private
1429 */
1430 function buildModulesString( moduleMap ) {
1431 var p, prefix,
1432 arr = [];
1433
1434 for ( prefix in moduleMap ) {
1435 p = prefix === '' ? '' : prefix + '.';
1436 arr.push( p + moduleMap[prefix].join( ',' ) );
1437 }
1438 return arr.join( '|' );
1439 }
1440
1441 /**
1442 * Asynchronously append a script tag to the end of the body
1443 * that invokes load.php
1444 * @private
1445 * @param {Object} moduleMap Module map, see #buildModulesString
1446 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1447 * @param {string} sourceLoadScript URL of load.php
1448 * @param {boolean} async Whether to load modules asynchronously.
1449 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1450 */
1451 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1452 var request = $.extend(
1453 { modules: buildModulesString( moduleMap ) },
1454 currReqBase
1455 );
1456 request = sortQuery( request );
1457 // Support: IE6
1458 // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
1459 // isn't actually used in IE6, but MediaWiki enforces it in general.
1460 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1461 }
1462
1463 /**
1464 * Resolve indexed dependencies.
1465 *
1466 * ResourceLoader uses an optimization to save space which replaces module names in
1467 * dependency lists with the index of that module within the array of module
1468 * registration data if it exists. The benefit is a significant reduction in the data
1469 * size of the startup module. This function changes those dependency lists back to
1470 * arrays of strings.
1471 *
1472 * @param {Array} modules Modules array
1473 */
1474 function resolveIndexedDependencies( modules ) {
1475 var i, iLen, j, jLen, module, dependency;
1476
1477 // Expand indexed dependency names
1478 for ( i = 0, iLen = modules.length; i < iLen; i++ ) {
1479 module = modules[i];
1480 if ( module[2] ) {
1481 for ( j = 0, jLen = module[2].length; j < jLen; j++ ) {
1482 dependency = module[2][j];
1483 if ( typeof dependency === 'number' ) {
1484 module[2][j] = modules[dependency][0];
1485 }
1486 }
1487 }
1488 }
1489 }
1490
1491 /* Public Members */
1492 return {
1493 /**
1494 * The module registry is exposed as an aid for debugging and inspecting page
1495 * state; it is not a public interface for modifying the registry.
1496 *
1497 * @see #registry
1498 * @property
1499 * @private
1500 */
1501 moduleRegistry: registry,
1502
1503 /**
1504 * @inheritdoc #newStyleTag
1505 * @method
1506 */
1507 addStyleTag: newStyleTag,
1508
1509 /**
1510 * Batch-request queued dependencies from the server.
1511 */
1512 work: function () {
1513 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1514 source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
1515 currReqBase, currReqBaseLength, moduleMap, l,
1516 lastDotIndex, prefix, suffix, bytesAdded, async;
1517
1518 // Build a list of request parameters common to all requests.
1519 reqBase = {
1520 skin: mw.config.get( 'skin' ),
1521 lang: mw.config.get( 'wgUserLanguage' ),
1522 debug: mw.config.get( 'debug' )
1523 };
1524 // Split module batch by source and by group.
1525 splits = {};
1526 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1527
1528 // Appends a list of modules from the queue to the batch
1529 for ( q = 0; q < queue.length; q += 1 ) {
1530 // Only request modules which are registered
1531 if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
1532 // Prevent duplicate entries
1533 if ( $.inArray( queue[q], batch ) === -1 ) {
1534 batch[batch.length] = queue[q];
1535 // Mark registered modules as loading
1536 registry[queue[q]].state = 'loading';
1537 }
1538 }
1539 }
1540
1541 mw.loader.store.init();
1542 if ( mw.loader.store.enabled ) {
1543 concatSource = [];
1544 origBatch = batch;
1545 batch = $.grep( batch, function ( module ) {
1546 var source = mw.loader.store.get( module );
1547 if ( source ) {
1548 concatSource.push( source );
1549 return false;
1550 }
1551 return true;
1552 } );
1553 try {
1554 $.globalEval( concatSource.join( ';' ) );
1555 } catch ( err ) {
1556 // Not good, the cached mw.loader.implement calls failed! This should
1557 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1558 // Depending on how corrupt the string is, it is likely that some
1559 // modules' implement() succeeded while the ones after the error will
1560 // never run and leave their modules in the 'loading' state forever.
1561
1562 // Since this is an error not caused by an individual module but by
1563 // something that infected the implement call itself, don't take any
1564 // risks and clear everything in this cache.
1565 mw.loader.store.clear();
1566 // Re-add the ones still pending back to the batch and let the server
1567 // repopulate these modules to the cache.
1568 // This means that at most one module will be useless (the one that had
1569 // the error) instead of all of them.
1570 mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
1571 origBatch = $.grep( origBatch, function ( module ) {
1572 return registry[module].state === 'loading';
1573 } );
1574 batch = batch.concat( origBatch );
1575 }
1576 }
1577
1578 // Early exit if there's nothing to load...
1579 if ( !batch.length ) {
1580 return;
1581 }
1582
1583 // The queue has been processed into the batch, clear up the queue.
1584 queue = [];
1585
1586 // Always order modules alphabetically to help reduce cache
1587 // misses for otherwise identical content.
1588 batch.sort();
1589
1590 // Split batch by source and by group.
1591 for ( b = 0; b < batch.length; b += 1 ) {
1592 bSource = registry[batch[b]].source;
1593 bGroup = registry[batch[b]].group;
1594 if ( !hasOwn.call( splits, bSource ) ) {
1595 splits[bSource] = {};
1596 }
1597 if ( !hasOwn.call( splits[bSource], bGroup ) ) {
1598 splits[bSource][bGroup] = [];
1599 }
1600 bSourceGroup = splits[bSource][bGroup];
1601 bSourceGroup[bSourceGroup.length] = batch[b];
1602 }
1603
1604 // Clear the batch - this MUST happen before we append any
1605 // script elements to the body or it's possible that a script
1606 // will be locally cached, instantly load, and work the batch
1607 // again, all before we've cleared it causing each request to
1608 // include modules which are already loaded.
1609 batch = [];
1610
1611 for ( source in splits ) {
1612
1613 sourceLoadScript = sources[source];
1614
1615 for ( group in splits[source] ) {
1616
1617 // Cache access to currently selected list of
1618 // modules for this group from this source.
1619 modules = splits[source][group];
1620
1621 // Calculate the highest timestamp
1622 maxVersion = 0;
1623 for ( g = 0; g < modules.length; g += 1 ) {
1624 if ( registry[modules[g]].version > maxVersion ) {
1625 maxVersion = registry[modules[g]].version;
1626 }
1627 }
1628
1629 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1630 // For user modules append a user name to the request.
1631 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1632 currReqBase.user = mw.config.get( 'wgUserName' );
1633 }
1634 currReqBaseLength = $.param( currReqBase ).length;
1635 async = true;
1636 // We may need to split up the request to honor the query string length limit,
1637 // so build it piece by piece.
1638 l = currReqBaseLength + 9; // '&modules='.length == 9
1639
1640 moduleMap = {}; // { prefix: [ suffixes ] }
1641
1642 for ( i = 0; i < modules.length; i += 1 ) {
1643 // Determine how many bytes this module would add to the query string
1644 lastDotIndex = modules[i].lastIndexOf( '.' );
1645
1646 // If lastDotIndex is -1, substr() returns an empty string
1647 prefix = modules[i].substr( 0, lastDotIndex );
1648 suffix = modules[i].slice( lastDotIndex + 1 );
1649
1650 bytesAdded = hasOwn.call( moduleMap, prefix )
1651 ? suffix.length + 3 // '%2C'.length == 3
1652 : modules[i].length + 3; // '%7C'.length == 3
1653
1654 // If the request would become too long, create a new one,
1655 // but don't create empty requests
1656 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1657 // This request would become too long, create a new one
1658 // and fire off the old one
1659 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1660 moduleMap = {};
1661 async = true;
1662 l = currReqBaseLength + 9;
1663 }
1664 if ( !hasOwn.call( moduleMap, prefix ) ) {
1665 moduleMap[prefix] = [];
1666 }
1667 moduleMap[prefix].push( suffix );
1668 if ( !registry[modules[i]].async ) {
1669 // If this module is blocking, make the entire request blocking
1670 // This is slightly suboptimal, but in practice mixing of blocking
1671 // and async modules will only occur in debug mode.
1672 async = false;
1673 }
1674 l += bytesAdded;
1675 }
1676 // If there's anything left in moduleMap, request that too
1677 if ( !$.isEmptyObject( moduleMap ) ) {
1678 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1679 }
1680 }
1681 }
1682 },
1683
1684 /**
1685 * Register a source.
1686 *
1687 * The #work method will use this information to split up requests by source.
1688 *
1689 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1690 *
1691 * @param {string} id Short string representing a source wiki, used internally for
1692 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1693 * @param {Object|string} loadUrl load.php url, may be an object for backwards-compatibility
1694 * @return {boolean}
1695 */
1696 addSource: function ( id, loadUrl ) {
1697 var source;
1698 // Allow multiple additions
1699 if ( typeof id === 'object' ) {
1700 for ( source in id ) {
1701 mw.loader.addSource( source, id[source] );
1702 }
1703 return true;
1704 }
1705
1706 if ( hasOwn.call( sources, id ) ) {
1707 throw new Error( 'source already registered: ' + id );
1708 }
1709
1710 if ( typeof loadUrl === 'object' ) {
1711 loadUrl = loadUrl.loadScript;
1712 }
1713
1714 sources[id] = loadUrl;
1715
1716 return true;
1717 },
1718
1719 /**
1720 * Register a module, letting the system know about it and its
1721 * properties. Startup modules contain calls to this function.
1722 *
1723 * When using multiple module registration by passing an array, dependencies that
1724 * are specified as references to modules within the array will be resolved before
1725 * the modules are registered.
1726 *
1727 * @param {string|Array} module Module name or array of arrays, each containing
1728 * a list of arguments compatible with this method
1729 * @param {number} version Module version number as a timestamp (falls backs to 0)
1730 * @param {string|Array|Function} dependencies One string or array of strings of module
1731 * names on which this module depends, or a function that returns that array.
1732 * @param {string} [group=null] Group which the module is in
1733 * @param {string} [source='local'] Name of the source
1734 * @param {string} [skip=null] Script body of the skip function
1735 */
1736 register: function ( module, version, dependencies, group, source, skip ) {
1737 var i, len;
1738 // Allow multiple registration
1739 if ( typeof module === 'object' ) {
1740 resolveIndexedDependencies( module );
1741 for ( i = 0, len = module.length; i < len; i++ ) {
1742 // module is an array of module names
1743 if ( typeof module[i] === 'string' ) {
1744 mw.loader.register( module[i] );
1745 // module is an array of arrays
1746 } else if ( typeof module[i] === 'object' ) {
1747 mw.loader.register.apply( mw.loader, module[i] );
1748 }
1749 }
1750 return;
1751 }
1752 // Validate input
1753 if ( typeof module !== 'string' ) {
1754 throw new Error( 'module must be a string, not a ' + typeof module );
1755 }
1756 if ( hasOwn.call( registry, module ) ) {
1757 throw new Error( 'module already registered: ' + module );
1758 }
1759 // List the module as registered
1760 registry[module] = {
1761 version: version !== undefined ? parseInt( version, 10 ) : 0,
1762 dependencies: [],
1763 group: typeof group === 'string' ? group : null,
1764 source: typeof source === 'string' ? source : 'local',
1765 state: 'registered',
1766 skip: typeof skip === 'string' ? skip : null
1767 };
1768 if ( typeof dependencies === 'string' ) {
1769 // Allow dependencies to be given as a single module name
1770 registry[module].dependencies = [ dependencies ];
1771 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1772 // Allow dependencies to be given as an array of module names
1773 // or a function which returns an array
1774 registry[module].dependencies = dependencies;
1775 }
1776 },
1777
1778 /**
1779 * Implement a module given the components that make up the module.
1780 *
1781 * When #load or #using requests one or more modules, the server
1782 * response contain calls to this function.
1783 *
1784 * All arguments are required.
1785 *
1786 * @param {string} module Name of module
1787 * @param {Function|Array} script Function with module code or Array of URLs to
1788 * be used as the src attribute of a new `<script>` tag.
1789 * @param {Object} [style] Should follow one of the following patterns:
1790 *
1791 * { "css": [css, ..] }
1792 * { "url": { <media>: [url, ..] } }
1793 *
1794 * And for backwards compatibility (needs to be supported forever due to caching):
1795 *
1796 * { <media>: css }
1797 * { <media>: [url, ..] }
1798 *
1799 * The reason css strings are not concatenated anymore is bug 31676. We now check
1800 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1801 *
1802 * @param {Object} [msgs] List of key/value pairs to be added to mw#messages.
1803 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1804 */
1805 implement: function ( module, script, style, msgs, templates ) {
1806 // Validate input
1807 if ( typeof module !== 'string' ) {
1808 throw new Error( 'module must be of type string, not ' + typeof module );
1809 }
1810 if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
1811 throw new Error( 'script must be of type function or array, not ' + typeof script );
1812 }
1813 if ( style && !$.isPlainObject( style ) ) {
1814 throw new Error( 'style must be of type object, not ' + typeof style );
1815 }
1816 if ( msgs && !$.isPlainObject( msgs ) ) {
1817 throw new Error( 'msgs must be of type object, not a ' + typeof msgs );
1818 }
1819 if ( templates && !$.isPlainObject( templates ) ) {
1820 throw new Error( 'templates must be of type object, not a ' + typeof templates );
1821 }
1822 // Automatically register module
1823 if ( !hasOwn.call( registry, module ) ) {
1824 mw.loader.register( module );
1825 }
1826 // Check for duplicate implementation
1827 if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
1828 throw new Error( 'module already implemented: ' + module );
1829 }
1830 // Attach components
1831 registry[module].script = script || [];
1832 registry[module].style = style || {};
1833 registry[module].messages = msgs || {};
1834 registry[module].templates = templates || {};
1835 // The module may already have been marked as erroneous
1836 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1837 registry[module].state = 'loaded';
1838 if ( allReady( registry[module].dependencies ) ) {
1839 execute( module );
1840 }
1841 }
1842 },
1843
1844 /**
1845 * Execute a function as soon as one or more required modules are ready.
1846 *
1847 * Example of inline dependency on OOjs:
1848 *
1849 * mw.loader.using( 'oojs', function () {
1850 * OO.compare( [ 1 ], [ 1 ] );
1851 * } );
1852 *
1853 * @param {string|Array} dependencies Module name or array of modules names the callback
1854 * dependends on to be ready before executing
1855 * @param {Function} [ready] Callback to execute when all dependencies are ready
1856 * @param {Function} [error] Callback to execute if one or more dependencies failed
1857 * @return {jQuery.Promise}
1858 * @since 1.23 this returns a promise
1859 */
1860 using: function ( dependencies, ready, error ) {
1861 var deferred = $.Deferred();
1862
1863 // Allow calling with a single dependency as a string
1864 if ( typeof dependencies === 'string' ) {
1865 dependencies = [ dependencies ];
1866 } else if ( !$.isArray( dependencies ) ) {
1867 // Invalid input
1868 throw new Error( 'Dependencies must be a string or an array' );
1869 }
1870
1871 if ( ready ) {
1872 deferred.done( ready );
1873 }
1874 if ( error ) {
1875 deferred.fail( error );
1876 }
1877
1878 // Resolve entire dependency map
1879 dependencies = resolve( dependencies );
1880 if ( allReady( dependencies ) ) {
1881 // Run ready immediately
1882 deferred.resolve();
1883 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1884 // Execute error immediately if any dependencies have errors
1885 deferred.reject(
1886 new Error( 'One or more dependencies failed to load' ),
1887 dependencies
1888 );
1889 } else {
1890 // Not all dependencies are ready: queue up a request
1891 request( dependencies, deferred.resolve, deferred.reject );
1892 }
1893
1894 return deferred.promise();
1895 },
1896
1897 /**
1898 * Load an external script or one or more modules.
1899 *
1900 * @param {string|Array} modules Either the name of a module, array of modules,
1901 * or a URL of an external script or style
1902 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1903 * external script or style; acceptable values are "text/css" and
1904 * "text/javascript"; if no type is provided, text/javascript is assumed.
1905 * @param {boolean} [async] Whether to load modules asynchronously.
1906 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1907 * Defaults to `true` if loading a URL, `false` otherwise.
1908 */
1909 load: function ( modules, type, async ) {
1910 var filtered, m, module, l;
1911
1912 // Validate input
1913 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1914 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1915 }
1916 // Allow calling with an external url or single dependency as a string
1917 if ( typeof modules === 'string' ) {
1918 if ( /^(https?:)?\/\//.test( modules ) ) {
1919 if ( async === undefined ) {
1920 // Assume async for bug 34542
1921 async = true;
1922 }
1923 if ( type === 'text/css' ) {
1924 // Support: IE 7-8
1925 // Use properties instead of attributes as IE throws security
1926 // warnings when inserting a <link> tag with a protocol-relative
1927 // URL set though attributes - when on HTTPS. See bug 41331.
1928 l = document.createElement( 'link' );
1929 l.rel = 'stylesheet';
1930 l.href = modules;
1931 $( 'head' ).append( l );
1932 return;
1933 }
1934 if ( type === 'text/javascript' || type === undefined ) {
1935 addScript( modules, null, async );
1936 return;
1937 }
1938 // Unknown type
1939 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1940 }
1941 // Called with single module
1942 modules = [ modules ];
1943 }
1944
1945 // Filter out undefined modules, otherwise resolve() will throw
1946 // an exception for trying to load an undefined module.
1947 // Undefined modules are acceptable here in load(), because load() takes
1948 // an array of unrelated modules, whereas the modules passed to
1949 // using() are related and must all be loaded.
1950 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1951 if ( hasOwn.call( registry, modules[m] ) ) {
1952 module = registry[modules[m]];
1953 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1954 filtered[filtered.length] = modules[m];
1955 }
1956 }
1957 }
1958
1959 if ( filtered.length === 0 ) {
1960 return;
1961 }
1962 // Resolve entire dependency map
1963 filtered = resolve( filtered );
1964 // If all modules are ready, nothing to be done
1965 if ( allReady( filtered ) ) {
1966 return;
1967 }
1968 // If any modules have errors: also quit.
1969 if ( filter( ['error', 'missing'], filtered ).length ) {
1970 return;
1971 }
1972 // Since some modules are not yet ready, queue up a request.
1973 request( filtered, undefined, undefined, async );
1974 },
1975
1976 /**
1977 * Change the state of one or more modules.
1978 *
1979 * @param {string|Object} module Module name or object of module name/state pairs
1980 * @param {string} state State name
1981 */
1982 state: function ( module, state ) {
1983 var m;
1984
1985 if ( typeof module === 'object' ) {
1986 for ( m in module ) {
1987 mw.loader.state( m, module[m] );
1988 }
1989 return;
1990 }
1991 if ( !hasOwn.call( registry, module ) ) {
1992 mw.loader.register( module );
1993 }
1994 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1995 && registry[module].state !== state ) {
1996 // Make sure pending modules depending on this one get executed if their
1997 // dependencies are now fulfilled!
1998 registry[module].state = state;
1999 handlePending( module );
2000 } else {
2001 registry[module].state = state;
2002 }
2003 },
2004
2005 /**
2006 * Get the version of a module.
2007 *
2008 * @param {string} module Name of module
2009 * @return {string|null} The version, or null if the module (or its version) is not
2010 * in the registry.
2011 */
2012 getVersion: function ( module ) {
2013 if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
2014 return null;
2015 }
2016 return formatVersionNumber( registry[module].version );
2017 },
2018
2019 /**
2020 * Get the state of a module.
2021 *
2022 * @param {string} module Name of module
2023 * @return {string|null} The state, or null if the module (or its state) is not
2024 * in the registry.
2025 */
2026 getState: function ( module ) {
2027 if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
2028 return null;
2029 }
2030 return registry[module].state;
2031 },
2032
2033 /**
2034 * Get the names of all registered modules.
2035 *
2036 * @return {Array}
2037 */
2038 getModuleNames: function () {
2039 return $.map( registry, function ( i, key ) {
2040 return key;
2041 } );
2042 },
2043
2044 /**
2045 * @inheritdoc mw.inspect#runReports
2046 * @method
2047 */
2048 inspect: function () {
2049 var args = slice.call( arguments );
2050 mw.loader.using( 'mediawiki.inspect', function () {
2051 mw.inspect.runReports.apply( mw.inspect, args );
2052 } );
2053 },
2054
2055 /**
2056 * On browsers that implement the localStorage API, the module store serves as a
2057 * smart complement to the browser cache. Unlike the browser cache, the module store
2058 * can slice a concatenated response from ResourceLoader into its constituent
2059 * modules and cache each of them separately, using each module's versioning scheme
2060 * to determine when the cache should be invalidated.
2061 *
2062 * @singleton
2063 * @class mw.loader.store
2064 */
2065 store: {
2066 // Whether the store is in use on this page.
2067 enabled: null,
2068
2069 // The contents of the store, mapping '[module name]@[version]' keys
2070 // to module implementations.
2071 items: {},
2072
2073 // Cache hit stats
2074 stats: { hits: 0, misses: 0, expired: 0 },
2075
2076 /**
2077 * Construct a JSON-serializable object representing the content of the store.
2078 * @return {Object} Module store contents.
2079 */
2080 toJSON: function () {
2081 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2082 },
2083
2084 /**
2085 * Get the localStorage key for the entire module store. The key references
2086 * $wgDBname to prevent clashes between wikis which share a common host.
2087 *
2088 * @return {string} localStorage item key
2089 */
2090 getStoreKey: function () {
2091 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2092 },
2093
2094 /**
2095 * Get a key on which to vary the module cache.
2096 * @return {string} String of concatenated vary conditions.
2097 */
2098 getVary: function () {
2099 return [
2100 mw.config.get( 'skin' ),
2101 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2102 mw.config.get( 'wgUserLanguage' )
2103 ].join( ':' );
2104 },
2105
2106 /**
2107 * Get a key for a specific module. The key format is '[name]@[version]'.
2108 *
2109 * @param {string} module Module name
2110 * @return {string|null} Module key or null if module does not exist
2111 */
2112 getModuleKey: function ( module ) {
2113 return hasOwn.call( registry, module ) ?
2114 ( module + '@' + registry[module].version ) : null;
2115 },
2116
2117 /**
2118 * Initialize the store.
2119 *
2120 * Retrieves store from localStorage and (if successfully retrieved) decoding
2121 * the stored JSON value to a plain object.
2122 *
2123 * The try / catch block is used for JSON & localStorage feature detection.
2124 * See the in-line documentation for Modernizr's localStorage feature detection
2125 * code for a full account of why we need a try / catch:
2126 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2127 */
2128 init: function () {
2129 var raw, data;
2130
2131 if ( mw.loader.store.enabled !== null ) {
2132 // Init already ran
2133 return;
2134 }
2135
2136 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
2137 // Disabled by configuration.
2138 // Clear any previous store to free up space. (T66721)
2139 mw.loader.store.clear();
2140 mw.loader.store.enabled = false;
2141 return;
2142 }
2143 if ( mw.config.get( 'debug' ) ) {
2144 // Disable module store in debug mode
2145 mw.loader.store.enabled = false;
2146 return;
2147 }
2148
2149 try {
2150 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2151 // If we get here, localStorage is available; mark enabled
2152 mw.loader.store.enabled = true;
2153 data = JSON.parse( raw );
2154 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2155 mw.loader.store.items = data.items;
2156 return;
2157 }
2158 } catch ( e ) {
2159 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
2160 }
2161
2162 if ( raw === undefined ) {
2163 // localStorage failed; disable store
2164 mw.loader.store.enabled = false;
2165 } else {
2166 mw.loader.store.update();
2167 }
2168 },
2169
2170 /**
2171 * Retrieve a module from the store and update cache hit stats.
2172 *
2173 * @param {string} module Module name
2174 * @return {string|boolean} Module implementation or false if unavailable
2175 */
2176 get: function ( module ) {
2177 var key;
2178
2179 if ( !mw.loader.store.enabled ) {
2180 return false;
2181 }
2182
2183 key = mw.loader.store.getModuleKey( module );
2184 if ( key in mw.loader.store.items ) {
2185 mw.loader.store.stats.hits++;
2186 return mw.loader.store.items[key];
2187 }
2188 mw.loader.store.stats.misses++;
2189 return false;
2190 },
2191
2192 /**
2193 * Stringify a module and queue it for storage.
2194 *
2195 * @param {string} module Module name
2196 * @param {Object} descriptor The module's descriptor as set in the registry
2197 */
2198 set: function ( module, descriptor ) {
2199 var args, key;
2200
2201 if ( !mw.loader.store.enabled ) {
2202 return false;
2203 }
2204
2205 key = mw.loader.store.getModuleKey( module );
2206
2207 if (
2208 // Already stored a copy of this exact version
2209 key in mw.loader.store.items ||
2210 // Module failed to load
2211 descriptor.state !== 'ready' ||
2212 // Unversioned, private, or site-/user-specific
2213 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2214 // Partial descriptor
2215 $.inArray( undefined, [ descriptor.script, descriptor.style,
2216 descriptor.messages, descriptor.templates ] ) !== -1
2217 ) {
2218 // Decline to store
2219 return false;
2220 }
2221
2222 try {
2223 args = [
2224 JSON.stringify( module ),
2225 typeof descriptor.script === 'function' ?
2226 String( descriptor.script ) :
2227 JSON.stringify( descriptor.script ),
2228 JSON.stringify( descriptor.style ),
2229 JSON.stringify( descriptor.messages ),
2230 JSON.stringify( descriptor.templates )
2231 ];
2232 // Attempted workaround for a possible Opera bug (bug T59567).
2233 // This regex should never match under sane conditions.
2234 if ( /^\s*\(/.test( args[1] ) ) {
2235 args[1] = 'function' + args[1];
2236 mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
2237 }
2238 } catch ( e ) {
2239 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
2240 return;
2241 }
2242
2243 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2244 mw.loader.store.update();
2245 },
2246
2247 /**
2248 * Iterate through the module store, removing any item that does not correspond
2249 * (in name and version) to an item in the module registry.
2250 */
2251 prune: function () {
2252 var key, module;
2253
2254 if ( !mw.loader.store.enabled ) {
2255 return false;
2256 }
2257
2258 for ( key in mw.loader.store.items ) {
2259 module = key.slice( 0, key.indexOf( '@' ) );
2260 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2261 mw.loader.store.stats.expired++;
2262 delete mw.loader.store.items[key];
2263 }
2264 }
2265 },
2266
2267 /**
2268 * Clear the entire module store right now.
2269 */
2270 clear: function () {
2271 mw.loader.store.items = {};
2272 localStorage.removeItem( mw.loader.store.getStoreKey() );
2273 },
2274
2275 /**
2276 * Sync modules to localStorage.
2277 *
2278 * This function debounces localStorage updates. When called multiple times in
2279 * quick succession, the calls are coalesced into a single update operation.
2280 * This allows us to call #update without having to consider the module load
2281 * queue; the call to localStorage.setItem will be naturally deferred until the
2282 * page is quiescent.
2283 *
2284 * Because localStorage is shared by all pages with the same origin, if multiple
2285 * pages are loaded with different module sets, the possibility exists that
2286 * modules saved by one page will be clobbered by another. But the impact would
2287 * be minor and the problem would be corrected by subsequent page views.
2288 *
2289 * @method
2290 */
2291 update: ( function () {
2292 var timer;
2293
2294 function flush() {
2295 var data,
2296 key = mw.loader.store.getStoreKey();
2297
2298 if ( !mw.loader.store.enabled ) {
2299 return false;
2300 }
2301 mw.loader.store.prune();
2302 try {
2303 // Replacing the content of the module store might fail if the new
2304 // contents would exceed the browser's localStorage size limit. To
2305 // avoid clogging the browser with stale data, always remove the old
2306 // value before attempting to set the new one.
2307 localStorage.removeItem( key );
2308 data = JSON.stringify( mw.loader.store );
2309 localStorage.setItem( key, data );
2310 } catch ( e ) {
2311 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
2312 }
2313 }
2314
2315 return function () {
2316 clearTimeout( timer );
2317 timer = setTimeout( flush, 2000 );
2318 };
2319 }() )
2320 }
2321 };
2322 }() ),
2323
2324 /**
2325 * HTML construction helper functions
2326 *
2327 * @example
2328 *
2329 * var Html, output;
2330 *
2331 * Html = mw.html;
2332 * output = Html.element( 'div', {}, new Html.Raw(
2333 * Html.element( 'img', { src: '<' } )
2334 * ) );
2335 * mw.log( output ); // <div><img src="&lt;"/></div>
2336 *
2337 * @class mw.html
2338 * @singleton
2339 */
2340 html: ( function () {
2341 function escapeCallback( s ) {
2342 switch ( s ) {
2343 case '\'':
2344 return '&#039;';
2345 case '"':
2346 return '&quot;';
2347 case '<':
2348 return '&lt;';
2349 case '>':
2350 return '&gt;';
2351 case '&':
2352 return '&amp;';
2353 }
2354 }
2355
2356 return {
2357 /**
2358 * Escape a string for HTML.
2359 *
2360 * Converts special characters to HTML entities.
2361 *
2362 * mw.html.escape( '< > \' & "' );
2363 * // Returns &lt; &gt; &#039; &amp; &quot;
2364 *
2365 * @param {string} s The string to escape
2366 * @return {string} HTML
2367 */
2368 escape: function ( s ) {
2369 return s.replace( /['"<>&]/g, escapeCallback );
2370 },
2371
2372 /**
2373 * Create an HTML element string, with safe escaping.
2374 *
2375 * @param {string} name The tag name.
2376 * @param {Object} attrs An object with members mapping element names to values
2377 * @param {Mixed} contents The contents of the element. May be either:
2378 *
2379 * - string: The string is escaped.
2380 * - null or undefined: The short closing form is used, e.g. `<br/>`.
2381 * - this.Raw: The value attribute is included without escaping.
2382 * - this.Cdata: The value attribute is included, and an exception is
2383 * thrown if it contains an illegal ETAGO delimiter.
2384 * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
2385 * @return {string} HTML
2386 */
2387 element: function ( name, attrs, contents ) {
2388 var v, attrName, s = '<' + name;
2389
2390 for ( attrName in attrs ) {
2391 v = attrs[attrName];
2392 // Convert name=true, to name=name
2393 if ( v === true ) {
2394 v = attrName;
2395 // Skip name=false
2396 } else if ( v === false ) {
2397 continue;
2398 }
2399 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2400 }
2401 if ( contents === undefined || contents === null ) {
2402 // Self close tag
2403 s += '/>';
2404 return s;
2405 }
2406 // Regular open tag
2407 s += '>';
2408 switch ( typeof contents ) {
2409 case 'string':
2410 // Escaped
2411 s += this.escape( contents );
2412 break;
2413 case 'number':
2414 case 'boolean':
2415 // Convert to string
2416 s += String( contents );
2417 break;
2418 default:
2419 if ( contents instanceof this.Raw ) {
2420 // Raw HTML inclusion
2421 s += contents.value;
2422 } else if ( contents instanceof this.Cdata ) {
2423 // CDATA
2424 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2425 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2426 }
2427 s += contents.value;
2428 } else {
2429 throw new Error( 'mw.html.element: Invalid type of contents' );
2430 }
2431 }
2432 s += '</' + name + '>';
2433 return s;
2434 },
2435
2436 /**
2437 * Wrapper object for raw HTML passed to mw.html.element().
2438 * @class mw.html.Raw
2439 */
2440 Raw: function ( value ) {
2441 this.value = value;
2442 },
2443
2444 /**
2445 * Wrapper object for CDATA element contents passed to mw.html.element()
2446 * @class mw.html.Cdata
2447 */
2448 Cdata: function ( value ) {
2449 this.value = value;
2450 }
2451 };
2452 }() ),
2453
2454 // Skeleton user object. mediawiki.user.js extends this
2455 user: {
2456 options: new Map(),
2457 tokens: new Map()
2458 },
2459
2460 /**
2461 * Registry and firing of events.
2462 *
2463 * MediaWiki has various interface components that are extended, enhanced
2464 * or manipulated in some other way by extensions, gadgets and even
2465 * in core itself.
2466 *
2467 * This framework helps streamlining the timing of when these other
2468 * code paths fire their plugins (instead of using document-ready,
2469 * which can and should be limited to firing only once).
2470 *
2471 * Features like navigating to other wiki pages, previewing an edit
2472 * and editing itself – without a refresh – can then retrigger these
2473 * hooks accordingly to ensure everything still works as expected.
2474 *
2475 * Example usage:
2476 *
2477 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2478 * mw.hook( 'wikipage.content' ).fire( $content );
2479 *
2480 * Handlers can be added and fired for arbitrary event names at any time. The same
2481 * event can be fired multiple times. The last run of an event is memorized
2482 * (similar to `$(document).ready` and `$.Deferred().done`).
2483 * This means if an event is fired, and a handler added afterwards, the added
2484 * function will be fired right away with the last given event data.
2485 *
2486 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2487 * Thus allowing flexible use and optimal maintainability and authority control.
2488 * You can pass around the `add` and/or `fire` method to another piece of code
2489 * without it having to know the event name (or `mw.hook` for that matter).
2490 *
2491 * var h = mw.hook( 'bar.ready' );
2492 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2493 *
2494 * Note: Events are documented with an underscore instead of a dot in the event
2495 * name due to jsduck not supporting dots in that position.
2496 *
2497 * @class mw.hook
2498 */
2499 hook: ( function () {
2500 var lists = {};
2501
2502 /**
2503 * Create an instance of mw.hook.
2504 *
2505 * @method hook
2506 * @member mw
2507 * @param {string} name Name of hook.
2508 * @return {mw.hook}
2509 */
2510 return function ( name ) {
2511 var list = hasOwn.call( lists, name ) ?
2512 lists[name] :
2513 lists[name] = $.Callbacks( 'memory' );
2514
2515 return {
2516 /**
2517 * Register a hook handler
2518 * @param {Function...} handler Function to bind.
2519 * @chainable
2520 */
2521 add: list.add,
2522
2523 /**
2524 * Unregister a hook handler
2525 * @param {Function...} handler Function to unbind.
2526 * @chainable
2527 */
2528 remove: list.remove,
2529
2530 /**
2531 * Run a hook.
2532 * @param {Mixed...} data
2533 * @chainable
2534 */
2535 fire: function () {
2536 return list.fireWith.call( this, null, slice.call( arguments ) );
2537 }
2538 };
2539 };
2540 }() )
2541 };
2542
2543 // Alias $j to jQuery for backwards compatibility
2544 // @deprecated since 1.23 Use $ or jQuery instead
2545 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2546
2547 /**
2548 * Log a message to window.console, if possible.
2549 *
2550 * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
2551 * also in production mode). Gets console references in each invocation instead of caching the
2552 * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
2553 *
2554 * @private
2555 * @method log_
2556 * @param {string} topic Stream name passed by mw.track
2557 * @param {Object} data Data passed by mw.track
2558 * @param {Error} [data.exception]
2559 * @param {string} data.source Error source
2560 * @param {string} [data.module] Name of module which caused the error
2561 */
2562 function log( topic, data ) {
2563 var msg,
2564 e = data.exception,
2565 source = data.source,
2566 module = data.module,
2567 console = window.console;
2568
2569 if ( console && console.log ) {
2570 msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
2571 if ( module ) {
2572 msg += ' in module ' + module;
2573 }
2574 msg += ( e ? ':' : '.' );
2575 console.log( msg );
2576
2577 // If we have an exception object, log it to the error channel to trigger a
2578 // proper stacktraces in browsers that support it. No fallback as we have no browsers
2579 // that don't support error(), but do support log().
2580 if ( e && console.error ) {
2581 console.error( String( e ), e );
2582 }
2583 }
2584 }
2585
2586 // subscribe to error streams
2587 mw.trackSubscribe( 'resourceloader.exception', log );
2588 mw.trackSubscribe( 'resourceloader.assert', log );
2589
2590 // Attach to window and globally alias
2591 window.mw = window.mediaWiki = mw;
2592 }( jQuery ) );