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