* @alternateClassName mediaWiki
* @singleton
*/
-/*jshint latedef:false */
+
+/* eslint-disable no-use-before-define */
+
( function ( $ ) {
'use strict';
- var mw,
+ var mw, StringSet, log,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
trackCallbacks = $.Callbacks( 'memory' ),
* @return {string} hash as an seven-character base 36 string
*/
function fnv132( str ) {
- /*jshint bitwise:false */
+ /* eslint-disable no-bitwise */
var hash = 0x811C9DC5,
i;
}
return hash;
+ /* eslint-enable no-bitwise */
}
+ 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 this.set.hasOwnProperty( value );
+ };
+ return StringSet;
+ }() );
+
/**
- * Create an object that can be read from or written to from methods that allow
+ * Create an object that can be read from or written to via methods that allow
* interaction both with single and multiple properties at once.
*
- * @example
- *
- * var collection, query, results;
- *
- * // Create your address book
- * collection = new mw.Map();
- *
- * // This data could be coming from an external source (eg. API/AJAX)
- * collection.set( {
- * 'John Doe': 'john@example.org',
- * 'Jane Doe': 'jane@example.org',
- * 'George van Halen': 'gvanhalen@example.org'
- * } );
- *
- * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
- *
- * // You can detect missing keys first
- * if ( !collection.exists( wanted ) ) {
- * // One or more are missing (in this case: "Daniel Jackson")
- * mw.log( 'One or more names were not found in your address book' );
- * }
- *
- * // Or just let it give you what it can. Optionally fill in from a default.
- * results = collection.get( wanted, 'nobody@example.com' );
- * mw.log( results['Jane Doe'] ); // "jane@example.org"
- * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
- *
+ * @private
* @class mw.Map
*
* @constructor
- * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
- * empty object.
- * For backwards-compatibility with mw.config, this can also be `true` in which case values
- * are copied to the Window object as global variables (T72470). Values are copied in
- * one direction only. Changes to globals are not reflected in the map.
+ * @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( values ) {
- if ( values === true ) {
- this.values = {};
+ function Map( global ) {
+ this.internalValues = {};
+ if ( global === true ) {
// Override #set to also set the global variable
this.set = function ( selection, value ) {
}
return false;
};
-
- return;
}
- this.values = values || {};
+ // Deprecated since MediaWiki 1.28
+ log.deprecate(
+ this,
+ 'values',
+ this.internalValues,
+ 'mw.Map#values is deprecated. Use mw.Map#get() instead.',
+ 'Map-values'
+ );
}
/**
* @param {Mixed} value
*/
function setGlobalMapValue( map, key, value ) {
- map.values[ key ] = value;
- mw.log.deprecate(
+ map.internalValues[ key ] = value;
+ log.deprecate(
window,
key,
value,
}
Map.prototype = {
+ constructor: Map,
+
/**
* Get the value of one or more keys.
*
* @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, the 'values' container is returned. (Beware that,
+ * If no selection is passed, the internal container is returned. (Beware that,
* as is the default in JavaScript, the object is returned by reference.)
*/
get: function ( selection, fallback ) {
}
if ( typeof selection === 'string' ) {
- if ( !hasOwn.call( this.values, selection ) ) {
+ if ( !hasOwn.call( this.internalValues, selection ) ) {
return fallback;
}
- return this.values[ selection ];
+ return this.internalValues[ selection ];
}
if ( selection === undefined ) {
- return this.values;
+ return this.internalValues;
}
// Invalid selection key
if ( $.isPlainObject( selection ) ) {
for ( s in selection ) {
- this.values[ s ] = selection[ s ];
+ this.internalValues[ s ] = selection[ s ];
}
return true;
}
if ( typeof selection === 'string' && arguments.length > 1 ) {
- this.values[ selection ] = value;
+ this.internalValues[ selection ] = value;
return true;
}
return false;
if ( $.isArray( selection ) ) {
for ( s = 0; s < selection.length; s++ ) {
- if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) {
+ if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.internalValues, selection[ s ] ) ) {
return false;
}
}
return true;
}
- return typeof selection === 'string' && hasOwn.call( this.values, selection );
+ return typeof selection === 'string' && hasOwn.call( this.internalValues, selection );
}
};
var text;
if ( !this.exists() ) {
- // Use <key> as text if key does not exist
- if ( this.format === 'escaped' || this.format === 'parse' ) {
- // format 'escaped' and 'parse' need to have the brackets and key html escaped
- return mw.html.escape( '<' + this.key + '>' );
- }
- return '<' + this.key + '>';
+ // 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' ) {
}
};
+ log = ( function () {
+ // Also update the restoration of methods in mediawiki.log.js
+ // when adding or removing methods here.
+ var log = function () {},
+ console = window.console;
+
+ /**
+ * @class mw.log
+ * @singleton
+ */
+
+ /**
+ * Write a message to the console's warning channel.
+ * Actions not supported by the browser console are silently ignored.
+ *
+ * @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 console's error channel.
+ *
+ * Most browsers provide a stacktrace by default if the argument
+ * is a caught Error object.
+ *
+ * @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 in 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;
+ }() );
+
/**
* @class mw
*/
},
/**
- * Dummy placeholder for {@link mw.log}
+ * No-op dummy placeholder for {@link mw.log} in debug mode.
*
* @method
*/
- log: ( function () {
- // Also update the restoration of methods in mediawiki.log.js
- // when adding or removing methods here.
- var log = function () {},
- console = window.console;
-
- /**
- * @class mw.log
- * @singleton
- */
-
- /**
- * Write a message to the console's warning channel.
- * Actions not supported by the browser console are silently ignored.
- *
- * @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 console's error channel.
- *
- * Most browsers provide a stacktrace by default if the argument
- * is a caught Error object.
- *
- * @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 in a host object that, when accessed, will produce
- * a deprecation warning in the console with backtrace.
- *
- * @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
- */
- log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
- obj[ key ] = val;
- } : function ( obj, key, val, msg ) {
- /*globals Set */
- msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
- var logged, loggedIsSet, uniqueTrace;
- if ( window.Set ) {
- logged = new Set();
- loggedIsSet = true;
- } else {
- logged = {};
- loggedIsSet = false;
- }
- uniqueTrace = function () {
- var trace = new Error().stack;
- if ( loggedIsSet ) {
- if ( logged.has( trace ) ) {
- return false;
- }
- logged.add( trace );
- return true;
- } else {
- if ( logged.hasOwnProperty( trace ) ) {
- return false;
- }
- logged[ trace ] = 1;
- return true;
- }
- };
- Object.defineProperty( obj, key, {
- configurable: true,
- enumerable: true,
- get: function () {
- if ( uniqueTrace() ) {
- mw.track( 'mw.deprecate', key );
- mw.log.warn( msg );
- }
- return val;
- },
- set: function ( newVal ) {
- if ( uniqueTrace() ) {
- mw.track( 'mw.deprecate', key );
- mw.log.warn( msg );
- }
- val = newVal;
- }
- } );
-
- };
-
- return log;
- }() ),
+ log: log,
/**
* Client for ResourceLoader server end point.
* dependencies, such that later modules depend on earlier modules. The array
* contains the module names. If the array contains already some module names,
* this function appends its result to the pre-existing array.
- * @param {Object} [unresolved] Hash used to track the current dependency
- * chain; used to report loops in the dependency graph.
+ * @param {StringSet} [unresolved] Used to track the current dependency
+ * chain, and to report loops in the dependency graph.
* @throws {Error} If any unregistered module or a dependency loop is encountered
*/
function sortDependencies( module, resolved, unresolved ) {
}
if ( registry[ module ].skip !== null ) {
- /*jshint evil:true */
+ // eslint-disable-next-line no-new-func
skip = new Function( registry[ module ].skip );
registry[ module ].skip = null;
if ( skip() ) {
}
// Create unresolved if not passed in
if ( !unresolved ) {
- unresolved = {};
+ unresolved = new StringSet();
}
// Tracks down dependencies
deps = registry[ module ].dependencies;
for ( i = 0; i < deps.length; i++ ) {
if ( $.inArray( deps[ i ], resolved ) === -1 ) {
- if ( unresolved[ deps[ i ] ] ) {
+ if ( unresolved.has( deps[ i ] ) ) {
throw new Error( mw.format(
'Circular reference detected: $1 -> $2',
module,
) );
}
- // Add to unresolved
- unresolved[ module ] = true;
+ unresolved.add( module );
sortDependencies( deps[ i ], resolved, unresolved );
}
}
* @private
* @param {string[]} modules Array of string module names
* @return {Array} List of dependencies, including 'module'.
+ * @throws {Error} If an unregistered module or a dependency loop is encountered
*/
function resolve( modules ) {
var resolved = [];
pendingRequests.push( function () {
if ( moduleName && hasOwn.call( registry, moduleName ) ) {
+ // Emulate runScript() part of execute()
window.require = mw.loader.require;
window.module = registry[ moduleName ].module;
}
addScript( src ).always( function () {
- // Clear environment
- delete window.require;
+ // 'module.exports' should not persist after the file is executed to
+ // avoid leakage to unrelated code. 'require' should be kept, however,
+ // as asynchronous access to 'require' is allowed and expected. (T144879)
delete window.module;
r.resolve();
} );
};
- implicitDependencies = ( $.inArray( module, legacyModules ) !== -1 )
- ? []
- : legacyModules;
+ implicitDependencies = ( $.inArray( module, legacyModules ) !== -1 ) ?
+ [] :
+ legacyModules;
if ( module === 'user' ) {
// Implicit dependency on the site module. Not real dependency because
implicitDependencies.push( 'site' );
}
- legacyWait = implicitDependencies.length
- ? mw.loader.using( implicitDependencies )
- : $.Deferred().resolve();
+ legacyWait = implicitDependencies.length ?
+ mw.loader.using( implicitDependencies ) :
+ $.Deferred().resolve();
legacyWait.always( function () {
try {
markModuleReady();
}
} catch ( e ) {
- // This needs to NOT use mw.log because these errors are common in production mode
- // and not in debug mode, such as when a symbol that should be global isn't exported
+ // Use mw.track instead of mw.log because these errors are common in production mode
+ // (e.g. undefined variable), and mw.log is only enabled in debug mode.
registry[ module ].state = 'error';
mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
handlePending( module );
prefix = modules[ i ].substr( 0, lastDotIndex );
suffix = modules[ i ].slice( lastDotIndex + 1 );
- bytesAdded = hasOwn.call( moduleMap, prefix )
- ? suffix.length + 3 // '%2C'.length == 3
- : modules[ i ].length + 3; // '%7C'.length == 3
+ bytesAdded = hasOwn.call( moduleMap, prefix ) ?
+ suffix.length + 3 : // '%2C'.length == 3
+ modules[ i ].length + 3; // '%7C'.length == 3
// If the url would become too long, create a new one,
// but don't create empty requests
}
/**
- * Evaluate a batch of load.php responses retrieved from mw.loader.store.
- *
* @private
* @param {string[]} implementations Array containing pieces of JavaScript code in the
* form of calls to mw.loader#implement().
* @param {Function} cb Callback in case of failure
* @param {Error} cb.err
*/
- function batchEval( implementations, cb ) {
+ function asyncEval( implementations, cb ) {
if ( !implementations.length ) {
return;
}
- mw.requestIdleCallback( function iterate( deadline ) {
- while ( implementations[ 0 ] && deadline.timeRemaining() > 5 ) {
- try {
- $.globalEval( implementations.shift() );
- } catch ( err ) {
- cb( err );
- return;
- }
- }
- if ( implementations[ 0 ] ) {
- mw.requestIdleCallback( iterate );
+ mw.requestIdleCallback( function () {
+ try {
+ $.globalEval( implementations.join( ';' ) );
+ } catch ( err ) {
+ cb( err );
}
} );
}
+ /**
+ * Make a versioned key for a specific module.
+ *
+ * @private
+ * @param {string} module Module name
+ * @return {string|null} Module key in format '`[name]@[version]`',
+ * or null if the module does not exist
+ */
+ function getModuleKey( module ) {
+ return hasOwn.call( registry, module ) ?
+ ( module + '@' + registry[ module ].version ) : null;
+ }
+
+ /**
+ * @private
+ * @param {string} key Module name or '`[name]@[version]`'
+ * @return {Object}
+ */
+ function splitModuleKey( key ) {
+ var index = key.indexOf( '@' );
+ if ( index === -1 ) {
+ return { name: key };
+ }
+ return {
+ name: key.slice( 0, index ),
+ version: key.slice( index + 1 )
+ };
+ }
+
/* Public Members */
return {
/**
}
return true;
} );
- batchEval( implementations, function ( err ) {
+ asyncEval( implementations, function ( err ) {
+ var failed;
// Not good, the cached mw.loader.implement calls failed! This should
// never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
// Depending on how corrupt the string is, it is likely that some
// modules' implement() succeeded while the ones after the error will
// never run and leave their modules in the 'loading' state forever.
+ mw.loader.store.stats.failed++;
+
// Since this is an error not caused by an individual module but by
// something that infected the implement call itself, don't take any
// risks and clear everything in this cache.
mw.loader.store.clear();
- mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
+ mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
// Re-add the failed ones that are still pending back to the batch
- var failed = $.grep( sourceModules, function ( module ) {
+ failed = $.grep( sourceModules, function ( module ) {
return registry[ module ].state === 'loading';
} );
batchRequest( failed );
* When #load() or #using() requests one or more modules, the server
* response contain calls to this function.
*
- * @param {string} module Name of module
+ * @param {string} module Name of module and current module version. Formatted
+ * as '`[name]@[version]`". This version should match the requested version
+ * (from #batchRequest and #registry). This avoids race conditions (T117587).
+ * For back-compat with MediaWiki 1.27 and earlier, the version may be omitted.
* @param {Function|Array|string} [script] Function with module code, list of URLs
* to load via `<script src>`, or string of module code for `$.globalEval()`.
* @param {Object} [style] Should follow one of the following patterns:
* @param {Object} [templates] List of key/value pairs to be added to mw#templates.
*/
implement: function ( module, script, style, messages, templates ) {
+ var split = splitModuleKey( module ),
+ name = split.name,
+ version = split.version;
// Automatically register module
- if ( !hasOwn.call( registry, module ) ) {
- mw.loader.register( module );
+ if ( !hasOwn.call( registry, name ) ) {
+ mw.loader.register( name );
}
// Check for duplicate implementation
- if ( hasOwn.call( registry, module ) && registry[ module ].script !== undefined ) {
- throw new Error( 'module already implemented: ' + module );
+ if ( hasOwn.call( registry, name ) && registry[ name ].script !== undefined ) {
+ throw new Error( 'module already implemented: ' + name );
+ }
+ if ( version ) {
+ // Without this reset, if there is a version mismatch between the
+ // requested and received module version, then mw.loader.store would
+ // cache the response under the requested key. Thus poisoning the cache
+ // indefinitely with a stale value. (T117587)
+ registry[ name ].version = version;
}
// Attach components
- registry[ module ].script = script || null;
- registry[ module ].style = style || null;
- registry[ module ].messages = messages || null;
- registry[ module ].templates = templates || null;
+ registry[ name ].script = script || null;
+ registry[ name ].style = style || null;
+ registry[ name ].messages = messages || null;
+ registry[ name ].templates = templates || null;
// The module may already have been marked as erroneous
- if ( $.inArray( registry[ module ].state, [ 'error', 'missing' ] ) === -1 ) {
- registry[ module ].state = 'loaded';
- if ( allReady( registry[ module ].dependencies ) ) {
- execute( module );
+ if ( $.inArray( registry[ name ].state, [ 'error', 'missing' ] ) === -1 ) {
+ registry[ name ].state = 'loaded';
+ if ( allReady( registry[ name ].dependencies ) ) {
+ execute( name );
}
}
},
deferred.fail( error );
}
- // Resolve entire dependency map
- dependencies = resolve( dependencies );
+ try {
+ // Resolve entire dependency map
+ dependencies = resolve( dependencies );
+ } catch ( e ) {
+ return deferred.reject( e ).promise();
+ }
if ( allReady( dependencies ) ) {
// Run ready immediately
deferred.resolve( mw.loader.require );
MODULE_SIZE_MAX: 100 * 1000,
- // The contents of the store, mapping '[module name]@[version]' keys
+ // The contents of the store, mapping '[name]@[version]' keys
// to module implementations.
items: {},
// Cache hit stats
- stats: { hits: 0, misses: 0, expired: 0 },
+ stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
/**
* Construct a JSON-serializable object representing the content of the store.
].join( ':' );
},
- /**
- * Get a key for a specific module. The key format is '[name]@[version]'.
- *
- * @param {string} module Module name
- * @return {string|null} Module key or null if module does not exist
- */
- getModuleKey: function ( module ) {
- return hasOwn.call( registry, module ) ?
- ( module + '@' + registry[ module ].version ) : null;
- },
-
/**
* Initialize the store.
*
return false;
}
- key = mw.loader.store.getModuleKey( module );
+ key = getModuleKey( module );
if ( key in mw.loader.store.items ) {
mw.loader.store.stats.hits++;
return mw.loader.store.items[ key ];
return false;
}
- key = mw.loader.store.getModuleKey( module );
+ key = getModuleKey( module );
if (
// Already stored a copy of this exact version
// Partial descriptor
// (e.g. skipped module, or style module with state=ready)
$.inArray( undefined, [ descriptor.script, descriptor.style,
- descriptor.messages, descriptor.templates ] ) !== -1
+ descriptor.messages, descriptor.templates ] ) !== -1
) {
// Decline to store
return false;
try {
args = [
- JSON.stringify( module ),
+ JSON.stringify( key ),
typeof descriptor.script === 'function' ?
String( descriptor.script ) :
JSON.stringify( descriptor.script ),
for ( key in mw.loader.store.items ) {
module = key.slice( 0, key.indexOf( '@' ) );
- if ( mw.loader.store.getModuleKey( module ) !== key ) {
+ if ( getModuleKey( module ) !== key ) {
mw.loader.store.stats.expired++;
delete mw.loader.store.items[ key ];
} else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
* - this.Raw: The raw value is directly included.
* - this.Cdata: The raw value is directly included. An exception is
* thrown if it contains any illegal ETAGO delimiter.
- * See <http://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
+ * See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
* @return {string} HTML
*/
element: function ( name, attrs, contents ) {
* reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
*
* @private
- * @method log_
* @param {string} topic Stream name passed by mw.track
* @param {Object} data Data passed by mw.track
* @param {Error} [data.exception]
* @param {string} data.source Error source
* @param {string} [data.module] Name of module which caused the error
*/
- function log( topic, data ) {
+ function logError( topic, data ) {
var msg,
e = data.exception,
source = data.source,
}
// Subscribe to error streams
- mw.trackSubscribe( 'resourceloader.exception', log );
- mw.trackSubscribe( 'resourceloader.assert', log );
+ mw.trackSubscribe( 'resourceloader.exception', logError );
+ mw.trackSubscribe( 'resourceloader.assert', logError );
/**
* Fired when all modules associated with the page have finished loading.