$fname -> __METHOD__
[lhc/web/wiklou.git] / includes / ResourceLoaderModule.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 Trevor Parscal
20 * @author Roan Kattouw
21 */
22
23 /**
24 * Interface for resource loader modules, with name registration and maxage functionality.
25 */
26 abstract class ResourceLoaderModule {
27 /* Protected Members */
28
29 protected $name = null;
30
31 /* Methods */
32
33 /**
34 * Get this module's name. This is set when the module is registered
35 * with ResourceLoader::register()
36 *
37 * @return Mixed: name (string) or null if no name was set
38 */
39 public function getName() {
40 return $this->name;
41 }
42
43 /**
44 * Set this module's name. This is called by ResourceLodaer::register()
45 * when registering the module. Other code should not call this.
46 *
47 * @param $name String: name
48 */
49 public function setName( $name ) {
50 $this->name = $name;
51 }
52
53 /**
54 * The maximum number of seconds to cache this module for in the
55 * client-side (browser) cache. Override this only if you have a good
56 * reason not to use $wgResourceLoaderClientMaxage.
57 *
58 * @return Integer: cache maxage in seconds
59 */
60 public function getClientMaxage() {
61 global $wgResourceLoaderClientMaxage;
62 return $wgResourceLoaderClientMaxage;
63 }
64
65 /**
66 * The maximum number of seconds to cache this module for in the
67 * server-side (Squid / proxy) cache. Override this only if you have a
68 * good reason not to use $wgResourceLoaderServerMaxage.
69 *
70 * @return Integer: cache maxage in seconds
71 */
72 public function getServerMaxage() {
73 global $wgResourceLoaderServerMaxage;
74 return $wgResourceLoaderServerMaxage;
75 }
76
77 /**
78 * Get whether CSS for this module should be flipped
79 */
80 public function getFlip( $context ) {
81 return $context->getDirection() === 'rtl';
82 }
83
84 /* Abstract Methods */
85
86 /**
87 * Get all JS for this module for a given language and skin.
88 * Includes all relevant JS except loader scripts.
89 *
90 * @param $context ResourceLoaderContext object
91 * @return String: JS
92 */
93 public abstract function getScript( ResourceLoaderContext $context );
94
95 /**
96 * Get all CSS for this module for a given skin.
97 *
98 * @param $context ResourceLoaderContext object
99 * @return array: strings of CSS keyed by media type
100 */
101 public abstract function getStyles( ResourceLoaderContext $context );
102
103 /**
104 * Get the messages needed for this module.
105 *
106 * To get a JSON blob with messages, use MessageBlobStore::get()
107 *
108 * @return array of message keys. Keys may occur more than once
109 */
110 public abstract function getMessages();
111
112 /**
113 * Get the loader JS for this module, if set.
114 *
115 * @return Mixed: loader JS (string) or false if no custom loader set
116 */
117 public abstract function getLoaderScript();
118
119 /**
120 * Get a list of modules this module depends on.
121 *
122 * Dependency information is taken into account when loading a module
123 * on the client side. When adding a module on the server side,
124 * dependency information is NOT taken into account and YOU are
125 * responsible for adding dependent modules as well. If you don't do
126 * this, the client side loader will send a second request back to the
127 * server to fetch the missing modules, which kind of defeats the
128 * purpose of the resource loader.
129 *
130 * To add dependencies dynamically on the client side, use a custom
131 * loader script, see getLoaderScript()
132 * @return Array of module names (strings)
133 */
134 public abstract function getDependencies();
135
136 /**
137 * Get this module's last modification timestamp for a given
138 * combination of language, skin and debug mode flag. This is typically
139 * the highest of each of the relevant components' modification
140 * timestamps. Whenever anything happens that changes the module's
141 * contents for these parameters, the mtime should increase.
142 *
143 * @param $context ResourceLoaderContext object
144 * @return int UNIX timestamp
145 */
146 public abstract function getModifiedTime( ResourceLoaderContext $context );
147 }
148
149 /**
150 * Module based on local JS/CSS files. This is the most common type of module.
151 */
152 class ResourceLoaderFileModule extends ResourceLoaderModule {
153 /* Protected Members */
154
155 protected $scripts = array();
156 protected $styles = array();
157 protected $messages = array();
158 protected $dependencies = array();
159 protected $debugScripts = array();
160 protected $languageScripts = array();
161 protected $skinScripts = array();
162 protected $skinStyles = array();
163 protected $loaders = array();
164 protected $parameters = array();
165
166 // In-object cache for file dependencies
167 protected $fileDeps = array();
168 // In-object cache for mtime
169 protected $modifiedTime = array();
170
171 /* Methods */
172
173 /**
174 * Construct a new module from an options array.
175 *
176 * @param $options array Options array. If empty, an empty module will be constructed
177 *
178 * $options format:
179 * array(
180 * // Required module options (mutually exclusive)
181 * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
182 *
183 * // Optional module options
184 * 'languageScripts' => array(
185 * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
186 * ...
187 * ),
188 * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
189 * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
190 *
191 * // Non-raw module options
192 * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
193 * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
194 * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
195 * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
196 * 'skinStyles' => array(
197 * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
198 * array( 'dir/file1.css' => array( 'media' => 'print' )
199 * ...
200 * ),
201 * 'messages' => array( 'message1', 'message2' ... ),
202 * )
203 */
204 public function __construct( $options = array() ) {
205 foreach ( $options as $option => $value ) {
206 switch ( $option ) {
207 case 'scripts':
208 $this->scripts = (array)$value;
209 break;
210 case 'styles':
211 $this->styles = (array)$value;
212 break;
213 case 'messages':
214 $this->messages = (array)$value;
215 break;
216 case 'dependencies':
217 $this->dependencies = (array)$value;
218 break;
219 case 'debugScripts':
220 $this->debugScripts = (array)$value;
221 break;
222 case 'languageScripts':
223 $this->languageScripts = (array)$value;
224 break;
225 case 'skinScripts':
226 $this->skinScripts = (array)$value;
227 break;
228 case 'skinStyles':
229 $this->skinStyles = (array)$value;
230 break;
231 case 'loaders':
232 $this->loaders = (array)$value;
233 break;
234 }
235 }
236 }
237
238 /**
239 * Add script files to this module. In order to be valid, a module
240 * must contain at least one script file.
241 *
242 * @param $scripts Mixed: path to script file (string) or array of paths
243 */
244 public function addScripts( $scripts ) {
245 $this->scripts = array_merge( $this->scripts, (array)$scripts );
246 }
247
248 /**
249 * Add style (CSS) files to this module.
250 *
251 * @param $styles Mixed: path to CSS file (string) or array of paths
252 */
253 public function addStyles( $styles ) {
254 $this->styles = array_merge( $this->styles, (array)$styles );
255 }
256
257 /**
258 * Add messages to this module.
259 *
260 * @param $messages Mixed: message key (string) or array of message keys
261 */
262 public function addMessages( $messages ) {
263 $this->messages = array_merge( $this->messages, (array)$messages );
264 }
265
266 /**
267 * Add dependencies. Dependency information is taken into account when
268 * loading a module on the client side. When adding a module on the
269 * server side, dependency information is NOT taken into account and
270 * YOU are responsible for adding dependent modules as well. If you
271 * don't do this, the client side loader will send a second request
272 * back to the server to fetch the missing modules, which kind of
273 * defeats the point of using the resource loader in the first place.
274 *
275 * To add dependencies dynamically on the client side, use a custom
276 * loader (see addLoaders())
277 *
278 * @param $dependencies Mixed: module name (string) or array of module names
279 */
280 public function addDependencies( $dependencies ) {
281 $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
282 }
283
284 /**
285 * Add debug scripts to the module. These scripts are only included
286 * in debug mode.
287 *
288 * @param $scripts Mixed: path to script file (string) or array of paths
289 */
290 public function addDebugScripts( $scripts ) {
291 $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
292 }
293
294 /**
295 * Add language-specific scripts. These scripts are only included for
296 * a given language.
297 *
298 * @param $lang String: language code
299 * @param $scripts Mixed: path to script file (string) or array of paths
300 */
301 public function addLanguageScripts( $lang, $scripts ) {
302 $this->languageScripts = array_merge_recursive(
303 $this->languageScripts,
304 array( $lang => $scripts )
305 );
306 }
307
308 /**
309 * Add skin-specific scripts. These scripts are only included for
310 * a given skin.
311 *
312 * @param $skin String: skin name, or 'default'
313 * @param $scripts Mixed: path to script file (string) or array of paths
314 */
315 public function addSkinScripts( $skin, $scripts ) {
316 $this->skinScripts = array_merge_recursive(
317 $this->skinScripts,
318 array( $skin => $scripts )
319 );
320 }
321
322 /**
323 * Add skin-specific CSS. These CSS files are only included for a
324 * given skin. If there are no skin-specific CSS files for a skin,
325 * the files defined for 'default' will be used, if any.
326 *
327 * @param $skin String: skin name, or 'default'
328 * @param $scripts Mixed: path to CSS file (string) or array of paths
329 */
330 public function addSkinStyles( $skin, $scripts ) {
331 $this->skinStyles = array_merge_recursive(
332 $this->skinStyles,
333 array( $skin => $scripts )
334 );
335 }
336
337 /**
338 * Add loader scripts. These scripts are loaded on every page and are
339 * responsible for registering this module using
340 * mediaWiki.loader.register(). If there are no loader scripts defined,
341 * the resource loader will register the module itself.
342 *
343 * Loader scripts are used to determine a module's dependencies
344 * dynamically on the client side (e.g. based on browser type/version).
345 * Note that loader scripts are included on every page, so they should
346 * be lightweight and use mediaWiki.loader.register()'s callback
347 * feature to defer dependency calculation.
348 *
349 * @param $scripts Mixed: path to script file (string) or array of paths
350 */
351 public function addLoaders( $scripts ) {
352 $this->loaders = array_merge( $this->loaders, (array)$scripts );
353 }
354
355 public function getScript( ResourceLoaderContext $context ) {
356 $retval = $this->getPrimaryScript() . "\n" .
357 $this->getLanguageScript( $context->getLanguage() ) . "\n" .
358 $this->getSkinScript( $context->getSkin() );
359
360 if ( $context->getDebug() ) {
361 $retval .= $this->getDebugScript();
362 }
363
364 return $retval;
365 }
366
367 public function getStyles( ResourceLoaderContext $context ) {
368 $styles = array();
369 foreach ( $this->getPrimaryStyles() as $media => $style ) {
370 if ( !isset( $styles[$media] ) ) {
371 $styles[$media] = '';
372 }
373 $styles[$media] .= $style;
374 }
375 foreach ( $this->getPrimaryStyles() as $media => $style ) {
376 if ( !isset( $styles[$media] ) ) {
377 $styles[$media] = '';
378 }
379 $styles[$media] .= $this->getSkinStyles( $context->getSkin() );
380 }
381
382 // Collect referenced files
383 $files = array();
384 foreach ( $styles as $media => $style ) {
385 // Extract and store the list of referenced files
386 $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
387 }
388
389 // Only store if modified
390 if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
391 $encFiles = FormatJson::encode( $files );
392 $dbw = wfGetDb( DB_MASTER );
393 $dbw->replace( 'module_deps',
394 array( array( 'md_module', 'md_skin' ) ), array(
395 'md_module' => $this->getName(),
396 'md_skin' => $context->getSkin(),
397 'md_deps' => $encFiles,
398 )
399 );
400
401 // Save into memcached
402 global $wgMemc;
403
404 $key = wfMemcKey( 'resourceloader', 'module_deps', $this->getName(), $context->getSkin() );
405 $wgMemc->set( $key, $encFiles );
406 }
407
408 return $styles;
409 }
410
411 public function getMessages() {
412 return $this->messages;
413 }
414
415 public function getDependencies() {
416 return $this->dependencies;
417 }
418
419 public function getLoaderScript() {
420 if ( count( $this->loaders ) == 0 ) {
421 return false;
422 }
423
424 return self::concatScripts( $this->loaders );
425 }
426
427 /**
428 * Get the last modified timestamp of this module, which is calculated
429 * as the highest last modified timestamp of its constituent files and
430 * the files it depends on (see getFileDependencies()). Only files
431 * relevant to the given language and skin are taken into account, and
432 * files only relevant in debug mode are not taken into account when
433 * debug mode is off.
434 *
435 * @param $context ResourceLoaderContext object
436 * @return Integer: UNIX timestamp
437 */
438 public function getModifiedTime( ResourceLoaderContext $context ) {
439 if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
440 return $this->modifiedTime[$context->getHash()];
441 }
442
443 // Sort of nasty way we can get a flat list of files depended on by all styles
444 $styles = array();
445 foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $media => $styleFiles ) {
446 $styles = array_merge( $styles, $styleFiles );
447 }
448 $skinFiles = (array) self::getSkinFiles(
449 $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
450 );
451 foreach ( $skinFiles as $media => $styleFiles ) {
452 $styles = array_merge( $styles, $styleFiles );
453 }
454
455 // Final merge, this should result in a master list of dependent files
456 $files = array_merge(
457 $this->scripts,
458 $styles,
459 $context->getDebug() ? $this->debugScripts : array(),
460 isset( $this->languageScripts[$context->getLanguage()] ) ?
461 (array) $this->languageScripts[$context->getLanguage()] : array(),
462 (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
463 $this->loaders,
464 $this->getFileDependencies( $context->getSkin() )
465 );
466
467 $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
468
469 // Get the mtime of the message blob
470 // TODO: This timestamp is queried a lot and queried separately for each module. Maybe it should be put in memcached?
471 $dbr = wfGetDb( DB_SLAVE );
472 $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
473 'mr_resource' => $this->getName(),
474 'mr_lang' => $context->getLanguage()
475 ), __METHOD__
476 );
477 $msgBlobMtime = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
478
479 $this->modifiedTime[$context->getHash()] = max( $filesMtime, $msgBlobMtime );
480 return $this->modifiedTime[$context->getHash()];
481 }
482
483 /* Protected Members */
484
485 /**
486 * Get the primary JS for this module. This is pulled from the
487 * script files added through addScripts()
488 *
489 * @return String: JS
490 */
491 protected function getPrimaryScript() {
492 return self::concatScripts( $this->scripts );
493 }
494
495 /**
496 * Get the primary CSS for this module. This is pulled from the CSS
497 * files added through addStyles()
498 *
499 * @return String: JS
500 */
501 protected function getPrimaryStyles() {
502 return self::concatStyles( $this->styles );
503 }
504
505 /**
506 * Get the debug JS for this module. This is pulled from the script
507 * files added through addDebugScripts()
508 *
509 * @return String: JS
510 */
511 protected function getDebugScript() {
512 return self::concatScripts( $this->debugScripts );
513 }
514
515 /**
516 * Get the language-specific JS for a given language. This is pulled
517 * from the language-specific script files added through addLanguageScripts()
518 *
519 * @return String: JS
520 */
521 protected function getLanguageScript( $lang ) {
522 if ( !isset( $this->languageScripts[$lang] ) ) {
523 return '';
524 }
525 return self::concatScripts( $this->languageScripts[$lang] );
526 }
527
528 /**
529 * Get the skin-specific JS for a given skin. This is pulled from the
530 * skin-specific JS files added through addSkinScripts()
531 *
532 * @return String: JS
533 */
534 protected function getSkinScript( $skin ) {
535 return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
536 }
537
538 /**
539 * Get the skin-specific CSS for a given skin. This is pulled from the
540 * skin-specific CSS files added through addSkinStyles()
541 *
542 * @return Array: list of CSS strings keyed by media type
543 */
544 protected function getSkinStyles( $skin ) {
545 return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
546 }
547
548 /**
549 * Helper function to get skin-specific data from an array.
550 *
551 * @param $skin String: skin name
552 * @param $map Array: map of skin names to arrays
553 * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
554 */
555 protected static function getSkinFiles( $skin, $map ) {
556 $retval = array();
557
558 if ( isset( $map[$skin] ) && $map[$skin] ) {
559 $retval = $map[$skin];
560 } else if ( isset( $map['default'] ) ) {
561 $retval = $map['default'];
562 }
563
564 return $retval;
565 }
566
567 /**
568 * Get the files this module depends on indirectly for a given skin.
569 * Currently these are only image files referenced by the module's CSS.
570 *
571 * @param $skin String: skin name
572 * @return array of files
573 */
574 protected function getFileDependencies( $skin ) {
575 // Try in-object cache first
576 if ( isset( $this->fileDeps[$skin] ) ) {
577 return $this->fileDeps[$skin];
578 }
579
580 // Now try memcached
581 global $wgMemc;
582
583 $key = wfMemcKey( 'resourceloader', 'module_deps', $this->getName(), $skin );
584 $deps = $wgMemc->get( $key );
585
586 if ( !$deps ) {
587 $dbr = wfGetDb( DB_SLAVE );
588 $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
589 'md_module' => $this->getName(),
590 'md_skin' => $skin,
591 ), __METHOD__
592 );
593 if ( !$deps ) {
594 $deps = '[]'; // Empty array so we can do negative caching
595 }
596 $wgMemc->set( $key, $deps );
597 }
598
599 $this->fileDeps = FormatJson::decode( $deps, true );
600
601 return $this->fileDeps;
602 }
603
604 /**
605 * Get the contents of a set of files and concatenate them, with
606 * newlines in between. Each file is used only once.
607 *
608 * @param $files Array of file names
609 * @return String: concatenated contents of $files
610 */
611 protected static function concatScripts( $files ) {
612 return implode( "\n", array_map( 'file_get_contents', array_map( array( __CLASS__, 'remapFilename' ), array_unique( (array) $files ) ) ) );
613 }
614
615 protected static function organizeFilesByOption( $files, $option, $default ) {
616 $organizedFiles = array();
617 foreach ( (array) $files as $key => $value ) {
618 if ( is_int( $key ) ) {
619 // File name as the value
620 if ( !isset( $organizedFiles[$default] ) ) {
621 $organizedFiles[$default] = array();
622 }
623 $organizedFiles[$default][] = $value;
624 } else if ( is_array( $value ) ) {
625 // File name as the key, options array as the value
626 $media = isset( $value[$option] ) ? $value[$option] : $default;
627 if ( !isset( $organizedFiles[$media] ) ) {
628 $organizedFiles[$media] = array();
629 }
630 $organizedFiles[$media][] = $key;
631 }
632 }
633 return $organizedFiles;
634 }
635
636 /**
637 * Get the contents of a set of CSS files, remap then and concatenate
638 * them, with newlines in between. Each file is used only once.
639 *
640 * @param $files Array of file names
641 * @return Array: list of concatenated and remapped contents of $files keyed by media type
642 */
643 protected static function concatStyles( $styles ) {
644 $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
645 foreach ( $styles as $media => $files ) {
646 $styles[$media] =
647 implode( "\n", array_map( array( __CLASS__, 'remapStyle' ), array_unique( (array) $files ) ) );
648 }
649 return $styles;
650 }
651
652 /**
653 * Remap a relative to $IP. Used as a callback for array_map()
654 *
655 * @param $file String: file name
656 * @return string $IP/$file
657 */
658 protected static function remapFilename( $file ) {
659 global $IP;
660
661 return "$IP/$file";
662 }
663
664 /**
665 * Get the contents of a CSS file and run it through CSSMin::remap().
666 * This wrapper is needed so we can use array_map() in concatStyles()
667 *
668 * @param $file String: file name
669 * @return string Remapped CSS
670 */
671 protected static function remapStyle( $file ) {
672 global $wgUseDataURLs;
673 return CSSMin::remap( file_get_contents( self::remapFilename( $file ) ), dirname( $file ), $wgUseDataURLs );
674 }
675 }
676
677 /**
678 * Custom module for MediaWiki:Common.js and MediaWiki:Skinname.js
679 * TODO: Add Site CSS functionality too
680 */
681 class ResourceLoaderSiteModule extends ResourceLoaderModule {
682 /* Protected Members */
683
684 // In-object cache for modified time
685 protected $modifiedTime = null;
686
687 /* Methods */
688
689 public function getScript( ResourceLoaderContext $context ) {
690 return Skin::newFromKey( $context->getSkin() )->generateUserJs();
691 }
692
693 public function getModifiedTime( ResourceLoaderContext $context ) {
694 if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
695 return $this->modifiedTime[$context->getHash()];
696 }
697
698 // HACK: We duplicate the message names from generateUserJs()
699 // here and weird things (i.e. mtime moving backwards) can happen
700 // when a MediaWiki:Something.js page is deleted
701 $jsPages = array( Title::makeTitle( NS_MEDIAWIKI, 'Common.js' ),
702 Title::makeTitle( NS_MEDIAWIKI, ucfirst( $context->getSkin() ) . '.js' )
703 );
704
705 // Do batch existence check
706 // TODO: This would work better if page_touched were loaded by this as well
707 $lb = new LinkBatch( $jsPages );
708 $lb->execute();
709
710 $this->modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
711
712 foreach ( $jsPages as $jsPage ) {
713 if ( $jsPage->exists() ) {
714 $this->modifiedTime = max( $this->modifiedTime, wfTimestamp( TS_UNIX, $jsPage->getTouched() ) );
715 }
716 }
717
718 return $this->modifiedTime;
719 }
720
721 public function getStyles( ResourceLoaderContext $context ) { return array(); }
722 public function getMessages() { return array(); }
723 public function getLoaderScript() { return ''; }
724 public function getDependencies() { return array(); }
725 }
726
727
728 class ResourceLoaderStartUpModule extends ResourceLoaderModule {
729 /* Protected Members */
730
731 protected $modifiedTime = null;
732
733 /* Methods */
734
735 public function getScript( ResourceLoaderContext $context ) {
736 global $IP;
737
738 $scripts = file_get_contents( "$IP/resources/startup.js" );
739
740 if ( $context->getOnly() === 'scripts' ) {
741 // Get all module registrations
742 $registration = ResourceLoader::getModuleRegistrations( $context );
743 // Build configuration
744 $config = FormatJson::encode(
745 array( 'server' => $context->getServer(), 'debug' => $context->getDebug() )
746 );
747 // Add a well-known start-up function
748 $scripts .= "window.startUp = function() { $registration mediaWiki.config.set( $config ); };";
749 // Build load query for jquery and mediawiki modules
750 $query = wfArrayToCGI(
751 array(
752 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
753 'only' => 'scripts',
754 'lang' => $context->getLanguage(),
755 'dir' => $context->getDirection(),
756 'skin' => $context->getSkin(),
757 'debug' => $context->getDebug(),
758 'version' => wfTimestamp( TS_ISO_8601, round( max(
759 ResourceLoader::getModule( 'jquery' )->getModifiedTime( $context ),
760 ResourceLoader::getModule( 'mediawiki' )->getModifiedTime( $context )
761 ), -2 ) )
762 )
763 );
764
765 // Build HTML code for loading jquery and mediawiki modules
766 $loadScript = Html::linkedScript( $context->getServer() . "?$query" );
767 // Add code to add jquery and mediawiki loading code; only if the current client is compatible
768 $scripts .= "if ( isCompatible() ) { document.write( '$loadScript' ); }";
769 // Delete the compatible function - it's not needed anymore
770 $scripts .= "delete window['isCompatible'];";
771 }
772
773 return $scripts;
774 }
775
776 public function getModifiedTime( ResourceLoaderContext $context ) {
777 global $IP;
778
779 if ( !is_null( $this->modifiedTime ) ) {
780 return $this->modifiedTime;
781 }
782
783 // HACK getHighestModifiedTime() calls this function, so protect against infinite recursion
784 $this->modifiedTime = filemtime( "$IP/resources/startup.js" );
785 $this->modifiedTime = ResourceLoader::getHighestModifiedTime( $context );
786 return $this->modifiedTime;
787 }
788
789 public function getClientMaxage() {
790 return 300; // 5 minutes
791 }
792
793 public function getServerMaxage() {
794 return 300; // 5 minutes
795 }
796
797 public function getStyles( ResourceLoaderContext $context ) { return array(); }
798
799 public function getFlip( $context ) {
800 global $wgContLang;
801
802 return $wgContLang->getDir() !== $context->getDirection();
803 }
804 public function getMessages() { return array(); }
805 public function getLoaderScript() { return ''; }
806 public function getDependencies() { return array(); }
807 }