r75022 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r75021‎ | r75022 | r75023 >
Date:18:25, 19 October 2010
Author:tparscal
Status:ok
Tags:
Comment:
Part 1 of 2, moving ResourceLoader*Module classes to their own files - this commit copies the ResourceLoaderModules.php file to each of the new files, as to maintain revision history. The next revision will remove non file specific code.
Modified paths:
  • /trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderSiteModule.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderUserModule.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderUserOptionsModule.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderWikiModule.php (added) (history)

Diff [purge]

Index: trunk/phase3/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderSiteModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderUserOptionsModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderUserModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderUserModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderFileModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderStartUpModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderWikiModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native

Status & tagging log