/** * 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 * * Capabilities required for modern run-time: * - ECMAScript 5 * - DOM Level 4 (including Selectors API) * - HTML5 (including Web Storage API) * * Browsers we support in our modern run-time (Grade A): * - Chrome 13+ * - IE 11+ * - Firefox 4+ * - Safari 5+ * - Opera 15+ * - Mobile Safari 6.0+ (iOS 6+) * - Android 4.1+ * * Browsers we support in our no-JavaScript, basic run-time (Grade C): * - Chrome 1+ * - IE 8+ * - Firefox 3+ * - Safari 3+ * - Opera 15+ * - Mobile Safari 5.0+ (iOS 4+) * - Android 2.0+ * - WebOS < 1.5 * - PlayStation * - Symbian-based browsers * - NetFront-based browser * - Opera Mini * - Nokia's Ovi Browser * - MeeGo's browser * - Google Glass * - UC Mini (speed mode on) * * Other browsers that pass the check are considered unknown (Grade X). * * @private * @param {string} ua User agent string * @return {boolean} User agent is compatible with MediaWiki JS */ function isCompatible( ua ) { return !!( // https://caniuse.com/#feat=es5 // https://caniuse.com/#feat=use-strict ( function () { 'use strict'; return !this && Function.prototype.bind; }() ) && // https://caniuse.com/#feat=queryselector 'querySelector' in document && // https://caniuse.com/#feat=namevalue-storage // https://developer.blackberry.com/html5/apis/v1_0/localstorage.html // https://blog.whatwg.org/this-week-in-html-5-episode-30 'localStorage' in window && // Force certain browsers into Basic mode, even if they pass the check. // // Some of the below are "remote browsers", where the webpage is actually // rendered remotely in a capable browser (cloud service) by the vendor, // with the client app receiving a graphical representation through a // format that is not HTML/CSS. These get a better user experience if // we turn JavaScript off, to avoid triggering JavaScript calls, which // either don't work or require a roundtrip to the server with added // latency. Note that remote browsers are sometimes referred to as // "proxy browsers", but that term is also conflated with browsers // that accelerate or compress web pages through a "proxy", where // client-side JS would generally be okay. // // Remember: // // - Add new entries on top, and document why and since when. // - Please extend the regex instead of adding new ones, for performance. // - Add a test case to startup.test.js. // // Forced into Basic mode: // // - MSIE 10: Bugs (since 2018, T187869). // Low traffic. Reduce support cost by no longer having to workaround // bugs in its JavaScript APIs. // // - UC Mini "Speed Mode": Improve UX, save data (since 2016, T147369). // Does not have an obvious user agent, other than ending with an // incomplete `Gecko/` token. // // - Google Web Light: Bugs, save data (since 2016, T152602). // Proxy breaks most JavaScript. // // - MeeGo: Bugs (since 2015, T97546). // // - Opera Mini: Improve UX, save data. (since 2013, T49572). // It is a remote browser. // // - Ovi Browser: Improve UX, save data (since 2013, T57600). // It is a remote browser. UA contains "S40OviBrowser". // // - Google Glass: Improve UX (since 2013, T58008). // Run modern browser engine, but limited UI is better served when // content is expand by default, requiring little interaction. // // - NetFront: Unsupported by jQuery (since 2013, commit c46fc74). // - PlayStation: Unsupported by jQuery (since 2013, commit c46fc74). // !ua.match( /MSIE 10|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight|PLAYSTATION|PlayStation/ ) ); } if ( !isCompatible( navigator.userAgent ) ) { // 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, if possible. * * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs * also in production mode). Gets console references in each invocation instead of caching the * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE). * * @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 ) { if ( con.log ) { 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 ) { 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. * * 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 = con.warn ? Function.prototype.bind.call( con.warn, con ) : function () {}; /** * @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 StringSet, store, hasOwn = Object.hasOwnProperty; function defineFallbacks() { // /** * @private * @class StringSet */ StringSet = window.Set || function () { var set = Object.create( null ); return { add: function ( value ) { set[ value ] = true; }, has: function ( value ) { return value in set; } }; }; } defineFallbacks(); // In test mode, this generates `mw.redefineFallbacksForTest = defineFallbacks;`. // Otherwise, it produces nothing. See also ResourceLoaderStartUpModule::getScript(). /** * 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 an 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 ); while ( hash.length < 5 ) { hash = '0' + hash; } /* eslint-enable no-bitwise */ return hash; } // Check whether the browser supports ES6. // We are feature detecting Promises and Arrow Functions with default params // (which are good indicators of overall support). An additional test for // regex behavior filters out Android 4.4.4 and Edge 18 or lower. // This check doesn't quite guarantee full ES6 support: Safari 11-13 don't // support non-BMP characters in identifiers, but support all other ES6 // features we care about. To guard against accidentally breaking these // Safari versions with code they can't parse, we have an eslint rule // prohibiting non-BMP characters from being used in identifiers. var isES6Supported = // Check for Promise support (filters out most non-ES6 browsers) typeof Promise === 'function' && // eslint-disable-next-line no-undef Promise.prototype.finally && // Check for RegExp.prototype.flags (filters out Android 4.4.4 and Edge <= 18) /./g.flags === 'g' && // Test for arrow functions and default arguments, a good proxy for a // wide range of ES6 support. Borrowed from Benjamin De Cock's snippet here: // https://gist.github.com/bendc/d7f3dbc83d0f65ca0433caf90378cd95 // This will exclude Safari and Mobile Safari prior to version 10. ( function () { try { // eslint-disable-next-line no-new, no-new-func new Function( '(a = 0) => a' ); return true; } catch ( e ) { return false; } }() ); /** * 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': '########' (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 from execute() or mw.loader.state() * 'state': 'registered', 'loading', 'loaded', 'executing', 'ready', 'error', or 'missing' * * // Optionally added at run-time by mw.loader.implement() * '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. * Once the module has no more dependencies in-flight, the module will be executed, * controlled via #setAndPropagate and #doPropagation. * - `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 {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, rAF = window.requestAnimationFrame || setTimeout; /** * 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 {HTMLElement} 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 `