Added missing name setting when initializing modules from preloaded info.
[lhc/web/wiklou.git] / includes / resourceloader / ResourceLoader.php
index 37ef160..e5f7a77 100644 (file)
  * @author Trevor Parscal
  */
 
-defined( 'MEDIAWIKI' ) || die( 1 );
-
 /**
  * Dynamic JavaScript and CSS resource loading system.
  *
- * Most of the documention is on the MediaWiki documentation wiki starting at
+ * Most of the documention is on the MediaWiki documentation wiki starting at:
  *    http://www.mediawiki.org/wiki/ResourceLoader
  */
 class ResourceLoader {
 
        /* Protected Static Members */
 
-       // @var array list of module name/ResourceLoaderModule object pairs
+       /** Array: List of module name/ResourceLoaderModule object pairs */
        protected $modules = array();
+       /** Associative array mapping module name to info associative array */
+       protected $moduleInfos = array();
 
        /* Protected Methods */
-       
+
        /**
-        * Loads information stored in the database about modules
+        * Loads information stored in the database about modules.
         * 
-        * This is not inside the module code because it's so much more performant to request all of the information at once
-        * than it is to have each module requests it's own information.
-        *
-        * This method grab modules dependencies from the database and initialize modules object.
-        * A first pass compute dependencies, a second one the blob mtime.
-        *
-        * @param $modules Array List of module names to preload information for
-        * @param $context ResourceLoaderContext Context to load the information within
+        * This method grabs modules dependencies from the database and updates modules 
+        * objects.
+        * 
+        * This is not inside the module code because it is much faster to 
+        * request all of the information at once than it is to have each module 
+        * requests its own information. This sacrifice of modularity yields a substantial
+        * performance improvement.
+        * 
+        * @param $modules Array: List of module names to preload information for
+        * @param $context ResourceLoaderContext: Context to load the information within
         */
        protected function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
                if ( !count( $modules ) ) {
-                       return; # or Database*::select() will explode
+                       return; // or else Database*::select() will explode, plus it's cheaper!
                }
-               $dbr = wfGetDb( DB_SLAVE );
+               $dbr = wfGetDB( DB_SLAVE );
                $skin = $context->getSkin();
                $lang = $context->getLanguage();
                
@@ -64,24 +66,25 @@ class ResourceLoader {
                        ), __METHOD__
                );
 
