MediaWiki  REL1_19
ResourceLoader.php
Go to the documentation of this file.
00001 <?php
00029 class ResourceLoader {
00030 
00031         /* Protected Static Members */
00032         protected static $filterCacheVersion = 7;
00033         protected static $requiredSourceProperties = array( 'loadScript' );
00034 
00036         protected $modules = array();
00037 
00039         protected $moduleInfos = array();
00040  
00043         protected $testModuleNames = array();
00044 
00046         protected $sources = array();
00047 
00048         /* Protected Methods */
00049 
00064         public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
00065                 if ( !count( $modules ) ) {
00066                         return; // or else Database*::select() will explode, plus it's cheaper!
00067                 }
00068                 $dbr = wfGetDB( DB_SLAVE );
00069                 $skin = $context->getSkin();
00070                 $lang = $context->getLanguage();
00071 
00072                 // Get file dependency information
00073                 $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
00074                                 'md_module' => $modules,
00075                                 'md_skin' => $skin
00076                         ), __METHOD__
00077                 );
00078 
00079                 // Set modules' dependencies
00080                 $modulesWithDeps = array();
00081                 foreach ( $res as $row ) {
00082                         $this->getModule( $row->md_module )->setFileDependencies( $skin,
00083                                 FormatJson::decode( $row->md_deps, true )
00084                         );
00085                         $modulesWithDeps[] = $row->md_module;
00086                 }
00087 
00088                 // Register the absence of a dependency row too
00089                 foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
00090                         $this->getModule( $name )->setFileDependencies( $skin, array() );
00091                 }
00092 
00093                 // Get message blob mtimes. Only do this for modules with messages
00094                 $modulesWithMessages = array();
00095                 foreach ( $modules as $name ) {
00096                         if ( count( $this->getModule( $name )->getMessages() ) ) {
00097                                 $modulesWithMessages[] = $name;
00098                         }
00099                 }
00100                 $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below
00101                 if ( count( $modulesWithMessages ) ) {
00102                         $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
00103                                         'mr_resource' => $modulesWithMessages,
00104                                         'mr_lang' => $lang
00105                                 ), __METHOD__
00106                         );
00107                         foreach ( $res as $row ) {
00108                                 $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang,
00109                                         wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
00110                                 unset( $modulesWithoutMessages[$row->mr_resource] );
00111                         }
00112                 }
00113                 foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
00114                         $this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
00115                 }
00116         }
00117 
00132         protected function filter( $filter, $data ) {
00133                 global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength;
00134                 wfProfileIn( __METHOD__ );
00135 
00136                 // For empty/whitespace-only data or for unknown filters, don't perform
00137                 // any caching or processing
00138                 if ( trim( $data ) === ''
00139                         || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) )
00140                 {
00141                         wfProfileOut( __METHOD__ );
00142                         return $data;
00143                 }
00144 
00145                 // Try for cache hit
00146                 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
00147                 $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) );
00148                 $cache = wfGetCache( CACHE_ANYTHING );
00149                 $cacheEntry = $cache->get( $key );
00150                 if ( is_string( $cacheEntry ) ) {
00151                         wfProfileOut( __METHOD__ );
00152                         return $cacheEntry;
00153                 }
00154 
00155                 $result = '';
00156                 // Run the filter - we've already verified one of these will work
00157                 try {
00158                         switch ( $filter ) {
00159                                 case 'minify-js':
00160                                         $result = JavaScriptMinifier::minify( $data,
00161                                                 $wgResourceLoaderMinifierStatementsOnOwnLine,
00162                                                 $wgResourceLoaderMinifierMaxLineLength
00163                                         );
00164                                         $result .= "\n\n/* cache key: $key */\n";
00165                                         break;
00166                                 case 'minify-css':
00167                                         $result = CSSMin::minify( $data );
00168                                         $result .= "\n\n/* cache key: $key */\n";
00169                                         break;
00170                         }
00171 
00172                         // Save filtered text to Memcached
00173                         $cache->set( $key, $result );
00174                 } catch ( Exception $exception ) {
00175                         // Return exception as a comment
00176                         $result = $this->formatException( $exception );
00177                 }
00178 
00179                 wfProfileOut( __METHOD__ );
00180 
00181                 return $result;
00182         }
00183 
00184         /* Methods */
00185 
00189         public function __construct() {
00190                 global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
00191 
00192                 wfProfileIn( __METHOD__ );
00193 
00194                 // Add 'local' source first
00195                 $this->addSource( 'local', array( 'loadScript' => $wgLoadScript, 'apiScript' => wfScript( 'api' ) ) );
00196 
00197                 // Add other sources
00198                 $this->addSource( $wgResourceLoaderSources );
00199 
00200                 // Register core modules
00201                 $this->register( include( "$IP/resources/Resources.php" ) );
00202                 // Register extension modules
00203                 wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
00204                 $this->register( $wgResourceModules );
00205 
00206                 if ( $wgEnableJavaScriptTest === true ) {
00207                         $this->registerTestModules();
00208                 }
00209 
00210 
00211                 wfProfileOut( __METHOD__ );
00212         }
00213 
00227         public function register( $name, $info = null ) {
00228                 wfProfileIn( __METHOD__ );
00229 
00230                 // Allow multiple modules to be registered in one call
00231                 $registrations = is_array( $name ) ? $name : array( $name => $info );
00232                 foreach ( $registrations as $name => $info ) {
00233                         // Disallow duplicate registrations
00234                         if ( isset( $this->moduleInfos[$name] ) ) {
00235                                 // A module has already been registered by this name
00236                                 throw new MWException(
00237                                         'ResourceLoader duplicate registration error. ' .
00238                                         'Another module has already been registered as ' . $name
00239                                 );
00240                         }
00241 
00242                         // Check $name for illegal characters
00243                         if ( preg_match( '/[|,!]/', $name ) ) {
00244                                 throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|), commas (,) or exclamation marks (!)" );
00245                         }
00246 
00247                         // Attach module
00248                         if ( is_object( $info ) ) {
00249                                 // Old calling convention
00250                                 // Validate the input
00251                                 if ( !( $info instanceof ResourceLoaderModule ) ) {
00252                                         throw new MWException( 'ResourceLoader invalid module error. ' .
00253                                                 'Instances of ResourceLoaderModule expected.' );
00254                                 }
00255 
00256                                 $this->moduleInfos[$name] = array( 'object' => $info );
00257                                 $info->setName( $name );
00258                                 $this->modules[$name] = $info;
00259                         } else {
00260                                 // New calling convention
00261                                 $this->moduleInfos[$name] = $info;
00262                         }
00263                 }
00264 
00265                 wfProfileOut( __METHOD__ );
00266         }
00267 
00270         public function registerTestModules() {
00271                 global $IP, $wgEnableJavaScriptTest;
00272 
00273                 if ( $wgEnableJavaScriptTest !== true ) {
00274                         throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
00275                 }
00276 
00277                 wfProfileIn( __METHOD__ );
00278 
00279                 // Get core test suites
00280                 $testModules = array();
00281                 $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
00282                 // Get other test suites (e.g. from extensions)
00283                 wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
00284 
00285                 // Add the testrunner (which configures QUnit) to the dependencies.
00286                 // Since it must be ready before any of the test suites are executed.
00287                 foreach( $testModules['qunit'] as $moduleName => $moduleProps ) {
00288                         $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
00289                 }
00290 
00291                 foreach( $testModules as $id => $names ) {
00292                         // Register test modules
00293                         $this->register( $testModules[$id] );
00294 
00295                         // Keep track of their names so that they can be loaded together
00296                         $this->testModuleNames[$id] = array_keys( $testModules[$id] );
00297                 }
00298 
00299                 wfProfileOut( __METHOD__ );
00300         }
00301 
00311         public function addSource( $id, $properties = null) {
00312                 // Allow multiple sources to be registered in one call
00313                 if ( is_array( $id ) ) {
00314                         foreach ( $id as $key => $value ) {
00315                                 $this->addSource( $key, $value );
00316                         }
00317                         return;
00318                 }
00319 
00320                 // Disallow duplicates
00321                 if ( isset( $this->sources[$id] ) ) {
00322                         throw new MWException(
00323                                 'ResourceLoader duplicate source addition error. ' .
00324                                 'Another source has already been registered as ' . $id
00325                         );
00326                 }
00327 
00328                 // Validate properties
00329                 foreach ( self::$requiredSourceProperties as $prop ) {
00330                         if ( !isset( $properties[$prop] ) ) {
00331                                 throw new MWException( "Required property $prop missing from source ID $id" );
00332                         }
00333                 }
00334 
00335                 $this->sources[$id] = $properties;
00336         }
00337 
00343         public function getModuleNames() {
00344                 return array_keys( $this->moduleInfos );
00345         }
00346  
00356         public function getTestModuleNames( $framework = 'all' ) {
00357                 if ( $framework == 'all' ) {
00358                         return $this->testModuleNames;
00359                 } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
00360                         return $this->testModuleNames[$framework];
00361                 } else {
00362                         return array();
00363                 }
00364         }
00365 
00372         public function getModule( $name ) {
00373                 if ( !isset( $this->modules[$name] ) ) {
00374                         if ( !isset( $this->moduleInfos[$name] ) ) {
00375                                 // No such module
00376                                 return null;
00377                         }
00378                         // Construct the requested object
00379                         $info = $this->moduleInfos[$name];
00380                         if ( isset( $info['object'] ) ) {
00381                                 // Object given in info array
00382                                 $object = $info['object'];
00383                         } else {
00384                                 if ( !isset( $info['class'] ) ) {
00385                                         $class = 'ResourceLoaderFileModule';
00386                                 } else {
00387                                         $class = $info['class'];
00388                                 }
00389                                 $object = new $class( $info );
00390                         }
00391                         $object->setName( $name );
00392                         $this->modules[$name] = $object;
00393                 }
00394 
00395                 return $this->modules[$name];
00396         }
00397 
00403         public function getSources() {
00404                 return $this->sources;
00405         }
00406 
00412         public function respond( ResourceLoaderContext $context ) {
00413                 global $wgCacheEpoch, $wgUseFileCache;
00414 
00415                 // Use file cache if enabled and available...
00416                 if ( $wgUseFileCache ) {
00417                         $fileCache = ResourceFileCache::newFromContext( $context );
00418                         if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) {
00419                                 return; // output handled
00420                         }
00421                 }
00422 
00423                 // Buffer output to catch warnings. Normally we'd use ob_clean() on the
00424                 // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
00425                 // is used: ob_clean() will clear the GZIP header in that case and it won't come
00426                 // back for subsequent output, resulting in invalid GZIP. So we have to wrap
00427                 // the whole thing in our own output buffer to be sure the active buffer
00428                 // doesn't use ob_gzhandler.
00429                 // See http://bugs.php.net/bug.php?id=36514
00430                 ob_start();
00431 
00432                 wfProfileIn( __METHOD__ );
00433                 $errors = '';
00434 
00435                 // Split requested modules into two groups, modules and missing
00436                 $modules = array();
00437                 $missing = array();
00438                 foreach ( $context->getModules() as $name ) {
00439                         if ( isset( $this->moduleInfos[$name] ) ) {
00440                                 $module = $this->getModule( $name );
00441                                 // Do not allow private modules to be loaded from the web.
00442                                 // This is a security issue, see bug 34907.
00443                                 if ( $module->getGroup() === 'private' ) {
00444                                         $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
00445                                         continue;
00446                                 }
00447                                 $modules[$name] = $this->getModule( $name );
00448                         } else {
00449                                 $missing[] = $name;
00450                         }
00451                 }
00452 
00453                 // Preload information needed to the mtime calculation below
00454                 try {
00455                         $this->preloadModuleInfo( array_keys( $modules ), $context );
00456                 } catch( Exception $e ) {
00457                         // Add exception to the output as a comment
00458                         $errors .= $this->formatException( $e );
00459                 }
00460 
00461                 wfProfileIn( __METHOD__.'-getModifiedTime' );
00462 
00463                 // To send Last-Modified and support If-Modified-Since, we need to detect
00464                 // the last modified time
00465                 $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
00466                 foreach ( $modules as $module ) {
00470                         try {
00471                                 // Calculate maximum modified time
00472                                 $mtime = max( $mtime, $module->getModifiedTime( $context ) );
00473                         } catch ( Exception $e ) {
00474                                 // Add exception to the output as a comment
00475                                 $errors .= $this->formatException( $e );
00476                         }
00477                 }
00478 
00479                 wfProfileOut( __METHOD__.'-getModifiedTime' );
00480 
00481                 // Send content type and cache related headers
00482                 $this->sendResponseHeaders( $context, $mtime );
00483 
00484                 // If there's an If-Modified-Since header, respond with a 304 appropriately
00485                 if ( $this->tryRespondLastModified( $context, $mtime ) ) {
00486                         wfProfileOut( __METHOD__ );
00487                         return; // output handled (buffers cleared)
00488                 }
00489 
00490                 // Generate a response
00491                 $response = $this->makeModuleResponse( $context, $modules, $missing );
00492 
00493                 // Prepend comments indicating exceptions
00494                 $response = $errors . $response;
00495 
00496                 // Capture any PHP warnings from the output buffer and append them to the
00497                 // response in a comment if we're in debug mode.
00498                 if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00499                         $response = $this->makeComment( $warnings ) . $response;
00500                 }
00501 
00502                 // Remove the output buffer and output the response
00503                 ob_end_clean();
00504                 echo $response;
00505 
00506                 // Save response to file cache unless there are errors
00507                 if ( isset( $fileCache ) && !$errors && !$missing ) {
00508                         // Cache single modules...and other requests if there are enough hits
00509                         if ( ResourceFileCache::useFileCache( $context ) ) {
00510                                 if ( $fileCache->isCacheWorthy() ) {
00511                                         $fileCache->saveText( $response );
00512                                 } else {
00513                                         $fileCache->incrMissesRecent( $context->getRequest() );
00514                                 }
00515                         }
00516                 }
00517 
00518                 wfProfileOut( __METHOD__ );
00519         }
00520 
00527         protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime ) {
00528                 global $wgResourceLoaderMaxage;
00529                 // If a version wasn't specified we need a shorter expiry time for updates
00530                 // to propagate to clients quickly
00531                 if ( is_null( $context->getVersion() ) ) {
00532                         $maxage  = $wgResourceLoaderMaxage['unversioned']['client'];
00533                         $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
00534                 // If a version was specified we can use a longer expiry time since changing
00535                 // version numbers causes cache misses
00536                 } else {
00537                         $maxage  = $wgResourceLoaderMaxage['versioned']['client'];
00538                         $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
00539                 }
00540                 if ( $context->getOnly() === 'styles' ) {
00541                         header( 'Content-Type: text/css; charset=utf-8' );
00542                 } else {
00543                         header( 'Content-Type: text/javascript; charset=utf-8' );
00544                 }
00545                 header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
00546                 if ( $context->getDebug() ) {
00547                         // Do not cache debug responses
00548                         header( 'Cache-Control: private, no-cache, must-revalidate' );
00549                         header( 'Pragma: no-cache' );
00550                 } else {
00551                         header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
00552                         $exp = min( $maxage, $smaxage );
00553                         header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
00554                 }
00555         }
00556 
00564         protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
00565                 // If there's an If-Modified-Since header, respond with a 304 appropriately
00566                 // Some clients send "timestamp;length=123". Strip the part after the first ';'
00567                 // so we get a valid timestamp.
00568                 $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
00569                 // Never send 304s in debug mode
00570                 if ( $ims !== false && !$context->getDebug() ) {
00571                         $imsTS = strtok( $ims, ';' );
00572                         if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
00573                                 // There's another bug in ob_gzhandler (see also the comment at
00574                                 // the top of this function) that causes it to gzip even empty
00575                                 // responses, meaning it's impossible to produce a truly empty
00576                                 // response (because the gzip header is always there). This is
00577                                 // a problem because 304 responses have to be completely empty
00578                                 // per the HTTP spec, and Firefox behaves buggily when they're not.
00579                                 // See also http://bugs.php.net/bug.php?id=51579
00580                                 // To work around this, we tear down all output buffering before
00581                                 // sending the 304.
00582                                 // On some setups, ob_get_level() doesn't seem to go down to zero
00583                                 // no matter how often we call ob_get_clean(), so instead of doing
00584                                 // the more intuitive while ( ob_get_level() > 0 ) ob_get_clean();
00585                                 // we have to be safe here and avoid an infinite loop.
00586                                 for ( $i = 0; $i < ob_get_level(); $i++ ) {
00587                                         ob_end_clean();
00588                                 }
00589 
00590                                 header( 'HTTP/1.0 304 Not Modified' );
00591                                 header( 'Status: 304 Not Modified' );
00592                                 return true;
00593                         }
00594                 }
00595                 return false;
00596         }
00597 
00605         protected function tryRespondFromFileCache(
00606                 ResourceFileCache $fileCache, ResourceLoaderContext $context
00607         ) {
00608                 global $wgResourceLoaderMaxage;
00609                 // Buffer output to catch warnings.
00610                 ob_start();
00611                 // Get the maximum age the cache can be
00612                 $maxage = is_null( $context->getVersion() )
00613                         ? $wgResourceLoaderMaxage['unversioned']['server']
00614                         : $wgResourceLoaderMaxage['versioned']['server'];
00615                 // Minimum timestamp the cache file must have
00616                 $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
00617                 if ( !$good ) {
00618                         try { // RL always hits the DB on file cache miss...
00619                                 wfGetDB( DB_SLAVE );
00620                         } catch( DBConnectionError $e ) { // ...check if we need to fallback to cache
00621                                 $good = $fileCache->isCacheGood(); // cache existence check
00622                         }
00623                 }
00624                 if ( $good ) {
00625                         $ts = $fileCache->cacheTimestamp();
00626                         // Send content type and cache headers
00627                         $this->sendResponseHeaders( $context, $ts, false );
00628                         // If there's an If-Modified-Since header, respond with a 304 appropriately
00629                         if ( $this->tryRespondLastModified( $context, $ts ) ) {
00630                                 return false; // output handled (buffers cleared)
00631                         }
00632                         $response = $fileCache->fetchText();
00633                         // Capture any PHP warnings from the output buffer and append them to the
00634                         // response in a comment if we're in debug mode.
00635                         if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00636                                 $response = "/*\n$warnings\n*/\n" . $response;
00637                         }
00638                         // Remove the output buffer and output the response
00639                         ob_end_clean();
00640                         echo $response . "\n/* Cached {$ts} */";
00641                         return true; // cache hit
00642                 }
00643                 // Clear buffer
00644                 ob_end_clean();
00645 
00646                 return false; // cache miss
00647         }
00648 
00649         protected function makeComment( $text ) {
00650                 $encText = str_replace( '*/', '* /', $text );
00651                 return "/*\n$encText\n*/\n";
00652         }
00653 
00660         protected function formatException( $e ) {
00661                 global $wgShowExceptionDetails;
00662 
00663                 if ( $wgShowExceptionDetails ) {
00664                         return $this->makeComment( $e->__toString() );
00665                 } else {
00666                         return $this->makeComment( wfMessage( 'internalerror' )->text() );
00667                 }
00668         }
00669 
00678         public function makeModuleResponse( ResourceLoaderContext $context,
00679                 array $modules, $missing = array() )
00680         {
00681                 $out = '';
00682                 $exceptions = '';
00683                 if ( $modules === array() && $missing === array() ) {
00684                         return '/* No modules requested. Max made me put this here */';
00685                 }
00686 
00687                 wfProfileIn( __METHOD__ );
00688                 // Pre-fetch blobs
00689                 if ( $context->shouldIncludeMessages() ) {
00690                         try {
00691                                 $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
00692                         } catch ( Exception $e ) {
00693                                 // Add exception to the output as a comment
00694                                 $exceptions .= $this->formatException( $e );
00695                         }
00696                 } else {
00697                         $blobs = array();
00698                 }
00699 
00700                 // Generate output
00701                 foreach ( $modules as $name => $module ) {
00706                         wfProfileIn( __METHOD__ . '-' . $name );
00707                         try {
00708                                 $scripts = '';
00709                                 if ( $context->shouldIncludeScripts() ) {
00710                                         // If we are in debug mode, we'll want to return an array of URLs if possible
00711                                         // However, we can't do this if the module doesn't support it
00712                                         // We also can't do this if there is an only= parameter, because we have to give
00713                                         // the module a way to return a load.php URL without causing an infinite loop
00714                                         if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00715                                                 $scripts = $module->getScriptURLsForDebug( $context );
00716                                         } else {
00717                                                 $scripts = $module->getScript( $context );
00718                                                 if ( is_string( $scripts ) ) {
00719                                                         // bug 27054: Append semicolon to prevent weird bugs
00720                                                         // caused by files not terminating their statements right
00721                                                         $scripts .= ";\n";
00722                                                 }
00723                                         }
00724                                 }
00725                                 // Styles
00726                                 $styles = array();
00727                                 if ( $context->shouldIncludeStyles() ) {
00728                                         // If we are in debug mode, we'll want to return an array of URLs
00729                                         // See comment near shouldIncludeScripts() for more details
00730                                         if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00731                                                 $styles = $module->getStyleURLsForDebug( $context );
00732                                         } else {
00733                                                 $styles = $module->getStyles( $context );
00734                                         }
00735                                 }
00736 
00737                                 // Messages
00738                                 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
00739 
00740                                 // Append output
00741                                 switch ( $context->getOnly() ) {
00742                                         case 'scripts':
00743                                                 if ( is_string( $scripts ) ) {
00744                                                         // Load scripts raw...
00745                                                         $out .= $scripts;
00746                                                 } elseif ( is_array( $scripts ) ) {
00747                                                         // ...except when $scripts is an array of URLs
00748                                                         $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() );
00749                                                 }
00750                                                 break;
00751                                         case 'styles':
00752                                                 $out .= self::makeCombinedStyles( $styles );
00753                                                 break;
00754                                         case 'messages':
00755                                                 $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
00756                                                 break;
00757                                         default:
00758                                                 // Minify CSS before embedding in mw.loader.implement call
00759                                                 // (unless in debug mode)
00760                                                 if ( !$context->getDebug() ) {
00761                                                         foreach ( $styles as $media => $style ) {
00762                                                                 if ( is_string( $style ) ) {
00763                                                                         $styles[$media] = $this->filter( 'minify-css', $style );
00764                                                                 }
00765                                                         }
00766                                                 }
00767                                                 $out .= self::makeLoaderImplementScript( $name, $scripts, $styles,
00768                                                         new XmlJsCode( $messagesBlob ) );
00769                                                 break;
00770                                 }
00771                         } catch ( Exception $e ) {
00772                                 // Add exception to the output as a comment
00773                                 $exceptions .= $this->formatException( $e );
00774 
00775                                 // Register module as missing
00776                                 $missing[] = $name;
00777                                 unset( $modules[$name] );
00778                         }
00779                         wfProfileOut( __METHOD__ . '-' . $name );
00780                 }
00781 
00782                 // Update module states
00783                 if ( $context->shouldIncludeScripts() ) {
00784                         // Set the state of modules loaded as only scripts to ready
00785                         if ( count( $modules ) && $context->getOnly() === 'scripts'
00786                                 && !isset( $modules['startup'] ) )
00787                         {
00788                                 $out .= self::makeLoaderStateScript(
00789                                         array_fill_keys( array_keys( $modules ), 'ready' ) );
00790                         }
00791                         // Set the state of modules which were requested but unavailable as missing
00792                         if ( is_array( $missing ) && count( $missing ) ) {
00793                                 $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
00794                         }
00795                 }
00796 
00797                 if ( !$context->getDebug() ) {
00798                         if ( $context->getOnly() === 'styles' ) {
00799                                 $out = $this->filter( 'minify-css', $out );
00800                         } else {
00801                                 $out = $this->filter( 'minify-js', $out );
00802                         }
00803                 }
00804 
00805                 wfProfileOut( __METHOD__ );
00806                 return $exceptions . $out;
00807         }
00808 
00809         /* Static Methods */
00810 
00825         public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
00826                 if ( is_string( $scripts ) ) {
00827                         $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" );
00828                 } elseif ( !is_array( $scripts ) ) {
00829                         throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
00830                 }
00831                 return Xml::encodeJsCall(
00832                         'mw.loader.implement',
00833                         array(
00834                                 $name,
00835                                 $scripts,
00836                                 (object)$styles,
00837                                 (object)$messages
00838                         ) );
00839         }
00840 
00849         public static function makeMessageSetScript( $messages ) {
00850                 return Xml::encodeJsCall( 'mw.messages.set', array( (object)$messages ) );
00851         }
00852 
00861         public static function makeCombinedStyles( array $styles ) {
00862                 $out = '';
00863                 foreach ( $styles as $media => $style ) {
00864                         // Transform the media type based on request params and config
00865                         // The way that this relies on $wgRequest to propagate request params is slightly evil
00866                         $media = OutputPage::transformCssMedia( $media );
00867 
00868                         if ( $media === null ) {
00869                                 // Skip
00870                         } elseif ( $media === '' || $media == 'all' ) {
00871                                 // Don't output invalid or frivolous @media statements
00872                                 $out .= "$style\n";
00873                         } else {
00874                                 $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n";
00875                         }
00876                 }
00877                 return $out;
00878         }
00879 
00895         public static function makeLoaderStateScript( $name, $state = null ) {
00896                 if ( is_array( $name ) ) {
00897                         return Xml::encodeJsCall( 'mw.loader.state', array( $name ) );
00898                 } else {
00899                         return Xml::encodeJsCall( 'mw.loader.state', array( $name, $state ) );
00900                 }
00901         }
00902 
00918         public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
00919                 $script = str_replace( "\n", "\n\t", trim( $script ) );
00920                 return Xml::encodeJsCall(
00921                         "( function( name, version, dependencies, group, source ) {\n\t$script\n} )",
00922                         array( $name, $version, $dependencies, $group, $source ) );
00923         }
00924 
00950         public static function makeLoaderRegisterScript( $name, $version = null,
00951                 $dependencies = null, $group = null, $source = null )
00952         {
00953                 if ( is_array( $name ) ) {
00954                         return Xml::encodeJsCall( 'mw.loader.register', array( $name ) );
00955                 } else {
00956                         $version = (int) $version > 1 ? (int) $version : 1;
00957                         return Xml::encodeJsCall( 'mw.loader.register',
00958                                 array( $name, $version, $dependencies, $group, $source ) );
00959                 }
00960         }
00961 
00977         public static function makeLoaderSourcesScript( $id, $properties = null ) {
00978                 if ( is_array( $id ) ) {
00979                         return Xml::encodeJsCall( 'mw.loader.addSource', array( $id ) );
00980                 } else {
00981                         return Xml::encodeJsCall( 'mw.loader.addSource', array( $id, $properties ) );
00982                 }
00983         }
00984 
00993         public static function makeLoaderConditionalScript( $script ) {
00994                 return "if(window.mw){\n".trim( $script )."\n}";
00995         }
00996 
01005         public static function makeConfigSetScript( array $configuration ) {
01006                 return Xml::encodeJsCall( 'mw.config.set', array( $configuration ) );
01007         }
01008 
01017         public static function makePackedModulesString( $modules ) {
01018                 $groups = array(); // array( prefix => array( suffixes ) )
01019                 foreach ( $modules as $module ) {
01020                         $pos = strrpos( $module, '.' );
01021                         $prefix = $pos === false ? '' : substr( $module, 0, $pos );
01022                         $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
01023                         $groups[$prefix][] = $suffix;
01024                 }
01025 
01026                 $arr = array();
01027                 foreach ( $groups as $prefix => $suffixes ) {
01028                         $p = $prefix === '' ? '' : $prefix . '.';
01029                         $arr[] = $p . implode( ',', $suffixes );
01030                 }
01031                 $str = implode( '|', $arr );
01032                 return $str;
01033         }
01034 
01040         public static function inDebugMode() {
01041                 global $wgRequest, $wgResourceLoaderDebug;
01042                 static $retval = null;
01043                 if ( !is_null( $retval ) ) {
01044                         return $retval;
01045                 }
01046                 return $retval = $wgRequest->getFuzzyBool( 'debug',
01047                         $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
01048         }
01049 
01064         public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01065                         $printable = false, $handheld = false, $extraQuery = array() ) {
01066                 global $wgLoadScript;
01067                 $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug,
01068                         $only, $printable, $handheld, $extraQuery
01069                 );
01070 
01071                 // Prevent the IE6 extension check from being triggered (bug 28840)
01072                 // by appending a character that's invalid in Windows extensions ('*')
01073                 return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE );
01074         }
01075 
01081         public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01082                         $printable = false, $handheld = false, $extraQuery = array() ) {
01083                 $query = array(
01084                         'modules' => self::makePackedModulesString( $modules ),
01085                         'lang' => $lang,
01086                         'skin' => $skin,
01087                         'debug' => $debug ? 'true' : 'false',
01088                 );
01089                 if ( $user !== null ) {
01090                         $query['user'] = $user;
01091                 }
01092                 if ( $version !== null ) {
01093                         $query['version'] = $version;
01094                 }
01095                 if ( $only !== null ) {
01096                         $query['only'] = $only;
01097                 }
01098                 if ( $printable ) {
01099                         $query['printable'] = 1;
01100                 }
01101                 if ( $handheld ) {
01102                         $query['handheld'] = 1;
01103                 }
01104                 $query += $extraQuery;
01105 
01106                 // Make queries uniform in order
01107                 ksort( $query );
01108                 return $query;
01109         }
01110 }