MediaWiki  REL1_20
ResourceLoader.php
Go to the documentation of this file.
00001 <?php
00031 class ResourceLoader {
00032 
00033         /* Protected Static Members */
00034         protected static $filterCacheVersion = 7;
00035         protected static $requiredSourceProperties = array( 'loadScript' );
00036 
00038         protected $modules = array();
00039 
00041         protected $moduleInfos = array();
00042  
00045         protected $testModuleNames = array();
00046 
00048         protected $sources = array();
00049 
00050         /* Protected Methods */
00051 
00066         public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
00067                 if ( !count( $modules ) ) {
00068                         return; // or else Database*::select() will explode, plus it's cheaper!
00069                 }
00070                 $dbr = wfGetDB( DB_SLAVE );
00071                 $skin = $context->getSkin();
00072                 $lang = $context->getLanguage();
00073 
00074                 // Get file dependency information
00075                 $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
00076                                 'md_module' => $modules,
00077                                 'md_skin' => $skin
00078                         ), __METHOD__
00079                 );
00080 
00081                 // Set modules' dependencies
00082                 $modulesWithDeps = array();
00083                 foreach ( $res as $row ) {
00084                         $this->getModule( $row->md_module )->setFileDependencies( $skin,
00085                                 FormatJson::decode( $row->md_deps, true )
00086                         );
00087                         $modulesWithDeps[] = $row->md_module;
00088                 }
00089 
00090                 // Register the absence of a dependency row too
00091                 foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
00092                         $this->getModule( $name )->setFileDependencies( $skin, array() );
00093                 }
00094 
00095                 // Get message blob mtimes. Only do this for modules with messages
00096                 $modulesWithMessages = array();
00097                 foreach ( $modules as $name ) {
00098                         if ( count( $this->getModule( $name )->getMessages() ) ) {
00099                                 $modulesWithMessages[] = $name;
00100                         }
00101                 }
00102                 $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below
00103                 if ( count( $modulesWithMessages ) ) {
00104                         $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
00105                                         'mr_resource' => $modulesWithMessages,
00106                                         'mr_lang' => $lang
00107                                 ), __METHOD__
00108                         );
00109                         foreach ( $res as $row ) {
00110                                 $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang,
00111                                         wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
00112                                 unset( $modulesWithoutMessages[$row->mr_resource] );
00113                         }
00114                 }
00115                 foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
00116                         $this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
00117                 }
00118         }
00119 
00134         protected function filter( $filter, $data ) {
00135                 global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength;
00136                 wfProfileIn( __METHOD__ );
00137 
00138                 // For empty/whitespace-only data or for unknown filters, don't perform
00139                 // any caching or processing
00140                 if ( trim( $data ) === ''
00141                         || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) )
00142                 {
00143                         wfProfileOut( __METHOD__ );
00144                         return $data;
00145                 }
00146 
00147                 // Try for cache hit
00148                 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
00149                 $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) );
00150                 $cache = wfGetCache( CACHE_ANYTHING );
00151                 $cacheEntry = $cache->get( $key );
00152                 if ( is_string( $cacheEntry ) ) {
00153                         wfProfileOut( __METHOD__ );
00154                         return $cacheEntry;
00155                 }
00156 
00157                 $result = '';
00158                 // Run the filter - we've already verified one of these will work
00159                 try {
00160                         switch ( $filter ) {
00161                                 case 'minify-js':
00162                                         $result = JavaScriptMinifier::minify( $data,
00163                                                 $wgResourceLoaderMinifierStatementsOnOwnLine,
00164                                                 $wgResourceLoaderMinifierMaxLineLength
00165                                         );
00166                                         $result .= "\n/* cache key: $key */";
00167                                         break;
00168                                 case 'minify-css':
00169                                         $result = CSSMin::minify( $data );
00170                                         $result .= "\n/* cache key: $key */";
00171                                         break;
00172                         }
00173 
00174                         // Save filtered text to Memcached
00175                         $cache->set( $key, $result );
00176                 } catch ( Exception $exception ) {
00177                         // Return exception as a comment
00178                         $result = $this->formatException( $exception );
00179                         $this->hasErrors = true;
00180                 }
00181 
00182                 wfProfileOut( __METHOD__ );
00183 
00184                 return $result;
00185         }
00186 
00187         /* Methods */
00188 
00192         public function __construct() {
00193                 global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
00194 
00195                 wfProfileIn( __METHOD__ );
00196 
00197                 // Add 'local' source first
00198                 $this->addSource( 'local', array( 'loadScript' => $wgLoadScript, 'apiScript' => wfScript( 'api' ) ) );
00199 
00200                 // Add other sources
00201                 $this->addSource( $wgResourceLoaderSources );
00202 
00203                 // Register core modules
00204                 $this->register( include( "$IP/resources/Resources.php" ) );
00205                 // Register extension modules
00206                 wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
00207                 $this->register( $wgResourceModules );
00208 
00209                 if ( $wgEnableJavaScriptTest === true ) {
00210                         $this->registerTestModules();
00211                 }
00212 
00213 
00214                 wfProfileOut( __METHOD__ );
00215         }
00216 
00230         public function register( $name, $info = null ) {
00231                 wfProfileIn( __METHOD__ );
00232 
00233                 // Allow multiple modules to be registered in one call
00234                 $registrations = is_array( $name ) ? $name : array( $name => $info );
00235                 foreach ( $registrations as $name => $info ) {
00236                         // Disallow duplicate registrations
00237                         if ( isset( $this->moduleInfos[$name] ) ) {
00238                                 // A module has already been registered by this name
00239                                 throw new MWException(
00240                                         'ResourceLoader duplicate registration error. ' .
00241                                         'Another module has already been registered as ' . $name
00242                                 );
00243                         }
00244 
00245                         // Check $name for validity
00246                         if ( !self::isValidModuleName( $name ) ) {
00247                                 throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
00248                         }
00249 
00250                         // Attach module
00251                         if ( is_object( $info ) ) {
00252                                 // Old calling convention
00253                                 // Validate the input
00254                                 if ( !( $info instanceof ResourceLoaderModule ) ) {
00255                                         throw new MWException( 'ResourceLoader invalid module error. ' .
00256                                                 'Instances of ResourceLoaderModule expected.' );
00257                                 }
00258 
00259                                 $this->moduleInfos[$name] = array( 'object' => $info );
00260                                 $info->setName( $name );
00261                                 $this->modules[$name] = $info;
00262                         } else {
00263                                 // New calling convention
00264                                 $this->moduleInfos[$name] = $info;
00265                         }
00266                 }
00267 
00268                 wfProfileOut( __METHOD__ );
00269         }
00270 
00273         public function registerTestModules() {
00274                 global $IP, $wgEnableJavaScriptTest;
00275 
00276                 if ( $wgEnableJavaScriptTest !== true ) {
00277                         throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
00278                 }
00279 
00280                 wfProfileIn( __METHOD__ );
00281 
00282                 // Get core test suites
00283                 $testModules = array();
00284                 $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
00285                 // Get other test suites (e.g. from extensions)
00286                 wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
00287 
00288                 // Add the testrunner (which configures QUnit) to the dependencies.
00289                 // Since it must be ready before any of the test suites are executed.
00290                 foreach( $testModules['qunit'] as $moduleName => $moduleProps ) {
00291                         $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
00292                 }
00293 
00294                 foreach( $testModules as $id => $names ) {
00295                         // Register test modules
00296                         $this->register( $testModules[$id] );
00297 
00298                         // Keep track of their names so that they can be loaded together
00299                         $this->testModuleNames[$id] = array_keys( $testModules[$id] );
00300                 }
00301 
00302                 wfProfileOut( __METHOD__ );
00303         }
00304 
00314         public function addSource( $id, $properties = null) {
00315                 // Allow multiple sources to be registered in one call
00316                 if ( is_array( $id ) ) {
00317                         foreach ( $id as $key => $value ) {
00318                                 $this->addSource( $key, $value );
00319                         }
00320                         return;
00321                 }
00322 
00323                 // Disallow duplicates
00324                 if ( isset( $this->sources[$id] ) ) {
00325                         throw new MWException(
00326                                 'ResourceLoader duplicate source addition error. ' .
00327                                 'Another source has already been registered as ' . $id
00328                         );
00329                 }
00330 
00331                 // Validate properties
00332                 foreach ( self::$requiredSourceProperties as $prop ) {
00333                         if ( !isset( $properties[$prop] ) ) {
00334                                 throw new MWException( "Required property $prop missing from source ID $id" );
00335                         }
00336                 }
00337 
00338                 $this->sources[$id] = $properties;
00339         }
00340 
00346         public function getModuleNames() {
00347                 return array_keys( $this->moduleInfos );
00348         }
00349  
00359         public function getTestModuleNames( $framework = 'all' ) {
00361                 if ( $framework == 'all' ) {
00362                         return $this->testModuleNames;
00363                 } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
00364                         return $this->testModuleNames[$framework];
00365                 } else {
00366                         return array();
00367                 }
00368         }
00369 
00376         public function getModule( $name ) {
00377                 if ( !isset( $this->modules[$name] ) ) {
00378                         if ( !isset( $this->moduleInfos[$name] ) ) {
00379                                 // No such module
00380                                 return null;
00381                         }
00382                         // Construct the requested object
00383                         $info = $this->moduleInfos[$name];
00384                         if ( isset( $info['object'] ) ) {
00385                                 // Object given in info array
00386                                 $object = $info['object'];
00387                         } else {
00388                                 if ( !isset( $info['class'] ) ) {
00389                                         $class = 'ResourceLoaderFileModule';
00390                                 } else {
00391                                         $class = $info['class'];
00392                                 }
00393                                 $object = new $class( $info );
00394                         }
00395                         $object->setName( $name );
00396                         $this->modules[$name] = $object;
00397                 }
00398 
00399                 return $this->modules[$name];
00400         }
00401 
00407         public function getSources() {
00408                 return $this->sources;
00409         }
00410 
00416         public function respond( ResourceLoaderContext $context ) {
00417                 global $wgCacheEpoch, $wgUseFileCache;
00418 
00419                 // Use file cache if enabled and available...
00420                 if ( $wgUseFileCache ) {
00421                         $fileCache = ResourceFileCache::newFromContext( $context );
00422                         if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) {
00423                                 return; // output handled
00424                         }
00425                 }
00426 
00427                 // Buffer output to catch warnings. Normally we'd use ob_clean() on the
00428                 // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
00429                 // is used: ob_clean() will clear the GZIP header in that case and it won't come
00430                 // back for subsequent output, resulting in invalid GZIP. So we have to wrap
00431                 // the whole thing in our own output buffer to be sure the active buffer
00432                 // doesn't use ob_gzhandler.
00433                 // See http://bugs.php.net/bug.php?id=36514
00434                 ob_start();
00435 
00436                 wfProfileIn( __METHOD__ );
00437                 $errors = '';
00438                 $this->hasErrors = false;
00439 
00440                 // Split requested modules into two groups, modules and missing
00441                 $modules = array();
00442                 $missing = array();
00443                 foreach ( $context->getModules() as $name ) {
00444                         if ( isset( $this->moduleInfos[$name] ) ) {
00445                                 $module = $this->getModule( $name );
00446                                 // Do not allow private modules to be loaded from the web.
00447                                 // This is a security issue, see bug 34907.
00448                                 if ( $module->getGroup() === 'private' ) {
00449                                         $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
00450                                         $this->hasErrors = true;
00451                                         continue;
00452                                 }
00453                                 $modules[$name] = $this->getModule( $name );
00454                         } else {
00455                                 $missing[] = $name;
00456                         }
00457                 }
00458 
00459                 // Preload information needed to the mtime calculation below
00460                 try {
00461                         $this->preloadModuleInfo( array_keys( $modules ), $context );
00462                 } catch( Exception $e ) {
00463                         // Add exception to the output as a comment
00464                         $errors .= $this->formatException( $e );
00465                         $this->hasErrors = true;
00466                 }
00467 
00468                 wfProfileIn( __METHOD__.'-getModifiedTime' );
00469 
00470                 // To send Last-Modified and support If-Modified-Since, we need to detect
00471                 // the last modified time
00472                 $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
00473                 foreach ( $modules as $module ) {
00477                         try {
00478                                 // Calculate maximum modified time
00479                                 $mtime = max( $mtime, $module->getModifiedTime( $context ) );
00480                         } catch ( Exception $e ) {
00481                                 // Add exception to the output as a comment
00482                                 $errors .= $this->formatException( $e );
00483                                 $this->hasErrors = true;
00484                         }
00485                 }
00486 
00487                 wfProfileOut( __METHOD__.'-getModifiedTime' );
00488 
00489                 // If there's an If-Modified-Since header, respond with a 304 appropriately
00490                 if ( $this->tryRespondLastModified( $context, $mtime ) ) {
00491                         wfProfileOut( __METHOD__ );
00492                         return; // output handled (buffers cleared)
00493                 }
00494 
00495                 // Generate a response
00496                 $response = $this->makeModuleResponse( $context, $modules, $missing );
00497 
00498                 // Prepend comments indicating exceptions
00499                 $response = $errors . $response;
00500 
00501                 // Capture any PHP warnings from the output buffer and append them to the
00502                 // response in a comment if we're in debug mode.
00503                 if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00504                         $response = $this->makeComment( $warnings ) . $response;
00505                         $this->hasErrors = true;
00506                 }
00507 
00508                 // Save response to file cache unless there are errors
00509                 if ( isset( $fileCache ) && !$errors && !$missing ) {
00510                         // Cache single modules...and other requests if there are enough hits
00511                         if ( ResourceFileCache::useFileCache( $context ) ) {
00512                                 if ( $fileCache->isCacheWorthy() ) {
00513                                         $fileCache->saveText( $response );
00514                                 } else {
00515                                         $fileCache->incrMissesRecent( $context->getRequest() );
00516                                 }
00517                         }
00518                 }
00519 
00520                 // Send content type and cache related headers
00521                 $this->sendResponseHeaders( $context, $mtime, $this->hasErrors );
00522 
00523                 // Remove the output buffer and output the response
00524                 ob_end_clean();
00525                 echo $response;
00526 
00527                 wfProfileOut( __METHOD__ );
00528         }
00529 
00537         protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
00538                 global $wgResourceLoaderMaxage;
00539                 // If a version wasn't specified we need a shorter expiry time for updates
00540                 // to propagate to clients quickly
00541                 // If there were errors, we also need a shorter expiry time so we can recover quickly
00542                 if ( is_null( $context->getVersion() ) || $errors ) {
00543                         $maxage  = $wgResourceLoaderMaxage['unversioned']['client'];
00544                         $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
00545                 // If a version was specified we can use a longer expiry time since changing
00546                 // version numbers causes cache misses
00547                 } else {
00548                         $maxage  = $wgResourceLoaderMaxage['versioned']['client'];
00549                         $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
00550                 }
00551                 if ( $context->getOnly() === 'styles' ) {
00552                         header( 'Content-Type: text/css; charset=utf-8' );
00553                 } else {
00554                         header( 'Content-Type: text/javascript; charset=utf-8' );
00555                 }
00556                 header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
00557                 if ( $context->getDebug() ) {
00558                         // Do not cache debug responses
00559                         header( 'Cache-Control: private, no-cache, must-revalidate' );
00560                         header( 'Pragma: no-cache' );
00561                 } else {
00562                         header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
00563                         $exp = min( $maxage, $smaxage );
00564                         header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
00565                 }
00566         }
00567 
00575         protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
00576                 // If there's an If-Modified-Since header, respond with a 304 appropriately
00577                 // Some clients send "timestamp;length=123". Strip the part after the first ';'
00578                 // so we get a valid timestamp.
00579                 $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
00580                 // Never send 304s in debug mode
00581                 if ( $ims !== false && !$context->getDebug() ) {
00582                         $imsTS = strtok( $ims, ';' );
00583                         if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
00584                                 // There's another bug in ob_gzhandler (see also the comment at
00585                                 // the top of this function) that causes it to gzip even empty
00586                                 // responses, meaning it's impossible to produce a truly empty
00587                                 // response (because the gzip header is always there). This is
00588                                 // a problem because 304 responses have to be completely empty
00589                                 // per the HTTP spec, and Firefox behaves buggily when they're not.
00590                                 // See also http://bugs.php.net/bug.php?id=51579
00591                                 // To work around this, we tear down all output buffering before
00592                                 // sending the 304.
00593                                 // On some setups, ob_get_level() doesn't seem to go down to zero
00594                                 // no matter how often we call ob_get_clean(), so instead of doing
00595                                 // the more intuitive while ( ob_get_level() > 0 ) ob_get_clean();
00596                                 // we have to be safe here and avoid an infinite loop.
00597                                 for ( $i = 0; $i < ob_get_level(); $i++ ) {
00598                                         ob_end_clean();
00599                                 }
00600 
00601                                 header( 'HTTP/1.0 304 Not Modified' );
00602                                 header( 'Status: 304 Not Modified' );
00603                                 return true;
00604                         }
00605                 }
00606                 return false;
00607         }
00608 
00616         protected function tryRespondFromFileCache(
00617                 ResourceFileCache $fileCache, ResourceLoaderContext $context
00618         ) {
00619                 global $wgResourceLoaderMaxage;
00620                 // Buffer output to catch warnings.
00621                 ob_start();
00622                 // Get the maximum age the cache can be
00623                 $maxage = is_null( $context->getVersion() )
00624                         ? $wgResourceLoaderMaxage['unversioned']['server']
00625                         : $wgResourceLoaderMaxage['versioned']['server'];
00626                 // Minimum timestamp the cache file must have
00627                 $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
00628                 if ( !$good ) {
00629                         try { // RL always hits the DB on file cache miss...
00630                                 wfGetDB( DB_SLAVE );
00631                         } catch( DBConnectionError $e ) { // ...check if we need to fallback to cache
00632                                 $good = $fileCache->isCacheGood(); // cache existence check
00633                         }
00634                 }
00635                 if ( $good ) {
00636                         $ts = $fileCache->cacheTimestamp();
00637                         // Send content type and cache headers
00638                         $this->sendResponseHeaders( $context, $ts, false );
00639                         // If there's an If-Modified-Since header, respond with a 304 appropriately
00640                         if ( $this->tryRespondLastModified( $context, $ts ) ) {
00641                                 return false; // output handled (buffers cleared)
00642                         }
00643                         $response = $fileCache->fetchText();
00644                         // Capture any PHP warnings from the output buffer and append them to the
00645                         // response in a comment if we're in debug mode.
00646                         if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00647                                 $response = "/*\n$warnings\n*/\n" . $response;
00648                         }
00649                         // Remove the output buffer and output the response
00650                         ob_end_clean();
00651                         echo $response . "\n/* Cached {$ts} */";
00652                         return true; // cache hit
00653                 }
00654                 // Clear buffer
00655                 ob_end_clean();
00656 
00657                 return false; // cache miss
00658         }
00659 
00660         protected function makeComment( $text ) {
00661                 $encText = str_replace( '*/', '* /', $text );
00662                 return "/*\n$encText\n*/\n";
00663         }
00664 
00671         protected function formatException( $e ) {
00672                 global $wgShowExceptionDetails;
00673 
00674                 if ( $wgShowExceptionDetails ) {
00675                         return $this->makeComment( $e->__toString() );
00676                 } else {
00677                         return $this->makeComment( wfMessage( 'internalerror' )->text() );
00678                 }
00679         }
00680 
00689         public function makeModuleResponse( ResourceLoaderContext $context,
00690                 array $modules, $missing = array() )
00691         {
00692                 $out = '';
00693                 $exceptions = '';
00694                 if ( $modules === array() && $missing === array() ) {
00695                         return '/* No modules requested. Max made me put this here */';
00696                 }
00697 
00698                 wfProfileIn( __METHOD__ );
00699                 // Pre-fetch blobs
00700                 if ( $context->shouldIncludeMessages() ) {
00701                         try {
00702                                 $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
00703                         } catch ( Exception $e ) {
00704                                 // Add exception to the output as a comment
00705                                 $exceptions .= $this->formatException( $e );
00706                                 $this->hasErrors = true;
00707                         }
00708                 } else {
00709                         $blobs = array();
00710                 }
00711 
00712                 // Generate output
00713                 $isRaw = false;
00714                 foreach ( $modules as $name => $module ) {
00719                         wfProfileIn( __METHOD__ . '-' . $name );
00720                         try {
00721                                 $scripts = '';
00722                                 if ( $context->shouldIncludeScripts() ) {
00723                                         // If we are in debug mode, we'll want to return an array of URLs if possible
00724                                         // However, we can't do this if the module doesn't support it
00725                                         // We also can't do this if there is an only= parameter, because we have to give
00726                                         // the module a way to return a load.php URL without causing an infinite loop
00727                                         if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00728                                                 $scripts = $module->getScriptURLsForDebug( $context );
00729                                         } else {
00730                                                 $scripts = $module->getScript( $context );
00731                                                 if ( is_string( $scripts ) && strlen( $scripts ) && substr( $scripts, -1 ) !== ';' ) {
00732                                                         // bug 27054: Append semicolon to prevent weird bugs
00733                                                         // caused by files not terminating their statements right
00734                                                         $scripts .= ";\n";
00735                                                 }
00736                                         }
00737                                 }
00738                                 // Styles
00739                                 $styles = array();
00740                                 if ( $context->shouldIncludeStyles() ) {
00741                                         // Don't create empty stylesheets like array( '' => '' ) for modules
00742                                         // that don't *have* any stylesheets (bug 38024).
00743                                         $stylePairs = $module->getStyles( $context );
00744                                         if ( count ( $stylePairs ) ) {
00745                                                 // If we are in debug mode without &only= set, we'll want to return an array of URLs
00746                                                 // See comment near shouldIncludeScripts() for more details
00747                                                 if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00748                                                         $styles = array(
00749                                                                 'url' => $module->getStyleURLsForDebug( $context )
00750                                                         );
00751                                                 } else {
00752                                                         // Minify CSS before embedding in mw.loader.implement call
00753                                                         // (unless in debug mode)
00754                                                         if ( !$context->getDebug() ) {
00755                                                                 foreach ( $stylePairs as $media => $style ) {
00756                                                                         // Can be either a string or an array of strings.
00757                                                                         if ( is_array( $style ) ) {
00758                                                                                 $stylePairs[$media] = array();
00759                                                                                 foreach ( $style as $cssText ) {
00760                                                                                         if ( is_string( $cssText ) ) {
00761                                                                                                 $stylePairs[$media][] = $this->filter( 'minify-css', $cssText );
00762                                                                                         }
00763                                                                                 }
00764                                                                         } elseif ( is_string( $style ) ) {
00765                                                                                 $stylePairs[$media] = $this->filter( 'minify-css', $style );
00766                                                                         }
00767                                                                 }
00768                                                         }
00769                                                         // Wrap styles into @media groups as needed and flatten into a numerical array
00770                                                         $styles = array(
00771                                                                 'css' => self::makeCombinedStyles( $stylePairs )
00772                                                         );
00773                                                 }
00774                                         }
00775                                 }
00776 
00777                                 // Messages
00778                                 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
00779 
00780                                 // Append output
00781                                 switch ( $context->getOnly() ) {
00782                                         case 'scripts':
00783                                                 if ( is_string( $scripts ) ) {
00784                                                         // Load scripts raw...
00785                                                         $out .= $scripts;
00786                                                 } elseif ( is_array( $scripts ) ) {
00787                                                         // ...except when $scripts is an array of URLs
00788                                                         $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() );
00789                                                 }
00790                                                 break;
00791                                         case 'styles':
00792                                                 // We no longer seperate into media, they are all combined now with
00793                                                 // custom media type groups into @media .. {} sections as part of the css string.
00794                                                 // Module returns either an empty array or a numerical array with css strings.
00795                                                 $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
00796                                                 break;
00797                                         case 'messages':
00798                                                 $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
00799                                                 break;
00800                                         default:
00801                                                 $out .= self::makeLoaderImplementScript(
00802                                                         $name,
00803                                                         $scripts,
00804                                                         $styles,
00805                                                         new XmlJsCode( $messagesBlob )
00806                                                 );
00807                                                 break;
00808                                 }
00809                         } catch ( Exception $e ) {
00810                                 // Add exception to the output as a comment
00811                                 $exceptions .= $this->formatException( $e );
00812                                 $this->hasErrors = true;
00813 
00814                                 // Register module as missing
00815                                 $missing[] = $name;
00816                                 unset( $modules[$name] );
00817                         }
00818                         $isRaw |= $module->isRaw();
00819                         wfProfileOut( __METHOD__ . '-' . $name );
00820                 }
00821 
00822                 // Update module states
00823                 if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
00824                         // Set the state of modules loaded as only scripts to ready
00825                         if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
00826                                 $out .= self::makeLoaderStateScript(
00827                                         array_fill_keys( array_keys( $modules ), 'ready' ) );
00828                         }
00829                         // Set the state of modules which were requested but unavailable as missing
00830                         if ( is_array( $missing ) && count( $missing ) ) {
00831                                 $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
00832                         }
00833                 }
00834 
00835                 if ( !$context->getDebug() ) {
00836                         if ( $context->getOnly() === 'styles' ) {
00837                                 $out = $this->filter( 'minify-css', $out );
00838                         } else {
00839                                 $out = $this->filter( 'minify-js', $out );
00840                         }
00841                 }
00842 
00843                 wfProfileOut( __METHOD__ );
00844                 return $exceptions . $out;
00845         }
00846 
00847         /* Static Methods */
00848 
00863         public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
00864                 if ( is_string( $scripts ) ) {
00865                         $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" );
00866                 } elseif ( !is_array( $scripts ) ) {
00867                         throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
00868                 }
00869                 return Xml::encodeJsCall(
00870                         'mw.loader.implement',
00871                         array(
00872                                 $name,
00873                                 $scripts,
00874                                 // Force objects. mw.loader.implement requires them to be javascript objects.
00875                                 // Although these variables are associative arrays, which become javascript
00876                                 // objects through json_encode. In many cases they will be empty arrays, and
00877                                 // PHP/json_encode() consider empty arrays to be numerical arrays and
00878                                 // output javascript "[]" instead of "{}". This fixes that.
00879                                 (object)$styles,
00880                                 (object)$messages
00881                         ) );
00882         }
00883 
00892         public static function makeMessageSetScript( $messages ) {
00893                 return Xml::encodeJsCall( 'mw.messages.set', array( (object)$messages ) );
00894         }
00895 
00904         private static function makeCombinedStyles( array $stylePairs ) {
00905                 $out = array();
00906                 foreach ( $stylePairs as $media => $styles ) {
00907                         // ResourceLoaderFileModule::getStyle can return the styles
00908                         // as a string or an array of strings. This is to allow separation in
00909                         // the front-end.
00910                         $styles = (array) $styles;
00911                         foreach ( $styles as $style ) {
00912                                 $style = trim( $style );
00913                                 // Don't output an empty "@media print { }" block (bug 40498)
00914                                 if ( $style !== '' ) {
00915                                         // Transform the media type based on request params and config
00916                                         // The way that this relies on $wgRequest to propagate request params is slightly evil
00917                                         $media = OutputPage::transformCssMedia( $media );
00918 
00919                                         if ( $media === '' || $media == 'all' ) {
00920                                                 $out[] = $style;
00921                                         } else if ( is_string( $media ) ) {
00922                                                 $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
00923                                         }
00924                                         // else: skip
00925                                 }
00926                         }
00927                 }
00928                 return $out;
00929         }
00930 
00946         public static function makeLoaderStateScript( $name, $state = null ) {
00947                 if ( is_array( $name ) ) {
00948                         return Xml::encodeJsCall( 'mw.loader.state', array( $name ) );
00949                 } else {
00950                         return Xml::encodeJsCall( 'mw.loader.state', array( $name, $state ) );
00951                 }
00952         }
00953 
00969         public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
00970                 $script = str_replace( "\n", "\n\t", trim( $script ) );
00971                 return Xml::encodeJsCall(
00972                         "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
00973                         array( $name, $version, $dependencies, $group, $source ) );
00974         }
00975 
01001         public static function makeLoaderRegisterScript( $name, $version = null,
01002                 $dependencies = null, $group = null, $source = null )
01003         {
01004                 if ( is_array( $name ) ) {
01005                         return Xml::encodeJsCall( 'mw.loader.register', array( $name ) );
01006                 } else {
01007                         $version = (int) $version > 1 ? (int) $version : 1;
01008                         return Xml::encodeJsCall( 'mw.loader.register',
01009                                 array( $name, $version, $dependencies, $group, $source ) );
01010                 }
01011         }
01012 
01028         public static function makeLoaderSourcesScript( $id, $properties = null ) {
01029                 if ( is_array( $id ) ) {
01030                         return Xml::encodeJsCall( 'mw.loader.addSource', array( $id ) );
01031                 } else {
01032                         return Xml::encodeJsCall( 'mw.loader.addSource', array( $id, $properties ) );
01033                 }
01034         }
01035 
01044         public static function makeLoaderConditionalScript( $script ) {
01045                 return "if(window.mw){\n" . trim( $script ) . "\n}";
01046         }
01047 
01056         public static function makeConfigSetScript( array $configuration ) {
01057                 return Xml::encodeJsCall( 'mw.config.set', array( $configuration ) );
01058         }
01059 
01068         public static function makePackedModulesString( $modules ) {
01069                 $groups = array(); // array( prefix => array( suffixes ) )
01070                 foreach ( $modules as $module ) {
01071                         $pos = strrpos( $module, '.' );
01072                         $prefix = $pos === false ? '' : substr( $module, 0, $pos );
01073                         $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
01074                         $groups[$prefix][] = $suffix;
01075                 }
01076 
01077                 $arr = array();
01078                 foreach ( $groups as $prefix => $suffixes ) {
01079                         $p = $prefix === '' ? '' : $prefix . '.';
01080                         $arr[] = $p . implode( ',', $suffixes );
01081                 }
01082                 $str = implode( '|', $arr );
01083                 return $str;
01084         }
01085 
01091         public static function inDebugMode() {
01092                 global $wgRequest, $wgResourceLoaderDebug;
01093                 static $retval = null;
01094                 if ( !is_null( $retval ) ) {
01095                         return $retval;
01096                 }
01097                 return $retval = $wgRequest->getFuzzyBool( 'debug',
01098                         $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
01099         }
01100 
01115         public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01116                         $printable = false, $handheld = false, $extraQuery = array() ) {
01117                 global $wgLoadScript;
01118                 $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug,
01119                         $only, $printable, $handheld, $extraQuery
01120                 );
01121 
01122                 // Prevent the IE6 extension check from being triggered (bug 28840)
01123                 // by appending a character that's invalid in Windows extensions ('*')
01124                 return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE );
01125         }
01126 
01132         public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01133                         $printable = false, $handheld = false, $extraQuery = array() ) {
01134                 $query = array(
01135                         'modules' => self::makePackedModulesString( $modules ),
01136                         'lang' => $lang,
01137                         'skin' => $skin,
01138                         'debug' => $debug ? 'true' : 'false',
01139                 );
01140                 if ( $user !== null ) {
01141                         $query['user'] = $user;
01142                 }
01143                 if ( $version !== null ) {
01144                         $query['version'] = $version;
01145                 }
01146                 if ( $only !== null ) {
01147                         $query['only'] = $only;
01148                 }
01149                 if ( $printable ) {
01150                         $query['printable'] = 1;
01151                 }
01152                 if ( $handheld ) {
01153                         $query['handheld'] = 1;
01154                 }
01155                 $query += $extraQuery;
01156 
01157                 // Make queries uniform in order
01158                 ksort( $query );
01159                 return $query;
01160         }
01161 
01171         public static function isValidModuleName( $moduleName ) {
01172                 return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
01173         }
01174 }