/** * This file is where we decide whether to initialise the modern support browser run-time. * * - Beware: This file MUST parse without errors on even the most ancient of browsers! */ /* eslint-disable no-implicit-globals */ /* global $CODE, RLQ:true, NORLQ:true */ /** * See * * Browsers that pass these checks get served our modern run-time. This includes all Grade A * browsers, and some Grade C and Grade X browsers. * * The following browsers are known to pass these checks: * - Chrome 63+ * - Edge 79+ * - Opera 50+ * - Firefox 58+ * - Safari 11.1+ * - Mobile Safari 11.2+ (iOS 11+) * - Android 5.0+ * * @private * @return {boolean} User agent is compatible with MediaWiki JS */ function isCompatible() { return !!( // Ensure DOM Level 4 features (including Selectors API). // // https://caniuse.com/#feat=queryselector 'querySelector' in document && // Ensure HTML 5 features (including Web Storage API) // // https://caniuse.com/#feat=namevalue-storage // https://blog.whatwg.org/this-week-in-html-5-episode-30 'localStorage' in window && // Ensure ES2015 grammar and runtime API (a.k.a. ES6) // // In practice, Promise.finally is a good proxy for overall ES6 support and // rejects most unsupporting browsers in one sweep. The feature itself // was specified in ES2018, however. // https://caniuse.com/promise-finally // Chrome 63+, Edge 18+, Opera 50+, Safari 11.1+, Firefox 58+, iOS 11+ // // eslint-disable-next-line es-x/no-promise, es-x/no-promise-prototype-finally, dot-notation typeof Promise === 'function' && Promise.prototype[ 'finally' ] && // ES6 Arrow Functions (with default params), this ensures // genuine syntax support for ES6 grammar, not just API coverage. // // https://caniuse.com/arrow-functions // Chrome 45+, Safari 10+, Firefox 22+, Opera 32+ // // Based on Benjamin De Cock's snippet here: // https://gist.github.com/bendc/d7f3dbc83d0f65ca0433caf90378cd95 ( function () { try { // eslint-disable-next-line no-new, no-new-func new Function( '(a = 0) => a' ); return true; } catch ( e ) { return false; } }() ) && // ES6 RegExp.prototype.flags // // https://caniuse.com/mdn-javascript_builtins_regexp_flags // Edge 79+ (Chromium-based, rejects MSEdgeHTML-based Edge <= 18) // // eslint-disable-next-line es-x/no-regexp-prototype-flags /./g.flags === 'g' ); } if ( !isCompatible() ) { // Handle basic supported browsers (Grade C). // Undo speculative modern (Grade A) root CSS class ``. // See ResourceLoaderClientHtml::getDocumentAttributes(). document.documentElement.className = document.documentElement.className .replace( /(^|\s)client-js(\s|$)/, '$1client-nojs$2' ); // Process any callbacks for basic support (Grade C). while ( window.NORLQ && NORLQ[ 0 ] ) { NORLQ.shift()(); } NORLQ = { push: function ( fn ) { fn(); } }; // Clear and disable the modern (Grade A) queue. RLQ = { push: function () {} }; } else { // Handle modern (Grade A). if ( window.performance && performance.mark ) { performance.mark( 'mwStartup' ); } // This embeds mediawiki.js, which defines 'mw' and 'mw.loader'. /** * Base library for MediaWiki. * * Exposed globally as `mw`, with `mediaWiki` as alias. * * @class mw * @singleton */ /* global $CODE */ ( function () { 'use strict'; var con = window.console; /** * Log a message to window.console. * * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs * also in production mode). * * @private * @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 logError( topic, data ) { var e = data.exception; var msg = ( e ? 'Exception' : 'Error' ) + ' in ' + data.source + ( data.module ? ' in module ' + data.module : '' ) + ( e ? ':' : '.' ); con.log( msg ); // If we have an exception object, log it to the warning channel to trigger // proper stacktraces in browsers that support it. if ( e ) { con.warn( e ); } } /** * 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 */ function Map() { this.values = Object.create( null ); } 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 ) { if ( arguments.length < 2 ) { fallback = null; } if ( typeof selection === 'string' ) { return selection in this.values ? this.values[ selection ] : fallback; } var results; if ( Array.isArray( selection ) ) { results = {}; for ( var i = 0; i < selection.length; i++ ) { if ( typeof selection[ i ] === 'string' ) { results[ selection[ i ] ] = selection[ i ] in this.values ? this.values[ selection[ i ] ] : fallback; } } return results; } if ( selection === undefined ) { results = {}; for ( var key in this.values ) { results[ key ] = this.values[ key ]; } 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 ) { // Use `arguments.length` because `undefined` is also a valid value. if ( arguments.length > 1 ) { // Set one key if ( typeof selection === 'string' ) { this.values[ selection ] = value; return true; } } else if ( typeof selection === 'object' ) { // Set multiple keys for ( var key in selection ) { this.values[ key ] = selection[ key ]; } return true; } return false; }, /** * Check if a given key exists in the map. * * @param {string} selection Key to check * @return {boolean} True if the key exists */ exists: function ( selection ) { return typeof selection === 'string' && selection in this.values; } }; /** * 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. * * See {@link mw.log} for other logging methods. * * @member mw * @param {...string} msg Messages to output to console. */ var log = function () { console.log.apply( console, arguments ); }; /** * Collection of methods to help log messages to the console. * * @class mw.log * @singleton */ /** * Write a message to the browser console's warning channel. * * @param {...string} msg Messages to output to console */ log.warn = Function.prototype.bind.call( con.warn, con ); /** * @class mw */ var mw = { /** * 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: function () { // Optimisation: Cache and re-use the chosen implementation. // Optimisation: Avoid startup overhead by re-defining on first call instead of IIFE. var perf = window.performance; var navStart = perf && perf.timing && perf.timing.navigationStart; // Define the relevant shortcut mw.now = navStart && perf.now ? function () { return navStart + perf.now(); } : Date.now; return mw.now(); }, /** * List of all analytic events emitted so far. * * Exposed only for use by mediawiki.base. * * @private * @property {Array} */ trackQueue: [], track: function ( topic, data ) { mw.trackQueue.push( { topic: topic, data: data } ); // This method is extended by mediawiki.base to also fire events. }, /** * Track an early error event via mw.track and send it to the window console. * * @private * @param {string} topic Topic name * @param {Object} data Data describing the event, encoded as an object; see mw#logError */ trackError: function ( topic, data ) { mw.track( topic, data ); logError( topic, data ); }, // Expose Map constructor Map: Map, /** * 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. * * @property {mw.Map} config */ config: new Map(), /** * Store for messages. * * @property {mw.Map} */ messages: new Map(), /** * Store for templates associated with a module. * * @property {mw.Map} */ templates: new Map(), // Expose mw.log log: log // mw.loader is defined in a separate file that is appended to this }; // Attach to window and globally alias window.mw = window.mediaWiki = mw; }() ); /*! * Defines mw.loader, the infrastructure for loading ResourceLoader * modules. * * This file is appended directly to the code in startup/mediawiki.js */ /* global $VARS, $CODE, mw */ ( function () { 'use strict'; var store, hasOwn = Object.hasOwnProperty; /** * 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 */ /** * 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 a five-character base 36 string */ function fnv132( str ) { var hash = 0x811C9DC5; /* eslint-disable no-bitwise */ for ( var 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 ).slice( 0, 5 ); /* eslint-enable no-bitwise */ while ( hash.length < 5 ) { hash = '0' + hash; } return hash; } /** * 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: * * - 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-json: JSON conversion error in mw.loader.store * - store-localstorage-update: localStorage conversion error in mw.loader.store. */ /** * Mapping of registered modules. * * See #implement and #execute for exact details on support for script, style and messages. * * @example Format: * * { * 'moduleName': { * // From mw.loader.register() * 'version': '#####' (five-character hash) * 'dependencies': ['required.foo', 'bar.also', ...] * 'group': string, integer, (or) null * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example;', (or) null, (or) boolean result of skip * 'module': export Object * * // Set by execute() or mw.loader.state() * // See mw.loader.getState() for documentation of the state machine * 'state': 'registered', 'loading', 'loaded', 'executing', 'ready', 'error', or 'missing' * * // Optionally added at run-time by mw.loader.impl() * 'script': closure, array of urls, or string * 'style': { ... } (see #execute) * 'messages': { 'key': 'value', ... } * } * } * * @property {Object} * @private */ var registry = Object.create( null ), // Mapping of sources, keyed by source-id, values are strings. // // Format: // // { // 'sourceId': 'http://example.org/w/load.php' // } // sources = Object.create( null ), // 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 #doPropagation(). * Typically when a job is created for a module, the job's dependencies contain * both the required module and all its recursive dependencies. * * @example Format: * * { * 'dependencies': [ module names ], * 'ready': Function callback * 'error': Function callback * } * * @property {Object[]} jobs * @private */ jobs = [], // For #setAndPropagate() and #doPropagation() willPropagate = false, errorModules = [], /** * @private * @property {Array} baseModules */ baseModules = [ "jquery", "mediawiki.base" ], /** * For #addEmbeddedCSS() and #addLink() * * @private * @property {HTMLElement|null} marker */ marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ), // For #addEmbeddedCSS() lastCssBuffer; /** * Append an HTML element to `document.head` or before a specified node. * * @private * @param {HTMLElement} el * @param {Node|null} [nextNode] */ function addToHead( el, nextNode ) { if ( nextNode && nextNode.parentNode ) { nextNode.parentNode.insertBefore( el, nextNode ); } else { document.head.appendChild( el ); } } /** * Create a new style element and add it to the DOM. * * @private * @param {string} text CSS text * @param {Node|null} [nextNode] The element where the style tag * should be inserted before * @return {HTMLStyleElement} Reference to the created style element */ function newStyleTag( text, nextNode ) { var el = document.createElement( 'style' ); el.appendChild( document.createTextNode( text ) ); addToHead( el, nextNode ); return el; } /** * @private * @param {Object} cssBuffer */ function flushCssBuffer( cssBuffer ) { // Make sure the next call to addEmbeddedCSS() starts a new buffer. // This must be done before we run the callbacks, as those may end up // queueing new chunks which would be lost otherwise (T105973). // // There can be more than one buffer in-flight (given "@import", and // generally due to race conditions). Only tell addEmbeddedCSS() to // start a new buffer if we're currently flushing the last one that it // started. If we're flushing an older buffer, keep the last one open. if ( cssBuffer === lastCssBuffer ) { lastCssBuffer = null; } newStyleTag( cssBuffer.cssText, marker ); for ( var i = 0; i < cssBuffer.callbacks.length; i++ ) { cssBuffer.callbacks[ i ](); } } /** * Add a bit of CSS text to the current browser page. * * The creation and insertion of the `