MediaWiki  REL1_22
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 
00051     protected $hasErrors = false;
00052 
00053     /* Protected Methods */
00054 
00069     public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
00070         if ( !count( $modules ) ) {
00071             return; // or else Database*::select() will explode, plus it's cheaper!
00072         }
00073         $dbr = wfGetDB( DB_SLAVE );
00074         $skin = $context->getSkin();
00075         $lang = $context->getLanguage();
00076 
00077         // Get file dependency information
00078         $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
00079                 'md_module' => $modules,
00080                 'md_skin' => $skin
00081             ), __METHOD__
00082         );
00083 
00084         // Set modules' dependencies
00085         $modulesWithDeps = array();
00086         foreach ( $res as $row ) {
00087             $this->getModule( $row->md_module )->setFileDependencies( $skin,
00088                 FormatJson::decode( $row->md_deps, true )
00089             );
00090             $modulesWithDeps[] = $row->md_module;
00091         }
00092 
00093         // Register the absence of a dependency row too
00094         foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
00095             $this->getModule( $name )->setFileDependencies( $skin, array() );
00096         }
00097 
00098         // Get message blob mtimes. Only do this for modules with messages
00099         $modulesWithMessages = array();
00100         foreach ( $modules as $name ) {
00101             if ( count( $this->getModule( $name )->getMessages() ) ) {
00102                 $modulesWithMessages[] = $name;
00103             }
00104         }
00105         $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below
00106         if ( count( $modulesWithMessages ) ) {
00107             $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
00108                     'mr_resource' => $modulesWithMessages,
00109                     'mr_lang' => $lang
00110                 ), __METHOD__
00111             );
00112             foreach ( $res as $row ) {
00113                 $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang,
00114                     wfTimestamp( TS_UNIX, $row->mr_timestamp ) );
00115                 unset( $modulesWithoutMessages[$row->mr_resource] );
00116             }
00117         }
00118         foreach ( array_keys( $modulesWithoutMessages ) as $name ) {
00119             $this->getModule( $name )->setMsgBlobMtime( $lang, 0 );
00120         }
00121     }
00122 
00137     protected function filter( $filter, $data ) {
00138         global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength;
00139         wfProfileIn( __METHOD__ );
00140 
00141         // For empty/whitespace-only data or for unknown filters, don't perform
00142         // any caching or processing
00143         if ( trim( $data ) === ''
00144             || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) )
00145         {
00146             wfProfileOut( __METHOD__ );
00147             return $data;
00148         }
00149 
00150         // Try for cache hit
00151         // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
00152         $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) );
00153         $cache = wfGetCache( CACHE_ANYTHING );
00154         $cacheEntry = $cache->get( $key );
00155         if ( is_string( $cacheEntry ) ) {
00156             wfIncrStats( "rl-$filter-cache-hits" );
00157             wfProfileOut( __METHOD__ );
00158             return $cacheEntry;
00159         }
00160 
00161         $result = '';
00162         // Run the filter - we've already verified one of these will work
00163         try {
00164             wfIncrStats( "rl-$filter-cache-misses" );
00165             switch ( $filter ) {
00166                 case 'minify-js':
00167                     $result = JavaScriptMinifier::minify( $data,
00168                         $wgResourceLoaderMinifierStatementsOnOwnLine,
00169                         $wgResourceLoaderMinifierMaxLineLength
00170                     );
00171                     $result .= "\n/* cache key: $key */";
00172                     break;
00173                 case 'minify-css':
00174                     $result = CSSMin::minify( $data );
00175                     $result .= "\n/* cache key: $key */";
00176                     break;
00177             }
00178 
00179             // Save filtered text to Memcached
00180             $cache->set( $key, $result );
00181         } catch ( Exception $e ) {
00182             MWExceptionHandler::logException( $e );
00183             wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" );
00184             $this->hasErrors = true;
00185             // Return exception as a comment
00186             $result = self::formatException( $e );
00187         }
00188 
00189         wfProfileOut( __METHOD__ );
00190 
00191         return $result;
00192     }
00193 
00194     /* Methods */
00195 
00199     public function __construct() {
00200         global $IP, $wgResourceModules, $wgResourceLoaderSources, $wgLoadScript, $wgEnableJavaScriptTest;
00201 
00202         wfProfileIn( __METHOD__ );
00203 
00204         // Add 'local' source first
00205         $this->addSource( 'local', array( 'loadScript' => $wgLoadScript, 'apiScript' => wfScript( 'api' ) ) );
00206 
00207         // Add other sources
00208         $this->addSource( $wgResourceLoaderSources );
00209 
00210         // Register core modules
00211         $this->register( include "$IP/resources/Resources.php" );
00212         // Register extension modules
00213         wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
00214         $this->register( $wgResourceModules );
00215 
00216         if ( $wgEnableJavaScriptTest === true ) {
00217             $this->registerTestModules();
00218         }
00219 
00220         wfProfileOut( __METHOD__ );
00221     }
00222 
00236     public function register( $name, $info = null ) {
00237         wfProfileIn( __METHOD__ );
00238 
00239         // Allow multiple modules to be registered in one call
00240         $registrations = is_array( $name ) ? $name : array( $name => $info );
00241         foreach ( $registrations as $name => $info ) {
00242             // Disallow duplicate registrations
00243             if ( isset( $this->moduleInfos[$name] ) ) {
00244                 wfProfileOut( __METHOD__ );
00245                 // A module has already been registered by this name
00246                 throw new MWException(
00247                     'ResourceLoader duplicate registration error. ' .
00248                     'Another module has already been registered as ' . $name
00249                 );
00250             }
00251 
00252             // Check $name for validity
00253             if ( !self::isValidModuleName( $name ) ) {
00254                 wfProfileOut( __METHOD__ );
00255                 throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
00256             }
00257 
00258             // Attach module
00259             if ( is_object( $info ) ) {
00260                 // Old calling convention
00261                 // Validate the input
00262                 if ( !( $info instanceof ResourceLoaderModule ) ) {
00263                     wfProfileOut( __METHOD__ );
00264                     throw new MWException( 'ResourceLoader invalid module error. ' .
00265                         'Instances of ResourceLoaderModule expected.' );
00266                 }
00267 
00268                 $this->moduleInfos[$name] = array( 'object' => $info );
00269                 $info->setName( $name );
00270                 $this->modules[$name] = $info;
00271             } else {
00272                 // New calling convention
00273                 $this->moduleInfos[$name] = $info;
00274             }
00275         }
00276 
00277         wfProfileOut( __METHOD__ );
00278     }
00279 
00282     public function registerTestModules() {
00283         global $IP, $wgEnableJavaScriptTest;
00284 
00285         if ( $wgEnableJavaScriptTest !== true ) {
00286             throw new MWException( 'Attempt to register JavaScript test modules but <code>$wgEnableJavaScriptTest</code> is false. Edit your <code>LocalSettings.php</code> to enable it.' );
00287         }
00288 
00289         wfProfileIn( __METHOD__ );
00290 
00291         // Get core test suites
00292         $testModules = array();
00293         $testModules['qunit'] = include "$IP/tests/qunit/QUnitTestResources.php";
00294         // Get other test suites (e.g. from extensions)
00295         wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
00296 
00297         // Add the testrunner (which configures QUnit) to the dependencies.
00298         // Since it must be ready before any of the test suites are executed.
00299         foreach ( $testModules['qunit'] as &$module ) {
00300             // Make sure all test modules are top-loading so that when QUnit starts
00301             // on document-ready, it will run once and finish. If some tests arrive
00302             // later (possibly after QUnit has already finished) they will be ignored.
00303             $module['position'] = 'top';
00304             $module['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
00305         }
00306 
00307         foreach ( $testModules as $id => $names ) {
00308             // Register test modules
00309             $this->register( $testModules[$id] );
00310 
00311             // Keep track of their names so that they can be loaded together
00312             $this->testModuleNames[$id] = array_keys( $testModules[$id] );
00313         }
00314 
00315         wfProfileOut( __METHOD__ );
00316     }
00317 
00328     public function addSource( $id, $properties = null ) {
00329         // Allow multiple sources to be registered in one call
00330         if ( is_array( $id ) ) {
00331             foreach ( $id as $key => $value ) {
00332                 $this->addSource( $key, $value );
00333             }
00334             return;
00335         }
00336 
00337         // Disallow duplicates
00338         if ( isset( $this->sources[$id] ) ) {
00339             throw new MWException(
00340                 'ResourceLoader duplicate source addition error. ' .
00341                 'Another source has already been registered as ' . $id
00342             );
00343         }
00344 
00345         // Validate properties
00346         foreach ( self::$requiredSourceProperties as $prop ) {
00347             if ( !isset( $properties[$prop] ) ) {
00348                 throw new MWException( "Required property $prop missing from source ID $id" );
00349             }
00350         }
00351 
00352         $this->sources[$id] = $properties;
00353     }
00354 
00360     public function getModuleNames() {
00361         return array_keys( $this->moduleInfos );
00362     }
00363 
00373     public function getTestModuleNames( $framework = 'all' ) {
00375         if ( $framework == 'all' ) {
00376             return $this->testModuleNames;
00377         } elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
00378             return $this->testModuleNames[$framework];
00379         } else {
00380             return array();
00381         }
00382     }
00383 
00390     public function getModule( $name ) {
00391         if ( !isset( $this->modules[$name] ) ) {
00392             if ( !isset( $this->moduleInfos[$name] ) ) {
00393                 // No such module
00394                 return null;
00395             }
00396             // Construct the requested object
00397             $info = $this->moduleInfos[$name];
00399             if ( isset( $info['object'] ) ) {
00400                 // Object given in info array
00401                 $object = $info['object'];
00402             } else {
00403                 if ( !isset( $info['class'] ) ) {
00404                     $class = 'ResourceLoaderFileModule';
00405                 } else {
00406                     $class = $info['class'];
00407                 }
00408                 $object = new $class( $info );
00409             }
00410             $object->setName( $name );
00411             $this->modules[$name] = $object;
00412         }
00413 
00414         return $this->modules[$name];
00415     }
00416 
00422     public function getSources() {
00423         return $this->sources;
00424     }
00425 
00431     public function respond( ResourceLoaderContext $context ) {
00432         global $wgCacheEpoch, $wgUseFileCache;
00433 
00434         // Use file cache if enabled and available...
00435         if ( $wgUseFileCache ) {
00436             $fileCache = ResourceFileCache::newFromContext( $context );
00437             if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) {
00438                 return; // output handled
00439             }
00440         }
00441 
00442         // Buffer output to catch warnings. Normally we'd use ob_clean() on the
00443         // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
00444         // is used: ob_clean() will clear the GZIP header in that case and it won't come
00445         // back for subsequent output, resulting in invalid GZIP. So we have to wrap
00446         // the whole thing in our own output buffer to be sure the active buffer
00447         // doesn't use ob_gzhandler.
00448         // See http://bugs.php.net/bug.php?id=36514
00449         ob_start();
00450 
00451         wfProfileIn( __METHOD__ );
00452         $errors = '';
00453 
00454         // Split requested modules into two groups, modules and missing
00455         $modules = array();
00456         $missing = array();
00457         foreach ( $context->getModules() as $name ) {
00458             if ( isset( $this->moduleInfos[$name] ) ) {
00459                 $module = $this->getModule( $name );
00460                 // Do not allow private modules to be loaded from the web.
00461                 // This is a security issue, see bug 34907.
00462                 if ( $module->getGroup() === 'private' ) {
00463                     wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" );
00464                     $this->hasErrors = true;
00465                     // Add exception to the output as a comment
00466                     $errors .= self::makeComment( "Cannot show private module \"$name\"" );
00467 
00468                     continue;
00469                 }
00470                 $modules[$name] = $module;
00471             } else {
00472                 $missing[] = $name;
00473             }
00474         }
00475 
00476         // Preload information needed to the mtime calculation below
00477         try {
00478             $this->preloadModuleInfo( array_keys( $modules ), $context );
00479         } catch ( Exception $e ) {
00480             MWExceptionHandler::logException( $e );
00481             wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" );
00482             $this->hasErrors = true;
00483             // Add exception to the output as a comment
00484             $errors .= self::formatException( $e );
00485         }
00486 
00487         wfProfileIn( __METHOD__ . '-getModifiedTime' );
00488 
00489         // To send Last-Modified and support If-Modified-Since, we need to detect
00490         // the last modified time
00491         $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
00492         foreach ( $modules as $module ) {
00496             try {
00497                 // Calculate maximum modified time
00498                 $mtime = max( $mtime, $module->getModifiedTime( $context ) );
00499             } catch ( Exception $e ) {
00500                 MWExceptionHandler::logException( $e );
00501                 wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" );
00502                 $this->hasErrors = true;
00503                 // Add exception to the output as a comment
00504                 $errors .= self::formatException( $e );
00505             }
00506         }
00507 
00508         wfProfileOut( __METHOD__ . '-getModifiedTime' );
00509 
00510         // If there's an If-Modified-Since header, respond with a 304 appropriately
00511         if ( $this->tryRespondLastModified( $context, $mtime ) ) {
00512             wfProfileOut( __METHOD__ );
00513             return; // output handled (buffers cleared)
00514         }
00515 
00516         // Generate a response
00517         $response = $this->makeModuleResponse( $context, $modules, $missing );
00518 
00519         // Prepend comments indicating exceptions
00520         $response = $errors . $response;
00521 
00522         // Capture any PHP warnings from the output buffer and append them to the
00523         // response in a comment if we're in debug mode.
00524         if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00525             $response = self::makeComment( $warnings ) . $response;
00526             $this->hasErrors = true;
00527         }
00528 
00529         // Save response to file cache unless there are errors
00530         if ( isset( $fileCache ) && !$errors && !$missing ) {
00531             // Cache single modules...and other requests if there are enough hits
00532             if ( ResourceFileCache::useFileCache( $context ) ) {
00533                 if ( $fileCache->isCacheWorthy() ) {
00534                     $fileCache->saveText( $response );
00535                 } else {
00536                     $fileCache->incrMissesRecent( $context->getRequest() );
00537                 }
00538             }
00539         }
00540 
00541         // Send content type and cache related headers
00542         $this->sendResponseHeaders( $context, $mtime, $this->hasErrors );
00543 
00544         // Remove the output buffer and output the response
00545         ob_end_clean();
00546         echo $response;
00547 
00548         wfProfileOut( __METHOD__ );
00549     }
00550 
00558     protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
00559         global $wgResourceLoaderMaxage;
00560         // If a version wasn't specified we need a shorter expiry time for updates
00561         // to propagate to clients quickly
00562         // If there were errors, we also need a shorter expiry time so we can recover quickly
00563         if ( is_null( $context->getVersion() ) || $errors ) {
00564             $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
00565             $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
00566         // If a version was specified we can use a longer expiry time since changing
00567         // version numbers causes cache misses
00568         } else {
00569             $maxage = $wgResourceLoaderMaxage['versioned']['client'];
00570             $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
00571         }
00572         if ( $context->getOnly() === 'styles' ) {
00573             header( 'Content-Type: text/css; charset=utf-8' );
00574             header( 'Access-Control-Allow-Origin: *' );
00575         } else {
00576             header( 'Content-Type: text/javascript; charset=utf-8' );
00577         }
00578         header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
00579         if ( $context->getDebug() ) {
00580             // Do not cache debug responses
00581             header( 'Cache-Control: private, no-cache, must-revalidate' );
00582             header( 'Pragma: no-cache' );
00583         } else {
00584             header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
00585             $exp = min( $maxage, $smaxage );
00586             header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
00587         }
00588     }
00589 
00597     protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
00598         // If there's an If-Modified-Since header, respond with a 304 appropriately
00599         // Some clients send "timestamp;length=123". Strip the part after the first ';'
00600         // so we get a valid timestamp.
00601         $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
00602         // Never send 304s in debug mode
00603         if ( $ims !== false && !$context->getDebug() ) {
00604             $imsTS = strtok( $ims, ';' );
00605             if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
00606                 // There's another bug in ob_gzhandler (see also the comment at
00607                 // the top of this function) that causes it to gzip even empty
00608                 // responses, meaning it's impossible to produce a truly empty
00609                 // response (because the gzip header is always there). This is
00610                 // a problem because 304 responses have to be completely empty
00611                 // per the HTTP spec, and Firefox behaves buggily when they're not.
00612                 // See also http://bugs.php.net/bug.php?id=51579
00613                 // To work around this, we tear down all output buffering before
00614                 // sending the 304.
00615                 wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
00616 
00617                 header( 'HTTP/1.0 304 Not Modified' );
00618                 header( 'Status: 304 Not Modified' );
00619                 return true;
00620             }
00621         }
00622         return false;
00623     }
00624 
00632     protected function tryRespondFromFileCache(
00633         ResourceFileCache $fileCache, ResourceLoaderContext $context
00634     ) {
00635         global $wgResourceLoaderMaxage;
00636         // Buffer output to catch warnings.
00637         ob_start();
00638         // Get the maximum age the cache can be
00639         $maxage = is_null( $context->getVersion() )
00640             ? $wgResourceLoaderMaxage['unversioned']['server']
00641             : $wgResourceLoaderMaxage['versioned']['server'];
00642         // Minimum timestamp the cache file must have
00643         $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
00644         if ( !$good ) {
00645             try { // RL always hits the DB on file cache miss...
00646                 wfGetDB( DB_SLAVE );
00647             } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
00648                 $good = $fileCache->isCacheGood(); // cache existence check
00649             }
00650         }
00651         if ( $good ) {
00652             $ts = $fileCache->cacheTimestamp();
00653             // Send content type and cache headers
00654             $this->sendResponseHeaders( $context, $ts, false );
00655             // If there's an If-Modified-Since header, respond with a 304 appropriately
00656             if ( $this->tryRespondLastModified( $context, $ts ) ) {
00657                 return false; // output handled (buffers cleared)
00658             }
00659             $response = $fileCache->fetchText();
00660             // Capture any PHP warnings from the output buffer and append them to the
00661             // response in a comment if we're in debug mode.
00662             if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
00663                 $response = "/*\n$warnings\n*/\n" . $response;
00664             }
00665             // Remove the output buffer and output the response
00666             ob_end_clean();
00667             echo $response . "\n/* Cached {$ts} */";
00668             return true; // cache hit
00669         }
00670         // Clear buffer
00671         ob_end_clean();
00672 
00673         return false; // cache miss
00674     }
00675 
00683     public static function makeComment( $text ) {
00684         $encText = str_replace( '*/', '* /', $text );
00685         return "/*\n$encText\n*/\n";
00686     }
00687 
00694     public static function formatException( $e ) {
00695         global $wgShowExceptionDetails;
00696 
00697         if ( $wgShowExceptionDetails ) {
00698             return self::makeComment( $e->__toString() );
00699         } else {
00700             return self::makeComment( wfMessage( 'internalerror' )->text() );
00701         }
00702     }
00703 
00712     public function makeModuleResponse( ResourceLoaderContext $context,
00713         array $modules, $missing = array()
00714     ) {
00715         $out = '';
00716         $exceptions = '';
00717         if ( $modules === array() && $missing === array() ) {
00718             return '/* No modules requested. Max made me put this here */';
00719         }
00720 
00721         wfProfileIn( __METHOD__ );
00722         // Pre-fetch blobs
00723         if ( $context->shouldIncludeMessages() ) {
00724             try {
00725                 $blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
00726             } catch ( Exception $e ) {
00727                 MWExceptionHandler::logException( $e );
00728                 wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" );
00729                 $this->hasErrors = true;
00730                 // Add exception to the output as a comment
00731                 $exceptions .= self::formatException( $e );
00732             }
00733         } else {
00734             $blobs = array();
00735         }
00736 
00737         // Generate output
00738         $isRaw = false;
00739         foreach ( $modules as $name => $module ) {
00744             wfProfileIn( __METHOD__ . '-' . $name );
00745             try {
00746                 $scripts = '';
00747                 if ( $context->shouldIncludeScripts() ) {
00748                     // If we are in debug mode, we'll want to return an array of URLs if possible
00749                     // However, we can't do this if the module doesn't support it
00750                     // We also can't do this if there is an only= parameter, because we have to give
00751                     // the module a way to return a load.php URL without causing an infinite loop
00752                     if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00753                         $scripts = $module->getScriptURLsForDebug( $context );
00754                     } else {
00755                         $scripts = $module->getScript( $context );
00756                         if ( is_string( $scripts ) && strlen( $scripts ) && substr( $scripts, -1 ) !== ';' ) {
00757                             // bug 27054: Append semicolon to prevent weird bugs
00758                             // caused by files not terminating their statements right
00759                             $scripts .= ";\n";
00760                         }
00761                     }
00762                 }
00763                 // Styles
00764                 $styles = array();
00765                 if ( $context->shouldIncludeStyles() ) {
00766                     // Don't create empty stylesheets like array( '' => '' ) for modules
00767                     // that don't *have* any stylesheets (bug 38024).
00768                     $stylePairs = $module->getStyles( $context );
00769                     if ( count ( $stylePairs ) ) {
00770                         // If we are in debug mode without &only= set, we'll want to return an array of URLs
00771                         // See comment near shouldIncludeScripts() for more details
00772                         if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) {
00773                             $styles = array(
00774                                 'url' => $module->getStyleURLsForDebug( $context )
00775                             );
00776                         } else {
00777                             // Minify CSS before embedding in mw.loader.implement call
00778                             // (unless in debug mode)
00779                             if ( !$context->getDebug() ) {
00780                                 foreach ( $stylePairs as $media => $style ) {
00781                                     // Can be either a string or an array of strings.
00782                                     if ( is_array( $style ) ) {
00783                                         $stylePairs[$media] = array();
00784                                         foreach ( $style as $cssText ) {
00785                                             if ( is_string( $cssText ) ) {
00786                                                 $stylePairs[$media][] = $this->filter( 'minify-css', $cssText );
00787                                             }
00788                                         }
00789                                     } elseif ( is_string( $style ) ) {
00790                                         $stylePairs[$media] = $this->filter( 'minify-css', $style );
00791                                     }
00792                                 }
00793                             }
00794                             // Wrap styles into @media groups as needed and flatten into a numerical array
00795                             $styles = array(
00796                                 'css' => self::makeCombinedStyles( $stylePairs )
00797                             );
00798                         }
00799                     }
00800                 }
00801 
00802                 // Messages
00803                 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
00804 
00805                 // Append output
00806                 switch ( $context->getOnly() ) {
00807                     case 'scripts':
00808                         if ( is_string( $scripts ) ) {
00809                             // Load scripts raw...
00810                             $out .= $scripts;
00811                         } elseif ( is_array( $scripts ) ) {
00812                             // ...except when $scripts is an array of URLs
00813                             $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() );
00814                         }
00815                         break;
00816                     case 'styles':
00817                         // We no longer seperate into media, they are all combined now with
00818                         // custom media type groups into @media .. {} sections as part of the css string.
00819                         // Module returns either an empty array or a numerical array with css strings.
00820                         $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
00821                         break;
00822                     case 'messages':
00823                         $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
00824                         break;
00825                     default:
00826                         $out .= self::makeLoaderImplementScript(
00827                             $name,
00828                             $scripts,
00829                             $styles,
00830                             new XmlJsCode( $messagesBlob )
00831                         );
00832                         break;
00833                 }
00834             } catch ( Exception $e ) {
00835                 MWExceptionHandler::logException( $e );
00836                 wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" );
00837                 $this->hasErrors = true;
00838                 // Add exception to the output as a comment
00839                 $exceptions .= self::formatException( $e );
00840 
00841                 // Register module as missing
00842                 $missing[] = $name;
00843                 unset( $modules[$name] );
00844             }
00845             $isRaw |= $module->isRaw();
00846             wfProfileOut( __METHOD__ . '-' . $name );
00847         }
00848 
00849         // Update module states
00850         if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
00851             // Set the state of modules loaded as only scripts to ready
00852             if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
00853                 $out .= self::makeLoaderStateScript(
00854                     array_fill_keys( array_keys( $modules ), 'ready' ) );
00855             }
00856             // Set the state of modules which were requested but unavailable as missing
00857             if ( is_array( $missing ) && count( $missing ) ) {
00858                 $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
00859             }
00860         }
00861 
00862         if ( !$context->getDebug() ) {
00863             if ( $context->getOnly() === 'styles' ) {
00864                 $out = $this->filter( 'minify-css', $out );
00865             } else {
00866                 $out = $this->filter( 'minify-js', $out );
00867             }
00868         }
00869 
00870         wfProfileOut( __METHOD__ );
00871         return $exceptions . $out;
00872     }
00873 
00874     /* Static Methods */
00875 
00891     public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
00892         if ( is_string( $scripts ) ) {
00893             $scripts = new XmlJsCode( "function () {\n{$scripts}\n}" );
00894         } elseif ( !is_array( $scripts ) ) {
00895             throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
00896         }
00897         return Xml::encodeJsCall(
00898             'mw.loader.implement',
00899             array(
00900                 $name,
00901                 $scripts,
00902                 // Force objects. mw.loader.implement requires them to be javascript objects.
00903                 // Although these variables are associative arrays, which become javascript
00904                 // objects through json_encode. In many cases they will be empty arrays, and
00905                 // PHP/json_encode() consider empty arrays to be numerical arrays and
00906                 // output javascript "[]" instead of "{}". This fixes that.
00907                 (object)$styles,
00908                 (object)$messages
00909             ),
00910             ResourceLoader::inDebugMode()
00911         );
00912     }
00913 
00922     public static function makeMessageSetScript( $messages ) {
00923         return Xml::encodeJsCall( 'mw.messages.set', array( (object)$messages ) );
00924     }
00925 
00934     private static function makeCombinedStyles( array $stylePairs ) {
00935         $out = array();
00936         foreach ( $stylePairs as $media => $styles ) {
00937             // ResourceLoaderFileModule::getStyle can return the styles
00938             // as a string or an array of strings. This is to allow separation in
00939             // the front-end.
00940             $styles = (array)$styles;
00941             foreach ( $styles as $style ) {
00942                 $style = trim( $style );
00943                 // Don't output an empty "@media print { }" block (bug 40498)
00944                 if ( $style !== '' ) {
00945                     // Transform the media type based on request params and config
00946                     // The way that this relies on $wgRequest to propagate request params is slightly evil
00947                     $media = OutputPage::transformCssMedia( $media );
00948 
00949                     if ( $media === '' || $media == 'all' ) {
00950                         $out[] = $style;
00951                     } elseif ( is_string( $media ) ) {
00952                         $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
00953                     }
00954                     // else: skip
00955                 }
00956             }
00957         }
00958         return $out;
00959     }
00960 
00976     public static function makeLoaderStateScript( $name, $state = null ) {
00977         if ( is_array( $name ) ) {
00978             return Xml::encodeJsCall( 'mw.loader.state', array( $name ) );
00979         } else {
00980             return Xml::encodeJsCall( 'mw.loader.state', array( $name, $state ) );
00981         }
00982     }
00983 
00999     public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $source, $script ) {
01000         $script = str_replace( "\n", "\n\t", trim( $script ) );
01001         return Xml::encodeJsCall(
01002             "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
01003             array( $name, $version, $dependencies, $group, $source ) );
01004     }
01005 
01031     public static function makeLoaderRegisterScript( $name, $version = null,
01032         $dependencies = null, $group = null, $source = null
01033     ) {
01034         if ( is_array( $name ) ) {
01035             return Xml::encodeJsCall( 'mw.loader.register', array( $name ) );
01036         } else {
01037             $version = (int)$version > 1 ? (int)$version : 1;
01038             return Xml::encodeJsCall( 'mw.loader.register',
01039                 array( $name, $version, $dependencies, $group, $source ) );
01040         }
01041     }
01042 
01058     public static function makeLoaderSourcesScript( $id, $properties = null ) {
01059         if ( is_array( $id ) ) {
01060             return Xml::encodeJsCall( 'mw.loader.addSource', array( $id ) );
01061         } else {
01062             return Xml::encodeJsCall( 'mw.loader.addSource', array( $id, $properties ) );
01063         }
01064     }
01065 
01074     public static function makeLoaderConditionalScript( $script ) {
01075         return "if(window.mw){\n" . trim( $script ) . "\n}";
01076     }
01077 
01086     public static function makeConfigSetScript( array $configuration ) {
01087         return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), ResourceLoader::inDebugMode() );
01088     }
01089 
01098     public static function makePackedModulesString( $modules ) {
01099         $groups = array(); // array( prefix => array( suffixes ) )
01100         foreach ( $modules as $module ) {
01101             $pos = strrpos( $module, '.' );
01102             $prefix = $pos === false ? '' : substr( $module, 0, $pos );
01103             $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
01104             $groups[$prefix][] = $suffix;
01105         }
01106 
01107         $arr = array();
01108         foreach ( $groups as $prefix => $suffixes ) {
01109             $p = $prefix === '' ? '' : $prefix . '.';
01110             $arr[] = $p . implode( ',', $suffixes );
01111         }
01112         $str = implode( '|', $arr );
01113         return $str;
01114     }
01115 
01121     public static function inDebugMode() {
01122         global $wgRequest, $wgResourceLoaderDebug;
01123         static $retval = null;
01124         if ( !is_null( $retval ) ) {
01125             return $retval;
01126         }
01127         return $retval = $wgRequest->getFuzzyBool( 'debug',
01128             $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) );
01129     }
01130 
01145     public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01146             $printable = false, $handheld = false, $extraQuery = array() ) {
01147         global $wgLoadScript;
01148         $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug,
01149             $only, $printable, $handheld, $extraQuery
01150         );
01151 
01152         // Prevent the IE6 extension check from being triggered (bug 28840)
01153         // by appending a character that's invalid in Windows extensions ('*')
01154         return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE );
01155     }
01156 
01174     public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
01175             $printable = false, $handheld = false, $extraQuery = array() ) {
01176         $query = array(
01177             'modules' => self::makePackedModulesString( $modules ),
01178             'lang' => $lang,
01179             'skin' => $skin,
01180             'debug' => $debug ? 'true' : 'false',
01181         );
01182         if ( $user !== null ) {
01183             $query['user'] = $user;
01184         }
01185         if ( $version !== null ) {
01186             $query['version'] = $version;
01187         }
01188         if ( $only !== null ) {
01189             $query['only'] = $only;
01190         }
01191         if ( $printable ) {
01192             $query['printable'] = 1;
01193         }
01194         if ( $handheld ) {
01195             $query['handheld'] = 1;
01196         }
01197         $query += $extraQuery;
01198 
01199         // Make queries uniform in order
01200         ksort( $query );
01201         return $query;
01202     }
01203 
01213     public static function isValidModuleName( $moduleName ) {
01214         return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
01215     }
01216 
01223     public static function getLessCompiler() {
01224         global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths;
01225 
01226         // When called from the installer, it is possible that a required PHP extension
01227         // is missing (at least for now; see bug 47564). If this is the case, throw an
01228         // exception (caught by the installer) to prevent a fatal error later on.
01229         if ( !function_exists( 'ctype_digit' ) ) {
01230             throw new MWException( 'lessc requires the Ctype extension' );
01231         }
01232 
01233         $less = new lessc();
01234         $less->setPreserveComments( true );
01235         $less->setVariables( self::getLESSVars() );
01236         $less->setImportDir( $wgResourceLoaderLESSImportPaths );
01237         foreach ( $wgResourceLoaderLESSFunctions as $name => $func ) {
01238             $less->registerFunction( $name, $func );
01239         }
01240         return $less;
01241     }
01242 
01249     public static function getLESSVars() {
01250         global $wgResourceLoaderLESSVars;
01251 
01252         static $lessVars = null;
01253         if ( $lessVars === null ) {
01254             $lessVars = $wgResourceLoaderLESSVars;
01255             // Sort by key to ensure consistent hashing for cache lookups.
01256             ksort( $lessVars );
01257         }
01258         return $lessVars;
01259     }
01260 }