Merge "Avoid multiple writes to changetags table in recentchanges_save hook"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.js
index 491564a..3122d42 100644 (file)
@@ -11,7 +11,7 @@
 ( function ( $ ) {
        'use strict';
 
-       var mw,
+       var mw, StringSet, log,
                hasOwn = Object.prototype.hasOwnProperty,
                slice = Array.prototype.slice,
                trackCallbacks = $.Callbacks( 'memory' ),
                return hash;
        }
 
+       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
         * interaction both with single and multiple properties at once.
                }
        };
 
+       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
+                */
+               log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
+                       obj[ key ] = val;
+               } : function ( obj, key, val, msg ) {
+                       msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+                       var logged = new StringSet();
+                       function uniqueTrace() {
+                               var trace = new Error().stack;
+                               if ( logged.has( trace ) ) {
+                                       return false;
+                               }
+                               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 );
+                                       }
+                                       val = newVal;
+                               }
+                       } );
+
+               };
+
+               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.
                                cssBuffer = '',
                                cssBufferTimer = null,
                                cssCallbacks = $.Callbacks(),
-                               isIE9 = document.documentMode === 9;
+                               isIE9 = document.documentMode === 9,
+                               rAF = window.requestAnimationFrame || setTimeout;
 
                        function getMarker() {
                                if ( !marker ) {
                                        if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
                                                // Linebreak for somewhat distinguishable sections
                                                cssBuffer += '\n' + cssText;
-                                               // TODO: Using requestAnimationFrame would perform better by not injecting
-                                               // styles while the browser is busy painting.
                                                if ( !cssBufferTimer ) {
-                                                       cssBufferTimer = setTimeout( function () {
+                                                       cssBufferTimer = rAF( function () {
+                                                               // Wrap in anonymous function that takes no arguments
                                                                // Support: Firefox < 13
                                                                // Firefox 12 has non-standard behaviour of passing a number
                                                                // as first argument to a setTimeout callback.
                                                j -= 1;
                                                try {
                                                        if ( hasErrors ) {
-                                                               if ( $.isFunction( job.error ) ) {
+                                                               if ( typeof job.error === 'function' ) {
                                                                        job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
                                                                }
                                                        } else {
-                                                               if ( $.isFunction( job.ready ) ) {
+                                                               if ( typeof job.ready === 'function' ) {
                                                                        job.ready();
                                                                }
                                                        }
                         *  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 ) {
                                }
 
                                // Resolves dynamic loader function and replaces it with its own results
-                               if ( $.isFunction( registry[ module ].dependencies ) ) {
+                               if ( typeof registry[ module ].dependencies === 'function' ) {
                                        registry[ module ].dependencies = registry[ module ].dependencies();
                                        // Ensures the module's dependencies are always in an array
                                        if ( typeof registry[ module ].dependencies !== 'object' ) {
                                }
                                // 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 );
                                        }
                                }
                                        // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
                                        // XHR for a same domain request instead of <script>, which changes the request
                                        // headers (potentially missing a cache hit), and reduces caching in general
-                                       // since browsers cache XHR much less (if at all). And XHR means we retreive
+                                       // since browsers cache XHR much less (if at all). And XHR means we retrieve
                                        // text, so we'd need to $.globalEval, which then messes up line numbers.
                                        crossDomain: true,
                                        cache: true
                                registry[ module ].state = 'executing';
 
                                runScript = function () {
-                                       var script, markModuleReady, nestedAddScript, legacyWait,
+                                       var script, markModuleReady, nestedAddScript, legacyWait, implicitDependencies,
                                                // Expand to include dependencies since we have to exclude both legacy modules
                                                // and their dependencies from the legacyWait (to prevent a circular dependency).
                                                legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
-                                       try {
-                                               script = registry[ module ].script;
-                                               markModuleReady = function () {
-                                                       registry[ module ].state = 'ready';
-                                                       handlePending( module );
-                                               };
-                                               nestedAddScript = function ( arr, callback, i ) {
-                                                       // Recursively call queueModuleScript() in its own callback
-                                                       // for each element of arr.
-                                                       if ( i >= arr.length ) {
-                                                               // We're at the end of the array
-                                                               callback();
-                                                               return;
-                                                       }
 
-                                                       queueModuleScript( arr[ i ], module ).always( function () {
-                                                               nestedAddScript( arr, callback, i + 1 );
-                                                       } );
-                                               };
+                                       script = registry[ module ].script;
+                                       markModuleReady = function () {
+                                               registry[ module ].state = 'ready';
+                                               handlePending( module );
+                                       };
+                                       nestedAddScript = function ( arr, callback, i ) {
+                                               // Recursively call queueModuleScript() in its own callback
+                                               // for each element of arr.
+                                               if ( i >= arr.length ) {
+                                                       // We're at the end of the array
+                                                       callback();
+                                                       return;
+                                               }
+
+                                               queueModuleScript( arr[ i ], module ).always( function () {
+                                                       nestedAddScript( arr, callback, i + 1 );
+                                               } );
+                                       };
 
-                                               legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
-                                                       ? $.Deferred().resolve()
-                                                       : mw.loader.using( legacyModules );
+                                       implicitDependencies = ( $.inArray( module, legacyModules ) !== -1 )
+                                               ? []
+                                               : legacyModules;
+
+                                       if ( module === 'user' ) {
+                                               // Implicit dependency on the site module. Not real dependency because
+                                               // it should run after 'site' regardless of whether it succeeds or fails.
+                                               implicitDependencies.push( 'site' );
+                                       }
 
-                                               legacyWait.always( function () {
+                                       legacyWait = implicitDependencies.length
+                                               ? mw.loader.using( implicitDependencies )
+                                               : $.Deferred().resolve();
+
+                                       legacyWait.always( function () {
+                                               try {
                                                        if ( $.isArray( script ) ) {
                                                                nestedAddScript( script, markModuleReady, 0 );
-                                                       } else if ( $.isFunction( script ) ) {
+                                                       } else if ( typeof script === 'function' ) {
                                                                // Pass jQuery twice so that the signature of the closure which wraps
                                                                // the script can bind both '$' and 'jQuery'.
                                                                script( $, $, mw.loader.require, registry[ module ].module );
                                                                // Site and user modules are legacy scripts that run in the global scope.
                                                                // This is transported as a string instead of a function to avoid needing
                                                                // to use string manipulation to undo the function wrapper.
-                                                               if ( module === 'user' ) {
-                                                                       // Implicit dependency on the site module. Not real dependency because
-                                                                       // it should run after 'site' regardless of whether it succeeds or fails.
-                                                                       mw.loader.using( 'site' ).always( function () {
-                                                                               $.globalEval( script );
-                                                                               markModuleReady();
-                                                                       } );
-                                                               } else {
-                                                                       $.globalEval( script );
-                                                                       markModuleReady();
-                                                               }
+                                                               $.globalEval( script );
+                                                               markModuleReady();
+
                                                        } else {
                                                                // Module without script
                                                                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
-                                               registry[ module ].state = 'error';
-                                               mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
-                                               handlePending( module );
-                                       }
+                                               } catch ( e ) {
+                                                       // 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 );
+                                               }
+                                       } );
                                };
 
                                // Add localizations to message system
                         * @param {Array} modules Modules array
                         */
                        function resolveIndexedDependencies( modules ) {
-                               $.each( modules, function ( idx, module ) {
-                                       if ( module[ 2 ] ) {
-                                               module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
-                                                       return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
-                                               } );
+                               var i, j, deps;
+                               function resolveIndex( dep ) {
+                                       return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
+                               }
+                               for ( i = 0; i < modules.length; i++ ) {
+                                       deps = modules[ i ][ 2 ];
+                                       if ( deps ) {
+                                               for ( j = 0; j < deps.length; j++ ) {
+                                                       deps[ j ] = resolveIndex( deps[ j ] );
+                                               }
                                        }
-                               } );
+                               }
                        }
 
                        /**
                                                }
                                        }
 
+                                       // Now that the queue has been processed into a batch, clear the queue.
+                                       // This MUST happen before we initiate any eval or network request. Otherwise,
+                                       // it is possible for a cached script to instantly trigger the same work queue
+                                       // again; all before we've cleared it causing each request to include modules
+                                       // which are already loaded.
+                                       queue = [];
+
+                                       if ( !batch.length ) {
+                                               return;
+                                       }
+
                                        mw.loader.store.init();
                                        if ( mw.loader.store.enabled ) {
                                                concatSource = [];
                                                }
                                        }
 
-                                       // Now that the queue has been processed into a batch, clear up the queue.
-                                       // This MUST happen before we initiate any network request. Else it's possible
-                                       // that a script will be locally cached, instantly load, and work the queue
-                                       // again; all before we've cleared it causing each request to include modules
-                                       // which are already loaded.
-                                       queue = [];
-
                                        batchRequest( batch );
                                },
 
                                 * @param {string} [skip=null] Script body of the skip function
                                 */
                                register: function ( module, version, dependencies, group, source, skip ) {
-                                       var i;
+                                       var i, deps;
                                        // Allow multiple registration
                                        if ( typeof module === 'object' ) {
                                                resolveIndexedDependencies( module );
                                        if ( hasOwn.call( registry, module ) ) {
                                                throw new Error( 'module already registered: ' + module );
                                        }
+                                       if ( typeof dependencies === 'string' ) {
+                                               // A single module name
+                                               deps = [ dependencies ];
+                                       } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
+                                               // Array of module names or a function that returns an array
+                                               deps = dependencies;
+                                       }
                                        // List the module as registered
                                        registry[ module ] = {
                                                // Exposed to execute() for mw.loader.implement() closures.
                                                        exports: {}
                                                },
                                                version: version !== undefined ? String( version ) : '',
-                                               dependencies: [],
+                                               dependencies: deps || [],
                                                group: typeof group === 'string' ? group : null,
                                                source: typeof source === 'string' ? source : 'local',
                                                state: 'registered',
                                                skip: typeof skip === 'string' ? skip : null
                                        };
-                                       if ( typeof dependencies === 'string' ) {
-                                               // Allow dependencies to be given as a single module name
-                                               registry[ module ].dependencies = [ dependencies ];
-                                       } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
-                                               // Allow dependencies to be given as an array of module names
-                                               // or a function which returns an array
-                                               registry[ module ].dependencies = dependencies;
-                                       }
                                },
 
                                /**
                                 * response contain calls to this function.
                                 *
                                 * @param {string} module Name of module
-                                * @param {Function|Array} [script] Function with module code or Array of URLs to
-                                *  be used as the src attribute of a new `<script>` tag.
+                                * @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:
                                 *
                                 *     { "css": [css, ..] }
                                        if ( !hasOwn.call( registry, module ) ) {
                                                mw.loader.register( module );
                                        }
-                                       if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
-                                               && registry[ module ].state !== state ) {
+                                       registry[ module ].state = state;
+                                       if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1 ) {
                                                // Make sure pending modules depending on this one get executed if their
                                                // dependencies are now fulfilled!
-                                               registry[ module ].state = state;
                                                handlePending( module );
-                                       } else {
-                                               registry[ module ].state = state;
                                        }
                                },
 
                                                        // Unversioned, private, or site-/user-specific
                                                        ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
                                                        // Partial descriptor
+                                                       // (e.g. skipped module, or style module with state=ready)
                                                        $.inArray( undefined, [ descriptor.script, descriptor.style,
                                                                        descriptor.messages, descriptor.templates ] ) !== -1
                                                ) {
         * 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.
                var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
                        return mw.loader.getState( module ) === 'loading';
                } );
-               // In order to use jQuery.when (which stops early if one of the promises got rejected)
-               // cast any loading failures into successes. We only need a callback, not the module.
-               loading = $.map( loading, function ( module ) {
-                       return mw.loader.using( module ).then( null, function () {
-                               return $.Deferred().resolve();
+               // We only need a callback, not any actual module. First try a single using()
+               // for all loading modules. If one fails, fall back to tracking each module
+               // separately via $.when(), this is expensive.
+               loading = mw.loader.using( loading ).then( null, function () {
+                       var all = $.map( loading, function ( module ) {
+                               return mw.loader.using( module ).then( null, function () {
+                                       return $.Deferred().resolve();
+                               } );
                        } );
+                       return $.when.apply( $, all );
                } );
-               $.when.apply( $, loading ).then( function () {
+               loading.then( function () {
                        mwPerformance.mark( 'mwLoadEnd' );
                        mw.hook( 'resourceloader.loadEnd' ).fire();
                } );