Merge "Update weblinks in comments from HTTP to HTTPS"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.js
index 3122d42..9c8fe70 100644 (file)
        }() );
 
        /**
-        * 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 );
                }
        };
 
                 * @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 ) {
-                       msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+               } : function ( obj, key, val, msg, logName ) {
+                       logName = logName || key;
+                       msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
                        var logged = new StringSet();
                        function uniqueTrace() {
                                var trace = new Error().stack;
                                logged.add( trace );
                                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 );
+                       // 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;
                                        }
-                                       val = newVal;
-                               }
-                       } );
-
+                               } );
+                       } catch ( err ) {
+                               obj[ key ] = val;
+                       }
                };
 
                return log;
 
                                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();
 
                                }
                        }
 
+                       /**
+                        * @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 asyncEval( implementations, cb ) {
+                               if ( !implementations.length ) {
+                                       return;
+                               }
+                               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 {
                                /**
                                 * @protected
                                 */
                                work: function () {
-                                       var q, batch, concatSource, origBatch;
+                                       var q, batch, implementations, sourceModules;
 
                                        batch = [];
 
 
                                        mw.loader.store.init();
                                        if ( mw.loader.store.enabled ) {
-                                               concatSource = [];
-                                               origBatch = batch;
+                                               implementations = [];
+                                               sourceModules = [];
                                                batch = $.grep( batch, function ( module ) {
-                                                       var source = mw.loader.store.get( module );
-                                                       if ( source ) {
-                                                               concatSource.push( source );
+                                                       var implementation = mw.loader.store.get( module );
+                                                       if ( implementation ) {
+                                                               implementations.push( implementation );
+                                                               sourceModules.push( module );
                                                                return false;
                                                        }
                                                        return true;
                                                } );
-                                               try {
-                                                       $.globalEval( concatSource.join( ';' ) );
-                                               } catch ( err ) {
+                                               asyncEval( implementations, function ( err ) {
                                                        // 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();
-                                                       // Re-add the ones still pending back to the batch and let the server
-                                                       // repopulate these modules to the cache.
-                                                       // This means that at most one module will be useless (the one that had
-                                                       // the error) instead of all of them.
+
                                                        mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
-                                                       origBatch = $.grep( origBatch, function ( module ) {
+                                                       // Re-add the failed ones that are still pending back to the batch
+                                                       var failed = $.grep( sourceModules, function ( module ) {
                                                                return registry[ module ].state === 'loading';
                                                        } );
-                                                       batch = batch.concat( origBatch );
-                                               }
+                                                       batchRequest( failed );
+                                               } );
                                        }
 
                                        batchRequest( batch );
                                 * 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 );
                                                }
                                        }
                                },
 
                                        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
 
                                                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 ) {