MediaWiki  REL1_23
ResourceLoader.php
Go to the documentation of this file.
00001 <?php
00031 class ResourceLoader {
00032 
00036     protected static $filterCacheVersion = 7;
00040     protected static $requiredSourceProperties = array( 'loadScript' );
00041 
00045     protected $modules = array();
00046 
00050     protected $moduleInfos = array();
00051 
00056     protected $testModuleNames = array();
00057 
00061     protected $sources = array();
00062 
00066     protected $hasErrors = false;
00067 
00082     public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
00083         if ( !count( $modules ) ) {
00084             // Or else Database*::select() will explode, plus it's cheaper!
00085             return;
00086         }
00087         $dbr = wfGetDB( DB_SLAVE );
00088         $skin = $context->getSkin();
00089         $lang = $context->getLanguage();
00090 
00091         // Get file dependency information
00092         $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
00093                 'md_module' => $modules,
00094                 'md_skin' => $skin
00095             ), __METHOD__
00096         );
00097 
00098         // Set modules' dependencies
00099         $modulesWithDeps = array();
00100         foreach ( $res as $row ) {
00101             $module = $this->getModule( $row->md_module );
00102             if ( $module ) {
00103                 $module->setFileDependencies( $skin, FormatJson::decode( $row->md_deps, true ) );
00104                 $modulesWithDeps[] = $row->md_module;
00105             }
00106         }
00107 
00108         // Register the absence of a dependency row too
00109         foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
00110             $module = $this->getModule( $name );
00111             if ( $module ) {
00112                 $this->getModule( $name )->setFileDependencies( $skin, array() );
00113             }
00114         }
00115 
00116         // Get message blob mtimes. Only do this for modules with messages
00117         $modulesWithMessages = array();
00118         foreach ( $modules as $name ) {
00119             $module = $this->getModule( $name );
00120             if ( $module && count( $module->getMessages() ) ) {
00121                 $modulesWithMessages[] = $name;
00122             }
00123         }
00124         $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below
00125         if ( count( $modulesWithMessages ) ) {
00126             $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
00127                     'mr_resource' => $modulesWithMessages,
00128                     'mr_lang' => $lang
00129                 ), __METHOD__
00130             );
00131             foreach ( $res as $row ) {
00132                 $module = $this->getModule( $row->mr_resource );
00133                 if ( $module ) {
00134                     $module->setMsgBlobMtime( $lang, wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
00135                     unset( $modulesWithoutMessages[$row->mr_resource] );
00136                 }
00137             }
00138         }
00139         foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
00140             $module = $this->getModule( $name );
00141             if ( $module ) {
00142                 $module->setMsgBlobMtime( $lang, 0 );
00143             }
00144         }
00145     }
00146 
00162     protected function filter( $filter, $data ) {
00163         global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength;
00164         wfProfileIn( __METHOD__ );
00165 
00166         // For empty/whitespace-only data or for unknown filters, don't perform
00167         // any caching or processing
00168         if ( trim( $data ) === '' || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) {
00169             wfProfileOut( __METHOD__ );
00170             return $data;
00171         }
00172 
00173         // Try for cache hit
00174         // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
00175         $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) );
00176         $cache = wfGetCache( CACHE_ANYTHING );
00177         $cacheEntry = $cache->get( $key );
00178         if ( is_string( $cacheEntry ) ) {
00179             wfIncrStats( "rl-$filter-cache-hits" );
00180             wfProfileOut( __METHOD__ );
00181             return $cacheEntry;
00182         }
00183 
00184         $result = '';
00185         // Run the filter - we've already verified one of these will work
00186         try {
00187             wfIncrStats( "rl-$filter-cache-misses" );
00188             switch ( $filter ) {
00189                 case 'minify-js':
00190                     $result = JavaScriptMinifier::minify( $data,
00191                         $wgResourceLoaderMinifierStatementsOnOwnLine,
00192                         $wgResourceLoaderMinifierMaxLineLength
00193                     );
00194                     $result .= "\n/* cache key: $key */";
00195                     break;
00196                 case 'minify-css':
00197                     $result = CSSMin::minify( $data );
00198                     $result .= "\n/* cache key: $key */";
00199                     break;
00200             }
00201 
00202             // Save filtered text to Memcached
00203             $cache->set( $key, $result );
00204         } catch ( Exception $e ) {
00205             MWExceptionHandler::logException( $e );
00206             wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" );
00207             $this->hasErrors = true;
00208             // Return exception as a comment
00209             $result = self::formatException( $e );
00210         }
00211 
00212         wfProfileOut( __METHOD__ );
00213 
00214         return $result;
00215     }
00216 
00217     /* Methods */
00218 
00222     public function __construct() {
00223         global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
00224 
00225         wfProfileIn( __METHOD__ );
00226 
00227         // Add 'local' source first
00228         $this->addSource( 'local', array( 'loadScript' => $wgLoadScript, 'apiScript' => wfScript( 'api' ) ) );
00229 
00230         // Add other sources
00231         $this->addSource( $wgResourceLoaderSources );
00232 
00233         // Register core modules
00234         $this->register( include "$IP/resources/Resources.php" );
00235         // Register extension modules
00236         wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
00237         $this->register( $wgResourceModules );
00238 
00239         if ( $wgEnableJavaScriptTest === true ) {
00240             $this->registerTestModules();
00241         }
00242 
00243         wfProfileOut( __METHOD__ );
00244     }
00245 
00259     public function register( $name, $info = null ) {
00260         wfProfileIn( __METHOD__ );
00261 
00262         // Allow multiple modules to be registered in one call
00263         $registrations = is_array( $name ) ? $name : array( $name => $info );
00264         foreach ( $registrations as $name => $info ) {
00265             // Disallow duplicate registrations
00266             if ( isset( $this->moduleInfos[$name] ) ) {
00267                 wfProfileOut( __METHOD__ );
00268                 // A module has already been registered by this name
00269                 throw new MWException(
00270                     'ResourceLoader duplicate registration error. ' .
00271                     'Another module has already been registered as ' . $name
00272                 );
00273             }
00274 
00275             // Check $name for validity
00276             if ( !self::isValidModuleName( $name ) ) {
00277                 wfProfileOut( __METHOD__ );
00278                 throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
00279             }
00280 
00281             // Attach module
00282             if ( $info instanceof ResourceLoaderModule ) {
00283                 $this->moduleInfos[$name] = array( 'object' => $info );
00284                 $info->setName( $name );
00285                 $this->modules[$name] = $info;
00286             } elseif ( is_array( $info ) ) {
00287                 // New calling convention
00288                 $this->moduleInfos[$name] = $info;
00289             } else {
00290                 wfProfileOut( __METHOD__ );
00291                 throw new MWException(
00292                     'ResourceLoader module info type error for module \'' . $name .
00293                     '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
00294                 );
00295             }
00296         }
00297 
00298         wfProfileOut( __METHOD__ );
00299     }
00300 
00303     public function registerTestModules() {
00304         global $IP, $wgEnableJavaScriptTest;
00305 
00306         if ( $wgEnableJavaScriptTest !== true ) {
00307             throw new MWException( 'Attempt to register JavaScript test modules but <code>$wgEnableJavaScriptTest</code> is false. Edit your <code>LocalSettings.php</code> to enable it.' );
00308         }
00309 
00310         wfProfileIn( __METHOD__ );
00311 
00312         // Get core test suites
00313         $testModules = array();
00314         $testModules['qunit'] = array();
00315         // Get other test suites (e.g. from extensions)
00316         wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
00317 
00318         // Add the testrunner (which configures QUnit) to the dependencies.
00319         // Since it must be ready before any of the test suites are executed.
00320         foreach ( $testModules['qunit'] as &$module ) {
00321             // Make sure all test modules are top-loading so that when QUnit starts
00322             // on document-ready, it will run once and finish. If some tests arrive
00323             // later (possibly after QUnit has already finished) they will be ignored.
00324             $module['position'] = 'top';
00325             $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
00326         }
00327 
00328         $testModules['qunit'] = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
00329 
00330         foreach ( $testModules as $id => $names ) {
00331             // Register test modules
00332             $this->register( $testModules[$id] );
00333 
00334             // Keep track of their names so that they can be loaded together
00335             $this->testModuleNames[$id] = array_keys( $testModules[$id] );
00336         }
00337 
00338         wfProfileOut( __METHOD__ );
00339     }
00340 
00351     public function addSource( $id, $properties = null ) {
00352         // Allow multiple sources to be registered in one call
00353         if ( is_array( $id ) ) {
00354             foreach ( $id as $key => $value ) {
00355                 $this->addSource( $key, $value );
00356             }
00357             return;
00358         }
00359 
00360         // Disallow duplicates
00361         if ( isset( $this->sources[$id] ) ) {
00362             throw new MWException(
00363                 'ResourceLoader duplicate source addition error. ' .
00364                 'Another source has already been registered as ' . $id
00365             );
00366         }
00367 
00368         // Validate properties
00369         foreach ( self::$requiredSourceProperties as $prop ) {
00370             if ( !isset( $properties[$prop] ) ) {
00371                 throw new MWException( "Required property $prop missing from source ID $id" );
00372             }
00373         }
00374 
00375         $this->sources[$id] = $properties;
00376     }
00377 
00383     public function getModuleNames() {
00384         return array_keys( $this->moduleInfos );
00385     }
00386 
00397     public function getTestModuleNames( $framework = 'all' ) {
00399         if ( $framework == 'all' ) {
00400             return $this->testModuleNames;
00401         } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
00402             return $this->testModuleNames[$framework];
00403         } else {
00404             return array();
00405         }
00406     }
00407 
00419     public function getModule( $name ) {
00420         if ( !isset( $this->modules[$name] ) ) {
00421             if ( !isset( $this->moduleInfos[$name] ) ) {
00422                 // No such module
00423                 return null;
00424             }
00425             // Construct the requested object
00426             $info = $this->moduleInfos[$name];
00428             if ( isset( $info['object'] ) ) {
00429                 // Object given in info array
00430                 $object = $info['object'];
00431             } else {
00432                 if ( !isset( $info['class'] ) ) {
00433                     $class = 'ResourceLoaderFileModule';
00434                 } else {
00435                     $class = $info['class'];
00436                 }
00437                 $object = new $class( $info );
00438             }
00439             $object->setName( $name );
00440             $this->modules[$name] = $object;
00441         }
00442 
00443         return $this->modules[$name];
00444     }
00445 
00451     public function getSources() {
00452         return $this->sources;
00453     }
00454 
00460     public function respond( ResourceLoaderContext $context ) {
00461         global $wgCacheEpoch, $wgUseFileCache;
00462 
00463         // Use file cache if enabled and available...
00464         if ( $wgUseFileCache ) {
00465             $fileCache = ResourceFileCache::newFromContext( $context );
00466             if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) {
00467                 return; // output handled
00468             }
00469         }
00470 
00471         // Buffer output to catch warnings. Normally we'd use ob_clean() on the
00472         // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
00473         // is used: ob_clean() will clear the GZIP header in that case and it won't come
00474         // back for subsequent output, resulting in invalid GZIP. So we have to wrap
00475         // the whole thing in our own output buffer to be sure the active buffer
00476         // doesn't use ob_gzhandler.
00477         // See http://bugs.php.net/bug.php?id=36514
00478         ob_start();
00479 
00480         wfProfileIn( __METHOD__ );
00481         $errors = '';
00482 
00483         // Find out which modules are missing and instantiate the others
00484         $modules = array();
00485         $missing = array();
00486         foreach ( $context->getModules() as $name ) {
00487             $module = $this->getModule( $name );
00488             if ( $module ) {
00489                 // Do not allow private modules to be loaded from the web.
00490                 // This is a security issue, see bug 34907.
00491                 if ( $module->getGroup() === 'private' ) {
00492                     wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" );
00493                     $this->hasErrors = true;
00494                     // Add exception to the output as a comment
00495                     $errors .= self::makeComment( "Cannot show private module \"$name\"" );
00496 
00497                     continue;
00498                 }
00499                 $modules[$name] = $module;
00500             } else {
00501                 $missing[] = $name;
00502             }
00503         }
00504 
00505         // Preload information needed to the mtime calculation below
00506         try {
00507             $this->preloadModuleInfo( array_keys( $modules ), $context );
00508         } catch ( Exception $e ) {
00509             MWExceptionHandler::logException( $e );
00510             wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" );
00511             $this->hasErrors = true;
00512             // Add exception to the output as a comment
00513             $errors .= self::formatException( $e );
00514         }
00515 
00516         wfProfileIn( __METHOD__ . '-getModifiedTime' );
00517 
00518         // To send Last-Modified and support If-Modified-Since, we need to detect
00519         // the last modified time
00520         $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
00521         foreach ( $modules as $module ) {
00525             try {
00526                 // Calculate maximum modified time
00527                 $mtime = max( $mtime, $module->getModifiedTime( $context ) );
00528             } catch ( Exception $e ) {
00529                 MWExceptionHandler::logException( $e );
00530                 wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" );
00531                 $this->hasErrors = true;
00532                 // Add exception to the output as a comment
00533                 $errors .= self::formatException( $e );
00534             }
00535         }
00536 
00537         wfProfileOut( __METHOD__ . '-getModifiedTime' );
00538 
00539         // If there's an If-Modified-Since header, respond with a 304 appropriately
00540         if ( $this->tryRespondLastModified( $context, $mtime ) ) {
00541             wfProfileOut( __METHOD__ );
00542             return; // output handled (buffers cleared)
00543         }
00544 
00545         // Generate a response
00546         $response = $this->makeModuleResponse( $context, $modules, $missing );
00547 
00548         // Prepend comments indicating exceptions
00549         $response = $errors . $response;
00550 
00551         // Capture any PHP warnings from the output buffer and append them to the
00552         // response in a comment if we're in debug mode.
00553         if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00554             $response = self::makeComment( $warnings ) . $response;
00555             $this->hasErrors = true;
00556         }
00557 
00558         // Save response to file cache unless there are errors
00559         if ( isset( $fileCache ) && !$errors && !count( $missing ) ) {
00560             // Cache single modules...and other requests if there are enough hits
00561             if ( ResourceFileCache::useFileCache( $context ) ) {
00562                 if ( $fileCache->isCacheWorthy() ) {
00563                     $fileCache->saveText( $response );
00564                 } else {
00565                     $fileCache->incrMissesRecent( $context->getRequest() );
00566                 }
00567             }
00568         }
00569 
00570         // Send content type and cache related headers
00571         $this->sendResponseHeaders( $context, $mtime, $this->hasErrors );
00572 
00573         // Remove the output buffer and output the response
00574         ob_end_clean();
00575         echo $response;
00576 
00577         wfProfileOut( __METHOD__ );
00578     }
00579 
00587     protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
00588         global $wgResourceLoaderMaxage;
00589         // If a version wasn't specified we need a shorter expiry time for updates
00590         // to propagate to clients quickly
00591         // If there were errors, we also need a shorter expiry time so we can recover quickly
00592         if ( is_null( $context->getVersion() ) || $errors ) {
00593             $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
00594             $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
00595         // If a version was specified we can use a longer expiry time since changing
00596         // version numbers causes cache misses
00597         } else {
00598             $maxage = $wgResourceLoaderMaxage['versioned']['client'];
00599             $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
00600         }
00601         if ( $context->getOnly() === 'styles' ) {
00602             header( 'Content-Type: text/css; charset=utf-8' );
00603             header( 'Access-Control-Allow-Origin: *' );
00604         } else {
00605             header( 'Content-Type: text/javascript; charset=utf-8' );
00606         }
00607         header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
00608         if ( $context->getDebug() ) {
00609             // Do not cache debug responses
00610             header( 'Cache-Control: private, no-cache, must-revalidate' );
00611             header( 'Pragma: no-cache' );
00612         } else {
00613             header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
00614             $exp = min( $maxage, $smaxage );
00615             header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
00616         }
00617     }
00618 
00629     protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
00630         // If there's an If-Modified-Since header, respond with a 304 appropriately
00631         // Some clients send "timestamp;length=123". Strip the part after the first ';'
00632         // so we get a valid timestamp.
00633         $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
00634         // Never send 304s in debug mode
00635         if ( $ims !== false && !$context->getDebug() ) {
00636             $imsTS = strtok( $ims, ';' );
00637             if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
00638                 // There's another bug in ob_gzhandler (see also the comment at
00639                 // the top of this function) that causes it to gzip even empty
00640                 // responses, meaning it's impossible to produce a truly empty
00641                 // response (because the gzip header is always there). This is
00642                 // a problem because 304 responses have to be completely empty
00643                 // per the HTTP spec, and Firefox behaves buggily when they're not.
00644                 // See also http://bugs.php.net/bug.php?id=51579
00645                 // To work around this, we tear down all output buffering before
00646                 // sending the 304.
00647                 wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
00648 
00649                 header( 'HTTP/1.0 304 Not Modified' );
00650                 header( 'Status: 304 Not Modified' );
00651                 return true;
00652             }
00653         }
00654         return false;
00655     }
00656 
00664     protected function tryRespondFromFileCache(
00665         ResourceFileCache $fileCache, ResourceLoaderContext $context
00666     ) {
00667         global $wgResourceLoaderMaxage;
00668         // Buffer output to catch warnings.
00669         ob_start();
00670         // Get the maximum age the cache can be
00671         $maxage = is_null( $context->getVersion() )
00672             ? $wgResourceLoaderMaxage['unversioned']['server']
00673             : $wgResourceLoaderMaxage['versioned']['server'];
00674         // Minimum timestamp the cache file must have
00675         $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
00676         if ( !$good ) {
00677             try { // RL always hits the DB on file cache miss...
00678                 wfGetDB( DB_SLAVE );
00679             } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
00680                 $good = $fileCache->isCacheGood(); // cache existence check
00681             }
00682         }
00683         if ( $good ) {
00684             $ts = $fileCache->cacheTimestamp();
00685             // Send content type and cache headers
00686             $this->sendResponseHeaders( $context, $ts, false );
00687             // If there's an If-Modified-Since header, respond with a 304 appropriately
00688             if ( $this->tryRespondLastModified( $context, $ts ) ) {
00689                 return false; // output handled (buffers cleared)
00690             }
00691             $response = $fileCache->fetchText();
00692             // Capture any PHP warnings from the output buffer and append them to the
00693             // response in a comment if we're in debug mode.
00694             if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00695                 $response = "/*\n$warnings\n*/\n" . $response;
00696             }
00697             // Remove the output buffer and output the response
00698             ob_end_clean();
00699             echo $response . "\n/* Cached {$ts} */";
00700             return true; // cache hit
00701         }
00702         // Clear buffer
00703         ob_end_clean();
00704 
00705         return false; // cache miss
00706     }
00707 
00716     public static function makeComment( $text ) {
00717         $encText = str_replace( '*/', '* /', $text );
00718         return "/*\n$encText\n*/\n";
00719     }
00720 
00727     public static function formatException( $e ) {
00728         global $wgShowExceptionDetails;
00729 
00730         if ( $wgShowExceptionDetails ) {
00731             return self::makeComment( $e->__toString() );
00732         } else {
00733             return self::makeComment( wfMessage( 'internalerror' )->text() );
00734         }
00735     }
00736 
00745     public function makeModuleResponse( ResourceLoaderContext $context,
00746         array $modules, array $missing = array()
00747     ) {
00748         $out = '';
00749         $exceptions = '';
00750         $states = array();
00751 
00752         if ( !count( $modules ) && !count( $missing ) ) {
00753             return "/* This file is the Web entry point for MediaWiki's ResourceLoader:
00754    <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
00755    no modules were requested. Max made me put this here. */";
00756         }
00757 
00758         wfProfileIn( __METHOD__ );
00759 
00760         // Pre-fetch blobs
00761         if ( $context->shouldIncludeMessages() ) {
00762             try {
00763                 $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
00764             } catch ( Exception $e ) {
00765                 MWExceptionHandler::logException( $e );
00766                 wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" );
00767                 $this->hasErrors = true;
00768                 // Add exception to the output as a comment
00769                 $exceptions .= self::formatException( $e );
00770             }
00771         } else {
00772             $blobs = array();
00773         }
00774 
00775         foreach ( $missing as $name ) {
00776             $states[$name] = 'missing';
00777         }
00778 
00779         // Generate output
00780         $isRaw = false;
00781         foreach ( $modules as $name => $module ) {
00786             wfProfileIn( __METHOD__ . '-' . $name );
00787             try {
00788                 $scripts = '';
00789                 if ( $context->shouldIncludeScripts() ) {
00790                     // If we are in debug mode, we'll want to return an array of URLs if possible
00791                     // However, we can't do this if the module doesn't support it
00792                     // We also can't do this if there is an only= parameter, because we have to give
00793                     // the module a way to return a load.php URL without causing an infinite loop
00794                     if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00795                         $scripts = $module->getScriptURLsForDebug( $context );
00796                     } else {
00797                         $scripts = $module->getScript( $context );
00798                         // rtrim() because there are usually a few line breaks after the last ';'.
00799                         // A new line at EOF, a new line added by ResourceLoaderFileModule::readScriptFiles, etc.
00800                         if ( is_string( $scripts ) && strlen( $scripts ) && substr( rtrim( $scripts ), -1 ) !== ';' ) {
00801                             // Append semicolon to prevent weird bugs caused by files not
00802                             // terminating their statements right (bug 27054)
00803                             $scripts .= ";\n";
00804                         }
00805                     }
00806                 }
00807                 // Styles
00808                 $styles = array();
00809                 if ( $context->shouldIncludeStyles() ) {
00810                     // Don't create empty stylesheets like array( '' => '' ) for modules
00811                     // that don't *have* any stylesheets (bug 38024).
00812                     $stylePairs = $module->getStyles( $context );
00813                     if ( count ( $stylePairs ) ) {
00814                         // If we are in debug mode without &only= set, we'll want to return an array of URLs
00815                         // See comment near shouldIncludeScripts() for more details
00816                         if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00817                             $styles = array(
00818                                 'url' => $module->getStyleURLsForDebug( $context )
00819                             );
00820                         } else {
00821                             // Minify CSS before embedding in mw.loader.implement call
00822                             // (unless in debug mode)
00823                             if ( !$context->getDebug() ) {
00824                                 foreach ( $stylePairs as $media => $style ) {
00825                                     // Can be either a string or an array of strings.
00826                                     if ( is_array( $style ) ) {
00827                                         $stylePairs[$media] = array();
00828                                         foreach ( $style as $cssText ) {
00829                                             if ( is_string( $cssText ) ) {
00830                                                 $stylePairs[$media][] = $this->filter( 'minify-css', $cssText );
00831                                             }
00832                                         }
00833                                     } elseif ( is_string( $style ) ) {
00834                                         $stylePairs[$media] = $this->filter( 'minify-css', $style );
00835                                     }
00836                                 }
00837                             }
00838                             // Wrap styles into @media groups as needed and flatten into a numerical array
00839                             $styles = array(
00840                                 'css' => self::makeCombinedStyles( $stylePairs )
00841                             );
00842                         }
00843                     }
00844                 }
00845 
00846                 // Messages
00847                 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
00848 
00849                 // Append output
00850                 switch ( $context->getOnly() ) {
00851                     case 'scripts':
00852                         if ( is_string( $scripts ) ) {
00853                             // Load scripts raw...
00854                             $out .= $scripts;
00855                         } elseif ( is_array( $scripts ) ) {
00856                             // ...except when $scripts is an array of URLs
00857                             $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() );
00858                         }
00859                         break;
00860                     case 'styles':
00861                         // We no longer seperate into media, they are all combined now with
00862                         // custom media type groups into @media .. {} sections as part of the css string.
00863                         // Module returns either an empty array or a numerical array with css strings.
00864                         $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
00865                         break;
00866                     case 'messages':
00867                         $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
00868                         break;
00869                     default:
00870                         $out .= self::makeLoaderImplementScript(
00871                             $name,
00872                             $scripts,
00873                             $styles,
00874                             new XmlJsCode( $messagesBlob )
00875                         );
00876                         break;
00877                 }
00878             } catch ( Exception $e ) {
00879                 MWExceptionHandler::logException( $e );
00880                 wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" );
00881                 $this->hasErrors = true;
00882                 // Add exception to the output as a comment
00883                 $exceptions .= self::formatException( $e );
00884 
00885                 // Respond to client with error-state instead of module implementation
00886                 $states[$name] = 'error';
00887                 unset( $modules[$name] );
00888             }
00889             $isRaw |= $module->isRaw();
00890             wfProfileOut( __METHOD__ . '-' . $name );
00891         }
00892 
00893         // Update module states
00894         if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
00895             if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
00896                 // Set the state of modules loaded as only scripts to ready as
00897                 // they don't have an mw.loader.implement wrapper that sets the state
00898                 foreach ( $modules as $name => $module ) {
00899                     $states[$name] = 'ready';
00900                 }
00901             }
00902 
00903             // Set the state of modules we didn't respond to with mw.loader.implement
00904             if ( count( $states ) ) {
00905                 $out .= self::makeLoaderStateScript( $states );
00906             }
00907         }
00908 
00909         if ( !$context->getDebug() ) {
00910             if ( $context->getOnly() === 'styles' ) {
00911                 $out = $this->filter( 'minify-css', $out );
00912             } else {
00913                 $out = $this->filter( 'minify-js', $out );
00914             }
00915         }
00916 
00917         wfProfileOut( __METHOD__ );
00918         return $exceptions . $out;
00919     }
00920 
00921     /* Static Methods */
00922 
00936     public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
00937         if ( is_string( $scripts ) ) {
00938             $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
00939         } elseif ( !is_array( $scripts ) ) {
00940             throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
00941         }
00942         return Xml::encodeJsCall(
00943             'mw.loader.implement',
00944             array(
00945                 $name,
00946                 $scripts,
00947                 // Force objects. mw.loader.implement requires them to be javascript objects.
00948                 // Although these variables are associative arrays, which become javascript
00949                 // objects through json_encode. In many cases they will be empty arrays, and
00950                 // PHP/json_encode() consider empty arrays to be numerical arrays and
00951                 // output javascript "[]" instead of "{}". This fixes that.
00952                 (object)$styles,
00953                 (object)$messages
00954             ),
00955             ResourceLoader::inDebugMode()
00956         );
00957     }
00958 
00966     public static function makeMessageSetScript( $messages ) {
00967         return Xml::encodeJsCall(
00968             'mw.messages.set',
00969             array( (object)$messages ),
00970             ResourceLoader::inDebugMode()
00971         );
00972     }
00973 
00981     private static function makeCombinedStyles( array $stylePairs ) {
00982         $out = array();
00983         foreach ( $stylePairs as $media => $styles ) {
00984             // ResourceLoaderFileModule::getStyle can return the styles
00985             // as a string or an array of strings. This is to allow separation in
00986             // the front-end.
00987             $styles = (array)$styles;
00988             foreach ( $styles as $style ) {
00989                 $style = trim( $style );
00990                 // Don't output an empty "@media print { }" block (bug 40498)
00991                 if ( $style !== '' ) {
00992                     // Transform the media type based on request params and config
00993                     // The way that this relies on $wgRequest to propagate request params is slightly evil
00994                     $media = OutputPage::transformCssMedia( $media );
00995 
00996                     if ( $media === '' || $media == 'all' ) {
00997                         $out[] = $style;
00998                     } elseif ( is_string( $media ) ) {
00999                         $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
01000                     }
01001                     // else: skip
01002                 }
01003             }
01004         }
01005         return $out;
01006     }
01007 
01022     public static function makeLoaderStateScript( $name, $state = null ) {
01023         if ( is_array( $name ) ) {
01024             return Xml::encodeJsCall(
01025                 'mw.loader.state',
01026                 array( $name ),
01027                 ResourceLoader::inDebugMode()
01028             );
01029         } else {
01030             return Xml::encodeJsCall(
01031                 'mw.loader.state',
01032                 array( $name, $state ),
01033                 ResourceLoader::inDebugMode()
01034             );
01035         }
01036     }
01037 
01052     public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
01053         $script = str_replace( "\n", "\n\t", trim( $script ) );
01054         return Xml::encodeJsCall(
01055             "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
01056             array( $name, $version, $dependencies, $group, $source ),
01057             ResourceLoader::inDebugMode()
01058         );
01059     }
01060 
01085     public static function makeLoaderRegisterScript( $name, $version = null,
01086         $dependencies = null, $group = null, $source = null
01087     ) {
01088         if ( is_array( $name ) ) {
01089             return Xml::encodeJsCall(
01090                 'mw.loader.register',
01091                 array( $name ),
01092                 ResourceLoader::inDebugMode()
01093             );
01094         } else {
01095             $version = (int)$version > 1 ? (int)$version : 1;
01096             return Xml::encodeJsCall(
01097                 'mw.loader.register',
01098                 array( $name, $version, $dependencies, $group, $source ),
01099                 ResourceLoader::inDebugMode()
01100             );
01101         }
01102     }
01103 
01118     public static function makeLoaderSourcesScript( $id, $properties = null ) {
01119         if ( is_array( $id ) ) {
01120             return Xml::encodeJsCall(
01121                 'mw.loader.addSource',
01122                 array( $id ),
01123                 ResourceLoader::inDebugMode()
01124             );
01125         } else {
01126             return Xml::encodeJsCall(
01127                 'mw.loader.addSource',
01128                 array( $id, $properties ),
01129                 ResourceLoader::inDebugMode()
01130             );
01131         }
01132     }
01133 
01141     public static function makeLoaderConditionalScript( $script ) {
01142         return "if(window.mw){\n" . trim( $script ) . "\n}";
01143     }
01144 
01152     public static function makeConfigSetScript( array $configuration ) {
01153         return Xml::encodeJsCall(
01154             'mw.config.set',
01155             array( $configuration ),
01156             ResourceLoader::inDebugMode()
01157         );
01158     }
01159 
01168     public static function makePackedModulesString( $modules ) {
01169         $groups = array(); // array( prefix => array( suffixes ) )
01170         foreach ( $modules as $module ) {
01171             $pos = strrpos( $module, '.' );
01172             $prefix = $pos === false ? '' : substr( $module, 0, $pos );
01173             $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
01174             $groups[$prefix][] = $suffix;
01175         }
01176 
01177         $arr = array();
01178         foreach ( $groups as $prefix => $suffixes ) {
01179             $p = $prefix === '' ? '' : $prefix . '.';
01180             $arr[] = $p . implode( ',', $suffixes );
01181         }
01182         $str = implode( '|', $arr );
01183         return $str;
01184     }
01185 
01191     public static function inDebugMode() {
01192         global $wgRequest, $wgResourceLoaderDebug;
01193         static $retval = null;
01194         if ( is_null( $retval ) ) {
01195             $retval = $wgRequest->getFuzzyBool( 'debug',
01196                 $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
01197         }
01198         return $retval;
01199     }
01200 
01215     public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01216             $printable = false, $handheld = false, $extraQuery = array() ) {
01217         global $wgLoadScript;
01218         $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug,
01219             $only, $printable, $handheld, $extraQuery
01220         );
01221 
01222         // Prevent the IE6 extension check from being triggered (bug 28840)
01223         // by appending a character that's invalid in Windows extensions ('*')
01224         return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE );
01225     }
01226 
01244     public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01245             $printable = false, $handheld = false, $extraQuery = array() ) {
01246         $query = array(
01247             'modules' => self::makePackedModulesString( $modules ),
01248             'lang' => $lang,
01249             'skin' => $skin,
01250             'debug' => $debug ? 'true' : 'false',
01251         );
01252         if ( $user !== null ) {
01253             $query['user'] = $user;
01254         }
01255         if ( $version !== null ) {
01256             $query['version'] = $version;
01257         }
01258         if ( $only !== null ) {
01259             $query['only'] = $only;
01260         }
01261         if ( $printable ) {
01262             $query['printable'] = 1;
01263         }
01264         if ( $handheld ) {
01265             $query['handheld'] = 1;
01266         }
01267         $query += $extraQuery;
01268 
01269         // Make queries uniform in order
01270         ksort( $query );
01271         return $query;
01272     }
01273 
01283     public static function isValidModuleName( $moduleName ) {
01284         return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
01285     }
01286 
01293     public static function getLessCompiler() {
01294         global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths;
01295 
01296         // When called from the installer, it is possible that a required PHP extension
01297         // is missing (at least for now; see bug 47564). If this is the case, throw an
01298         // exception (caught by the installer) to prevent a fatal error later on.
01299         if ( !function_exists( 'ctype_digit' ) ) {
01300             throw new MWException( 'lessc requires the Ctype extension' );
01301         }
01302 
01303         $less = new lessc();
01304         $less->setPreserveComments( true );
01305         $less->setVariables( self::getLESSVars() );
01306         $less->setImportDir( $wgResourceLoaderLESSImportPaths );
01307         foreach ( $wgResourceLoaderLESSFunctions as $name => $func ) {
01308             $less->registerFunction( $name, $func );
01309         }
01310         return $less;
01311     }
01312 
01319     public static function getLESSVars() {
01320         global $wgResourceLoaderLESSVars;
01321 
01322         $lessVars = $wgResourceLoaderLESSVars;
01323         // Sort by key to ensure consistent hashing for cache lookups.
01324         ksort( $lessVars );
01325         return $lessVars;
01326     }
01327 }