/** * Base library for MediaWiki. * * Exposed globally as `mediaWiki` with `mw` as shortcut. * * @class mw * @alternateClassName mediaWiki * @singleton */ /* global mwNow */ /* eslint-disable no-use-before-define */ ( function ( $ ) { 'use strict'; var mw, StringSet, log, hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice, trackCallbacks = $.Callbacks( 'memory' ), trackHandlers = [], trackQueue = []; /** * FNV132 hash function * * This function implements the 32-bit version of FNV-1. * It is equivalent to hash( 'fnv132', ... ) in PHP, except * its output is base 36 rather than hex. * See * * @private * @param {string} str String to hash * @return {string} hash as an seven-character base 36 string */ function fnv132( str ) { /* eslint-disable no-bitwise */ var hash = 0x811C9DC5, i; for ( i = 0; i < str.length; i++ ) { hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 ); hash ^= str.charCodeAt( i ); } hash = ( hash >>> 0 ).toString( 36 ); while ( hash.length < 7 ) { hash = '0' + hash; } return hash; /* eslint-enable no-bitwise */ } function defineFallbacks() { // StringSet = window.Set || ( function () { /** * @private * @class */ function StringSet() { this.set = {}; } StringSet.prototype.add = function ( value ) { this.set[ value ] = true; }; StringSet.prototype.has = function ( value ) { return hasOwn.call( this.set, value ); }; return StringSet; }() ); } /** * Create an object that can be read from or written to via methods that allow * interaction both with single and multiple properties at once. * * @private * @class mw.Map * * @constructor * @param {boolean} [global=false] Whether to synchronise =values to the global * window object (for backwards-compatibility with mw.config; T72470). Values are * copied in one direction only. Changes to globals do not reflect in the map. */ function Map( global ) { this.values = {}; if ( global === true ) { // Override #set to also set the global variable this.set = function ( selection, value ) { var s; if ( $.isPlainObject( selection ) ) { for ( s in selection ) { setGlobalMapValue( this, s, selection[ s ] ); } return true; } if ( typeof selection === 'string' && arguments.length ) { setGlobalMapValue( this, selection, value ); return true; } return false; }; } } /** * Alias property to the global object. * * @private * @static * @param {mw.Map} map * @param {string} key * @param {Mixed} value */ function setGlobalMapValue( map, key, value ) { map.values[ key ] = value; log.deprecate( window, key, value, // Deprecation notice for mw.config globals (T58550, T72470) map === mw.config && 'Use mw.config instead.' ); } Map.prototype = { constructor: Map, /** * Get the value of one or more keys. * * If called with no arguments, all values are returned. * * @param {string|Array} [selection] Key or array of keys to retrieve values for. * @param {Mixed} [fallback=null] Value for keys that don't exist. * @return {Mixed|Object|null} If selection was a string, returns the value, * If selection was an array, returns an object of key/values. * If no selection is passed, a new object with all key/values is returned. */ get: function ( selection, fallback ) { var results, i; fallback = arguments.length > 1 ? fallback : null; if ( Array.isArray( selection ) ) { results = {}; for ( i = 0; i < selection.length; i++ ) { if ( typeof selection[ i ] === 'string' ) { results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ? this.values[ selection[ i ] ] : fallback; } } return results; } if ( typeof selection === 'string' ) { return hasOwn.call( this.values, selection ) ? this.values[ selection ] : fallback; } if ( selection === undefined ) { results = {}; for ( i in this.values ) { results[ i ] = this.values[ i ]; } return results; } // Invalid selection key return fallback; }, /** * Set one or more key/value pairs. * * @param {string|Object} selection Key to set value for, or object mapping keys to values * @param {Mixed} [value] Value to set (optional, only in use when key is a string) * @return {boolean} True on success, false on failure */ set: function ( selection, value ) { var s; if ( $.isPlainObject( selection ) ) { for ( s in selection ) { this.values[ s ] = selection[ s ]; } return true; } if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[ selection ] = value; return true; } return false; }, /** * Check if one or more keys exist. * * @param {Mixed} selection Key or array of keys to check * @return {boolean} True if the key(s) exist */ exists: function ( selection ) { var i; if ( Array.isArray( selection ) ) { for ( i = 0; i < selection.length; i++ ) { if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) { return false; } } return true; } return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** * Object constructor for messages. * * Similar to the Message class in MediaWiki PHP. * * Format defaults to 'text'. * * @example * * var obj, str; * mw.messages.set( { * 'hello': 'Hello world', * 'hello-user': 'Hello, $1!', * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3' * } ); * * obj = new mw.Message( mw.messages, 'hello' ); * mw.log( obj.text() ); * // Hello world * * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] ); * mw.log( obj.text() ); * // Hello, John Doe! * * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] ); * mw.log( obj.text() ); * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago * * // Using mw.message shortcut * obj = mw.message( 'hello-user', 'John Doe' ); * mw.log( obj.text() ); * // Hello, John Doe! * * // Using mw.msg shortcut * str = mw.msg( 'hello-user', 'John Doe' ); * mw.log( str ); * // Hello, John Doe! * * // Different formats * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] ); * * obj.format = 'text'; * str = obj.toString(); * // Same as: * str = obj.text(); * * mw.log( str ); * // Hello, John "Wiki" <3 Doe! * * mw.log( obj.escaped() ); * // Hello, John "Wiki" <3 Doe! * * @class mw.Message * * @constructor * @param {mw.Map} map Message store * @param {string} key * @param {Array} [parameters] */ function Message( map, key, parameters ) { this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); return this; } Message.prototype = { /** * Get parsed contents of the message. * * The default parser does simple $N replacements and nothing else. * This may be overridden to provide a more complex message parser. * The primary override is in the mediawiki.jqueryMsg module. * * This function will not be called for nonexistent messages. * * @return {string} Parsed message */ parser: function () { return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) ); }, // eslint-disable-next-line valid-jsdoc /** * Add (does not replace) parameters for `$N` placeholder values. * * @param {Array} parameters * @chainable */ params: function ( parameters ) { var i; for ( i = 0; i < parameters.length; i++ ) { this.parameters.push( parameters[ i ] ); } return this; }, /** * Convert message object to its string form based on current format. * * @return {string} Message as a string in the current form, or `` if key * does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use ⧼key⧽ as text if key does not exist // Err on the side of safety, ensure that the output // is always html safe in the event the message key is // missing, since in that case its highly likely the // message key is user-controlled. // '⧼' is used instead of '<' to side-step any // double-escaping issues. // (Keep synchronised with Message::toString() in PHP.) return '⧼' + mw.html.escape( this.key ) + '⧽'; } if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } if ( this.format === 'escaped' ) { text = this.parser(); text = mw.html.escape( text ); } return text; }, /** * Change format to 'parse' and convert message to string * * If jqueryMsg is loaded, this parses the message text from wikitext * (where supported) to HTML * * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ parse: function () { this.format = 'parse'; return this.toString(); }, /** * Change format to 'plain' and convert message to string * * This substitutes parameters, but otherwise does not change the * message text. * * @return {string} String form of plain message */ plain: function () { this.format = 'plain'; return this.toString(); }, /** * Change format to 'text' and convert message to string * * If jqueryMsg is loaded, {{-transformation is done where supported * (such as {{plural:}}, {{gender:}}, {{int:}}). * * Otherwise, it is equivalent to plain * * @return {string} String form of text message */ text: function () { this.format = 'text'; return this.toString(); }, /** * Change the format to 'escaped' and convert message to string * * This is equivalent to using the 'text' format (see #text), then * HTML-escaping the output. * * @return {string} String form of html escaped message */ escaped: function () { this.format = 'escaped'; return this.toString(); }, /** * Check if a message exists * * @see mw.Map#exists * @return {boolean} */ exists: function () { return this.map.exists( this.key ); } }; defineFallbacks(); /* eslint-disable no-console */ log = ( function () { /** * Write a verbose message to the browser's console in debug mode. * * This method is mainly intended for verbose logging. It is a no-op in production mode. * In ResourceLoader debug mode, it will use the browser's console if available, with * fallback to creating a console interface in the DOM and logging messages there. * * See {@link mw.log} for other logging methods. * * @member mw * @param {...string} msg Messages to output to console. */ var log = function () {}, console = window.console; // Note: Keep list of methods in sync with restoration in mediawiki.log.js // when adding or removing mw.log methods below! /** * Collection of methods to help log messages to the console. * * @class mw.log * @singleton */ /** * Write a message to the browser console's warning channel. * * This method is a no-op in browsers that don't implement the Console API. * * @param {...string} msg Messages to output to console */ log.warn = console && console.warn && Function.prototype.bind ? Function.prototype.bind.call( console.warn, console ) : $.noop; /** * Write a message to the browser console's error channel. * * Most browsers also print a stacktrace when calling this method if the * argument is an Error object. * * This method is a no-op in browsers that don't implement the Console API. * * @since 1.26 * @param {Error|...string} msg Messages to output to console */ log.error = console && console.error && Function.prototype.bind ? Function.prototype.bind.call( console.error, console ) : $.noop; /** * Create a property on a host object that, when accessed, will produce * a deprecation warning in the console. * * @param {Object} obj Host object of deprecated property * @param {string} key Name of property to create in `obj` * @param {Mixed} val The value this property should return when accessed * @param {string} [msg] Optional text to include in the deprecation message * @param {string} [logName=key] Optional custom name for the feature. * This is used instead of `key` in the message and `mw.deprecate` tracking. */ log.deprecate = !Object.defineProperty ? function ( obj, key, val ) { obj[ key ] = val; } : function ( obj, key, val, msg, logName ) { var logged = new StringSet(); logName = logName || key; msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' ); function uniqueTrace() { var trace = new Error().stack; if ( logged.has( trace ) ) { return false; } logged.add( trace ); return true; } // Support: Safari 5.0 // Throws "not supported on DOM Objects" for Node or Element objects (incl. document) // Safari 4.0 doesn't have this method, and it was fixed in Safari 5.1. try { Object.defineProperty( obj, key, { configurable: true, enumerable: true, get: function () { if ( uniqueTrace() ) { mw.track( 'mw.deprecate', logName ); mw.log.warn( msg ); } return val; }, set: function ( newVal ) { if ( uniqueTrace() ) { mw.track( 'mw.deprecate', logName ); mw.log.warn( msg ); } val = newVal; } } ); } catch ( err ) { obj[ key ] = val; } }; return log; }() ); /* eslint-enable no-console */ /** * @class mw */ mw = { redefineFallbacksForTest: function () { if ( !window.QUnit ) { throw new Error( 'Reset not allowed outside unit tests' ); } defineFallbacks(); }, /** * Get the current time, measured in milliseconds since January 1, 1970 (UTC). * * On browsers that implement the Navigation Timing API, this function will produce floating-point * values with microsecond precision that are guaranteed to be monotonic. On all other browsers, * it will fall back to using `Date`. * * @return {number} Current time */ now: mwNow, // mwNow is defined in startup.js /** * Format a string. Replace $1, $2 ... $N with positional arguments. * * Used by Message#parser(). * * @since 1.25 * @param {string} formatString Format string * @param {...Mixed} parameters Values for $N replacements * @return {string} Formatted string */ format: function ( formatString ) { var parameters = slice.call( arguments, 1 ); return formatString.replace( /\$(\d+)/g, function ( str, match ) { var index = parseInt( match, 10 ) - 1; return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match; } ); }, /** * Track an analytic event. * * This method provides a generic means for MediaWiki JavaScript code to capture state * information for analysis. Each logged event specifies a string topic name that describes * the kind of event that it is. Topic names consist of dot-separated path components, * arranged from most general to most specific. Each path component should have a clear and * well-defined purpose. * * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of * events that match their subcription, including those that fired before the handler was * bound. * * @param {string} topic Topic name * @param {Object} [data] Data describing the event, encoded as an object */ track: function ( topic, data ) { trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } ); trackCallbacks.fire( trackQueue ); }, /** * Register a handler for subset of analytic events, specified by topic. * * Handlers will be called once for each tracked event, including any events that fired before the * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating * the exact time at which the event fired, a string 'topic' property naming the event, and a * 'data' property which is an object of event-specific data. The event topic and event data are * also passed to the callback as the first and second arguments, respectively. * * @param {string} topic Handle events whose name starts with this string prefix * @param {Function} callback Handler to call for each matching tracked event * @param {string} callback.topic * @param {Object} [callback.data] */ trackSubscribe: function ( topic, callback ) { var seen = 0; function handler( trackQueue ) { var event; for ( ; seen < trackQueue.length; seen++ ) { event = trackQueue[ seen ]; if ( event.topic.indexOf( topic ) === 0 ) { callback.call( event, event.topic, event.data ); } } } trackHandlers.push( [ handler, callback ] ); trackCallbacks.add( handler ); }, /** * Stop handling events for a particular handler * * @param {Function} callback */ trackUnsubscribe: function ( callback ) { trackHandlers = trackHandlers.filter( function ( fns ) { if ( fns[ 1 ] === callback ) { trackCallbacks.remove( fns[ 0 ] ); // Ensure the tuple is removed to avoid holding on to closures return false; } return true; } ); }, // Expose Map constructor Map: Map, // Expose Message constructor Message: Message, /** * Map of configuration values. * * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config) * on mediawiki.org. * * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the * global `window` object. * * @property {mw.Map} config */ // Dummy placeholder later assigned in ResourceLoaderStartUpModule config: null, /** * Empty object for third-party libraries, for cases where you don't * want to add a new global, or the global is bad and needs containment * or wrapping. * * @property */ libs: {}, /** * Access container for deprecated functionality that can be moved from * from their legacy location and attached to this object (e.g. a global * function that is deprecated and as stop-gap can be exposed through here). * * This was reserved for future use but never ended up being used. * * @deprecated since 1.22 Let deprecated identifiers keep their original name * and use mw.log#deprecate to create an access container for tracking. * @property */ legacy: {}, /** * Store for messages. * * @property {mw.Map} */ messages: new Map(), /** * Store for templates associated with a module. * * @property {mw.Map} */ templates: new Map(), /** * Get a message object. * * Shortcut for `new mw.Message( mw.messages, key, parameters )`. * * @see mw.Message * @param {string} key Key of message to get * @param {...Mixed} parameters Values for $N replacements * @return {mw.Message} */ message: function ( key ) { var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** * Get a message string using the (default) 'text' format. * * Shortcut for `mw.message( key, parameters... ).text()`. * * @see mw.Message * @param {string} key Key of message to get * @param {...Mixed} parameters Values for $N replacements * @return {string} */ msg: function () { return mw.message.apply( mw.message, arguments ).toString(); }, // Expose mw.log log: log, /** * Client for ResourceLoader server end point. * * This client is in charge of maintaining the module registry and state * machine, initiating network (batch) requests for loading modules, as * well as dependency resolution and execution of source code. * * For more information, refer to * * * @class mw.loader * @singleton */ loader: ( function () { /** * Fired via mw.track on various resource loading errors. * * @event resourceloader_exception * @param {Error|Mixed} e The error that was thrown. Almost always an Error * object, but in theory module code could manually throw something else, and that * might also end up here. * @param {string} [module] Name of the module which caused the error. Omitted if the * error is not module-related or the module cannot be easily identified due to * batched handling. * @param {string} source Source of the error. Possible values: * * - style: stylesheet error (only affects old IE where a special style loading method * is used) * - load-callback: exception thrown by user callback * - module-execute: exception thrown by module code * - resolve: failed to sort dependencies for a module in mw.loader.load * - store-eval: could not evaluate module code cached in localStorage * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init * - store-localstorage-json: JSON conversion error in mw.loader.store.set * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update */ /** * Fired via mw.track on resource loading error conditions. * * @event resourceloader_assert * @param {string} source Source of the error. Possible values: * * - bug-T59567: failed to cache script due to an Opera function -> string conversion * bug; see for details */ /** * Mapping of registered modules. * * See #implement and #execute for exact details on support for script, style and messages. * * Format: * * { * 'moduleName': { * // From mw.loader.register() * 'version': '########' (hash) * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example', (or) null * 'module': export Object * * // Set from execute() or mw.loader.state() * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing' * * // Optionally added at run-time by mw.loader.implement() * 'skipped': true * 'script': closure, array of urls, or string * 'style': { ... } (see #execute) * 'messages': { 'key': 'value', ... } * } * } * * State machine: * * - `registered`: * The module is known to the system but not yet required. * Meta data is registered via mw.loader#register. Calls to that method are * generated server-side by the startup module. * - `loading`: * The module was required through mw.loader (either directly or as dependency of * another module). The client will fetch module contents from the server. * The contents are then stashed in the registry via mw.loader#implement. * - `loaded`: * The module has been loaded from the server and stashed via mw.loader#implement. * If the module has no more dependencies in-flight, the module will be executed * immediately. Otherwise execution is deferred, controlled via #handlePending. * - `executing`: * The module is being executed. * - `ready`: * The module has been successfully executed. * - `error`: * The module (or one of its dependencies) produced an error during execution. * - `missing`: * The module was registered client-side and requested, but the server denied knowledge * of the module's existence. * * @property * @private */ var registry = {}, // Mapping of sources, keyed by source-id, values are strings. // // Format: // // { // 'sourceId': 'http://example.org/w/load.php' // } // sources = {}, // For queueModuleScript() handlingPendingRequests = false, pendingRequests = [], // List of modules to be loaded queue = [], /** * List of callback jobs waiting for modules to be ready. * * Jobs are created by #enqueue() and run by #handlePending(). * * Typically when a job is created for a module, the job's dependencies contain * both the required module and all its recursive dependencies. * * Format: * * { * 'dependencies': [ module names ], * 'ready': Function callback * 'error': Function callback * } * * @property {Object[]} jobs * @private */ jobs = [], // For getMarker() marker = null, // For addEmbeddedCSS() cssBuffer = '', cssBufferTimer = null, cssCallbacks = $.Callbacks(), rAF = window.requestAnimationFrame || setTimeout; function getMarker() { if ( !marker ) { // Cache marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ); if ( !marker ) { mw.log( 'Created ResourceLoaderDynamicStyles marker dynamically' ); marker = document.createElement( 'meta' ); marker.name = 'ResourceLoaderDynamicStyles'; document.head.appendChild( marker ); } } return marker; } /** * Create a new style element and add it to the DOM. * * @private * @param {string} text CSS text * @param {Node} [nextNode] The element where the style tag * should be inserted before * @return {HTMLElement} Reference to the created style element */ function newStyleTag( text, nextNode ) { var s = document.createElement( 'style' ); s.appendChild( document.createTextNode( text ) ); if ( nextNode && nextNode.parentNode ) { nextNode.parentNode.insertBefore( s, nextNode ); } else { document.head.appendChild( s ); } return s; } /** * Add a bit of CSS text to the current browser page. * * The CSS will be appended to an existing ResourceLoader-created `