]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/resourceloader/ResourceLoader.php
MediaWiki 1.17.3
[autoinstalls/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 = $this->makeComment( $exception->__toString() );
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                 $errors = '';
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                                 $module = $this->getModule( $name );
317                                 // Do not allow private modules to be loaded from the web.
318                                 // This is a security issue, see bug 34907.
319                                 if ( $module->getGroup() === 'private' ) {
320                                         $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
321                                         continue;
322                                 }
323                                 $modules[$name] = $this->getModule( $name );
324                         } else {
325                                 $missing[] = $name;
326                         }
327                 }
328
329                 // If a version wasn't specified we need a shorter expiry time for updates 
330                 // to propagate to clients quickly
331                 if ( is_null( $context->getVersion() ) ) {
332                         $maxage  = $wgResourceLoaderMaxage['unversioned']['client'];
333                         $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
334                 }
335                 // If a version was specified we can use a longer expiry time since changing 
336                 // version numbers causes cache misses
337                 else {
338                         $maxage  = $wgResourceLoaderMaxage['versioned']['client'];
339                         $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
340                 }
341
342                 // Preload information needed to the mtime calculation below
343                 try {
344                         $this->preloadModuleInfo( array_keys( $modules ), $context );
345                 } catch( Exception $e ) {
346                         // Add exception to the output as a comment
347                         $errors .= $this->makeComment( $e->__toString() );
348                 }
349
350                 wfProfileIn( __METHOD__.'-getModifiedTime' );
351
352                 // To send Last-Modified and support If-Modified-Since, we need to detect 
353                 // the last modified time
354                 $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
355                 foreach ( $modules as $module ) {
356                         try {
357                                 // Calculate maximum modified time
358                                 $mtime = max( $mtime, $module->getModifiedTime( $context ) );
359                         } catch ( Exception $e ) {
360                                 // Add exception to the output as a comment
361                                 $errors .= $this->makeComment( $e->__toString() );
362                         }
363                 }
364
365                 wfProfileOut( __METHOD__.'-getModifiedTime' );
366
367                 if ( $context->getOnly() === 'styles' ) {
368                         header( 'Content-Type: text/css; charset=utf-8' );
369                 } else {
370                         header( 'Content-Type: text/javascript; charset=utf-8' );
371                 }
372                 header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
373                 if ( $context->getDebug() ) {
374                         // Do not cache debug responses
375                         header( 'Cache-Control: private, no-cache, must-revalidate' );
376                         header( 'Pragma: no-cache' );
377                 } else {
378                         header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
379                         $exp = min( $maxage, $smaxage );
380                         header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
381                 }
382
383                 // If there's an If-Modified-Since header, respond with a 304 appropriately
384                 // Some clients send "timestamp;length=123". Strip the part after the first ';'
385                 // so we get a valid timestamp.
386                 $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
387                 if ( $ims !== false ) {
388                         $imsTS = strtok( $ims, ';' );
389                         if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
390                                 // There's another bug in ob_gzhandler (see also the comment at
391                                 // the top of this function) that causes it to gzip even empty
392                                 // responses, meaning it's impossible to produce a truly empty
393                                 // response (because the gzip header is always there). This is
394                                 // a problem because 304 responses have to be completely empty
395                                 // per the HTTP spec, and Firefox behaves buggily when they're not.
396                                 // See also http://bugs.php.net/bug.php?id=51579
397                                 // To work around this, we tear down all output buffering before
398                                 // sending the 304.
399                                 // On some setups, ob_get_level() doesn't seem to go down to zero
400                                 // no matter how often we call ob_get_clean(), so instead of doing
401                                 // the more intuitive while ( ob_get_level() > 0 ) ob_get_clean();
402                                 // we have to be safe here and avoid an infinite loop.
403                                 for ( $i = 0; $i < ob_get_level(); $i++ ) {
404                                         ob_end_clean();
405                                 }
406                                 
407                                 header( 'HTTP/1.0 304 Not Modified' );
408                                 header( 'Status: 304 Not Modified' );
409                                 wfProfileOut( __METHOD__ );
410                                 return;
411                         }
412                 }
413                 
414                 // Generate a response
415                 $response = $this->makeModuleResponse( $context, $modules, $missing );
416                 
417                 // Prepend comments indicating exceptions
418                 $response = $errors . $response;
419
420                 // Capture any PHP warnings from the output buffer and append them to the
421                 // response in a comment if we're in debug mode.
422                 if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
423                         $response = $this->makeComment( $warnings ) . $response;
424                 }
425
426                 // Remove the output buffer and output the response
427                 ob_end_clean();
428                 echo $response;
429
430                 wfProfileOut( __METHOD__ );
431         }
432
433         protected function makeComment( $text ) {
434                 $encText = str_replace( '*/', '* /', $text );
435                 return "/*\n$encText\n*/\n";
436         }
437
438         /**
439          * Generates code for a response
440          * 
441          * @param $context ResourceLoaderContext: Context in which to generate a response
442          * @param $modules Array: List of module objects keyed by module name
443          * @param $missing Array: List of unavailable modules (optional)
444          * @return String: Response data
445          */
446         public function makeModuleResponse( ResourceLoaderContext $context, 
447                 array $modules, $missing = array() ) 
448         {
449                 $out = '';
450                 $exceptions = '';
451                 if ( $modules === array() && $missing === array() ) {
452                         return '/* No modules requested. Max made me put this here */';
453                 }
454                 
455                 wfProfileIn( __METHOD__ );
456                 // Pre-fetch blobs
457                 if ( $context->shouldIncludeMessages() ) {
458                         try {
459                                 $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
460                         } catch ( Exception $e ) {
461                                 // Add exception to the output as a comment
462                                 $exceptions .= $this->makeComment( $e->__toString() );
463                         }
464                 } else {
465                         $blobs = array();
466                 }
467
468                 // Generate output
469                 foreach ( $modules as $name => $module ) {
470                         wfProfileIn( __METHOD__ . '-' . $name );
471                         try {
472                                 // Scripts
473                                 $scripts = '';
474                                 if ( $context->shouldIncludeScripts() ) {
475                                         // bug 27054: Append semicolon to prevent weird bugs
476                                         // caused by files not terminating their statements right
477                                         $scripts .= $module->getScript( $context ) . ";\n";
478                                 }
479
480                                 // Styles
481                                 $styles = array();
482                                 if ( $context->shouldIncludeStyles() ) {
483                                         $styles = $module->getStyles( $context );
484                                 }
485
486                                 // Messages
487                                 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
488
489                                 // Append output
490                                 switch ( $context->getOnly() ) {
491                                         case 'scripts':
492                                                 $out .= $scripts;
493                                                 break;
494                                         case 'styles':
495                                                 $out .= self::makeCombinedStyles( $styles );
496                                                 break;
497                                         case 'messages':
498                                                 $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
499                                                 break;
500                                         default:
501                                                 // Minify CSS before embedding in mediaWiki.loader.implement call
502                                                 // (unless in debug mode)
503                                                 if ( !$context->getDebug() ) {
504                                                         foreach ( $styles as $media => $style ) {
505                                                                 $styles[$media] = $this->filter( 'minify-css', $style );
506                                                         }
507                                                 }
508                                                 $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
509                                                         new XmlJsCode( $messagesBlob ) );
510                                                 break;
511                                 }
512                         } catch ( Exception $e ) {
513                                 // Add exception to the output as a comment
514                                 $exceptions .= $this->makeComment( $e->__toString() );
515
516                                 // Register module as missing
517                                 $missing[] = $name;
518                                 unset( $modules[$name] );
519                         }
520                         wfProfileOut( __METHOD__ . '-' . $name );
521                 }
522
523                 // Update module states
524                 if ( $context->shouldIncludeScripts() ) {
525                         // Set the state of modules loaded as only scripts to ready
526                         if ( count( $modules ) && $context->getOnly() === 'scripts' 
527                                 && !isset( $modules['startup'] ) ) 
528                         {
529                                 $out .= self::makeLoaderStateScript( 
530                                         array_fill_keys( array_keys( $modules ), 'ready' ) );
531                         }
532                         // Set the state of modules which were requested but unavailable as missing
533                         if ( is_array( $missing ) && count( $missing ) ) {
534                                 $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
535                         }
536                 }
537
538                 if ( !$context->getDebug() ) {
539                         if ( $context->getOnly() === 'styles' ) {
540                                 $out = $this->filter( 'minify-css', $out );
541                         } else {
542                                 $out = $this->filter( 'minify-js', $out );
543                         }
544                 }
545                 
546                 wfProfileOut( __METHOD__ );
547                 return $exceptions . $out;
548         }
549
550         /* Static Methods */
551
552         /**
553          * Returns JS code to call to mediaWiki.loader.implement for a module with 
554          * given properties.
555          *
556          * @param $name Module name
557          * @param $scripts Array: List of JavaScript code snippets to be executed after the 
558          *     module is loaded
559          * @param $styles Array: List of CSS strings keyed by media type
560          * @param $messages Mixed: List of messages associated with this module. May either be an 
561          *     associative array mapping message key to value, or a JSON-encoded message blob containing
562          *     the same data, wrapped in an XmlJsCode object.
563          */
564         public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
565                 if ( is_array( $scripts ) ) {
566                         $scripts = implode( $scripts, "\n" );
567                 }
568                 return Xml::encodeJsCall( 
569                         'mediaWiki.loader.implement', 
570                         array(
571                                 $name,
572                                 new XmlJsCode( "function( $, mw ) {{$scripts}}" ),
573                                 (object)$styles,
574                                 (object)$messages
575                         ) );
576         }
577
578         /**
579          * Returns JS code which, when called, will register a given list of messages.
580          *
581          * @param $messages Mixed: Either an associative array mapping message key to value, or a
582          *     JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object.
583          */
584         public static function makeMessageSetScript( $messages ) {
585                 return Xml::encodeJsCall( 'mediaWiki.messages.set', array( (object)$messages ) );
586         }
587
588         /**
589          * Combines an associative array mapping media type to CSS into a 
590          * single stylesheet with @media blocks.
591          *
592          * @param $styles Array: List of CSS strings keyed by media type
593          */
594         public static function makeCombinedStyles( array $styles ) {
595                 $out = '';
596                 foreach ( $styles as $media => $style ) {
597                         // Transform the media type based on request params and config
598                         // The way that this relies on $wgRequest to propagate request params is slightly evil
599                         $media = OutputPage::transformCssMedia( $media );
600                         
601                         if ( $media === null ) {
602                                 // Skip
603                         } else if ( $media === '' || $media == 'all' ) {
604                                 // Don't output invalid or frivolous @media statements
605                                 $out .= "$style\n";
606                         } else {
607                                 $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n";
608                         }
609                 }
610                 return $out;
611         }
612
613         /**
614          * Returns a JS call to mediaWiki.loader.state, which sets the state of a 
615          * module or modules to a given value. Has two calling conventions:
616          *
617          *    - ResourceLoader::makeLoaderStateScript( $name, $state ):
618          *         Set the state of a single module called $name to $state
619          *
620          *    - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ):
621          *         Set the state of modules with the given names to the given states
622          */
623         public static function makeLoaderStateScript( $name, $state = null ) {
624                 if ( is_array( $name ) ) {
625                         return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name ) );
626                 } else {
627                         return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name, $state ) );
628                 }
629         }
630
631         /**
632          * Returns JS code which calls the script given by $script. The script will
633          * be called with local variables name, version, dependencies and group, 
634          * which will have values corresponding to $name, $version, $dependencies 
635          * and $group as supplied. 
636          *
637          * @param $name String: Module name
638          * @param $version Integer: Module version number as a timestamp
639          * @param $dependencies Array: List of module names on which this module depends
640          * @param $group String: Group which the module is in.
641          * @param $script String: JavaScript code
642          */
643         public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) {
644                 $script = str_replace( "\n", "\n\t", trim( $script ) );
645                 return Xml::encodeJsCall( 
646                         "( function( name, version, dependencies, group ) {\n\t$script\n} )",
647                         array( $name, $version, $dependencies, $group ) );
648         }
649
650         /**
651          * Returns JS code which calls mediaWiki.loader.register with the given 
652          * parameters. Has three calling conventions:
653          *
654          *   - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group ):
655          *       Register a single module.
656          *
657          *   - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ):
658          *       Register modules with the given names.
659          *
660          *   - ResourceLoader::makeLoaderRegisterScript( array(
661          *        array( $name1, $version1, $dependencies1, $group1 ),
662          *        array( $name2, $version2, $dependencies1, $group2 ),
663          *        ...
664          *     ) ):
665          *        Registers modules with the given names and parameters.
666          *
667          * @param $name String: Module name
668          * @param $version Integer: Module version number as a timestamp
669          * @param $dependencies Array: List of module names on which this module depends
670          * @param $group String: group which the module is in.
671          */
672         public static function makeLoaderRegisterScript( $name, $version = null, 
673                 $dependencies = null, $group = null ) 
674         {
675                 if ( is_array( $name ) ) {
676                         return Xml::encodeJsCall( 'mediaWiki.loader.register', array( $name ) );
677                 } else {
678                         $version = (int) $version > 1 ? (int) $version : 1;
679                         return Xml::encodeJsCall( 'mediaWiki.loader.register', 
680                                 array( $name, $version, $dependencies, $group ) );
681                 }
682         }
683
684         /**
685          * Returns JS code which runs given JS code if the client-side framework is 
686          * present.
687          *
688          * @param $script String: JavaScript code
689          */
690         public static function makeLoaderConditionalScript( $script ) {
691                 $script = str_replace( "\n", "\n\t", trim( $script ) );
692                 return "if ( window.mediaWiki ) {\n\t$script\n}\n";
693         }
694
695         /**
696          * Returns JS code which will set the MediaWiki configuration array to 
697          * the given value.
698          *
699          * @param $configuration Array: List of configuration values keyed by variable name
700          */
701         public static function makeConfigSetScript( array $configuration ) {
702                 return Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) );
703         }
704         
705         /**
706          * Convert an array of module names to a packed query string.
707          * 
708          * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' )
709          * becomes 'foo.bar,baz|bar.baz,quux'
710          * @param $modules array of module names (strings)
711          * @return string Packed query string
712          */
713         public static function makePackedModulesString( $modules ) {
714                 $groups = array(); // array( prefix => array( suffixes ) )
715                 foreach ( $modules as $module ) {
716                         $pos = strrpos( $module, '.' );
717                         $prefix = $pos === false ? '' : substr( $module, 0, $pos );
718                         $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
719                         $groups[$prefix][] = $suffix;
720                 }
721                 
722                 $arr = array();
723                 foreach ( $groups as $prefix => $suffixes ) {
724                         $p = $prefix === '' ? '' : $prefix . '.';
725                         $arr[] = $p . implode( ',', $suffixes );
726                 }
727                 return implode( '|', $arr );
728         }
729         
730         /**
731          * Determine whether debug mode was requested
732          * Order of priority is 1) request param, 2) cookie, 3) $wg setting
733          * @return bool
734          */
735         public static function inDebugMode() {
736                 global $wgRequest, $wgResourceLoaderDebug;
737                 static $retval = null;
738                 if ( !is_null( $retval ) )
739                         return $retval;
740                 return $retval = $wgRequest->getFuzzyBool( 'debug',
741                         $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
742         }
743 }