]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/resourceloader/ResourceLoader.php
MediaWiki 1.17.1
[autoinstallsdev/mediawiki.git] / includes / resourceloader / ResourceLoader.php
1 <?php
2 /**
3  * This program is free software; you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation; either version 2 of the License, or
6  * (at your option) any later version.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16  * http://www.gnu.org/copyleft/gpl.html
17  *
18  * @file
19  * @author Roan Kattouw
20  * @author Trevor Parscal
21  */
22
23 /**
24  * Dynamic JavaScript and CSS resource loading system.
25  *
26  * Most of the documention is on the MediaWiki documentation wiki starting at:
27  *    http://www.mediawiki.org/wiki/ResourceLoader
28  */
29 class ResourceLoader {
30
31         /* Protected Static Members */
32         protected static $filterCacheVersion = 2;
33
34         /** Array: List of module name/ResourceLoaderModule object pairs */
35         protected $modules = array();
36         /** Associative array mapping module name to info associative array */
37         protected $moduleInfos = array();
38
39         /* Protected Methods */
40
41         /**
42          * Loads information stored in the database about modules.
43          * 
44          * This method grabs modules dependencies from the database and updates modules 
45          * objects.
46          * 
47          * This is not inside the module code because it is much faster to 
48          * request all of the information at once than it is to have each module 
49          * requests its own information. This sacrifice of modularity yields a substantial
50          * performance improvement.
51          * 
52          * @param $modules Array: List of module names to preload information for
53          * @param $context ResourceLoaderContext: Context to load the information within
54          */
55         public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
56                 if ( !count( $modules ) ) {
57                         return; // or else Database*::select() will explode, plus it's cheaper!
58                 }
59                 $dbr = wfGetDB( DB_SLAVE );
60                 $skin = $context->getSkin();
61                 $lang = $context->getLanguage();
62                 
63                 // Get file dependency information
64                 $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
65                                 'md_module' => $modules,
66                                 'md_skin' => $context->getSkin()
67                         ), __METHOD__
68                 );
69
70                 // Set modules' dependencies
71                 $modulesWithDeps = array();
72                 foreach ( $res as $row ) {
73                         $this->getModule( $row->md_module )->setFileDependencies( $skin,
74                                 FormatJson::decode( $row->md_deps, true )
75                         );
76                         $modulesWithDeps[] = $row->md_module;
77                 }
78
79                 // Register the absence of a dependency row too
80                 foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
81                         $this->getModule( $name )->setFileDependencies( $skin, array() );
82                 }
83                 
84                 // Get message blob mtimes. Only do this for modules with messages
85                 $modulesWithMessages = array();
86                 foreach ( $modules as $name ) {
87                         if ( count( $this->getModule( $name )->getMessages() ) ) {
88                                 $modulesWithMessages[] = $name;
89                         }
90                 }
91                 $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below
92                 if ( count( $modulesWithMessages ) ) {
93                         $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
94                                         'mr_resource' => $modulesWithMessages,
95                                         'mr_lang' => $lang
96                                 ), __METHOD__
97                         );
98                         foreach ( $res as $row ) {
99                                 $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, 
100                                         wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
101                                 unset( $modulesWithoutMessages[$row->mr_resource] );
102                         }
103                 } 
104                 foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
105                         $this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
106                 }
107         }
108
109         /**
110          * Runs JavaScript or CSS data through a filter, caching the filtered result for future calls.
111          * 
112          * Available filters are:
113          *  - minify-js \see JavaScriptMinifier::minify
114          *  - minify-css \see CSSMin::minify
115          * 
116          * If $data is empty, only contains whitespace or the filter was unknown, 
117          * $data is returned unmodified.
118          * 
119          * @param $filter String: Name of filter to run
120          * @param $data String: Text to filter, such as JavaScript or CSS text
121          * @return String: Filtered data, or a comment containing an error message
122          */
123         protected function filter( $filter, $data ) {
124                 global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength;
125                 wfProfileIn( __METHOD__ );
126
127                 // For empty/whitespace-only data or for unknown filters, don't perform 
128                 // any caching or processing
129                 if ( trim( $data ) === '' 
130                         || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) 
131                 {
132                         wfProfileOut( __METHOD__ );
133                         return $data;
134                 }
135
136                 // Try for cache hit
137                 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
138                 $key = wfMemcKey( 'resourceloader', 'filter', $filter, md5( $data ) );
139                 $cache = wfGetCache( CACHE_ANYTHING );
140                 $cacheEntry = $cache->get( $key );
141                 if ( is_string( $cacheEntry ) ) {
142                         wfProfileOut( __METHOD__ );
143                         return $cacheEntry;
144                 }
145
146                 // Run the filter - we've already verified one of these will work
147                 try {
148                         switch ( $filter ) {
149                                 case 'minify-js':
150                                         $result = JavaScriptMinifier::minify( $data,
151                                                 $wgResourceLoaderMinifierStatementsOnOwnLine,
152                                                 $wgResourceLoaderMinifierMaxLineLength
153                                         );
154                                         break;
155                                 case 'minify-css':
156                                         $result = CSSMin::minify( $data );
157                                         break;
158                         }
159
160                         // Save filtered text to Memcached
161                         $cache->set( $key, $result );
162                 } catch ( Exception $exception ) {
163                         // Return exception as a comment
164                         $result = "/*\n{$exception->__toString()}\n*/\n";
165                 }
166
167                 wfProfileOut( __METHOD__ );
168                 
169                 return $result;
170         }
171
172         /* Methods */
173
174         /**
175          * Registers core modules and runs registration hooks.
176          */
177         public function __construct() {
178                 global $IP, $wgResourceModules;
179                 
180                 wfProfileIn( __METHOD__ );
181                 
182                 // Register core modules
183                 $this->register( include( "$IP/resources/Resources.php" ) );
184                 // Register extension modules
185                 wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
186                 $this->register( $wgResourceModules );
187                 
188                 wfProfileOut( __METHOD__ );
189         }
190
191         /**
192          * Registers a module with the ResourceLoader system.
193          * 
194          * @param $name Mixed: Name of module as a string or List of name/object pairs as an array
195          * @param $info Module info array. For backwards compatibility with 1.17alpha, 
196          *   this may also be a ResourceLoaderModule object. Optional when using 
197          *   multiple-registration calling style.
198          * @throws MWException: If a duplicate module registration is attempted
199          * @throws MWException: If a module name contains illegal characters (pipes or commas)
200          * @throws MWException: If something other than a ResourceLoaderModule is being registered
201          * @return Boolean: False if there were any errors, in which case one or more modules were not
202          *     registered
203          */
204         public function register( $name, $info = null ) {
205                 wfProfileIn( __METHOD__ );
206
207                 // Allow multiple modules to be registered in one call
208                 if ( is_array( $name ) ) {
209                         foreach ( $name as $key => $value ) {
210                                 $this->register( $key, $value );
211                         }
212                         wfProfileOut( __METHOD__ );
213                         return;
214                 }
215
216                 // Disallow duplicate registrations
217                 if ( isset( $this->moduleInfos[$name] ) ) {
218                         // A module has already been registered by this name
219                         throw new MWException(
220                                 'ResourceLoader duplicate registration error. ' . 
221                                 'Another module has already been registered as ' . $name
222                         );
223                 }
224                 
225                 // Check $name for illegal characters
226                 if ( preg_match( '/[|,]/', $name ) ) {
227                         throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|) or commas (,)" );
228                 }
229
230                 // Attach module
231                 if ( is_object( $info ) ) {
232                         // Old calling convention
233                         // Validate the input
234                         if ( !( $info instanceof ResourceLoaderModule ) ) {
235                                 throw new MWException( 'ResourceLoader invalid module error. ' . 
236                                         'Instances of ResourceLoaderModule expected.' );
237                         }
238
239                         $this->moduleInfos[$name] = array( 'object' => $info );
240                         $info->setName( $name );
241                         $this->modules[$name] = $info;
242                 } else {
243                         // New calling convention
244                         $this->moduleInfos[$name] = $info;
245                 }
246
247                 wfProfileOut( __METHOD__ );
248         }
249
250         /**
251          * Get a list of module names
252          *
253          * @return Array: List of module names
254          */
255         public function getModuleNames() {
256                 return array_keys( $this->moduleInfos );
257         }
258
259         /**
260          * Get the ResourceLoaderModule object for a given module name.
261          *
262          * @param $name String: Module name
263          * @return Mixed: ResourceLoaderModule if module has been registered, null otherwise
264          */
265         public function getModule( $name ) {
266                 if ( !isset( $this->modules[$name] ) ) {
267                         if ( !isset( $this->moduleInfos[$name] ) ) {
268                                 // No such module
269                                 return null;
270                         }
271                         // Construct the requested object
272                         $info = $this->moduleInfos[$name];
273                         if ( isset( $info['object'] ) ) {
274                                 // Object given in info array
275                                 $object = $info['object'];
276                         } else {
277                                 if ( !isset( $info['class'] ) ) {
278                                         $class = 'ResourceLoaderFileModule';
279                                 } else {
280                                         $class = $info['class'];
281                                 }
282                                 $object = new $class( $info );
283                         }
284                         $object->setName( $name );
285                         $this->modules[$name] = $object;
286                 }
287
288                 return $this->modules[$name];
289         }
290
291         /**
292          * Outputs a response to a resource load-request, including a content-type header.
293          *
294          * @param $context ResourceLoaderContext: Context in which a response should be formed
295          */
296         public function respond( ResourceLoaderContext $context ) {
297                 global $wgResourceLoaderMaxage, $wgCacheEpoch;
298                 
299                 // Buffer output to catch warnings. Normally we'd use ob_clean() on the
300                 // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
301                 // is used: ob_clean() will clear the GZIP header in that case and it won't come
302                 // back for subsequent output, resulting in invalid GZIP. So we have to wrap
303                 // the whole thing in our own output buffer to be sure the active buffer
304                 // doesn't use ob_gzhandler.
305                 // See http://bugs.php.net/bug.php?id=36514
306                 ob_start();
307
308                 wfProfileIn( __METHOD__ );
309                 $exceptions = '';
310
311                 // Split requested modules into two groups, modules and missing
312                 $modules = array();
313                 $missing = array();
314                 foreach ( $context->getModules() as $name ) {
315                         if ( isset( $this->moduleInfos[$name] ) ) {
316                                 $modules[$name] = $this->getModule( $name );
317                         } else {
318                                 $missing[] = $name;
319                         }
320                 }
321
322                 // If a version wasn't specified we need a shorter expiry time for updates 
323                 // to propagate to clients quickly
324                 if ( is_null( $context->getVersion() ) ) {
325                         $maxage  = $wgResourceLoaderMaxage['unversioned']['client'];
326                         $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
327                 }
328                 // If a version was specified we can use a longer expiry time since changing 
329                 // version numbers causes cache misses
330                 else {
331                         $maxage  = $wgResourceLoaderMaxage['versioned']['client'];
332                         $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
333                 }
334
335                 // Preload information needed to the mtime calculation below
336                 try {
337                         $this->preloadModuleInfo( array_keys( $modules ), $context );
338                 } catch( Exception $e ) {
339                         // Add exception to the output as a comment
340                         $exceptions .= "/*\n{$e->__toString()}\n*/\n";
341                 }
342
343                 wfProfileIn( __METHOD__.'-getModifiedTime' );
344
345                 $private = false;
346                 // To send Last-Modified and support If-Modified-Since, we need to detect 
347                 // the last modified time
348                 $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
349                 foreach ( $modules as $module ) {
350                         try {
351                                 // Bypass Squid and other shared caches if the request includes any private modules
352                                 if ( $module->getGroup() === 'private' ) {
353                                         $private = true;
354                                 }
355                                 // Calculate maximum modified time
356                                 $mtime = max( $mtime, $module->getModifiedTime( $context ) );
357                         } catch ( Exception $e ) {
358                                 // Add exception to the output as a comment
359                                 $exceptions .= "/*\n{$e->__toString()}\n*/\n";
360                         }
361                 }
362
363                 wfProfileOut( __METHOD__.'-getModifiedTime' );
364
365                 if ( $context->getOnly() === 'styles' ) {
366                         header( 'Content-Type: text/css; charset=utf-8' );
367                 } else {
368                         header( 'Content-Type: text/javascript; charset=utf-8' );
369                 }
370                 header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
371                 if ( $context->getDebug() ) {
372                         // Do not cache debug responses
373                         header( 'Cache-Control: private, no-cache, must-revalidate' );
374                         header( 'Pragma: no-cache' );
375                 } else {
376                         if ( $private ) {
377                                 header( "Cache-Control: private, max-age=$maxage" );
378                                 $exp = $maxage;
379                         } else {
380                                 header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
381                                 $exp = min( $maxage, $smaxage );
382                         }
383                         header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
384                 }
385
386                 // If there's an If-Modified-Since header, respond with a 304 appropriately
387                 // Some clients send "timestamp;length=123". Strip the part after the first ';'
388                 // so we get a valid timestamp.
389                 $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
390                 if ( $ims !== false ) {
391                         $imsTS = strtok( $ims, ';' );
392                         if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
393                                 // There's another bug in ob_gzhandler (see also the comment at
394                                 // the top of this function) that causes it to gzip even empty
395                                 // responses, meaning it's impossible to produce a truly empty
396                                 // response (because the gzip header is always there). This is
397                                 // a problem because 304 responses have to be completely empty
398                                 // per the HTTP spec, and Firefox behaves buggily when they're not.
399                                 // See also http://bugs.php.net/bug.php?id=51579
400                                 // To work around this, we tear down all output buffering before
401                                 // sending the 304.
402                                 // On some setups, ob_get_level() doesn't seem to go down to zero
403                                 // no matter how often we call ob_get_clean(), so instead of doing
404                                 // the more intuitive while ( ob_get_level() > 0 ) ob_get_clean();
405                                 // we have to be safe here and avoid an infinite loop.
406                                 for ( $i = 0; $i < ob_get_level(); $i++ ) {
407                                         ob_end_clean();
408                                 }
409                                 
410                                 header( 'HTTP/1.0 304 Not Modified' );
411                                 header( 'Status: 304 Not Modified' );
412                                 wfProfileOut( __METHOD__ );
413                                 return;
414                         }
415                 }
416                 
417                 // Generate a response
418                 $response = $this->makeModuleResponse( $context, $modules, $missing );
419                 
420                 // Prepend comments indicating exceptions
421                 $response = $exceptions . $response;
422
423                 // Capture any PHP warnings from the output buffer and append them to the
424                 // response in a comment if we're in debug mode.
425                 if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
426                         $response = "/*\n$warnings\n*/\n" . $response;
427                 }
428
429                 // Remove the output buffer and output the response
430                 ob_end_clean();
431                 echo $response;
432
433                 wfProfileOut( __METHOD__ );
434         }
435
436         /**
437          * Generates code for a response
438          * 
439          * @param $context ResourceLoaderContext: Context in which to generate a response
440          * @param $modules Array: List of module objects keyed by module name
441          * @param $missing Array: List of unavailable modules (optional)
442          * @return String: Response data
443          */
444         public function makeModuleResponse( ResourceLoaderContext $context, 
445                 array $modules, $missing = array() ) 
446         {
447                 $out = '';
448                 $exceptions = '';
449                 if ( $modules === array() && $missing === array() ) {
450                         return '/* No modules requested. Max made me put this here */';
451                 }
452                 
453                 wfProfileIn( __METHOD__ );
454                 // Pre-fetch blobs
455                 if ( $context->shouldIncludeMessages() ) {
456                         try {
457                                 $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
458                         } catch ( Exception $e ) {
459                                 // Add exception to the output as a comment
460                                 $exceptions .= "/*\n{$e->__toString()}\n*/\n";
461                         }
462                 } else {
463                         $blobs = array();
464                 }
465
466                 // Generate output
467                 foreach ( $modules as $name => $module ) {
468                         wfProfileIn( __METHOD__ . '-' . $name );
469                         try {
470                                 // Scripts
471                                 $scripts = '';
472                                 if ( $context->shouldIncludeScripts() ) {
473                                         // bug 27054: Append semicolon to prevent weird bugs
474                                         // caused by files not terminating their statements right
475                                         $scripts .= $module->getScript( $context ) . ";\n";
476                                 }
477
478                                 // Styles
479                                 $styles = array();
480                                 if ( $context->shouldIncludeStyles() ) {
481                                         $styles = $module->getStyles( $context );
482                                 }
483
484                                 // Messages
485                                 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
486
487                                 // Append output
488                                 switch ( $context->getOnly() ) {
489                                         case 'scripts':
490                                                 $out .= $scripts;
491                                                 break;
492                                         case 'styles':
493                                                 $out .= self::makeCombinedStyles( $styles );
494                                                 break;
495                                         case 'messages':
496                                                 $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
497                                                 break;
498                                         default:
499                                                 // Minify CSS before embedding in mediaWiki.loader.implement call
500                                                 // (unless in debug mode)
501                                                 if ( !$context->getDebug() ) {
502                                                         foreach ( $styles as $media => $style ) {
503                                                                 $styles[$media] = $this->filter( 'minify-css', $style );
504                                                         }
505                                                 }
506                                                 $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
507                                                         new XmlJsCode( $messagesBlob ) );
508                                                 break;
509                                 }
510                         } catch ( Exception $e ) {
511                                 // Add exception to the output as a comment
512                                 $exceptions .= "/*\n{$e->__toString()}\n*/\n";
513
514                                 // Register module as missing
515                                 $missing[] = $name;
516                                 unset( $modules[$name] );
517                         }
518                         wfProfileOut( __METHOD__ . '-' . $name );
519                 }
520
521                 // Update module states
522                 if ( $context->shouldIncludeScripts() ) {
523                         // Set the state of modules loaded as only scripts to ready
524                         if ( count( $modules ) && $context->getOnly() === 'scripts' 
525                                 && !isset( $modules['startup'] ) ) 
526                         {
527                                 $out .= self::makeLoaderStateScript( 
528                                         array_fill_keys( array_keys( $modules ), 'ready' ) );
529                         }
530                         // Set the state of modules which were requested but unavailable as missing
531                         if ( is_array( $missing ) && count( $missing ) ) {
532                                 $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
533                         }
534                 }
535
536                 if ( !$context->getDebug() ) {
537                         if ( $context->getOnly() === 'styles' ) {
538                                 $out = $this->filter( 'minify-css', $out );
539                         } else {
540                                 $out = $this->filter( 'minify-js', $out );
541                         }
542                 }
543                 
544                 wfProfileOut( __METHOD__ );
545                 return $exceptions . $out;
546         }
547
548         /* Static Methods */
549
550         /**
551          * Returns JS code to call to mediaWiki.loader.implement for a module with 
552          * given properties.
553          *
554          * @param $name Module name
555          * @param $scripts Array: List of JavaScript code snippets to be executed after the 
556          *     module is loaded
557          * @param $styles Array: List of CSS strings keyed by media type
558          * @param $messages Mixed: List of messages associated with this module. May either be an 
559          *     associative array mapping message key to value, or a JSON-encoded message blob containing
560          *     the same data, wrapped in an XmlJsCode object.
561          */
562         public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
563                 if ( is_array( $scripts ) ) {
564                         $scripts = implode( $scripts, "\n" );
565                 }
566                 return Xml::encodeJsCall( 
567                         'mediaWiki.loader.implement', 
568                         array(
569                                 $name,
570                                 new XmlJsCode( "function( $, mw ) {{$scripts}}" ),
571                                 (object)$styles,
572                                 (object)$messages
573                         ) );
574         }
575
576         /**
577          * Returns JS code which, when called, will register a given list of messages.
578          *
579          * @param $messages Mixed: Either an associative array mapping message key to value, or a
580          *     JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object.
581          */
582         public static function makeMessageSetScript( $messages ) {
583                 return Xml::encodeJsCall( 'mediaWiki.messages.set', array( (object)$messages ) );
584         }
585
586         /**
587          * Combines an associative array mapping media type to CSS into a 
588          * single stylesheet with @media blocks.
589          *
590          * @param $styles Array: List of CSS strings keyed by media type
591          */
592         public static function makeCombinedStyles( array $styles ) {
593                 $out = '';
594                 foreach ( $styles as $media => $style ) {
595                         // Transform the media type based on request params and config
596                         // The way that this relies on $wgRequest to propagate request params is slightly evil
597                         $media = OutputPage::transformCssMedia( $media );
598                         
599                         if ( $media === null ) {
600                                 // Skip
601                         } else if ( $media === '' || $media == 'all' ) {
602                                 // Don't output invalid or frivolous @media statements
603                                 $out .= "$style\n";
604                         } else {
605                                 $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n";
606                         }
607                 }
608                 return $out;
609         }
610
611         /**
612          * Returns a JS call to mediaWiki.loader.state, which sets the state of a 
613          * module or modules to a given value. Has two calling conventions:
614          *
615          *    - ResourceLoader::makeLoaderStateScript( $name, $state ):
616          *         Set the state of a single module called $name to $state
617          *
618          *    - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ):
619          *         Set the state of modules with the given names to the given states
620          */
621         public static function makeLoaderStateScript( $name, $state = null ) {
622                 if ( is_array( $name ) ) {
623                         return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name ) );
624                 } else {
625                         return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name, $state ) );
626                 }
627         }
628
629         /**
630          * Returns JS code which calls the script given by $script. The script will
631          * be called with local variables name, version, dependencies and group, 
632          * which will have values corresponding to $name, $version, $dependencies 
633          * and $group as supplied. 
634          *
635          * @param $name String: Module name
636          * @param $version Integer: Module version number as a timestamp
637          * @param $dependencies Array: List of module names on which this module depends
638          * @param $group String: Group which the module is in.
639          * @param $script String: JavaScript code
640          */
641         public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) {
642                 $script = str_replace( "\n", "\n\t", trim( $script ) );
643                 return Xml::encodeJsCall( 
644                         "( function( name, version, dependencies, group ) {\n\t$script\n} )",
645                         array( $name, $version, $dependencies, $group ) );
646         }
647
648         /**
649          * Returns JS code which calls mediaWiki.loader.register with the given 
650          * parameters. Has three calling conventions:
651          *
652          *   - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group ):
653          *       Register a single module.
654          *
655          *   - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ):
656          *       Register modules with the given names.
657          *
658          *   - ResourceLoader::makeLoaderRegisterScript( array(
659          *        array( $name1, $version1, $dependencies1, $group1 ),
660          *        array( $name2, $version2, $dependencies1, $group2 ),
661          *        ...
662          *     ) ):
663          *        Registers modules with the given names and parameters.
664          *
665          * @param $name String: Module name
666          * @param $version Integer: Module version number as a timestamp
667          * @param $dependencies Array: List of module names on which this module depends
668          * @param $group String: group which the module is in.
669          */
670         public static function makeLoaderRegisterScript( $name, $version = null, 
671                 $dependencies = null, $group = null ) 
672         {
673                 if ( is_array( $name ) ) {
674                         return Xml::encodeJsCall( 'mediaWiki.loader.register', array( $name ) );
675                 } else {
676                         $version = (int) $version > 1 ? (int) $version : 1;
677                         return Xml::encodeJsCall( 'mediaWiki.loader.register', 
678                                 array( $name, $version, $dependencies, $group ) );
679                 }
680         }
681
682         /**
683          * Returns JS code which runs given JS code if the client-side framework is 
684          * present.
685          *
686          * @param $script String: JavaScript code
687          */
688         public static function makeLoaderConditionalScript( $script ) {
689                 $script = str_replace( "\n", "\n\t", trim( $script ) );
690                 return "if ( window.mediaWiki ) {\n\t$script\n}\n";
691         }
692
693         /**
694          * Returns JS code which will set the MediaWiki configuration array to 
695          * the given value.
696          *
697          * @param $configuration Array: List of configuration values keyed by variable name
698          */
699         public static function makeConfigSetScript( array $configuration ) {
700                 return Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) );
701         }
702         
703         /**
704          * Convert an array of module names to a packed query string.
705          * 
706          * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' )
707          * becomes 'foo.bar,baz|bar.baz,quux'
708          * @param $modules array of module names (strings)
709          * @return string Packed query string
710          */
711         public static function makePackedModulesString( $modules ) {
712                 $groups = array(); // array( prefix => array( suffixes ) )
713                 foreach ( $modules as $module ) {
714                         $pos = strrpos( $module, '.' );
715                         $prefix = $pos === false ? '' : substr( $module, 0, $pos );
716                         $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
717                         $groups[$prefix][] = $suffix;
718                 }
719                 
720                 $arr = array();
721                 foreach ( $groups as $prefix => $suffixes ) {
722                         $p = $prefix === '' ? '' : $prefix . '.';
723                         $arr[] = $p . implode( ',', $suffixes );
724                 }
725                 return implode( '|', $arr );
726         }
727         
728         /**
729          * Determine whether debug mode was requested
730          * Order of priority is 1) request param, 2) cookie, 3) $wg setting
731          * @return bool
732          */
733         public static function inDebugMode() {
734                 global $wgRequest, $wgResourceLoaderDebug;
735                 static $retval = null;
736                 if ( !is_null( $retval ) )
737                         return $retval;
738                 return $retval = $wgRequest->getFuzzyBool( 'debug',
739                         $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
740         }
741 }