-               // Set modules dependecies              
+               // Set modules' dependecies             
                $modulesWithDeps = array();
                foreach ( $res as $row ) {
-                       $this->modules[$row->md_module]->setFileDependencies( $skin,
+                       $this->getModule( $row->md_module )->setFileDependencies( $skin,
                                FormatJson::decode( $row->md_deps, true )
                        );
                        $modulesWithDeps[] = $row->md_module;
                }
+
                // Register the absence of a dependency row too
                foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
-                       $this->modules[$name]->setFileDependencies( $skin, array() );
+                       $this->getModule( $name )->setFileDependencies( $skin, array() );
                }
                
                // Get message blob mtimes. Only do this for modules with messages
                $modulesWithMessages = array();
                $modulesWithoutMessages = array();
                foreach ( $modules as $name ) {
-                       if ( count( $this->modules[$name]->getMessages() ) ) {
+                       if ( count( $this->getModule( $name )->getMessages() ) ) {
                                $modulesWithMessages[] = $name;
                        } else {
                                $modulesWithoutMessages[] = $name;
@@ -94,53 +97,54 @@ class ResourceLoader {
                                ), __METHOD__
                        );
                        foreach ( $res as $row ) {
-                               $this->modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp );
+                               $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, $row->mr_timestamp );
                        }
                }
                foreach ( $modulesWithoutMessages as $name ) {
-                       $this->modules[$name]->setMsgBlobMtime( $lang, 0 );
+                       $this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
                }
        }
 
        /**
-        * Runs text (js,CSS) through a filter, caching the filtered result for future calls.
+        * Runs JavaScript or CSS data through a filter, caching the filtered result for future calls.
         * 
-        * Availables filters are:
+        * Available filters are:
         *  - minify-js \see JSMin::minify
         *  - minify-css \see CSSMin::minify
         *  - flip-css \see CSSJanus::transform
-        * When the filter names does not exist, text is returned as is.
-        *
-        * @param $filter String: name of filter to run
-        * @param $data String: text to filter, such as JavaScript or CSS text
-        * @param $file String: path to file being filtered, (optional: only required for CSS to resolve paths)
-        * @return String: filtered data
+        * 
+        * If $data is empty, only contains whitespace or the filter was unknown, 
+        * $data is returned unmodified.
+        * 
+        * @param $filter String: Name of filter to run
+        * @param $data String: Text to filter, such as JavaScript or CSS text
+        * @return String: Filtered data
         */
        protected function filter( $filter, $data ) {
-               global $wgMemc;
                wfProfileIn( __METHOD__ );
 
-               // For empty or whitespace-only things, don't do any processing
-               # FIXME: we should return the data unfiltered if $filter is not supported.
-               # that would save up a md5 computation and one memcached get.
-               if ( trim( $data ) === '' ) {
+               // For empty/whitespace-only data or for unknown filters, don't perform 
+               // any caching or processing
+               if ( trim( $data ) === '' 
+                       || !in_array( $filter, array( 'minify-js', 'minify-css', 'flip-css' ) ) ) 
+               {
                        wfProfileOut( __METHOD__ );
                        return $data;
                }
 
-               // Try memcached
+               // Try for cache hit
+               // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
                $key = wfMemcKey( 'resourceloader', 'filter', $filter, md5( $data ) );
-               $cached = $wgMemc->get( $key );
-
-               if ( $cached !== false && $cached !== null ) {
+               $cache = wfGetCache( CACHE_ANYTHING );
+               $cacheEntry = $cache->get( $key );
+               if ( is_string( $cacheEntry ) ) {
                        wfProfileOut( __METHOD__ );
-                       return $cached;
+                       return $cacheEntry;
                }
 
-               // Run the filter
+               // Run the filter - we've already verified one of these will work
                try {
                        switch ( $filter ) {
-                               # note: if adding a new filter. Please update method documentation above. 
                                case 'minify-js':
                                        $result = JSMin::minify( $data );
                                        break;
@@ -150,29 +154,27 @@ class ResourceLoader {
                                case 'flip-css':
                                        $result = CSSJanus::transform( $data, true, false );
                                        break;
-                               default:
-                                       // Don't cache anything, just pass right through
-                                       wfProfileOut( __METHOD__ );
-                                       return $data;
                        }
                } catch ( Exception $exception ) {
-                       throw new MWException( 'Filter threw an exception: ' . $exception->getMessage() );
+                       throw new MWException( 'ResourceLoader filter error. ' . 
+                               'Exception was thrown: ' . $exception->getMessage() );
                }
 
-               // Save filtered text to memcached
-               $wgMemc->set( $key, $result );
+               // Save filtered text to Memcached
+               $cache->set( $key, $result );
 
                wfProfileOut( __METHOD__ );
+               
                return $result;
        }
 
        /* Methods */
 
        /**
-        * Registers core modules and runs registration hooks
+        * Registers core modules and runs registration hooks.
         */
        public function __construct() {
-               global $IP;
+               global $IP, $wgResourceModules;
                
                wfProfileIn( __METHOD__ );
                
@@ -180,100 +182,127 @@ class ResourceLoader {
                $this->register( include( "$IP/resources/Resources.php" ) );
                // Register extension modules
                wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
+               $this->register( $wgResourceModules );
                
                wfProfileOut( __METHOD__ );
        }
 
        /**
         * Registers a module with the ResourceLoader system.
-        *
-        * Note that registering the same object under multiple names is not supported 
-        * and may silently fail in all kinds of interesting ways.
-        *
-        * @param $name Mixed: string of name of module or array of name/object pairs
-        * @param $object ResourceLoaderModule: module object (optional when using 
-        *    multiple-registration calling style)
-        * @return Boolean: false if there were any errors, in which case one or more 
-        *    modules were not registered
-        *
-        * @todo We need much more clever error reporting, not just in detailing what 
-        *    happened, but in bringing errors to the client in a way that they can 
-        *    easily see them if they want to, such as by using FireBug
+        * 
+        * @param $name Mixed: Name of module as a string or List of name/object pairs as an array
+        * @param $info Module info array. For backwards compatibility with 1.17alpha, 
+        *   this may also be a ResourceLoaderModule object. Optional when using 
+        *   multiple-registration calling style.
+        * @throws MWException: If a duplicate module registration is attempted
+        * @throws MWException: If something other than a ResourceLoaderModule is being registered
+        * @return Boolean: False if there were any errors, in which case one or more modules were not
+        *     registered
         */
-       public function register( $name, ResourceLoaderModule $object = null ) {
+       public function register( $name, $info = null ) {
                wfProfileIn( __METHOD__ );
-               
+
                // Allow multiple modules to be registered in one call
-               if ( is_array( $name ) && !isset( $object ) ) {
+               if ( is_array( $name ) ) {
                        foreach ( $name as $key => $value ) {
                                $this->register( $key, $value );
                        }
-
-                       wfProfileOut( __METHOD__ );
                        return;
                }
 
                // Disallow duplicate registrations
-               if ( isset( $this->modules[$name] ) ) {
+               if ( isset( $this->moduleInfos[$name] ) ) {
                        // A module has already been registered by this name
-                       throw new MWException( 'Another module has already been registered as ' . $name );
-               }
-               
-               // Validate the input (type hinting lets null through)
-               if ( !( $object instanceof ResourceLoaderModule ) ) {
-                       throw new MWException( 'Invalid ResourceLoader module error. Instances of ResourceLoaderModule expected.' );
+                       throw new MWException(
+                               'ResourceLoader duplicate registration error. ' . 
+                               'Another module has already been registered as ' . $name
+                       );
                }
-               
+
                // Attach module
-               $this->modules[$name] = $object;
-               $object->setName( $name );
-               
+               if ( is_object( $info ) ) {
+                       // Old calling convention
+                       // Validate the input
+                       if ( !( $info instanceof ResourceLoaderModule ) ) {
+                               throw new MWException( 'ResourceLoader invalid module error. ' . 
+                                       'Instances of ResourceLoaderModule expected.' );
+                       }
+
+                       $this->moduleInfos[$name] = array( 'object' => $info );
+                       $info->setName( $name );
+                       $this->modules[$name] = $info;
+               } else {
+                       // New calling convention
+                       $this->moduleInfos[$name] = $info;
+               }
+
                wfProfileOut( __METHOD__ );
        }
 
-       /**
-        * Gets a map of all modules and their options
+       /**
+        * Get a list of module names
         *
-        * @return Array: array( modulename => ResourceLoaderModule )
+        * @return Array: List of module names
         */
-       public function getModules() {
-               return $this->modules;
+       public function getModuleNames() {
+               return array_keys( $this->moduleInfos );
        }
 
        /**
-        * Get the ResourceLoaderModule object for a given module name
+        * Get the ResourceLoaderModule object for a given module name.
         *
-        * @param $name String: module name
-        * @return mixed ResourceLoaderModule or null if not registered
+        * @param $name String: Module name
+        * @return Mixed: ResourceLoaderModule if module has been registered, null otherwise
         */
        public function getModule( $name ) {
-               return isset( $this->modules[$name] ) ? $this->modules[$name] : null;
+               if ( !isset( $this->modules[$name] ) ) {
+                       if ( !isset( $this->moduleInfos[$name] ) ) {
+                               // No such module
+                               return null;
+                       }
+                       // Construct the requested object
+                       $info = $this->moduleInfos[$name];
+                       if ( isset( $info['object'] ) ) {
+                               // Object given in info array
+                               $object = $info['object'];
+                       } else {
+                               if ( !isset( $info['class'] ) ) {
+                                       $class = 'ResourceLoaderFileModule';
+                               } else {
+                                       $class = $info['class'];
+                               }
+                               $object = new $class( $info );
+                       }
+                       $object->setName( $name );
+                       $this->modules[$name] = $object;
+               }
+
+               return $this->modules[$name];
        }
 
        /**
-        * Outputs a response to a resource load-request, including a content-type header
+        * Outputs a response to a resource load-request, including a content-type header.
         *
-        * @param $context ResourceLoaderContext object
+        * @param $context ResourceLoaderContext: Context in which a response should be formed
         */
        public function respond( ResourceLoaderContext $context ) {
                global $wgResourceLoaderMaxage, $wgCacheEpoch;
 
                wfProfileIn( __METHOD__ );
-               
+
                // Split requested modules into two groups, modules and missing
                $modules = array();
                $missing = array();
-               
                foreach ( $context->getModules() as $name ) {
-                       if ( isset( $this->modules[$name] ) ) {
-                               $modules[$name] = $this->modules[$name];
+                       if ( isset( $this->moduleInfos[$name] ) ) {
+                               $modules[$name] = $this->getModule( $name );
                        } else {
                                $missing[] = $name;
                        }
                }
 
-               // If a version wasn't specified we need a shorter expiry time for updates to 
-               // propagate to clients quickly
+               // If a version wasn't specified we need a shorter expiry time for updates 
+               // to propagate to clients quickly
                if ( is_null( $context->getVersion() ) ) {
                        $maxage  = $wgResourceLoaderMaxage['unversioned']['client'];
                        $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
@@ -288,9 +317,10 @@ class ResourceLoader {
                // Preload information needed to the mtime calculation below
                $this->preloadModuleInfo( array_keys( $modules ), $context );
 
+               wfProfileIn( __METHOD__.'-getModifiedTime' );
+
                // To send Last-Modified and support If-Modified-Since, we need to detect 
                // the last modified time
-               wfProfileIn( __METHOD__.'-getModifiedTime' );
                $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
                foreach ( $modules as $module ) {
                        // Bypass squid cache if the request includes any private modules
@@ -300,12 +330,21 @@ class ResourceLoader {
                        // Calculate maximum modified time
                        $mtime = max( $mtime, $module->getModifiedTime( $context ) );
                }
+
                wfProfileOut( __METHOD__.'-getModifiedTime' );
 
-               header( 'Content-Type: ' . ( $context->getOnly() === 'styles' ? 'text/css' : 'text/javascript' ) );
+               if ( $context->getOnly() === 'styles' ) {
+                       header( 'Content-Type: text/css' );
+               } else {
+                       header( 'Content-Type: text/javascript' );
+               }
                header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
-               header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
-               header( 'Expires: ' . wfTimestamp( TS_RFC2822, min( $maxage, $smaxage ) + time() ) );
+               if ( $context->getDebug() ) {
+                       header( 'Cache-Control: must-revalidate' );
+               } else {
+                       header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
+                       header( 'Expires: ' . wfTimestamp( TS_RFC2822, min( $maxage, $smaxage ) + time() ) );
+               }
 
                // If there's an If-Modified-Since header, respond with a 304 appropriately
                $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
@@ -315,11 +354,15 @@ class ResourceLoader {
                        wfProfileOut( __METHOD__ );
                        return;
                }
-
+               
+               // Generate a response
                $response = $this->makeModuleResponse( $context, $modules, $missing );
+
+               // Tack on PHP warnings as a comment in debug mode
                if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
                        $response .= "/*\n$warnings\n*/";
                }
+
                // Clear any warnings from the buffer
                ob_clean();
                echo $response;
@@ -328,19 +371,27 @@ class ResourceLoader {
        }
 
        /**
-        *
-        * @param $context ResourceLoaderContext
-        * @param $modules Array array( modulename => ResourceLoaderModule )
-        * @param $missing Unavailables modules (Default null)
+        * Generates code for a response
+        * 
+        * @param $context ResourceLoaderContext: Context in which to generate a response
+        * @param $modules Array: List of module objects keyed by module name
+        * @param $missing Array: List of unavailable modules (optional)
+        * @return String: Response data
         */
-       public function makeModuleResponse( ResourceLoaderContext $context, array $modules, $missing = null ) {
+       public function makeModuleResponse( ResourceLoaderContext $context, 
+               array $modules, $missing = array() ) 
+       {
                // Pre-fetch blobs
-               $blobs = $context->shouldIncludeMessages() ?
-                       MessageBlobStore::get( $this, $modules, $context->getLanguage() ) : array();
+               if ( $context->shouldIncludeMessages() ) {
+                       $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
+               } else {
+                       $blobs = array();
+               }
 
                // Generate output
                $out = '';
                foreach ( $modules as $name => $module ) {
+
                        wfProfileIn( __METHOD__ . '-' . $name );
 
                        // Scripts
@@ -351,9 +402,10 @@ class ResourceLoader {
 
                        // Styles
                        $styles = array();
-                       if ( $context->shouldIncludeStyles() && ( count( $styles = $module->getStyles( $context ) ) ) ) {
+                       if ( $context->shouldIncludeStyles() ) {
+                               $styles = $module->getStyles( $context );
                                // Flip CSS on a per-module basis
-                               if ( $this->modules[$name]->getFlip( $context ) ) {
+                               if ( $styles && $module->getFlip( $context ) ) {
                                        foreach ( $styles as $media => $style ) {
                                                $styles[$media] = $this->filter( 'flip-css', $style );
                                        }
@@ -361,7 +413,7 @@ class ResourceLoader {
                        }
 
                        // Messages
-                       $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
+                       $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : array();
 
                        // Append output
                        switch ( $context->getOnly() ) {
@@ -372,27 +424,32 @@ class ResourceLoader {
                                        $out .= self::makeCombinedStyles( $styles );
                                        break;
                                case 'messages':
-                                       $out .= self::makeMessageSetScript( $messages );
+                                       $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
                                        break;
                                default:
-                                       // Minify CSS before embedding in mediaWiki.loader.implement call (unless in debug mode)
+                                       // Minify CSS before embedding in mediaWiki.loader.implement call 
+                                       // (unless in debug mode)
                                        if ( !$context->getDebug() ) {
                                                foreach ( $styles as $media => $style ) {
                                                        $styles[$media] = $this->filter( 'minify-css', $style );
                                                }
                                        }
-                                       $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages );
+                                       $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, 
+                                               new XmlJsCode( $messagesBlob ) );
                                        break;
                        }
-                       
+
                        wfProfileOut( __METHOD__ . '-' . $name );
                }
 
                // Update module states
                if ( $context->shouldIncludeScripts() ) {
                        // Set the state of modules loaded as only scripts to ready
-                       if ( count( $modules ) && $context->getOnly() === 'scripts' && !isset( $modules['startup'] ) ) {
-                               $out .= self::makeLoaderStateScript( array_fill_keys( array_keys( $modules ), 'ready' ) );
+                       if ( count( $modules ) && $context->getOnly() === 'scripts' 
+                               && !isset( $modules['startup'] ) ) 
+                       {
+                               $out .= self::makeLoaderStateScript( 
+                                       array_fill_keys( array_keys( $modules ), 'ready' ) );
                        }
                        // Set the state of modules which were requested but unavailable as missing
                        if ( is_array( $missing ) && count( $missing ) ) {
@@ -410,29 +467,51 @@ class ResourceLoader {
                        }
                }
        }
-       
+
        /* Static Methods */
-       
+
+       /**
+        * Returns JS code to call to mediaWiki.loader.implement for a module with 
+        * given properties.
+        *
+        * @param $name Module name
+        * @param $scripts Array: List of JavaScript code snippets to be executed after the 
+        *     module is loaded
+        * @param $styles Array: List of CSS strings keyed by media type
+        * @param $messages Mixed: List of messages associated with this module. May either be an 
+        *     associative array mapping message key to value, or a JSON-encoded message blob containing
+        *     the same data, wrapped in an XmlJsCode object.
+        */
        public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
                if ( is_array( $scripts ) ) {
                        $scripts = implode( $scripts, "\n" );
                }
-               if ( is_array( $styles ) ) {
-                       $styles = count( $styles ) ? FormatJson::encode( $styles ) : 'null';
-               }
-               if ( is_array( $messages ) ) {
-                       $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null';
-               }
-               return "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n";
+               return Xml::encodeJsCall( 
+                       'mediaWiki.loader.implement', 
+                       array(
+                               $name,
+                               new XmlJsCode( "function() {{$scripts}}" ),
+                               (object)$styles,
+                               (object)$messages
+                       ) );
        }
 
+       /**
+        * Returns JS code which, when called, will register a given list of messages.
+        *
+        * @param $messages Mixed: Either an associative array mapping message key to value, or a
+        *     JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object.
+        */
        public static function makeMessageSetScript( $messages ) {
-               if ( is_array( $messages ) ) {
-                       $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null';
-               }
-               return "mediaWiki.msg.set( $messages );\n";
+               return Xml::encodeJsCall( 'mediaWiki.messages.set', array( (object)$messages ) );
        }
 
+       /**
+        * Combines an associative array mapping media type to CSS into a 
+        * single stylesheet with @media blocks.
+        *
+        * @param $styles Array: List of CSS strings keyed by media type
+        */
        public static function makeCombinedStyles( array $styles ) {
                $out = '';
                foreach ( $styles as $media => $style ) {
@@ -441,47 +520,109 @@ class ResourceLoader {
                return $out;
        }
 
+       /**
+        * Returns a JS call to mediaWiki.loader.state, which sets the state of a 
+        * module or modules to a given value. Has two calling conventions:
+        *
+        *    - ResourceLoader::makeLoaderStateScript( $name, $state ):
+        *         Set the state of a single module called $name to $state
+        *
+        *    - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ):
+        *         Set the state of modules with the given names to the given states
+        */
        public static function makeLoaderStateScript( $name, $state = null ) {
                if ( is_array( $name ) ) {
-                       $statuses = FormatJson::encode( $name );
-                       return "mediaWiki.loader.state( $statuses );\n";
+                       return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name ) );
                } else {
-                       $name = Xml::escapeJsString( $name );
-                       $state = Xml::escapeJsString( $state );
-                       return "mediaWiki.loader.state( '$name', '$state' );\n";
+                       return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name, $state ) );
                }
        }
 
+       /**
+        * Returns JS code which calls the script given by $script. The script will
+        * be called with local variables name, version, dependencies and group, 
+        * which will have values corresponding to $name, $version, $dependencies 
+        * and $group as supplied. 
+        *
+        * @param $name String: Module name
+        * @param $version Integer: Module version number as a timestamp
+        * @param $dependencies Array: List of module names on which this module depends
+        * @param $group String: Group which the module is in.
+        * @param $script String: JavaScript code
+        */
        public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) {
-               $name = Xml::escapeJsString( $name );
-               $version = (int) $version > 1 ? (int) $version : 1;
-               $dependencies = FormatJson::encode( $dependencies );
-               $group = FormatJson::encode( $group );
                $script = str_replace( "\n", "\n\t", trim( $script ) );
-               return "( function( name, version, dependencies, group ) {\n\t$script\n} )" .
-                       "( '$name', $version, $dependencies, $group );\n";
+               return Xml::encodeJsCall( 
+                       "( function( name, version, dependencies, group ) {\n\t$script\n} )",
+                       array( $name, $version, $dependencies, $group ) );
        }
 
-       public static function makeLoaderRegisterScript( $name, $version = null, $dependencies = null, $group = null ) {
+       /**
+        * Returns JS code which calls mediaWiki.loader.register with the given 
+        * parameters. Has three calling conventions:
+        *
+        *   - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group ):
+        *       Register a single module.
+        *
+        *   - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ):
+        *       Register modules with the given names.
+        *
+        *   - ResourceLoader::makeLoaderRegisterScript( array(
+        *        array( $name1, $version1, $dependencies1, $group1 ),
+        *        array( $name2, $version2, $dependencies1, $group2 ),
+        *        ...
+        *     ) ):
+        *        Registers modules with the given names and parameters.
+        *
+        * @param $name String: Module name
+        * @param $version Integer: Module version number as a timestamp
+        * @param $dependencies Array: List of module names on which this module depends
+        * @param $group String: group which the module is in.
+        */
+       public static function makeLoaderRegisterScript( $name, $version = null, 
+               $dependencies = null, $group = null ) 
+       {
                if ( is_array( $name ) ) {
-                       $registrations = FormatJson::encode( $name );
-                       return "mediaWiki.loader.register( $registrations );\n";
+                       return Xml::encodeJsCall( 'mediaWiki.loader.register', array( $name ) );
                } else {
-                       $name = Xml::escapeJsString( $name );
                        $version = (int) $version > 1 ? (int) $version : 1;
-                       $dependencies = FormatJson::encode( $dependencies );
-                       $group = FormatJson::encode( $group );
-                       return "mediaWiki.loader.register( '$name', $version, $dependencies, $group );\n";
+                       return Xml::encodeJsCall( 'mediaWiki.loader.register', 
+                               array( $name, $version, $dependencies, $group ) );
                }
        }
 
+       /**
+        * Returns JS code which runs given JS code if the client-side framework is 
+        * present.
+        *
+        * @param $script String: JavaScript code
+        */
        public static function makeLoaderConditionalScript( $script ) {
                $script = str_replace( "\n", "\n\t", trim( $script ) );
                return "if ( window.mediaWiki ) {\n\t$script\n}\n";
        }
 
+       /**
+        * Returns JS code which will set the MediaWiki configuration array to 
+        * the given value.
+        *
+        * @param $configuration Array: List of configuration values keyed by variable name
+        */
        public static function makeConfigSetScript( array $configuration ) {
-               $configuration = FormatJson::encode( $configuration );
-               return "mediaWiki.config.set( $configuration );\n";
+               return Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) );
+       }
+       
+       /**
+        * Determine whether debug mode was requested
+        * Order of priority is 1) request param, 2) cookie, 3) $wg setting
+        * @return bool
+        */
+       public static function inDebugMode() {
+               global $wgRequest, $wgResourceLoaderDebug;
+               static $retval = null;
+               if ( !is_null( $retval ) )
+                       return $retval;
+               return $retval = $wgRequest->getFuzzyBool( 'debug',
+                       $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
        }
 }